Skip to content

fix(supervise): a retained child's settlement names why it is pending, as a value - #1266

Merged
drewstone merged 1 commit into
mainfrom
fix/retained-pending-cause
Sep 16, 2026
Merged

drewstone merged 1 commit into
mainfrom
fix/retained-pending-cause

Conversation

@drewstone

Copy link
Copy Markdown
Contributor

Closes #1204.

The hole

retained provider execution requires reconciliation before replacement was the settle reason for two situations that call for opposite operator responses: an execution whose status genuinely cannot be determined (refuse to replace; reconcile first or pay twice), and a provider that broke its contract (nothing to reconcile; fix or report the provider). Six exhibits in three days wore the first name for the second fault. The runtime made it worse: it minted one RetainedRunProviderContractError both when the provider answered wrongly and when a read of the provider merely failed, so even exhibit 4 (the one genuine "unknown") looked like a contract violation from the inside.

The fix

  • RetainedExecutionPendingError.pendingCause: RetainedPendingCause'unobservable' (the safety refusal), 'provider-contract', 'request-rejected', 'transport', 'nested-recovery' — and the message names which one. Classified from the typed cause's structure (class name, code, HTTP status, Zod issues, AggregateError members), never message text: the provider is not a dependency of this package.
  • Phase-aware. A 4xx, a not-found, or a client deadline after admission means the provider cannot resolve what it admitted — exhibit 4's situation — and stays 'unobservable'. 'request-rejected' is named only at admission. The first draft of this got that inverted and the independent check caught it; the unit test now pins READ_FAILED → NotFoundError(404) as unobservable.
  • Wrappers vs verdicts. A contract error whose code says a READ failed is a wrapper and what it wraps decides. The event-stream catch is split (RETAINED_EVENT_STREAM_READ_FAILED for the iterator, RETAINED_EVENT_STREAM_INVALID for the runtime's own checks); the live stream's event-binding check mints RETAINED_EVENT_BINDING_INVALID, so an event bound to another run classifies as the contract violation it is (e2e test). Every driverExecutor nested-recovery throw states its cause instead of inheriting whatever the driver threw.
  • retainedPendingCause beside retainedExecution on Settled's down arm, the reconciled and terminal records (one shared field spread, so live and healed released records agree), NodeSnapshot, replay, both agent.child payloads, PursuitNodeProjection.
  • Not covered, stated: exhibits 3 (event arrived without a stable id) and 6 (value exceeds its JSON bound) are thrown by agent-provider-tangle as plain Errors with no code. A structure-only classifier cannot name them; they land on 'unobservable', the safe side, until the provider types them. The CHANGELOG says so; no test pretends otherwise.
  • Refusal behaviour unchanged: every cause still settles down, infra: true, retained-pending, and counts in fleetYield.releasedUnrecovered once released.

Process

Three independent checkers (exhibit truth, every-surface agreement, wrapper rule) on the first commit: sound-with-corrections, five confirmed defects, all fixed — including two invented error codes the first test suite certified but no code path produces, now deleted. Every shape in retained-pending-cause.test.ts is one the runtime or the Sandbox SDK actually throws.

Evidence

  • 441/441 across 21 files touching retained paths; 26 new assertions (unit) + 3 e2e cases (lost read → unobservable on every surface; SDK ServerError{502}transport; foreign-run event → provider-contract).
  • typecheck, biome, build, api-surface, docs:api (0 warnings), docs:freshness, check:version-bump (5 export changes → 0.235.0, release:prepare applied), check:testing-fixture, check:publish-workflow all exit 0. merge-tree clean against main.
  • discovery-lab's disco report reads the reason only as fixture text; nothing there changes.

🤖 Generated with Claude Code

…, as a value

One reason string covered a safety refusal and a provider contract violation,
which call for opposite operator responses (#1204: six exhibits in three days,
five of them contract violations wearing the refusal's name). The runtime made
it worse by minting one RetainedRunProviderContractError both when the provider
answered wrongly and when a read of the provider merely failed.

RetainedExecutionPendingError carries pendingCause — unobservable,
provider-contract, request-rejected, transport, nested-recovery — classified
from the typed cause's structure (class name, code, HTTP status, Zod issues,
AggregateError members), never its message text. Classification is
phase-aware: a 4xx, a not-found or a client deadline AFTER admission is the
provider failing to resolve what it admitted (unobservable), not a rejected
request. A read-failed contract error is a wrapper and what it wraps decides;
the event-stream catch splits READ_FAILED from INVALID so the runtime's own
checks keep their verdict; the live stream's event-binding check mints
RETAINED_EVENT_BINDING_INVALID; every driverExecutor nested-recovery throw
states its cause.

retainedPendingCause rides beside retainedExecution on Settled, the reconciled
and terminal records (one shared spread, so live and healed released records
agree), NodeSnapshot, replay, both agent.child payloads and the observer
projection. Exhibits 3 and 6 are plain provider Errors a structure-only
classifier cannot name; they stay unobservable and the CHANGELOG says so.
Refusal behaviour unchanged. 0.235.0: exported shapes moved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@drewstone
drewstone merged commit 9b74ebd into main Sep 16, 2026
4 checks passed
@drewstone

Copy link
Copy Markdown
Contributor Author

Cutting v0.235.0 at the merge commit now (tip of main, no Publish run in flight, latest tag v0.234.0).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

One reason string carries two incompatible meanings: a safety refusal and a provider contract violation

1 participant