# PR #23586 proof audit, pass 1 (head 9d6926bdb5, base 8d3b061121)

Abbreviations: RT = packages/provider-acp/src/server/AcpSessionRuntime.ts, AD = packages/provider-acp/src/server/adapter.ts,
PSM = apps/server/src/orchestration-v2/ProviderSessionManager.ts, JT = apps/server/src/provider/acp/AcpJsonRpcConnection.test.ts,
AT = apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts, MOCK = apps/server/scripts/acp-mock-agent.ts.

Tests: `vp test run apps/server/src/provider/acp/AcpJsonRpcConnection.test.ts apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts`
from /home/ubuntu/wt/pr-23586. Ran once: 2 files, 95 tests passed, 74 s, exit 0 (log: /tmp/ev-23586/audit1-tests.log).

## Matrix

| Row | Implementation (head) | Test | Transcript | Verdict |
|---|---|---|---|---|
| R1 final response before idle | RT:2109-2113 race tags `provisional:false`; RT:2133-2137 interrupts responseFiber; RT:2164 lateSessionLoadRef not set, so followLateSessionLoad no-ops at RT:1984 | Implicit only: JT:1170 "ignores session/update replay notifications during session/load" (EMIT_LOAD_REPLAY, normal load). Nothing asserts that no SessionSetupAdopted is emitted | yes ("both [final response before idle]") | COVERED (transcript + implicit test) |
| R2 late response after idle, adopted + SessionSetupAdopted | RT:2104-2108 forked responseFiber; RT:2164-2168 store late; RT:2442 (start path) / RT:2583 (loadSession path) followLateSessionLoad; RT:1991-2031 adopt + emit | JT:1041 "adopts config options from a session/load response that arrives after replay idle" (options become [mode, model]). The SessionSetupAdopted event is used as a sync point in AT:2261/AT:2357, so it's exercised but not asserted on its own | yes (after row: options appear at 6630ms) | COVERED |
| R3 late response, selection sent with reply pending: dropped | RT:1386-1393 runLoggedSessionWrite bumps the counter before sending; RT:2004 counter check | JT:1104 "drops a late session/load response when a selection was sent before it" (HOLD knob; checks sessionSetupResult identity and model=gpt-5.4) | no | COVERED |
| R4 late response, selection applied: dropped | RT:2002 configOptionsRef identity (updateConfigOptions RT:1908) + RT:2004 counter | JT:1072 "keeps a selection made before a late session/load response arrives". This only checks currentValue. It still proves the drop: an adopted response would carry the stale `default` value, so the test would fail | no | COVERED |
| R4b setMode (session/set_mode) / setModel (session/set_model) writes | RT:2809 / RT:2834 are counted. setProvider RT:2699 is not counted (probably fine) | none; only set_config_option is tested | no | PARTIAL |
| R5 agent-side config/mode notification after idle: dropped | RT:2002-2003 identity of configOptionsRef/modeStateRef (processSessionUpdate, RT:2891 etc.) | none | no | UNCOVERED |
| R6 superseded by another load | RT:2093 cancelLateSessionLoad inside runLoadSessionWithReplayIdle; RT:1995 fiber identity; RT:1984 provisional identity | none | no | UNCOVERED |
| R7 superseded by resume | RT:2587 cancelLateSessionLoad; RT:1997-1998 startState.result identity | none | no | UNCOVERED |
| R8 superseded by fork | RT:2604 cancelLateSessionLoad | none | no | UNCOVERED |
| R9 superseded by shutdown/close | fiber forked in runtimeScope (RT:2108, RT:2065); RT:1814 stoppingRef finalizer; RT:1999 stopping check; RT:2059-2063 interrupt-only means no failed log | none | no | UNCOVERED |
| R10 late error response: logged failed, no crash | RT:2033-2066 Fiber.join failure leads to onError logRequest failed, then Effect.ignore | none (JT:1013 covers only the non-late failure) | no | UNCOVERED |
| R11 late timeout (sessionLoadTimeout) | RT:2108 timeoutOption on responseFiber; RT:2035-2043 None becomes AcpTransportError "session/load timed out after replay idle", then logged failed | none | no | UNCOVERED |
| R12 supportsModelSwitchInSession false to true, published as provider_session.updated | AD:568 canSwitchModelInSession; AD:6913-6922 initial OR; AD:6932-6953 refresh (upgrade-only) + emit; AD:5988-5993 hooks on ConfigOptionsUpdated/SessionSetupAdopted; AD:7799 getter | AT:2184 "enables model switching when session/load answers with a model after replay idle" asserts `runtime.providerSession...supportsModelSwitchInSession`. It does not assert that a `provider_session.updated` event is emitted, and does not check that upgrade-only never revokes | no | PARTIAL |
| R13a exactly one catalog publish | AD:5945-5970 adoptLateSessionSetup calls onSessionConfigurationUpdate once | AT:2184 (counts model-bearing publishes == 1) | no | COVERED |
| R13b thread model applied on next turn | AD:5956 clears activeSelection | AT:2261 "applies the thread's model on the next turn after a late session/load" | no | COVERED |
| R13c Plan restore uses the adopted mode | AD:5957-5961 clears interaction mode + deletes nativeBuild snapshot | AT:2357 "restores the native build mode after a Plan turn that followed a late session/load" | no | COVERED |
| R13d ProviderSessionManager getter (orchestrator sees the live capability) | PSM:1737 `get providerSession()`; consumer packages/provider-core/src/server/selectionTransition.ts:24; persisted via PSM:1955/1900 | none (the AT tests use the adapter directly, not PSM) | no | UNCOVERED |
| R14 adapter ignores adoption for a different/no active session | AD:5951-5953 guard | none | no | UNCOVERED (low value) |

