From 7306d19cc4979f360e528ce2eb499d1f2c4a9b4a Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Fri, 25 Sep 2026 13:30:23 -0700 Subject: [PATCH] fix(orchestration): deliver a cleared structured worker's mail to its live successor A terminal keeps its handle across /clear; a structured worker's successor now does the same. Mail at the worker's handle, its dispatch mailbox, and a Run it coordinates resolved to the session minted for the worker, which /clear replaced. Each now walks the /clear lineage forward to the live session, which the caller resolver already treats as the worker. --- ...rca-runtime-get-pty-record-for-pane-key.ts | 23 ++++-- .../structured-session-mail-target.test.ts | 77 +++++++++++++++++++ .../structured-session-mail-target.ts | 22 +++++- 3 files changed, 111 insertions(+), 11 deletions(-) diff --git a/src/main/runtime/orca-runtime-get-pty-record-for-pane-key.ts b/src/main/runtime/orca-runtime-get-pty-record-for-pane-key.ts index c408451a028..6e9f9593338 100644 --- a/src/main/runtime/orca-runtime-get-pty-record-for-pane-key.ts +++ b/src/main/runtime/orca-runtime-get-pty-record-for-pane-key.ts @@ -12,7 +12,8 @@ import { handleLessCoordinatorSessionId, structuredSessionAddressTarget, structuredSessionMailTarget, - structuredSessionIdleEdgeMailboxes + structuredSessionIdleEdgeMailboxes, + structuredWorkerMailSessionId } from './orchestration/structured-session-mail-target' import { resolveTerminalIdentityFromProbes, @@ -242,8 +243,8 @@ export class OrcaRuntimeWithGetPtyRecordForPaneKey extends OrcaRuntimeWithPruneM if (!assignee) { return null } - const identity = resolveStructuredWorkerAuthority(assignee, this._orchestrationDb)?.identity - return identity ? { sessionId: identity.sessionId, dispatchId } : null + const sessionId = this.liveStructuredWorkerSessionId(assignee) + return sessionId ? { sessionId, dispatchId } : null } /** @@ -266,8 +267,8 @@ export class OrcaRuntimeWithGetPtyRecordForPaneKey extends OrcaRuntimeWithPruneM if (!coordinator) { return null } - const identity = resolveStructuredWorkerAuthority(coordinator, this._orchestrationDb)?.identity - return identity ? { sessionId: identity.sessionId, dispatchId: null } : null + const workerSessionId = this.liveStructuredWorkerSessionId(coordinator) + return workerSessionId ? { sessionId: workerSessionId, dispatchId: null } : null } /** @@ -289,11 +290,19 @@ export class OrcaRuntimeWithGetPtyRecordForPaneKey extends OrcaRuntimeWithPruneM // Answers null for anything that is not a live structured worker of THIS runtime, so `run:` // and PTY handles fall through to the PTY lane exactly as before. const identity = resolveStructuredWorkerAuthority(handle, db)?.identity - if (!identity) { + const sessionId = identity ? structuredWorkerMailSessionId(identity.sessionId) : null + if (!identity || !sessionId) { return null } const dispatchId = db?.findActiveDispatchForAssignee?.(handle, identity.paneKey)?.id ?? null - return { sessionId: identity.sessionId, dispatchId } + return { sessionId, dispatchId } + } + + /** The live session behind a structured worker handle of this runtime; see + * `structuredWorkerMailSessionId`. */ + private liveStructuredWorkerSessionId(handle: string): string | null { + const identity = resolveStructuredWorkerAuthority(handle, this._orchestrationDb)?.identity + return identity ? structuredWorkerMailSessionId(identity.sessionId) : null } protected scheduleRestoredMessageRepoints(): void { diff --git a/src/main/runtime/orchestration/structured-session-mail-target.test.ts b/src/main/runtime/orchestration/structured-session-mail-target.test.ts index 743b729d987..5ed9c1ce26f 100644 --- a/src/main/runtime/orchestration/structured-session-mail-target.test.ts +++ b/src/main/runtime/orchestration/structured-session-mail-target.test.ts @@ -29,6 +29,7 @@ const { resolveOrcaSessionParty } = await import('./orchestration-party') const { mintStructuredWorkerHandle, mintStructuredWorkerPaneKey, + structuredWorkerIdentities, structuredWorkerProcessIncarnation } = await import('../structured-worker-identity') @@ -452,3 +453,79 @@ describe('a coordinator chat continued by /clear', () => { expect(db.getMessageById(direct.id)).toMatchObject({ to_handle: CHAT_ADDRESS, read: 0 }) }) }) + +describe('a structured worker continued by /clear', () => { + const WORKER = testOrcaSessionId('9c2e4a61-3f7b-4d8e-b105-6a2d8e4f1c93') + const WORKER_SUCCESSOR = testOrcaSessionId('clear-a1b2c3d4e5f60718293a4b5c6d7e8f9012345678') + + afterEach(() => { + structuredWorkerIdentities.clear() + }) + + /** A worker minted for WORKER, whose conversation `/clear` continued in WORKER_SUCCESSOR. */ + function clearedWorker(): { handle: string; paneKey: string } { + const store = installStore(null) + const minted = agentSessionRecordFixture( + agentSessionLeaseFixture({ sessionId: WORKER, runtimeKind: 'native' }) + ) + store.records.set(WORKER, { + ...minted, + conversationCommand: { + command: 'clear', + state: 'completed', + replacementSessionId: WORKER_SUCCESSOR, + operationId: 'op-worker', + callerKey: 'caller', + phase: 'committed' + } + }) + store.records.set( + WORKER_SUCCESSOR, + agentSessionRecordFixture( + agentSessionLeaseFixture({ sessionId: WORKER_SUCCESSOR, runtimeKind: 'native' }) + ) + ) + const identity = structuredWorkerIdentities.register({ + handle: mintStructuredWorkerHandle(), + sessionId: WORKER, + agent: 'codex', + paneKey: mintStructuredWorkerPaneKey(WORKER), + processIncarnation: structuredWorkerProcessIncarnation(WORKER), + worktreeId: 'wt_1', + hostScope: { kind: 'local', hostId: 'local' } + }) + return { handle: identity.handle, paneKey: identity.paneKey } + } + + it("delivers mail at the worker's handle to the live successor, as a terminal keeps its handle", () => { + // The strand this pins: the handle resolved to the session minted for it, which `/clear` + // replaced, so the worker's own mail was pointed at a session that no longer runs its turns. + const { handle } = clearedWorker() + expect(probe().target(handle)).toEqual({ sessionId: WORKER_SUCCESSOR, dispatchId: null }) + }) + + it("delivers the worker's dispatch mailbox and a Run it coordinates to the live successor", () => { + const { handle, paneKey } = clearedWorker() + const dispatch = db.createDispatchContext({ + taskId: db.createTask({ runId: chatCoordinatedRun(), spec: 'work' }).id, + assigneeHandle: handle, + assigneePaneKey: paneKey, + processIncarnation: structuredWorkerProcessIncarnation(WORKER), + creator: { kind: 'system' }, + maxDepth: Number.MAX_SAFE_INTEGER + }) + expect(probe().target(`dispatch:${dispatch.id}`)).toEqual({ + sessionId: WORKER_SUCCESSOR, + dispatchId: dispatch.id + }) + const workerRun = db.createRun({ + objective: 'o', + coordinatorHandle: handle, + coordinatorPaneKey: paneKey + }).id + expect(probe().target(`run:${workerRun}`)).toEqual({ + sessionId: WORKER_SUCCESSOR, + dispatchId: null + }) + }) +}) diff --git a/src/main/runtime/orchestration/structured-session-mail-target.ts b/src/main/runtime/orchestration/structured-session-mail-target.ts index 23a0d1e5555..dd6ad617ba4 100644 --- a/src/main/runtime/orchestration/structured-session-mail-target.ts +++ b/src/main/runtime/orchestration/structured-session-mail-target.ts @@ -1,8 +1,8 @@ /** - * Where a mailbox owned by a structured session is delivered, for sessions that are not structured - * workers: a chat that coordinates a Run (`run:` with no coordinator handle) and a session - * addressed directly at `session:`. The session is resolved here, never a pane, and takes the - * pointer as a session turn. + * Where a mailbox owned by a structured session is delivered: a chat that coordinates a Run + * (`run:` with no coordinator handle), a session addressed directly at `session:`, and the + * live session behind a structured worker's handle. The session is resolved here, never a pane, and + * takes the pointer as a session turn. */ import { @@ -13,12 +13,14 @@ import { } from '../../../shared/orca-session-address' import type { OrchestrationDb } from './db' import { currentRunCoordinatorOrcaSessionId } from './db/runs/run-coordinator-orca-session' +import { structuredWorkerHostScope } from '../structured-worker-identity' import type { StructuredPointerTarget } from './structured-mailbox-pointer-delivery' import { addressableSessionParty, structuredSessionMailReach } from './structured-session-mail-address' import { + lineageLiveSession, readAgentSessionRecordStore, type AgentSessionRecordReader } from './structured-session-lineage' @@ -61,6 +63,18 @@ export function structuredSessionMailTarget( : null } +/** + * The session a structured worker's mail reaches: the one minted for it, or that session's live + * `/clear` successor, which carries on as the worker the way a terminal keeps its handle. + */ +export function structuredWorkerMailSessionId( + mintedSessionId: string, + store: AgentSessionRecordReader | null = readAgentSessionRecordStore() +): string | null { + const live = store ? lineageLiveSession(store, mintedSessionId) : null + return live && structuredWorkerHostScope(live.location) ? live.sessionId : null +} + /** * The target of a `session:` mailbox; `undefined` when the handle is not a session address at * all, so other address forms keep their own resolution.