From 417622c49d61f6195926a0ccea1bd82a8d22b845 Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Sat, 15 Aug 2026 00:47:07 -0600 Subject: [PATCH 1/2] feat(supervise): re-attach bridge sessions for continuity 'resume' workerFromBackend honors `continuity: 'resume'` on the bridge backend by session re-attachment. A bridge session id is the harness conversation key, so a resume spawn binds the prior worker's recorded session id instead of deriving a new one. A fresh to resume to resume chain stays on one harness conversation. Every other backend continues to refuse a resume spawn. Each refusal throws before a worker exists, so the kernel never ledgers `continuity: 'resume'` over a session that was not re-attached. Scope the derived external session id by the spawning manager node. Assignment ordinals restart at `ordinal:0` under every manager, so an unscoped digest mapped worker 1 of every run to one bridge session id. Refs #694 --- CHANGELOG.md | 13 ++ docs/api/primitive-catalog.md | 2 +- docs/api/runtime.md | 10 + docs/canonical-api.md | 2 +- package.json | 2 +- src/runtime/supervise/supervise.ts | 87 +++++++-- .../fixtures/agent-improvement-proposal.json | 10 +- .../agent-profile-improvement-proposal.json | 6 +- tests/runtime/bridge-executor.test.ts | 180 ++++++++++++++++++ 9 files changed, 289 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8c9b104..d346a0dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 0.135.0 + +### workerFromBackend honors `continuity: 'resume'` on the bridge backend + +The backend-derived worker seam re-attaches cli-bridge sessions (#694, the chat-transport resume executor's bridge arm). +A bridge session id IS the harness conversation key — cli-bridge maps it to the CLI's own resume (opencode `-s `, claude `--resume`) — so a resume spawn is real session re-attachment, never a fresh session wearing a `resume` stamp. + +- `workerFromBackend` records the session id each supervised bridge spawn was bound to, keyed by the worker id the Scope assigned. A `continuity: 'resume'` spawn binds the prior worker's recorded session id (`spawnContext.resume.ofWorker`) instead of deriving a fresh one, so the new worker continues the exact harness conversation. A fresh → resume → resume chain stays on ONE session. +- The record is process-local by construction, which matches the kernel's documented resume boundary: a prior process's workers are not resume targets. +- Fail-loud contract unchanged everywhere else: a non-bridge backend still refuses a resume spawn (its executors have no re-attachable session), and a resume of a worker this seam never bound a session for refuses by name. Every refusal throws BEFORE a worker exists, so the kernel never ledgers `continuity: 'resume'` over a session that was not re-attached. +- `runGraph` handed a `backend: 'bridge'` config now honors a delegates edge's `continuity: 'resume'` natively — the revise-edge pattern (write fresh, revise by resuming the writer's session) runs without a custom `makeWorkerAgent`. +- FIX, found by the live proof: the derived external session id now scopes by the spawning manager node. Assignment ordinals restart at `ordinal:0` under every manager, so the unscoped digest mapped worker 1 of EVERY run — and of every sibling manager — to ONE bridge session id; on a bridge with a persistent session store, a `'fresh'` spawn then silently continued a foreign run's harness conversation (measured live: three separate runs shared one claude conversation, `turns: 6`). Durable recovery within a run is unchanged (a replay re-issues the same manager node id and assignments). Migration: a journaled run from an older version replayed under this version derives DIFFERENT session ids and will not re-attach its old harness sessions. + ## 0.134.4 - Consume Core 0.8.0, Eval 0.145.11, Interface 0.52.0, Knowledge 7.2.6, Profile Materialize 0.14.2, and Sandbox 0.26.1 as one compatible dependency set. diff --git a/docs/api/primitive-catalog.md b/docs/api/primitive-catalog.md index 33533c57..31dfe8e2 100644 --- a/docs/api/primitive-catalog.md +++ b/docs/api/primitive-catalog.md @@ -7,7 +7,7 @@ # Primitive catalog — the never-stale anti-reinvention inventory -> **GENERATED** from `@tangle-network/agent-runtime@0.134.9` and `@tangle-network/agent-eval@0.145.11` by `scripts/gen-primitive-catalog.mjs`. Do NOT hand-edit — run `pnpm run docs:api`. This is the mechanical companion to the JUDGMENT in `canonical-api.md` (§2 decision table + §1.5 AgentProfile law): that doc says WHICH primitive to reach for and what NOT to build; this catalog proves WHAT exists. Per-symbol signatures + `file:line` live in the per-module pages under `docs/api/`. +> **GENERATED** from `@tangle-network/agent-runtime@0.135.0` and `@tangle-network/agent-eval@0.145.11` by `scripts/gen-primitive-catalog.mjs`. Do NOT hand-edit — run `pnpm run docs:api`. This is the mechanical companion to the JUDGMENT in `canonical-api.md` (§2 decision table + §1.5 AgentProfile law): that doc says WHICH primitive to reach for and what NOT to build; this catalog proves WHAT exists. Per-symbol signatures + `file:line` live in the per-module pages under `docs/api/`. ## 1. agent-runtime — own public surface diff --git a/docs/api/runtime.md b/docs/api/runtime.md index 09b69807..0111ad2e 100644 --- a/docs/api/runtime.md +++ b/docs/api/runtime.md @@ -26035,6 +26035,16 @@ construction, so a caller may hand back something the run only learns later — `supervise()` gives a traced run's workers their trace context without ordering the span recorder ahead of the worker seam. +Continuity: the `bridge` backend honors `continuity: 'resume'` by session re-attachment. A +bridge session id IS the harness conversation key (cli-bridge maps it to the CLI's own resume — +opencode `-s `, claude `--resume`), so this seam records the session id each supervised +spawn was bound to, keyed by the worker id the Scope assigned, and a resume spawn binds the +prior worker's recorded session id instead of deriving a fresh one. The record is process-local +by construction, which matches the kernel's resume boundary (a prior process's workers are not +resume targets). Every other backend keeps failing loud: their executors have no re-attachable +session, and accepting the spawn would ledger `continuity: 'resume'` over a brand-new session — +a stamp asserting something that never happened. + #### Parameters ##### backend diff --git a/docs/canonical-api.md b/docs/canonical-api.md index f42f27e2..8846cec0 100644 --- a/docs/canonical-api.md +++ b/docs/canonical-api.md @@ -4,7 +4,7 @@ Generated signatures and the complete export list live in docs/api/. Run pnpm docs:freshness after editing this file. --> -> **Version 0.134.9.** +> **Version 0.135.0.** > [`docs/api/primitive-catalog.md`](./api/primitive-catalog.md) lists every export and import path. > `agent-eval` must satisfy `>=0.145.11 <0.146.0`. > `sandbox` must satisfy `>=0.26.1 <0.27.0`. diff --git a/package.json b/package.json index 36677cee..9786b182 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tangle-network/agent-runtime", - "version": "0.134.9", + "version": "0.135.0", "description": "Shared task-lifecycle skeleton for agents: a recursive loop kernel for chat turns, one-shot tasks, and multi-attempt loops, with trace capture and eval-gated self-improvement. Domain behavior lives in adapters; scoring and ship-gates in @tangle-network/agent-eval.", "homepage": "https://github.com/tangle-network/agent-runtime#readme", "repository": { diff --git a/src/runtime/supervise/supervise.ts b/src/runtime/supervise/supervise.ts index bad59cad..55cccc84 100644 --- a/src/runtime/supervise/supervise.ts +++ b/src/runtime/supervise/supervise.ts @@ -141,6 +141,16 @@ import { WORKER_TRACE_PROPAGATION } from './worker-trace' * construction, so a caller may hand back something the run only learns later — which is exactly how * `supervise()` gives a traced run's workers their trace context without ordering the span recorder * ahead of the worker seam. + * + * Continuity: the `bridge` backend honors `continuity: 'resume'` by session re-attachment. A + * bridge session id IS the harness conversation key (cli-bridge maps it to the CLI's own resume — + * opencode `-s `, claude `--resume`), so this seam records the session id each supervised + * spawn was bound to, keyed by the worker id the Scope assigned, and a resume spawn binds the + * prior worker's recorded session id instead of deriving a fresh one. The record is process-local + * by construction, which matches the kernel's resume boundary (a prior process's workers are not + * resume targets). Every other backend keeps failing loud: their executors have no re-attachable + * session, and accepting the spawn would ledger `continuity: 'resume'` over a brand-new session — + * a stamp asserting something that never happened. */ export function workerFromBackend( backend: ExecutorConfig, @@ -150,6 +160,7 @@ export function workerFromBackend( const capturedBackend = captureReusableExecutorConfig(backend, 'workerFromBackend') const unscopedNamespace = randomUUID() let unscopedOrdinal = 0 + const bridgeSessionByWorker = new Map() return (rawProfile, spawnContext) => { const parsed = agentProfileSchema.safeParse(rawProfile) if (!parsed.success) { @@ -157,30 +168,45 @@ export function workerFromBackend( } const profile = parsed.data assertBackendProfileMaterialization(profile, capturedBackend, 'workerFromBackend') - // Fail closed on a resume this seam cannot honor: workerFromBackend creates a NEW executor - // per spawn and has no session re-attachment, so accepting a 'resume' spawn would ledger - // `continuity: 'resume'` over a brand-new session — a stamp asserting something that never - // happened. Custom makeWorkerAgent seams that re-attach sessions are the resume consumers. - if (spawnContext?.continuity === 'resume') { - throw new ValidationError( - 'workerFromBackend: this backend seam does not re-attach sessions and cannot honor ' + - "continuity: 'resume' — provide a makeWorkerAgent that resumes (it receives " + - "spawnContext.resume.ofWorker), or use continuity: 'fresh'", - ) - } + // Resolved BEFORE any worker exists, so a resume this seam cannot honor fails the spawn + // itself and the kernel never ledgers a `continuity: 'resume'` stamp for it. + const resumeSessionId = bridgeResumeSessionId( + capturedBackend, + spawnContext, + bridgeSessionByWorker, + ) const name = profile.name ?? 'worker' // A Scope assignment is stable across reconstruction. Direct callers that omit that context // still get isolation, but only Scope-backed calls claim durable external-session recovery. const assignmentId = spawnContext?.assignmentId ?? `unscoped:${unscopedNamespace}:${unscopedOrdinal++}` + // The derived execution id scopes by the spawning manager: assignment ordinals restart at + // `ordinal:0` under every manager, so an unscoped digest maps worker 1 of EVERY run (and of + // every sibling manager) to one external session — on a bridge with a persistent session + // store, a 'fresh' spawn then silently continues a foreign run's harness conversation. + // Durable recovery is preserved: a replay of the same run re-issues the same manager node id + // and the same assignments, so it derives the same session ids. + const executionScope = spawnContext?.parentNodeId const boundBackend = bindReusableExecutorExecutionId( capturedBackend, - externalExecutionId('supervised-worker', { assignmentId }), + resumeSessionId ?? + externalExecutionId('supervised-worker', { + assignmentId, + ...(executionScope === undefined ? {} : { scope: executionScope }), + }), ) + const boundSessionId = boundBackend.backend === 'bridge' ? boundBackend.sessionId : undefined const baseFactory = createExecutor(boundBackend) // Carry the configured factory into Scope. It is built only AFTER reservation with the real // child signal/context, so a rejected or already-completed keyed spawn creates no executor. const executorFactory = (spec: AgentSpec, ctx: ExecutorContext) => { + // Record the bridge session this worker is bound to under its Scope-assigned worker id, + // so a later resume of the node can re-attach it. A resumed worker records the SAME + // session under its own id, which is what keeps a fresh → resume → resume chain on one + // harness conversation. + if (boundSessionId !== undefined && ctx.node?.nodeId !== undefined) { + bridgeSessionByWorker.set(ctx.node.nodeId, boundSessionId) + } // Caller-supplied seams sit UNDER the per-child seams the Scope seeds, so the scope's // recursion and trace context always win on a key collision. const extraSeams = seams?.() @@ -207,6 +233,43 @@ function externalExecutionId(kind: string, identity: unknown): string { return `${kind}-${digest.slice('sha256:'.length)}` } +/** + * The bridge session id a `'resume'` spawn re-attaches, or `undefined` for a fresh spawn. + * Every refusal throws BEFORE a worker exists, which is what keeps the kernel's continuity + * ledger true: a `'resume'` stamp can only appear over a session that was actually re-attached. + */ +function bridgeResumeSessionId( + backend: ExecutorConfig, + spawnContext: WorkerSpawnContext | undefined, + sessions: ReadonlyMap, +): string | undefined { + if (spawnContext?.continuity !== 'resume') return undefined + if (backend.backend !== 'bridge') { + throw new ValidationError( + `workerFromBackend: the '${backend.backend}' backend seam does not re-attach sessions and ` + + "cannot honor continuity: 'resume' — only the 'bridge' backend resumes here (cli-bridge " + + 'keys the harness conversation by session id). Provide a makeWorkerAgent that resumes ' + + "(it receives spawnContext.resume.ofWorker), or use continuity: 'fresh'", + ) + } + const ofWorker = spawnContext.resume?.ofWorker + if (ofWorker === undefined) { + throw new ValidationError( + "workerFromBackend: a 'resume' spawn carries no resume lineage — the kernel stamps " + + 'spawnContext.resume for ledgered spawns; a direct caller must pass { ofWorker, sequence }', + ) + } + const prior = sessions.get(ofWorker) + if (prior === undefined) { + throw new ValidationError( + `workerFromBackend: no recorded bridge session for worker '${ofWorker}' — this seam ` + + 're-attaches only sessions it bound in this process (the kernel resume boundary). ' + + "Spawn the node fresh first, or use continuity: 'fresh'", + ) + } + return prior +} + /** * The `trace-unpropagated` declaration for a worker backend, or `undefined` when the backend HAS a * propagation channel. The census (`WORKER_TRACE_PROPAGATION`) says WHETHER a backend propagates; diff --git a/src/testing/fixtures/agent-improvement-proposal.json b/src/testing/fixtures/agent-improvement-proposal.json index d5240251..f6cf6f35 100644 --- a/src/testing/fixtures/agent-improvement-proposal.json +++ b/src/testing/fixtures/agent-improvement-proposal.json @@ -1,6 +1,6 @@ { "changedSurfaces": ["prompt"], - "digest": "sha256:bdcb2a49dec1a39ec71d01f845da6a2c6e55282805e271eff52a1b176a938601", + "digest": "sha256:2c580eafb8ac241bcb7a5d90ed4d400dc469e70776be38e92d0ca3cca8b1b3ba", "evaluation": { "decision": { "contributingChecks": [ @@ -4882,7 +4882,7 @@ ], "metadata": { "fixture": "agent-improvement-proposal", - "runtimeVersion": "0.134.9" + "runtimeVersion": "0.135.0" }, "objectives": [ { @@ -4993,8 +4993,8 @@ "baselineContentHash": "sha256:5c21ee53e513fc604cb09754e21c392b24a424da0ef37dbf8f1ee4a8a0b08f09", "candidateContentHash": "sha256:60fcbb1c728194bd51d7d19cb732d1c3f1881dce7e0a6266b41c8b98cfd65693", "kind": "agent-eval-loop", - "recordDigest": "sha256:b6bb5f018d1f2d5210293918085073c1fa2f78fc21faf01364dddf8560af69ac", - "runId": "agent-runtime-0.134.9-proposal-fixture", + "recordDigest": "sha256:cb02f870cf34f127846fa273bcd957b888dcbe8b6021f049272d2bafe021a8f5", + "runId": "agent-runtime-0.135.0-proposal-fixture", "schema": "agent-candidate-experiment" } }, @@ -5021,5 +5021,5 @@ ], "kind": "agent-improvement-proposal", "proposedAt": "2026-07-10T01:00:00.000Z", - "runId": "agent-runtime-0.134.9-proposal-fixture" + "runId": "agent-runtime-0.135.0-proposal-fixture" } diff --git a/src/testing/fixtures/agent-profile-improvement-proposal.json b/src/testing/fixtures/agent-profile-improvement-proposal.json index 59aeb48d..43b6b70f 100644 --- a/src/testing/fixtures/agent-profile-improvement-proposal.json +++ b/src/testing/fixtures/agent-profile-improvement-proposal.json @@ -1,6 +1,6 @@ { "changedSurfaces": ["prompt", "skills"], - "digest": "sha256:5bf45cc86b707dbae89345609e3ee8fe0c4e7444f15bf4fbcc9d3369f5ae9274", + "digest": "sha256:fcf087fac77b1e1cc64c7735aa120df7cd66366d595ea903c156d04449eb9412", "evaluation": { "decision": { "contributingChecks": [ @@ -1715,7 +1715,7 @@ ], "metadata": { "fixture": "agent-profile-improvement-proposal", - "runtimeVersion": "0.134.9" + "runtimeVersion": "0.135.0" }, "objectives": [ { @@ -1826,7 +1826,7 @@ "baselineContentHash": "sha256:21c495a37c418c10bde64fbaa188beddeed31f1f051ea60a6a6582a9ee0db704", "candidateContentHash": "sha256:103f77bc8481601eef1ad5fe6ba84a40dffabc3a44f421f8c8559121edab84e9", "kind": "agent-eval-loop", - "recordDigest": "sha256:15c4af0c9eca66190ac1690ecf4fcc5ec400919e947f310354d230d3a90d3406", + "recordDigest": "sha256:1615f2b547311bbba123dea28a11004d09b60191bcbab4637f0364fb39975868", "runId": "profile-improvement-1", "schema": "agent-profile-improvement-experiment" } diff --git a/tests/runtime/bridge-executor.test.ts b/tests/runtime/bridge-executor.test.ts index 6536cd42..f532409b 100644 --- a/tests/runtime/bridge-executor.test.ts +++ b/tests/runtime/bridge-executor.test.ts @@ -10,11 +10,13 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { InMemoryResultBlobStore, InMemorySpawnJournal } from '../../src/durable/spawn-journal' import { createExecutor, type ExecutorConfig, inlineSandboxClient } from '../../src/runtime' import { createBudgetPool } from '../../src/runtime/supervise/budget' +import type { AgentGraph } from '../../src/runtime/supervise/graph' import { runtimeOwnedExecutorMaterialization, runtimeOwnedExecutorProviderEvidence, runtimeOwnedPendingExecutorMaterialization, } from '../../src/runtime/supervise/materialization' +import { promptHandle } from '../../src/runtime/supervise/prompt-registry' import { bridgeExecutor, bridgeStopSignalKey, @@ -23,6 +25,9 @@ import { import { createScope } from '../../src/runtime/supervise/scope' import { workerFromBackend } from '../../src/runtime/supervise/supervise' import type { Agent, AgentSpec, Executor, UsageEvent } from '../../src/runtime/supervise/types' +import { runGraph } from '../helpers/runtime-with-test-brain' +import { scriptedBrain } from '../kernel/scripted-brain' +import { testAgentProfile } from '../kernel/test-agent-profile' // `bridgeExecutor` POSTs each turn over the `node:http` core client, not global // `fetch`: the bridge runs a harness CLI and streams only once it starts @@ -1423,3 +1428,178 @@ describe('profile-selected model keeps its provider', () => { ).rejects.toThrow(/model\.default is runtime-selected/) }) }) + +describe('workerFromBackend continuity — bridge session re-attachment', () => { + afterEach(() => { + bridgeHttpHandler = null + }) + + const bridgeBackend = { + backend: 'bridge' as const, + bridgeUrl: 'http://bridge.test', + bridgeBearer: 'secret', + } + const nodeCtx = (nodeId: string) => ({ + signal: new AbortController().signal, + seams: {}, + node: { rootId: 'root', parentId: 'root', nodeId, attemptId: `attempt-${nodeId}` }, + }) + const spawnCtx = (assignmentId: string, extra: Record = {}) => ({ + assignmentId, + parentNodeId: 'root', + budget: { maxIterations: 1, maxTokens: 100 }, + task: 'go', + label: 'worker', + ...extra, + }) + + it("re-attaches the prior worker's bridge session across a fresh → resume → resume chain", async () => { + const seen: Array> = [] + bridgeHttpHandler = (payload) => { + seen.push(payload) + return sse('ok', 1, 2) + } + const make = workerFromBackend(bridgeBackend) + const spawns: Array<[string, Record]> = [ + ['w1', spawnCtx('ordinal:0', { continuity: 'fresh' })], + [ + 'w2', + spawnCtx('ordinal:1', { continuity: 'resume', resume: { ofWorker: 'w1', sequence: 2 } }), + ], + [ + 'w3', + spawnCtx('ordinal:2', { continuity: 'resume', resume: { ofWorker: 'w2', sequence: 3 } }), + ], + ] + for (const [nodeId, context] of spawns) { + const worker = make(exactBridgeProfile('worker', 'safe-model'), context as never) as Agent< + unknown, + unknown + > & { executorSpec: AgentSpec } + const executor = worker.executorSpec.executorFactory?.(worker.executorSpec, nodeCtx(nodeId)) + if (!executor) throw new Error('worker executor factory missing') + await drainExecutor(executor) + } + + expect(seen).toHaveLength(3) + const sessions = seen.map((request) => request.session_id) + expect(sessions[0]).toMatch(/^supervised-worker-[a-f0-9]{64}$/) + // Re-attachment on the wire: every resumed turn carries the FIRST worker's session id, so + // cli-bridge continues the same harness conversation instead of opening a new one. + expect(sessions[1]).toBe(sessions[0]) + expect(sessions[2]).toBe(sessions[0]) + }) + + it('isolates equal assignment ordinals under different managers into different sessions', async () => { + const seen: Array> = [] + bridgeHttpHandler = (payload) => { + seen.push(payload) + return sse('ok', 1, 2) + } + // Two runs (or two sibling managers) both issue 'ordinal:0'. Without manager scoping the + // derived session id collides and a 'fresh' spawn continues a FOREIGN harness conversation + // wherever the bridge session store persists. + for (const parent of ['run-a:root', 'run-b:root']) { + const worker = workerFromBackend(bridgeBackend)( + exactBridgeProfile('worker', 'safe-model'), + spawnCtx('ordinal:0', { parentNodeId: parent }) as never, + ) as Agent & { executorSpec: AgentSpec } + const executor = worker.executorSpec.executorFactory?.(worker.executorSpec, nodeCtx(parent)) + if (!executor) throw new Error('worker executor factory missing') + await drainExecutor(executor) + } + expect(seen).toHaveLength(2) + expect(seen[0]?.session_id).not.toBe(seen[1]?.session_id) + }) + + it('refuses a resume of a worker this seam never bound a session for', () => { + const make = workerFromBackend(bridgeBackend) + expect(() => + make( + exactBridgeProfile('worker', 'safe-model'), + spawnCtx('ordinal:0', { + continuity: 'resume', + resume: { ofWorker: 'ghost', sequence: 2 }, + }) as never, + ), + ).toThrow(/no recorded bridge session for worker 'ghost'/) + }) + + it('keeps failing loud on a backend whose sessions cannot be re-attached', () => { + const make = workerFromBackend({ + backend: 'router', + routerBaseUrl: 'http://router.test', + routerKey: 'key', + }) + expect(() => + make( + testAgentProfile('worker', { harness: 'cli-base' }), + spawnCtx('ordinal:0', { + continuity: 'resume', + resume: { ofWorker: 'w1', sequence: 2 }, + }) as never, + ), + ).toThrow(/'router' backend seam does not re-attach sessions/) + }) + + it("runGraph over a bridge backend: a resume edge re-attaches the session and ledgers 'resume' with the wire as witness", async () => { + const seen: Array> = [] + bridgeHttpHandler = (payload) => { + seen.push(payload) + return sse('ok', 1, 2) + } + const graph: AgentGraph = { + nodes: [ + { + id: 'driver', + profile: testAgentProfile('driver', { + harness: 'cli-base', + prompt: { systemPrompt: 'Drive the worker until it delivers.' }, + }), + }, + { id: 'worker', profile: exactBridgeProfile('worker', 'safe-model') }, + ], + edges: [ + { + kind: 'delegates', + from: 'driver', + to: 'worker', + directive: promptHandle('delegates/worker-brief/v1'), + maxTraversals: 3, + continuity: 'resume', + }, + ], + deliverable: { describe: 'the built artifact', check: (out) => out !== undefined }, + budget: { maxIterations: 20, maxTokens: 50_000 }, + } + const spawnTurn = (task: string) => ({ + toolCalls: [{ name: 'spawn_agent', arguments: { profile: { name: 'worker' }, task } }], + }) + const awaitTurn = { toolCalls: [{ name: 'await_event', arguments: {} }] } + const res = await runGraph(graph, { + runId: 'bridge-resume', + journal: new InMemorySpawnJournal(), + backend: bridgeBackend, + brain: scriptedBrain([ + spawnTurn('shot 1'), + awaitTurn, + spawnTurn('shot 2'), + awaitTurn, + { content: 'done' }, + ]), + }) + expect(res.result.kind).toBe('winner') + + // The kernel ledgered the SECOND traversal as 'resume' and the wire proves the claim: both + // worker turns carried one bridge session id, so the stamp records a real re-attachment. + const rows = res.ledger.filter((row) => row.kind === 'delegates') + expect(rows.map((row) => [row.traversal, row.outcome, row.continuity])).toEqual([ + [1, 'delivered', 'fresh'], + [2, 'delivered', 'resume'], + ]) + expect(rows[0]?.workerId).not.toBe(rows[1]?.workerId) + expect(seen).toHaveLength(2) + expect(seen[0]?.session_id).toMatch(/^supervised-worker-[a-f0-9]{64}$/) + expect(seen[1]?.session_id).toBe(seen[0]?.session_id) + }) +}) From a0b5d5aa68fcc37280ca7b11d30e4b6dc274f476 Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Sat, 15 Aug 2026 02:43:11 -0600 Subject: [PATCH 2/2] docs(index): workerFromBackend is now the bridge resume consumer --- src/runtime/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/runtime/index.ts b/src/runtime/index.ts index ffdbe884..2a85357b 100644 --- a/src/runtime/index.ts +++ b/src/runtime/index.ts @@ -548,8 +548,9 @@ export { } from './supervise/budget' // The chat-transport leaf (#721): a worker that IS a model conversation on a bare // OpenAI-compatible /v1/chat/completions endpoint — no sandbox. Ships with its session store and -// the continuity-honoring `makeWorkerAgent` seam (the resume consumer `workerFromBackend` -// refuses to be), so conversation graphs and chat-shot loops compose from data. +// the continuity-honoring `makeWorkerAgent` seam. `workerFromBackend` resumes only bridge-backed +// workers; this leaf resumes its own sessions, so conversation graphs and chat-shot loops +// compose from data on any transport. export { type ChatCompletionsTransport, type ChatSessionStore,