Skip to content

feat: support standard ACP subagent sessions - #655

Closed
benbrandt wants to merge 7 commits into
mainfrom
feat/standard-subagent-associations
Closed

benbrandt wants to merge 7 commits into
mainfrom
feat/standard-subagent-associations

Conversation

@benbrandt

@benbrandt benbrandt commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Incremental extraction — completed; reference only

This PR was split into independently reviewable and shippable changes; all eight planned extractions are now merged. Keep it as the reference implementation; do not merge the full diff wholesale. The extraction PRs target main directly, not this branch or each other.

  1. Merged: ACP SDK patch update, chore(deps): update ACP SDK to 2.0.2 #676 — two dependency files, no adapter code.
  2. Merged: owned Vitest discovery, test: restrict Vitest discovery to owned suites #677 — one 11-line configuration, independently validated with main's SDK 2.0.0.
  3. Merged: mock Responses server improvements, test: support concurrent mock Responses requests #680 — test-only request routing, async responses, safe failures, and direct standalone coverage.
  4. Merged: behavior-preserving item-rendering state extraction, refactor: extract streamed item rendering state #681 — two source files; the same handler owns the same state, with unchanged outputs and no new subagent behavior.
  5. Merged: live standard-subagent core, feat: add live standard ACP subagent sessions #682 — identity, ancestry, mutation restrictions, interactions, child-owned rendering, and lifecycle/reconnect safety together. Association-only replay is explicit; recursive transcripts and historical outcomes remain deferred. Native model reporting from feat: report the model of each native subagent session #678 is preserved.
  6. Merged: recorded child transcript replay and reconciliation, feat: replay recorded subagent conversations #684 — both spawn formats, legacy/paginated stores, recursive ancestry, best-effort gaps, and state reconciliation without duplicate presentation.
  7. Merged: read-only child configuration snapshots, refactor: report read-only child configuration snapshots #685 — complete observed model/effort singleton snapshots, historical/current separation, verified post-response refresh, and nonblocking reads with stale-result fencing. Existing model reporting and AIR behavior are preserved; no controls are enabled.
  8. Merged: historical child outcomes, refactor: replay historical child outcomes #686 — latest recorded turn completion/cancellation/failure before the response, independent of current foreground state, interactions, ownership, and controls; legacy and paginated stores covered. This is the final planned extraction, not permission to merge the old combined diff.

Completion audit — main at 9091b73

All eight planned slices above are on trunk, including #685 and #686. Compared the old reference tree directly against current main rather than treating the stale three-dot PR diff as unlanded work. Most remaining differences are superseded implementation, differently organized/expanded tests, and intervening trunk changes. Do not copy back the old non-awaiting transition helper, weaker replay reconciliation, pre-#678 model reporting, older dependency/release files, or old tests wholesale.

Merged audit follow-up: #687. A coordinator already known through a pathless spawn did not learn its available recorded path before nested child replay. Four new v1/v2 warm/cold regressions fail without the fix; two more preserve an already observed path against older history. The fix is two files, +39/-1 lines, with no identity, parent, title, ownership, capability, or AIR changes. Current validation: typecheck/build, 2,070 unit tests, 15 real-app-server integration tests, and all six bundles. The follow-up merged to main as 6cce035. All planned extractions and this audit fix are merged; the old combined diff is superseded and must not be merged wholesale. The original branch remains available for reference.

Separate follow-up candidates, not bundled into #687: the reference uses iterative drains where two standard-router pending-buffer drains still use argument spreading (a byte cap is not an argument-count cap), and contains some additional focused assertion coverage for existing foreground/writer behavior. These are hardening/test follow-ups, not missing planned feature scopes. Child controls and directed-message extensions remain deliberately outside this extraction.

Main also includes native child model reporting from #678. Subsequent runtime extractions must preserve/reuse that implementation rather than overwrite it with the older reference code.

Tests and snapshots travel with the behavior they verify. New feature pieces must be validated separately; this combined PR's green CI does not establish that an arbitrary subset is standalone.

Known gap to close before the configuration piece lands

A focused runtime probe confirmed that enqueueSessionTransition completes before an async callback settles. Configuration publication passes such a callback. Fixed in the #682 live-core extraction: async completion and rejection propagation now participate in the notification lane, with a permanent ordering/rejection regression. The old reference branch itself still contains the earlier helper; do not copy it back over the extracted implementation. Neither #676 nor #677 includes this helper or depends on the subagent implementation.


Summary

Implement the first coherent slice of the unstable standard ACP subagent contract, independently of AIR's existing private contract.

  • Use standard associations for opted-in non-AIR v1 clients and non-AIR v2 clients. Preserve AIR's updates, generation-based identities, and opt-out fallback without emitting both contracts.
  • Keep one opaque child session identity across delegations, idle work, and parent replay. Announce immediate parents before children, buffer unresolved ancestry instead of flattening it, and namespace copied fork histories separately.
  • Report child work independently of conversation lifetime and the parent's foreground prompt. Observed endings become idle; lost observability becomes unknown, not a fabricated terminal failure. V2 also gets child-scoped native state updates.
  • Route permissions and elicitation under the child ID. Keep request scopes alive through parent replay, withdraw them at actual work boundaries (even under output backpressure), and reject late replies locally even if a client ignores cancellation.
  • Advertise empty child capabilities and reject unadvertised session mutations, including private extensions and raw-provider-ID bypasses. Direct child cancellation is ignored; parent cancellation cascades to observed live descendants.
  • Replay each child conversation once, deduplicate overlapping completed items, and publish fresh work snapshots after the replay response. Serialize standard same-parent attachment operations, then release fresh child traffic after the latest setup response is written, including batched responses. Keep close/cancel independent and fence queued setups after close.
  • Keep child titles, plans, and unfinished tools separate from the parent. Child Markdown plans use authoritative completed snapshots. Agent-owned child input is not misreported as ordinary user input.

