Skip to content

fix(acp): support root session replacement in V2 - #1

Draft
nullStack65 wants to merge 1 commit into
orchestrator-v2-base-3d45b3056from
feat/acp-root-session-adoption-3d45b3056
Draft

nullStack65 wants to merge 1 commit into
orchestrator-v2-base-3d45b3056from
feat/acp-root-session-adoption-3d45b3056

Conversation

@nullStack65

Copy link
Copy Markdown
Owner

What this is

Fork-held slice for the V2 architecture (USE_GENERIC_ACP_V2_WITH_SMALL_GENERIC_FIXES). It adds the generic, provider-neutral root-session-replacement capability the V2 ACP runtime needs so an agent that swaps its root session behind the same connection (omp /fresh) does not go silent.

  • Upstream PR source: pingdotgg/t3code#2829
  • Exact feat(orchestrator): introduce new orchestrator pingdotgg/t3code#2829 head used: 3d45b305632c72cbb8fd8a6dd5077b94c25d7eb5
  • Base snapshot branch: orchestrator-v2-base-3d45b3056 (3d45b3056)
  • Feature branch/head: feat/acp-root-session-adoption-3d45b3056 (9ba3a54d83c0fc4f8317ae605bd6a6a528213396)
  • T3 main at completion: 4749035bda13b4b6260499caedbc0d69a2f60e6f
  • Frozen bespoke reference (unmodified): pingdotgg/t3code#11973 @ 4f9419b72bb92bebc6669dd95f345e1ccdf4bced
  • Not intended for upstream submission until the V2 architecture stabilizes.

Problem

An agent can replace the root session on the same ACP connection. Real omp 18.2.4 behavior, proven live:

  • durable ACP session = A (returned by session/new, addressable, replayable via session/load);
  • the user runs /fresh;
  • every later session/update notification arrives under a new id B, including the direct response to the /fresh prompt;
  • B is not independently addressable: prompts/cancel must keep using A, and session/list only ever shows A;
  • V2 dropped B as a foreign session, so the thread went silent after /fresh.

Design (generic, no provider branches)

AcpSessionRuntimeOptions.adoptRootSessionReplacement (default false) plus onRootSessionReplaced for diagnostics.

  • Durable vs live identity: the durable setup id is never rewritten. Prompts, cancellation, session/load, session/close, model/config requests, and assistant item identity all keep using the durable id.
  • Adoption rule: a new id can replace the live root only when it is first seen while a root prompt is in flight, and at most once per prompt. Foreign ids seen while idle are always rejected (child/background traffic cannot become root accidentally).
  • Projection: adopted notifications are projected back onto the durable id before either the runtime's internal consumer or the adapter sees them, so downstream session-id checks cannot reject the live root.
  • Durable stragglers accepted, stale replaced-live ids dropped: updates on the durable id remain root traffic; a previously adopted live id is dropped once replaced.
  • Wiring: the generic ACP Registry adapter exposes a per-instance opt-in through AcpRegistrySettings.rootSessionReplacement (default false, generic, usable by future agents). No global relaxation for every registry agent. No if agent === "oh-my-pi" anywhere.
  • The opt-in contract is explicit: enable only for agents whose connection publishes a single live root session id and never child/subagent session ids; child traffic must be normalized before it reaches the runtime.

Net change: 6 files, ~+609/-5.

Tests

apps/server/src/provider/acp/AcpJsonRpcConnection.test.ts — new root-session-replacement block (all green, 47/47 total):

  • A strict default: replaced root rejected (text stays root before fresh)
  • B opt-in: A→B adopted, B updates continue, item ids stay in the durable namespace
  • C/E successive replacements A→B→C deterministic; onRootSessionReplaced reports each change once
  • D prompt/cancel requests keep the durable id after replacement
  • F idle cancel while a replaced-root turn hangs targets the durable id (and never B)
  • G durable stragglers accepted, stale replaced-live ids dropped
  • H foreign session while idle never adopted
  • I opt-in off = zero behavior change

