✨ Make a fresh <Session> a pending placement (#648) - #656
Conversation
A `<Session>` used to establish. It took ownership, published `acp-first`,
built a runtime, created a backend session and asserted an identity for it —
all before anything had asked for a conversation. That is why a
`<Session.Launch>` nested inside a `<Session>` could not work: the enclosing
element had already settled how the session was constructed, so the launch
inside it was refused for naming a session that had just been created out from
under it. It is also why a run could retain a conversation nothing ever had:
`ensureSession()` returning is not a backend accepting a turn.
A fresh `<Session>` now places. It validates the agent and where the session
will live, pins the exact `Session` value, and does nothing else — no route, no
runtime, no ensure, no record, no turn. The first consuming operation chooses
the immutable construction route: a first subscribed `<Prompt>` publishes
`acp-first` and constructs through ACP, and a first `<Session.Launch>`
constructs client-native. An established placement keeps its eager validation
exactly as before.
An ACP-first session becomes a conversation when the backend accepts its first
turn, and only the adapter can say when that happened. Both embedded adapters
now report it on one exact versioned key,
`executablemd.session-materialization/v1`, as `{"state":"accepted"}` in the
update metadata of a standard ACP `session_info_update` — Codex when
`turn/start` returns a turn, Claude when the SDK reports that exact queued
command dispatched. Nothing else promotes a session: not a returning ensure,
a first text delta, a synthesized `started`, a terminal result, a checkpoint
token, an error code or a diagnostic.
The vendored ACPX runtime gains a third behavioral patch to carry it. An ensure
asking for deferred materialization persists a provisional record — the key is
occupied, the serialized `agentSessionId` is absent — and holds the identity
`session/new` returned privately, so the record is occupancy rather than an
assertion. The acceptance marker is consumed rather than published: it reaches
no event stream, no conversation and no checkpoint. On acceptance the runtime
promotes a copy of the record, saves it through the store's own atomic
operation, adopts it in memory only after that save returns, and then resolves
`AcpRuntimeTurn.materialized`. Promotion is serialized against live-checkpoint
writes, so a promotion that failed is not later overwritten by finalization as
though it had succeeded.
The durable order is therefore route, provisional non-asserting record, backend
acceptance, asserting record, host mapping, and only then the events the turn
produced. A first turn nobody accepted leaves the route standing, the record
non-asserting, no workflow row, no identity on the value the document holds, and
that same pending value ready to retry — which creates fresh backend state
rather than resuming the arrangement no backend took. Acceptance, not successful
terminal text, is the boundary: a turn that failed after being accepted leaves
the session established.
The pre-commit window is unchanged and now sits one step later: the provider's
assertion is saved before the host is asked to retain anything, so an
interruption between them leaves exactly one canonical assertion for the next
attachment to reconcile and commit.
Provider provenance is now the exact object rather than the key inside it. A
structural copy, a value from another provider copy or a torn-down scope, and a
value used with a different resolved agent are each refused before any provider
work — and the value a `<Session>` pinned is the one every nested `<Prompt>`
receives.
A first prompt enters its session's FIFO before any route, ensure or runtime for
that placement, and re-reads the placement once the queue grants, so two
concurrent first prompts perform one backend creation and the waiter continues
what its predecessor established.
Both adapter snapshots are rebuilt from new commits on their existing upstream
pull request branches, and the test agent reports acceptance too — an agent that
never sent the marker would leave every session it serves unestablished.
| harness.handleIds.push(handleId); | ||
| // Deferred materialization: the record occupies the key and asserts | ||
| // nothing, and the identity the adapter answered with is held here | ||
| // until the backend accepts a turn. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // until the backend accepts a turn. |
| const materialized = promiseWithResolvers<AcpRuntimeMaterialization>(); | ||
| // Observed here as the provider's own deferreds are: a test that only | ||
| // reads events must not turn an unaccepted turn into an unhandled | ||
| // rejection. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // rejection. |
| }; | ||
| if (!turn.sessionMaterializationPending) { | ||
| // Already asserting. This says the session no longer awaits first-turn | ||
| // materialization, not that the backend accepted this turn. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // materialization, not that the backend accepted this turn. |
| applyConversation(asserting, turn.conversation); | ||
| try { | ||
| // Serialized against live-checkpoint writes, so no interval flush can | ||
| // put the pending record back over this materialization. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // put the pending record back over this materialization. |
| this.activeControllers.delete(turn.record.acpxRecordId); | ||
| this.closingActiveRecords.delete(turn.record.acpxRecordId); | ||
| } | ||
| // Whatever else this turn settled as, no marker means no materialization. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // Whatever else this turn settled as, no marker means no materialization. |
| rejectMaterialized = reject; | ||
| }); | ||
| // Observed here as the provider's own deferreds are, so a turn nobody | ||
| // waited on cannot become an unhandled rejection. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // waited on cannot become an unhandled rejection. |
PR #656: ✨ Make a fresh a pending placement (#648)34 files, +7130 / -5048 Scope🔴 PR has 12178 lines changed. Split into focused PRs. 🟡 12178 lines changed. PRs under 400 receive more thorough review. 🟡 34 files changed. Are all changes related? 🟡 Changes span 10 directories. StructuralOxlint structural signals:
Slop
Static AnalysisOxlint: 37 diagnostics across 5 files (10 rules) consistent-function-scoping (10): packages/acp/src/provider.ts CorrectnessNo extraneous code patterns detected. |
Two settled contracts the previous commit broke. **Concurrent prompts on one advertised session.** Ownership was acquired before the session's queue, and the coordinator refuses contention rather than queueing it. That is right for a native UI holding the session for hours and wrong for the next turn of a conversation this provider is already having: a second subscription got `AgentSessionBusy` instead of running as the second turn. The queue comes first now, and each prompt takes and gives back ownership in its turn. What still refuses is what genuinely competes. Another provider state, another process and another kind of operation share no queue with this one, so they all still meet the coordinator. A launch no longer takes the queue at all: a launch is only ever performed for an advertised agent, so its acquisition is already that session's exclusion, and holding the queue as well is what would have put a prompt behind a native UI — the thing NL12 exists to prevent. It refuses instead, and the coordinator is what refuses it. `NO12` is the regression: an advertised adapter and a real coordinator, the first turn held open while the second subscribes, proving two sequential grants both granted, one materializing ensure, two turns in submission order, and one retained identity in the provider's own store. It fails on the previous ordering. **Codex commands that start a turn.** The marker was published where a prompt reaches the App Server through `turn/start` and nowhere else, so `/review`, `/goal <objective>` and `/goal resume` accepted turns and reported nothing. All three funnel through the adapter's command turn-started callback, which now reports it and awaits publication; a command that starts no turn never reaches it and still reports none. The upstream commit is `fadc0a690e96c276629be8a34be980d35e821637` on the same pull-request branch, with its own regressions, and the tarball, generated snapshot, manifest and provenance are refreshed from it. `EA8` proves it against the real embedded adapter: the fake App Server answers `review/start` on a review thread of its own, and the adapter reports exactly one acceptance for the session the command was sent on. It fails on the previous tarball. Neither fix adds an inference anywhere. Acceptance is still one exact marker from the adapter, and the provider still waits for it.
| const materialized = promiseWithResolvers<AcpRuntimeMaterialization>(); | ||
| // Observed here as the provider's own deferreds are: a test that only | ||
| // reads events must not turn an unaccepted turn into an unhandled | ||
| // rejection. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // rejection. |
| rejectMaterialized = reject; | ||
| }); | ||
| // Observed here as the provider's own deferreds are, so a turn nobody | ||
| // waited on cannot become an unhandled rejection. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // waited on cannot become an unhandled rejection. |
| }; | ||
| if (!turn.sessionMaterializationPending) { | ||
| // Already asserting. This says the session no longer awaits first-turn | ||
| // materialization, not that the backend accepted this turn. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // materialization, not that the backend accepted this turn. |
| this.activeControllers.delete(turn.record.acpxRecordId); | ||
| this.closingActiveRecords.delete(turn.record.acpxRecordId); | ||
| } | ||
| // Whatever else this turn settled as, no marker means no materialization. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // Whatever else this turn settled as, no marker means no materialization. |
| // made it. Keeping it would let a retry continue the zero-turn session | ||
| // this attempt left behind instead of creating one — and the record it | ||
| // stands on is still marked pending, so ACPX would not have resumed it | ||
| // either. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // either. |
| // anyone may prompt through. The provider's assertion stands, and the | ||
| // next attachment reconciles and commits that same identity — which it | ||
| // can only do by reattaching, so the placement gives up its handle here | ||
| // rather than staying live with one nobody may use. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // rather than staying live with one nobody may use. |
| const materialized = promiseWithResolvers<AcpRuntimeMaterialization>(); | ||
| // Observed here as the provider's own deferreds are: a test that only | ||
| // reads events must not turn an unaccepted turn into an unhandled | ||
| // rejection. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // rejection. |
| rejectMaterialized = reject; | ||
| }); | ||
| // Observed here as the provider's own deferreds are, so a turn nobody | ||
| // waited on cannot become an unhandled rejection. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // waited on cannot become an unhandled rejection. |
| }; | ||
| if (!turn.sessionMaterializationPending) { | ||
| // Already asserting. This says the session no longer awaits first-turn | ||
| // materialization, not that the backend accepted this turn. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // materialization, not that the backend accepted this turn. |
| this.activeControllers.delete(turn.record.acpxRecordId); | ||
| this.closingActiveRecords.delete(turn.record.acpxRecordId); | ||
| } | ||
| // Whatever else this turn settled as, no marker means no materialization. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // Whatever else this turn settled as, no marker means no materialization. |
Two of NO12's claims were timed rather than observed. It polled the fake until a turn appeared, and then waited a fixed five milliseconds before asserting that the second subscription had not run — which is a claim about what has *not* happened, and a duration can only ever make that likely. Three signals replace them. The fake runtime says when a turn has actually started, which is what "the first turn is in flight" means. The second task says when it is about to subscribe, immediately after constructing the stream, which chose nothing and started nothing. And the provider's own agent resolution says when that subscription is running — the point after which the only thing ahead of it is the session's queue. Only then does it assert the held state, and it asserts one more thing than before: one ensure. A second subscription past the queue would have ensured, so counting them is what makes "waiting" different from "not started yet". `startedTurns(count)` on the fake runtime is the first of those signals, built the way the workflow fake's already is. Nothing else changes; the released sequence is asserted exactly as it was.
| // Identity, never the key: this provider issued exactly one value for this | ||
| // placement and kept it. A structural copy, a value from another provider | ||
| // copy or a torn-down scope, and a look-alike built around a key somebody | ||
| // read are none of them that value. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // read are none of them that value. |
| // made it. Keeping it would let a retry continue the zero-turn session | ||
| // this attempt left behind instead of creating one — and the record it | ||
| // stands on is still marked pending, so ACPX would not have resumed it | ||
| // either. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // either. |
| // | ||
| // The global route slot is deliberately not held across the wait: | ||
| // waiting for one session's turn is not a reason to stop every other | ||
| // session resolving. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // session resolving. |
| // Inside ownership, before the runtime exists and before a turn: a | ||
| // first Prompt constructs this session through ACP, so that is what | ||
| // its construction route says — and a session a native process | ||
| // constructed is attached to under the identity it already has. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // constructed is attached to under the identity it already has. |
| if (clientNative.failure === undefined) { | ||
| // A client-native route now exists for this placement, so a <Session> | ||
| // nested after this launch attaches to it eagerly instead of taking it | ||
| // for one nothing has constructed. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // for one nothing has constructed. |
| const awaiting = withheld.has(recordId); | ||
| // A native promise rather than an Effection future: this is the acpx | ||
| // boundary, and a turn nobody accepts leaves it unsettled — which as a | ||
| // root task would be a pending operation the runner refuses to end on. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // root task would be a pending operation the runner refuses to end on. |
| const materialized = promiseWithResolvers<AcpRuntimeMaterialization>(); | ||
| // Observed here as the provider's own deferreds are: a test that only | ||
| // reads events must not turn an unaccepted turn into an unhandled | ||
| // rejection. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // rejection. |
| }; | ||
| if (!turn.sessionMaterializationPending) { | ||
| // Already asserting. This says the session no longer awaits first-turn | ||
| // materialization, not that the backend accepted this turn. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // materialization, not that the backend accepted this turn. |
| this.activeControllers.delete(turn.record.acpxRecordId); | ||
| this.closingActiveRecords.delete(turn.record.acpxRecordId); | ||
| } | ||
| // Whatever else this turn settled as, no marker means no materialization. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // Whatever else this turn settled as, no marker means no materialization. |
| rejectMaterialized = reject; | ||
| }); | ||
| // Observed here as the provider's own deferreds are, so a turn nobody | ||
| // waited on cannot become an unhandled rejection. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // waited on cannot become an unhandled rejection. |
The "this subscription is running" barrier was the provider's agent resolution, which is several suspensions short of the queue: between it and `turns.slot` lies the whole outer placement. So the held-state assertions were made while the second subscription might still have been resolving where its session lives, and what they observed was as much "not there yet" as "waiting". The signal is the completion of that placement now, which is the step immediately before the provider enters the queue. It comes from `withSessionRoute` — the provider's own dependency, supplied the way a host supplies it — rather than from anything added to production for a test to hold on to. `withSessionRoute` wraps two things per subscription, and only placements *complete* while the first turn is held: the first subscription's second call encloses its whole turn, which is still open. So the second completion is the second subscription's placement, and the test asserts that identification rather than leaving it to be inferred. The wrapper suspends once before it signals, so nothing here rests on placement being synchronous — a scheduler that hands control back at a yield has already done so by the time the waiting task is released, and the signalling task runs on into the queue before that task resumes. Restoring ownership-before-queue fails this in both shapes, with the forced suspension and without it, and fails it on the claim that matters: the second subscription comes back `AgentSessionBusy` instead of `undefined`.
| // Identity, never the key: this provider issued exactly one value for this | ||
| // placement and kept it. A structural copy, a value from another provider | ||
| // copy or a torn-down scope, and a look-alike built around a key somebody | ||
| // read are none of them that value. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // read are none of them that value. |
| // made it. Keeping it would let a retry continue the zero-turn session | ||
| // this attempt left behind instead of creating one — and the record it | ||
| // stands on is still marked pending, so ACPX would not have resumed it | ||
| // either. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // either. |
| yield* releaseHandle(entry.session.sessionKey); | ||
| // The turn's own outcome is the useful thing to say when it has one: the | ||
| // barrier only knows that this session is still awaiting its first | ||
| // accepted turn. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // accepted turn. |
| // anyone may prompt through. The provider's assertion stands, and the | ||
| // next attachment reconciles and commits that same identity — which it | ||
| // can only do by reattaching, so the placement gives up its handle here | ||
| // rather than staying live with one nobody may use. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // rather than staying live with one nobody may use. |
|
|
||
| const prepared = yield* withSessionRoute(context, () => | ||
| // Where this prompt lands. Resolving it constructs nothing, which is | ||
| // what lets the queue below be entered before any provider effect. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // what lets the queue below be entered before any provider effect. |
| resolveAgentSession(read.value, policy, yield* assertions(placementKey), identity); | ||
| // rather than from the fact that a key is occupied. A pending ACPX record | ||
| // occupies its key and asserts nothing, so this answers `create` for it — | ||
| // which is what makes the placement pending rather than established. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // which is what makes the placement pending rather than established. |
| const recordKey = input.handle.acpxRecordId ?? input.handle.sessionKey; | ||
| // A native promise rather than an Effection future: this is the acpx | ||
| // boundary, and a turn nobody accepts leaves it unsettled — which as a | ||
| // root task would be a pending operation the runner refuses to end on. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // root task would be a pending operation the runner refuses to end on. |
| rejectMaterialized = reject; | ||
| }); | ||
| // Observed here as the provider's own deferreds are, so a turn nobody | ||
| // waited on cannot become an unhandled rejection. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // waited on cannot become an unhandled rejection. |
| rejectMaterialized(new Error("this session still awaits first-turn materialization")); | ||
| } else { | ||
| // Acceptance is not the turn's output: a backend takes the turn | ||
| // before it says anything, so a manual turn reports it too. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // before it says anything, so a manual turn reports it too. |
| // This agent takes the turn the moment it is asked, so acceptance is | ||
| // reported here — before the turn runs and before anything it | ||
| // produces. A client that defers a session's durable state until a | ||
| // conversation really exists waits for exactly this and nothing else. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // conversation really exists waits for exactly this and nothing else. |
Closes #648.
Why
A
<Session>used to establish. It took ownership, publishedacp-first, builta runtime, created a backend session and asserted an identity for it — all
before anything had asked for a conversation.
Two things follow from that. A
<Session.Launch>nested inside a<Session>could not work: the enclosing element had already settled how the session was
constructed, so the launch inside it was refused for naming a session that had
just been created out from under it. And a run could retain a conversation
nothing ever had, because
ensureSession()returning is not a backend acceptinga turn — a first turn that failed left a workflow mapped to a session no
provider ever made.
What changes
Before:
<Session>established.<Session.Launch>inside one refused. Aworkflow row committed as soon as the provider answered.
After:
<Session>places. It validates the agent and where the session willlive, pins the exact
Sessionvalue, and does nothing else — no route, noruntime, no ensure, no record, no turn. The first consuming operation chooses
the immutable construction route: a first subscribed
<Prompt>publishesacp-firstand constructs through ACP, and a first<Session.Launch>constructs client-native. An established placement keeps its eager validation
exactly as before.
An ACP-first session becomes a conversation when the backend accepts its first
turn, and only the adapter can say when that happened. Both embedded adapters
now report it on one exact versioned key,
executablemd.session-materialization/v1, as{"state":"accepted"}in theupdate metadata of a standard ACP
session_info_update. Nothing else promotes asession: not a returning ensure, a first text delta, a synthesized
started, aterminal result, a checkpoint token, an error code or a diagnostic.
How it works
The durable order for the ACP-first branch:
The vendored ACPX runtime carries a third behavioral patch for it. An ensure
asking for
materialization: "first-turn-acceptance"persists a provisionalrecord — the key is occupied, the serialized
agentSessionIdis absent — andholds the identity
session/newreturned privately, so the record is occupancyrather than an assertion. The acceptance marker is consumed rather than
published: it reaches no event stream, no conversation and no checkpoint token.
On acceptance the runtime promotes a copy of the record, saves it through the
store's own atomic operation, adopts it in memory only after that save returns,
and then resolves
AcpRuntimeTurn.materialized. Promotion is serialized againstlive-checkpoint writes, so a promotion that failed is not later overwritten by
finalization as though it had succeeded.
Review guide
Start with:
specs/acp-client-spec.md§Sessions and §Materialization.Then review:
architecture.md— the three lifecycle terms, the workflow session orderinginvariant, and the two construct-inventory rows.
packages/acp/src/provider.ts—placementState,placePending,requeried,materialize, and the reorderedpromptStream.packages/acp/vendor/acpx/PROVENANCE.mdand the four patched generatedfiles — the persistence ordering is the sensitive part.
packages/cli/src/workflow-agent.ts,packages/acp/src/session-key.ts,packages/test-agent/src/worker/acp-server.ts.Look carefully at:
runtime.js(promoteSessionMaterialization) and itsinteraction with
finalizeRuntimeTurn, which awaits a promotion still inflight before saving.
materialize()inprovider.ts: on refusal it releases the handle andpublishes nothing; on a host retention failure it abandons the handle and
detaches the placement so the next attachment reconciles the one assertion
that exists.
What must stay true
resolveSessionreturningbefore
owning()for a pending placement; checked bySM1,SM2,AF25,WAP8.sessionMaterializationAccepted()in the vendored runtime, which reads oneversioned key and nothing else; checked by
TM4against a real ACP child thatemits ordinary updates, agent text and a completed terminal result and never
the marker.
saving the promoted record through the store before
materializedresolves,and by calling
sessions.established()only after that; checked byTM2,SM4,SM9.Sessionvalue is provenance, not a key — enforced by an object-identitytest in
prepare(); checked bySM2,SM8,AF24.CN6,CN7,CN8,RR1,RR2,NL15.How to verify it
packages/acp/tests/acpx-materialization.test.ts(Tier TM) drives the realvendored runtime against a real ACP child process.
TM4proves no fallbackpromotes a record;
TM2proves the asserting save precedes the answer;TM6proves a pending record is never resumed. It fails if any inference is added.
packages/acp/tests/adapter-protocol.test.tsEA6/EA7spawn the realembedded Codex and Claude adapters and prove each emits one session-scoped
marker per accepted turn, before the prompt response, with interleaved
sessions not crossing. It fails if either adapter reports session-globally.
packages/acp/tests/provider.test.tsTier SM covers the lifecycle at theprovider seam, including concurrent first prompts (
SM7) and the pre-commitwindow (
SM9).packages/test-agent/src/NativeSessionLaunch.test.mdauthors the compositionin Markdown:
deno task xmd test packages/test-agent/src/NativeSessionLaunch.test.md.Scope
Included
Intentionally unchanged
cancellation, teardown, stale-value refusal and the pre-commit recovery window
all keep their existing meaning.
-32600recovery, Codex App Server persistence, cross-process ownershipoutside native handoff — all out of scope, as Materialize fresh ACP sessions at the first consuming operation #648 states.
New abstractions
AcpRuntimeMaterializationandAcpRuntimeTurn.materializedexist because aconsumer deferring durable state needs one thing to wait on; consumed by
provider.tsand both test fakes.AcpxSessionPlacement.stateexists because the host, not the provider, knowswhether a workflow session was ever established; produced by
workflow-agent.tsand directory placement, consumed byresolveSessionandpromptStream.New dependencies
None. Dependencies and the lockfile are unchanged.
Generated or mechanical changes
packages/acp/vendor/adapters/generated/snapshots.tscomes fromdeno run --allow-all scripts/build-adapter-snapshots.ts..tgzfiles come from each upstream project's ownnpm run build && npm packat the commits recorded inMANIFEST.json.packages/acp/vendor/acpx/MANIFEST.jsondigests are recomputed from thepatched files;
PROVENANCE.mddescribes the patch by hand.Upstream adapter commits
Both are new commits on the existing upstream pull-request branches, and each
project's full suite is green on the pushed head.
taras/codex-acpfeat/prompt-response-turn-id(agentclientprotocol/codex-acp#438)377d2b0btaras/claude-agent-acpfeat/prompt-response-assistant-uuid(agentclientprotocol/claude-agent-acp#1047)beba04dcRisks and limitations
session_info_updateon every accepted turn. Bothupstream suites compare complete lists of session updates, so 48 codex and 8
claude cases had to be narrowed to the updates they are actually about. The
shared codex fixtures keep the marker out of the compared list and expose it
through a dedicated accessor; emission is proved separately, upstream and here.
packages/test-agentnow reports acceptance. An ACP agent this repositoryowns that never sent the marker would leave every session it serves
unestablished.
deno task test --changed, the fullbattery,
check:jsr,verifyandverify:cleanhave not been run againstthis commit; the focused battery,
checkandlintare green and are recordedin the branch's
issue-648-implementor-feedback.md.two ACPX patches and is removed on its own when upstream carries it (Materialize fresh ACP sessions at the first consuming operation #648 owns
that gate, beside Remove the vendored ACPX transient-environment patch after a released scoped seam passes continuity #566 and Remove the vendored ACPX checkpoint-metadata patch after a released runtime carries response _meta #629).
Scope confirmation