Review corrections

  • Child approval, elicitation, and accepted-URL completion state now belong to the child conversation, not the parent's current prompt or resume handler.
  • Replay release observes successful SDK wire writes rather than the next event-loop turn. Delayed/failed batch members cannot cause pre-response fresh snapshots, and two same-parent resumes in one batch do not deadlock.
  • Existing nested-child metadata uses the same ancestor-announcement gate as child events. The regression fails on both protocol versions without the gate.
  • The root Vitest configuration is JavaScript ESM, fixing the application TypeScript root-directory failure while retaining owned-test discovery.

Second review: lifecycle corrections

  • Restored child delivery validates current provider parentage through the ancestor chain. Loaded owned runtimes are rejoined without execution-context overrides. Owned inactive children get routes without being loaded; copied foreign fork history cannot steal the original runtime's events or authorize interrupts.
  • Child item-rendering state now survives parent handler changes, including terminals, streamed-output bookkeeping, message phases, and item presentation tracking.
  • Standard child admission precedes tool/terminal cleanup as well as normal rendering. Setup transitions share the session notification lane; replay snapshots share live state-publication ordering.
  • Replayed tool snapshots reconcile persistent tracking before duplicate live completions are suppressed. Outstanding-tool cleanup is turn-scoped so an old completion cannot fail newer work.
  • Both recorded spawn formats use the same child-history traversal.
  • Live child requests retain foreground/action tracking during replay, including a new native turn arriving while presentation remains buffered.

26 permanent lifecycle regressions cover these paths on both protocol versions, plus real-Codex terminal-lifetime tests. Existing AIR golden transcripts are unchanged. The terminal fixture uses full-access mode only for its fixed local script in an isolated workspace, avoiding CI hosts that cannot create sandbox network namespaces; production sandbox policy is unchanged.

CodeQL correction

Unexpected mock response-handler exceptions are logged server-side and return only a fixed HTTP 500 message. Regression tests cover synchronous exceptions, rejected stack-trace strings, and recovery on the next request. No production adapter behavior is changed.

Latest specification / SDK alignment

  • Upgrade the published ACP SDK dependency to ~2.0.2 (lockfile resolves 2.0.2).
  • Report observed child models and reasoning effort as complete, read-only singleton configuration snapshots, never by copying parent values or offering parent choices. Child configuration changes do not mutate the parent or grant session/set_config_option.
  • Optional configuration reads run outside the notification lane; publication rejoins the existing ordered setup/notification lane. Pending or late reads cannot stall child output, replace newer observations, or bypass replay boundaries. Refreshed current configuration follows the replay response, and unchanged settings are re-established on reattachment.
  • Replay a known final child turn outcome as historical idle before the response, with its actual success/cancellation/failure reason. Do not promote it into the foreground-state cache, runtime ownership, or mutation authorization. Missing or still-running history invents no outcome. Both legacy and paginated histories are covered; unavailable turn summaries do not discard available transcripts.
  • Preserve the actual batch-write freshness boundary and empty child capability allowlist. Claude is an implementation reference, not a replacement for Codex provider ownership checks or the spec.

Adds 22 reporting regressions across v1/v2, plus real-app-server assertions for initial/read-only child configuration and refresh on parent replay. The replayed-idle wording is currently on the upstream rfd-replayed-subagent-outcomes branch, not spec main; this historical-only reporting is compatible with both.

Deliberate limits / next package

  • No child mutation controls are advertised yet.
  • Dedicated session_message / directed-message reporting remains follow-up work; ordinary runtime operation reports remain available.
  • Child history and missing ancestry remain best-effort. Copied history alone never authorizes interrupting the original runtime.
  • The full live-model E2E suite was not run, and cross-platform bundles were compiled, not executed on every target OS.
  • npm audit reports 5 findings (3 high, 2 moderate) in unchanged dependencies. Unrelated dependency remediation is not included here.

Validation

Local validation below is for merge commit 05d625c, which brings Codex main at a77829f into the feature branch without rewriting history. Dependency conflicts retain ACP SDK 2.0.2 alongside Codex 0.164.0 and the main lockfile's Vitest 5.0.5. Compatibility fixes only add the new nullable activity fields to typed feature-test fixtures; no additional production subagent behavior was changed. The PR itself has not been merged into main. CI passed for this merge commit, including typecheck, unit and integration tests, and binary bundling; both CodeQL workflows also passed.

  • npm run typecheck
  • npm test: 1,980 passed, 39 skipped
  • npm run build
  • npm run test:integration: 15 passed, including actual Codex 0.164.0 active-child replay, fork identity isolation, cancellation, close, and child terminal completion across independent parent prompts through both ACP versions with a local mock model; also verifies child configuration snapshots and replay refresh
  • npm run bundle:all: all six targets compile
  • npm run codex-test: live-model no-tool smoke test completed with end_turn
  • git diff --check