Additional green:

  • AcpRegistryAdapterV2.test.ts 3/3 — includes a new end-to-end test through the real registry runtime with the instance opt-in.
  • AcpAdapterV2.test.ts 105/105 (V2 provider adapter runtime policy).
  • OrchestratorReplayFixtures.integration.test.ts 73/73 (V2 replay fixtures).
  • apps/server tsc --noEmit: clean. packages/contracts tsc --noEmit: clean.
  • Targeted vp lint on every changed file: clean. git diff --check: clean.
  • Test-fixture-only platform fix: the registry fixture did not declare a darwin-x86_64 distribution, so the pre-existing E2E test failed on this host. Same failure reproduces on the base snapshot 3d45b3056 (ACP Registry agent fixture-agent has no compatible distribution for darwin-x64), classified as a base/host fixture gap, now fixed in the fixture (no production code).

Live zero-inference proof (real omp 18.2.4, registry PR pingdotgg#613 metadata)

OMP updated to 18.2.4; local binary sha256 780a47a5... matches the exact darwin-x86_64 sha256 in agentclientprotocol/registry#613 (eed36bcaa677749b0bdf24010dfc3883834612c9). Registry JSON served from an injected HttpClient using the exact oh-my-pi/agent.json metadata; commandPath pointed at the local omp; authMethodId: "agent". No model prompt; only local commands (/context, /fresh, /rename).

Through the patched V2 registry adapter (rootSessionReplacement: true):

Direct AcpSessionRuntime with adoptRootSessionReplacement: true:

  • start id A = 01a0b151-102d-...; session/list contains A
  • /fresh → onRootSessionReplaced { previousSessionId: A, sessionId: 01a0b151-12be-... } (live identity observed without overwriting A)
  • idle session/cancel succeeds on A
  • /context after /fresh succeeds
  • session/load(A) returns A; session/list after load still contains A

Dependencies / not covered here

  • Registry metadata remains agentclientprotocol/registry#613 (oh-my-pi 18.2.4). This branch does not vendor registry metadata.
  • Inference-gated items (model prompt round-trips, thinking/model config application, usage) were intentionally not exercised; this proof stops at zero-inference behavior.
  • Separate follow-ups (not in this slice): ANSI cleanup, thread-title projection, subagent presentation, usage UI, onboarding.

An agent can replace the root session behind the same ACP connection: omp's
/fresh starts a new provider session and publishes every later session/update
under a new id while the original id stays the one session/load replays. The
V2 runtime treated those updates as a foreign child session, so the thread
went silent for the rest of the turn.

Add a generic, default-off runtime capability
(AcpSessionRuntimeOptions.adoptRootSessionReplacement) that adopts a new live
root session id first seen while a root prompt is in flight, at most once per
prompt, and projects adopted notifications back onto the durable setup id.
Prompts, cancellation, session loading, and item identity keep using the
durable session; foreign ids seen while idle stay rejected. The ACP Registry
adapter exposes the opt-in per instance through AcpRegistrySettings.

The frozen bespoke reference is PR pingdotgg#11973; this slice carries only the
provider-neutral adoption mechanism.
@nullStack65

Copy link
Copy Markdown
Owner Author

ENVFIX1:STD-OMP START

Executing the retained OMP4a40 focused test obligation once, without installation or a model call.

  • Source: 9ba3a54d83c0fc4f8317ae605bd6a6a528213396 / tree 6c9ada799401b4bc3febaa4795066c79d0a0b6e3, clean
  • CWD: apps/server in /Users/businessaccount/Dev/t3code-omp-pr1-9ba-validation-20261004
  • Runtime: Node v24.21.0, workspace-local vp v0.3.0, Vitest v4.1.11
  • Isolation: existing sandbox denies network and all source writes; only the task evidence directory is writable
  • Exact scope: AcpRegistryAdapterV2.test.ts and AcpJsonRpcConnection.test.ts
  • Non-effects: no pnpm, install, dependency/store/source mutation, trust-lockfile/warn bypass, publisher, CI, or paid fallback

The original execution owner will collect the exact child PID/PGID, finite deadline, exit, test counts, output hash, source pins, and denied-operation evidence.

@nullStack65

Copy link
Copy Markdown
Owner Author

ENVFIX1:STD-OMP RESULT

The single authorized direct vp test run attempt is terminal and collected. It did not execute tests.

  • Process: PID/PGID 15904, start 2026-10-06T04:34:58.360632Z, deadline 04:44:58.360632Z, terminal 04:35:04.872673Z, exit 1; PID and PGID absent
  • Result: 0 test files and 0 test cases
  • Concrete failure: Vite config startup attempted mkdir apps/server/node_modules/.vite-temp; the source-read-only sandbox denied it with EPERM
  • Isolation: no pnpm, install, network attempt, dependency/store mutation, source mutation, or model call
  • Source after: unchanged clean 9ba3a54d83c0fc4f8317ae605bd6a6a528213396 / tree 6c9ada799401b4bc3febaa4795066c79d0a0b6e3; both test hashes unchanged
  • Output hashes: stdout e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855; stderr 63403d9aeaa01e5c1cbe4d037f3dd9eed2b068a1bd6d79317bafc0db6ebe6bdf
  • Durable receipt: ENVFIX1-STD-OMP-NO-INSTALL-ACTUAL-RESULT-20261006.json, SHA-256 a6f1c1029a7fa911c9ae86693f76a6588c1f5e6de2d77a3bcd9be0c6b52751d1

The unchanged command was not retried. The exact remaining primitive is a no-install Vite/Vitest startup mode that redirects .vite-temp to task-writable storage (for example, only if the already-running owner validates --configLoader runner), while retaining network denial and source immutability. Standards has separately reconciled an authoritative same-operation runtime qualification as active; this result does not duplicate or supersede that owner.

@nullStack65

Copy link
Copy Markdown
Owner Author

ENVFIX1:STD-IMPORT-CLOSURE RESULT — OMP4a40/res4cfa read-only artifact/runtime qualification

Outcome: NOT_RUNTIME_QUALIFIED; no rebuild, test replay, install, source/config/cache/artifact write, network/provider/pilot/funds/key/proxy effect.

Exact inputs preserved

  • Source HEAD 9ba3a54d83c0fc4f8317ae605bd6a6a528213396, tree 6c9ada799401b4bc3febaa4795066c79d0a0b6e3, tracked clean.
  • Build receipt OMP-EXACT-CACHE-BUILD-ACTUAL-TERMINAL-COLLECTION-20261006.json SHA-256 3753c79342b469f8432f7c8b2f581678542b7836ab6549c625cf894b5d3f7f51 matches.
  • All 23 artifact hashes rechecked and match the receipt. Prior exact two-file 50/50 test result and direct build exit 0 are preserved, not rerun.

Import classification

  • 58 UNRESOLVED_IMPORT occurrences are not the intended runtime-external/native closure.
  • 57 are real missing build-time resolution edges from exactly three workspace packages: packages/tailscale (8), packages/ssh (19), and packages/effect-codex-app-server (30). Their local node_modules directories are absent.
  • Those 57 comprise @t3tools/shared/hostProcess (3) and effect/* (54): Cause 2, Config 1, Context 2, Data 1, Deferred 1, Duration 2, Effect 7, Exit 1, FileSystem 2, Layer 2, Option 4, Path 2, PlatformError 1, Queue 2, Ref 1, Schema 6, Scope 3, Sink 1, Stdio 3, Stream 5, unstable/http 1, unstable/process 3, unstable/process/ChildProcessSpawner 1.
  • These must be bundled. apps/server/vite.config.ts explicitly says bundle everything except packages accepted by scripts/lib/cli-external-packages.ts; neither effect nor @t3tools/shared is in CLI_RUNTIME_EXTERNAL_PREFIXES.
  • The 58th warning, bun:sqlite, is an optional dynamic Cursor SDK module-table branch. The intended server entrypoint imports node:sqlite; under Node it is not a top-level runtime requirement. It is not evidence that Bun is the intended runtime.
  • Deliberate file-backed/runtime externals are the native closure selected by repository policy: direct roots node-pty, msgpackr-extract, and @ff-labs/fff-node, plus their selected transitive native/load dependencies. Generated code accesses them via createRequire/lazy loading; scripts/build-cli-archive.ts assembles their physical node_modules tree.

Observed runtime and load evidence

  • Intended build/runtime: Node v24.21.0, executable /Users/businessaccount/.local/share/vite-plus/js_runtime/node/24.21.0/bin/node, SHA-256 7abcf39bd37ab251015337ff75304d7555f0d8e88c6e0fbf04bce8ce34636f49 (package engine also declares Node; vp targets node22.16+).
  • Bun 1.4.0 exists at /Users/businessaccount/.bun/bin/bun, SHA-256 ca8a18d0116d7b6b19f53bb0d8c48e487c0757cab4dc3f4f8cc5e43a44cd75d8, but is not the target for apps/server.
  • All 12 emitted .mjs files pass node --check; importing bin.mjs as a non-entry wrapper passes.
  • A write- and network-denied bounded real entrypoint load, Node 24.21 bin.mjs --help, exits 1 before CLI startup: ERR_MODULE_NOT_FOUND for @t3tools/shared from binCli-CvQiu--R.mjs. This proves the bundle is not loadable in its current retained directory.

Candidate/source correspondence

  • bin.mjs.map binds apps/server/src/entrypoint.ts and apps/server/src/bin.ts; binCli-*.mjs.map binds apps/server/src/binCli.ts; claude-history-worker.mjs.map binds apps/server/src/claude-history-worker.ts and the three warning-producing workspace package sources.
  • apps/server/package.json#bin.t3 points to ./dist/bin.mjs; the retained omp-candidate-bundle is an alternate vp pack -d output and is not referenced by that package or by a release archive.
  • Therefore it is a source-corresponding build-output candidate only, not an intended T3 installable artifact. The production archive additionally requires the repository-selected runtime external tree, web client, resource monitor, and platform/native checks.

Precise DEV materializer primitive

  1. Under DEV's existing sole placement/materializer custody, restore the lock-derived workspace dependency link graph for only packages/tailscale, packages/ssh, and packages/effect-codex-app-server using the canonical installed pnpm/vp materializer in offline + frozen-lockfile + ignore-scripts mode. Do not edit manifests/lockfile/store/source and do not hand-create duplicate package trees.
  2. Expected existing targets are already present: workspace packages/shared, packages/contracts, and locked store effect@4.0.0-rc.112 with patch hash 8bef799f.... Required direct links are tailscale→shared/effect; ssh→contracts/shared/effect; effect-codex-app-server→effect. Reconcile exact generated links against the lock before accepting.
  3. Rebuild once to a fresh task-owned output. Refuse acceptance if any effect/* or @t3tools/shared/* remains unresolved. Treat the optional bun:sqlite warning separately; do not add Bun as a server runtime dependency.
  4. Later release materialization must use scripts/build-cli-archive.ts/the existing packaging path to stage the native runtime-external closure; copying the 23 files alone is insufficient.

Bounded no-live-effect validation after materialization

# From each of the three workspace package roots: resolution only
node --input-type=module -e 'for (const s of ["effect/Effect","effect/Schema","@t3tools/shared/hostProcess"]) { try { console.log(s, import.meta.resolve(s)) } catch (e) { console.error(s,e.code); process.exitCode=1 } }'

# Candidate syntax and top-level import audit
for f in "$CANDIDATE"/*.mjs; do "$NODE24" --check "$f"; done
rg -n '^import .* from "(effect/|@t3tools/shared/)|^import "(effect/|@t3tools/shared/)' "$CANDIDATE" -g '*.mjs'

# Entry wrapper, then actual help path, with no writes or network
sandbox-exec -p '(version 1)(allow default)(deny network*)(deny file-write*)' "$NODE24" --input-type=module -e "await import('file://$CANDIDATE/bin.mjs')"
sandbox-exec -p '(version 1)(allow default)(deny network*)(deny file-write*)' "$NODE24" "$CANDIDATE/bin.mjs" --help

The top-level audit must return no effect/* or @t3tools/shared/*; the actual help path must exit 0 without loading bun:sqlite, making provider calls, or writing state. Separate native-external/package/archive validation remains required before any installed/native/pilot acceptance.

@nullStack65

Copy link
Copy Markdown
Owner Author

ENVFIX1:Standards — RESULT: Node24 no-evaluation TypeScript format qualification

Outcome: QUALIFIED for the narrow syntax/type-erasure boundary; not qualified for linking, import evaluation, native loading, transitive closure, or runtime acceptance.

Exact identities:

  • Worktree HEAD 9ba3a54d83c0fc4f8317ae605bd6a6a528213396, tree 6c9ada799401b4bc3febaa4795066c79d0a0b6e3.
  • Runtime /Users/businessaccount/.local/share/vite-plus/js_runtime/node/24.21.0/bin/node, v24.21.0, SHA-256 7abcf39bd37ab251015337ff75304d7555f0d8e88c6e0fbf04bce8ce34636f49.
  • Target /Users/businessaccount/Dev/t3code-omp-pr1-9ba-validation-20261004/packages/shared/src/hostProcess.ts, 3,880 bytes, SHA-256 7083abb0210f717a174da1c1cff7bee4b1f0bd96241d68784e13154dcddcf576 before and after.
  • Manifest SHA-256 60fb77033a190d3a9f81beb1937ccaf7e3b324234c791cb7f1649bdc96360cfd; type: module, no numeric version, ./hostProcess types/import both map to ./src/hostProcess.ts.

Commands and direct results:

  1. env -i PATH=/usr/bin:/bin HOME=/var/empty NODE_NO_WARNINGS=1 <node24> --check <hostProcess.ts> -> exit 0. This is syntax checking only; the target module was not executed.
  2. env -i PATH=/usr/bin:/bin HOME=/var/empty NODE_NO_WARNINGS=1 <node24> --input-type=module --eval '<read pinned file; module.stripTypeScriptTypes(mode=strip, sourceMap=false); hash result in memory>' <target> <expected-sha> -> exit 0. Erased output remained memory-only: 3,880 bytes, SHA-256 f21674bb91a97eadbd30d77d22ab4ee0c27ae128888ef4cd369908c64eb076d9; output differed from source as expected.
  3. The same in-memory erasure was passed to vm.SourceTextModule under --experimental-vm-modules, without link or evaluate -> exit 0, status unlinked; parsed dependency specifiers were effect/Context, effect/Effect, node:dns, node:os, node:sea.

Boundaries verified:

  • The wrong /usr/local/bin/node (v25.6.0) was not used for qualification.
  • No target/bundle/server/tool/provider/native module was imported, linked, evaluated, or executed.
  • No files, dependencies, configs, links, builds, tests, services, network calls, pilots, keys, or proxies were changed.
  • Target and manifest remain clean in the worktree.

Limitations / next predicate:

  • This proves only Node 24.21 can parse the pinned TypeScript and erase its erasable syntax into parseable unlinked ESM.
  • It does not prove the four dependency specifiers resolve in the intended deployed location, that TypeScript source loading is enabled for the consuming bundle, that effect/shared links are portable, or that native/protocol/auth/billing/lifecycle behavior is acceptable.

@nullStack65

Copy link
Copy Markdown
Owner Author

ENVFIX1:Standards — CORRECTION to Node24 no-evaluation TypeScript format result

The earlier direct node --check hostProcess.ts exit-0 observation is non-authoritative and must not be used as TypeScript acceptance. Under the supported Node 24.21 contract, direct .ts --check does not establish the intended file TypeScript semantics.

The narrow QUALIFIED verdict rests solely on this exact bounded chain:

  1. Read the pinned hostProcess.ts bytes and verify SHA-256 7083abb0210f717a174da1c1cff7bee4b1f0bd96241d68784e13154dcddcf576.
  2. Run node:module.stripTypeScriptTypes(source, { mode: "strip", sourceMap: false }) in memory under the pinned Node 24.21 binary.
  3. Parse the erased output with vm.SourceTextModule without linking or evaluating it.

That chain exited 0, produced erased-output SHA-256 f21674bb91a97eadbd30d77d22ab4ee0c27ae128888ef4cd369908c64eb076d9, and left the module unlinked. No import, link, evaluation, file write, or network action occurred.

Limitations are unchanged: this does not prove dependency resolution, deployed .ts loading, transitive closure, native loading, or runtime/protocol/auth/billing/lifecycle acceptance.

@nullStack65

Copy link
Copy Markdown
Owner Author

ENVFIX1:OMPREG START/RECONCILED — bounded installed OMP 18.2.4 no-auth registry inspection

Scope: read installed executable/catalog/docs and supported nonsecret effective config only for openrouter/openai/gpt-5.6-luna. No provider request, auth lookup, API GET, credential read, model inference, ACP initialize/session setup, write, install, or pilot. Input receipt SHA256 1150878462cb68f4ce020d8f7faed86293d1cd97102dce7759b83341eed0dde4; source remains 9ba3a54d83c0fc4f8317ae605bd6a6a528213396 / tree 6c9ada799401b4bc3febaa4795066c79d0a0b6e3 clean.

@nullStack65

Copy link
Copy Markdown
Owner Author

ENVFIX1:OMPREG RESULT — endpoint resolved; pilot route NOT qualified

Installed executable: /Users/businessaccount/.local/bin/omp, omp/18.2.4, SHA256 780a47a5e5668807f5ac1ca37067ddbc528a64edcdf964286bf346f7a83e0609. Embedded catalog resolves exact selector openrouter/openai/gpt-5.6-luna to provider openrouter, catalog API openrouter, base URL https://openrouter.ai/api/v1. PI_OPENROUTER_RESPONSES is unset with no relevant env-file override, so installed runtime resolves the wire to openai-responses. No global/project models override file exists.

Safety result: NOT a sole/single-request/no-spillover route. Effective config is retry.enabled=true, retry.maxRetries=10, retry.modelFallback=true, retry.fallbackChains={}, enabledModels=[], disabledProviders=[]. Empty chains means no explicit alternate chain; it does not disable retry/fallback controls. The exact static row has no OpenRouter provider.only/order fence, so upstream spillover is not qualified. Auth was intentionally not inspected and remains unqualified. No provider/API/model/pilot calls or secrets/funds reads occurred.

Receipt: /Users/businessaccount/Documents/t3-investigation-20260929/execution/standards-parallel-included-execution-20261005/OMP-INSTALLED-REGISTRY-NOAUTH-READBACK-20261006.json, 6018 bytes, SHA256 4b9a95f450b1e490078b0cff2df348985d7b8c1b6d9f90ea6a3c1c0b21a4467c. Source 9ba3a54d83c0fc4f8317ae605bd6a6a528213396, tree 6c9ada799401b4bc3febaa4795066c79d0a0b6e3, tracked clean; six pins/23 artifacts/7 links preserved by no-write.

Next exact primitive: a task-scoped, source-backed effective request policy that demonstrably disables session retries and model fallback and pins OpenRouter upstream routing (provider.only) before any auth/funds/pilot qualification. This inspection did not modify config.

@nullStack65

Copy link
Copy Markdown
Owner Author

ENVRESUME:RUNTIME START

Reconciled the existing OMP 18.2.4 no-auth registry result. I am qualifying a task-scoped, isolated policy only: retry disabled, model fallback disabled, exact enabled-model selector, and OpenRouter upstream routing fence. No provider/API/auth/secret/funds/pilot call; no project source write or install. I will return exact executable/config hashes, effective readback, and any remaining unsupported primitive.

@nullStack65

Copy link
Copy Markdown
Owner Author

ENVRESUME:RUNTIME RESULT

Task-local OMP request-policy boundary qualified without provider access. Installed omp/18.2.4 SHA 780a47a5e5668807f5ac1ca37067ddbc528a64edcdf964286bf346f7a83e0609 accepted and read back retry.enabled=false, retry.modelFallback=false, enabledModels=[openrouter/openai/gpt-5.6-luna]. The separately bound future selector is openrouter/openai/gpt-5.6-luna@openai; installed source at decimal offset 138696092 projects the parsed suffix to provider.only=[openai]. A network-denied, auth-helper-denied, protected-secret-denied models read exited 0 and found exactly one matching catalog row. Provider/API/inference/auth/funds/pilot calls: 0. Source head/tree remained 9ba3a54 / 6c9ada799401b4bc3febaa4795066c79d0a0b6e3.

Receipt: /Users/businessaccount/Documents/t3-investigation-20260929/execution/standards-parallel-included-execution-20261005/OMP-TASKLOCAL-NO-PROVIDER-POLICY-QUALIFICATION-20261006.json
SHA-256: 77a196cfd243f9a259f4293a43a9a7e9e4affff1bfbaec8b4c09eb6d2dbd68dc

This qualifies the policy boundary only. Authentication, current balance, funded pilot authorization, ACP lifecycle, and native execution remain separate open predicates; no pilot was started.

This branch has not been deployed

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

Labels

vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant