Repository navigation
fix(a2a): detach sandbox runs from client streams - #33
Merged
Merged
Conversation
Keep both intents from #51 and this branch: - src/types.ts: retain SandboxPromptOptions and prepareBudgetedPrompt from main alongside dispatchPrompt, session controls, and the run control reference. - tests/a2a.test.ts: keep the durable-sandbox harness and main's authorizeConsumer. - tests/failed-execution-receipts.test.ts: the A2A failed-receipt cases now run through the durable test adapter because A2A execution fails closed without dispatchPrompt and session controls. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
tangletools
approved these changes
Sep 19, 2026
tangletools
left a comment
There was a problem hiding this comment.
Approved for merge: branch is mergeable against its base, PR is the named change, and its checks (where the repo runs them) are green. Verified live state before approving.
drewstone
added a commit
that referenced
this pull request
Sep 20, 2026
…box surface (#55) * fix(a2a): answer tasks/get without awaiting the detached run `session.result()` awaits a terminal result. Reconciling on every tasks/get with allowActive=true therefore turned a status read into a long poll that held the request open for the whole agent turn — the exact coupling the detached-execution fence removed from the stream path. Probe the run instead: the durable session contract now carries `runs()`, and reconciliation skips `result()` while the execution fence is live and the run is still active. An expired fence still awaits the result, because there is no owner left to settle the task. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(a2a): emit the resume cursor resubscribe already accepts `TaskIdParams.lastEventId` and `SandboxStreamEvent.id` landed together, but the resubscribe stream never wrote an SSE `id:` field, so a client had no way to learn the cursor it was asked to send back. The resume path was write-only and untested. Write `id:` on every artifact-update frame, skipping ids that carry a line break so a runtime value cannot forge a frame boundary. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(a2a): collapse what the fence PRs left duplicated - `dispatchDetachedSandboxStreamRich` restated every parameter of `dispatchSandboxStreamRich` only to forward them, and guarded at runtime against an options argument both call sites always pass. Forward the base function's own argument tuple with the sandbox factory swapped; the options are now the first, required argument. - `gatewayExecutionRecovery` was a dead constant in handler.ts plus two hardcoded literals. One exported constant, two users. - `exactReference` normalized the run control ref twice and carried a second failure branch that only changed an error message. - `withTaskExecution`, `clearTaskExecution` and `hasExpiredTaskExecution` each took a detour through a nested conditional, an IIFE, or a second marker read. - `hasDetachedSandbox` has no caller outside its module. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(a2a): share the fixture the two end-to-end suites copied a2a.test.ts and a2a-long-horizon.test.ts carried their own copies of the same agent, stub sandbox, demo key verifier, API key header, message builder and SSE parser, and the copies had already drifted — one `textMessage` took positional arguments, the other an options object. One harness, one shape. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(deps): upgrade first-party SDKs and pin the adopted sandbox surface @tangle-network/sandbox 0.37.0 -> 0.43.0, agent-app 0.47.6 -> 0.48.3, agent-interface 2.3.0 -> 2.10.0, agent-runtime 0.192.2 -> 0.240.0. The SDK was a devDependency nothing imported, so the detached-execution surface adopted in #33 was never checked against it. src/sandbox-sdk- contract.ts asserts what the gateway reads out of the SDK — the dispatch result, the session result, the run listing, the interrupt outcome — so an SDK that reshapes any of it fails `pnpm typecheck`. That check immediately caught a live defect: `SandboxPromptResult` declared `question?: string`, but the runtime returns `{ questionId, questions }`. A detached run that finished awaiting a question therefore put that object straight into the A2A status message as a text part. Read a prompt out of the payload instead. agent-app 0.48.3 declares peer ranges for sandbox 0.40.x, agent-runtime 0.231.x and agent-eval 0.182.x, so pnpm reports those peers as unmet at the newest versions. Typecheck, the full suite and the build are green on the newest set, and agent-app is a devDependency used by one composition test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(a2a): probe the run before settling a lapsed execution lease A lapsed lease says the execution's owner is gone, not that the sandbox stopped. The expired-lease recovery path still reached the blocking `session.result()`, so a `tasks/get` against an orphaned-but-live run hung for the rest of that run — the same failure the status-read guard had just closed, one branch over. Probe the run state independently of lease freshness. Reconciliation now returns early whenever the sandbox still owns the run, and expired-lease recovery leaves such a task `working` instead of failing it, so a live run is neither waited on nor abandoned; a later poll settles it from its result. Reported by Codex review on #55. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(a2a): read only known terminal statuses as a finished run `SandboxDurableSession.runs()` accepts any status string, but the probe recognized only the literal `active`. A host sandbox reporting `running` or `queued` — both already treated as non-terminal elsewhere in reconciliation — would be read as finished, and `tasks/get` would fall into the blocking `session.result()`. Invert the test: `completed`, `failed`, `cancelled` and `canceled` are terminal, case-insensitively, and every other status means the sandbox still owns the run. An unrecognized status now costs one deferred poll instead of a hung request. Reported by Codex review on #55. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
dispatchPrompt+ exact session execution controls for A2A task runs.tasks/resubscribereplays the exact run andtasks/getreconciles its result before lease failure.tasks/cancel, and fail closed when production adapters lack durable controls.This follows the Sandbox SDK README “Resume a session” contract and its
session.events,session.result, andsession.interruptprimitives.Proof
pnpm typecheckpnpm test -- --reporter=dot— 24 files, 392 tests passed