## Repro notes for UNCOVERED/PARTIAL rows

Mock knobs relevant here (MOCK:83-91): `T3_ACP_FAIL_LOAD_SESSION`, `T3_ACP_FAIL_LOAD_SESSION_AFTER_CONFIG_REPLAY`,
`T3_ACP_EMIT_LOAD_REPLAY`, `T3_ACP_HANG_LOAD_SESSION_AFTER_REPLAY`, `T3_ACP_DELAY_LOAD_SESSION_AFTER_REPLAY`,
`T3_ACP_LOAD_SESSION_DELAY_MS` (default 5000), `T3_ACP_HOLD_SET_CONFIG_OPTION_UNTIL_LOAD_REPLY` (new),
`T3_ACP_SESSION_LIFECYCLE` (advertises fork, MOCK:110/585), `T3_ACP_FAIL_SET_CONFIG_OPTION`, `T3_ACP_FAIL_SET_MODE_CONFIG_OPTION`,
`T3_ACP_EXIT_ON_SET_CONFIG_OPTION`, `T3_ACP_OMIT_MODEL_CONFIG_OPTION`. Note: HANG and DELAY share one branch (MOCK:756-773). "HANG" also replies after
LOAD_SESSION_DELAY_MS; it does not hang forever.

Base setup for every late-path driver: env `T3_ACP_DELAY_LOAD_SESSION_AFTER_REPLAY=1`, `T3_ACP_LOAD_SESSION_DELAY_MS=500..3000`,
runtime options `resumeSessionId: "mock-session-1"`, `sessionLoadReplayIdleGap: "100 millis"`. Watch `requestLogger` for the
session/load succeeded/failed entries and `getEvents()` for `SessionSetupAdopted`.

- R4b: existing knobs. After start(), call `runtime.setMode(...)`. The mock's set_mode handler emits current_mode_update (MOCK:~2565).
  Or call `runtime.setModel(...)`. Then assert no SessionSetupAdopted.
- R5: existing knobs plus a driver-side call. After start() and before the late reply, call `runtime.request("_test/startup-metadata", {})`
  (MOCK:2497). It emits config_option_update + mode metadata without a counted write. Check that the mock's `sessionId` equals the loaded id
  "mock-session-1". Assert no SessionSetupAdopted and that config options keep the notification's model=gpt-5.4.
- R6: existing knobs plus a driver-side call. After start(), call `runtime.loadSession("mock-session-1")` before the first late reply.
  Expect: the first late reply is not adopted and logs neither succeeded nor failed (interrupted). The second load goes through the same
  delayed path and can adopt.
- R7: existing knobs plus a driver-side call `runtime.resumeSession("mock-session-1")` (the mock handles resume, MOCK:683). Expect no adoption.
- R8: `T3_ACP_SESSION_LIFECYCLE=1` plus `runtime.forkSession("mock-session-1")` (MOCK:810 returns `<id>-fork`). Expect no adoption.
- R9: existing knobs plus driver-side scope close (run make() in an inner `Effect.scoped`, exit it at ~200 ms, wait past the delay).
  Expect no failed log, no event, no unhandled-error log.
- R10: no knob for "error after replay idle". FAIL_LOAD_SESSION* fail before the idle gap, which is the non-late path. This needs either
  a stdio proxy (`spawn: { command: "node", args: [proxy.js, MOCK] }`) that rewrites the session/load result into a JSON-RPC error, or a
  new mock knob (for example, fail after `Effect.sleep(loadSessionDelayMs)`). Expect a failed log and the runtime still usable (start() returns
  the provisional result, prompt works).
- R11: existing knobs. `T3_ACP_DELAY_LOAD_SESSION_AFTER_REPLAY=1`, `T3_ACP_LOAD_SESSION_DELAY_MS=3000`, runtime option
  `sessionLoadTimeout: "1 second"`, idle gap 100 ms. Expect a failed log containing "timed out after replay idle" at ~1 s, and no adoption.
- R12 event: adapter-level only (AT style). Collect `runtime.events` for `provider_session.updated` with
  supportsModelSwitchInSession=true. Existing knobs cover it.
- R13d: needs a PSM-level harness (existing ProviderSessionManager tests) or reasoning only. The getter is tiny and correct by inspection:
  the spread copies the old value, then the getter overrides it.

## How the tests build a real runtime over stdio

- Runtime level: JT:26-31 `mockRuntimeOptions` (`spawn: { command: "node", args: [MOCK] }`, cwd, clientInfo, authMethodId "test").
  Then `AcpSessionRuntime.make({...mockRuntimeOptions, spawn: {...mockRuntimeOptions.spawn, env: {...}}, resumeSessionId, sessionLoadReplayIdleGap, requestLogger})`
  inside `Effect.scoped` + `Effect.provide(NodeServices.layer)`, using `it.live`. Template: JT:1041-1070 / JT:1104-1137.
- Adapter level: AT:137 `makeMockRuntime({ childProcessSpawner, mockAgentPath, environment, protocolEvents, sessionLoadReplayIdleGap, wrapRuntime })`
  passed to `makeAcpAdapterV2({ instanceId, flavor: { driver: ACP_TEST_DRIVER, capabilities: AcpProviderCapabilitiesV2, makeRuntime }, selfInvocation })`,
  with `Effect.provide(layerTest)` (AT:101). Template: AT:2184-2259.

## Concrete issues (pass 1)

1. PLAUSIBLE race, AD:6902-6903: `configureSession(...)` is followed by `Ref.set(activeSelection, input.modelSelection)` (and interaction mode).
   If SessionSetupAdopted is handled (forked handler, AD:5988) while configureSession runs, the clear at AD:5956 gets overwritten.
   The next turn then skips re-applying the model against the real options, which is the bug R13b fixes. The window is short when the response
   arrives just after replay idle, but it is real for agents whose reply lands within ms of idle.
2. PLAUSIBLE race, AD:6640 vs AD:5951: if adoption arrives before `setActiveSessionSetup(started)`, adoptLateSessionSetup returns early
   (activeSessionSetup still null). Then AD:6640 stores the provisional `started`. The capability is still right because AD:6919 reads the live
   config options, but activeSessionSetup/models stay provisional and the catalog publish only comes from configureSession's own publish.
   Narrow window.
3. Minor observability: a dropped late response still logs `session/load succeeded` with the stale result (RT:2045-2054 runs logRequest
   regardless of whether adoptLateResponse returned early). It can't be told apart from an adoption in logs. JT:1072/1104 use this log as their sync point.
4. Minor: adoption is blocked by any reference change of configOptionsRef/modeStateRef, including a no-op re-emit of the same options by the
   agent. That's conservative and safe (the late response is dropped, provisional state kept), but an agent that streams config during replay tail
   after idle would never adopt. Probably acceptable.
5. Mock: the HANG knob doesn't hang (shares DELAY's branch, MOCK:756). This predates the PR but is misleading for R11 authors.
