Skip to content

One bridge process per provider artifact; delete the codex per-thread lane (WS4 L3) - #2239

Closed
SawyerHood wants to merge 1 commit into
bb/ws4-l2-delete-runtime-recovery-regexes-thr_qdx8ashimgfrom
bb/ws4-l3-one-process-per-provider-thr_qdx8ashimg
Closed

One bridge process per provider artifact; delete the codex per-thread lane (WS4 L3)#2239
SawyerHood wants to merge 1 commit into
bb/ws4-l2-delete-runtime-recovery-regexes-thr_qdx8ashimgfrom
bb/ws4-l3-one-process-per-provider-thr_qdx8ashimg

Conversation

@SawyerHood

@SawyerHood SawyerHood commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

WS4 layer 3 of 7, stacked on #2236 (L2). Design: ideal-state-provider-api.md §"Runtime" — one process per provider artifact; the bridge supervises any children; the runtime never scopes processes per thread — and §7 "per-thread process keys … the daemon per-thread lane mirror".

What was wrong

Codex alone ran one bridge process per bb thread: resolveProviderProcessKey minted codex\0thread:<id> keys, isThreadScopedCodexProcess gated a per-thread shutdown in releaseIdleProviderProcess and the idle reaper, EnsureProviderArgs.forThreadId carried the thread into process resolution, and the daemon's getProviderProcessLaneKey mirrored the lane with a \0thread: suffix for codex only. Every one of these was a providerId === "codex" branch in core.

Why the lane existed (the #130 finding). git log -S CODEX_THREAD_PROCESS_KEY_PREFIX lands on one commit, 2a84ecf "Run Codex app-server per thread (#130)". Its PR body gives two reasons: (1) a targeted account restart — after a terminal 401/429 restart and resume only that thread's app-server before its next turn, so an external codex login takes effect without a bb restart and without touching other threads; (2) daemon lane isolation — key host-daemon process lanes per codex thread so thread.stop does not block unrelated codex threads. Before #130 one app-server was shared per environment, so "codex app-server cannot host two sessions" was never the reason. Since the bridge migration the codex bridge supervises one codex app-server child per bb thread itself, and L1 moved reason (1) into the bridge (rebuildBeforeNextTurnReason). Reason (2) is the one deliberate behavior change below.

What changed

  • packages/agent-runtime/src/runtime.ts: resolveProviderProcessKey keys on the provider artifact alone (<providerId>#bridge:<hash>[#acp:<fp>]); deleted CODEX_THREAD_PROCESS_KEY_PREFIX, isThreadScopedCodexProcess, the per-thread branch of releaseIdleProviderProcess (only retireSupersededBridgeProcessIfIdle remains, unchanged), and ResolveProviderProcessKeyArgs.threadId. The idle reaper's open-work guard (backgroundWorkState.hasOpenThreadWork) now applies to the pre-experiment codex path too (it used to trust the thread-scoped process instead); CODEX_PROVIDER_ID survives in one place, the pre-experiment reap policy gate, which is product policy rather than topology.
  • packages/agent-runtime/src/types.ts: EnsureProviderArgs.forThreadId deleted (it would have been accepted-but-ignored).
  • apps/host-daemon/src/command-router.ts: getProviderProcessLaneKey returns ${environmentId}\0${providerId} for every provider; ProviderProcessLaneKeyArgs.threadId and the daemon's CODEX_PROVIDER_ID are gone. Behavior change: a codex thread.stop holds the codex process's write lane for the length of its dispatch, and another codex thread's turn.submit in the same environment waits on it — exactly what claude and ACP threads do today. The lane covers the dispatch (until the runtime accepts the command), not the turn.
  • threadUnarchiveBarriers kept as is (it serializes a thread, not a process). No wire field changed: no HOST_DAEMON_PROTOCOL_VERSION bump.
  • plugins/provider-codex/src/bridge/fake-codex-app-server.mjs: processLogPath logs spawn:<pid>:<ppid> / exit:<pid>:<ppid> (also on SIGTERM), so a test can count children and the bridge processes that own them.
  • Ratchet baseline 142 → 136 (runtime.ts 6 → 3; command-router.ts drops out of the core-file list).

How you verified

The topology, stated from a testpackages/agent-runtime/src/runtime.codex-topology.test.ts drives the real plugins/provider-codex bridge module through the runtime (the daemon's path) with the fake app-server as its child:

Step Bridge processes App-server children alive What happened
start t1, t2, t3 1 3 (one per thread) N concurrent codex threads share one bridge; the bridge spawned one child each (3 spawns, 1 distinct parent pid)
turn on t2 1 3 no extra child
stopThread(t1) (release) 1 2 t1's child exited; bridge and other children stay
archiveThread(t2) 1 1 t2's child exited (bridge releaseAfter); bridge stays
start t4 with a new artifact digest 2 2 the plugin update gets its own bridge (t4's child); the old bridge keeps serving t3
stopThread(t3) 1 1 the old bridge lost its last thread → retireSupersededBridgeProcessIfIdle SIGTERMs it → its onSigterm kills t3's child; runtime reports one expected bridge exit
unarchive + resume t2 on the new bridge 2 parents seen, 1 live 2 the unarchive ran on a one-shot maintenance child (spawned and reaped), the resume got a fresh child

Runtime unit tests rewritten for the shared process (runtime.process-lifecycle.test.ts): two codex threads answer from the same pid and a stop keeps the process; a failed construction keeps the process; the idle reap releases the session (thread/stop) and resumes on the same process (1 spawn, 0 exits). Daemon: routes every codex thread through the provider's one process lane replaces the #130 exception test.

Oracle (--concurrency 4): typecheck 5/5; tests agent-runtime 451, host-daemon 555, provider-codex 201 — green. Parity codex 16 passed / 1 skipped, zero diffs. A4: 307 threads, 93,534 rows, zero diffs. Ratchet --base origin/main: 136 ≤ 142, baseline written.

Part of the provider-plugin migration (WS4). L4 makes the skill roots one generic shape.

AGENT GENERATED: by Claude Opus 5

… lane (WS4 L3)

The per-thread process key (#130) existed for a targeted account restart
and for daemon lane isolation, not because codex app-server could not host
two sessions; the codex bridge already supervises one app-server child per
thread, and L1 moved the restart into it. The runtime now keys every
provider's process by its artifact alone, the daemon mirrors one lane per
provider per environment, and the per-thread reap no longer shuts a
process down. A runtime test drives the real codex bridge with the fake
app-server: N threads, one bridge, one child each; children die on stop,
archive, and bridge retirement. Ratchet 142 → 136.

Co-Authored-By: Claude <noreply@anthropic.com>
@SawyerHood SawyerHood closed this Aug 21, 2026
@SawyerHood
SawyerHood deleted the bb/ws4-l3-one-process-per-provider-thr_qdx8ashimg branch August 21, 2026 21:41
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.

1 participant