Includes 97 focused selection/wire/lifecycle tests (94 run; 3 v2-batch cases skipped on v1), four response-delivery tests, and a foreground-state regression, with golden transcripts for both protocol versions. Existing AIR snapshots are unchanged.

The parallel validation run also reproduced a test-discovery bug: Codex downloaded plugins containing tests into an integration fixture under tmp/. Vitest now discovers only the repository-owned src/__tests__/ and scripts/ suites, using its default filename patterns, instead of collecting generated third-party fixtures.

References checked

Comment thread src/__tests__/integration/mock-responses-server.ts Fixed
benbrandt added a commit that referenced this pull request Oct 26, 2026
## Summary

First independent extraction from #655. This PR targets `main` directly
and does not depend on the standard-subagent implementation or any other
extraction.

- Raise the minimum ACP SDK patch from `~2.0.0` to `~2.0.2`.
- Update only the SDK's lockfile version, URL, and integrity. No other
dependencies or adapter source files change.
- SDK 2.0.1/2.0.2 include HTTP stream-conflict isolation, CR-only SSE
line-ending support, and request-scoped POST failure handling. This
adapter's existing stdio behavior is checked against the new package; no
HTTP-specific adapter changes or new subagent behavior are included.

## Validation

Run on this branch based directly on `main` at `a77829f`, without #655's
test configuration or runtime changes:

- `npm ci --ignore-scripts`: SDK resolves to 2.0.2; Codex remains
0.164.0 and Vitest remains 5.0.5.
- `npm run typecheck`
- `npm test`: **1,879 passed**, 36 skipped
- `npm run test:integration`: **7 passed**, using the real Codex
app-server with a local mock model
- `npm run build`
- `npm run bundle:all`: all six targets compile
- `git diff --check`

The full live-model E2E suite, direct HTTP transport tests, and
execution of every cross-platform bundle were not run. The existing five
npm audit findings are outside this dependency-only extraction.

## Split plan

This can be reviewed and merged independently. #655 stays open as the
reference implementation; subsequent changes will be extracted into
separate, independently validated PRs rather than landing that
monolithic diff.
benbrandt added a commit that referenced this pull request Oct 26, 2026
## Summary

Second independent extraction from #655. **One file, 11 additions.**
Based directly on `main`; it does not depend on the SDK update in #676
or on any subagent code.

Codex integration fixtures can download plugins containing their own
tests under `tmp/`. Vitest's default repository-wide discovery can
collect those foreign tests. Scope discovery to the repository-owned
`src/__tests__/` and `scripts/` directories instead.

- Reuse `configDefaults.include`, preserving Vitest's supported
test/spec filename patterns rather than narrowing to `.test.ts`.
- Keep the release-tooling tests under `scripts/`.
- Use an ESM `.mjs` configuration, which does not introduce a root-level
TypeScript file outside the application's `rootDir: src`.
- Leave test implementations, skip behavior, command-line exclusions,
dependencies, and production code unchanged.

## Validation

Validated independently on `main` at `a77829f`, using its SDK **2.0.0**,
Codex **0.164.0**, and Vitest **5.0.5**:

- Controlled before/after discovery check using `vitest list --filesOnly
--json`:
- Default discovery included a temporary
`tmp/vitest-discovery-probe.spec.ts`.
- The new configuration excluded only that foreign file; every other
file was preserved.
- An owned `.spec.mjs` probe and `scripts/next-preview-version.test.mjs`
remained discoverable.
- Temporary probes were removed before running the suites and are not
part of this PR.
- `npm run typecheck`
- `npm test`: **1,879 passed**, 36 skipped, matching the unmodified
discovery run on the SDK extraction
- `npm run test:integration`: **7 passed**, with the real Codex
app-server and a local mock model
- `npm run build`
- `git diff --check`

No snapshots changed. The full live-model E2E suite and local
cross-platform bundling were not run for this config-only change.
benbrandt added a commit that referenced this pull request Oct 26, 2026
## Summary

Third standalone extraction from #655, after #676 and #677 landed. Based
directly on current `main` (`12e2d16`), including #678; no subagent
runtime or production files change.

- Add an optional request-body response selector so concurrent root,
child, and title requests can receive the correct scripted responses
without depending on arrival order.
- Support synchronous handlers and handlers that await a response.
Preserve FIFO behavior when no selector is installed; explicit HTTP
errors still take precedence, and an undefined selection is an
unexpected request rather than a queue fallback.
- Return a fixed generic HTTP 500 on handler failure and keep
diagnostics in server-side logs. Do not write replies to disconnected
clients.
- Generalize function-call SSE replies to named/namespaced tools. The
existing shell helper delegates without changing its payload.

## Standalone coverage

Seven direct tests cover FIFO compatibility, concurrent out-of-order
completion, error/path precedence, unhandled requests,
synchronous/asynchronous failure and recovery, and named/namespaced
function serialization. Snapshots contain stable payloads rather than
changing response IDs.

The original #655 only tested handler failures directly; this extraction
adds coverage for the fixture's useful behavior without needing the
later subagent feature.

## Validation

- `npm ci --ignore-scripts`
- `npm run typecheck`
- Focused mock-server/model-request suite: **10 passed**
- `npm test`: **1,915 passed**, 36 skipped
- `npm run test:integration`: **7 passed**, using the real Codex 0.164.0
app-server and local mock model, including existing shell-tool flows
- `npm run build`
- `git diff --check`

Only test infrastructure is changed. The full live-model E2E suite and
local cross-platform bundling were not run for this test-only
extraction. Existing dependency audit findings are outside this scope.
benbrandt added a commit that referenced this pull request Oct 26, 2026
## Summary

Fourth standalone extraction from #655, after #676, #677, and #680
landed. **Two source files, +34/-27 lines.** Based directly on current
`main` (`1685cde`), preserving native child model reporting from #678.

Group the existing command/fuzzy-search/guardian reporters, image
bookkeeping, reasoning deduplication, and message-phase map into
`CodexItemStreamState`.

- Each event handler still allocates exactly one private instance, with
the same seven field types and initializers.
- Every existing access goes through that instance. No state sharing,
injection, lifetime extension, routing changes, or new standard-subagent
code is introduced.
- Plans, errors, tool-call tracking, session state, and subagent
ownership are untouched.

This makes the existing rendering-state boundary explicit and usable
today without landing the later lifecycle changes. It is not the change
that makes child rendering survive parent handler replacement; that
belongs in the complete live-subagent core.

## Behavior-preservation evidence

A source comparison against `main` verified that the constructor and all
method bodies are byte-identical after removing the added
`this.itemStreams.` prefix, and that all seven initializers match their
original declarations. The three reporter classes have no constructor
side effects.

No tests or snapshots were added, updated, or removed. Existing
event-driven suites provide the output-regression coverage, including
AIR and v1/v2 paths.

## Validation

- `npm ci --ignore-scripts`
- `npm run typecheck`
- `npm run build`
- `npm test`: **1,915 passed**, 36 skipped
- `npm run test:integration`: **7 passed**, with the real Codex 0.164.0
app-server and a local mock model
- `npm run bundle:all`: all six targets compile
- Live-model no-tool smoke: ACP text chunks formed
`item-state-refactor-ok`
- `git diff --check`

The full live-model E2E suite and execution of every cross-platform
binary were not run. Existing dependency audit findings are outside this
refactor.
benbrandt added a commit that referenced this pull request Oct 26, 2026
## Scope: standalone live core

Fifth incremental extraction from #655, on current `main` (`091f9b2`),
after #676, #677, #680, and #681 landed. This is the larger atomic
feature slice discussed in the split plan: **43 files, +2,898/-181**,
including 1,573 added test/snapshot lines and 1,301 added runtime lines.
No dependency or generated app-server files change.

### Included

- Standard associations for opted-in non-AIR v1 clients and non-AIR v2
clients. AIR retains its existing opt-ins, private updates, generation
identities, and fallback.
- One opaque conversation ID across assignments, idle work, and handler
replacement. Announce immediate parents before children; buffer
unresolved ancestry rather than flattening it.
- Child-owned streamed-item/tool state, plans, titles, permissions, and
elicitation. Accepted URL completion state survives parent attachment
changes; late approval replies are rejected locally when actual work
ends.
- Empty child capabilities and an allowlist boundary for all client
mutations, including private extensions and raw provider IDs. Known raw
IDs are restricted even while announcement publication is backpressured.
Parent cancellation cascades to observed live children; an actual ending
produces idle, observability loss produces unknown.
- Association-only replay and safe reattachment: current provider
parentage must match through known ancestors, inactive owned children
receive future events without loading them, and copied forks cannot
steal the original runtime. Parent history still replays normally; an
explicit advisory says recorded child transcripts are not included.
- Current child snapshots follow the real successful wire write,
including complete JSON-RPC batches. Same-parent setup execution is
serialized without waiting on each other's shared batch delivery.
Closing a parent fences queued reopen attempts.
- Preserve the model reporting already landed in #678: reuse its source
priority and read-only model option helper. Model observations are not
guessed from spawn arguments or parent values; buffered older settings
cannot replace newer observations.

### Explicitly deferred

No recursive child transcript replay, history/live item reconciliation,
recorded historical work outcomes, new configuration readback/effort
reporting, directed-session messages, or child mutation controls. Those
remain separate changes after this live core is reviewed and landed.

The current RFD explicitly allows live support without complete child
history. Recoverable associations are replayed truthfully; omitted
transcripts are not silently presented as complete.

## Review order

1. Contract selection, connection-owned identities/restrictions, and ACP
mutation dispatch.
2. Live router admission, child-owned rendering/work/request state, and
existing model reporting.
3. Provider subscriptions and ownership verification; no parent
cwd/MCP/instruction overrides when rejoining a child.
4. Conversation-owned interaction handlers and request cancellation
scopes.
5. Session setup lane, batch-write freshness boundary, and
association-only replay.
6. Behavior-focused tests and wire snapshots for both versions.

The async queue-completion gap found while planning the split is fixed
here: a session transition waits for async callbacks and propagates
rejection without breaking the notification lane. A regression
reproduces both ordering and rejection behavior. The old #655 reference
helper must not be copied back over this implementation.

## Validation

Final validation used main's latest lockfile (including #679):

- `npm run typecheck`
- `npm run build`
- `npm test`: **1,959 passed**, 38 skipped
- Focused live/wire/queue suites: **44 passed**, 2 v2-only cases skipped
on v1
- `npm run test:integration`: **15 passed** — eight new real-Codex
0.164.0 cases cover active-child reattachment, fork identity/ownership
isolation, parent cancel/close, and child terminal output/exit across
later parent prompts, on v1 and v2, using a local scripted model
- `npm run bundle:all`: all six targets compile
- Real live-model no-tool smoke completed with `end_turn`
- `git diff --check`

Existing AIR and parent-session golden files are unchanged. The
fixed-command terminal fixture alone uses full-access mode to avoid CI
hosts without sandbox namespace support; production sandbox settings are
unchanged. The full live-model E2E suite and execution of every OS
bundle were not run.

## References

- ACP specification at `8aa92106feba5cdd0da210a87a39dfee4dc24f13`:
[identity, announcement, and automatic
routing](https://github.com/agentclientprotocol/agent-client-protocol/blob/8aa92106feba5cdd0da210a87a39dfee4dc24f13/docs/rfds/subagents.mdx#L166-L246),
[best-effort replay and response
freshness](https://github.com/agentclientprotocol/agent-client-protocol/blob/8aa92106feba5cdd0da210a87a39dfee4dc24f13/docs/rfds/subagents.mdx#L713-L840).
- Official TypeScript SDK checkout
`c7e07388feed7a530f8211d0dae4951cb6d26d09` and published 2.0.2 for
transport/runtime evidence; normative behavior comes from the spec, not
the SDK implementation.
benbrandt added a commit that referenced this pull request Oct 26, 2026
## Scope

Sixth standalone extraction from #655, directly on current `main`
(`fe3ae92`) after the live core #682 landed. **16 files, +864/-50**,
mostly direct tests and snapshots. No negotiation, capability,
dependency, or generated app-server changes.

Enhances only the existing standard-subagent path: non-AIR v1 still
requires client opt-in; non-AIR v2 uses it by default. AIR retains its
separate replay and identity rules.

## Changes

- Follow both recorded `subAgentActivity` and collab spawn formats,
replaying each conversation once, recursively under its immediate
parent. Resolve recoverable reversed ancestry, bound cycles, and never
guess or change parentage.
- Read legacy or paginated child items lazily. Current-provider
ownership verification and live subscription restoration happen before
item reads. A retained transcript remains displayable without granting
ownership or current work state.
- Send all selected history before the load/resume response, preserving
the live core's actual batch-write freshness boundary.
- Reconcile tool/rendering bookkeeping before suppressing duplicate
presentation. Replayed completed tools cannot be failed by later cleanup
or resurrected by stale running snapshots. Live completion bookkeeping
still runs, and completed runnable items do not leave a live permission
incorrectly reported as running.
- Restore in-progress command classification without resetting
streamed-byte tracking. Duplicate live starts do not reset the reporter,
and completion does not resend already streamed output.
- Report known gaps once at the parent while continuing available
sibling replay. Reject mismatched thread identities. Empty history alone
invents no gap, ending, failure, or runtime.

### Conservative limits

Agent-owned input is not reinterpreted as human input. Incomplete text
snapshots have no safe delta cursor and are omitted rather than
duplicated over newer buffered deltas; unfinished command replay carries
metadata rather than previously streamed bytes. Only renderers with a
safe partial form are used for unfinished items. Open-ended provider
status strings (such as image generation status) are not treated as
completion enums: those items use recorded completion metadata instead,
with legacy and paginated regressions. These omissions produce an
advisory. Complete retained content is replayed normally.

Historical work-state outcomes, directed-session messages, new child
controls, and broader configuration reporting remain deferred. Existing
provider-parentage checks still prevent copied fork history from
stealing the original runtime.

## Coverage

**36 new v1/v2 cases** cover both spawn formats, one visit per
conversation, nested/reversed ancestry, paginated item ordering,
missing/mismatched snapshots, completed-tool reconciliation,
running-command restoration, incomplete-text overlap, stale running
history, live permissions, cyclic records, close-during-read, and
no-replay behavior. Wire assertions verify recorded content precedes the
response; no recorded permission or elicitation request is reissued.

Real-Codex integration cases now reload finished children and assert
that their retained answers replay under the same child IDs, on v1 and
v2. The existing fork/cancel/close/terminal-lifetime checks remain in
place.

## Validation

- `npm run typecheck`
- `npm run build`
- `npm test`: **1,995 passed**, 38 skipped
- Focused live/replay suites: **77 passed**, 2 v2-only cases skipped on
v1
- `npm run test:integration`: **15 passed**, with the real Codex 0.164.0
app-server and a local scripted model
- `npm run bundle:all`: all six targets compile
- Live-model no-tool smoke completed with `end_turn`
- `git diff --check`

Existing golden files are unchanged. One live-core assertion changes
intentionally: an empty retained transcript no longer produces the
blanket “child transcripts are not included” advisory. New replay cases
verify advisories for actual failures and incomplete/omitted content.

The full live-model E2E suite and execution of every OS bundle were not
run. Native integration uses the default provider history store;
explicit paginated-branch coverage uses SDK-level fixtures.

## Contract references

ACP specification `53620d0b325d6815b3cff1ca7e909351d488fdf0`:
[best-effort replay, per-session identity/order, gaps, and
freshness](https://github.com/agentclientprotocol/agent-client-protocol/blob/53620d0b325d6815b3cff1ca7e909351d488fdf0/docs/rfds/subagents.mdx#L713-L836).
Official TypeScript SDK `c7e07388feed7a530f8211d0dae4951cb6d26d09` /
published 2.0.2 is runtime evidence, not the normative contract.
benbrandt added a commit that referenced this pull request Oct 26, 2026
## Summary

Step 7 of the incremental extraction tracked in #655, directly against
current `main` after #684. This is read-only configuration reporting,
not historical work outcomes or child controls.

- Extend standard child configuration from model-only reports to
complete reported model/reasoning-effort snapshots. Every select offers
only its observed current value; neither parent values nor requested
spawn arguments supply child values.
- Preserve the existing model-source precedence and model labels. Newer
observations remove unset/unavailable effort, including empty snapshots
when no fields remain reportable. A model change cannot carry the
previous model's effort.
- Keep recorded configuration historical. Reattachment drops cached
confirmation; fresh observations and verified, loaded provider metadata
establish current configuration after the actual response write,
including whole JSON-RPC batches.
- Run optional metadata reads outside the notification lane and rejoin
that lane only to publish. Newer observations, unload/close, or another
attachment invalidate late results. Reads verify the physical child ID
and immediate parent without loading or changing the child.
- Ignore foreign runtime starts for history-only children; copied
associations do not establish current configuration or ownership.

V1 standard opt-in, v2 defaults, empty child capabilities, mutation
restrictions, and AIR's existing contract remain unchanged. AIR's only
source change is exporting the existing source-priority constant for
reuse. Dependencies and generated app-server types are untouched.
Existing AIR and parent-session snapshots have no changes.

## Review size

9 files, +653/-36 lines. About four fifths of the additions are
tests/snapshots. Runtime changes stay within the standard router, its
server wiring, and a small shared snapshot builder used by live and
historical reporting.

## Validation

- `npm run typecheck`
- `npm test`: **2,028 passed**, 39 skipped. Includes **33 new v1/v2 wire
regressions**, with one v2-only batch case skipped on v1.
- `npm run build`
- `npm run test:integration`: **15 passed** against real Codex 0.164.0
and a local mock Responses API. Native child tests now compare reported
effort with the child's actual Responses request, both initially and
after parent reattachment.
- `npm run bundle:all`: all six platform targets compiled.
- `npm run codex-test -- -p 'Reply with exactly child-configuration-ok.
Do not read files or use tools.' -c <isolated-scratch-directory> -o
summary`: live-model smoke completed with `end_turn`.
- `git diff --check`

Wire tests cover complete singleton snapshots,
effort-only/unknown/cleared values, model changes, source precedence and
pre-announcement buffering, no-op mutation rejection, changed and
unchanged reattachment snapshots, full and no-replay attachment, delayed
batches, pending reads without blocked output, newer native settings,
late reads across attachments/unload/close, untrusted metadata, and
historical-only configuration.

The full live-model E2E suite was not run. Cross-platform bundles were
compiled, not executed on each target OS.

## Protocol / reference check

- Specification `main` at `53620d0b325d6815b3cff1ca7e909351d488fdf0`:
[RFD child
configuration](https://github.com/agentclientprotocol/agent-client-protocol/blob/53620d0b325d6815b3cff1ca7e909351d488fdf0/docs/rfds/subagents.mdx#L252-L285)
supplies the proposed child reporting, singleton-choice, and
reattachment semantics. Existing draft configuration notifications
require complete state in
[v1](https://github.com/agentclientprotocol/agent-client-protocol/blob/53620d0b325d6815b3cff1ca7e909351d488fdf0/docs/protocol/v1/draft/session-config-options.mdx#L337-L368)
and
[v2](https://github.com/agentclientprotocol/agent-client-protocol/blob/53620d0b325d6815b3cff1ca7e909351d488fdf0/docs/protocol/v2/draft/session-config-options.mdx#L333-L382).
- TypeScript SDK `main` at `c7e07388feed7a530f8211d0dae4951cb6d26d09`,
with the installed SDK **2.0.2** used for real wire decoding/schema
checks. SDK runtime evidence is separate from the specification; the
existing actual-write response boundary and ordered session-transition
helper are reused, not replaced.
- Codex's generated `Thread` metadata documents model/effort as current
configured values when loaded, or persisted values otherwise. Readback
accepts only loaded, correctly parented children; persisted metadata
alone does not confirm current configuration.

Historical work outcomes remain the next extraction. This PR does not
merge or replace #655 wholesale.
benbrandt added a commit that referenced this pull request Oct 26, 2026
## Summary

Final planned extraction (step 8) from #655, directly against current
`main` after #685.

- Replay the **latest recorded child turn's** normal completion,
cancellation, or failure as historical `idle` before the load/resume
response, using the same stop-reason/error mapping as live turn endings.
- Emit no outcome for missing or still-running turn metadata. Do not
substitute an older completed turn for a latest running turn, or infer
endings from activity-item kind, missing content, or runtime loss.
- Reuse the legacy transcript's turn payload without a second full read.
Paginated history adds one optional latest-turn summary read (`limit:
1`, descending, no items); an unavailable summary retains available
transcript content and uses the existing history-gap advisory.
- Publish historical state directly, without changing the live
foreground cache, ending current interactions, establishing runtime
ownership, or enabling mutations. Parent closure fences late summaries.

V1 opt-in gating, v2 defaults, AIR behavior, current configuration
reporting, child identities, ancestry, and empty capabilities remain
unchanged. Dependencies and generated app-server types are untouched.
Existing AIR and parent-session golden transcripts are unchanged. Two
existing v2 child-replay snapshots gain the intended historical
`state_update`.

## Review size

15 files, +422/-22 lines. Most additions are tests/snapshots; runtime
changes touch only the history reader, standard replay dispatcher, and
standard child router. The router shares one explicit terminal-status
mapping between live and historical reporting, but historical
publication bypasses the live state cache.

## Validation

- `npm run typecheck`
- `npm test`: **2,064 passed**, 39 skipped, including **36 new v1/v2
outcome regressions**.
- `npm run build`
- `npm run test:integration`: **15 passed** with real Codex 0.164.0 and
a local mock Responses API. The v1/v2 completion cases now close the
parent and cold-load it before checking the recorded ending, avoiding a
cached live ending as a false positive.
- `npm run bundle:all`: all six targets compiled.
- `npm run codex-test -- -p 'Reply with exactly historical-outcomes-ok.
Do not read files or use tools.' -c <isolated-scratch-directory> -o
summary`: live-model smoke completed with `end_turn`.
- `git diff --check`

Wire regressions cover both history stores,
completion/cancellation/failure, unavailable or running final turns,
response ordering, no mutation/interrupt authority, independent
paginated summary failures, latest-turn selection, a subsequent
non-replaying resume, newer live permissions and actual work completion,
nested ancestry and repeated spawn records, late summaries after close,
and failures without details.

The full live-model E2E suite was not run. Cross-platform bundles were
compiled, not executed on each target OS.

## Specification / reference check

- Current specification `main`,
`5a5ab96ea8e0f6d3d5c5d7de3a7d29a87c79ddd2`: the [subagent RFD replay
rules and freshness
boundary](https://github.com/agentclientprotocol/agent-client-protocol/blob/5a5ab96ea8e0f6d3d5c5d7de3a7d29a87c79ddd2/docs/rfds/subagents.mdx#L724-L805)
distinguish historical pre-response state from confirmed current
activity and controls, and prohibit inventing outcomes from missing
history.
- The [explicit replayed-idle
clarification](https://github.com/agentclientprotocol/agent-client-protocol/blob/e839564daab1da2c68cb8a006c786dfcbf0e68f1/docs/rfds/subagents.mdx#L763-L809)
is still on the pending `rfd-replayed-subagent-outcomes` branch, **not
specification main**. It permits keeping a recorded idle outcome visible
without treating it as current activity or authorization. This
implementation is compatible with both versions and requires no new wire
fields.
- TypeScript SDK `main`, `c7e07388feed7a530f8211d0dae4951cb6d26d09`,
checked separately as reference/type evidence. Installed SDK **2.0.2**
performs real wire decoding in the regression tests; this change reuses
the existing v1/v2 state publication and actual response-write freshness
boundary.

This finishes the eight planned extraction scopes once merged; it does
not merge the old combined #655 wholesale. Child mutation controls and
directed-message extensions remain outside this work.
benbrandt added a commit that referenced this pull request Oct 26, 2026
## Summary

Small replay omission found while auditing the completed #655 extraction
after #686 merged. All eight planned slices are already on `main`; this
is a narrowly scoped correctness follow-up, not another feature tranche.

A legacy `spawnAgent` record can establish a coordinator's identity and
parent without an agent path. Later replay may supply that coordinator's
path and newly recorded nested child activity. Previously
`announceHistory` returned immediately for the known coordinator,
discarding the available path. The nested activity then could not
resolve its immediate parent, so valid descendant history was omitted
with a gap advisory.

Fill a known child's **missing** normalized path from recorded activity
before descending into its history. Do not replace an already observed
path with older history. Identities, established parent links, titles,
runtime ownership, capabilities, AIR behavior, and v1/v2 negotiation
remain unchanged.

**2 files, +39/-1 lines. No snapshot changes.**

## Regression evidence

- Four v1/v2 warm/cold replay cases failed on current `main` before the
fix: a live or restored pathless coordinator prevented newly recovered
descendant text from replaying.
- Those four cases now replay the text under the correct immediate
parent without a false gap.
- Two additional v1/v2 cases preserve an already observed parent path
when an older spawn record disagrees.

## Validation

- `npm run typecheck`
- `npm test`: **2,070 passed**, 39 skipped
- `npm run build`
- `npm run test:integration`: **15 passed** against real Codex 0.164.0
with a local mock Responses API
- `npm run bundle:all`: all six targets compiled
- `git diff --check`

The full live-model E2E suite and an additional live-model smoke test
were not run for this fix. Cross-platform bundles were compiled, not
executed on each target OS.

## Audit context

The remaining old #655 diff largely reflects replaced implementations,
differently organized tests/snapshots, and intervening trunk work such
as native model reporting. In particular its older queue-completion
helper and model implementation must not replace the corrected trunk
versions. The combined PR should not be merged wholesale.

Checked current specification revision
`c0897908cb2305e89203218c348d69ab4acf9d61`. This fix retains the [RFD's
truthful identity, parentage, and parent-before-child replay
ordering](https://github.com/agentclientprotocol/agent-client-protocol/blob/c0897908cb2305e89203218c348d69ab4acf9d61/docs/rfds/subagents.mdx#L724-L755);
it adds no wire fields or mutation authority.
@benbrandt

Copy link
Copy Markdown
Member Author

Superseded by the independently validated and merged incremental PRs: #676, #677, #680, #681, #682, #684, #685, and #686, plus the completion-audit fix #687 (main at 6cce035). Closing without merging the old combined diff. The reference branch is intentionally retained. The tracker records the remaining optional hardening/test follow-ups separately; child controls and directed-message extensions remain outside this work.

benbrandt added a commit that referenced this pull request Oct 26, 2026
## Summary

Expose Codex's existing child-turn interruption through standard ACP
`session/cancel`, without opening other child mutation controls.

- Advertise `capabilities.cancel: {}` only for owned, observable
standard children whose current turn can be identified. Revoke with
`cancel: null` when the work ends or becomes unobservable. Idle or
history-only children get no grant.
- Accept the opaque ACP child ID, never a raw provider-ID bypass.
Interrupt that child's current turn and observed owned descendant work,
not its parent or siblings. A later assignment retains the same child
identity and can receive a fresh grant.
- Withdraw affected permission/elicitation requests locally and reject
accepted late replies. Deduplicate in-flight interrupts and permit retry
after provider failure.
- Use receipt-time completion/observability facts and the latest
provider turn ID even while rendering is backpressured. A successful
interrupt acknowledgement does not fabricate a cancelled outcome; actual
provider completion supplies it.
- Suspend existing grants at parent setup admission. Only the latest
successful, non-cancelled setup response can restore them, after its
actual wire write (including the full JSON-RPC batch). Failed attempts
cannot release current child controls.

Standard v1 opt-in, v2 defaults, and AIR's existing contract are
unchanged. Prompt/configuration/load/close/private-extension mutations
remain forbidden for standard children. Copied history cannot acquire
the original runtime's controls. Dependencies and generated app-server
types are unchanged.

## Review scope

19 files, +602/-20 lines, mostly tests/snapshots. Runtime work is
concentrated in the standard child router, the cancel dispatch, and the
successful setup-response boundary. Eight existing standard-child
snapshots gain the intended cancel grant/revocation; AIR and ordinary
parent-session snapshots are unchanged.

## Validation

- `npm run typecheck`
- `npm test`: **2,095 passed**, 40 skipped; final run did not update
snapshots. Includes **25 new v1/v2 wire regressions** (one v2-only batch
case skipped on v1).
- `npm run build`
- `npm run test:integration`: **17 passed** against real Codex 0.164.0
with a local mock Responses API. New v1/v2 cases receive a current
cancel grant after reattachment, send `session/cancel` to the child,
observe Codex's actual cancelled ending, and continue prompting the
parent independently.
- `npm run bundle:all`: all six platform targets compiled.
- `npm run codex-test -- -p 'Reply with exactly child-cancel-ok. Do not
read files or use tools.' -c <isolated-scratch-directory> -o summary`:
live-model smoke completed with `end_turn`.
- `git diff --check`

Wire coverage includes sibling/ancestor isolation, nested descendant
cascade, real-ending confirmation and later work, request withdrawal and
late replies, in-flight deduplication, raw/foreign/stale IDs, unload
under backpressure, newest provider turn targeting, provider-error
retry, loaded foreign fork history, delayed replay responses, failures
before/after provider setup, and whole-batch freshness.

The full live-model E2E suite was not run. Cross-platform binaries were
compiled, not executed on every target OS.

## Protocol / provider evidence

- ACP specification revision `36b3fcc14d1834d6cc2a5ea3f317a49f198c2f79`:
the [subagent RFD cancellation and ownership
rules](https://github.com/agentclientprotocol/agent-client-protocol/blob/36b3fcc14d1834d6cc2a5ea3f317a49f198c2f79/docs/rfds/subagents.mdx#L859-L930)
define the object-valued cancel capability, child-directed cancellation,
descendant policy, actual completion evidence, and pending requests. The
[replay freshness
boundary](https://github.com/agentclientprotocol/agent-client-protocol/blob/36b3fcc14d1834d6cc2a5ea3f317a49f198c2f79/docs/rfds/subagents.mdx#L763-L805)
forbids historical grants and invalidates failed attempts.
- SDK reference revision `c7e07388feed7a530f8211d0dae4951cb6d26d09`,
separately checked for capability/notification shapes. Installed SDK
**2.0.2** decodes the real v1/v2 test wire. No protocol/schema changes
or new RPC methods are needed.
- Codex's existing `turn/interrupt` method is exercised directly by the
real-app-server child cancellation cases, not inferred from schema
alone.

This follows the completed native-subagent extraction; it does not
reopen or merge the superseded combined #655.
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.

2 participants