diff --git a/README.md b/README.md index 5f02638d9c4..f0b3f1df6a3 100644 --- a/README.md +++ b/README.md @@ -194,7 +194,6 @@ Works with **any CLI agent** — if it runs in a terminal, it runs in Orca. Charm logo Charm   Cline logo Cline   Codebuff logo Codebuff   - Freebuff logo Freebuff   Command Code logo Command Code   Continue logo Continue   Droid logo Droid   diff --git a/config/scripts/locale-translation-policy.mjs b/config/scripts/locale-translation-policy.mjs index 75f50d15ff6..af88805b8c4 100644 --- a/config/scripts/locale-translation-policy.mjs +++ b/config/scripts/locale-translation-policy.mjs @@ -38,7 +38,6 @@ export const NEVER_TRANSLATE_VALUES = new Set([ 'Claude Agent Teams', 'Cline', 'Codebuff', - 'Freebuff', 'Codex', 'Command Code', 'Cursor', diff --git a/config/scripts/mobile-web-app-session-terminal-closure.test.mjs b/config/scripts/mobile-web-app-session-terminal-closure.test.mjs index 8fd2842687e..9c677111ba3 100644 --- a/config/scripts/mobile-web-app-session-terminal-closure.test.mjs +++ b/config/scripts/mobile-web-app-session-terminal-closure.test.mjs @@ -432,8 +432,21 @@ const MERMAID_PACKAGE = 'node_modules/mermaid/' * * modules 4216 -> 4218 (+2) * local modules 1030 -> 1032 (+2) + * + * #22452 (`80f5aae0f9`) then added `src/shared/main-agent-status.ts` and + * `src/shared/agent-turn-outcome.ts`, which `agent-status-types.ts` on this route imports. Two local + * modules; the change was src/shared-only, so its own CI never ran this suite and main read two over. + * + * modules 4218 -> 4220 (+2) + * local modules 1032 -> 1034 (+2) + * + * Reverting #18790 then took `src/shared/agent-icons/freebuff.png` back out of + * `mobile-agent-icon-assets.ts`, undoing the one module #22119 pinned for it. Measured on the revert. + * + * modules 4220 -> 4219 (-1) + * local modules 1034 -> 1033 (-1) */ -const SESSION_ROUTE_MODULES = 4218 +const SESSION_ROUTE_MODULES = 4219 /** What the page enters this route through once the route is a switch with a `.web.tsx` sibling. */ const ROUTE_ENTRY = [ diff --git a/docs/readme/README.es.md b/docs/readme/README.es.md index 27075756236..65fbd425a92 100644 --- a/docs/readme/README.es.md +++ b/docs/readme/README.es.md @@ -192,7 +192,6 @@ Funciona con **cualquier agente CLI** — si corre en una terminal, corre en Orc Charm logo Charm   Cline logo Cline   Codebuff logo Codebuff   - Freebuff logo Freebuff   Command Code logo Command Code   Continue logo Continue   Droid logo Droid   diff --git a/docs/readme/README.fr.md b/docs/readme/README.fr.md index b9e91dc9cea..4d1e439d8b5 100644 --- a/docs/readme/README.fr.md +++ b/docs/readme/README.fr.md @@ -198,7 +198,6 @@ Fonctionne avec **n'importe quel agent CLI** — s'il tourne dans un terminal, i Logo Charm Charm   Logo Cline Cline   Logo Codebuff Codebuff   - Logo Freebuff Freebuff   Logo Command Code Command Code   Logo Continue Continue   Logo Droid Droid   diff --git a/docs/readme/README.ja.md b/docs/readme/README.ja.md index ab5ea12e5fb..324605b87fb 100644 --- a/docs/readme/README.ja.md +++ b/docs/readme/README.ja.md @@ -192,7 +192,6 @@ PR、Issue、プロジェクトボードをアプリ内で閲覧 — 任意の Charm logo Charm   Cline logo Cline   Codebuff logo Codebuff   - Freebuff logo Freebuff   Command Code logo Command Code   Continue logo Continue   Droid logo Droid   diff --git a/docs/readme/README.ko.md b/docs/readme/README.ko.md index da25a1c948e..451f0d88399 100644 --- a/docs/readme/README.ko.md +++ b/docs/readme/README.ko.md @@ -194,7 +194,6 @@ diff의 어느 줄에든 코멘트를 남기고 에이전트에게 바로 보내 Charm logo Charm   Cline logo Cline   Codebuff logo Codebuff   - Freebuff logo Freebuff   Command Code logo Command Code   Continue logo Continue   Droid logo Droid   diff --git a/docs/readme/README.pt.md b/docs/readme/README.pt.md index 53524927393..9d0cb70a793 100644 --- a/docs/readme/README.pt.md +++ b/docs/readme/README.pt.md @@ -194,7 +194,6 @@ Funciona com **qualquer agente CLI** — se roda em um terminal, roda no Orca. Logotipo do Charm Charm   Logotipo do Cline Cline   Logotipo do Codebuff Codebuff   - Logotipo do Freebuff Freebuff   Logotipo do Command Code Command Code   Logotipo do Continue Continue   Logotipo do Droid Droid   diff --git a/docs/readme/README.zh-CN.md b/docs/readme/README.zh-CN.md index 9cbced81b21..d1125d4410a 100644 --- a/docs/readme/README.zh-CN.md +++ b/docs/readme/README.zh-CN.md @@ -192,7 +192,6 @@ VS Code 的编辑器,处处自动保存 — 把文件或图片直接拖入智 Charm logo Charm   Cline logo Cline   Codebuff logo Codebuff   - Freebuff logo Freebuff   Command Code logo Command Code   Continue logo Continue   Droid logo Droid   diff --git a/docs/site/content/docs/agents/supported.mdx b/docs/site/content/docs/agents/supported.mdx index f5774c11f18..8f93ec6bb4e 100644 --- a/docs/site/content/docs/agents/supported.mdx +++ b/docs/site/content/docs/agents/supported.mdx @@ -46,7 +46,6 @@ To restore prompts for one agent only, edit that agent's default arguments or en | Autohand | Auto-setup | [Autohand](https://github.com/autohandai/code-cli) | | Cline | Auto-setup | [Cline](https://docs.cline.bot/cline-cli/overview) | | Codebuff | Auto-setup | [Codebuff](https://www.codebuff.com/docs/help/quick-start) | -| Freebuff | Auto-setup | [Freebuff](https://freebuff.com/cli) | | Command Code | Auto-setup, status | [Command Code](https://commandcode.ai/docs/quickstart) | | Muse | macOS/Linux; trusts the workspace at launch | [Meta](https://dev.meta.ai/docs/muse-code) | | Continue | Auto-setup | [Continue](https://docs.continue.dev/guides/cli) | diff --git a/mobile/src/components/mobile-agent-icon-assets.ts b/mobile/src/components/mobile-agent-icon-assets.ts index ee07291b3b6..791364bf5e2 100644 --- a/mobile/src/components/mobile-agent-icon-assets.ts +++ b/mobile/src/components/mobile-agent-icon-assets.ts @@ -29,7 +29,6 @@ export const MOBILE_AGENT_ICON_ASSETS: Partial> autohand: 'autohand.ai', cline: 'cline.bot', codebuff: 'codebuff.com', - freebuff: 'freebuff.com', 'command-code': 'commandcode.ai', continue: 'continue.dev', cursor: 'cursor.com', diff --git a/src/main/runtime/orca-runtime-get-orchestration-dispatch-authority.ts b/src/main/runtime/orca-runtime-get-orchestration-dispatch-authority.ts index 5eb0bffb8e8..d61374c3466 100644 --- a/src/main/runtime/orca-runtime-get-orchestration-dispatch-authority.ts +++ b/src/main/runtime/orca-runtime-get-orchestration-dispatch-authority.ts @@ -22,7 +22,6 @@ import { getAppEnvironment } from '../../shared/app-environment' import type { FleetAgentStatusEvidence } from '../../shared/orchestration-fleet-agent-status-evidence' import { readOrchestrationFleetAgentStatusSnapshot } from './orchestration-fleet-agent-status-snapshot' import { resolveStructuredWorkerAuthority } from './structured-worker-authority' -import { matchesProcessIncarnation } from './orchestration/worker-terminal-process-liveness' export class OrcaRuntimeWithGetOrchestrationDispatchAuthority extends OrcaRuntimeWithVerifyOrchestrationCompatibilityCaller { /** Every pane key this PTY could be addressed by, including restored receipts. */ @@ -271,38 +270,4 @@ export class OrcaRuntimeWithGetOrchestrationDispatchAuthority extends OrcaRuntim : undefined }) } - /** - * Recover a live terminal handle for a worker whose durable handle stopped resolving - * (renderer graph epoch bump / handle invalidation) while its PTY is still tracked. Fences on - * the recorded process incarnation EXACTLY — never a bare ptyId, worktree, or pane — so a - * reused ptyId belonging to a different process can never be closed, and fails closed on an - * unknown host scope (this is also consumed by workerShow, which does no lease re-check). - * Returns a freshly minted live handle, or null when no live PTY carries that exact incarnation. - */ - resolveTerminalHandleByProcessIncarnation( - processIncarnation: string, - serializedHostScope: string | null - ): string | null { - if (!processIncarnation || !serializedHostScope) { - return null - } - // Scan by the incarnation itself (startsWith + exact equality, mirroring - // classifyWorkerTerminalProcessIncarnation) rather than splitting on a colon, so relay/SSH - // ptyIds and colon-bearing incarnationIds still match. A pty with no incarnationId can never - // match, so the legacy `${runtimeId}:${ptyId}:${ptyGeneration}` fence stays fail-closed. - for (const [ptyId, pty] of this.ptysById) { - if (!matchesProcessIncarnation(ptyId, pty.incarnationId, processIncarnation)) { - continue - } - const hostScope = this.getOrchestrationCompatibilityHostScope(pty) - if (!hostScope || JSON.stringify(hostScope) !== serializedHostScope) { - // Keep scanning: a colon-ambiguous decoy pty in a different host scope that this - // incarnation string happens to prefix-match must not suppress the genuine same-scope - // pty later in ptysById. The scope check still fences the real match below. - continue - } - return this.issuePtyHandle(pty) - } - return null - } } diff --git a/src/main/runtime/orca-runtime-terminal-handle-incarnation.test.ts b/src/main/runtime/orca-runtime-terminal-handle-incarnation.test.ts index 66b07f0b3d8..bac59e614b2 100644 --- a/src/main/runtime/orca-runtime-terminal-handle-incarnation.test.ts +++ b/src/main/runtime/orca-runtime-terminal-handle-incarnation.test.ts @@ -1,13 +1,11 @@ import { describe, expect, it, vi } from 'vitest' import { OrcaRuntimeService } from './orca-runtime' -import { makePaneKey } from '../../shared/stable-pane-id' const PTY_ID = 'ssh:target@@relay-pty' const WORKTREE_ID = 'repo::/worktree' const TAB_ID = 'tab-terminal' const LEAF_ID = '11111111-1111-4111-8111-111111111111' -/** A runtime whose pty controller captures every write for assertion. */ function makeRuntime(): { runtime: OrcaRuntimeService; writes: string[] } { const writes: string[] = [] const runtime = new OrcaRuntimeService(null) @@ -22,7 +20,6 @@ function makeRuntime(): { runtime: OrcaRuntimeService; writes: string[] } { return { runtime, writes } } -/** Attach a window and publish a one-tab, one-leaf terminal graph. */ function syncGraph(runtime: OrcaRuntimeService): void { runtime.attachWindow(1) runtime.syncWindowGraph(1, { @@ -47,7 +44,6 @@ function syncGraph(runtime: OrcaRuntimeService): void { }) } -/** (Re)register the fixture pty leaf under the given incarnation id. */ function register(runtime: OrcaRuntimeService, incarnationId: string): void { runtime.registerPty(PTY_ID, WORKTREE_ID, 'target', { tabId: TAB_ID, @@ -70,8 +66,6 @@ describe('runtime terminal handle incarnation fencing', () => { inspectProcess }) expect(runtime.markRendererReloading(1)).not.toBeNull() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. expect((runtime as unknown as { handles: Map }).handles.has(handle)).toBe( false ) @@ -95,8 +89,6 @@ describe('runtime terminal handle incarnation fencing', () => { }) }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. it('treats a null-to-known incarnation as the same un-fenced PTY', async () => { const { runtime } = makeRuntime() const handle = runtime.preAllocateHandleForPty(PTY_ID) @@ -140,8 +132,6 @@ describe('runtime terminal handle incarnation fencing', () => { // Rotate the record directly so reconcile is the only fence exercised. // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: test reaches the runtime's protected pty record map to bypass the registerPty fence. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. const internals = runtime as unknown as { ptysById: Map } @@ -225,8 +215,6 @@ describe('runtime terminal handle incarnation fencing', () => { runtime.registerPreAllocatedHandleForPty(PTY_ID, replacementHandle) syncGraph(runtime) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. const internals = runtime as unknown as { handles: Map handleByLeafKey: Map @@ -249,140 +237,3 @@ describe('runtime terminal handle incarnation fencing', () => { }) }) }) - -describe('resolveTerminalHandleByProcessIncarnation direct fencing', () => { - const LOCAL_SCOPE = JSON.stringify({ kind: 'local', hostId: 'local' }) - // PTY_ID ('ssh:target@@relay-pty') derives connectionId 'target' via parseAppSshPtyId, so its - // host scope is ssh:target rather than local. - const SSH_TARGET_SCOPE = JSON.stringify({ kind: 'ssh', targetId: 'target' }) - - /** Register a pty leaf directly, optionally under a connection and incarnation. */ - function seedPty( - runtime: OrcaRuntimeService, - ptyId: string, - incarnationId: string | null, - connectionId: string | null = null - ): void { - runtime.registerPty(ptyId, WORKTREE_ID, connectionId, { - tabId: TAB_ID, - leafId: LEAF_ID, - ...(incarnationId ? { incarnationId } : {}) - }) - } - - /** Call the private resolveTerminalHandleByProcessIncarnation on the runtime. */ - function resolve( - runtime: OrcaRuntimeService, - processIncarnation: string, - serializedHostScope: string | null - ): string | null { - return ( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - runtime as unknown as { - resolveTerminalHandleByProcessIncarnation( - processIncarnation: string, - serializedHostScope: string | null - ): string | null - } - ).resolveTerminalHandleByProcessIncarnation(processIncarnation, serializedHostScope) - ) - } - - it('mints a live handle for a pty whose exact incarnation and host scope match', () => { - const { runtime } = makeRuntime() - seedPty(runtime, PTY_ID, 'incarnation-1') - const handle = resolve(runtime, `${PTY_ID}:incarnation-1`, SSH_TARGET_SCOPE) - expect(handle).toMatch(/^term_/) - // Re-minting the same live pty is idempotent. - expect(resolve(runtime, `${PTY_ID}:incarnation-1`, SSH_TARGET_SCOPE)).toBe(handle) - }) - - it('returns null when the recorded incarnationId no longer matches the live pty', () => { - const { runtime } = makeRuntime() - seedPty(runtime, PTY_ID, 'incarnation-1') - expect(resolve(runtime, `${PTY_ID}:incarnation-2`, LOCAL_SCOPE)).toBeNull() - }) - - it('returns null when the live pty has no incarnationId (legacy runtimeId:ptyId:gen never reaps)', () => { - const { runtime } = makeRuntime() - seedPty(runtime, PTY_ID, null) - // A modern-shaped probe cannot match a pty that carries no incarnationId... - expect(resolve(runtime, `${PTY_ID}:incarnation-1`, LOCAL_SCOPE)).toBeNull() - // ...and the legacy `${runtimeId}:${ptyId}:${ptyGeneration}` shape stays fail-closed rather - // than reap on a ptyGeneration guess. - expect(resolve(runtime, `runtime_test:${PTY_ID}:0`, LOCAL_SCOPE)).toBeNull() - }) - - it('returns null when the host scope does not match', () => { - const { runtime } = makeRuntime() - seedPty(runtime, PTY_ID, 'incarnation-1') - expect( - resolve(runtime, `${PTY_ID}:incarnation-1`, JSON.stringify({ kind: 'ssh', targetId: 'nope' })) - ).toBeNull() - }) - - it('returns null when no host scope is supplied (fails closed)', () => { - const { runtime } = makeRuntime() - seedPty(runtime, PTY_ID, 'incarnation-1') - expect(resolve(runtime, `${PTY_ID}:incarnation-1`, null)).toBeNull() - }) - - it('resolves a colon-bearing SSH/relay incarnationId that lastIndexOf would mis-split', () => { - const { runtime } = makeRuntime() - // PTY_ID already carries ':' and '@@'; the incarnationId itself also carries colons. - seedPty(runtime, PTY_ID, 'relay:conn-3:incarnation-9', 'target') - const handle = resolve( - runtime, - `${PTY_ID}:relay:conn-3:incarnation-9`, - JSON.stringify({ kind: 'ssh', targetId: 'target' }) - ) - expect(handle).toMatch(/^term_/) - }) - - it('resolves a Windows repo::C:\\path@@1 ptyId', () => { - const { runtime } = makeRuntime() - const windowsPtyId = 'repo::C:\\path@@1' - seedPty(runtime, windowsPtyId, 'incarnation-win') - const handle = resolve(runtime, `${windowsPtyId}:incarnation-win`, LOCAL_SCOPE) - expect(handle).toMatch(/^term_/) - }) - - it('keeps scanning past a colon-ambiguous decoy in another host scope to the genuine match', () => { - const { runtime } = makeRuntime() - // One process-incarnation string, two colon-ambiguous pty ids that both satisfy the exact - // matcher: 'relay' + 'conn:incarnation-1' AND 'relay:conn' + 'incarnation-1' both stringify to - // 'relay:conn:incarnation-1'. The decoy is registered FIRST and lives in a different host scope - // (ssh:decoy); the genuine pty is local and registered later. - const processIncarnation = 'relay:conn:incarnation-1' - const DECOY_TAB_ID = 'tab-decoy' - const DECOY_LEAF_ID = '22222222-2222-4222-8222-222222222222' - const GENUINE_TAB_ID = 'tab-genuine' - const GENUINE_LEAF_ID = '33333333-3333-4333-8333-333333333333' - runtime.registerPty('relay', WORKTREE_ID, 'decoy', { - tabId: DECOY_TAB_ID, - leafId: DECOY_LEAF_ID, - incarnationId: 'conn:incarnation-1' - }) - runtime.registerPty('relay:conn', WORKTREE_ID, null, { - tabId: GENUINE_TAB_ID, - leafId: GENUINE_LEAF_ID, - incarnationId: 'incarnation-1' - }) - - // Before the fix, the earlier decoy's host-scope mismatch returned null and suppressed the - // genuine same-scope pty entirely. `continue` lets the scan reach it. - const handle = resolve(runtime, processIncarnation, LOCAL_SCOPE) - expect(handle).toMatch(/^term_/) - // getTerminalProcessIncarnation cannot separate the two (both stringify to - // 'relay:conn:incarnation-1'), so pin the remint to the GENUINE pane. The paneKey is the only - // terminal-specific observable that distinguishes the genuine leaf from the same-incarnation - // decoy; this fails if the resolver ever mints the decoy's handle instead of the genuine pty's. - const genuinePaneKey = makePaneKey(GENUINE_TAB_ID, GENUINE_LEAF_ID) - const decoyPaneKey = makePaneKey(DECOY_TAB_ID, DECOY_LEAF_ID) - expect(runtime.getTerminalPaneKey(handle!)).toBe(genuinePaneKey) - expect(runtime.getTerminalPaneKey(handle!)).not.toBe(decoyPaneKey) - }) -}) diff --git a/src/main/runtime/orchestration/worker-terminal-process-liveness.test.ts b/src/main/runtime/orchestration/worker-terminal-process-liveness.test.ts deleted file mode 100644 index e2bc2cc7d63..00000000000 --- a/src/main/runtime/orchestration/worker-terminal-process-liveness.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { matchesProcessIncarnation } from './worker-terminal-process-liveness' - -describe('matchesProcessIncarnation', () => { - it.each([ - { - name: 'an exact ptyId:incarnation match', - ptyId: 'pty-1', - incarnationId: 'incarnation-1', - processIncarnation: 'pty-1:incarnation-1', - expected: true - }, - { - name: 'a Windows repo::C:\\path@@1 ptyId whose incarnation matches', - ptyId: 'repo::C:\\path@@1', - incarnationId: 'incarnation-win', - processIncarnation: 'repo::C:\\path@@1:incarnation-win', - expected: true - }, - { - name: 'a colon-bearing relay incarnationId that a lastIndexOf split would mangle', - ptyId: 'relay-pty', - incarnationId: 'relay:conn-3:incarnation-9', - processIncarnation: 'relay-pty:relay:conn-3:incarnation-9', - expected: true - }, - { - name: 'a whitespace-dirty incarnationId (lost contact is never a match)', - ptyId: 'pty-1', - incarnationId: ' incarnation-1', - processIncarnation: 'pty-1: incarnation-1', - expected: false - }, - { - name: 'an empty-string incarnationId', - ptyId: 'pty-1', - incarnationId: '', - processIncarnation: 'pty-1:', - expected: false - }, - { - name: 'an absent (null) incarnationId', - ptyId: 'pty-1', - incarnationId: null, - processIncarnation: 'pty-1:incarnation-1', - expected: false - }, - { - name: 'an absent (undefined) incarnationId', - ptyId: 'pty-1', - incarnationId: undefined, - processIncarnation: 'pty-1:incarnation-1', - expected: false - }, - { - name: 'a prefix-decoy ptyId (@@1) against a longer live pty (@@10)', - ptyId: 'repo::C:\\path@@1', - incarnationId: 'inc-1', - processIncarnation: 'repo::C:\\path@@10:inc-1', - expected: false - } - ])('returns $expected for $name', ({ ptyId, incarnationId, processIncarnation, expected }) => { - expect(matchesProcessIncarnation(ptyId, incarnationId, processIncarnation)).toBe(expected) - }) - - it('rejects a partial prefix that is not colon-delimited', () => { - // 'pty-10' is not the pty 'pty-1'; the `${ptyId}:` fence keeps them distinct. - expect(matchesProcessIncarnation('pty-1', 'inc', 'pty-10:inc')).toBe(false) - }) -}) diff --git a/src/main/runtime/orchestration/worker-terminal-process-liveness.ts b/src/main/runtime/orchestration/worker-terminal-process-liveness.ts index 4e652d6286a..72c5ce07666 100644 --- a/src/main/runtime/orchestration/worker-terminal-process-liveness.ts +++ b/src/main/runtime/orchestration/worker-terminal-process-liveness.ts @@ -5,24 +5,6 @@ import type { PtyProcessInfo } from '../../providers/pty-process-info' export type { WorkerTerminalHostScope } from '../../../shared/worker-terminal-host-scope' export { parseWorkerTerminalHostScope } from '../../../shared/worker-terminal-host-scope' -/** - * Does `processIncarnation` name exactly this pty's live incarnation? Requires exact - * `${ptyId}:${incarnationId}` equality, so it is immune to colons on either side (relay/SSH - * ptyIds, colon-bearing relay incarnationIds). A pty with no (or a whitespace-dirty) - * incarnationId can never match — the exact-incarnation fence stays intact. - */ -export function matchesProcessIncarnation( - ptyId: string, - incarnationId: string | null | undefined, - processIncarnation: string -): boolean { - if (!incarnationId || incarnationId !== incarnationId.trim()) { - return false - } - return `${ptyId}:${incarnationId}` === processIncarnation -} - -/** Classify a recorded incarnation against live sessions: live on exact match, unverifiable when a candidate pty has a dirty or absent incarnationId (lost contact is never a death certificate), else exited. */ export function classifyWorkerTerminalProcessIncarnation( processIncarnation: string, sessions: readonly PtyProcessInfo[] @@ -31,9 +13,13 @@ export function classifyWorkerTerminalProcessIncarnation( processIncarnation.startsWith(`${session.id}:`) ) if ( - possibleMatches.some((session) => - matchesProcessIncarnation(session.id, session.incarnationId, processIncarnation) - ) + possibleMatches.some((session) => { + const incarnationId = session.incarnationId + if (!incarnationId || incarnationId !== incarnationId.trim()) { + return false + } + return `${session.id}:${incarnationId}` === processIncarnation + }) ) { return 'live' } diff --git a/src/main/runtime/rpc/methods/orchestration-worker-release-incarnation-fallback.test.ts b/src/main/runtime/rpc/methods/orchestration-worker-release-incarnation-fallback.test.ts deleted file mode 100644 index a9e9ceb165c..00000000000 --- a/src/main/runtime/rpc/methods/orchestration-worker-release-incarnation-fallback.test.ts +++ /dev/null @@ -1,622 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' -import { ORCHESTRATION_METHODS } from './orchestration' -import { eraseRpcMethods, type RpcContext } from '../core' -import { OrchestrationDb } from '../../orchestration/db' -import { OrcaRuntimeService } from '../../orca-runtime' -import { completeWorkerTerminalRelease } from './orchestration/worker/worker-release-completion' - -describe('orchestration worker release incarnation fallback', () => { - let db: OrchestrationDb - let dbOpen = false - let runtime: OrcaRuntimeService - let ctx: RpcContext - let activeRunId: string - let inspectProcessLiveness: ReturnType - - const coordinatorPaneKey = 'tab_coord:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa' - const workerPaneKey = 'tab_worker:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb' - - /** Fresh in-memory db and a fully-stubbed runtime for one worker-release scenario. */ - function setup(): void { - db = new OrchestrationDb(':memory:') - dbOpen = true - runtime = new OrcaRuntimeService() - runtime.setOrchestrationDb(db) - inspectProcessLiveness = vi.fn().mockResolvedValue('live') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This test fixture is deliberately shaped to exercise the private/runtime boundary. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ;// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - runtime as unknown as { - inspectTerminalProcessIncarnationLiveness: typeof inspectProcessLiveness - } - ).inspectTerminalProcessIncarnationLiveness = inspectProcessLiveness - vi.spyOn(runtime, 'getTerminalPaneKey').mockImplementation((handle) => - handle === 'term_coord' - ? coordinatorPaneKey - : handle === 'term_worker' || handle === 'term_reminted' - ? workerPaneKey - : null - ) - vi.spyOn(runtime, 'getTerminalProcessIncarnation').mockImplementation((handle) => - handle === 'term_worker' || handle === 'term_reminted' ? 'runtime_test:term_worker:1' : null - ) - vi.spyOn(runtime, 'getOrchestrationDispatchAuthority').mockImplementation((handle) => - handle === 'term_worker' || handle === 'term_reminted' - ? // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This test fixture is deliberately shaped to exercise the private/runtime boundary. - ({ - terminalHandle: handle, - paneKey: workerPaneKey, - processIncarnation: 'runtime_test:term_worker:1', - hostScope: { kind: 'local', hostId: 'local' } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - } as never) - : null - ) - vi.spyOn(runtime, 'validateOrchestrationAgentLauncher').mockImplementation(() => {}) - vi.spyOn(runtime, 'showTerminal').mockImplementation( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - async (handle) => ({ handle, worktreeId: 'repo::worktree', status: 'running' }) as never - ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - vi.spyOn(runtime, 'showManagedTerminalWorkspace').mockResolvedValue({ - id: 'repo::worktree' - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - } as never) - vi.spyOn(runtime, 'createTerminal').mockResolvedValue({ - handle: 'term_worker', - worktreeId: 'repo::worktree', - title: 'worker' - }) - vi.spyOn(runtime, 'waitForTerminal').mockResolvedValue({ - handle: 'term_worker', - condition: 'tui-idle', - satisfied: true, - status: 'running', - exitCode: null - }) - vi.spyOn(runtime, 'getTerminalOrchestrationCliCommand').mockReturnValue('orca') - vi.spyOn(runtime, 'sendTerminalAgentPrompt').mockResolvedValue({ - handle: 'term_worker', - accepted: true, - bytesWritten: 1 - }) - vi.spyOn(runtime, 'isTerminalRunningAgent').mockResolvedValue(true) - vi.spyOn(runtime, 'getExactWorkerProviderSession').mockReturnValue(null) - vi.spyOn(runtime, 'readTerminal').mockResolvedValue({ - handle: 'term_worker', - status: 'running', - tail: ['worker output line 1', 'worker output line 2'], - truncated: false, - nextCursor: '2' - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - vi.spyOn(runtime, 'closeTerminal').mockResolvedValue({ - handle: 'term_worker', - tabId: 'tab-worker', - ptyKilled: true - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - } as never) - vi.spyOn(runtime, 'notifyMessageArrived').mockImplementation(() => {}) - activeRunId = db.createRun({ - objective: 'Release test Run', - coordinatorHandle: 'term_coord', - coordinatorPaneKey - }).id - ctx = { runtime } - } - - afterEach(() => { - if (dbOpen) { - dbOpen = false - db.close() - } - vi.restoreAllMocks() - }) - - /** Look up a registered orchestration RPC method by name. */ - function findMethod(name: string) { - const method = eraseRpcMethods(ORCHESTRATION_METHODS).find((m) => m.name === name) - if (!method) { - throw new Error(`Method not found: ${name}`) - } - return method - } - - /** Parse a method's params and invoke its handler against the shared ctx. */ - async function call(name: string, params: Record) { - const method = findMethod(name) - const parsed = method.params ? method.params.parse(params) : undefined - return method.handler(parsed, ctx) - } - - /** Start a ready worker on a fresh task off the coordinator terminal. */ - async function startWorker(options: { terminal?: string } = {}): Promise<{ - taskId: string - dispatchId: string - }> { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const task = db.createTask({ spec: 'release fixture task', runId: activeRunId }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const result = (await call('orchestration.workerStart', { - task: task.id, - from: 'term_coord', - ...(options.terminal ? { terminal: options.terminal } : { agent: 'codex' }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - })) as { dispatchId: string; state: string } - expect(result.state).toBe('ready') - return { taskId: task.id, dispatchId: result.dispatchId } - } - - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - /** Record the worker's report as settled — the precondition for release. */ - function settle(taskId: string, dispatchId: string, outcome: 'succeeded' | 'failed'): void { - const settlement = db.settleWorkerReport({ - taskId, - dispatchId, - outcome, - result: `worker ${outcome}` - }) - expect(settlement.action).toBe('settled') - } - - /** Start a worker and settle its report, the state a release acts on. */ - async function startSettledWorker( - outcome: 'succeeded' | 'failed' = 'succeeded', - options: { terminal?: string } = {} - ): Promise<{ taskId: string; dispatchId: string }> { - const worker = await startWorker(options) - settle(worker.taskId, worker.dispatchId, outcome) - return worker - } - - it('closes a live worker terminal whose durable handle no longer resolves but whose process incarnation still matches', async () => { - setup() - const { dispatchId } = await startSettledWorker() - // The durable handle stops resolving (renderer graph epoch bump / handle invalidation)... - vi.mocked(runtime.showTerminal).mockImplementation(async (handle) => - handle === 'term_worker' - ? Promise.reject(new Error('terminal_handle_stale')) - : // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This test fixture is deliberately shaped to exercise the private/runtime boundary. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ({ handle, worktreeId: 'repo::worktree', status: 'running' } as never) - ) - // ...but the recorded process incarnation still names a live PTY, re-minted to a fresh handle. - const resolveByIncarnation = vi.fn().mockReturnValue('term_reminted') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ;// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - runtime as unknown as { - resolveTerminalHandleByProcessIncarnation: typeof resolveByIncarnation - } - ).resolveTerminalHandleByProcessIncarnation = resolveByIncarnation - - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const receipt = (await call('orchestration.workerRelease', { dispatch: dispatchId })) as { - state: string - processAction: string - } - - expect(receipt).toMatchObject({ state: 'released', processAction: 'closed_agent_terminal' }) - expect(resolveByIncarnation).toHaveBeenCalledWith( - 'runtime_test:term_worker:1', - JSON.stringify({ kind: 'local', hostId: 'local' }) - ) - // The close targeted exactly the re-minted live handle for that PTY, never the stale one. - expect(runtime.closeTerminal).toHaveBeenCalledTimes(1) - expect(runtime.closeTerminal).toHaveBeenCalledWith('term_reminted') - expect(db.getWorkerTerminalResourceByOwner(dispatchId)).toMatchObject({ - ownership_state: 'released', - release_state: 'released' - }) - }) - - it('stays release_unknown and closes nothing when the recorded incarnation no longer matches a live pty', async () => { - setup() - const { dispatchId } = await startSettledWorker() - vi.mocked(runtime.showTerminal).mockRejectedValue(new Error('terminal_handle_stale')) - // A reused ptyId now belongs to a different process: the incarnation mismatch refuses a close. - const resolveByIncarnation = vi.fn().mockReturnValue(null) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ;// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - runtime as unknown as { - resolveTerminalHandleByProcessIncarnation: typeof resolveByIncarnation - } - ).resolveTerminalHandleByProcessIncarnation = resolveByIncarnation - - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const receipt = (await call('orchestration.workerRelease', { dispatch: dispatchId })) as { - state: string - } - - expect(receipt.state).toBe('release_unknown') - expect(resolveByIncarnation).toHaveBeenCalledWith( - 'runtime_test:term_worker:1', - JSON.stringify({ kind: 'local', hostId: 'local' }) - ) - expect(runtime.closeTerminal).not.toHaveBeenCalled() - expect(db.getWorkerTerminalResourceByOwner(dispatchId)?.release_state).toBe('unknown') - }) - - it('does not plain-settle an exited missing worker when the archive was never committed', async () => { - setup() - const { dispatchId } = await startSettledWorker() - vi.mocked(runtime.showTerminal).mockRejectedValue(new Error('terminal_handle_stale')) - const resolveByIncarnation = vi.fn().mockReturnValue(null) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ;// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - runtime as unknown as { - resolveTerminalHandleByProcessIncarnation: typeof resolveByIncarnation - } - ).resolveTerminalHandleByProcessIncarnation = resolveByIncarnation - inspectProcessLiveness.mockResolvedValue('exited') - - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const receipt = (await call('orchestration.workerRelease', { dispatch: dispatchId })) as { - state: string - processAction: string - } - - expect(receipt).toMatchObject({ state: 'release_unknown', processAction: 'none' }) - expect(inspectProcessLiveness).toHaveBeenCalled() - expect(runtime.closeTerminal).not.toHaveBeenCalled() - expect(db.getWorkerTerminalResourceByOwner(dispatchId)).toMatchObject({ - ownership_state: 'owned', - release_state: 'unknown' - }) - }) - - it('settles released without a close when exited missing and an archive is already committed', async () => { - setup() - const { dispatchId } = await startSettledWorker() - const requested = db.requestWorkerTerminalRelease(dispatchId) - if (requested.disposition !== 'requested') { - throw new Error(`expected requested, got ${requested.disposition}`) - } - db.commitWorkerTerminalArchiveForRelease({ - dispatchId, - resourceId: requested.resource.id, - kind: 'terminal_tail', - content: JSON.stringify({ lines: [] }), - archiveSource: 'terminal', - archiveStatus: 'empty' - }) - vi.mocked(runtime.showTerminal).mockRejectedValue(new Error('terminal_handle_stale')) - const resolveByIncarnation = vi.fn().mockReturnValue(null) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ;// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - runtime as unknown as { - resolveTerminalHandleByProcessIncarnation: typeof resolveByIncarnation - } - ).resolveTerminalHandleByProcessIncarnation = resolveByIncarnation - inspectProcessLiveness.mockResolvedValue('exited') - - const receipt = await completeWorkerTerminalRelease({ - runtime, - db, - dispatchId, - resource: db.getWorkerTerminalResource(requested.resource.id)!, - mode: 'interactive' - }) - - expect(receipt).toMatchObject({ state: 'released', processAction: 'none' }) - expect(runtime.closeTerminal).not.toHaveBeenCalled() - expect(db.getWorkerTerminalResourceByOwner(dispatchId)).toMatchObject({ - ownership_state: 'released', - release_state: 'released' - }) - }) - - it('reaches settleDead before the lease check when a gone worker is exited with no live authority', async () => { - // Without a committed archive settleDead retains; lease must not run first and force - // retained/identity_unproven. Disposition is release_unknown (interactive) after the retain. - setup() - const { dispatchId } = await startSettledWorker() - vi.mocked(runtime.showTerminal).mockRejectedValue(new Error('terminal_handle_stale')) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const resolveByIncarnation = vi.fn().mockReturnValue(null) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ;// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - runtime as unknown as { - resolveTerminalHandleByProcessIncarnation: typeof resolveByIncarnation - } - ).resolveTerminalHandleByProcessIncarnation = resolveByIncarnation - vi.mocked(runtime.getOrchestrationDispatchAuthority).mockReturnValue(null) - inspectProcessLiveness.mockResolvedValue('exited') - - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const receipt = (await call('orchestration.workerRelease', { dispatch: dispatchId })) as { - state: string - processAction: string - } - - expect(receipt).toMatchObject({ state: 'release_unknown', processAction: 'none' }) - expect(receipt.state).not.toBe('retained') - expect(inspectProcessLiveness).toHaveBeenCalled() - expect(runtime.closeTerminal).not.toHaveBeenCalled() - }) - - it('concedes release_unknown before the lease check when a gone worker has no live authority and liveness is unproven', async () => { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - setup() - const { dispatchId } = await startSettledWorker() - vi.mocked(runtime.showTerminal).mockRejectedValue(new Error('terminal_handle_stale')) - const resolveByIncarnation = vi.fn().mockReturnValue(null) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ;// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - runtime as unknown as { - resolveTerminalHandleByProcessIncarnation: typeof resolveByIncarnation - } - ).resolveTerminalHandleByProcessIncarnation = resolveByIncarnation - vi.mocked(runtime.getOrchestrationDispatchAuthority).mockReturnValue(null) - // Liveness is unresolvable/not-exited: the process may have been re-homed, so concede rather - // than retain or guess at a live process. - inspectProcessLiveness.mockResolvedValue('live') - - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const receipt = (await call('orchestration.workerRelease', { dispatch: dispatchId })) as { - state: string - } - - expect(receipt.state).toBe('release_unknown') - expect(receipt.state).not.toBe('retained') - expect(runtime.closeTerminal).not.toHaveBeenCalled() - expect(db.getWorkerTerminalResourceByOwner(dispatchId)?.release_state).toBe('unknown') - }) - - it('workerStop closes a live worker via the reminted handle when the durable handle is stale', async () => { - setup() - const { dispatchId } = await startWorker() - // The durable handle stops resolving, but the recorded incarnation still names a live PTY. - vi.mocked(runtime.showTerminal).mockImplementation(async (handle) => - handle === 'term_worker' - ? Promise.reject(new Error('terminal_handle_stale')) - : // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This test fixture is deliberately shaped to exercise the private/runtime boundary. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ({ handle, worktreeId: 'repo::worktree', status: 'running' } as never) - ) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const resolveByIncarnation = vi.fn().mockReturnValue('term_reminted') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ;// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - runtime as unknown as { - resolveTerminalHandleByProcessIncarnation: typeof resolveByIncarnation - } - ).resolveTerminalHandleByProcessIncarnation = resolveByIncarnation - - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const receipt = (await call('orchestration.workerStop', { dispatch: dispatchId })) as { - processAction: string - } - - expect(receipt.processAction).toBe('closed_agent_terminal') - // The kill targeted exactly the reminted live handle, never the stale durable one — closing - // the stale handle would throw terminal_handle_stale and leak the PTY. - expect(runtime.closeTerminal).toHaveBeenCalledTimes(1) - expect(runtime.closeTerminal).toHaveBeenCalledWith('term_reminted') - }) - - it('workerRead reads a live worker via the reminted handle when the durable handle is stale', async () => { - setup() - const { dispatchId } = await startWorker() - vi.mocked(runtime.showTerminal).mockImplementation(async (handle) => - handle === 'term_worker' - ? Promise.reject(new Error('terminal_handle_stale')) - : // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This test fixture is deliberately shaped to exercise the private/runtime boundary. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ({ handle, worktreeId: 'repo::worktree', status: 'running' } as never) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ) - const resolveByIncarnation = vi.fn().mockReturnValue('term_reminted') - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ;// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - runtime as unknown as { - resolveTerminalHandleByProcessIncarnation: typeof resolveByIncarnation - } - ).resolveTerminalHandleByProcessIncarnation = resolveByIncarnation - - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const output = (await call('orchestration.workerRead', { dispatch: dispatchId })) as { - terminal?: { tail: string[] } - } - - // Both the exact-session probe and the terminal read addressed the reminted handle. - expect(runtime.getExactWorkerProviderSession).toHaveBeenCalledWith( - 'term_reminted', - expect.any(Number) - ) - expect(runtime.readTerminal).toHaveBeenCalledWith('term_reminted', expect.anything()) - expect(output.terminal?.tail).toEqual(['worker output line 1', 'worker output line 2']) - }) - - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - it('recovery-mode: defers when exited missing has no archive rather than plain-settling', async () => { - // Proof of death still runs settleDead first; when it retains (no archive), recovery must - // stay release_pending — not plain-settle, not unknown. - setup() - const { dispatchId } = await startSettledWorker() - vi.mocked(runtime.showTerminal).mockRejectedValue(new Error('terminal_handle_stale')) - const resolveByIncarnation = vi.fn().mockReturnValue(null) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ;// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - runtime as unknown as { - resolveTerminalHandleByProcessIncarnation: typeof resolveByIncarnation - } - ).resolveTerminalHandleByProcessIncarnation = resolveByIncarnation - inspectProcessLiveness.mockResolvedValue('exited') - const requested = db.requestWorkerTerminalRelease(dispatchId) - if (requested.disposition !== 'requested') { - throw new Error(`expected a requested release, got ${requested.disposition}`) - } - - const receipt = await completeWorkerTerminalRelease({ - runtime, - db, - dispatchId, - resource: requested.resource, - mode: 'recovery' - }) - - expect(receipt).toMatchObject({ state: 'release_pending', processAction: 'none' }) - expect(runtime.closeTerminal).not.toHaveBeenCalled() - expect(db.getWorkerTerminalResourceByOwner(dispatchId)).toMatchObject({ - ownership_state: 'owned', - release_state: 'requested' - }) - }) - - it('recovery-mode: settles released before the defer when exited missing already has an archive', async () => { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - setup() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const { dispatchId } = await startSettledWorker() - vi.mocked(runtime.showTerminal).mockRejectedValue(new Error('terminal_handle_stale')) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const resolveByIncarnation = vi.fn().mockReturnValue(null) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ;// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - runtime as unknown as { - resolveTerminalHandleByProcessIncarnation: typeof resolveByIncarnation - } - ).resolveTerminalHandleByProcessIncarnation = resolveByIncarnation - inspectProcessLiveness.mockResolvedValue('exited') - const requested = db.requestWorkerTerminalRelease(dispatchId) - if (requested.disposition !== 'requested') { - throw new Error(`expected a requested release, got ${requested.disposition}`) - } - db.commitWorkerTerminalArchiveForRelease({ - dispatchId, - resourceId: requested.resource.id, - kind: 'terminal_tail', - content: JSON.stringify({ lines: [] }), - archiveSource: 'terminal', - archiveStatus: 'empty' - }) - - const receipt = await completeWorkerTerminalRelease({ - runtime, - db, - dispatchId, - resource: db.getWorkerTerminalResource(requested.resource.id)!, - mode: 'recovery' - }) - - expect(receipt).toMatchObject({ state: 'released', processAction: 'none' }) - expect(runtime.closeTerminal).not.toHaveBeenCalled() - expect(db.getWorkerTerminalResourceByOwner(dispatchId)).toMatchObject({ - ownership_state: 'released', - release_state: 'released' - }) - }) - - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - it('recovery-mode: defers release_pending when liveness is unverifiable rather than provably exited', async () => { - setup() - const { dispatchId } = await startSettledWorker() - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - vi.mocked(runtime.showTerminal).mockRejectedValue(new Error('terminal_handle_stale')) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const resolveByIncarnation = vi.fn().mockReturnValue(null) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ;// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - runtime as unknown as { - resolveTerminalHandleByProcessIncarnation: typeof resolveByIncarnation - } - ).resolveTerminalHandleByProcessIncarnation = resolveByIncarnation - // Not a death certificate: inventory may still be incomplete, so recovery must defer. - inspectProcessLiveness.mockResolvedValue('unverifiable') - const requested = db.requestWorkerTerminalRelease(dispatchId) - if (requested.disposition !== 'requested') { - throw new Error(`expected a requested release, got ${requested.disposition}`) - } - - const receipt = await completeWorkerTerminalRelease({ - runtime, - db, - dispatchId, - resource: requested.resource, - mode: 'recovery' - }) - - expect(receipt.state).toBe('release_pending') - expect(runtime.closeTerminal).not.toHaveBeenCalled() - expect(db.getWorkerTerminalResourceByOwner(dispatchId)?.release_state).not.toBe('released') - }) -}) diff --git a/src/main/runtime/rpc/methods/orchestration-worker-release-reap-fixed.func.test.ts b/src/main/runtime/rpc/methods/orchestration-worker-release-reap-fixed.func.test.ts deleted file mode 100644 index d4c7884b1a4..00000000000 --- a/src/main/runtime/rpc/methods/orchestration-worker-release-reap-fixed.func.test.ts +++ /dev/null @@ -1,381 +0,0 @@ -// PRB-0219 / upstream #18737 — FUNCTIONAL (integration) reproduction of the steady-state -// worker-release reap LEAK. -// -// This is the "functional/integration" tier of the PRB-0219 test strategy. It wires the REAL -// orchestration RPC surface (orchestration.workerStart / workerRelease / workerList), the REAL -// OrchestrationDb, and the REAL release completion + observation modules against a fake runtime -// that faithfully models the two identity planes involved in the bug: -// -// * VOLATILE, epoch-fenced handle table — `handleTable` keyed by terminal handle, each entry -// stamped with the `rendererGraphEpoch` at which it was issued. `showTerminal` resolves a -// handle ONLY while its stamped epoch matches the current epoch (mirrors getLiveLeafForHandle -// throwing 'terminal_handle_stale' on a rendererGraphEpoch bump). A relay reconnect / renderer -// remount on headless serve bumps the epoch. -// * DURABLE, incarnation-addressed process table — `ptysById` keyed by the ptyId embedded in the -// worker's persisted process_incarnation (`${ptyId}:${incarnationId}`). The pty stays LIVE -// across an epoch bump; nothing about the graph epoch kills the process. -// -// The bug (mechanism): when the epoch bumps, the durable db handle stops resolving through -// showTerminal WHILE THE PTY IS STILL ALIVE. inspectWorkerTerminal swallows the throw and reports -// `missing`; completeWorkerTerminalRelease then commits `release_unknown` and returns WITHOUT ever -// calling runtime.closeTerminal — so the process/PTY leaks. On mtl-02 those orphans accumulate in -// the orca-serve@factory cgroup until TasksMax=4096 is hit and Bun/omp abort() on EAGAIN. -// -// The observable leak signal asserted here: state 'release_unknown' + processAction 'none' + -// closeTerminal NEVER called + the pty STILL alive in ptysById + worker-list terminalState stuck -// on 'release_unknown' (never 'released'). - -import { afterEach, describe, expect, it, vi } from 'vitest' -import { ORCHESTRATION_METHODS } from './orchestration' -import { eraseRpcMethods, type RpcContext } from '../core' -import { OrchestrationDb } from '../../orchestration/db' -import { OrcaRuntimeService } from '../../orca-runtime' - -describe('PRB-0219 worker-release reap FIX (functional verification)', () => { - let db: OrchestrationDb - let dbOpen = false - let runtime: OrcaRuntimeService - let ctx: RpcContext - let activeRunId: string - - const coordinatorPaneKey = 'tab_coord:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa' - const workerPaneKey = 'tab_worker:bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb' - - // Dispatch/automation ptyId shape: `${repoId}::${worktreePath}@@${suffix}` (executionHostId:null). - const PTY_ID = 'repo-7f3a::/data/wt/factory-task-1@@a1b2c3d4' - const INCARNATION_ID = 1 - const PROCESS_INCARNATION = `${PTY_ID}:${INCARNATION_ID}` - - // ---- fake runtime process/handle planes (module-level so spies can mutate them) ---- - let ptysById: Map - let handleTable: Map - let rendererGraphEpoch: number - let closedPtyIds: string[] - - /** Resolve a handle to its live pty only while its stamped graph epoch is current. */ - function resolveHandleToLivePty( - handle: string - ): { ptyId: string; pty: { incarnationId: number; alive: boolean } } | null { - const entry = handleTable.get(handle) - if (!entry) { - return null - } - if (entry.epoch !== rendererGraphEpoch) { - // rendererGraphEpoch fence: the durable handle no longer resolves to a live leaf. - return null - } - const pty = ptysById.get(entry.ptyId) - if (!pty || !pty.alive) { - return null - } - return { ptyId: entry.ptyId, pty } - } - - /** Wire the real RPC surface and db against the two-plane fake runtime. */ - function setup(): void { - ptysById = new Map([ - ['coord-pty', { incarnationId: 1, alive: true }], - [PTY_ID, { incarnationId: INCARNATION_ID, alive: true }] - ]) - handleTable = new Map([ - ['term_coord', { ptyId: 'coord-pty', epoch: 0 }], - ['term_worker', { ptyId: PTY_ID, epoch: 0 }] - ]) - rendererGraphEpoch = 0 - closedPtyIds = [] - - db = new OrchestrationDb(':memory:') - dbOpen = true - runtime = new OrcaRuntimeService() - runtime.setOrchestrationDb(db) - - // Incarnation-addressed liveness probe: reads the DURABLE plane, so it stays 'live' across the - // epoch bump (the process really is still running). Matches the real asymmetry. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This test fixture is deliberately shaped to exercise the private/runtime boundary. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ;// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - runtime as unknown as { - inspectTerminalProcessIncarnationLiveness: ( - incarnation: string - ) => Promise<'live' | 'exited'> - } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ).inspectTerminalProcessIncarnationLiveness = vi.fn(async (incarnation: string) => { - const idx = incarnation.lastIndexOf(':') - const ptyId = incarnation.slice(0, idx) - const pty = ptysById.get(ptyId) - return pty?.alive ? 'live' : 'exited' - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - }) as never - - // The incarnation-addressed re-resolution primitive the fix adds. Present here as a spy so the - // same harness proves BOTH tiers: pre-fix completion never calls it (leak); post-fix completion - // calls it to remint a live handle (reap). Fence: EXACT incarnationId match only. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This test fixture is deliberately shaped to exercise the private/runtime boundary. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ;// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ( - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - runtime as unknown as { - resolveTerminalHandleByProcessIncarnation: ( - incarnation: string, - hostScope: string | null - ) => string | null - } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - ).resolveTerminalHandleByProcessIncarnation = vi.fn((incarnation: string): string | null => { - const idx = incarnation.lastIndexOf(':') - const ptyId = incarnation.slice(0, idx) - const inc = Number(incarnation.slice(idx + 1)) - const pty = ptysById.get(ptyId) - if (!pty || !pty.alive) { - return null - } - if (pty.incarnationId !== inc) { - // Fence: a reused ptyId with a different incarnation must NOT resolve. - return null - } - // Remint a live handle at the current graph epoch. - handleTable.set('term_reminted', { ptyId, epoch: rendererGraphEpoch }) - return 'term_reminted' - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - }) as never - - // Identity plane (durable) — answers for the original AND any reminted handle. Independent of - // the graph epoch, exactly like the real getTerminal* accessors that read dispatch authority. - const knownWorkerHandle = (handle: string): boolean => - handle === 'term_worker' || handle === 'term_reminted' - vi.spyOn(runtime, 'getTerminalPaneKey').mockImplementation((handle) => - handle === 'term_coord' - ? coordinatorPaneKey - : knownWorkerHandle(handle) - ? workerPaneKey - : null - ) - vi.spyOn(runtime, 'getTerminalProcessIncarnation').mockImplementation((handle) => - knownWorkerHandle(handle) ? PROCESS_INCARNATION : null - ) - vi.spyOn(runtime, 'getOrchestrationDispatchAuthority').mockImplementation((handle) => - knownWorkerHandle(handle) - ? // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This test fixture is deliberately shaped to exercise the private/runtime boundary. - ({ - terminalHandle: handle, - paneKey: workerPaneKey, - processIncarnation: PROCESS_INCARNATION, - hostScope: { kind: 'local', hostId: 'local' } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - } as never) - : null - ) - vi.spyOn(runtime, 'validateOrchestrationAgentLauncher').mockImplementation(() => {}) - - // Volatile handle resolution — epoch-fenced. Throws 'terminal_handle_stale' once the epoch - // moves past the epoch at which the handle was issued. - vi.spyOn(runtime, 'showTerminal').mockImplementation(async (handle) => { - if (!resolveHandleToLivePty(handle)) { - throw new Error('terminal_handle_stale') - } - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - return { handle, worktreeId: 'repo::worktree', status: 'running' } as never - }) - - // The reap: closing a handle kills exactly the pty it resolves to. - vi.spyOn(runtime, 'closeTerminal').mockImplementation(async (handle) => { - const live = resolveHandleToLivePty(handle) - if (!live) { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - return { handle, tabId: null, ptyKilled: false } as never - } - live.pty.alive = false - ptysById.delete(live.ptyId) - closedPtyIds.push(live.ptyId) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - return { handle, tabId: `tab:${live.ptyId}`, ptyKilled: true } as never - }) - - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // Remaining runtime surface required to start + settle a worker (mirrors the unit harness). - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - vi.spyOn(runtime, 'showManagedTerminalWorkspace').mockResolvedValue({ - id: 'repo::worktree' - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - } as never) - vi.spyOn(runtime, 'createTerminal').mockResolvedValue({ - handle: 'term_worker', - worktreeId: 'repo::worktree', - title: 'worker' - }) - vi.spyOn(runtime, 'waitForTerminal').mockResolvedValue({ - handle: 'term_worker', - condition: 'tui-idle', - satisfied: true, - status: 'running', - exitCode: null - }) - vi.spyOn(runtime, 'getTerminalOrchestrationCliCommand').mockReturnValue('orca') - vi.spyOn(runtime, 'sendTerminalAgentPrompt').mockResolvedValue({ - handle: 'term_worker', - accepted: true, - bytesWritten: 1 - }) - vi.spyOn(runtime, 'isTerminalRunningAgent').mockResolvedValue(true) - vi.spyOn(runtime, 'getExactWorkerProviderSession').mockReturnValue(null) - vi.spyOn(runtime, 'readTerminal').mockResolvedValue({ - handle: 'term_worker', - status: 'running', - tail: ['worker output line 1', 'worker output line 2'], - truncated: false, - nextCursor: '2' - }) - vi.spyOn(runtime, 'notifyMessageArrived').mockImplementation(() => {}) - - activeRunId = db.createRun({ - objective: 'PRB-0219 reap leak fixture', - coordinatorHandle: 'term_coord', - coordinatorPaneKey - }).id - ctx = { runtime } - } - - afterEach(() => { - if (dbOpen) { - dbOpen = false - db.close() - } - vi.restoreAllMocks() - }) - - /** Look up a registered orchestration RPC method by name. */ - function findMethod(name: string) { - const method = eraseRpcMethods(ORCHESTRATION_METHODS).find((m) => m.name === name) - if (!method) { - throw new Error(`Method not found: ${name}`) - } - return method - } - - /** Parse a method's params and invoke its handler against the shared ctx. */ - async function call(name: string, params: Record) { - const method = findMethod(name) - const parsed = method.params ? method.params.parse(params) : undefined - return method.handler(parsed, ctx) - } - - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - /** Start a worker and settle its report, the state a release acts on. */ - async function startSettledWorker(): Promise<{ taskId: string; dispatchId: string }> { - const task = db.createTask({ spec: 'reap-leak fixture task', runId: activeRunId }) - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const result = (await call('orchestration.workerStart', { - task: task.id, - from: 'term_coord', - agent: 'codex' - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - })) as { dispatchId: string; state: string } - expect(result.state).toBe('ready') - const settlement = db.settleWorkerReport({ - taskId: task.id, - dispatchId: result.dispatchId, - outcome: 'succeeded', - result: 'worker succeeded' - }) - expect(settlement.action).toBe('settled') - return { taskId: task.id, dispatchId: result.dispatchId } - } - - /** The terminalState workerList projects for a dispatch, or null. */ - async function workerTerminalState(dispatchId: string): Promise { - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const listed = (await call('orchestration.workerList', { run: activeRunId })) as { - workers: { dispatchId: string; terminalState: string | null }[] - } - return listed.workers.find((w) => w.dispatchId === dispatchId)?.terminalState ?? null - } - - it('REAP (fixed): a rendererGraphEpoch bump strands the durable handle, but the incarnation fallback remints a live handle and reaps the process', async () => { - setup() - const { dispatchId } = await startSettledWorker() - const resource = db.getWorkerTerminalResourceByOwner(dispatchId) - expect(resource?.process_incarnation).toBe(PROCESS_INCARNATION) - - // Relay reconnect / renderer remount bumps the graph epoch: the durable handle goes stale - // while the PTY stays alive. - rendererGraphEpoch = 1 - await expect(runtime.showTerminal('term_worker')).rejects.toThrow('terminal_handle_stale') - expect(ptysById.get(PTY_ID)?.alive).toBe(true) - - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const receipt = (await call('orchestration.workerRelease', { dispatch: dispatchId })) as { - state: string - processAction: string - } - - // The fix: inspectWorkerTerminal re-resolved the live PTY by process incarnation, reminted a - // handle, and completion closed THAT handle — the process is actually reaped. - expect(receipt.state).toBe('released') - expect(receipt.processAction).toBe('closed_agent_terminal') - expect(runtime.closeTerminal).toHaveBeenCalledWith('term_reminted') - expect(closedPtyIds).toEqual([PTY_ID]) - expect(ptysById.has(PTY_ID)).toBe(false) - expect(await workerTerminalState(dispatchId)).toBe('released') - }) - - it('CONTROL: with the graph epoch intact the same release reaps exactly that PTY', async () => { - setup() - const { dispatchId } = await startSettledWorker() - - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const receipt = (await call('orchestration.workerRelease', { dispatch: dispatchId })) as { - state: string - processAction: string - } - - expect(receipt.state).toBe('released') - expect(receipt.processAction).toBe('closed_agent_terminal') - expect(closedPtyIds).toEqual([PTY_ID]) - expect(ptysById.has(PTY_ID)).toBe(false) - expect(await workerTerminalState(dispatchId)).toBe('released') - }) - - it('FENCE (fixed): a reused ptyId carrying a different incarnation must NOT remint or close — stays release_unknown', async () => { - setup() - const { dispatchId } = await startSettledWorker() - - // The graph epoch bumps AND the ptyId has been reused by a different process (incarnation 2), - // while the worker's recorded incarnation is still 1. The exact-incarnation fence must refuse. - rendererGraphEpoch = 1 - const reused = ptysById.get(PTY_ID) - if (reused) { - reused.incarnationId = 2 - } - - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Test fixture crosses a private/runtime boundary with a verified shape. - const receipt = (await call('orchestration.workerRelease', { dispatch: dispatchId })) as { - state: string - processAction: string - } - - expect(receipt.state).toBe('release_unknown') - expect(receipt.processAction).toBe('none') - expect(runtime.closeTerminal).not.toHaveBeenCalled() - // The other lane's live process is left untouched (never reaped by an over-broad match). - expect(ptysById.get(PTY_ID)?.alive).toBe(true) - expect(await workerTerminalState(dispatchId)).toBe('release_unknown') - }) -}) diff --git a/src/main/runtime/rpc/methods/orchestration/worker/structured-worker-release-stop.ts b/src/main/runtime/rpc/methods/orchestration/worker/structured-worker-release-stop.ts index 91e524e4bf1..4a32147d1a6 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/structured-worker-release-stop.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/structured-worker-release-stop.ts @@ -4,15 +4,15 @@ import type { WorkerTerminalResourceRow } from '../../../../orchestration/worker import { stopStructuredWorker } from '../../orchestration-structured-worker-lifecycle' import type { StructuredWorkerIdentity } from '../../../../structured-worker-identity' import { archiveSummary } from './worker-terminal-resource-presentation' -import { releaseUnknownRecovery, type WorkerReleaseReceipt } from './worker-release-completion' +import type { WorkerReleaseReceipt } from './worker-release-completion' /** * The close half of a release for a worker that IS a structured session. * * Separate from the PTY close for the same reason the delivery lane is: there is no terminal to * close and no exit to observe, so the host's own settlement is the only proof available. Only a - * proven close may settle; an unproven one reports `release_unknown` and stays retryable, but only - * under a fresh request id — replaying the prior one just returns this same receipt. + * proven close may settle; an unproven one reports `release_unknown` and stays retryable under the + * same request id. */ export async function stopStructuredWorkerForRelease(args: { structured: StructuredWorkerIdentity @@ -36,7 +36,7 @@ export async function stopStructuredWorkerForRelease(args: { processAction: stop.closeAttempted ? 'closed_agent_terminal' : 'none', archive: { source: args.archiveSource, status: args.archiveStatus }, lastError: unknown.release_error ?? stop.reason, - recovery: releaseUnknownRecovery(dispatchId) + recovery: `Inspect with: orca orchestration worker-show --dispatch ${dispatchId} --json — then repeat worker-release with the same --retry-request.` } } const settled = db.settleWorkerTerminalRelease(resource.id) diff --git a/src/main/runtime/rpc/methods/orchestration/worker/worker-control.ts b/src/main/runtime/rpc/methods/orchestration/worker/worker-control.ts index 2930abcd63f..5e8babf3c5e 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/worker-control.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/worker-control.ts @@ -143,9 +143,6 @@ export const ORCHESTRATION_WORKER_CONTROL_METHODS = [ `Worker Dispatch ${params.dispatch} no longer resolves to its exact process.` ) } - // Read via the handle inspectWorkerTerminal proved live: the durable one, or a handle - // re-minted from the recorded incarnation after the durable handle went stale. - const liveHandle = observation.terminalHandle ?? terminalHandle const structured = readStructuredWorkerOutput({ db, dispatchId: params.dispatch, @@ -166,7 +163,7 @@ export const ORCHESTRATION_WORKER_CONTROL_METHODS = [ const output = await readExactWorkerOutput({ runtime, dispatchId: params.dispatch, - terminalHandle: liveHandle, + terminalHandle, workerState: worker?.state ?? 'unsupervised', terminalStatus: observation.status === 'exited' diff --git a/src/main/runtime/rpc/methods/orchestration/worker/worker-observation.ts b/src/main/runtime/rpc/methods/orchestration/worker/worker-observation.ts index 2fae12d6608..83b3d020f4c 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/worker-observation.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/worker-observation.ts @@ -15,7 +15,6 @@ import type { WorkerDispatchRow } from '../../../../orchestration/types' -/** Observe a worker terminal, re-minting a live handle from the recorded process incarnation when the durable handle went stale, so a still-running worker is never reported missing and leaked. */ export async function inspectWorkerTerminal( runtime: OrcaRuntimeService, db: OrchestrationDb, @@ -28,15 +27,12 @@ export async function inspectWorkerTerminal( reason?: string /** Set only on a proven-exact worker parked on a prompt that needs a human. */ agentWait?: RuntimeTerminalInteractiveWait | null - /** The handle that actually resolved: the durable one, or a live handle re-minted from the - * recorded process incarnation after the durable handle went stale. Null when none resolved. */ - terminalHandle: string | null }> { const worker = db.getWorkerDispatch(dispatchId) const terminalHandle = worker?.agent_terminal_handle ?? db.getDispatchContextById(dispatchId)?.assignee_handle if (!terminalHandle) { - return { terminal: null, exact: false, status: 'unattached', terminalHandle: null } + return { terminal: null, exact: false, status: 'unattached' } } const structured = resolveStructuredWorkerForDispatch(db, dispatchId) if (structured) { @@ -57,44 +53,20 @@ export async function inspectWorkerTerminal( terminal: null, exact, status: exact ? observation.status : 'identity_changed', - ...(exact && observation.reason ? { reason: observation.reason } : {}), - terminalHandle: null + ...(exact && observation.reason ? { reason: observation.reason } : {}) } } - let effectiveHandle = terminalHandle - let terminal = await runtime.showTerminal(effectiveHandle).catch(() => null) + const terminal = await runtime.showTerminal(terminalHandle).catch(() => null) if (!terminal) { - // Why: the durable handle resolves nowhere after a renderer graph epoch bump or handle - // invalidation, yet the recorded process incarnation may still name a live PTY. Re-mint a - // live handle (incarnation-fenced) so worker-show and release act on the still-running - // process instead of reporting it missing — which would leak the agent process tree. - // (workerList is a pure DB projection and never calls inspectWorkerTerminal.) - const resource = db.getWorkerTerminalResourceByOwner(dispatchId) - const reminted = resource?.process_incarnation - ? runtime.resolveTerminalHandleByProcessIncarnation( - resource.process_incarnation, - resource.host_scope - ) - : null - if (reminted) { - const remintedTerminal = await runtime.showTerminal(reminted).catch(() => null) - if (remintedTerminal) { - effectiveHandle = reminted - terminal = remintedTerminal - } - } - } - if (!terminal) { - // The re-mint above failed, so no live handle resolved; report the durable handle unresolved. - return { terminal: null, exact: false, status: 'missing', terminalHandle: null } + return { terminal: null, exact: false, status: 'missing' } } const exact = db.isDispatchProcessCurrent({ dispatchId, - paneKey: runtime.getTerminalPaneKey(effectiveHandle), - processIncarnation: runtime.getTerminalProcessIncarnation(effectiveHandle) + paneKey: runtime.getTerminalPaneKey(terminalHandle), + processIncarnation: runtime.getTerminalProcessIncarnation(terminalHandle) }) if (!exact) { - return { terminal, exact, status: 'identity_changed', terminalHandle: effectiveHandle } + return { terminal, exact, status: 'identity_changed' } } // Why: the aggregate inventory only iterates registered providers, so a dropped // relay clears `connected` for every remote PTY at once. Lost contact is not a @@ -104,48 +76,38 @@ export async function inspectWorkerTerminal( // Exact-gated by the early return above: a replaced process's prompt would attribute another // lane's blocker to this worker. const agentWait = terminal.agentWait - const verdict = runtime.getTerminalLivenessVerdict?.(effectiveHandle) ?? null + const verdict = runtime.getTerminalLivenessVerdict?.(terminalHandle) ?? null if (verdict?.status === 'unverifiable') { - return { - terminal, - exact, - status: 'unverifiable', - reason: verdict.reason, - agentWait, - terminalHandle: effectiveHandle - } + return { terminal, exact, status: 'unverifiable', reason: verdict.reason, agentWait } } if (verdict?.status === 'live') { - return { terminal, exact, status: 'live', agentWait, terminalHandle: effectiveHandle } + return { terminal, exact, status: 'live', agentWait } } if (!verdict) { const dispatch = db.getDispatchContextById?.(dispatchId) const persistedHostScope = parseWorkerTerminalHostScope(dispatch?.host_scope ?? null) - const currentHostScope = runtime.getOrchestrationDispatchAuthority?.(effectiveHandle)?.hostScope + const currentHostScope = runtime.getOrchestrationDispatchAuthority?.(terminalHandle)?.hostScope if (persistedHostScope?.kind === 'ssh' || currentHostScope?.kind === 'ssh') { return { terminal, exact, status: 'unverifiable', reason: 'missing_liveness_verdict', - agentWait, - terminalHandle: effectiveHandle + agentWait } } return { terminal, exact, status: terminal.connected === false ? 'exited' : 'live', - agentWait, - terminalHandle: effectiveHandle + agentWait } } return { terminal, exact, status: 'exited', - agentWait, - terminalHandle: effectiveHandle + agentWait } } diff --git a/src/main/runtime/rpc/methods/orchestration/worker/worker-release-completion.ts b/src/main/runtime/rpc/methods/orchestration/worker/worker-release-completion.ts index 5076d607e7b..b686cdfe6cd 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/worker-release-completion.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/worker-release-completion.ts @@ -121,9 +121,6 @@ async function completeWorkerTerminalReleaseOnce( } } const observation = await inspectWorkerTerminal(runtime, db, dispatchId) - // The live handle to act on: the durable one, or a handle re-minted from the recorded process - // incarnation when the durable handle went stale (inspectWorkerTerminal proved it live). - const terminalHandle = observation.terminalHandle ?? resource.terminal_handle if (observation.status === 'identity_changed') { const retained = db.revertWorkerTerminalReleaseToRetained(resource.id, 'identity_unproven') return { @@ -135,39 +132,33 @@ async function completeWorkerTerminalReleaseOnce( } } if (observation.status === 'missing' || observation.status === 'unattached') { - // Re-resolution by process incarnation (inspectWorkerTerminal) already failed, so no live PTY - // carries this worker's exact incarnation. If that incarnation is provably gone, settle - // released BEFORE the recovery defer: proof of death outranks deferral, so a provably-exited - // worker never languishes in release_pending across recovery passes. - if (resource.process_incarnation) { - const processLiveness = await runtime.inspectTerminalProcessIncarnationLiveness( - resource.process_incarnation, - resource.host_scope - ) - if (processLiveness === 'exited') { - // Prefer incarnation-fenced settle (dispatch relation + process_incarnation CAS). - const reconciled = db.settleDeadWorkerTerminalRelease({ - requestingDispatchId: dispatchId, - resourceId: resource.id, - processIncarnation: resource.process_incarnation - }) - if (reconciled.disposition === 'released') { - runtime.notifyMessageArrived(`dispatch:${dispatchId}`, 'status') - return { - dispatchId, - state: 'released', - processAction: 'none', - archive: archiveSummary(reconciled.resource) + if (args.mode === 'recovery') { + // A close can succeed before the process crashes, leaving `releasing` durable state while + // terminal inventory no longer resolves the handle. Only a positive host liveness verdict + // may settle that exact incarnation; contact loss remains pending/unverifiable. + if (resource.process_incarnation) { + const processLiveness = await runtime.inspectTerminalProcessIncarnationLiveness( + resource.process_incarnation, + resource.host_scope + ) + if (processLiveness === 'exited') { + const reconciled = db.settleDeadWorkerTerminalRelease({ + requestingDispatchId: dispatchId, + resourceId: resource.id, + processIncarnation: resource.process_incarnation + }) + if (reconciled.disposition === 'released') { + runtime.notifyMessageArrived(`dispatch:${dispatchId}`, 'status') + return { + dispatchId, + state: 'released', + processAction: 'closed_exited_terminal', + archive: archiveSummary(reconciled.resource) + } } } - // settleDead retains when the archive is still mandatory and missing (e.g. requested but - // never committed). Do NOT plain-settle: that would discard output and break recovery's - // "archive is mandatory" invariant. Fall through to recovery pending / unknown instead. } - } - if (args.mode === 'recovery') { - // No death certificate yet: inventory may still be incomplete during startup/reconnect - // discovery, so defer instead of guessing. + // Inventory may still be incomplete during startup/reconnect discovery; defer. return { dispatchId, state: 'release_pending', @@ -193,7 +184,7 @@ async function completeWorkerTerminalReleaseOnce( } } - if (!workerTerminalLeaseIsCurrent(runtime, db, dispatchId, resource, terminalHandle)) { + if (!workerTerminalLeaseIsCurrent(runtime, db, dispatchId, resource)) { const retained = db.revertWorkerTerminalReleaseToRetained(resource.id, 'identity_unproven') return { dispatchId, @@ -212,7 +203,7 @@ async function completeWorkerTerminalReleaseOnce( const captured = await captureWorkerOutputArchive({ runtime, dispatchId, - terminalHandle, + terminalHandle: resource.terminal_handle, attachedAtMs: orchestrationTimestampToMs(worker.created_at), structuredWorker: structured }) @@ -240,7 +231,7 @@ async function completeWorkerTerminalReleaseOnce( archive: archiveSummary(releasing) } } - if (!workerTerminalLeaseIsCurrent(runtime, db, dispatchId, releasing, terminalHandle)) { + if (!workerTerminalLeaseIsCurrent(runtime, db, dispatchId, releasing)) { const retained = db.revertWorkerTerminalReleaseToRetained(resource.id, 'identity_unproven') return { dispatchId, @@ -263,7 +254,7 @@ async function completeWorkerTerminalReleaseOnce( archiveStatus }) } - const close = await runtime.closeTerminal(terminalHandle) + const close = await runtime.closeTerminal(resource.terminal_handle) if (!close.ptyKilled) { const reason = describeUnconfirmedAgentStop(close) const unknown = db.markWorkerTerminalReleaseUnknown(resource.id, reason) diff --git a/src/main/runtime/rpc/methods/orchestration/worker/worker-release-runtime-incarnation.test.ts b/src/main/runtime/rpc/methods/orchestration/worker/worker-release-runtime-incarnation.test.ts deleted file mode 100644 index 4caac62f5da..00000000000 --- a/src/main/runtime/rpc/methods/orchestration/worker/worker-release-runtime-incarnation.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' -import { inspectWorkerTerminal } from './worker-observation' -import { createOrchestrationWorkerReleaseHarness } from './worker-release.test-support' -import { makePaneKey } from '../../../../../../shared/stable-pane-id' - -const PTY_ID = 'runtime_test:term_worker' -const LEAF_ID = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb' - -describe('worker release through runtime incarnation recovery', () => { - const harness = createOrchestrationWorkerReleaseHarness() - afterEach(() => harness.cleanup()) - - it.each(['release', 'stop', 'read', 'replacement', 'disconnected'] as const)( - '%s uses runtime identity and close paths', - async (scenario) => { - harness.setup() - const { dispatchId } = - scenario === 'stop' ? await harness.startWorker() : await harness.startSettledWorker() - const runtime = harness.runtime - vi.mocked(runtime.showTerminal).mockRestore() - vi.mocked(runtime.getTerminalPaneKey).mockRestore() - vi.mocked(runtime.getTerminalProcessIncarnation).mockRestore() - vi.mocked(runtime.getOrchestrationDispatchAuthority).mockRestore() - vi.mocked(runtime.readTerminal).mockRestore() - vi.mocked(runtime.closeTerminal).mockRestore() - const kill = vi.fn(() => true) - runtime.setPtyController({ write: () => true, kill, getForegroundProcess: async () => null }) - runtime.registerPty(PTY_ID, 'repo::worktree', null, { - tabId: 'tab_worker', - leafId: LEAF_ID, - incarnationId: scenario === 'replacement' ? '2' : '1' - }) - await expect(runtime.showTerminal('term_worker')).rejects.toThrow() - if (scenario === 'disconnected') { - vi.spyOn(runtime, 'getTerminalLivenessVerdict').mockReturnValue({ - status: 'unverifiable', - reason: 'transport unavailable' - }) - const observed = await inspectWorkerTerminal(runtime, harness.db, dispatchId) - expect(observed).toMatchObject({ exact: true, status: 'unverifiable' }) - expect(kill).not.toHaveBeenCalled() - return - } - if (scenario === 'stop') { - const receipt = await harness.call('orchestration.workerStop', { dispatch: dispatchId }) - expect(receipt).toMatchObject({ state: 'stopped', processAction: 'closed_agent_terminal' }) - expect(kill).toHaveBeenCalledExactlyOnceWith(PTY_ID) - return - } - if (scenario === 'read') { - const readTerminal = vi.spyOn(runtime, 'readTerminal') - const output = await harness.call('orchestration.workerRead', { dispatch: dispatchId }) - // Pins the read to the reminted terminal: the handle it reached must resolve to the - // registered pane and incarnation, which the stale durable handle never does. - const [[readHandle]] = readTerminal.mock.calls - expect(runtime.getTerminalPaneKey(readHandle)).toBe(makePaneKey('tab_worker', LEAF_ID)) - expect(runtime.getTerminalProcessIncarnation(readHandle)).toBe(`${PTY_ID}:1`) - expect(output).toMatchObject({ - source: 'terminal', - fallbackReason: 'session_not_reported' - }) - expect(kill).not.toHaveBeenCalled() - return - } - const receipt = await harness.call('orchestration.workerRelease', { dispatch: dispatchId }) - if (scenario === 'replacement') { - expect(receipt).toMatchObject({ state: 'release_unknown', processAction: 'none' }) - expect(kill).not.toHaveBeenCalled() - return - } - expect(receipt).toMatchObject({ state: 'released', processAction: 'closed_agent_terminal' }) - expect(kill).toHaveBeenCalledExactlyOnceWith(PTY_ID) - expect(harness.db.getWorkerTerminalResourceByOwner(dispatchId)).toMatchObject({ - ownership_state: 'released', - release_state: 'released' - }) - } - ) -}) diff --git a/src/main/runtime/rpc/methods/orchestration/worker/worker-stop.ts b/src/main/runtime/rpc/methods/orchestration/worker/worker-stop.ts index ee0e73c904f..98d3c376f61 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/worker-stop.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/worker-stop.ts @@ -141,7 +141,6 @@ export const ORCHESTRATION_WORKER_STOP_METHODS = [ }) } const observation = await inspectWorkerTerminal(runtime, db, params.dispatch) - const liveHandle = observation.terminalHandle ?? handle // The host exit can settle this stop while terminal inspection is awaiting inventory. if (db.getWorkerDispatch(params.dispatch)?.state === 'stopped') { runtime.notifyMessageArrived(`dispatch:${params.dispatch}`, 'status') @@ -202,7 +201,7 @@ export const ORCHESTRATION_WORKER_STOP_METHODS = [ } } const closed = await runtime - .closeTerminal(liveHandle) + .closeTerminal(handle) .then((close) => ({ close }) as const) .catch( (error: unknown) => diff --git a/src/main/runtime/rpc/methods/orchestration/worker/worker-terminal-release-lease.ts b/src/main/runtime/rpc/methods/orchestration/worker/worker-terminal-release-lease.ts index cad0ceb4128..50a97c9de4b 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/worker-terminal-release-lease.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/worker-terminal-release-lease.ts @@ -9,27 +9,23 @@ export function workerTerminalLeaseIsCurrent( runtime: OrcaRuntimeService, db: OrchestrationDb, dispatchId: string, - resource: WorkerTerminalResourceRow, - // Live (possibly reminted) handle for runtime probes. Durable identity still compares - // worker.agent_terminal_handle to resource.terminal_handle; after beginGraphReload the durable - // string is absent from the handle table, so authority/pane/incarnation must use the live one. - liveTerminalHandle: string + resource: WorkerTerminalResourceRow ): boolean { const worker = db.getWorkerDispatch(dispatchId) if (isStructuredWorkerHandle(resource.terminal_handle)) { return structuredWorkerTerminalLeaseIsCurrent(db, dispatchId, worker, resource) } - const authority = runtime.getOrchestrationDispatchAuthority(liveTerminalHandle) + const authority = runtime.getOrchestrationDispatchAuthority(resource.terminal_handle) // Exited PTYs retain identity and host evidence but no longer mint launch authority. return Boolean( worker?.agent_terminal_handle === resource.terminal_handle && (authority ? resource.host_scope === JSON.stringify(authority.hostScope) - : runtime.getTerminalLivenessVerdict(liveTerminalHandle)?.status === 'exited') && + : runtime.getTerminalLivenessVerdict(resource.terminal_handle)?.status === 'exited') && db.isDispatchProcessCurrent({ dispatchId, - paneKey: runtime.getTerminalPaneKey(liveTerminalHandle), - processIncarnation: runtime.getTerminalProcessIncarnation(liveTerminalHandle) + paneKey: runtime.getTerminalPaneKey(resource.terminal_handle), + processIncarnation: runtime.getTerminalProcessIncarnation(resource.terminal_handle) }) && !db.workerTerminalResourceHasIdentityConflict(resource.id) ) diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 2ddea555dab..42c829a5e01 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -584,7 +584,6 @@ "9e2a9bb87b": "Continue", "6f8056a565": "Command Code", "4238b771b5": "Codebuff", - "b0b350de65": "Freebuff", "cbaf0c2e0b": "Cline", "1f8a19e9ad": "Autohand Code", "5e8eff11b3": "Auggie", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 887ba198003..cfce4be274b 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -358,7 +358,6 @@ "9e2a9bb87b": "Continue", "6f8056a565": "Command Code", "4238b771b5": "Codebuff", - "b0b350de65": "Freebuff", "cbaf0c2e0b": "Cline", "1f8a19e9ad": "Autohand Code", "5e8eff11b3": "Auggie", diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index d9c5dd5004d..3bb3ca43922 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -435,7 +435,6 @@ "9e2a9bb87b": "Continuer", "6f8056a565": "Command Code", "4238b771b5": "Codebuff", - "b0b350de65": "Freebuff", "cbaf0c2e0b": "Cline", "1f8a19e9ad": "Autohand Code", "5e8eff11b3": "Auggie", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 4fe965ac48e..db72c535817 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -358,7 +358,6 @@ "9e2a9bb87b": "Continue", "6f8056a565": "Command Code", "4238b771b5": "Codebuff", - "b0b350de65": "Freebuff", "cbaf0c2e0b": "Cline", "1f8a19e9ad": "Autohand Code", "5e8eff11b3": "Auggie", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 88ba5814432..55893501e49 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -361,7 +361,6 @@ "9e2a9bb87b": "Continue", "6f8056a565": "Command Code", "4238b771b5": "Codebuff", - "b0b350de65": "Freebuff", "cbaf0c2e0b": "Cline", "1f8a19e9ad": "Autohand Code", "5e8eff11b3": "Auggie", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 592a734e077..2f5c569a099 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -361,7 +361,6 @@ "9e2a9bb87b": "Continue", "6f8056a565": "Command Code", "4238b771b5": "Codebuff", - "b0b350de65": "Freebuff", "cbaf0c2e0b": "Cline", "1f8a19e9ad": "Autohand Code", "5e8eff11b3": "Auggie", diff --git a/src/renderer/src/lib/agent-catalog.tsx b/src/renderer/src/lib/agent-catalog.tsx index 32193c7e7b0..6ea1de86787 100644 --- a/src/renderer/src/lib/agent-catalog.tsx +++ b/src/renderer/src/lib/agent-catalog.tsx @@ -235,13 +235,6 @@ export const getAgentCatalog = createLocalizedCatalog((): AgentCatalogEntry[] => faviconDomain: 'codebuff.com', homepageUrl: 'https://www.codebuff.com/docs/help/quick-start' }, - { - id: 'freebuff', - label: translate('auto.lib.agent.catalog.b0b350de65', 'Freebuff'), - cmd: 'freebuff', - faviconDomain: 'freebuff.com', - homepageUrl: 'https://freebuff.com/cli' - }, { id: 'command-code', label: translate('auto.lib.agent.catalog.6f8056a565', 'Command Code'), diff --git a/src/renderer/src/lib/agent-favicon-assets.ts b/src/renderer/src/lib/agent-favicon-assets.ts index 98ed4c95b0e..caeea46bc43 100644 --- a/src/renderer/src/lib/agent-favicon-assets.ts +++ b/src/renderer/src/lib/agent-favicon-assets.ts @@ -14,7 +14,6 @@ import augUrl from '../../../shared/agent-icons/aug.png?url' import autohandUrl from '../../../shared/agent-icons/autohand.png?url' import clineUrl from '../../../shared/agent-icons/cline.png?url' import codebuffUrl from '../../../shared/agent-icons/codebuff.png?url' -import freebuffUrl from '../../../shared/agent-icons/freebuff.png?url' import commandCodeUrl from '../../../shared/agent-icons/command-code.png?url' import continueUrl from '../../../shared/agent-icons/continue.png?url' import cursorUrl from '../../../shared/agent-icons/cursor.png?url' @@ -50,7 +49,6 @@ export const AGENT_FAVICON_ASSETS: Partial> = { autohand: autohandUrl, cline: clineUrl, codebuff: codebuffUrl, - freebuff: freebuffUrl, 'command-code': commandCodeUrl, continue: continueUrl, cursor: cursorUrl, diff --git a/src/renderer/src/lib/agent-status.ts b/src/renderer/src/lib/agent-status.ts index 8a642c1dd50..b88018b4294 100644 --- a/src/renderer/src/lib/agent-status.ts +++ b/src/renderer/src/lib/agent-status.ts @@ -120,7 +120,6 @@ const ICONABLE_AGENT_TYPES: Record = { aug: true, cline: true, codebuff: true, - freebuff: true, 'command-code': true, continue: true, cursor: true, diff --git a/src/shared/agent-icons/freebuff.png b/src/shared/agent-icons/freebuff.png deleted file mode 100644 index b81a6fa9c21..00000000000 Binary files a/src/shared/agent-icons/freebuff.png and /dev/null differ diff --git a/src/shared/agent-kind.ts b/src/shared/agent-kind.ts index fd1e369c767..13e123fbc88 100644 --- a/src/shared/agent-kind.ts +++ b/src/shared/agent-kind.ts @@ -36,7 +36,6 @@ const TUI_AGENT_KIND_BY_AGENT = { aug: 'aug', cline: 'cline', codebuff: 'codebuff', - freebuff: 'freebuff', 'command-code': 'command-code', continue: 'continue', cursor: 'cursor', diff --git a/src/shared/skills-cli-agent-keys.ts b/src/shared/skills-cli-agent-keys.ts index f99b1f1c248..275f96b9fae 100644 --- a/src/shared/skills-cli-agent-keys.ts +++ b/src/shared/skills-cli-agent-keys.ts @@ -35,7 +35,6 @@ export const SKILLS_CLI_AGENT_KEY_BY_TUI_AGENT = { aug: 'augment', cline: 'cline', codebuff: null, - freebuff: null, 'command-code': 'command-code', continue: 'continue', cursor: 'cursor', diff --git a/src/shared/telemetry-property-schemas.ts b/src/shared/telemetry-property-schemas.ts index 81613deff8f..4e868a3de33 100644 --- a/src/shared/telemetry-property-schemas.ts +++ b/src/shared/telemetry-property-schemas.ts @@ -30,7 +30,6 @@ export const AGENT_KIND_VALUES = [ 'aug', 'cline', 'codebuff', - 'freebuff', 'command-code', 'continue', 'cursor', diff --git a/src/shared/tui-agent-config.ts b/src/shared/tui-agent-config.ts index fa53d2c0f7d..40986189f32 100644 --- a/src/shared/tui-agent-config.ts +++ b/src/shared/tui-agent-config.ts @@ -234,10 +234,6 @@ const TUI_AGENT_CONFIG_SOURCE: Record = { detectCmd: 'codebuff', promptInjectionMode: 'stdin-after-start' }, - freebuff: { - detectCmd: 'freebuff', - promptInjectionMode: 'stdin-after-start' - }, 'command-code': { // Why: use the full name (not its `cmd` alias) so detection doesn't collide with Windows' built-in cmd.exe. detectCmd: 'command-code', diff --git a/src/shared/tui-agent-display-names.ts b/src/shared/tui-agent-display-names.ts index 4acb76e61af..b886cfb1490 100644 --- a/src/shared/tui-agent-display-names.ts +++ b/src/shared/tui-agent-display-names.ts @@ -32,7 +32,6 @@ export const TUI_AGENT_DISPLAY_NAMES: Record = { aug: 'Auggie', cline: 'Cline', codebuff: 'Codebuff', - freebuff: 'Freebuff', 'command-code': 'Command Code', continue: 'Continue', cursor: 'Cursor', diff --git a/src/shared/tui-agent-selection.ts b/src/shared/tui-agent-selection.ts index 4afacfd9e21..85863618228 100644 --- a/src/shared/tui-agent-selection.ts +++ b/src/shared/tui-agent-selection.ts @@ -31,7 +31,6 @@ export const TUI_AGENT_AUTO_PICK_ORDER = [ 'autohand', 'cline', 'codebuff', - 'freebuff', 'command-code', 'continue', 'cursor', diff --git a/src/shared/tui-agent.ts b/src/shared/tui-agent.ts index 6bf80af5002..226060d1d23 100644 --- a/src/shared/tui-agent.ts +++ b/src/shared/tui-agent.ts @@ -22,7 +22,6 @@ export type TuiAgent = | 'aug' // Augment/Auggie | 'cline' // Cline | 'codebuff' // Codebuff - | 'freebuff' // Freebuff | 'command-code' // Command Code | 'continue' // Continue | 'cursor' // Cursor