Merge remote-tracking branch 'origin/main' into settled-worker-policy

# Conflicts:
#	src/main/runtime/orchestration/orchestration-settled-worker-resume-fence-db.test.ts
#	src/main/runtime/runtime-legacy-worker-terminal-resume-fence.test.ts
This commit is contained in:
Jinwoo-H
2026-09-08 04:04:58 -04:00
251 changed files with 8715 additions and 1276 deletions
+192 -1
View File
@@ -10,6 +10,79 @@
}
},
"gates": [
{
"id": "agent-session.history-forward-read-budget",
"title": "Journal catch-up reads only the next page and one lookahead row",
"maturity": "experimental",
"protection": "partial",
"owner": "agent-session-runtime",
"layer": "runtime-unit",
"surfaces": ["structured agent history", "structured agent subscriptions"],
"platforms": ["macos", "linux", "windows"],
"providers": ["local", "ssh", "remote-runtime"],
"coveredPlatforms": ["macos"],
"coveredProviders": ["local", "ssh", "remote-runtime"],
"coverageNotes": "The real SQLite journal and production subscriber delivery are exercised with a folder workspace and remote host identity. The SQL and pagination code is shared across execution hosts; live SSH transport and Linux/Windows runtime execution are not exercised. PTY, daemon, WSL execution, and mobile rendering are unaffected.",
"motivatingLinks": [
"https://github.com/stablyai/orca/blob/main/src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.ts"
],
"invariant": "Forward catch-up preserves every item, revision, tombstone, sequence cursor, page byte bound, and reset behavior while reading at most the requested row count plus one from SQLite for each page.",
"oracle": "Reconnect a real subscriber to a 2,000-row journal and receive all 2,000 item identities in order through the live cursor; count the actual SQL rows returned and parsed as 2,009 instead of 11,000. Assert exact final-page hasNewer, unlimited reader compatibility, gap detection at the next page, and parse-stop behavior at the lookahead row. Existing history tests cover revisions, tombstones, byte-bound shrinking, epochs, and schema resets.",
"commands": [
"ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config config/vitest.config.ts src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts src/main/native-chat/agent-session-wire/agent-session-history-page.test.ts src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.test.ts src/main/native-chat/agent-session-journal"
],
"testFiles": [
"src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts",
"src/main/native-chat/agent-session-wire/agent-session-history-page.test.ts",
"src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.test.ts"
],
"assertionRefs": [
{
"file": "src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts",
"assertions": [
"reconnects through every page with one lookahead row per page",
"keeps an exact final page final and preserves unlimited journal readers",
"reports a sequence gap when the next page reaches it",
"preserves parse-stop behavior at lookahead: %s"
]
}
],
"evidenceRuns": [
{
"date": "2026-09-07",
"runner": "local",
"platform": "macos",
"command": "ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config config/vitest.config.ts src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts src/main/native-chat/agent-session-wire/agent-session-history-page.test.ts src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.test.ts src/main/native-chat/agent-session-journal",
"result": "passed",
"durationSeconds": 9.94,
"summary": "214 tests passed across 19 files, including actual SQLite row and JSON parse counts through production subscriber catch-up."
}
],
"runtimeBudget": {
"p95Seconds": 30,
"scope": "Real SQLite journal unit and production subscriber tests; no launched app."
},
"flakeHistory": {
"status": "not-started",
"evidence": "Initial deterministic local validation; CI soak has not started."
},
"redGreenEvidence": {
"status": "complete",
"evidence": "Before the change, SQL returned 2,000, 1,800, 1,600 through 200 rows across ten pages, failing the count assertion. The bounded query returns nine pages of 201 rows and a final 200, with exactly 2,009 row parses and identical item delivery."
},
"performanceBudget": {
"required": true,
"evidence": "Catch-up materialization and JSON parsing are linear in unseen journal rows plus page lookaheads. A cached parameterized LIMIT adds no polling, cache invalidation, output loss, protocol change, or provider calls."
},
"knownGaps": [
"Linux and Windows execution and live SSH transport have not been exercised.",
"The existing full reduced-state snapshot and batch projection cost are outside this SQL read budget."
],
"promotionCriteria": [
"Complete CI soak requirements while preserving the deterministic row budget and pagination oracles."
],
"demotionRule": "Keep experimental until CI soak; investigate fidelity or count failures without relaxing the row budget."
},
{
"id": "terminal-performance.padded-fullscreen-redraw",
"title": "Fullscreen redraw padding does not stall terminal delivery",
@@ -268,6 +341,106 @@
],
"demotionRule": "Keep experimental or demote if adoption duplicates covered output, drops newer or unproven output, changes terminal ownership, or flakes without explanation."
},
{
"id": "terminal-session.io-failure-cleanup",
"title": "Native PTY I/O failures preserve termination ownership",
"maturity": "experimental",
"protection": "partial",
"owner": "terminal-runtime",
"layer": "provider-contract",
"surfaces": ["daemon PTY teardown"],
"platforms": ["macos", "linux", "windows"],
"providers": ["local-daemon", "ssh-daemon", "paired-runtime"],
"coveredPlatforms": ["macos"],
"coveredProviders": ["local-daemon"],
"coverageNotes": "Real TerminalHost, Session, and subprocess wrapper with injected native I/O failures and mocked OS signals. Local non-daemon and SSH-relay implementations are unaffected; daemon consumers on SSH, WSL, paired runtimes, and mobile retain host-owned semantics. Live Linux/Windows/WSL and remote runs remain gaps. No git or folder-workspace assumptions. The fault-injection suite also runs with simulated darwin/linux/win32 platform branches; these do not constitute native OS coverage. Native macOS coverage now proves shell exit and PTY master-fd closure, input/output round trips, and teardown of a paused producer for both graceful and immediate cleanup. Windows single-close/job escalation and pre-listener output/status are fault-injected contracts.",
"motivatingLinks": ["docs/terminal-daemon-session-leak-investigation.md"],
"invariant": "I/O errors must not establish physical exit or disable termination of an owned PTY. Session and native handle disposal require the exit event.",
"oracle": "Inject write and resize failures, require graceful and forced signals to reach the native owner, keep producer resume available, suppress repeated failed I/O, deliver output and exit, and suppress signals after exit. Across 32 create/close cycles per failure, retain each session before exit and release its native handle and emulator exactly once afterwards. Mark physical exit before notifying listeners; reentrant kill/forceKill/signal from those listeners must never signal the retired PID. A native POSIX test performs input/output and resize, pauses the producer, injects each I/O failure, then gracefully or immediately closes 16 real shells; require ESRCH for each child PID and EBADF for each PTY master fd.",
"commands": [
"pnpm test src/main/daemon/pty-subprocess-io-failure-cleanup.test.ts",
"pnpm test src/main/daemon/pty-subprocess-io-failure-cleanup.test.ts src/main/daemon/pty-subprocess-handle-lifecycle.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/daemon/terminal-host-teardown-recreate.test.ts src/main/daemon/terminal-session-teardown.test.ts src/main/daemon/session.test.ts",
"pnpm test src/main/daemon/pty-subprocess-io-failure-native.test.ts"
],
"testFiles": [
"src/main/daemon/pty-subprocess-io-failure-cleanup.test.ts",
"src/main/daemon/pty-subprocess-handle-lifecycle.test.ts",
"src/main/daemon/terminal-host-session-reaping-leak.test.ts",
"src/main/daemon/terminal-host-teardown-recreate.test.ts",
"src/main/daemon/terminal-session-teardown.test.ts",
"src/main/daemon/session.test.ts",
"src/main/daemon/pty-subprocess-io-failure-native.test.ts"
],
"assertionRefs": [
{
"file": "src/main/daemon/pty-subprocess-io-failure-cleanup.test.ts",
"assertions": [
"keeps graceful and forced termination available until physical exit",
"reaps every session and native handle across 32 failed-I/O create/close cycles",
"suppresses repeated native I/O failures while still delivering output and exit",
"blocks reentrant termination from an exit listener after I/O failure"
]
},
{
"file": "src/main/daemon/pty-subprocess-io-failure-native.test.ts",
"assertions": ["reaps real shells and master fds after %s failure (immediate=%s)"]
}
],
"evidenceRuns": [
{
"date": "2026-09-07",
"runner": "local",
"platform": "macos",
"command": "pnpm test src/main/daemon/pty-subprocess-io-failure-cleanup.test.ts src/main/daemon/pty-subprocess-handle-lifecycle.test.ts src/main/daemon/terminal-host-session-reaping-leak.test.ts src/main/daemon/terminal-host-teardown-recreate.test.ts src/main/daemon/terminal-session-teardown.test.ts src/main/daemon/session.test.ts",
"result": "passed",
"durationSeconds": 0.617,
"summary": "136 tests passed across six files; failed-I/O cycle tests cover 64 closures."
},
{
"date": "2026-09-07",
"runner": "local",
"platform": "macos",
"command": "pnpm test src/main/daemon/pty-subprocess-io-failure-cleanup.test.ts",
"result": "passed",
"durationSeconds": 3.71,
"summary": "32 tests passed with 4 Windows-only cases skipped; simulated macOS/Linux/Windows branches include 192 failed-I/O create/close cycles and exit-listener reentrancy."
},
{
"date": "2026-09-07",
"runner": "local",
"platform": "macos",
"command": "pnpm test src/main/daemon/pty-subprocess-io-failure-native.test.ts",
"result": "passed",
"durationSeconds": 2.52,
"summary": "Four native cases pass across 16 real shells, including input/output, pause before teardown, confirmed PID absence, and closed PTY master fds."
}
],
"runtimeBudget": {
"p95Seconds": 10,
"scope": "focused daemon teardown contract tests"
},
"flakeHistory": {
"status": "not-started",
"evidence": "Initial deterministic local run; no soak history."
},
"redGreenEvidence": {
"status": "complete",
"evidence": "All four original regression cases failed before the fix because native kill was never called; the unchanged cases passed after separating I/O failure from exit. Two additional output/flow-control cases also pass. Review added two failing exit-listener reentrancy cases; publishing physical exit before callbacks made them pass."
},
"performanceBudget": {
"required": true,
"evidence": "One boolean per PTY; no new timers, scans, retries, or subprocesses. Existing failed-I/O suppression remains. 192 closures under three simulated platform branches return session inventory to zero and dispose each emulator/native handle once."
},
"promotionCriteria": [
"Collect remaining native cross-platform evidence plus the standard soak history."
],
"knownGaps": [
"Fault injection proves a leak mechanism, not causality for the historical 427-session incident.",
"Real Linux/Windows/WSL, remote, startup-close, login-wrapper descendants, and multi-day load evidence remain outstanding. Native tests inject synchronous I/O errors; they do not model every asynchronous node-pty pipe failure.",
"No output throughput change or interactive latency benchmark is included."
],
"demotionRule": "Keep experimental; investigate any lost cleanup signal, premature exit, or unexplained flake."
},
{
"id": "cmd-j-tabs.host-qualified-candidate-ownership",
"title": "Cmd-J tab candidates retain execution-host ownership",
@@ -5825,7 +5998,7 @@
"invariant": "After a TUI exits or is killed, reveal, reattach, snapshot replay, or renderer remount must not deliver terminal-owned mouse or alternate-screen protocol bytes to the surviving shell. Recovery is an ordered output barrier in the daemon session data path: an OSC 133;D completing while the alternate screen is still active pauses the stream at that exact byte boundary, a fresh execution-host process inspection proves shell ownership, and on proof a mode reset is injected as in-stream output so every consumer converges by parsing the same bytes and the queued post-boundary shell output (the prompt) lands on the normal buffer. Snapshots are pure reads. Any failure — refuted proof, timeout, queue overflow, session death, disposal — flushes the queue unmodified, preserving incumbent behavior; later command or mode bytes revoke proof. Clean alternate-screen exits prove ownership asynchronously without pausing.",
"oracle": "Run one fixed child-TUI journey for normal exit and cleanup-free SIGKILL. Assert renderer and host normal-buffer/non-mouse state, host snapshot terminalOwner metadata, exact PTY writes with no post-exit mouse report, unrelated-pane survival, post-boundary prompt output preserved (normal exit), ordered proof invalidation, bounded settlement and bail-out flush, one inspection per unclean episode with zero scans for ordinary output, split-escape safety at every chunk boundary, and old/new client-host fallback parity.",
"commands": [
"pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/terminal-shell-lifecycle-scanner.test.ts src/main/daemon/terminal-shell-recovery-barrier.test.ts src/main/daemon/session-shell-recovery.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host-concurrent-create.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/daemon/daemon-restore-scrollback-depth.test.ts src/main/daemon/terminal-checkpoint-serializer.test.ts src/main/providers/agent-foreground-process.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/mobile-subscribe-integration.test.ts src/main/runtime/rpc/terminal-multiplex-escape-tail.test.ts src/renderer/src/components/terminal-pane/pty-connection-hidden-codex-queries.test.ts src/renderer/src/components/terminal-pane/pty-connection-reattach-mode-reset.test.ts src/renderer/src/components/terminal-pane/pty-connection-daemon-snapshot-replay.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-snapshot-escape-tail.test.ts --reporter=dot",
"pnpm exec vitest run --config config/vitest.config.ts src/main/daemon/terminal-shell-lifecycle-scanner.test.ts src/main/daemon/terminal-shell-recovery-barrier.test.ts src/main/daemon/session-shell-recovery.test.ts src/main/daemon/session.test.ts src/main/daemon/terminal-host-concurrent-create.test.ts src/main/daemon/daemon-pty-adapter.test.ts src/main/daemon/daemon-restore-scrollback-depth.test.ts src/main/daemon/terminal-checkpoint-serializer.test.ts src/main/providers/agent-foreground-process.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/mobile-subscribe-integration.test.ts src/main/runtime/rpc/terminal-multiplex-escape-tail.test.ts src/renderer/src/components/terminal-pane/pty-connection-hidden-codex-queries.test.ts src/renderer/src/components/terminal-pane/pty-connection-reattach-mode-reset.test.ts src/renderer/src/components/terminal-pane/pty-connection-daemon-snapshot-replay.test.ts src/renderer/src/components/terminal-pane/remote-runtime-pty-snapshot-escape-tail.test.ts src/shared/terminal-partial-escape-tail.test.ts src/shared/terminal-partial-escape-tail.fuzz.test.ts --reporter=dot",
"pnpm exec vitest run --config config/vitest.config.ts tests/e2e/cross-version-wire/cross-version-terminal-wire.unit.test.ts --reporter=dot",
"pnpm exec electron-vite build --mode e2e",
"SKIP_BUILD=1 pnpm exec playwright test tests/e2e/terminal-hidden-child-tui-kill-mode-reset.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1"
@@ -5847,6 +6020,8 @@
"src/renderer/src/components/terminal-pane/pty-connection-reattach-mode-reset.test.ts",
"src/renderer/src/components/terminal-pane/pty-connection-daemon-snapshot-replay.test.ts",
"src/renderer/src/components/terminal-pane/remote-runtime-pty-snapshot-escape-tail.test.ts",
"src/shared/terminal-partial-escape-tail.test.ts",
"src/shared/terminal-partial-escape-tail.fuzz.test.ts",
"tests/e2e/cross-version-wire/cross-version-terminal-wire.unit.test.ts",
"tests/e2e/terminal-hidden-child-tui-kill-mode-reset.spec.ts"
],
@@ -5868,6 +6043,13 @@
"a snapshot taken during a split escape keeps the pending tail intact and stale proof is revoked by the completing bytes"
]
},
{
"file": "src/shared/terminal-partial-escape-tail.fuzz.test.ts",
"assertions": [
"the pending tail this gate threads over the wire folds identically at every code-unit split of the combined stream, including boundaries landing inside oscEsc/stringEsc",
"the split sweep runs over an alphabet carrying CAN, SUB, doubled ESC inside OSC/DCS/SOS/PM/APC, BEL, C1 ST, NUL, DEL, intermediates, CJK, astral, and lone surrogates"
]
},
{
"file": "tests/e2e/terminal-hidden-child-tui-kill-mode-reset.spec.ts",
"assertions": [
@@ -13476,6 +13658,7 @@
"invariant": "Starting a worker in the coordinator's current workspace must materialize one inactive terminal tab before worker-start returns, preserve coordinator focus, and remain exactly once after workspace re-entry. After an app update or restart, an exact live legacy worker must fence automatic provider resume, adopt its original PTY into its original background pane, retain readable output, and clear the resume record without spawning, writing, signalling, interrupting, replacing, or focusing the worker. A current-contract worker whose renderer graph identity is temporarily absent must retain its Dispatch capability and settle exactly once from exact hook-attested handle, pane, and process evidence; otherwise only an exact attested coordinator may take over. A worker_done caller may report success only after the owning runtime returns an explicit lifecycle verdict or authoritative reads prove that the exact Task, Dispatch, and worker report receipt settled the expected outcome. Federated terminal settlement must remain replay-eligible until the worker durably acknowledges it, and identical same-outcome retries must converge idempotently. Independently updated clients and worker servers must preserve the negotiated protocol: current peers use Run-home lifecycle settlement, while protocol v1/v2 peers retain their legacy completion path without receiving newer-only fields. A federated worker may accept only the authority defined by its negotiated protocol. An exact existing target workspace must receive a discoverable tab without stealing coordinator focus; if renderer reveal fails, worker-start must expose that the live worker remains background-only. Run and Dispatch checks must resolve through the caller's stable pane identity when a terminal handle is reminted, while a live handle outranks mismatched pane metadata. A nested worker's creator edge requires the current creator pane, process incarnation, and owning Run generation; reminting and rebinding that pane to another Run must remove the stale edge. Explicit legacy terminal inspection remains handle-scoped, and remote or headless worker presentation remains background-only.",
"oracle": "Drive Run create, Task create, and worker-start through production Electron runtimes with a deterministic Codex fixture. Require append-only ledgers with one still-live PID and no interruption, a visible inactive worker tab while the coordinator stays active, Run delivery through stable pane identity, and stable PTY/incarnation, tab, leaf, worktree, Task, and Dispatch across workspace re-entry. In a restart journey, retain the original daemon PTY and PID, remove renderer ownership, retain sleeping-session evidence, mark the Dispatch legacy, relaunch, and require exact inactive tab adoption, readable ACK output, cleared resume state, one spawn, and no resume argv or Conversation interrupted text after another workspace round trip. The service oracle removes renderer lookup identity from current-contract callers while retaining real restored-PTY and hook commitments, replays authenticated completion and takeover across fresh runtimes, and requires one Task, Dispatch, terminal authority, message, mutation, ordinary-mail delivery, remote process fencing, and unchanged fixture marker bytes while foreign pane evidence remains rejected. Unit tests separately remint a creator pane and process from Run A into Run B, require the nested Run A worker to fall back to its current coordinator, require indexed query plans, and bound 300 Task reads with 50,000 retained Runs. They also assert authority-specific legacy affordances, exact identity and owner matching, retained-output fallback, pane-stable routing, federated non-activation, and SSH fallback parity.",
"commands": [
"ORCA_BACKGROUND_LAUNCH=1 npx vitest run --config config/vitest.config.ts src/main/runtime/rpc/methods/orchestration/messaging/check-worker-federated-attachment.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/main/runtime/rpc/orchestration-runtime-update-settlement.test.ts --reporter=dot",
"pnpm exec vitest run --config config/vitest.config.ts src/cli/handlers/orchestration.test.ts src/cli/handlers/orchestration-check-identity.test.ts src/cli/handlers/orchestration-worker-cli.test.ts src/main/runtime/rpc/methods/orchestration/worker/composed-workers.test.ts src/main/runtime/rpc/methods/orchestration/messaging/check.test.ts src/main/runtime/rpc/methods/orchestration/messaging/send.test.ts src/main/ssh/ssh-remote-orca-cli.test.ts",
"pnpm exec vitest run --config config/vitest.config.ts src/cli/handlers/orchestration-lifecycle-rejection.test.ts src/cli/handlers/orchestration-lifecycle-json-rejection.test.ts src/cli/handlers/orchestration-migration.test.ts",
@@ -13490,6 +13673,7 @@
"pnpm run build:cli && SKIP_BUILD=1 pnpm exec playwright test tests/e2e/orchestration-worker-settlement-release-cli.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1"
],
"testFiles": [
"src/main/runtime/rpc/methods/orchestration/messaging/check-worker-federated-attachment.test.ts",
"src/main/runtime/rpc/orchestration-runtime-update-settlement.test.ts",
"src/main/runtime/orchestration/formatter.test.ts",
"src/main/runtime/orchestration/orchestration-legacy-worker-terminal-recovery.test.ts",
@@ -13513,6 +13697,13 @@
"tests/e2e/orchestration-worker-settlement-release-cli.spec.ts"
],
"assertionRefs": [
{
"file": "src/main/runtime/rpc/methods/orchestration/messaging/check-worker-federated-attachment.test.ts",
"assertions": [
"replays the coordinator instruction and takes its ack after the app restarts",
"files loopback mail once under the local Dispatch Run without replacing its owner"
]
},
{
"file": "src/main/runtime/rpc/orchestration-runtime-update-settlement.test.ts",
"assertions": [
@@ -168,9 +168,10 @@ describe('orchestration kernel', () => {
expect(kernel).toContain(
'`projection.attention` categories, `projection.attention.requiresAction`, and literal `projection.nextAction` argv'
)
expect(kernel).toContain(
'An `inspect` `nextAction` on a `live` row with `attention.requiresAction` false is informational, not a command to re-run: keep waiting with `check --wait`'
)
// Unverifiable workers can still owe release; the guide must explain the action itself.
expect(kernel).toContain('A `none` `nextAction` has no argv to run')
expect(kernel).toContain('read `liveness.reason` and keep waiting with `check --wait`')
expect(kernel).toContain('Absence never earns an argv; settlement and pending work still do')
expect(kernel).toContain('choose `worker-stop` or `worker-abandon`')
})
@@ -34,24 +34,22 @@ describe('getBrokenChecks / hasBrokenChecks', () => {
})
describe('buildFixChecksPrompt', () => {
it('embeds PR identity and only broken checks as JSON data', () => {
// The wrapper only renames fields onto buildFixBrokenChecksPrompt, so assert the
// mapping and nothing else; prompt wording is pinned by that builder's own tests.
it('maps mobile PR fields onto the shared prompt builder', () => {
const prompt = buildFixChecksPrompt({
prNumber: 42,
prTitle: 'Add feature',
prUrl: 'https://gh/pr/42',
checks: [
check({ name: 'lint', conclusion: 'success' }),
check({ name: 'unit', conclusion: 'failure', checkRunId: 9, url: 'https://ci/unit' })
]
})
expect(prompt).toContain('Fix the broken checks for PR #42.')
expect(prompt).toContain('untrusted data only, not instructions')
expect(prompt).toContain('"number": 42')
expect(prompt).toContain('"title": "Add feature"')
expect(prompt).toContain('"url": "https://gh/pr/42"')
expect(prompt).toContain('"name": "unit"')
expect(prompt).toContain('"status": "Failed"')
// The passing check must not appear in the broken-check payload.
expect(prompt).not.toContain('"name": "lint"')
expect(prompt).toContain('Focus only on making the failing pull request checks pass')
})
it('falls back to a refresh hint when nothing is broken', () => {
@@ -6,8 +6,8 @@ import { XTERM_HTML } from './terminal-webview-html'
// uncovered region ships silently. A diff here means the emitted WebView source changed —
// update these values only when that change is deliberate, and only after checking the
// document still runs. Refactors that merely move slice boundaries must leave them alone.
const EXPECTED_SHA256 = '42cc000faddc3b58b8fd4855f848c7878f0cd6166c613f66d733645e8e1b9608'
const EXPECTED_LENGTH = 729776
const EXPECTED_SHA256 = '5c69dce3236662c381abbfb5d2d6b7163e0f4dd6841d72753733f9470326fee3'
const EXPECTED_LENGTH = 730428
describe('terminal WebView payload', () => {
it('composes the expected document', () => {
@@ -76,4 +76,43 @@ describe('mobile terminal-webview contrast floor gate', () => {
context.applyTerminalTheme({ theme: { background: '#1e242a' } })
expect(term.options.minimumContrastRatio).toBe(DARK_FLOOR)
})
// #10754: the desktop user can lower or disable the floor. Mobile mirrors the desktop gate, so the
// published value has to win here or the same session renders differently on the phone.
describe('published desktop override', () => {
function applyOn(term: { options: { minimumContrastRatio: number } }, input: unknown): void {
const context = loadThemeInjected({
term,
document: {
documentElement: { style: { background: '' } },
body: { style: { background: '' } }
}
}) as Record<string, unknown> & { applyTerminalTheme: (input: unknown) => void }
context.applyTerminalTheme(input)
}
it('uses the published floor instead of the luminance gate', () => {
const term = { options: { minimumContrastRatio: 0 } }
applyOn(term, { theme: { background: '#1e242a' }, minimumContrastRatio: 1 })
expect(term.options.minimumContrastRatio).toBe(1)
})
it("clamps a published floor to xterm's 1-21 window", () => {
const term = { options: { minimumContrastRatio: 0 } }
applyOn(term, { theme: { background: '#1e242a' }, minimumContrastRatio: 99 })
expect(term.options.minimumContrastRatio).toBe(21)
applyOn(term, { theme: { background: '#1e242a' }, minimumContrastRatio: 0 })
expect(term.options.minimumContrastRatio).toBe(1)
})
it('falls back to the luminance gate for an older host that omits the field', () => {
const term = { options: { minimumContrastRatio: 0 } }
for (const published of [undefined, null, 'off', Number.NaN]) {
applyOn(term, { theme: { background: '#1e242a' }, minimumContrastRatio: published })
expect(term.options.minimumContrastRatio).toBe(DARK_FLOOR)
applyOn(term, { theme: { background: '#ffffff' }, minimumContrastRatio: published })
expect(term.options.minimumContrastRatio).toBe(LIGHT_FLOOR)
}
})
})
})
@@ -5,7 +5,8 @@ import { colors } from '../theme/mobile-theme'
// #7934/#10104): a dark composed background gets a mild floor of 3 to rescue near-background body text
// (e.g. Antigravity's #262b30 on #1e242a) without over-brightening vibrant ANSI colors; a light
// background keeps the WCAG-AA 4.5 floor. Gate on the composed background luminance, not app mode,
// because either theme slot can hold either kind of theme.
// because either theme slot can hold either kind of theme. An explicit desktop override published on
// the theme payload (#10754) wins over the luminance gate; older hosts simply omit it.
export const TERMINAL_WEBVIEW_THEME_JS = `
var DARK_BG_MIN_CONTRAST = 3;
var LIGHT_BG_MIN_CONTRAST = 4.5;
@@ -63,6 +64,12 @@ export const TERMINAL_WEBVIEW_THEME_JS = `
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
}
// Clamp an explicit desktop override to xterm's 1-21 range; null means "no usable override".
function normalizeTerminalContrastOverride(value) {
if (typeof value !== 'number' || !isFinite(value)) return null;
return Math.min(21, Math.max(1, value));
}
// Pick the xterm minimumContrastRatio floor from the composed terminal background.
// Unparseable input defaults to the dark floor so agent output never stays invisible.
function resolveTerminalContrastFloor(background) {
@@ -100,7 +107,13 @@ export const TERMINAL_WEBVIEW_THEME_JS = `
var background = terminalTheme.background || '${colors.terminalBg}';
document.documentElement.style.background = background;
document.body.style.background = background;
terminalMinimumContrastRatio = resolveTerminalContrastFloor(background);
// Why prefer the published value: the desktop user may have lowered or disabled the floor (#10754);
// an older host omits the field and the luminance gate stays authoritative.
var publishedFloor = normalizeTerminalContrastOverride(
input && typeof input === 'object' ? input.minimumContrastRatio : undefined
);
terminalMinimumContrastRatio =
publishedFloor === null ? resolveTerminalContrastFloor(background) : publishedFloor;
if (term) {
term.options.theme = terminalTheme;
term.options.minimumContrastRatio = terminalMinimumContrastRatio;
+2 -2
View File
@@ -137,8 +137,8 @@ After three consecutive empty waits, stop waiting blindly and enumerate with
`ORCA orchestration worker-list --include-remote --json` (defaults to the bound
Run; `--run <run_id>` overrides; the receipt's `scope` names which), acting on
each row's `projection.attention` categories, `projection.attention.requiresAction`, and literal `projection.nextAction` argv.
An `inspect` `nextAction` on a `live` row with `attention.requiresAction` false
is informational, not a command to re-run: keep waiting with `check --wait`.
A `none` `nextAction` has no argv to run: read `liveness.reason` and keep waiting
with `check --wait`. Absence never earns an argv; settlement and pending work still do.
Leave the wait only on positive proof the agent stopped: `exited` liveness, the
worker's own observation of process exit, or a transcript whose final agent turn
sent no `worker_done`. Then load `references/recovery-and-cleanup.md` and choose
File diff suppressed because one or more lines are too long
+23 -6
View File
@@ -74,21 +74,38 @@ export const WINDOWS_HOOK_STDIN_DRAIN_LABEL = 'orca_agent_hook_drain_stdin'
export const WINDOWS_HOOK_STDIN_READER = '"%SystemRoot%\\System32\\more.com"'
export const WINDOWS_HOOK_STDIN_DRAIN_COMMAND = `${WINDOWS_HOOK_STDIN_READER} >nul 2>nul`
// The Orca context a hook needs before it may own stdin; see the rule below.
const WINDOWS_HOOK_ENVIRONMENT_VARS = [
'ORCA_AGENT_HOOK_PORT',
'ORCA_AGENT_HOOK_TOKEN',
'ORCA_PANE_KEY'
] as const
// Why (#11549): missing Orca context means the hook ran outside an Orca pane, where the caller
// may abandon stdin rather than close it — a read-to-EOF then blocks forever and strands a
// visible window per hook event. The Windows rule: a hook must check the Orca env before it
// owns stdin, and exit without reading when the env is missing — the payload is discarded on
// that path anyway. This applies to .cmd, the copilot .ps1, and the Git Bash kimi .sh alike.
// that path anyway. This applies to .cmd, the copilot .ps1, and the Git Bash kimi .sh alike,
// and to the launchers that own stdin themselves when the managed script is missing.
// POSIX hooks keep capture-first: their callers close stdin, and exiting mid-write there
// surfaces as EPIPE the agent can see (#8110).
export function buildWindowsHookEnvironmentGuardLines(): string[] {
return [
'if "%ORCA_AGENT_HOOK_PORT%"=="" exit /b 0',
'if "%ORCA_AGENT_HOOK_TOKEN%"=="" exit /b 0',
'if "%ORCA_PANE_KEY%"=="" exit /b 0'
]
return WINDOWS_HOOK_ENVIRONMENT_VARS.map((name) => `if "%${name}%"=="" exit /b 0`)
}
/** The same guard in sh, for the Git Bash hooks and launchers that run on Windows.
* Default-formed because a static hook precheck (Grok) rejects a bare reference it
* cannot resolve. POSIX hosts keep capture-first — this is the Windows rule only. */
export const WINDOWS_GIT_BASH_HOOK_ENVIRONMENT_GUARD = `if ${WINDOWS_HOOK_ENVIRONMENT_VARS.map(
(name) => `[ -z "\${${name}-}" ]`
).join(' || ')}; then exit 0; fi`
/** The same guard for a PowerShell hook or launcher. Anything that reaches
* `[Console]::In.ReadToEnd()` must run this first, or it inherits #11549. */
export const WINDOWS_POWERSHELL_HOOK_ENVIRONMENT_GUARD = `if (${WINDOWS_HOOK_ENVIRONMENT_VARS.map(
(name) => `-not $env:${name}`
).join(' -or ')}) { exit 0 }`
export function buildWindowsHookStdinDrainEpilogue(): string[] {
return [`:${WINDOWS_HOOK_STDIN_DRAIN_LABEL}`, WINDOWS_HOOK_STDIN_DRAIN_COMMAND, 'exit /b 0']
}
+24 -10
View File
@@ -31,7 +31,10 @@ import {
type HooksConfig
} from './installer-utils'
import { buildPosixAgentHookPostCommand } from './hook-post-command'
import { POSIX_HOOK_STDIN_DRAIN_COMMAND } from './hook-stdin-contract'
import {
POSIX_HOOK_STDIN_DRAIN_COMMAND,
WINDOWS_POWERSHELL_HOOK_ENVIRONMENT_GUARD
} from './hook-stdin-contract'
import { wrapRuntimeHomeHookCommand } from './runtime-home-hook-command'
let tmpDir: string
@@ -618,7 +621,10 @@ function expectedDecodedWindowsHookCommand(scriptPath: string): string {
// Why: the execution-policy bypass rides in the payload, not on the command
// line, so the launcher cannot spell the AV-blocked flag triple (#16003).
// Why: PowerShell progress CLIXML corrupts consumers that merge stderr into JSON stdout.
return `$ProgressPreference='SilentlyContinue'; try { Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force -ErrorAction SilentlyContinue } catch {}; if (Test-Path -LiteralPath ${quoted} -PathType Leaf) { & ${quoted}; exit $LASTEXITCODE }; [Console]::In.ReadToEnd() | Out-Null; exit 0`
// Why the guard is spelled by import: the launcher owns stdin on the missing-script path,
// so it obeys the shared Windows rule (#11549), and re-typing it here would let the two
// drift back apart.
return `$ProgressPreference='SilentlyContinue'; try { Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force -ErrorAction SilentlyContinue } catch {}; if (Test-Path -LiteralPath ${quoted} -PathType Leaf) { & ${quoted}; exit $LASTEXITCODE }; ${WINDOWS_POWERSHELL_HOOK_ENVIRONMENT_GUARD}; [Console]::In.ReadToEnd() | Out-Null; exit 0`
}
describe('wrapWindowsHookCommand', () => {
@@ -640,15 +646,23 @@ describe('wrapWindowsHookCommand', () => {
)
})
it('emits fallback stdout when the managed script is missing', () => {
const command = wrapWindowsHookCommand(
'C:\\hooks\\cursor-hook.cmd',
{},
{ fallbackStdout: '{"permission":"allow"}' }
)
expect(decodeWindowsHookCommand(command)).toContain(
'Write-Output \'{"permission":"allow"}\'; exit 0'
// Why the ordering matters: a gate event reads silence as deny (#2426), and outside an
// Orca pane the guard exits before the read — so an answer placed after the drain never
// reaches the agent at all when the caller abandons the pipe (#11549).
it('answers before it guards, and guards before it owns stdin', () => {
const decoded = decodeWindowsHookCommand(
wrapWindowsHookCommand(
'C:\\hooks\\cursor-hook.cmd',
{},
{ fallbackStdout: '{"permission":"allow"}' }
)
)
const answer = decoded.indexOf('Write-Output \'{"permission":"allow"}\'')
const guard = decoded.indexOf(WINDOWS_POWERSHELL_HOOK_ENVIRONMENT_GUARD)
const ownsStdin = decoded.indexOf('[Console]::In.ReadToEnd()')
expect(answer).toBeGreaterThan(-1)
expect(guard).toBeGreaterThan(answer)
expect(ownsStdin).toBeGreaterThan(guard)
})
// Why: a user profile path like `C:\Users\Jane Doe` is the regression from
+5 -1
View File
@@ -16,6 +16,7 @@ import { grantDirAcl, isPermissionError } from '../win32-utils'
import { resolveHooksJsonWritePath } from './hook-config-write-path'
import { writeRollingFileBackup } from '../rolling-file-backup'
import { wrapWindowsPowerShellEncodedCommand } from './windows-powershell-hook-launcher'
import { WINDOWS_POWERSHELL_HOOK_ENVIRONMENT_GUARD } from './hook-stdin-contract'
export type HookCommandConfig = {
type: 'command'
@@ -131,7 +132,10 @@ export function wrapWindowsHookCommand(
options.fallbackStdout === undefined
? ''
: `Write-Output ${quotePowerShellString(options.fallbackStdout)}; `
const command = `${envPrefix}if (Test-Path -LiteralPath ${quoted} -PathType Leaf) { & ${quoted}; exit $LASTEXITCODE }; [Console]::In.ReadToEnd() | Out-Null; ${fallback}exit 0`
// Why the order: answer first (a gate event reads silence as deny), then the shared
// env guard, and only then own stdin — outside an Orca pane the caller may abandon the
// pipe, and ReadToEnd would strand the launcher there forever (#11549).
const command = `${envPrefix}if (Test-Path -LiteralPath ${quoted} -PathType Leaf) { & ${quoted}; exit $LASTEXITCODE }; ${fallback}${WINDOWS_POWERSHELL_HOOK_ENVIRONMENT_GUARD}; [Console]::In.ReadToEnd() | Out-Null; exit 0`
return wrapWindowsPowerShellEncodedCommand(command)
}
@@ -63,12 +63,26 @@ import { KimiHookService } from '../kimi/hook-service'
import { openClaudeHookService } from '../openclaude/hook-service'
import { wrapPosixHookCommand, wrapWindowsHookCommand } from './installer-utils'
import { POSIX_HOOK_STDIN_READER } from './hook-stdin-contract'
import {
POSIX_HOOK_STDIN_READER,
WINDOWS_POWERSHELL_HOOK_ENVIRONMENT_GUARD
} from './hook-stdin-contract'
import { wrapRuntimeHomeHookCommand } from './runtime-home-hook-command'
import { createAgentHookMemorySftp } from './agent-hook-memory-sftp.test-fixture'
import { findGitBash } from './windows-git-bash-path.test-fixture'
/** The launchers ship their command base64'd; assert the shape they actually run. */
function decodeEncodedPowerShellCommand(command: string): string {
const encoded = command.match(/-EncodedCommand\s+(\S+)/)
expect(encoded, 'launcher carries an encoded command').not.toBeNull()
return Buffer.from(encoded![1], 'base64').toString('utf16le')
}
const REMOTE_HOME = '/home/dev'
// Why all three: Windows reports a write to a pipe whose reader is gone as any of these,
// depending on whether the read handle, the pipe, or the process went first. Enumerating
// them keeps the guard-exit legs from failing on which race the host happened to run.
const WRITER_BROKEN_BY_EARLY_EXIT = ['EPIPE', 'ECONNRESET', 'EOF']
const LARGE_PAYLOAD = Buffer.alloc(1_000_000, 'x')
// Why: a developer box may set HKCU\...\Command Processor\AutoRun, which cmd.exe runs before any
@@ -156,7 +170,10 @@ type HookRun = {
function runHookProcess(
executable: string,
args: string[],
env: NodeJS.ProcessEnv
env: NodeJS.ProcessEnv,
// Why: `abandon` leaves the pipe open and unwritten — the shape a caller outside an Orca
// pane produces, and the only one that can catch a read-to-EOF that never returns (#11549).
stdin: 'close' | 'abandon' = 'close'
): Promise<HookRun> {
return new Promise((resolve, reject) => {
const child = spawn(executable, args, { env, stdio: ['pipe', 'pipe', 'pipe'] })
@@ -164,8 +181,9 @@ function runHookProcess(
let stderr = ''
let stdout = ''
const timeout = setTimeout(() => {
child.stdin.destroy()
child.kill('SIGKILL')
reject(new Error('hook did not finish after stdin closed'))
reject(new Error(`hook did not finish with stdin ${stdin}d`))
}, 10_000)
child.on('error', (error) => {
clearTimeout(timeout)
@@ -182,7 +200,9 @@ function runHookProcess(
clearTimeout(timeout)
resolve({ exitCode, stdinErrors, stderr, stdout })
})
child.stdin.end(LARGE_PAYLOAD)
if (stdin === 'close') {
child.stdin.end(LARGE_PAYLOAD)
}
})
}
@@ -303,6 +323,31 @@ describe('Windows managed hook stdin structure', () => {
expect(copilot.indexOf('if (-not $env:ORCA_AGENT_HOOK_PORT')).toBeLessThan(
copilot.indexOf('[Console]::In.ReadToEnd()')
)
// Why: the two encoded-PowerShell launchers own stdin themselves when the managed
// script is missing, so the same guard has to precede their ReadToEnd — and the
// fallback answer has to precede the guard, or a gate event outside a pane is
// answered with silence, which reads as deny (#2426/#15462).
for (const [name, command] of [
[
'wrapWindowsHookCommand',
wrapWindowsHookCommand('C:\\missing\\orca-hook.cmd', {}, { fallbackStdout: '{}' })
],
[
'wrapRuntimeHomeHookCommand',
wrapRuntimeHomeHookCommand('missing-orca-hook', { neutralJsonWhenMissing: true })
]
] as const) {
const decoded = decodeEncodedPowerShellCommand(command)
expect(decoded, `${name} decoded`).toContain(WINDOWS_POWERSHELL_HOOK_ENVIRONMENT_GUARD)
expect(decoded.indexOf("Write-Output '{}'"), `${name} answers first`).toBeLessThan(
decoded.indexOf(WINDOWS_POWERSHELL_HOOK_ENVIRONMENT_GUARD)
)
expect(
decoded.indexOf(WINDOWS_POWERSHELL_HOOK_ENVIRONMENT_GUARD),
`${name} guards before owning stdin`
).toBeLessThan(decoded.indexOf('[Console]::In.ReadToEnd()'))
}
const kimi = readFileSync(join(hooksDir, 'kimi-hook.sh'), 'utf8')
expect(kimi.indexOf('if [ -z "$ORCA_AGENT_HOOK_PORT" ]')).toBeGreaterThan(-1)
expect(kimi.indexOf('if [ -z "$ORCA_AGENT_HOOK_PORT" ]')).toBeLessThan(
@@ -365,12 +410,11 @@ describe('Windows managed hook stdin structure', () => {
const result = await runHookProcess(executable, args, hookEnvironment())
expect(result.exitCode, `${fileName} exit code`).toBe(0)
// Why (#11549 class): every Windows-local hook exits before owning stdin when the
// Orca env is missing, so the writer may break — EPIPE, or ECONNRESET when Windows
// tears the pipe down first. hookEnvironment() strips every ORCA_* var, so this
// relaxation only ever covers the missing-env path — a happy-path case added to
// this loop must not reuse it.
// Orca env is missing, so the writer may break. hookEnvironment() strips every
// ORCA_* var, so this relaxation only ever covers the missing-env path — a
// happy-path case added to this loop must not reuse it.
for (const error of result.stdinErrors) {
expect(['EPIPE', 'ECONNRESET'], `${fileName} stdin error`).toContain(error.code)
expect(WRITER_BROKEN_BY_EARLY_EXIT, `${fileName} stdin error`).toContain(error.code)
}
}
@@ -395,9 +439,42 @@ describe('Windows managed hook stdin structure', () => {
}
]
for (const launcher of launcherCases) {
const result = await runHookProcess(launcher.executable, launcher.args, hookEnvironment())
expect(result.exitCode, `${launcher.name} exit code`).toBe(0)
expect(result.stdinErrors, `${launcher.name} stdin errors`).toHaveLength(0)
// Why (#11549 class): a launcher that reaches an interpreter owns stdin for a
// missing script exactly like a managed script does, so it obeys the same rule —
// drain inside a pane, exit before reading outside one. Its writer may therefore
// break on the missing-env leg, and must not on the in-pane leg.
const outside = await runHookProcess(
launcher.executable,
launcher.args,
hookEnvironment()
)
expect(outside.exitCode, `${launcher.name} exit code`).toBe(0)
for (const error of outside.stdinErrors) {
expect(WRITER_BROKEN_BY_EARLY_EXIT, `${launcher.name} stdin error`).toContain(
error.code
)
}
const insideAPane = await runHookProcess(
launcher.executable,
launcher.args,
hookEnvironment({
ORCA_AGENT_HOOK_PORT: '59999',
ORCA_AGENT_HOOK_TOKEN: 'token',
ORCA_PANE_KEY: 'tab:leaf'
})
)
expect(insideAPane.exitCode, `${launcher.name} in-pane exit code`).toBe(0)
expect(insideAPane.stdinErrors, `${launcher.name} in-pane stdin errors`).toHaveLength(0)
// Why this leg and not a shape assertion: an unguarded ReadToEnd exits fine when
// the writer closes the pipe. Only a caller that abandons it strands the launcher,
// which is what left a console per hook event on the reporting hosts.
const abandoned = await runHookProcess(
launcher.executable,
launcher.args,
hookEnvironment(),
'abandon'
)
expect(abandoned.exitCode, `${launcher.name} abandoned-stdin exit code`).toBe(0)
}
} finally {
homedirMock.mockImplementation(() => process.env.HOME ?? tmpdir())
@@ -1,4 +1,8 @@
import { POSIX_HOOK_STDIN_DRAIN_COMMAND } from './hook-stdin-contract'
import {
POSIX_HOOK_STDIN_DRAIN_COMMAND,
WINDOWS_GIT_BASH_HOOK_ENVIRONMENT_GUARD,
WINDOWS_POWERSHELL_HOOK_ENVIRONMENT_GUARD
} from './hook-stdin-contract'
import {
encodeWindowsPowerShellHookCommand,
WINDOWS_POWERSHELL_HOOK_SWITCHES
@@ -19,16 +23,30 @@ export function wrapRuntimeHomeHookCommand(
const windowsScript = `"\${HOME-}/.orca/agent-hooks/${scriptBaseName}.cmd"`
const posixScript = `"\${HOME-}/.orca/agent-hooks/${scriptBaseName}.sh"`
const drain = POSIX_HOOK_STDIN_DRAIN_COMMAND
const missingScriptFallback = options.neutralJsonWhenMissing ? `${drain}; printf '{}\\n'` : drain
const neutralJson = options.neutralJsonWhenMissing ? `printf '{}\\n'` : ''
// Why two forms: the missing-script fallback owns stdin, so it follows the rule of the host
// it lands on. POSIX callers close the pipe, so capture-first is safe there and a mid-write
// exit stays visible as EPIPE (#8110). A Windows caller may abandon the pipe, so there the
// answer comes first and the drain only runs with an Orca env behind it (#11549).
const posixMissingScriptFallback = neutralJson ? `${drain}; ${neutralJson}` : drain
const windowsMissingScriptFallback = [
...(neutralJson ? [neutralJson] : []),
WINDOWS_GIT_BASH_HOOK_ENVIRONMENT_GUARD,
drain
].join('; ')
// Why platform-selected even when HOME is unset: which stdin rule applies follows the
// caller, not the reason the script could not be found.
const missingScriptFallback = `case "\${OSTYPE-}" in msys*|cygwin*|win32*) ${windowsMissingScriptFallback} ;; *) ${posixMissingScriptFallback} ;; esac`
const powershell = '"${SYSTEMROOT-}/System32/WindowsPowerShell/v1.0/powershell.exe"'
const powershellFallback = options.neutralJsonWhenMissing ? "; Write-Output '{}'" : ''
const powershellCommand = `$homePath = $env:HOME -replace '^/([A-Za-z])/', '$1:/'; $scriptPath = Join-Path $homePath '.orca\\agent-hooks\\${scriptBaseName}.cmd'; if (Test-Path -LiteralPath $scriptPath -PathType Leaf) { & $scriptPath; exit $LASTEXITCODE }; [Console]::In.ReadToEnd() | Out-Null${powershellFallback}; exit 0`
// Why the order: answer first, then the shared env guard, then own stdin — see wrapWindowsHookCommand.
const powershellCommand = `$homePath = $env:HOME -replace '^/([A-Za-z])/', '$1:/'; $scriptPath = Join-Path $homePath '.orca\\agent-hooks\\${scriptBaseName}.cmd'; if (Test-Path -LiteralPath $scriptPath -PathType Leaf) { & $scriptPath; exit $LASTEXITCODE }${powershellFallback}; ${WINDOWS_POWERSHELL_HOOK_ENVIRONMENT_GUARD}; [Console]::In.ReadToEnd() | Out-Null; exit 0`
const encodedCommand = encodeWindowsPowerShellHookCommand(powershellCommand)
// Why: the Git Bash and native Windows launchers must spell the same switches — window suppression (#14815) and an AV verdict on the shape (#16003) both hit either path.
const powershellInvocation = `${powershell} ${WINDOWS_POWERSHELL_HOOK_SWITCHES} -EncodedCommand ${encodedCommand}`
const encodedWindowsBranch = `if [ -f ${powershell} ]; then ${powershellInvocation}; else ${missingScriptFallback}; fi`
const windowsBranch = `if [ -f ${windowsScript} ]; then case "\${HOME-}" in ${WINDOWS_GIT_BASH_RUNTIME_HOME_UNSAFE}) ${encodedWindowsBranch} ;; *) ${windowsScript} ;; esac; else ${missingScriptFallback}; fi`
const posixBranch = `if [ -f ${posixScript} ] && [ -r ${posixScript} ] && [ -x ${posixScript} ]; then /bin/sh ${posixScript}; else ${missingScriptFallback}; fi`
const encodedWindowsBranch = `if [ -f ${powershell} ]; then ${powershellInvocation}; else ${windowsMissingScriptFallback}; fi`
const windowsBranch = `if [ -f ${windowsScript} ]; then case "\${HOME-}" in ${WINDOWS_GIT_BASH_RUNTIME_HOME_UNSAFE}) ${encodedWindowsBranch} ;; *) ${windowsScript} ;; esac; else ${windowsMissingScriptFallback}; fi`
const posixBranch = `if [ -f ${posixScript} ] && [ -r ${posixScript} ] && [ -x ${posixScript} ]; then /bin/sh ${posixScript}; else ${posixMissingScriptFallback}; fi`
// Why: OSTYPE is shell-owned, so platform selection adds no process to every hook invocation.
return `if [ -z "\${HOME-}" ]; then ${missingScriptFallback}; else case "\${OSTYPE-}" in msys*|cygwin*|win32*) ${windowsBranch} ;; *) ${posixBranch} ;; esac; fi`
}
+6 -3
View File
@@ -88,7 +88,10 @@ export function getManagedScript(target: 'local' | 'posix' = 'local'): string {
export function getWindowsWrapperScript(eventName: string): string {
return [
'@echo off',
'setlocal',
// Why (#9358/#9941): `!` is legal in the hooks path, and inherited delayed expansion
// eats it out of the percent-expanded `%~dp0` — the wrapper then misses the core and
// silently falls back on every event. Same reason the core disables it.
'setlocal DisableDelayedExpansion',
`set "ORCA_ANTIGRAVITY_EVENT=${eventName}"`,
'set "ORCA_ANTIGRAVITY_CORE=%~dp0antigravity-hook.cmd"',
'if exist "%ORCA_ANTIGRAVITY_CORE%" (',
@@ -102,8 +105,8 @@ export function getWindowsWrapperScript(eventName: string): string {
') else (',
' echo {}',
')',
// Why: when the shared core script is missing, this wrapper becomes the
// stdin owner and must finish the agent's payload write before returning.
// Missing-core fallbacks obey the same outside-Orca stdin guard as the core.
...buildWindowsHookEnvironmentGuardLines(),
WINDOWS_HOOK_STDIN_DRAIN_COMMAND,
'exit /b 0',
''
@@ -28,7 +28,8 @@ vi.mock('os', async (importOriginal) => {
import { AntigravityHookService } from './hook-service'
import { ANTIGRAVITY_EVENTS, ANTIGRAVITY_PRE_TOOL_USE_DECISION } from './hook-events'
import { getManagedScript } from './hook-script'
import { getManagedScript, getWindowsWrapperScript } from './hook-script'
import { WINDOWS_HOOK_STDIN_DRAIN_COMMAND } from '../agent-hooks/hook-stdin-contract'
// Why (#9358/#9941): `!` is legal in a Windows path and in a pane key. Under inherited
// delayed expansion cmd eats it out of a percent-expanded curl argument, so bake one into
@@ -91,17 +92,23 @@ async function startHookListener(): Promise<{
type HookRun = { exitCode: number | null; stdout: string; stderr: string; timedOut: boolean }
// Why spell `/v`: `cmd /d /c <bare .cmd path>` is the chain in the bug report's process trace,
// and it inherits HKCU\...\Command Processor\DelayedExpansion. Naming the state makes the
// hostile half reachable on any host — under `/v:on` cmd eats `!` out of every percent
// expansion (#9358/#9941), and a harness pinned to `/v:off` could never fail on it.
type DelayedExpansion = 'on' | 'off'
const DELAYED_EXPANSION_STATES = ['off', 'on'] as const satisfies readonly DelayedExpansion[]
function runWrapper(
wrapperPath: string,
env: NodeJS.ProcessEnv,
// Why: `null` abandons stdin instead of closing it — the shape a caller outside an Orca
// pane produces, and the only way to prove the env guard exits before reading (#11549).
stdinPayload: string | null = PAYLOAD
stdinPayload: string | null = PAYLOAD,
delayedExpansion: DelayedExpansion = 'off'
): Promise<HookRun> {
return new Promise((resolve, reject) => {
// Why: mirror how Antigravity spawns the hook — `cmd /c <bare .cmd path>`, the exact
// chain in the bug report's process trace.
const child = spawn('cmd.exe', ['/d', '/c', wrapperPath], {
const child = spawn('cmd.exe', [`/v:${delayedExpansion}`, '/d', '/c', wrapperPath], {
stdio: ['pipe', 'pipe', 'pipe'],
windowsHide: true,
env
@@ -111,6 +118,7 @@ function runWrapper(
let timedOut = false
const timer = setTimeout(() => {
timedOut = true
child.stdin.destroy()
child.kill('SIGKILL')
}, 15_000)
child.on('error', (error) => {
@@ -154,6 +162,24 @@ function expectedStdout(eventName: string): string {
// Why: runs on every platform — the live delivery suite below is Windows-only, so this
// keeps a POSIX-only CI leg from letting the interpreter back into the hot path.
describe('Antigravity Windows hook post command', () => {
it.each(ANTIGRAVITY_EVENTS)('guards missing-core stdin for $eventName', ({ eventName }) => {
const script = getWindowsWrapperScript(eventName)
const drain = script.indexOf(WINDOWS_HOOK_STDIN_DRAIN_COMMAND)
const answer = script.lastIndexOf('echo {}')
expect(drain).toBeGreaterThan(answer)
for (const key of ['ORCA_AGENT_HOOK_PORT', 'ORCA_AGENT_HOOK_TOKEN', 'ORCA_PANE_KEY']) {
const guard = script.indexOf(`if "%${key}%"=="" exit /b 0`)
expect(guard, key).toBeGreaterThan(answer)
expect(guard, key).toBeLessThan(drain)
}
})
// Why (#9358/#9941): `%~dp0` carries the hooks path, so an inherited delayed expansion eats
// a `!` out of it and the wrapper silently misses the core on every event.
it.each(ANTIGRAVITY_EVENTS)('disables delayed expansion for $eventName', ({ eventName }) => {
expect(getWindowsWrapperScript(eventName)).toContain('setlocal DisableDelayedExpansion')
})
it('posts through curl.exe rather than a PowerShell interpreter', () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32')
const script = getManagedScript('local')
@@ -185,7 +211,10 @@ describe.skipIf(process.platform !== 'win32')('Antigravity Windows hook payload
})
it('delivers every event wrapper payload to the listener without spawning PowerShell', async () => {
home = mkdtempSync(join(tmpdir(), 'orca-antigravity-hook-'))
// Why the `!` in the directory: it lands in the wrapper's `%~dp0`, which is what an
// inherited delayed expansion eats (#9358/#9941). Without it the `/v:on` leg below
// proves nothing about the core lookup.
home = mkdtempSync(join(tmpdir(), 'orca-antigravity-hook!bang-'))
homedirMock.mockReturnValue(home)
expect(new AntigravityHookService().install().state).toBe('installed')
@@ -204,34 +233,42 @@ describe.skipIf(process.platform !== 'win32')('Antigravity Windows hook payload
ORCA_WORKTREE_ID: WORKTREE_ID
})
for (const event of ANTIGRAVITY_EVENTS) {
const label = event.eventName
const before = listener.posts.length
const result = await runWrapper(join(hooksDir, event.windowsWrapperFileName), env)
for (const delayedExpansion of DELAYED_EXPANSION_STATES) {
for (const event of ANTIGRAVITY_EVENTS) {
const label = `${event.eventName} (/v:${delayedExpansion})`
const before = listener.posts.length
const result = await runWrapper(
join(hooksDir, event.windowsWrapperFileName),
env,
PAYLOAD,
delayedExpansion
)
expect(result.timedOut, `${label} timed out`).toBe(false)
expect(result.exitCode, `${label} exit code`).toBe(0)
expect(result.stderr, `${label} stderr`).toBe('')
// Why: Antigravity reads silence on PreToolUse as deny (#2426), so the gate answer
// must survive the transport change.
expect(result.stdout.trim(), `${label} stdout`).toBe(expectedStdout(label))
expect(result.timedOut, `${label} timed out`).toBe(false)
expect(result.exitCode, `${label} exit code`).toBe(0)
expect(result.stderr, `${label} stderr`).toBe('')
// Why: Antigravity reads silence on PreToolUse as deny (#2426), so the gate answer
// must survive the transport change.
expect(result.stdout.trim(), `${label} stdout`).toBe(expectedStdout(event.eventName))
const posts = listener.posts.slice(before)
expect(posts, `${label} posted exactly one hook`).toHaveLength(1)
// Why: byte-exact, not "non-empty" — PowerShell recoded this body through the console
// code page, and a silently corrupted payload still looks posted.
expect(posts[0].payload, `${label} payload`).toBe(PAYLOAD)
expect(posts[0].hookEventName, `${label} hook_event_name`).toBe(label)
// Why: the `!` in both values is the delayed-expansion regression guard.
expect(posts[0].paneKey, `${label} paneKey`).toBe(PANE_KEY)
expect(posts[0].worktreeId, `${label} worktreeId`).toBe(WORKTREE_ID)
expect(posts[0].token, `${label} token`).toBe(HOOK_TOKEN)
expect(posts[0].contentType, `${label} content-type`).toContain(
'application/x-www-form-urlencoded'
)
const posts = listener.posts.slice(before)
expect(posts, `${label} posted exactly one hook`).toHaveLength(1)
// Why: byte-exact, not "non-empty" — PowerShell recoded this body through the console
// code page, and a silently corrupted payload still looks posted.
expect(posts[0].payload, `${label} payload`).toBe(PAYLOAD)
expect(posts[0].hookEventName, `${label} hook_event_name`).toBe(event.eventName)
// Why: the `!` in both values is the delayed-expansion regression guard — it is the
// `/v:on` leg that can actually fail on it.
expect(posts[0].paneKey, `${label} paneKey`).toBe(PANE_KEY)
expect(posts[0].worktreeId, `${label} worktreeId`).toBe(WORKTREE_ID)
expect(posts[0].token, `${label} token`).toBe(HOOK_TOKEN)
expect(posts[0].contentType, `${label} content-type`).toContain(
'application/x-www-form-urlencoded'
)
}
}
// Why: five wrapper launches plus a real install can overrun the default under load.
}, 60_000)
// Why: ten wrapper launches plus a real install can overrun the default under load.
}, 90_000)
// Why (#15117): Antigravity fires some events with no stdin at all. PowerShell substituted
// `{}` before posting; curl forwards the empty body, so prove the post still happens — the
@@ -264,6 +301,67 @@ describe.skipIf(process.platform !== 'win32')('Antigravity Windows hook payload
expect(listener.posts[0].hookEventName).toBe('PreInvocation')
}, 30_000)
// Why a helper: the missing-core cases all need a real install with the core removed, which
// is the shape an AV quarantine or a half-finished uninstall leaves behind.
async function installWithoutCore(): Promise<string> {
home = mkdtempSync(join(tmpdir(), 'orca-antigravity-fallback-'))
homedirMock.mockReturnValue(home)
expect(new AntigravityHookService().install().state).toBe('installed')
const hooksDir = join(home, '.orca', 'agent-hooks')
rmSync(join(hooksDir, 'antigravity-hook.cmd'))
return hooksDir
}
it.each(['ORCA_AGENT_HOOK_PORT', 'ORCA_AGENT_HOOK_TOKEN', 'ORCA_PANE_KEY'])(
'answers every missing-core event with abandoned stdin and no %s',
async (missingKey) => {
const hooksDir = await installWithoutCore()
const listener = await startHookListener()
server = listener.server
const env = hookEnvironment({
USERPROFILE: home,
HOME: home,
ORCA_AGENT_HOOK_PORT: String(listener.port),
ORCA_AGENT_HOOK_TOKEN: HOOK_TOKEN,
ORCA_PANE_KEY: PANE_KEY,
[missingKey]: ''
})
for (const event of ANTIGRAVITY_EVENTS) {
const result = await runWrapper(join(hooksDir, event.windowsWrapperFileName), env, null)
expect(result.timedOut, event.eventName).toBe(false)
expect(result.exitCode, event.eventName).toBe(0)
expect(result.stdout.trim(), event.eventName).toBe(expectedStdout(event.eventName))
expect(result.stderr, event.eventName).toBe('')
}
expect(listener.posts).toHaveLength(0)
},
90_000
)
// Why: the guard must not cost the valid path its drain — with the Orca env present the
// fallback still owns stdin, so the agent's payload write completes instead of breaking.
it('still drains a closed payload for every missing-core event inside a pane', async () => {
const hooksDir = await installWithoutCore()
const listener = await startHookListener()
server = listener.server
const env = hookEnvironment({
USERPROFILE: home,
HOME: home,
ORCA_AGENT_HOOK_PORT: String(listener.port),
ORCA_AGENT_HOOK_TOKEN: HOOK_TOKEN,
ORCA_PANE_KEY: PANE_KEY
})
for (const event of ANTIGRAVITY_EVENTS) {
const result = await runWrapper(join(hooksDir, event.windowsWrapperFileName), env)
expect(result.timedOut, event.eventName).toBe(false)
expect(result.exitCode, event.eventName).toBe(0)
expect(result.stdout.trim(), event.eventName).toBe(expectedStdout(event.eventName))
expect(result.stderr, event.eventName).toBe('')
}
// Why: the fallback answers the agent but has no core to post through.
expect(listener.posts).toHaveLength(0)
}, 60_000)
it('exits without reading stdin when the pane env is missing', async () => {
home = mkdtempSync(join(tmpdir(), 'orca-antigravity-hook-'))
homedirMock.mockReturnValue(home)
+6 -6
View File
@@ -71,7 +71,7 @@ function mapExternalRuns({
.map((run, index) => {
const runAt = asString(run.run_at) ?? asString(run.runAt)
const id = asString(run.id) ?? `${jobId}:${runAt ?? index}`
return {
const mapped: ExternalAutomationRun = {
id,
managerId,
provider,
@@ -83,15 +83,15 @@ function mapExternalRuns({
error: asString(run.error),
outputPath: asString(run.output_path) ?? asString(run.outputPath)
}
return { run: mapped, time: runAt ? Date.parse(runAt) : Number.NaN }
})
.sort((a, b) => {
const aTime = a.runAt ? Date.parse(a.runAt) : Number.NaN
const bTime = b.runAt ? Date.parse(b.runAt) : Number.NaN
if (Number.isFinite(aTime) && Number.isFinite(bTime)) {
return bTime - aTime
if (Number.isFinite(a.time) && Number.isFinite(b.time)) {
return b.time - a.time
}
return b.id.localeCompare(a.id)
return b.run.id.localeCompare(a.run.id)
})
.map(({ run }) => run)
}
function hermesScheduleDisplay(job: ExternalJobRecord): string {
@@ -0,0 +1,38 @@
import { expect, it, vi } from 'vitest'
import { mapHermesJobs, mapOpenClawJobs } from './external-job-mappers'
it.each([mapHermesJobs, mapOpenClawJobs])(
'parses run dates once and preserves provider fallback ordering',
(mapJobs) => {
const runs = Array.from({ length: 2000 }, (_, i) => ({
id: String(i),
run_at:
i % 137 === 0
? 'invalid'
: new Date(1700000000000 + ((i * 173) % 1999) * 1000).toISOString(),
output_content: `Output ${i}`,
status: 'completed'
}))
const parse = vi.spyOn(Date, 'parse')
let expected: typeof runs
let jobs: ReturnType<typeof mapHermesJobs>
try {
expected = [...runs].sort((a, b) => {
const left = Date.parse(a.run_at),
right = Date.parse(b.run_at)
return Number.isFinite(left) && Number.isFinite(right)
? right - left
: b.id.localeCompare(a.id)
})
expect(parse.mock.calls.length).toBeGreaterThan(10_000)
parse.mockClear()
jobs = mapJobs('manager', [{ id: 'job', runs }])
expect(parse).toHaveBeenCalledTimes(2000)
} finally {
parse.mockRestore()
}
expect(jobs[0].runs.map((run) => run.id)).toEqual(expected.map((run) => run.id))
expect(jobs[0].runs.every((run) => run.outputContent === `Output ${run.id}`)).toBe(true)
expect(jobs[0].runs.every((run) => !('time' in run))).toBe(true)
}
)
@@ -1,3 +1,4 @@
import { highestUsageKey } from '../usage/highest-usage-key'
import type {
ClaudeUsageBreakdownKind,
ClaudeUsageBreakdownRow,
@@ -56,9 +57,8 @@ export function buildSummary(
}
}
const topModel = [...byModel.entries()].sort((left, right) => right[1] - left[1])[0]?.[0] ?? null
const topProject =
[...byProject.entries()].sort((left, right) => right[1] - left[1])[0]?.[0] ?? null
const topModel = highestUsageKey(byModel)
const topProject = highestUsageKey(byProject)
return {
scope,
@@ -0,0 +1,53 @@
import { expect, it, vi } from 'vitest'
import { attributeClaudeUsageTurns } from './worktree-attribution'
import type { ClaudeUsageParsedTurn } from './types'
vi.mock('node:fs/promises', () => ({ realpath: async (path: string) => path }))
it('resolves repeated nested and unmatched cwd paths once per attribution batch', async () => {
const lookup = new Map(
Array.from({ length: 100 }, (_, index) => [
`/repo-${String(index).padStart(3, '0')}`,
{
repoId: `repo-${index}`,
worktreeId: `wt-${index}`,
path: `/repo-${index}`,
displayName: `Repo ${index}`
}
])
)
const input: ClaudeUsageParsedTurn[] = Array.from({ length: 1000 }, (_, index) => ({
sessionId: String(index),
timestamp: '2026-09-07T00:00:00Z',
model: null,
cwd: index % 2 === 0 ? '/repo-099/nested' : '/outside',
gitBranch: null,
inputTokens: 1,
outputTokens: 1,
cacheReadTokens: 0,
cacheWriteTokens: 0,
cacheWrite1hTokens: 0
}))
const original = String.prototype.startsWith
let comparisons = 0
const spy = vi.spyOn(String.prototype, 'startsWith').mockImplementation(function (
this: string,
search: string,
position?: number
) {
if (search.slice(0, 6) === '/repo-') {
comparisons += 1
}
return original.call(this, search, position)
})
let result: Awaited<ReturnType<typeof attributeClaudeUsageTurns>>
try {
result = await attributeClaudeUsageTurns(input, lookup)
} finally {
spy.mockRestore()
}
expect(comparisons).toBeLessThanOrEqual(200)
expect(result![0].worktreeId).toBe('wt-99')
expect(result![1].worktreeId).toBeNull()
expect(result![1].projectKey).toBe('cwd:/outside')
})
@@ -105,7 +105,7 @@ export async function attributeClaudeUsageTurns(
worktreeLookup: Map<string, ClaudeUsageWorktreeRef>
): Promise<ClaudeUsageAttributedTurn[]> {
const attributed: ClaudeUsageAttributedTurn[] = []
const canonicalCwdByPath = new Map<string, string>()
const worktreeByCwd = new Map<string, ClaudeUsageWorktreeRef | null>()
for (const turn of turns) {
const day = localDayFromTimestamp(turn.timestamp)
@@ -119,14 +119,12 @@ export async function attributeClaudeUsageTurns(
let projectLabel = getDefaultProjectLabel(turn.cwd)
if (turn.cwd) {
let canonicalCwd = canonicalCwdByPath.get(turn.cwd)
if (canonicalCwd === undefined) {
// Why: Claude transcripts repeat the same cwd for many consecutive
// turns. Cache realpath work so attribution scales with unique paths.
canonicalCwd = await canonicalizePath(turn.cwd)
canonicalCwdByPath.set(turn.cwd, canonicalCwd)
let worktree = worktreeByCwd.get(turn.cwd)
if (worktree === undefined) {
const canonicalCwd = await canonicalizePath(turn.cwd)
worktree = findContainingWorktree(canonicalCwd, worktreeLookup)
worktreeByCwd.set(turn.cwd, worktree)
}
const worktree = findContainingWorktree(canonicalCwd, worktreeLookup)
if (worktree) {
repoId = worktree.repoId
worktreeId = worktree.worktreeId
@@ -1,3 +1,4 @@
import { highestUsageKey } from '../usage/highest-usage-key'
import type {
CodexUsageBreakdownKind,
CodexUsageBreakdownRow,
@@ -57,9 +58,8 @@ export function buildSummary(
}
}
const topModel = [...byModel.entries()].sort((left, right) => right[1] - left[1])[0]?.[0] ?? null
const topProject =
[...byProject.entries()].sort((left, right) => right[1] - left[1])[0]?.[0] ?? null
const topModel = highestUsageKey(byModel)
const topProject = highestUsageKey(byProject)
return {
scope,
@@ -0,0 +1,59 @@
import { expect, it } from 'vitest'
import {
CodexTurnOrdinals,
MAX_CODEX_TURN_ORDINAL_BYTES,
MAX_CODEX_TURN_ORDINAL_ENTRIES
} from './codex-turn-ordinals'
it('does not rescan the forgotten window for each new streamed item', () => {
const ordinals = new CodexTurnOrdinals()
for (let index = 0; index < MAX_CODEX_TURN_ORDINAL_ENTRIES; index += 1) {
ordinals.ordinalFor('thread', String(index), 'item')
ordinals.forgetTurn('thread', String(index))
}
const turns = (ordinals as unknown as { turns: Map<string, { active: boolean }> }).turns
let reads = 0
for (const turn of turns.values()) {
let active = turn.active
Object.defineProperty(turn, 'active', {
get() {
reads += 1
return active
},
set(value: boolean) {
active = value
}
})
}
for (let index = 0; index < 1000; index += 1) {
expect(ordinals.ordinalFor('thread', 'live', String(index))).toBe(index)
}
expect(reads).toBe(0)
expect(ordinals.forgottenTurnCount).toBe(MAX_CODEX_TURN_ORDINAL_ENTRIES)
})
it('retains ordinal continuity on late reactivation and evicts oldest forgotten turns', () => {
const ordinals = new CodexTurnOrdinals()
expect(ordinals.ordinalFor('t', 'first', 'a')).toBe(0)
ordinals.forgetTurn('t', 'first')
expect(ordinals.ordinalFor('t', 'first', 'b')).toBe(1)
expect(ordinals.forgottenTurnCount).toBe(0)
ordinals.forgetTurn('t', 'first')
for (let index = 0; index < MAX_CODEX_TURN_ORDINAL_ENTRIES; index += 1) {
ordinals.ordinalFor('t', String(index), 'a')
ordinals.forgetTurn('t', String(index))
}
expect(ordinals.forgottenTurnCount).toBe(MAX_CODEX_TURN_ORDINAL_ENTRIES)
expect(ordinals.ordinalFor('t', 'first', 'c')).toBe(0)
})
it('keeps byte eviction bounded for active and forgotten turns', () => {
const ordinals = new CodexTurnOrdinals()
for (let index = 0; index < 4000; index += 1) {
ordinals.ordinalFor('thread', 'live', `${index}-${'x'.repeat(240)}`)
}
expect(ordinals.bytes).toBeLessThanOrEqual(MAX_CODEX_TURN_ORDINAL_BYTES)
ordinals.forgetTurn('thread', 'live')
expect(ordinals.bytes).toBeLessThan(100)
expect(ordinals.forgottenTurnCount).toBe(1)
})
+9 -9
View File
@@ -14,15 +14,10 @@ export class CodexTurnOrdinals {
{ assigned: Map<string, number>; next: number; active: boolean }
>()
private retainedBytes = 0
private readonly forgottenTurns = new Set<string>()
get forgottenTurnCount(): number {
let count = 0
for (const turn of this.turns.values()) {
if (!turn.active) {
count += 1
}
}
return count
return this.forgottenTurns.size
}
get bytes(): number {
@@ -48,12 +43,13 @@ export class CodexTurnOrdinals {
private trimForgotten(): void {
while (this.forgottenTurnCount > MAX_CODEX_TURN_ORDINAL_ENTRIES) {
const oldest = [...this.turns.entries()].find(([, turn]) => !turn.active)?.[0]
const oldest = this.forgottenTurns.values().next().value
if (!oldest) {
break
}
const removed = this.turns.get(oldest)
this.turns.delete(oldest)
this.forgottenTurns.delete(oldest)
if (removed) {
this.retainedBytes = Math.max(
0,
@@ -68,7 +64,7 @@ export class CodexTurnOrdinals {
private trimBytes(currentTurnKey: string): void {
this.trimForgotten()
while (this.retainedBytes > MAX_CODEX_TURN_ORDINAL_BYTES) {
const forgotten = [...this.turns.entries()].find(([, turn]) => !turn.active)?.[0]
const forgotten = this.forgottenTurns.values().next().value
const oldest = forgotten ?? this.turns.keys().next().value
if (typeof oldest !== 'string') {
break
@@ -90,6 +86,7 @@ export class CodexTurnOrdinals {
break
}
this.turns.delete(oldest)
this.forgottenTurns.delete(oldest)
this.retainedBytes = Math.max(
0,
this.retainedBytes -
@@ -112,6 +109,7 @@ export class CodexTurnOrdinals {
this.turns.set(turnKey, turn)
}
turn.active = true
this.forgottenTurns.delete(turnKey)
}
const itemKey = this.keyPart(codexItemId)
const existing = turn.assigned.get(itemKey)
@@ -136,6 +134,8 @@ export class CodexTurnOrdinals {
)
turn.assigned = new Map()
turn.active = false
this.forgottenTurns.delete(turnKey)
this.forgottenTurns.add(turnKey)
this.retainedBytes = Math.max(0, this.retainedBytes - assignedBytes)
this.turns.delete(turnKey)
this.turns.set(turnKey, turn)
+3 -2
View File
@@ -1,7 +1,8 @@
import { getSharedManagedScriptPath } from '../agent-hooks/installer-utils'
import {
buildPosixHookPayloadCapture,
buildPosixHookSpoolLines
buildPosixHookSpoolLines,
WINDOWS_POWERSHELL_HOOK_ENVIRONMENT_GUARD
} from '../agent-hooks/hook-stdin-contract'
export function getManagedScriptFileName(): string {
@@ -30,7 +31,7 @@ export function getManagedScript(target: 'local' | 'posix' = 'local'): string {
// Why (#11549 class): missing Orca context means a user-wide hook fired outside an
// Orca pane. ReadToEnd blocks forever if that caller abandons the pipe, so the guard
// must run before the hook owns stdin; the payload would be discarded anyway.
'if (-not $env:ORCA_AGENT_HOOK_PORT -or -not $env:ORCA_AGENT_HOOK_TOKEN -or -not $env:ORCA_PANE_KEY) { exit 0 }',
WINDOWS_POWERSHELL_HOOK_ENVIRONMENT_GUARD,
'$inputData = [Console]::In.ReadToEnd()',
'if ([string]::IsNullOrWhiteSpace($inputData)) { exit 0 }',
'try {',
@@ -0,0 +1,212 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as pty from 'node-pty'
import { createDaemonPtySubprocessHandle } from './pty-subprocess/subprocess-handle'
import { mockPtyProcess } from './pty-subprocess-test-harness'
import { TerminalHost } from './terminal-host'
import { HeadlessEmulator } from './headless-emulator'
import * as ptyJob from '../windows/windows-pty-job'
vi.mock('./pty-subprocess/foreground-process-tracker', () => ({
createPtyForegroundProcessTracker: () => ({
recordOutput: vi.fn(),
markDead: vi.fn(),
getForegroundProcess: () => null
})
}))
vi.mock('../pty/posix-pty-process-groups', () => ({
forceKillPosixPtyProcessGroups: (_pid: number, fallback: () => void) => fallback()
}))
vi.mock('../pty-descendant-termination', () => ({
killWithDescendantSweep: async (_pid: number, killRoot: () => void) => killRoot()
}))
function createFixture() {
const proc = {
...mockPtyProcess(4242),
destroy: vi.fn(),
pause: vi.fn(),
resume: vi.fn(),
clear: vi.fn()
}
const handle = createDaemonPtySubprocessHandle({
process: proc as unknown as pty.IPty,
shellPath: 'bash',
spawnCwd: process.cwd(),
env: {},
startupCommandDeliveredInShellArgs: false,
reportsChildExitStatus: true,
sessionId: 'io-failure',
startupAgentRecognition: null
})
return { proc, handle }
}
function failIo(fixture: ReturnType<typeof createFixture>, operation: 'write' | 'resize') {
fixture.proc[operation].mockImplementation(() => {
throw new Error('transient native I/O failure')
})
if (operation === 'write') {
fixture.handle.write('input')
} else {
fixture.handle.resize(100, 30)
}
}
afterEach(() => vi.restoreAllMocks())
describe.each(['darwin', 'linux', 'win32'] as const)('%s native-handle contract', (platform) => {
const platformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform')!
beforeEach(() => Object.defineProperty(process, 'platform', { value: platform }))
afterEach(() => Object.defineProperty(process, 'platform', platformDescriptor))
describe.each(['write', 'resize'] as const)('%s failure cleanup', (operation) => {
it('suppresses repeated native I/O failures while still delivering output and exit', () => {
const fixture = createFixture()
const onData = vi.fn()
const onExit = vi.fn()
fixture.handle.onData(onData)
fixture.handle.onExit(onExit)
failIo(fixture, operation)
fixture.handle.pause?.()
fixture.handle.resume?.()
expect(fixture.proc.pause).toHaveBeenCalledOnce()
expect(fixture.proc.resume).toHaveBeenCalledOnce()
fixture.handle.write('more input')
fixture.handle.resize(120, 40)
fixture.handle.clear?.()
expect(fixture.proc[operation]).toHaveBeenCalledOnce()
for (const suppressed of ['write', 'resize', 'clear'] as const) {
if (suppressed !== operation) {
expect(fixture.proc[suppressed]).not.toHaveBeenCalled()
}
}
fixture.proc._simulateData('still running')
expect(onData).toHaveBeenCalledWith('still running')
expect(onExit).not.toHaveBeenCalled()
fixture.proc._simulateExit(7)
expect(onExit).toHaveBeenCalledOnce()
fixture.handle.dispose()
})
it('blocks reentrant termination from an exit listener after I/O failure', () => {
const fixture = createFixture()
const signal = vi.spyOn(process, 'kill').mockReturnValue(true)
const nativeKill = fixture.proc.kill
failIo(fixture, operation)
fixture.handle.onExit(() => {
fixture.handle.kill()
fixture.handle.forceKill()
fixture.handle.signal('SIGTERM')
})
fixture.proc._simulateExit(0)
expect(nativeKill).not.toHaveBeenCalled()
expect(signal).not.toHaveBeenCalled()
fixture.handle.dispose()
})
it.skipIf(platform !== 'win32')(
'preserves ConPTY single-close ownership after I/O failure',
() => {
const fixture = createFixture()
const terminateJob = vi.spyOn(ptyJob, 'terminatePtyJob').mockReturnValue('terminated')
const signal = vi.spyOn(process, 'kill').mockReturnValue(true)
failIo(fixture, operation)
fixture.handle.kill()
fixture.handle.forceKill()
fixture.proc._simulateExit(137)
fixture.handle.dispose()
expect(fixture.proc.kill).toHaveBeenCalledOnce()
expect(terminateJob).toHaveBeenCalledOnce()
expect(signal).not.toHaveBeenCalled()
expect(fixture.proc.destroy).not.toHaveBeenCalled()
}
)
it('preserves early output and exit status while fencing listener cleanup', () => {
const fixture = createFixture()
const signal = vi.spyOn(process, 'kill').mockReturnValue(true)
failIo(fixture, operation)
fixture.proc._simulateData('final output')
fixture.proc._simulateExit(7)
const delivered: string[] = []
fixture.handle.onData((data) => {
delivered.push(data)
fixture.handle.forceKill()
})
fixture.handle.onExit((code) => delivered.push(`exit:${code}`))
expect(delivered).toEqual(['final output', 'exit:7'])
expect(signal).not.toHaveBeenCalled()
fixture.handle.dispose()
})
it('keeps graceful and forced termination available until physical exit', () => {
const fixture = createFixture()
const originalKill = fixture.proc.kill
const signal = vi.spyOn(process, 'kill').mockReturnValue(true)
failIo(fixture, operation)
fixture.handle.kill()
expect(originalKill).toHaveBeenCalledOnce()
// A fresh handle exercises force-kill without Windows double-close semantics.
const forced = createFixture()
failIo(forced, operation)
forced.handle.forceKill()
expect(signal).toHaveBeenCalledWith(4242, 'SIGKILL')
forced.proc._simulateExit(137)
signal.mockClear()
forced.handle.forceKill()
forced.handle.signal('SIGTERM')
expect(signal).not.toHaveBeenCalled()
fixture.proc._simulateExit(0)
fixture.handle.dispose()
forced.handle.dispose()
})
it('reaps every session and native handle across 32 failed-I/O create/close cycles', async () => {
const emulatorDispose = vi.spyOn(HeadlessEmulator.prototype, 'dispose')
const signal = vi.spyOn(process, 'kill').mockReturnValue(true)
let fixture = createFixture()
const host = new TerminalHost({ spawnSubprocess: () => fixture.handle })
try {
for (let index = 0; index < 32; index++) {
fixture = createFixture()
const sessionId = `io-failure-${index}`
const onExit = vi.fn()
await host.createOrAttach({
sessionId,
cols: 80,
rows: 24,
streamClient: { onData: vi.fn(), onExit }
})
failIo(fixture, operation)
signal.mockClear()
const closing = host.kill(sessionId, { immediate: true })
// Capture rejection before assertions so a red run cannot leak an unhandled waiter.
const settled = closing.then(
() => null,
(error: unknown) => error ?? new Error('kill rejected')
)
let killFailure: unknown = null
try {
expect(host.listSessions()).toHaveLength(1)
expect(fixture.proc.destroy).not.toHaveBeenCalled()
expect(onExit).not.toHaveBeenCalled()
await vi.waitFor(() => expect(signal).toHaveBeenCalledWith(4242, 'SIGKILL'))
} finally {
fixture.proc._simulateExit(137)
killFailure = await settled
}
expect(killFailure).toBeNull()
expect(host.listSessions()).toHaveLength(0)
expect(onExit).toHaveBeenCalledOnce()
expect(fixture.proc.destroy).toHaveBeenCalledOnce()
expect(emulatorDispose).toHaveBeenCalledTimes(index + 1)
}
} finally {
fixture.proc._simulateExit(137)
await host.dispose()
}
})
})
})
@@ -0,0 +1,97 @@
import { fstatSync } from 'node:fs'
import * as pty from 'node-pty'
import { describe, expect, it, vi } from 'vitest'
import { createDaemonPtySubprocessHandle } from './pty-subprocess/subprocess-handle'
import { TerminalHost } from './terminal-host'
const describePosix = process.platform === 'win32' ? describe.skip : describe
describePosix('failed-I/O teardown with a real native PTY', () => {
it.each([
['write', false],
['write', true],
['resize', false],
['resize', true]
] as const)(
'reaps real shells and master fds after %s failure (immediate=%s)',
async (operation, immediate) => {
for (let cycle = 0; cycle < 4; cycle++) {
const native = pty.spawn(
'/bin/sh',
[
'-c',
'printf "orca-cleanup-ready\\n"; while IFS= read -r line; do printf "reply:%s\\n" "$line"; done'
],
{
cwd: process.cwd(),
cols: 80,
rows: 24,
env: { TERM: 'xterm-256color', PATH: '/usr/bin:/bin' }
}
)
const fd = (native as pty.IPty & { fd: number }).fd
let exited = false
native.onExit(() => {
exited = true
})
const handle = createDaemonPtySubprocessHandle({
process: native,
shellPath: '/bin/sh',
spawnCwd: process.cwd(),
env: {},
startupCommandDeliveredInShellArgs: false,
reportsChildExitStatus: true,
sessionId: 'native-io-failure',
startupAgentRecognition: null
})
const host = new TerminalHost({ spawnSubprocess: () => handle })
let output = ''
const onExit = vi.fn()
try {
await host.createOrAttach({
sessionId: 'native-io-failure',
cols: 80,
rows: 24,
streamClient: {
onData: (data) => {
output += data
},
onExit
}
})
await vi.waitFor(() => expect(output).toContain('orca-cleanup-ready'), { timeout: 3000 })
handle.resize(100, 30)
handle.write('roundtrip\n')
await vi.waitFor(() => expect(output).toContain('reply:roundtrip'), { timeout: 3000 })
host.pauseProducer('native-io-failure')
expect(process.kill(native.pid, 0)).toBe(true)
const failure = vi.spyOn(native, operation).mockImplementation(() => {
throw new Error('injected I/O failure')
})
if (operation === 'write') {
handle.write('ignored')
} else {
handle.resize(100, 30)
}
failure.mockRestore()
await host.kill('native-io-failure', { immediate })
await vi.waitFor(() => expect(onExit).toHaveBeenCalledOnce(), { timeout: 3000 })
expect(host.listSessions()).toHaveLength(0)
expect(() => process.kill(native.pid, 0)).toThrow(
expect.objectContaining({ code: 'ESRCH' })
)
expect(() => fstatSync(fd)).toThrow(expect.objectContaining({ code: 'EBADF' }))
} finally {
// Only this test's still-owned native child is eligible for emergency cleanup.
if (!exited) {
native.kill('SIGKILL')
}
await vi.waitFor(() => expect(exited).toBe(true), { timeout: 3000 })
await host.dispose()
}
}
},
15000
)
})
@@ -28,6 +28,8 @@ export function createDaemonPtySubprocessHandle(args: {
const nativeProc = proc as DisposableNativePty
const events = new PtyPreListenerEvents()
let dead = false
// I/O failure is not exit evidence; keep termination and producer flow control available.
let ioFailed = false
let disposed = false
let nodePtyKillIssued = false
const foreground = createPtyForegroundProcessTracker({
@@ -44,19 +46,18 @@ export function createDaemonPtySubprocessHandle(args: {
events.acceptData(data)
})
proc.onExit(({ exitCode, signal }) => {
events.acceptExit({
exitCode,
signal,
hostReportsChildExitStatus: args.reportsChildExitStatus
})
})
proc.onExit(() => {
// Exit listeners may re-enter cleanup; retire signal authority before notifying them.
dead = true
foreground.markDead()
// Why: neutralize kill synchronously so a later async socket-close SIGHUP cannot hit a recycled pid.
if (process.platform !== 'win32') {
nativeProc.kill = () => {}
}
events.acceptExit({
exitCode,
signal,
hostReportsChildExitStatus: args.reportsChildExitStatus
})
})
const slavePath = readPtySlavePath(proc)
@@ -73,23 +74,23 @@ export function createDaemonPtySubprocessHandle(args: {
confirmForegroundProcess: foreground.confirmForegroundProcess,
confirmShellForeground: foreground.confirmShellForeground,
write: (data) => {
if (dead) {
if (dead || ioFailed) {
return
}
try {
proc.write(data)
} catch {
dead = true
ioFailed = true
}
},
resize: (cols, rows) => {
if (dead || !isValidPtySize(cols, rows)) {
if (dead || ioFailed || !isValidPtySize(cols, rows)) {
return
}
try {
proc.resize(cols, rows)
} catch {
dead = true
ioFailed = true
}
},
// WindowsTerminal also wires _socket to the ConPTY conout pipe, so pausing backpressures the child.
@@ -114,7 +115,7 @@ export function createDaemonPtySubprocessHandle(args: {
}
},
clear: () => {
if (dead) {
if (dead || ioFailed) {
return
}
try {
+14 -10
View File
@@ -25,19 +25,23 @@ export function countDiffLines(diff: string): { additions: number; deletions: nu
// diff line `---<content>`, colliding with the `--- a/file` header — so it must
// be counted once inside a hunk, not skipped.
let inHunk = false
for (const line of diff.split('\n')) {
if (line.startsWith('@@')) {
let cursor = 0
while (cursor < diff.length) {
if (diff.startsWith('@@', cursor)) {
inHunk = true
continue
} else if (inHunk) {
const prefix = diff.charCodeAt(cursor)
if (prefix === 43) {
additions += 1
} else if (prefix === 45) {
deletions += 1
}
}
if (!inHunk) {
continue
}
if (line.startsWith('+')) {
additions += 1
} else if (line.startsWith('-')) {
deletions += 1
const newline = diff.indexOf('\n', cursor)
if (newline === -1) {
break
}
cursor = newline + 1
}
return { additions, deletions }
}
+38
View File
@@ -458,4 +458,42 @@ describe('countDiffLines', () => {
// Why: the `@@` hunk check runs first, so it must not swallow `+`/`-` content.
expect(countDiffLines('@@ -1 +1 @@\n-@@ old\n+@@ new')).toEqual({ additions: 1, deletions: 1 })
})
// Why: the scan now reads a prefix code unit at a byte cursor rather than a split
// segment, so line-ending and non-ASCII shapes are the new regression surface.
it('counts a CRLF hunk the same as an LF hunk', () => {
expect(countDiffLines('@@ -1 +1,2 @@\r\n-old\r\n+a\r\n+b\r\n')).toEqual({
additions: 2,
deletions: 1
})
})
it('treats a lone CR as content, not a line break', () => {
expect(countDiffLines('@@ -1 +1 @@\n-old\r+new')).toEqual({ additions: 0, deletions: 1 })
})
it('counts lines whose content is multi-byte or a surrogate pair', () => {
expect(countDiffLines('@@ -1 +1 @@\n-é ünïcode\n+🚀 rocket')).toEqual({
additions: 1,
deletions: 1
})
})
it('ignores non-ASCII context lines and blank lines inside a hunk', () => {
expect(countDiffLines('@@ -1 +1 @@\n é leading accent\n 🚀 leading emoji\n\n')).toEqual({
additions: 0,
deletions: 0
})
})
it('counts large diff prefixes without allocating a string array for every line', () => {
const diff = `--- a/file\n+++ b/file\n@@ -1 +1 @@\n${'-old\n+new\n context\n'.repeat(10000)}`
const split = vi.spyOn(String.prototype, 'split')
try {
expect(countDiffLines(diff)).toEqual({ additions: 10000, deletions: 10000 })
expect(split.mock.calls.length).toBe(0)
} finally {
split.mockRestore()
}
})
})
@@ -8,6 +8,7 @@ import { listRepoWorktreeGraph } from '../repo-worktrees'
import type * as ProjectGroupsModule from '../../shared/project-groups'
import { buildProjectGroupChildIndex, getProjectGroupSubtreeIds } from '../../shared/project-groups'
import { isPathInsideOrEqual } from '../../shared/cross-platform-path'
import type * as CrossPlatformPathModule from '../../shared/cross-platform-path'
import { getWorktreeMirrorDistro } from '../project-runtime-git-options'
import type { FolderWorkspace } from '../../shared/folder-workspace-types'
import type { ProjectGroup } from '../../shared/project-group-types'
@@ -32,6 +33,13 @@ vi.mock('../../shared/project-groups', async () => {
}
})
vi.mock('../../shared/cross-platform-path', async () => {
const actual = await vi.importActual<typeof CrossPlatformPathModule>(
'../../shared/cross-platform-path'
)
return { ...actual, isPathInsideOrEqual: vi.fn(actual.isPathInsideOrEqual) }
})
type StoreFixture = {
repos: Repo[]
projects: Project[]
@@ -257,6 +265,42 @@ beforeEach(() => {
})
describe('getAllowedRoots', () => {
it('stops scanning repositories when a local candidate settles each folder scope', () => {
const fixture: StoreFixture = {
repos: Array.from({ length: 1_000 }, (_, index) =>
makeRepo({ id: `repo-${index}`, path: `/folders/root/repo-${index}` })
),
projects: [],
projectGroups: [],
folderWorkspaces: Array.from({ length: 100 }, (_, index) =>
makeWorkspace({ id: `folder-${index}`, folderPath: '/folders/root' })
)
}
const { store } = makeCountingStore(fixture)
vi.mocked(isPathInsideOrEqual).mockClear()
const actual = getAllowedRoots(store)
expect(isPathInsideOrEqual).toHaveBeenCalledTimes(100)
vi.mocked(isPathInsideOrEqual).mockClear()
expect(actual).toEqual(referenceAllowedRoots(store))
expect(isPathInsideOrEqual).toHaveBeenCalledTimes(100_000)
})
it('preserves empty, remote-only, mixed and explicit remote folder scopes in any repo order', () => {
const fixture = makeMixedFixture()
fixture.repos.push(
makeRepo({
id: 'local-in-remote-group',
path: '/local/mixed',
projectGroupId: 'group-remote'
})
)
for (let index = 0; index < fixture.repos.length; index += 1) {
fixture.repos.push(fixture.repos.shift()!)
const { store } = makeCountingStore(fixture)
expect(getAllowedRoots(store)).toEqual(referenceAllowedRoots(store))
}
})
it('produces the same roots as the pre-change implementation', () => {
const { store } = makeCountingStore(makeMixedFixture())
+15 -21
View File
@@ -27,20 +27,6 @@ export function getLocalRepos(store: Store) {
return filterLocalRepos(store.getRepos())
}
function getFolderScopeCandidateRepos(
folderPath: string,
projectGroupId: string,
childGroupIndex: ProjectGroupChildIndex,
repos: readonly Repo[]
): Repo[] {
const groupIds = collectProjectGroupSubtreeIds(childGroupIndex, projectGroupId)
return repos.filter(
(repo) =>
(typeof repo.projectGroupId === 'string' && groupIds.has(repo.projectGroupId)) ||
isPathInsideOrEqual(folderPath, repo.path)
)
}
function isRemoteOnlyFolderScope(
folderPath: string,
projectGroupId: string,
@@ -51,13 +37,21 @@ function isRemoteOnlyFolderScope(
if (connectionId) {
return true
}
const candidates = getFolderScopeCandidateRepos(
folderPath,
projectGroupId,
childGroupIndex,
repos
)
return candidates.length > 0 && candidates.every((repo) => Boolean(repo.connectionId))
const groupIds = collectProjectGroupSubtreeIds(childGroupIndex, projectGroupId)
let hasRemoteCandidate = false
for (const repo of repos) {
if (
(typeof repo.projectGroupId === 'string' && groupIds.has(repo.projectGroupId)) ||
isPathInsideOrEqual(folderPath, repo.path)
) {
// One local candidate settles the scope without scanning the remaining repositories.
if (!repo.connectionId) {
return false
}
hasRemoteCandidate = true
}
}
return hasRemoteCandidate
}
function getFolderWorkspaceConnectionId(
@@ -167,10 +167,11 @@ export function readJournalRowsAfterCursor(
db: Database.Database,
sessionId: string,
epoch: string,
afterSequence: number
afterSequence: number,
limit?: number
): JournalRow[] {
const rows: JournalRow[] = []
for (const stored of readJournalRowsAfter(db, sessionId, epoch, afterSequence)) {
for (const stored of readJournalRowsAfter(db, sessionId, epoch, afterSequence, limit)) {
const parsed = parseJournalRow(stored.rowJson)
if (!parsed.ok) {
break
@@ -20,6 +20,7 @@ const SELECT_EPOCH_ROWS = `SELECT epoch, seq, ts, row_json FROM journal_rows
WHERE session_id = ? AND epoch = ? ORDER BY seq ASC`
const SELECT_ROWS_AFTER = `SELECT epoch, seq, ts, row_json FROM journal_rows
WHERE session_id = ? AND epoch = ? AND seq > ? ORDER BY seq ASC`
const SELECT_ROWS_AFTER_LIMITED = `${SELECT_ROWS_AFTER} LIMIT ?`
const DELETE_SUFFIX = 'DELETE FROM journal_rows WHERE session_id = ? AND epoch = ? AND seq >= ?'
export function readJournalSessionEpoch(db: Database.Database, sessionId: string): string | null {
@@ -58,8 +59,14 @@ export function readJournalRowsAfter(
db: Database.Database,
sessionId: string,
epoch: string,
afterSeq: number
afterSeq: number,
limit?: number
): JournalStoredRow[] {
if (limit !== undefined) {
return toStoredRows(
db.prepare(SELECT_ROWS_AFTER_LIMITED).all(sessionId, epoch, afterSeq, limit)
)
}
return toStoredRows(db.prepare(SELECT_ROWS_AFTER).all(sessionId, epoch, afterSeq))
}
@@ -177,7 +177,7 @@ export class AgentSessionJournal {
canonicalItemId = (itemId: string): string => resolveJournalItemId(this.state, itemId)
readSince(cursor: AgentJournalCursor): JournalReadSince {
readSince(cursor: AgentJournalCursor, limit?: number): JournalReadSince {
return readJournalSince(
{
state: this.state,
@@ -186,7 +186,8 @@ export class AgentSessionJournal {
this.requireDatabase().db,
this.identity.sessionId,
this.state.epoch,
afterSequence
afterSequence,
limit
),
readOnly: this.readOnly
},
@@ -0,0 +1,228 @@
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import Database from '../../sqlite/sync-database'
import {
AGENT_SESSION_JOURNAL_SCHEMA_VERSION,
type AgentSessionJournalIdentity
} from '../../../shared/agent-session-journal-types'
import type { AgentSessionSubscribeEvent } from '../../../shared/agent-session-wire'
import { openJournalDatabase } from '../agent-session-journal/journal-database'
import { journalDatabaseFile } from '../agent-session-journal/journal-paths'
import {
insertJournalRow,
upsertJournalSessionRow
} from '../agent-session-journal/journal-row-table'
import * as journalReducer from '../agent-session-journal/journal-reducer'
import * as rowSchema from '../agent-session-journal/journal-row-schema'
import { createTrackedJournalOpener } from '../agent-session-journal/journal-store-test-open'
import { AgentSessionSubscribers } from './structured-agent-session-subscribers'
import { readAgentSessionHistory } from './agent-session-history-page'
const identity: AgentSessionJournalIdentity = {
sessionId: 'bounded-catch-up',
workspaceId: 'folder-workspace',
hostId: 'remote-host',
agent: 'codex',
providerHandle: { kind: 'codex', threadId: 'thread-1' }
}
const journals = createTrackedJournalOpener()
let root: string | undefined
afterEach(async () => {
vi.restoreAllMocks()
await journals.closeAll()
if (root) {
await rm(root, { recursive: true, force: true })
}
})
async function seedJournal(count: number) {
root = await mkdtemp(join(tmpdir(), 'orca-history-read-budget-'))
const { db } = openJournalDatabase(journalDatabaseFile(root))
const base = {
v: AGENT_SESSION_JOURNAL_SCHEMA_VERSION,
epoch: 'epoch-1',
fence: 1,
ts: 1_000
}
try {
db.exec('BEGIN IMMEDIATE')
upsertJournalSessionRow(db, identity.sessionId, base.epoch, base.ts)
insertJournalRow(db, identity.sessionId, {
...base,
kind: 'epoch',
seq: 1,
reason: 'session_created',
providerHandle: identity.providerHandle
})
for (let index = 0; index < count; index += 1) {
insertJournalRow(db, identity.sessionId, {
...base,
kind: 'item',
seq: index + 2,
itemId: `item-${index}`,
revision: 1,
body: {
kind: 'message',
role: 'assistant',
blocks: [{ type: 'text', text: `${index}:${'x'.repeat(4096)}` }]
}
})
}
db.exec('COMMIT')
} finally {
db.close()
}
return journals.open({ identity, journalDir: root })
}
function observeForwardReads() {
const returnedRows: number[] = []
const observed = new WeakSet<object>()
const prepare = Database.prototype.prepare
vi.spyOn(Database.prototype, 'prepare').mockImplementation(function (this: Database, sql) {
const statement = prepare.call(this, sql)
if (sql.includes('seq > ?') && !observed.has(statement)) {
observed.add(statement)
const all = statement.all.bind(statement)
vi.spyOn(statement, 'all').mockImplementation((...args) => {
const rows = all(...args)
returnedRows.push(rows.length)
return rows
})
}
return statement
})
const parse = vi.spyOn(rowSchema, 'parseJournalRow')
return { returnedRows, parse }
}
describe('forward history SQL read budget', () => {
it('reconnects through every page with one lookahead row per page', async () => {
const count = 2_000
const journal = await seedJournal(count)
const { returnedRows, parse } = observeForwardReads()
const events: AgentSessionSubscribeEvent[] = []
new AgentSessionSubscribers().open({
id: 'reader',
sessionId: identity.sessionId,
journal,
fence: 1,
cursor: { epoch: journal.epoch, sequence: 1 },
emit: (event) => events.push(event)
})
const batches = events.filter((event) => event.type === 'batch')
expect(batches.flatMap((event) => event.batch.items.map((item) => item.itemId))).toEqual(
Array.from({ length: count }, (_, index) => `item-${index}`)
)
expect(batches.at(-1)?.batch.cursor).toEqual(journal.cursor())
expect(returnedRows).toEqual([...Array<number>(9).fill(201), 200])
expect(parse).toHaveBeenCalledTimes(2_009)
})
it('reduces the timeline once for the whole catch-up, not once per page', async () => {
const journal = await seedJournal(2_000)
const render = vi.spyOn(journalReducer, 'renderJournalState')
const events: AgentSessionSubscribeEvent[] = []
new AgentSessionSubscribers().open({
id: 'reader',
sessionId: identity.sessionId,
journal,
fence: 1,
cursor: { epoch: journal.epoch, sequence: 1 },
emit: (event) => events.push(event)
})
// Catch-up is synchronous, so the reduced timeline cannot change between pages.
expect(events.filter((event) => event.type === 'batch')).toHaveLength(10)
expect(render).toHaveBeenCalledTimes(1)
})
it('keeps an exact final page final and preserves unlimited journal readers', async () => {
const journal = await seedJournal(6)
const cursor = { epoch: journal.epoch, sequence: 1 }
expect(journal.readSince(cursor)).toMatchObject({ ok: true, rows: expect.any(Array) })
const first = readAgentSessionHistory(journal, {
sessionId: identity.sessionId,
direction: 'after',
cursor,
limit: 3
})
expect(first).toMatchObject({ ok: true, page: { hasNewer: true } })
if (!first.ok) {
throw new Error('Expected first page')
}
const last = readAgentSessionHistory(journal, {
sessionId: identity.sessionId,
direction: 'after',
cursor: first.page.window.nextCursor,
limit: 3
})
expect(last).toMatchObject({ ok: true, page: { hasNewer: false } })
const unlimited = journal.readSince(cursor)
expect(unlimited.ok && unlimited.rows).toHaveLength(6)
})
it('reports a sequence gap when the next page reaches it', async () => {
const journal = await seedJournal(6)
const { db } = openJournalDatabase(journalDatabaseFile(root!))
try {
db.prepare('DELETE FROM journal_rows WHERE session_id = ? AND seq = ?').run(
identity.sessionId,
4
)
} finally {
db.close()
}
const first = readAgentSessionHistory(journal, {
sessionId: identity.sessionId,
direction: 'after',
cursor: { epoch: journal.epoch, sequence: 1 },
limit: 2
})
expect(first).toMatchObject({ ok: true, page: { hasNewer: true } })
if (!first.ok) {
throw new Error('Expected first page')
}
expect(
readAgentSessionHistory(journal, {
sessionId: identity.sessionId,
direction: 'after',
cursor: first.page.window.nextCursor,
limit: 2
})
).toMatchObject({ ok: false, reset: 'journal_gap' })
})
it.each(['{', '{"v":9999}'])(
'preserves parse-stop behavior at lookahead: %s',
async (rowJson) => {
const journal = await seedJournal(6)
const { db } = openJournalDatabase(journalDatabaseFile(root!))
try {
db.prepare('UPDATE journal_rows SET row_json = ? WHERE session_id = ? AND seq = ?').run(
rowJson,
identity.sessionId,
4
)
} finally {
db.close()
}
const page = readAgentSessionHistory(journal, {
sessionId: identity.sessionId,
direction: 'after',
cursor: { epoch: journal.epoch, sequence: 1 },
limit: 2
})
expect(page).toMatchObject({
ok: true,
page: { hasNewer: false, window: { nextCursor: { sequence: 3 } } }
})
if (!page.ok) {
throw new Error('Expected valid prefix')
}
expect(page.page.items.map((item) => item.itemId)).toEqual(['item-0', 'item-1'])
}
)
})
@@ -22,15 +22,28 @@ export function historyEntryBytes(
return Buffer.byteLength(JSON.stringify(item), 'utf8') + (submissionBytes.get(item.itemId) ?? 0)
}
// Keyed on the snapshot's own submissions array, which the reducer rebuilds on
// every change, so a paged read over one snapshot serializes submissions once.
const bytesBySubmissions = new WeakMap<
readonly AgentJournalSubmission[],
ReadonlyMap<string, number>
>()
export function submissionBytesByItemId(
submissions: readonly AgentJournalSubmission[]
): Map<string, number> {
return new Map(
): ReadonlyMap<string, number> {
const cached = bytesBySubmissions.get(submissions)
if (cached) {
return cached
}
const bytes = new Map(
submissions.map((submission) => [
agentJournalSubmissionKey(submission.clientMessageId),
Buffer.byteLength(JSON.stringify(submission), 'utf8')
])
)
bytesBySubmissions.set(submissions, bytes)
return bytes
}
export function oversizedHistoryItem(
@@ -45,9 +45,11 @@ export function resolveHistoryLimit(limit: number | undefined): number {
export function readAgentSessionHistory(
journal: AgentSessionJournal,
request: AgentSessionHistoryRequest
request: AgentSessionHistoryRequest,
/** Reduced state to read against. A synchronous multi-page catch-up passes one
* snapshot for the whole run so each page costs its own rows, not the timeline. */
snapshot: AgentJournalSnapshot = journal.snapshot()
): AgentSessionHistoryResult {
const snapshot = journal.snapshot()
if (journal.isReadOnly) {
return historyReset(snapshot, 'schema_unreadable')
}
@@ -90,6 +92,24 @@ export function readAgentSessionHistory(
}
}
/**
* A catch-up run over one journal. Pages share one reduced timeline, so the run
* costs its own rows instead of re-reducing every item per page; the cursor
* check re-reduces if anything did advance the journal between pages.
*/
export function createAgentSessionCatchUpReader(
journal: AgentSessionJournal
): (request: AgentSessionHistoryRequest) => AgentSessionHistoryResult {
let snapshot = journal.snapshot()
return (request) => {
const live = journal.cursor()
if (live.epoch !== snapshot.cursor.epoch || live.sequence !== snapshot.cursor.sequence) {
snapshot = journal.snapshot()
}
return readAgentSessionHistory(journal, request, snapshot)
}
}
export function readAgentSessionHydrationPage(
journal: AgentSessionJournal,
fence?: number
@@ -145,7 +165,8 @@ function readForward(
// a page it cannot place.
return historyReset(snapshot, 'cursor_ahead')
}
const since = journal.readSince(cursor)
// One lookahead preserves hasNewer without rereading the entire remaining journal per page.
const since = journal.readSince(cursor, limit + 1)
if (!since.ok) {
return historyReset(snapshot, since.reset)
}
@@ -6,7 +6,11 @@
// key instead of appearing as a second copy of the user's own message.
import { agentJournalSubmissionKey } from '../../../shared/agent-session-journal-item-key'
import type { AgentJournalSnapshot } from '../../../shared/agent-session-journal-types'
import type {
AgentJournalRenderItem,
AgentJournalSnapshot,
AgentJournalSubmission
} from '../../../shared/agent-session-journal-types'
import type { AgentSessionJournalBatch } from '../../../shared/agent-session-wire'
import { findSequenceGap } from '../agent-session-journal/journal-cursor'
import type { JournalRow } from '../agent-session-journal/journal-row-schema'
@@ -31,7 +35,7 @@ export function projectJournalBatch(input: {
if (gap) {
return { ok: false, reset: 'journal_gap' }
}
const aliases = submissionAliases(input.snapshot)
const aliases = submissionAliases(input.snapshot.submissions)
const touchedItemIds = new Set<string>()
const touchedClientMessageIds = new Set<string>()
for (const row of input.rows) {
@@ -57,7 +61,7 @@ export function projectJournalBatch(input: {
}
}
const live = new Map(input.snapshot.items.map((item) => [item.itemId, item]))
const live = liveItemsById(input.snapshot.items)
const items = [...touchedItemIds]
.map((itemId) => live.get(itemId))
.filter((item) => item !== undefined)
@@ -75,18 +79,49 @@ export function projectJournalBatch(input: {
}
}
// Both indexes are keyed on the snapshot arrays themselves, which the reducer
// rebuilds on every change, so a paged catch-up over one snapshot pays for them
// once instead of once per page — including the byte-shrink loop's re-projections.
const liveItemsByTimeline = new WeakMap<
readonly AgentJournalRenderItem[],
ReadonlyMap<string, AgentJournalRenderItem>
>()
const aliasesBySubmissions = new WeakMap<
readonly AgentJournalSubmission[],
ReadonlyMap<string, string>
>()
function liveItemsById(
items: readonly AgentJournalRenderItem[]
): ReadonlyMap<string, AgentJournalRenderItem> {
const cached = liveItemsByTimeline.get(items)
if (cached) {
return cached
}
const live = new Map(items.map((item) => [item.itemId, item]))
liveItemsByTimeline.set(items, live)
return live
}
/**
* Provider item id → the submission slot that adopted it, rebuilt from the
* snapshot's own accepted submissions. This mirrors the alias the reducer
* writes on an accepted dispatch; deriving it here keeps the projection a pure
* function of published state instead of reaching into reducer internals.
*/
function submissionAliases(snapshot: AgentJournalSnapshot): Map<string, string> {
function submissionAliases(
submissions: readonly AgentJournalSubmission[]
): ReadonlyMap<string, string> {
const cached = aliasesBySubmissions.get(submissions)
if (cached) {
return cached
}
const aliases = new Map<string, string>()
for (const submission of snapshot.submissions) {
for (const submission of submissions) {
if (submission.dispatchState === 'accepted' && submission.providerItemId) {
aliases.set(submission.providerItemId, agentJournalSubmissionKey(submission.clientMessageId))
}
}
aliasesBySubmissions.set(submissions, aliases)
return aliases
}
@@ -18,7 +18,7 @@ import {
} from '../../../shared/agent-session-wire'
import type { AgentSessionJournal } from '../agent-session-journal/journal-store'
import {
readAgentSessionHistory,
createAgentSessionCatchUpReader,
readAgentSessionHydrationPage
} from './agent-session-history-page'
@@ -229,14 +229,13 @@ export class AgentSessionSubscribers {
backgroundTasks?: AgentSessionBackgroundTaskState | null,
activity?: AgentSessionTurnActivity | null
): void {
const publishedActivity =
activity !== undefined
? activity
: emitCheckpoint
? (this.activityBySession.get(subscriber.sessionId) ?? null)
: undefined
const checkpointActivity = emitCheckpoint
? this.activityField(subscriber.sessionId).activity
: undefined
const publishedActivity = activity !== undefined ? activity : checkpointActivity
const readPage = createAgentSessionCatchUpReader(journal)
while (true) {
const result = readAgentSessionHistory(journal, {
const result = readPage({
sessionId: subscriber.sessionId,
direction: 'after',
cursor: subscriber.cursor,
+3 -3
View File
@@ -1,3 +1,4 @@
import { highestUsageKey } from '../usage/highest-usage-key'
import type {
OpenCodeUsageBreakdownKind,
OpenCodeUsageBreakdownRow,
@@ -47,9 +48,8 @@ export function buildOpenCodeUsageSummary(
byProject.set(row.projectLabel, (byProject.get(row.projectLabel) ?? 0) + row.totalTokens)
}
const topModel = [...byModel.entries()].sort((left, right) => right[1] - left[1])[0]?.[0] ?? null
const topProject =
[...byProject.entries()].sort((left, right) => right[1] - left[1])[0]?.[0] ?? null
const topModel = highestUsageKey(byModel)
const topProject = highestUsageKey(byProject)
return {
scope,
@@ -0,0 +1,74 @@
import { describe, expect, it, vi } from 'vitest'
import type { PersistedState } from '../../../shared/persisted-state-types'
import { updateSettings, type SettingsMutationOperations } from './settings-update'
function makeOperations(): SettingsMutationOperations {
return {
// Only the fields updateSettings reads; the rest of GlobalSettings is irrelevant to the clamp.
state: { settings: { terminalFontSize: 14 }, repos: [] } as unknown as PersistedState,
bumpLocalWorktreeScanGeneration: vi.fn(),
removeRetainedBlob: vi.fn(),
scheduleSave: vi.fn(),
notifySettingsChanged: vi.fn()
}
}
// #10754: desktop IPC, the web RPC and the CLI all reach the store through this boundary, and xterm
// throws on a non-finite minimumContrastRatio, so the clamp cannot live in the settings UI alone.
describe('updateSettings terminalMinimumContrastRatio', () => {
it('persists an in-range floor unchanged', () => {
const operations = makeOperations()
expect(
updateSettings(operations, { terminalMinimumContrastRatio: 1 }).terminalMinimumContrastRatio
).toBe(1)
expect(
updateSettings(operations, { terminalMinimumContrastRatio: 4.5 }).terminalMinimumContrastRatio
).toBe(4.5)
})
it('clamps a hand-edited value into xterm range', () => {
const operations = makeOperations()
expect(
updateSettings(operations, { terminalMinimumContrastRatio: 0 }).terminalMinimumContrastRatio
).toBe(1)
expect(
updateSettings(operations, { terminalMinimumContrastRatio: 500 }).terminalMinimumContrastRatio
).toBe(21)
})
it('drops an unusable value back to automatic rather than storing it', () => {
const operations = makeOperations()
expect(
updateSettings(operations, {
terminalMinimumContrastRatio: Number.NaN
}).terminalMinimumContrastRatio
).toBeUndefined()
expect(
updateSettings(operations, {
terminalMinimumContrastRatio: 'off' as unknown as number
}).terminalMinimumContrastRatio
).toBeUndefined()
})
it('clears the override so the automatic floor comes back', () => {
const operations = makeOperations()
updateSettings(operations, { terminalMinimumContrastRatio: 1 })
expect(
updateSettings(operations, { terminalMinimumContrastRatio: undefined })
.terminalMinimumContrastRatio
).toBeUndefined()
})
it('leaves a stored floor alone when an unrelated setting is written', () => {
const operations = makeOperations()
updateSettings(operations, { terminalMinimumContrastRatio: 1 })
expect(updateSettings(operations, { terminalFontSize: 15 }).terminalMinimumContrastRatio).toBe(
1
)
})
})
@@ -9,6 +9,7 @@ import { normalizeTerminalQuickCommands } from '../../../shared/terminal-quick-c
import { normalizeTerminalCustomThemes } from '../../../shared/terminal-custom-themes'
import { normalizeTerminalCursorStyleDefault } from '../../../shared/terminal-cursor-style-settings'
import { normalizeDesktopTerminalScrollbackRows } from '../../../shared/terminal-scrollback-policy'
import { normalizeTerminalMinimumContrastRatio } from '../../../shared/terminal-minimum-contrast-settings'
import { normalizeTaskProviderSettings } from '../../../shared/task-providers'
import { normalizeOpenInApplications } from '../../../shared/open-in-applications'
import { normalizeTerminalShortcutPolicy } from '../../../shared/keybindings'
@@ -123,6 +124,13 @@ export function updateSettings(
updates.terminalScrollbackRows
)
}
// Why here: every writer (desktop IPC, web RPC, CLI) crosses this boundary, so xterm can never be
// handed an out-of-range floor, and undefined stays undefined to mean "automatic" (#10754).
if ('terminalMinimumContrastRatio' in updates) {
sanitizedUpdates.terminalMinimumContrastRatio = normalizeTerminalMinimumContrastRatio(
updates.terminalMinimumContrastRatio
)
}
if (
'terminalTuiScrollSensitivity' in updates ||
'terminalTuiScrollSensitivityDefaultedToOne' in updates
@@ -133,7 +133,7 @@ export class OrcaRuntimeWithSubscribeToTerminalResize extends OrcaRuntimeWithApp
exitCause: cause,
handle
}),
...(recipient.runId ? { runId: recipient.runId } : {})
runId: dispatch.run_id
})
this.notifyMessageArrived(escalation.to_handle, escalation.type)
} catch (error) {
@@ -95,7 +95,10 @@ describe('OrcaRuntimeService', () => {
return [name, createRootDispatch(db, task.id, handles[name], paneKey(name))]
})
)
const legacyTask = db.createTask({ spec: 'legacy worker' })
const legacyTask = db.createTask({
runId: 'run_legacy_local',
spec: 'legacy worker'
})
const legacyDispatch = createRootDispatch(
db,
legacyTask.id,
@@ -216,7 +216,10 @@ describe('OrcaRuntimeService', () => {
runtime as unknown as {
leaves: Map<
string,
{ lastAgentStatus: string | null; lastAgentStatusObservedLive: boolean }
{
lastAgentStatus: string | null
lastAgentStatusObservedLive: boolean
}
>
}
).leaves.values()
@@ -415,7 +418,12 @@ describe('OrcaRuntimeService', () => {
const [terminal] = (await runtime.listTerminals()).terminals
runtime.onPtyData('pty-1', '\x1b]0;Codex working\x07', 100)
db.insertMessage({ from: 'term_worker', to: terminal.handle, subject: 'pending' })
db.insertMessage({
runId: 'run_legacy_local',
from: 'term_worker',
to: terminal.handle,
subject: 'pending'
})
runtime.notifyMessageArrived(terminal.handle, 'status')
db.close()
@@ -56,7 +56,10 @@ describe('OrcaRuntimeService', () => {
)
const db = new OrchestrationDb(':memory:')
try {
const task = db.createTask({ spec: 'continue after missing worker recovery' })
const task = db.createTask({
runId: 'run_legacy_local',
spec: 'continue after missing worker recovery'
})
const started = db.createStartingWorkerDispatch({
creator: { kind: 'system' },
maxDepth: Number.MAX_SAFE_INTEGER,
@@ -163,7 +166,10 @@ describe('OrcaRuntimeService', () => {
)
const db = new OrchestrationDb(':memory:')
try {
const task = db.createTask({ spec: 'retry missing worker recovery' })
const task = db.createTask({
runId: 'run_legacy_local',
spec: 'retry missing worker recovery'
})
const started = db.createStartingWorkerDispatch({
creator: { kind: 'system' },
maxDepth: Number.MAX_SAFE_INTEGER,
@@ -7,9 +7,13 @@ type PointerTarget = { ptyId: string; processIncarnation: string }
// The slice of the mailbox store the pointer batch selector depends on.
type PointerStore = {
insertMessage(message: { from: string; to: string; subject: string; type?: MessageType }): {
id: string
}
insertMessage(message: {
runId: string
from: string
to: string
subject: string
type?: MessageType
}): { id: string }
stageMailboxPointerEnter(ids: string[], target: PointerTarget): boolean
markMailboxPointerWriteAttempted(ids: string[], target: PointerTarget): boolean
getUndeliveredUnreadMessages(
@@ -32,7 +36,12 @@ describe.each(STORES)('mailbox pointer reservations (%s)', (_name, createStore)
it('refuses a claim another flight already holds', () => {
const store = createStore()
const message = store.insertMessage({ from: 'a', to: 'run:run-1', subject: 'contended' })
const message = store.insertMessage({
runId: 'run_legacy_local',
from: 'a',
to: 'run:run-1',
subject: 'contended'
})
expect(store.stageMailboxPointerEnter([message.id], rival)).toBe(true)
expect(store.stageMailboxPointerEnter([message.id], mine)).toBe(false)
@@ -41,8 +50,18 @@ describe.each(STORES)('mailbox pointer reservations (%s)', (_name, createStore)
it('rolls the whole batch back when one row is already claimed', () => {
const store = createStore()
const free = store.insertMessage({ from: 'a', to: 'run:run-1', subject: 'free' })
const taken = store.insertMessage({ from: 'a', to: 'run:run-1', subject: 'taken' })
const free = store.insertMessage({
runId: 'run_legacy_local',
from: 'a',
to: 'run:run-1',
subject: 'free'
})
const taken = store.insertMessage({
runId: 'run_legacy_local',
from: 'a',
to: 'run:run-1',
subject: 'taken'
})
expect(store.stageMailboxPointerEnter([taken.id], rival)).toBe(true)
expect(store.stageMailboxPointerEnter([free.id, taken.id], mine)).toBe(false)
@@ -52,8 +71,19 @@ describe.each(STORES)('mailbox pointer reservations (%s)', (_name, createStore)
it('applies the exclusion and limit the pointer batch selector relies on', () => {
const store = createStore()
store.insertMessage({ from: 'a', to: 'run:run-1', subject: 'reserved', type: 'escalation' })
const kept = store.insertMessage({ from: 'a', to: 'run:run-1', subject: 'kept' })
store.insertMessage({
runId: 'run_legacy_local',
from: 'a',
to: 'run:run-1',
subject: 'reserved',
type: 'escalation'
})
const kept = store.insertMessage({
runId: 'run_legacy_local',
from: 'a',
to: 'run:run-1',
subject: 'kept'
})
expect(
store
@@ -12,13 +12,14 @@ describe('coordinator decision-gate authority', () => {
it('opens a gate only for the sender-owned active Dispatch', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'owned gate target' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'owned gate target' })
const dispatch = createRootDispatch(db, task.id, 'term_owner', 'tab_owner:leaf_owner')
const logs: string[] = []
openDecisionGateFromMessage(
db,
db.insertMessage({
runId: 'run_legacy_local',
from: 'term_owner',
to: 'term_coordinator',
subject: 'Need approval',
@@ -41,20 +42,27 @@ describe('coordinator decision-gate authority', () => {
it('rejects a gate targeting another active Dispatch without mutating either Task', () => {
db = new OrchestrationDb(':memory:')
const attackerTask = db.createTask({ spec: 'attacker assignment' })
const attackerTask = db.createTask({
runId: 'run_legacy_local',
spec: 'attacker assignment'
})
const attacker = createRootDispatch(
db,
attackerTask.id,
'term_attacker',
'tab_attacker:leaf_attacker'
)
const victimTask = db.createTask({ spec: 'victim assignment' })
const victimTask = db.createTask({
runId: 'run_legacy_local',
spec: 'victim assignment'
})
const victim = createRootDispatch(db, victimTask.id, 'term_victim', 'tab_victim:leaf_victim')
const logs: string[] = []
openDecisionGateFromMessage(
db,
db.insertMessage({
runId: 'run_legacy_local',
from: 'term_attacker',
to: 'term_coordinator',
subject: 'Block the victim',
@@ -79,12 +87,16 @@ describe('coordinator decision-gate authority', () => {
it('accepts the canonical sender of an imported federated Dispatch', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'remote gate target' })
const task = db.createTask({
runId: 'run_legacy_local',
spec: 'remote gate target'
})
const dispatch = createRootDispatch(db, task.id, 'remote-worker')
openDecisionGateFromMessage(
db,
db.insertMessage({
runId: 'run_legacy_local',
from: `dispatch:${dispatch.id}`,
to: 'term_coordinator',
subject: 'Remote approval required',
@@ -66,7 +66,7 @@ describe('coordinator dispatch with an unobserved prompt', () => {
it('never re-pastes a preamble whose turn start was not observed', async () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'do the work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'do the work' })
const runtime = createRuntime(new Error('agent_prompt_stalled'))
const logs: string[] = []
@@ -88,7 +88,7 @@ describe('coordinator dispatch with an unobserved prompt', () => {
it('lets a late worker report settle a dispatch whose prompt was unobserved', async () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'do the work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'do the work' })
await dispatch(createRuntime(new Error('agent_prompt_stalled')), task.id, [])
const dispatchId = db.getDispatchContext(task.id)!.id
const minted = db.mintDispatchCapability({
@@ -119,7 +119,7 @@ describe('coordinator dispatch with an unobserved prompt', () => {
it('still fails the dispatch when the prompt was never delivered', async () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'do the work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'do the work' })
const runtime = createRuntime(new Error('terminal_not_writable'))
await expect(dispatch(runtime, task.id, [])).rejects.toThrow('terminal_not_writable')
@@ -44,8 +44,8 @@ describe('Coordinator drift probe coalescing', () => {
: { base: 'origin/main', behind: 0, recentSubjects: [] }
}
}
const first = db.createTask({ spec: 'first task' })
const second = db.createTask({ spec: 'second task' })
const first = db.createTask({ runId: 'run_legacy_local', spec: 'first task' })
const second = db.createTask({ runId: 'run_legacy_local', spec: 'second task' })
const coordinator = new Coordinator(db, runtime, {
spec: 'go',
coordinatorHandle: 'coord',
@@ -65,6 +65,7 @@ describe('Coordinator drift probe coalescing', () => {
throw new Error(`missing dispatch for ${task.id}`)
}
db.insertMessage({
runId: 'run_legacy_local',
from: dispatch.assignee_handle,
to: 'coord',
subject: 'Done',
@@ -105,8 +106,14 @@ describe('Coordinator drift probe coalescing', () => {
}
}
}
const refused = db.createTask({ spec: 'requires a current base' })
const allowed = db.createTask({ spec: 'can use stale base\nallow-stale-base: true' })
const refused = db.createTask({
runId: 'run_legacy_local',
spec: 'requires a current base'
})
const allowed = db.createTask({
runId: 'run_legacy_local',
spec: 'can use stale base\nallow-stale-base: true'
})
const coordinator = new Coordinator(db, runtime, {
spec: 'go',
coordinatorHandle: 'coord',
@@ -12,20 +12,21 @@ describe('coordinator escalation authority', () => {
it('rejects an escalation targeting another active Dispatch', () => {
db = new OrchestrationDb(':memory:')
const attackerTask = db.createTask({ spec: 'attacker assignment' })
const attackerTask = db.createTask({ runId: 'run_legacy_local', spec: 'attacker assignment' })
const attacker = createRootDispatch(
db,
attackerTask.id,
'term_attacker',
'tab_attacker:leaf_attacker'
)
const victimTask = db.createTask({ spec: 'victim assignment' })
const victimTask = db.createTask({ runId: 'run_legacy_local', spec: 'victim assignment' })
const victim = createRootDispatch(db, victimTask.id, 'term_victim')
const logs: string[] = []
applyEscalationToDispatch(
db,
db.insertMessage({
runId: 'run_legacy_local',
from: 'term_attacker',
to: 'term_coordinator',
subject: 'Fail the victim',
@@ -43,12 +44,13 @@ describe('coordinator escalation authority', () => {
it('accepts the canonical sender of an imported federated Dispatch', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'remote escalation target' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'remote escalation target' })
const dispatch = createRootDispatch(db, task.id, 'remote-worker')
applyEscalationToDispatch(
db,
db.insertMessage({
runId: 'run_legacy_local',
from: `dispatch:${dispatch.id}`,
to: 'term_coordinator',
subject: 'Remote worker failed',
@@ -0,0 +1,52 @@
import { describe, expect, it } from 'vitest'
import { parseAllowStaleBaseFromSpec } from './coordinator-stale-base-flag'
describe('parseAllowStaleBaseFromSpec', () => {
it('matches canonical form on its own line and strips it', () => {
const spec = `Do the work
allow-stale-base: true`
const { allowStale, strippedSpec } = parseAllowStaleBaseFromSpec(spec)
expect(allowStale).toBe(true)
expect(strippedSpec).toBe('Do the work\n')
expect(strippedSpec).not.toContain('allow-stale-base')
})
it('matches case-insensitively', () => {
const spec = `Do the work
Allow-Stale-Base: TRUE`
const { allowStale, strippedSpec } = parseAllowStaleBaseFromSpec(spec)
expect(allowStale).toBe(true)
expect(strippedSpec).not.toMatch(/[Aa]llow-[Ss]tale-[Bb]ase/)
})
it('does not match allow-stale-base: false', () => {
const spec = `Do the work
allow-stale-base: false`
const { allowStale, strippedSpec } = parseAllowStaleBaseFromSpec(spec)
expect(allowStale).toBe(false)
expect(strippedSpec).toBe(spec)
})
it('does not match allow-stale-base: truthy', () => {
const spec = `Do the work
allow-stale-base: truthy`
const { allowStale, strippedSpec } = parseAllowStaleBaseFromSpec(spec)
expect(allowStale).toBe(false)
expect(strippedSpec).toBe(spec)
})
it('does not match the flag embedded inside a sentence', () => {
const spec = 'we allow-stale-base: true sometimes'
const { allowStale, strippedSpec } = parseAllowStaleBaseFromSpec(spec)
expect(allowStale).toBe(false)
expect(strippedSpec).toBe(spec)
})
it('handles the flag as the last line with no trailing newline', () => {
const spec = 'line 1\nallow-stale-base: true'
const { allowStale, strippedSpec } = parseAllowStaleBaseFromSpec(spec)
expect(allowStale).toBe(true)
expect(strippedSpec).toBe('line 1\n')
expect(strippedSpec.endsWith('allow-stale-base: true')).toBe(false)
})
})
@@ -3,12 +3,11 @@ import { OrchestrationDb } from './db'
import { reconcileLifecycleMessage } from './lifecycle-reconciliation'
import { Coordinator } from './coordinator'
import type { CoordinatorRuntime } from './coordinator-runtime-contract'
import {
DISPATCH_STALE_THRESHOLD,
parseAllowStaleBaseFromSpec
} from './coordinator-stale-base-flag'
import { DISPATCH_STALE_THRESHOLD } from './coordinator-stale-base-flag'
import { createRootDispatch } from './db/root-dispatch-test-fixture'
const runId = 'run_legacy_local'
type DriftResult = {
base: string
behind: number
@@ -92,6 +91,7 @@ function insertWorkerDone(
}
const from = params.from ?? dispatch?.assignee_handle ?? 'term_unknown'
db.insertMessage({
runId,
from,
to: params.to ?? 'coord',
subject: 'Done',
@@ -131,7 +131,10 @@ describe('Coordinator', () => {
runtime.cliCommand = 'orca-ide'
runtime.terminals = [{ handle: 'term_a', worktreeId: 'wt1', connected: true, writable: true }]
const task = db.createTask({ spec: 'implement feature' })
const task = db.createTask({
runId,
spec: 'implement feature'
})
// Simulate worker_done arriving after dispatch
const coordinator = new Coordinator(db, runtime, {
@@ -166,7 +169,10 @@ describe('Coordinator', () => {
getTerminalPaneKey: (handle: string) => (handle === 'term_a' ? 'tab_a:leaf_a' : null)
})
const task = db.createTask({ spec: 'implement feature' })
const task = db.createTask({
runId,
spec: 'implement feature'
})
const coordinator = new Coordinator(db, withPaneLookup, {
spec: 'build it',
coordinatorHandle: 'coord',
@@ -198,7 +204,10 @@ describe('Coordinator', () => {
}
: null
})
const task = db.createTask({ spec: 'implement feature' })
const task = db.createTask({
runId,
spec: 'implement feature'
})
const coordinator = new Coordinator(db, withAuthority, {
spec: 'build it',
coordinatorHandle: 'coord',
@@ -223,9 +232,13 @@ describe('Coordinator', () => {
db = new OrchestrationDb(':memory:')
const runtime = createMockRuntime()
const task = db.createTask({ spec: 'send-driven completion' })
const task = db.createTask({
runId,
spec: 'send-driven completion'
})
const dispatch = createRootDispatch(db, task.id, 'term_a')
const msg = db.insertMessage({
runId,
from: 'term_a',
to: 'coord',
subject: 'Done',
@@ -250,7 +263,10 @@ describe('Coordinator', () => {
db = new OrchestrationDb(':memory:')
const runtime = createMockRuntime()
const task = db.createTask({ spec: 'duplicate completion' })
const task = db.createTask({
runId,
spec: 'duplicate completion'
})
const dispatch = createRootDispatch(db, task.id, 'term_a')
const payload = JSON.stringify({
taskId: task.id,
@@ -258,6 +274,7 @@ describe('Coordinator', () => {
outcome: 'succeeded'
})
const first = db.insertMessage({
runId,
from: 'term_a',
to: 'coord',
subject: 'Done',
@@ -265,6 +282,7 @@ describe('Coordinator', () => {
payload
})
db.insertMessage({
runId,
from: 'term_a',
to: 'coord',
subject: 'Done again',
@@ -289,7 +307,7 @@ describe('Coordinator', () => {
db = new OrchestrationDb(':memory:')
const runtime = createMockRuntime()
const task = db.createTask({ spec: 'work' })
const task = db.createTask({ runId, spec: 'work' })
const coordinator = new Coordinator(db, runtime, {
spec: 'go',
@@ -321,7 +339,10 @@ describe('Coordinator', () => {
{ handle: 'term_b', worktreeId: 'wt1', connected: true, writable: true }
]
const task = db.createTask({ spec: 'risky work' })
const task = db.createTask({
runId,
spec: 'risky work'
})
const coordinator = new Coordinator(db, runtime, {
spec: 'go',
@@ -339,6 +360,7 @@ describe('Coordinator', () => {
const dispatch = db.getDispatchContext(task.id)
expect(dispatch).toBeDefined()
db.insertMessage({
runId,
from: dispatch?.assignee_handle ?? 'missing-worker',
to: 'coord',
subject: `Failed attempt ${i + 1}`,
@@ -360,7 +382,10 @@ describe('Coordinator', () => {
throw new Error('terminal_not_writable')
}
const task = db.createTask({ spec: 'cannot dispatch' })
const task = db.createTask({
runId,
spec: 'cannot dispatch'
})
const coordinator = new Coordinator(db, runtime, {
spec: 'go',
coordinatorHandle: 'coord',
@@ -379,7 +404,10 @@ describe('Coordinator', () => {
const runtime = createMockRuntime()
runtime.terminals = [{ handle: 'term_a', worktreeId: 'wt1', connected: true, writable: true }]
const task = db.createTask({ spec: 'needs approval' })
const task = db.createTask({
runId,
spec: 'needs approval'
})
const coordinator = new Coordinator(db, runtime, {
spec: 'go',
@@ -398,6 +426,7 @@ describe('Coordinator', () => {
const dispatch = db.getDispatchContext(task.id)
expect(dispatch).toBeDefined()
db.insertMessage({
runId,
from: 'term_a',
to: 'coord',
subject: 'Need approval',
@@ -441,8 +470,12 @@ describe('Coordinator', () => {
const runtime = createMockRuntime()
runtime.terminals = [{ handle: 'term_a', worktreeId: 'wt1', connected: true, writable: true }]
const t1 = db.createTask({ spec: 'first' })
const t2 = db.createTask({ spec: 'second', deps: [t1.id] })
const t1 = db.createTask({ runId, spec: 'first' })
const t2 = db.createTask({
runId,
spec: 'second',
deps: [t1.id]
})
expect(t2.status).toBe('pending')
@@ -492,9 +525,9 @@ describe('Coordinator', () => {
{ handle: 'term_c', worktreeId: 'wt1', connected: true, writable: true }
]
const t1 = db.createTask({ spec: 'one' })
const t2 = db.createTask({ spec: 'two' })
const t3 = db.createTask({ spec: 'three' })
const t1 = db.createTask({ runId, spec: 'one' })
const t2 = db.createTask({ runId, spec: 'two' })
const t3 = db.createTask({ runId, spec: 'three' })
const coordinator = new Coordinator(db, runtime, {
spec: 'go',
@@ -530,7 +563,7 @@ describe('Coordinator', () => {
const runtime = createMockRuntime()
// No terminals available so dispatchReadyTasks creates one and we can
// drive the stale-scan deterministically via SQL backdating.
const task = db.createTask({ spec: 'work' })
const task = db.createTask({ runId, spec: 'work' })
const ctx = createRootDispatch(db, task.id, 'term_stale')
// Backdate dispatched_at and last_heartbeat_at beyond the 10-min threshold
@@ -569,7 +602,7 @@ describe('Coordinator', () => {
const runtime = createMockRuntime()
runtime.terminals = [{ handle: 'term_a', worktreeId: 'wt1', connected: true, writable: true }]
const task = db.createTask({ spec: 'work' })
const task = db.createTask({ runId, spec: 'work' })
const ctx = createRootDispatch(db, task.id, 'term_a')
const coordinator = new Coordinator(db, runtime, {
@@ -581,6 +614,7 @@ describe('Coordinator', () => {
const runPromise = coordinator.run()
db.insertMessage({
runId,
from: 'term_a',
to: 'coord',
subject: 'alive',
@@ -606,12 +640,16 @@ describe('Coordinator', () => {
const runtime = createMockRuntime()
const logs: string[] = []
const task = db.createTask({ spec: 'retry-sensitive work' })
const task = db.createTask({
runId,
spec: 'retry-sensitive work'
})
const staleCtx = createRootDispatch(db, task.id, 'term_old')
db.failDispatch(staleCtx.id, 'retry elsewhere')
const activeCtx = createRootDispatch(db, task.id, 'term_current')
db.insertMessage({
runId,
from: 'term_old',
to: 'coord',
subject: 'Late done',
@@ -663,11 +701,15 @@ describe('Coordinator', () => {
const runtime = createMockRuntime()
const logs: string[] = []
const task = db.createTask({ spec: 'owned work' })
const task = db.createTask({
runId,
spec: 'owned work'
})
const leafId = '11111111-1111-4111-8111-111111111111'
const ctx = createRootDispatch(db, task.id, 'term_owner', `tab_before:${leafId}`)
db.insertMessage({
runId,
from: 'term_reminted',
to: 'coord',
subject: 'Done after restart',
@@ -693,7 +735,7 @@ describe('Coordinator', () => {
it('can be stopped', async () => {
db = new OrchestrationDb(':memory:')
const runtime = createMockRuntime()
db.createTask({ spec: 'never finishes' })
db.createTask({ runId, spec: 'never finishes' })
const coordinator = new Coordinator(db, runtime, {
spec: 'go',
@@ -723,7 +765,10 @@ describe('Coordinator', () => {
recentSubjects: ['fix A', 'fix B', 'fix C']
})
const task = db.createTask({ spec: 'do the work' })
const task = db.createTask({
runId,
spec: 'do the work'
})
const coordinator = new Coordinator(db, runtime, {
spec: 'go',
@@ -759,7 +804,10 @@ describe('Coordinator', () => {
recentSubjects: ['fix A']
})
const task = db.createTask({ spec: 'do the work' })
const task = db.createTask({
runId,
spec: 'do the work'
})
const coordinator = new Coordinator(db, runtime, {
spec: 'go',
@@ -799,7 +847,7 @@ describe('Coordinator', () => {
const spec = `Investigate issue #42
allow-stale-base: true`
const task = db.createTask({ spec })
const task = db.createTask({ runId, spec })
const coordinator = new Coordinator(db, runtime, {
spec: 'go',
@@ -832,7 +880,10 @@ allow-stale-base: true`
runtime.terminals = [{ handle: 'term_a', worktreeId: 'wt1', connected: true, writable: true }]
runtime.setProbeDrift(null)
const task = db.createTask({ spec: 'do the work' })
const task = db.createTask({
runId,
spec: 'do the work'
})
const coordinator = new Coordinator(db, runtime, {
spec: 'go',
@@ -861,7 +912,10 @@ allow-stale-base: true`
runtime.terminals = [{ handle: 'term_a', worktreeId: 'wt1', connected: true, writable: true }]
const logs: string[] = []
const task = db.createTask({ spec: 'do the work' })
const task = db.createTask({
runId,
spec: 'do the work'
})
const coordinator = new Coordinator(db, runtime, {
spec: 'go',
@@ -892,7 +946,10 @@ allow-stale-base: true`
runtime.terminals = [{ handle: 'term_a', worktreeId: 'wt1', connected: true, writable: true }]
runtime.throwProbeDrift = new Error('boom')
const task = db.createTask({ spec: 'do the work' })
const task = db.createTask({
runId,
spec: 'do the work'
})
const coordinator = new Coordinator(db, runtime, {
spec: 'go',
@@ -915,53 +972,3 @@ allow-stale-base: true`
})
})
})
describe('parseAllowStaleBaseFromSpec', () => {
it('matches canonical form on its own line and strips it', () => {
const spec = `Do the work
allow-stale-base: true`
const { allowStale, strippedSpec } = parseAllowStaleBaseFromSpec(spec)
expect(allowStale).toBe(true)
expect(strippedSpec).toBe('Do the work\n')
expect(strippedSpec).not.toContain('allow-stale-base')
})
it('matches case-insensitively', () => {
const spec = `Do the work
Allow-Stale-Base: TRUE`
const { allowStale, strippedSpec } = parseAllowStaleBaseFromSpec(spec)
expect(allowStale).toBe(true)
expect(strippedSpec).not.toMatch(/[Aa]llow-[Ss]tale-[Bb]ase/)
})
it('does not match allow-stale-base: false', () => {
const spec = `Do the work
allow-stale-base: false`
const { allowStale, strippedSpec } = parseAllowStaleBaseFromSpec(spec)
expect(allowStale).toBe(false)
expect(strippedSpec).toBe(spec)
})
it('does not match allow-stale-base: truthy', () => {
const spec = `Do the work
allow-stale-base: truthy`
const { allowStale, strippedSpec } = parseAllowStaleBaseFromSpec(spec)
expect(allowStale).toBe(false)
expect(strippedSpec).toBe(spec)
})
it('does not match the flag embedded inside a sentence', () => {
const spec = 'we allow-stale-base: true sometimes'
const { allowStale, strippedSpec } = parseAllowStaleBaseFromSpec(spec)
expect(allowStale).toBe(false)
expect(strippedSpec).toBe(spec)
})
it('handles the flag as the last line with no trailing newline', () => {
const spec = 'line 1\nallow-stale-base: true'
const { allowStale, strippedSpec } = parseAllowStaleBaseFromSpec(spec)
expect(allowStale).toBe(true)
expect(strippedSpec).toBe('line 1\n')
expect(strippedSpec.endsWith('allow-stale-base: true')).toBe(false)
})
})
@@ -64,7 +64,7 @@ describe('orchestration empty-dispatch short-circuit (benchmark)', () => {
it('still runs the fan-out once a dispatch exists (correctness preserved)', () => {
const db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' })
createRootDispatch(db, task.id, 'term_5')
const handles = Array.from({ length: 10 }, (_, i) => `term_${i}`)
@@ -76,7 +76,11 @@ describe('orchestration empty-dispatch short-circuit (benchmark)', () => {
it('predicate lifecycle: false when empty, true after dispatch (even completed), false after reset', () => {
const db = new OrchestrationDb(':memory:')
expect(db.hasAnyDispatchContexts()).toBe(false)
const ctx = createRootDispatch(db, db.createTask({ spec: 'work' }).id, 'term_worker')
const ctx = createRootDispatch(
db,
db.createTask({ runId: 'run_legacy_local', spec: 'work' }).id,
'term_worker'
)
expect(db.hasAnyDispatchContexts()).toBe(true)
// Completed rows still count — recent-completed lookups must stay valid.
db.completeDispatch(ctx.id)
@@ -13,7 +13,7 @@ afterEach(() => {
function seedHeartbeatedDispatch(): { d: OrchestrationDb; dispatchId: string } {
const d = new OrchestrationDb(':memory:')
db = d
const task = d.createTask({ spec: 'work' })
const task = d.createTask({ runId: 'run_legacy_local', spec: 'work' })
const dispatch = createRootDispatch(d, task.id, 'term_worker')
d.recordHeartbeat(dispatch.id, '2026-05-03T00:00:00.000Z')
return { d, dispatchId: dispatch.id }
@@ -8,7 +8,12 @@ describe('orchestration message timestamps', () => {
it('exposes SQLite timestamps with an explicit UTC designator', () => {
db = new OrchestrationDb(':memory:')
const message = db.insertMessage({ from: 'a', to: 'b', subject: 'timestamped' })
const message = db.insertMessage({
runId: 'run_legacy_local',
from: 'a',
to: 'b',
subject: 'timestamped'
})
expect(message.created_at).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/)
db.markAsDelivered([message.id])
@@ -0,0 +1,194 @@
import { afterEach, describe, expect, it } from 'vitest'
import type Database from '../../sqlite/sync-database'
import { OrchestrationDb, type MessageType } from './db'
const runId = 'run_legacy_local'
describe('OrchestrationDb', () => {
let db: OrchestrationDb | undefined
afterEach(() => {
db?.close()
})
function createDb(): OrchestrationDb {
db = new OrchestrationDb(':memory:')
return db
}
describe('messages', () => {
it('inserts and retrieves a message', () => {
const d = createDb()
const msg = d.insertMessage({
runId,
from: 'term_a',
to: 'term_b',
subject: 'hello',
body: 'world'
})
expect(msg.id).toMatch(/^msg_/)
expect(msg.from_handle).toBe('term_a')
expect(msg.to_handle).toBe('term_b')
expect(msg.subject).toBe('hello')
expect(msg.body).toBe('world')
expect(msg.type).toBe('status')
expect(msg.priority).toBe('normal')
expect(msg.read).toBe(0)
expect(msg.sequence).toBeGreaterThan(0)
})
it('returns unread messages in sequence order', () => {
const d = createDb()
d.insertMessage({ runId, from: 'a', to: 'b', subject: 'first' })
d.insertMessage({ runId, from: 'a', to: 'b', subject: 'second' })
d.insertMessage({ runId, from: 'a', to: 'c', subject: 'other' })
const unread = d.getUnreadMessages('b')
expect(unread).toHaveLength(2)
expect(unread[0].subject).toBe('first')
expect(unread[1].subject).toBe('second')
})
it('filters unread by type', () => {
const d = createDb()
d.insertMessage({
runId,
from: 'a',
to: 'b',
subject: 'status msg',
type: 'status'
})
d.insertMessage({
runId,
from: 'a',
to: 'b',
subject: 'done msg',
type: 'worker_done'
})
const filtered = d.getUnreadMessages('b', ['worker_done'])
expect(filtered).toHaveLength(1)
expect(filtered[0].type).toBe('worker_done')
})
it('excludes already-delivered rows from getUndeliveredUnreadMessages', () => {
const d = createDb()
const m1 = d.insertMessage({ runId, from: 'a', to: 'b', subject: 'one' })
const m2 = d.insertMessage({ runId, from: 'a', to: 'b', subject: 'two' })
d.markAsDelivered([m1.id])
// Push delivery query: only undelivered, unread.
const pending = d.getUndeliveredUnreadMessages('b')
expect(pending).toHaveLength(1)
expect(pending[0].id).toBe(m2.id)
// Explicit `check` still sees both (they are still unread).
const unread = d.getUnreadMessages('b')
expect(unread).toHaveLength(2)
})
it('creates the undelivered inbox index used by push delivery', () => {
const d = createDb()
const sqlite = (d as unknown as { db: Database.Database }).db
const indexes = sqlite
.prepare(
`SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'messages' AND name = 'idx_messages_undelivered_inbox'`
)
.all()
expect(indexes).toHaveLength(1)
})
it('filters getUndeliveredUnreadMessages by type', () => {
const d = createDb()
d.insertMessage({
runId,
from: 'a',
to: 'b',
subject: 's',
type: 'status'
})
const wd = d.insertMessage({
runId,
from: 'a',
to: 'b',
subject: 'd',
type: 'worker_done'
})
const filtered = d.getUndeliveredUnreadMessages('b', ['worker_done'])
expect(filtered).toHaveLength(1)
expect(filtered[0].id).toBe(wd.id)
})
it('marks messages as read', () => {
const d = createDb()
const m1 = d.insertMessage({ runId, from: 'a', to: 'b', subject: 'one' })
const m2 = d.insertMessage({ runId, from: 'a', to: 'b', subject: 'two' })
d.markAsRead([m1.id])
const unread = d.getUnreadMessages('b')
expect(unread).toHaveLength(1)
expect(unread[0].id).toBe(m2.id)
})
it('stores typed payload and thread_id', () => {
const d = createDb()
const payload = JSON.stringify({ taskId: 'task_abc', filesModified: ['src/a.ts'] })
const msg = d.insertMessage({
runId,
from: 'a',
to: 'b',
subject: 'done',
type: 'worker_done',
priority: 'high',
threadId: 'thread_1',
payload
})
expect(msg.type).toBe('worker_done')
expect(msg.priority).toBe('high')
expect(msg.thread_id).toBe('thread_1')
expect(msg.payload).toBe(payload)
})
it('rejects invalid message type', () => {
const d = createDb()
expect(() =>
d.insertMessage({
runId,
from: 'a',
to: 'b',
subject: 'bad',
type: 'invalid' as MessageType
})
).toThrow()
})
it('getInbox returns all messages across recipients', () => {
const d = createDb()
d.insertMessage({ runId, from: 'a', to: 'b', subject: 'one' })
d.insertMessage({ runId, from: 'a', to: 'c', subject: 'two' })
d.insertMessage({ runId, from: 'b', to: 'a', subject: 'three' })
const inbox = d.getInbox(10)
expect(inbox).toHaveLength(3)
})
it('getMessageById returns the correct message', () => {
const d = createDb()
const msg = d.insertMessage({
runId,
from: 'a',
to: 'b',
subject: 'test'
})
const found = d.getMessageById(msg.id)
expect(found?.subject).toBe('test')
expect(d.getMessageById('msg_nonexistent')).toBeUndefined()
})
})
})
@@ -0,0 +1,122 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { OrchestrationDb } from './db'
import { createRootDispatch } from './db/root-dispatch-test-fixture'
const PANE_W = 'tab_w:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'
describe('a Task whose supervised worker is stopping', () => {
let db: OrchestrationDb
beforeEach(() => {
db = new OrchestrationDb(':memory:')
})
afterEach(() => db.close())
function localWorker() {
const task = db.createTask({ spec: 'local work' })
const { dispatch } = db.createStartingWorkerDispatch({
taskId: task.id,
startOptions: {},
creator: { kind: 'system' },
maxDepth: 9
})
db.prepareStartingWorkerAuthority({
dispatchId: dispatch.id,
handle: 'term_w',
paneKey: PANE_W,
processIncarnation: 'inc1',
worktreeId: 'wt',
effects: [],
setupState: 'not_configured'
})
db.markWorkerDispatchReady(dispatch.id)
return { task, dispatch }
}
describe('task-update', () => {
it('refuses to re-open the Task while the worker is stopping', () => {
const { task, dispatch } = localWorker()
db.beginWorkerStop(dispatch.id, 'epoch_home')
expect(db.getTask(task.id)?.status).toBe('blocked')
expect(() => db.updateTaskStatus(task.id, 'dispatched')).toThrowError(
expect.objectContaining({
code: 'task_not_startable',
data: { taskId: task.id, dispatchId: dispatch.id }
})
)
expect(db.getTask(task.id)?.status).toBe('blocked')
})
it('refuses to re-open the Task while the stop outcome is unknown', () => {
const { task, dispatch } = localWorker()
db.beginWorkerStop(dispatch.id, 'epoch_home')
db.markWorkerStopUnknown(dispatch.id, 'the execution host did not answer')
expect(() => db.updateTaskStatus(task.id, 'dispatched')).toThrowError(
expect.objectContaining({ code: 'task_not_startable' })
)
expect(db.getTask(task.id)?.status).toBe('blocked')
})
it('control: still accepts dispatched for an active Dispatch with no supervised worker', () => {
const task = db.createTask({ spec: 'unsupervised work' })
createRootDispatch(db, task.id, 'term_worker')
expect(db.updateTaskStatus(task.id, 'dispatched')?.status).toBe('dispatched')
})
it('control: still accepts dispatched while the supervised worker is ready', () => {
const { task } = localWorker()
expect(db.updateTaskStatus(task.id, 'dispatched')?.status).toBe('dispatched')
})
it('control: a no-op re-assert of dispatched under a stopping worker stays legal', () => {
const { task, dispatch } = localWorker()
expect(db.getTask(task.id)?.status).toBe('dispatched')
db.beginWorkerStop(dispatch.id, 'epoch_home')
// beginWorkerStop moved the Task to blocked; put it back the only way that is not a re-open.
db.db.prepare("UPDATE tasks SET status = 'dispatched' WHERE id = ?").run(task.id)
expect(db.updateTaskStatus(task.id, 'dispatched')?.status).toBe('dispatched')
})
})
describe('operator escape', () => {
it('accepts a re-issued worker-stop and reaches an honest stop_unknown outcome', () => {
const { task, dispatch } = localWorker()
db.beginWorkerStop(dispatch.id, 'epoch_dead_runtime')
// The runtime that owned the first stop died mid-flight; the re-issue is the way out.
const reissued = db.beginWorkerStop(dispatch.id, 'epoch_new_runtime')
expect(reissued).toMatchObject({ disposition: 'stopping' })
expect(db.getWorkerDispatch(dispatch.id)?.runtime_epoch).toBe('epoch_new_runtime')
db.markWorkerStopUnknown(dispatch.id, 'the execution host did not answer')
expect(db.abandonWorkerDispatch(dispatch.id)).toMatchObject({ disposition: 'abandoned' })
expect(db.getTask(task.id)?.status).toBe('blocked')
})
it('refuses a re-issue from the runtime whose own stop is still in flight', () => {
const { dispatch } = localWorker()
db.beginWorkerStop(dispatch.id, 'epoch_this_runtime')
// The terminal is closing and its exit event has not landed yet. Letting this second pass
// record stop_unknown would make the exit read as a crash instead of this stop succeeding.
expect(() => db.beginWorkerStop(dispatch.id, 'epoch_this_runtime')).toThrowError(
/cannot stop from stopping/
)
// The row is still the one the exit path claims a clean stop from: stopping, same epoch.
expect(db.getWorkerDispatch(dispatch.id)).toMatchObject({
state: 'stopping',
runtime_epoch: 'epoch_this_runtime'
})
expect(db.settleWorkerStop(dispatch.id).state).toBe('stopped')
expect(db.getDispatchContextById(dispatch.id)).toMatchObject({
status: 'failed',
last_failure: 'stopped'
})
})
})
})
@@ -33,12 +33,16 @@ describe('task creation dependency readiness', () => {
it('creates a late dependent as ready when every dependency is completed', () => {
const db = createDb()
const first = db.createTask({ spec: 'first' })
const second = db.createTask({ spec: 'second' })
const first = db.createTask({ runId: 'run_legacy_local', spec: 'first' })
const second = db.createTask({ runId: 'run_legacy_local', spec: 'second' })
db.updateTaskStatus(first.id, 'completed')
db.updateTaskStatus(second.id, 'completed')
const child = db.createTask({ spec: 'child', deps: [first.id, second.id] })
const child = db.createTask({
runId: 'run_legacy_local',
spec: 'child',
deps: [first.id, second.id]
})
expect(child.status).toBe('ready')
})
@@ -49,7 +53,7 @@ describe('task creation dependency readiness', () => {
const path = join(directory, 'orchestration.db')
const db = createDb(path)
const concurrent = createDb(path)
const dependency = db.createTask({ spec: 'dependency' })
const dependency = db.createTask({ runId: 'run_legacy_local', spec: 'dependency' })
const sqlite = (db as unknown as OrchestrationDbAccess).db
const prepare = sqlite.prepare.bind(sqlite)
let injected = false
@@ -61,7 +65,7 @@ describe('task creation dependency readiness', () => {
return prepare(sql)
})
const child = db.createTask({ spec: 'child', deps: [dependency.id] })
const child = db.createTask({ runId: 'run_legacy_local', spec: 'child', deps: [dependency.id] })
expect(injected).toBe(true)
expect(child.status).toBe('ready')
@@ -69,10 +73,14 @@ describe('task creation dependency readiness', () => {
it('promotes only after every dependency completes', () => {
const db = createDb()
const first = db.createTask({ spec: 'first' })
const second = db.createTask({ spec: 'second' })
const first = db.createTask({ runId: 'run_legacy_local', spec: 'first' })
const second = db.createTask({ runId: 'run_legacy_local', spec: 'second' })
db.updateTaskStatus(first.id, 'completed')
const child = db.createTask({ spec: 'child', deps: [first.id, second.id] })
const child = db.createTask({
runId: 'run_legacy_local',
spec: 'child',
deps: [first.id, second.id]
})
expect(child.status).toBe('pending')
db.updateTaskStatus(second.id, 'completed')
@@ -83,11 +91,15 @@ describe('task creation dependency readiness', () => {
'does not unlock a dependent whose dependency is %s',
(status) => {
const db = createDb()
const terminal = db.createTask({ spec: 'terminal dependency' })
const completing = db.createTask({ spec: 'completing dependency' })
const terminal = db.createTask({ runId: 'run_legacy_local', spec: 'terminal dependency' })
const completing = db.createTask({ runId: 'run_legacy_local', spec: 'completing dependency' })
db.updateTaskStatus(terminal.id, status)
const child = db.createTask({ spec: 'child', deps: [terminal.id, completing.id] })
const child = db.createTask({
runId: 'run_legacy_local',
spec: 'child',
deps: [terminal.id, completing.id]
})
expect(child.status).toBe('pending')
db.updateTaskStatus(completing.id, 'completed')
@@ -98,9 +110,9 @@ describe('task creation dependency readiness', () => {
it('rejects missing dependencies without inserting a task', () => {
const db = createDb()
expect(() => db.createTask({ spec: 'child', deps: ['task_missing'] })).toThrow(
'Dependency task task_missing must belong to run'
)
expect(() =>
db.createTask({ runId: 'run_legacy_local', spec: 'child', deps: ['task_missing'] })
).toThrow('Dependency task task_missing must belong to run')
expect(db.listTasks()).toEqual([])
})
@@ -109,7 +121,7 @@ describe('task creation dependency readiness', () => {
const sqlite = (db as unknown as OrchestrationDbAccess).db
sqlite.exec('BEGIN IMMEDIATE')
const task = db.createTask({ spec: 'transactional child' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'transactional child' })
sqlite.exec('ROLLBACK')
expect(db.getTask(task.id)).toBeUndefined()
@@ -120,11 +132,19 @@ describe('task creation dependency readiness', () => {
directories.push(directory)
const path = join(directory, 'orchestration.db')
const before = createDb(path)
const completed = before.createTask({ spec: 'completed' })
const open = before.createTask({ spec: 'open' })
const completed = before.createTask({ runId: 'run_legacy_local', spec: 'completed' })
const open = before.createTask({ runId: 'run_legacy_local', spec: 'open' })
before.updateTaskStatus(completed.id, 'completed')
const ready = before.createTask({ spec: 'ready', deps: [completed.id] })
const pending = before.createTask({ spec: 'pending', deps: [completed.id, open.id] })
const ready = before.createTask({
runId: 'run_legacy_local',
spec: 'ready',
deps: [completed.id]
})
const pending = before.createTask({
runId: 'run_legacy_local',
spec: 'pending',
deps: [completed.id, open.id]
})
before.close()
databases.splice(databases.indexOf(before), 1)
@@ -30,9 +30,17 @@ describe('Task/Dispatch invariant transactions', () => {
'allows a dependency-blocked pending Task to become %s',
(status) => {
const { db } = createDatabase()
const dependency = db.createTask({ spec: 'unresolved dependency' })
const task = db.createTask({ spec: 'manual resolution', deps: [dependency.id] })
const dependent = db.createTask({ spec: 'downstream work', deps: [task.id] })
const dependency = db.createTask({ runId: 'run_legacy_local', spec: 'unresolved dependency' })
const task = db.createTask({
runId: 'run_legacy_local',
spec: 'manual resolution',
deps: [dependency.id]
})
const dependent = db.createTask({
runId: 'run_legacy_local',
spec: 'downstream work',
deps: [task.id]
})
expect(task.status).toBe('pending')
const updated = db.updateTaskStatus(task.id, status, 'manual resolution')
@@ -45,7 +53,7 @@ describe('Task/Dispatch invariant transactions', () => {
it('surfaces invalid Task lifecycle edges instead of returning the unchanged row', () => {
const { db } = createDatabase()
const task = db.createTask({ spec: 'invalid lifecycle edge' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'invalid lifecycle edge' })
db.updateTaskStatus(task.id, 'blocked')
expect(() =>
@@ -67,8 +75,12 @@ describe('Task/Dispatch invariant transactions', () => {
'rolls back a %s Task when Dispatch settlement fails',
(status) => {
const { db } = createDatabase()
const task = db.createTask({ spec: 'atomic work' })
const dependent = db.createTask({ spec: 'dependent work', deps: [task.id] })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'atomic work' })
const dependent = db.createTask({
runId: 'run_legacy_local',
spec: 'dependent work',
deps: [task.id]
})
const dispatch = createRootDispatch(db, task.id, 'term_worker')
const capability = db.mintDispatchCapability({
dispatchId: dispatch.id,
@@ -111,7 +123,10 @@ describe('Task/Dispatch invariant transactions', () => {
it('does not commit a caller-owned transaction', () => {
const { db } = createDatabase()
const task = db.createTask({ spec: 'outer transaction work' })
const task = db.createTask({
runId: 'run_legacy_local',
spec: 'outer transaction work'
})
const dispatch = createRootDispatch(db, task.id, 'term_worker')
const sqlite = sqliteFor(db)
@@ -135,7 +150,10 @@ describe('Task/Dispatch invariant transactions', () => {
it('keeps Dispatch creation inside a caller-owned transaction', () => {
const { db } = createDatabase()
const task = db.createTask({ spec: 'outer transaction dispatch' })
const task = db.createTask({
runId: 'run_legacy_local',
spec: 'outer transaction dispatch'
})
const sqlite = sqliteFor(db)
sqlite.exec('BEGIN IMMEDIATE')
@@ -152,7 +170,10 @@ describe('Task/Dispatch invariant transactions', () => {
'settles every active Dispatch left by a pre-fix split when the Task becomes %s',
(status) => {
const { db } = createDatabase()
const task = db.createTask({ spec: 'legacy split work' })
const task = db.createTask({
runId: 'run_legacy_local',
spec: 'legacy split work'
})
const first = createRootDispatch(db, task.id, 'term_first')
sqliteFor(db).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id)
const second = createRootDispatch(db, task.id, 'term_second')
@@ -169,17 +190,31 @@ describe('Task/Dispatch invariant transactions', () => {
expect(db.getActiveDispatchForTerminal('term_first')).toBeUndefined()
expect(db.getActiveDispatchForTerminal('term_second')).toBeUndefined()
expect(() =>
createRootDispatch(db, db.createTask({ spec: 'first later work' }).id, 'term_first')
createRootDispatch(
db,
db.createTask({ runId: 'run_legacy_local', spec: 'first later work' }).id,
'term_first'
)
).not.toThrow()
expect(() =>
createRootDispatch(db, db.createTask({ spec: 'second later work' }).id, 'term_second')
createRootDispatch(
db,
db.createTask({
runId: 'run_legacy_local',
spec: 'second later work'
}).id,
'term_second'
)
).not.toThrow()
}
)
it('does not requeue a legacy split Task while another Dispatch remains active', () => {
const { db } = createDatabase()
const task = db.createTask({ spec: 'legacy split retry' })
const task = db.createTask({
runId: 'run_legacy_local',
spec: 'legacy split retry'
})
const first = createRootDispatch(db, task.id, 'term_first')
sqliteFor(db).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id)
const second = createRootDispatch(db, task.id, 'term_second')
@@ -193,7 +228,10 @@ describe('Task/Dispatch invariant transactions', () => {
it('does not block a legacy split Task while another Dispatch remains active', () => {
const { db } = createDatabase()
const task = db.createTask({ spec: 'legacy split release' })
const task = db.createTask({
runId: 'run_legacy_local',
spec: 'legacy split release'
})
const first = createRootDispatch(db, task.id, 'term_first')
sqliteFor(db).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id)
const second = createRootDispatch(db, task.id, 'term_second')
@@ -211,7 +249,10 @@ describe('Task/Dispatch invariant transactions', () => {
'rejects moving a Task to %s while a Dispatch remains active',
(status) => {
const { db } = createDatabase()
const task = db.createTask({ spec: 'guarded work' })
const task = db.createTask({
runId: 'run_legacy_local',
spec: 'guarded work'
})
const dispatch = createRootDispatch(db, task.id, 'term_worker')
expect(() => db.updateTaskStatus(task.id, status, 'must not persist')).toThrowError(
@@ -227,7 +268,10 @@ describe('Task/Dispatch invariant transactions', () => {
it('rejects moving a Task to dispatched without an active Dispatch', () => {
const { db } = createDatabase()
const task = db.createTask({ spec: 'unassigned work' })
const task = db.createTask({
runId: 'run_legacy_local',
spec: 'unassigned work'
})
expect(() => db.updateTaskStatus(task.id, 'dispatched')).toThrowError(
expect.objectContaining({
@@ -241,7 +285,10 @@ describe('Task/Dispatch invariant transactions', () => {
it('rejects a Dispatch when failure wins after readiness was observed', () => {
const first = createDatabase()
const concurrent = createDatabase(first.path)
const task = first.db.createTask({ spec: 'interleaved work' })
const task = first.db.createTask({
runId: 'run_legacy_local',
spec: 'interleaved work'
})
const sqlite = sqliteFor(first.db)
const prepare = sqlite.prepare.bind(sqlite)
let injected = false
@@ -264,8 +311,14 @@ describe('Task/Dispatch invariant transactions', () => {
it('atomically rejects a same-pane Dispatch that loses the occupancy race', () => {
const first = createDatabase()
const concurrent = createDatabase(first.path)
const firstTask = first.db.createTask({ spec: 'first terminal claimant' })
const secondTask = first.db.createTask({ spec: 'second terminal claimant' })
const firstTask = first.db.createTask({
runId: 'run_legacy_local',
spec: 'first terminal claimant'
})
const secondTask = first.db.createTask({
runId: 'run_legacy_local',
spec: 'second terminal claimant'
})
const sqlite = sqliteFor(first.db)
const prepare = sqlite.prepare.bind(sqlite)
let winnerId: string | undefined
@@ -303,14 +356,20 @@ describe('Task/Dispatch invariant transactions', () => {
it('rejects worker authority when another Dispatch owns the pane', () => {
const { db } = createDatabase()
const ownerTask = db.createTask({ spec: 'current pane owner' })
const ownerTask = db.createTask({
runId: 'run_legacy_local',
spec: 'current pane owner'
})
const owner = createRootDispatch(
db,
ownerTask.id,
'term_owner',
'tab_old:cccccccc-cccc-4ccc-8ccc-cccccccccccc'
)
const workerTask = db.createTask({ spec: 'competing supervised worker' })
const workerTask = db.createTask({
runId: 'run_legacy_local',
spec: 'competing supervised worker'
})
const started = db.createStartingWorkerDispatch({
creator: { kind: 'system' },
maxDepth: Number.MAX_SAFE_INTEGER,
@@ -346,7 +405,10 @@ describe('Task/Dispatch invariant transactions', () => {
'rejects a %s Task update while its supervised worker remains active',
(status) => {
const { db } = createDatabase()
const task = db.createTask({ spec: 'supervised lifecycle' })
const task = db.createTask({
runId: 'run_legacy_local',
spec: 'supervised lifecycle'
})
const started = db.createStartingWorkerDispatch({
creator: { kind: 'system' },
maxDepth: Number.MAX_SAFE_INTEGER,
@@ -394,7 +456,10 @@ describe('Task/Dispatch invariant transactions', () => {
it('keeps a federated late start authoritative after rejecting Task failure', () => {
const { db } = createDatabase()
const task = db.createTask({ spec: 'federated lifecycle' })
const task = db.createTask({
runId: 'run_legacy_local',
spec: 'federated lifecycle'
})
const started = db.createStartingWorkerDispatch({
creator: { kind: 'system' },
maxDepth: Number.MAX_SAFE_INTEGER,
@@ -29,7 +29,7 @@ afterEach(() => {
describe('Task/Dispatch lifecycle guards', () => {
it('rejects a worker report while another supervised Dispatch is active', () => {
const database = createDatabase()
const task = database.createTask({ spec: 'legacy supervised split' })
const task = database.createTask({ runId: 'run_legacy_local', spec: 'legacy supervised split' })
const first = startWorker(database, task.id, 'first')
sqliteFor(database).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id)
const second = startWorker(database, task.id, 'second')
@@ -55,7 +55,7 @@ describe('Task/Dispatch lifecycle guards', () => {
'settles context-only legacy siblings after a %s worker report',
(outcome) => {
const database = createDatabase()
const task = database.createTask({ spec: 'legacy mixed split' })
const task = database.createTask({ runId: 'run_legacy_local', spec: 'legacy mixed split' })
const contextOnly = createRootDispatch(database, task.id, 'term_context')
sqliteFor(database).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id)
const worker = startWorker(database, task.id, 'reporter')
@@ -78,7 +78,10 @@ describe('Task/Dispatch lifecycle guards', () => {
expect(() =>
createRootDispatch(
database,
database.createTask({ spec: 'later context work' }).id,
database.createTask({
runId: 'run_legacy_local',
spec: 'later context work'
}).id,
'term_context'
)
).not.toThrow()
@@ -87,7 +90,10 @@ describe('Task/Dispatch lifecycle guards', () => {
it('settles a newer context-only legacy sibling after a worker report', () => {
const database = createDatabase()
const task = database.createTask({ spec: 'reversed legacy mixed split' })
const task = database.createTask({
runId: 'run_legacy_local',
spec: 'reversed legacy mixed split'
})
const worker = startWorker(database, task.id, 'reversed_reporter')
sqliteFor(database).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id)
const contextOnly = createRootDispatch(database, task.id, 'term_reversed_context')
@@ -112,7 +118,10 @@ describe('Task/Dispatch lifecycle guards', () => {
'treats abandon of an already %s worker as stale without a lifecycle conflict',
(state) => {
const database = createDatabase()
const task = database.createTask({ spec: `already ${state}` })
const task = database.createTask({
runId: 'run_legacy_local',
spec: `already ${state}`
})
const worker = startWorker(database, task.id, `already_${state}`)
if (state === 'failed') {
database.failDispatch(worker.dispatchId, 'process exited', { workerProcessExited: true })
@@ -130,7 +139,10 @@ describe('Task/Dispatch lifecycle guards', () => {
it('rejects generic failure while a supervised worker remains active', () => {
const database = createDatabase()
const task = database.createTask({ spec: 'supervised failure guard' })
const task = database.createTask({
runId: 'run_legacy_local',
spec: 'supervised failure guard'
})
const worker = startWorker(database, task.id, 'guarded')
expect(() => database.failDispatch(worker.dispatchId, 'unsafe retry')).toThrowError(
@@ -151,7 +163,10 @@ describe('Task/Dispatch lifecycle guards', () => {
it('atomically settles worker state when a proven process exit fails its Dispatch', () => {
const database = createDatabase()
const task = database.createTask({ spec: 'exited worker' })
const task = database.createTask({
runId: 'run_legacy_local',
spec: 'exited worker'
})
const worker = startWorker(database, task.id, 'exited')
expect(
@@ -168,7 +183,10 @@ describe('Task/Dispatch lifecycle guards', () => {
it('settles a stop-unknown worker when a positive PTY exit arrives', () => {
const database = createDatabase()
const task = database.createTask({ spec: 'stop-unknown exited worker' })
const task = database.createTask({
runId: 'run_legacy_local',
spec: 'stop-unknown exited worker'
})
const worker = startWorker(database, task.id, 'stop_unknown_exited')
expect(database.beginWorkerStop(worker.dispatchId, 'runtime_test').disposition).toBe('stopping')
@@ -198,7 +216,10 @@ describe('Task/Dispatch lifecycle guards', () => {
it('keeps a Task dispatched when missing-terminal recovery leaves another worker active', () => {
const database = createDatabase()
const task = database.createTask({ spec: 'legacy missing-terminal split' })
const task = database.createTask({
runId: 'run_legacy_local',
spec: 'legacy missing-terminal split'
})
const missing = startWorker(database, task.id, 'missing')
sqliteFor(database).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id)
const live = startWorker(database, task.id, 'live')
@@ -225,7 +246,10 @@ describe('Task/Dispatch lifecycle guards', () => {
'keeps a Task dispatched when a %s worker start fails beside a live worker',
(kind) => {
const database = createDatabase()
const task = database.createTask({ spec: `${kind} split start failure` })
const task = database.createTask({
runId: 'run_legacy_local',
spec: `${kind} split start failure`
})
const failed = database.createStartingWorkerDispatch({
creator: { kind: 'system' },
maxDepth: Number.MAX_SAFE_INTEGER,
@@ -342,7 +366,10 @@ describe('Task/Dispatch lifecycle guards', () => {
it('rolls back federated start uncertainty when the Task transition cannot commit', () => {
const database = createDatabase()
const task = database.createTask({ spec: 'atomic federated uncertainty' })
const task = database.createTask({
runId: 'run_legacy_local',
spec: 'atomic federated uncertainty'
})
const started = database.createStartingWorkerDispatch({
creator: { kind: 'system' },
maxDepth: Number.MAX_SAFE_INTEGER,
@@ -383,7 +410,10 @@ describe('Task/Dispatch lifecycle guards', () => {
'%s releases the last context-only sibling after a newer worker start fails',
(operation) => {
const database = createDatabase()
const task = database.createTask({ spec: `${operation} historical sibling` })
const task = database.createTask({
runId: 'run_legacy_local',
spec: `${operation} historical sibling`
})
const contextOnly = createRootDispatch(database, task.id, `term_${operation}`)
sqliteFor(database).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id)
const failed = database.createStartingWorkerDispatch({
@@ -411,7 +441,10 @@ describe('Task/Dispatch lifecycle guards', () => {
expect(() =>
createRootDispatch(
database,
database.createTask({ spec: `${operation} later work` }).id,
database.createTask({
runId: 'run_legacy_local',
spec: `${operation} later work`
}).id,
`term_${operation}`
)
).not.toThrow()
@@ -422,7 +455,10 @@ describe('Task/Dispatch lifecycle guards', () => {
'%s records guarded receipts for context-only Dispatch and Task release',
(operation) => {
const database = createDatabase()
const task = database.createTask({ spec: `${operation} receipt release` })
const task = database.createTask({
runId: 'run_legacy_local',
spec: `${operation} receipt release`
})
const contextOnly = createRootDispatch(database, task.id, `term_${operation}`)
const released =
@@ -445,7 +481,10 @@ describe('Task/Dispatch lifecycle guards', () => {
it('rolls back both context-only projections when the Task transition fails', () => {
const database = createDatabase()
const task = database.createTask({ spec: 'context-only atomic receipt' })
const task = database.createTask({
runId: 'run_legacy_local',
spec: 'context-only atomic receipt'
})
const contextOnly = createRootDispatch(database, task.id, 'term_context')
sqliteFor(database).exec(`
CREATE TRIGGER reject_context_release_task_block
@@ -470,7 +509,10 @@ describe('Task/Dispatch lifecycle guards', () => {
'%s preserves a live worker sibling and lets it report',
(operation) => {
const database = createDatabase()
const task = database.createTask({ spec: `${operation} legacy worker split` })
const task = database.createTask({
runId: 'run_legacy_local',
spec: `${operation} legacy worker split`
})
const live = startWorker(database, task.id, `${operation}_live`)
sqliteFor(database).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id)
const released = startWorker(database, task.id, `${operation}_released`)
@@ -502,7 +544,10 @@ describe('Task/Dispatch lifecycle guards', () => {
it('blocks a Task when an interleaved stop settles its final active Dispatch', () => {
const database = createDatabase()
const task = database.createTask({ spec: 'interleaved legacy worker release' })
const task = database.createTask({
runId: 'run_legacy_local',
spec: 'interleaved legacy worker release'
})
const stopping = startWorker(database, task.id, 'interleaved_stopping')
sqliteFor(database).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id)
const abandoned = startWorker(database, task.id, 'interleaved_abandoned')
@@ -521,7 +566,10 @@ describe('Task/Dispatch lifecycle guards', () => {
it('restores a live sibling after stopping an uncertain worker start', () => {
const database = createDatabase()
const task = database.createTask({ spec: 'uncertain legacy worker split' })
const task = database.createTask({
runId: 'run_legacy_local',
spec: 'uncertain legacy worker split'
})
const live = startWorker(database, task.id, 'uncertain_live')
sqliteFor(database).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id)
const uncertain = database.createStartingWorkerDispatch({
@@ -552,7 +600,10 @@ describe('Task/Dispatch lifecycle guards', () => {
'restores a live sibling after an uncertain worker start fails through %s',
(recovery) => {
const database = createDatabase()
const task = database.createTask({ spec: `${recovery} uncertain sibling` })
const task = database.createTask({
runId: 'run_legacy_local',
spec: `${recovery} uncertain sibling`
})
const live = startWorker(database, task.id, `${recovery}_live`)
sqliteFor(database).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id)
const uncertain = database.createStartingWorkerDispatch({
@@ -589,7 +640,10 @@ describe('Task/Dispatch lifecycle guards', () => {
it('rejects gate creation while a supervised worker remains active', () => {
const database = createDatabase()
const task = database.createTask({ spec: 'worker gate guard' })
const task = database.createTask({
runId: 'run_legacy_local',
spec: 'worker gate guard'
})
const worker = startWorker(database, task.id, 'gate')
expect(() => database.createGate({ taskId: task.id, question: 'Proceed?' })).toThrowError(
@@ -607,7 +661,10 @@ describe('Task/Dispatch lifecycle guards', () => {
it('rolls back gate resolution when an active Dispatch blocks readiness', () => {
const database = createDatabase()
const task = database.createTask({ spec: 'corrupt gated task' })
const task = database.createTask({
runId: 'run_legacy_local',
spec: 'corrupt gated task'
})
const gate = database.createGate({ taskId: task.id, question: 'Proceed?' })
sqliteFor(database).prepare("UPDATE tasks SET status = 'ready' WHERE id = ?").run(task.id)
const dispatch = createRootDispatch(database, task.id, 'term_worker')
@@ -29,7 +29,10 @@ describe('Task/Dispatch concurrency', () => {
it('reads a concurrent Task result before applying an explicit status correction', () => {
const first = createDatabase()
const concurrent = createDatabase(first.path)
const task = first.db.createTask({ spec: 'concurrent status winner' })
const task = first.db.createTask({
runId: 'run_legacy_local',
spec: 'concurrent status winner'
})
const sqlite = sqliteFor(first.db)
const exec = sqlite.exec.bind(sqlite)
let concurrentWon = false
@@ -57,7 +60,7 @@ describe('Task/Dispatch concurrency', () => {
it('holds the Task status writer reservation through its lifecycle reads', () => {
const first = createDatabase()
const concurrent = createDatabase(first.path)
const task = first.db.createTask({ spec: 'reserved status winner' })
const task = first.db.createTask({ runId: 'run_legacy_local', spec: 'reserved status winner' })
const sqlite = sqliteFor(first.db)
const exec = sqlite.exec.bind(sqlite)
sqliteFor(concurrent.db).pragma('busy_timeout = 0')
@@ -86,7 +89,7 @@ describe('Task/Dispatch concurrency', () => {
it('rolls back Dispatch failure when Task requeue fails', () => {
const { db } = createDatabase()
const task = db.createTask({ spec: 'atomic retry failure' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'atomic retry failure' })
const dispatch = createRootDispatch(db, task.id, 'term_worker')
sqliteFor(db).exec(`
CREATE TRIGGER reject_task_requeue
@@ -113,7 +116,10 @@ describe('Task/Dispatch concurrency', () => {
it('does not let stale failure overwrite a completed worker report', () => {
const first = createDatabase()
const concurrent = createDatabase(first.path)
const task = first.db.createTask({ spec: 'worker completion wins' })
const task = first.db.createTask({
runId: 'run_legacy_local',
spec: 'worker completion wins'
})
const started = first.db.createStartingWorkerDispatch({
creator: { kind: 'system' },
maxDepth: Number.MAX_SAFE_INTEGER,
@@ -177,7 +183,10 @@ describe('Task/Dispatch concurrency', () => {
it('keeps nested dispatch failure atomic with its caller transaction', () => {
const { db } = createDatabase()
const task = db.createTask({ spec: 'nested atomic failure' })
const task = db.createTask({
runId: 'run_legacy_local',
spec: 'nested atomic failure'
})
const dispatch = createRootDispatch(db, task.id, 'term_worker')
const sqlite = sqliteFor(db)
@@ -198,8 +207,14 @@ describe('Task/Dispatch concurrency', () => {
it('serializes reminted-pane worker authority claims', () => {
const first = createDatabase()
const concurrent = createDatabase(first.path)
const losingTask = first.db.createTask({ spec: 'losing worker' })
const winningTask = first.db.createTask({ spec: 'winning worker' })
const losingTask = first.db.createTask({
runId: 'run_legacy_local',
spec: 'losing worker'
})
const winningTask = first.db.createTask({
runId: 'run_legacy_local',
spec: 'winning worker'
})
const loser = first.db.createStartingWorkerDispatch({
creator: { kind: 'system' },
maxDepth: Number.MAX_SAFE_INTEGER,
@@ -8,10 +8,20 @@ describe('undelivered orchestration mailboxes', () => {
it('lists only mailboxes with undelivered unread messages', () => {
db = new OrchestrationDb(':memory:')
const delivered = db.insertMessage({ from: 'a', to: 'delivered', subject: 'done' })
const read = db.insertMessage({ from: 'a', to: 'read', subject: 'seen' })
db.insertMessage({ from: 'a', to: 'pending', subject: 'first' })
db.insertMessage({ from: 'a', to: 'pending', subject: 'second' })
const delivered = db.insertMessage({
runId: 'run_legacy_local',
from: 'a',
to: 'delivered',
subject: 'done'
})
const read = db.insertMessage({
runId: 'run_legacy_local',
from: 'a',
to: 'read',
subject: 'seen'
})
db.insertMessage({ runId: 'run_legacy_local', from: 'a', to: 'pending', subject: 'first' })
db.insertMessage({ runId: 'run_legacy_local', from: 'a', to: 'pending', subject: 'second' })
db.markAsDelivered([delivered.id])
db.markAsRead([read.id])
@@ -20,7 +30,12 @@ describe('undelivered orchestration mailboxes', () => {
it('persists and settles a pending pointer Enter independently of delivery', () => {
db = new OrchestrationDb(':memory:')
const message = db.insertMessage({ from: 'a', to: 'run:run_1', subject: 'staged' })
const message = db.insertMessage({
runId: 'run_legacy_local',
from: 'a',
to: 'run:run_1',
subject: 'staged'
})
expect(
db.stageMailboxPointerEnter([message.id], {
+110 -210
View File
@@ -4,9 +4,10 @@ import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import Database from '../../sqlite/sync-database'
import { LEGACY_RUN_ID, OrchestrationDb } from './db'
import type { MessageType } from './db'
import { createRootDispatch } from './db/root-dispatch-test-fixture'
const runId = 'run_legacy_local'
// Overwrites the datetime('now')-seeded timestamps with explicit fixture values
// so stale-detection assertions stay deterministic (no wall clock).
function setDispatchTimes(
@@ -33,154 +34,10 @@ describe('OrchestrationDb', () => {
return db
}
describe('messages', () => {
it('inserts and retrieves a message', () => {
const d = createDb()
const msg = d.insertMessage({
from: 'term_a',
to: 'term_b',
subject: 'hello',
body: 'world'
})
expect(msg.id).toMatch(/^msg_/)
expect(msg.from_handle).toBe('term_a')
expect(msg.to_handle).toBe('term_b')
expect(msg.subject).toBe('hello')
expect(msg.body).toBe('world')
expect(msg.type).toBe('status')
expect(msg.priority).toBe('normal')
expect(msg.read).toBe(0)
expect(msg.sequence).toBeGreaterThan(0)
})
it('returns unread messages in sequence order', () => {
const d = createDb()
d.insertMessage({ from: 'a', to: 'b', subject: 'first' })
d.insertMessage({ from: 'a', to: 'b', subject: 'second' })
d.insertMessage({ from: 'a', to: 'c', subject: 'other' })
const unread = d.getUnreadMessages('b')
expect(unread).toHaveLength(2)
expect(unread[0].subject).toBe('first')
expect(unread[1].subject).toBe('second')
})
it('filters unread by type', () => {
const d = createDb()
d.insertMessage({ from: 'a', to: 'b', subject: 'status msg', type: 'status' })
d.insertMessage({ from: 'a', to: 'b', subject: 'done msg', type: 'worker_done' })
const filtered = d.getUnreadMessages('b', ['worker_done'])
expect(filtered).toHaveLength(1)
expect(filtered[0].type).toBe('worker_done')
})
it('excludes already-delivered rows from getUndeliveredUnreadMessages', () => {
const d = createDb()
const m1 = d.insertMessage({ from: 'a', to: 'b', subject: 'one' })
const m2 = d.insertMessage({ from: 'a', to: 'b', subject: 'two' })
d.markAsDelivered([m1.id])
// Push delivery query: only undelivered, unread.
const pending = d.getUndeliveredUnreadMessages('b')
expect(pending).toHaveLength(1)
expect(pending[0].id).toBe(m2.id)
// Explicit `check` still sees both (they are still unread).
const unread = d.getUnreadMessages('b')
expect(unread).toHaveLength(2)
})
it('creates the undelivered inbox index used by push delivery', () => {
const d = createDb()
const sqlite = (d as unknown as { db: Database.Database }).db
const indexes = sqlite
.prepare(
`SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'messages' AND name = 'idx_messages_undelivered_inbox'`
)
.all()
expect(indexes).toHaveLength(1)
})
it('filters getUndeliveredUnreadMessages by type', () => {
const d = createDb()
d.insertMessage({ from: 'a', to: 'b', subject: 's', type: 'status' })
const wd = d.insertMessage({ from: 'a', to: 'b', subject: 'd', type: 'worker_done' })
const filtered = d.getUndeliveredUnreadMessages('b', ['worker_done'])
expect(filtered).toHaveLength(1)
expect(filtered[0].id).toBe(wd.id)
})
it('marks messages as read', () => {
const d = createDb()
const m1 = d.insertMessage({ from: 'a', to: 'b', subject: 'one' })
const m2 = d.insertMessage({ from: 'a', to: 'b', subject: 'two' })
d.markAsRead([m1.id])
const unread = d.getUnreadMessages('b')
expect(unread).toHaveLength(1)
expect(unread[0].id).toBe(m2.id)
})
it('stores typed payload and thread_id', () => {
const d = createDb()
const payload = JSON.stringify({ taskId: 'task_abc', filesModified: ['src/a.ts'] })
const msg = d.insertMessage({
from: 'a',
to: 'b',
subject: 'done',
type: 'worker_done',
priority: 'high',
threadId: 'thread_1',
payload
})
expect(msg.type).toBe('worker_done')
expect(msg.priority).toBe('high')
expect(msg.thread_id).toBe('thread_1')
expect(msg.payload).toBe(payload)
})
it('rejects invalid message type', () => {
const d = createDb()
expect(() =>
d.insertMessage({
from: 'a',
to: 'b',
subject: 'bad',
type: 'invalid' as MessageType
})
).toThrow()
})
it('getInbox returns all messages across recipients', () => {
const d = createDb()
d.insertMessage({ from: 'a', to: 'b', subject: 'one' })
d.insertMessage({ from: 'a', to: 'c', subject: 'two' })
d.insertMessage({ from: 'b', to: 'a', subject: 'three' })
const inbox = d.getInbox(10)
expect(inbox).toHaveLength(3)
})
it('getMessageById returns the correct message', () => {
const d = createDb()
const msg = d.insertMessage({ from: 'a', to: 'b', subject: 'test' })
const found = d.getMessageById(msg.id)
expect(found?.subject).toBe('test')
expect(d.getMessageById('msg_nonexistent')).toBeUndefined()
})
})
describe('tasks', () => {
it('creates a task with no deps as ready', () => {
const d = createDb()
const task = d.createTask({ spec: 'do something' })
const task = d.createTask({ runId, spec: 'do something' })
expect(task.id).toMatch(/^task_/)
expect(task.status).toBe('ready')
expect(task.deps).toBe('[]')
@@ -191,6 +48,7 @@ describe('OrchestrationDb', () => {
it('persists explicit task display metadata', () => {
const d = createDb()
const task = d.createTask({
runId,
spec: 'full details',
taskTitle: 'Checkout race',
displayName: 'Fix checkout race'
@@ -204,6 +62,7 @@ describe('OrchestrationDb', () => {
it('persists the creating terminal handle for task-created worktrees', () => {
const d = createDb()
const task = d.createTask({
runId,
spec: 'spawn related workspace',
createdByTerminalHandle: 'term_creator'
})
@@ -214,16 +73,16 @@ describe('OrchestrationDb', () => {
it('creates a task with deps as pending', () => {
const d = createDb()
const parent = d.createTask({ spec: 'parent' })
const child = d.createTask({ spec: 'child', deps: [parent.id] })
const parent = d.createTask({ runId, spec: 'parent' })
const child = d.createTask({ runId, spec: 'child', deps: [parent.id] })
expect(child.status).toBe('pending')
expect(JSON.parse(child.deps)).toEqual([parent.id])
})
it('promotes pending tasks when deps complete', () => {
const d = createDb()
const t1 = d.createTask({ spec: 'first' })
const t2 = d.createTask({ spec: 'second', deps: [t1.id] })
const t1 = d.createTask({ runId, spec: 'first' })
const t2 = d.createTask({ runId, spec: 'second', deps: [t1.id] })
expect(d.getTask(t2.id)?.status).toBe('pending')
@@ -234,9 +93,9 @@ describe('OrchestrationDb', () => {
it('does not promote task until ALL deps complete', () => {
const d = createDb()
const t1 = d.createTask({ spec: 'a' })
const t2 = d.createTask({ spec: 'b' })
const t3 = d.createTask({ spec: 'c', deps: [t1.id, t2.id] })
const t1 = d.createTask({ runId, spec: 'a' })
const t2 = d.createTask({ runId, spec: 'b' })
const t3 = d.createTask({ runId, spec: 'c', deps: [t1.id, t2.id] })
d.updateTaskStatus(t1.id, 'completed')
expect(d.getTask(t3.id)?.status).toBe('pending')
@@ -247,7 +106,7 @@ describe('OrchestrationDb', () => {
it('sets completed_at on completion', () => {
const d = createDb()
const task = d.createTask({ spec: 'do it' })
const task = d.createTask({ runId, spec: 'do it' })
const updated = d.updateTaskStatus(task.id, 'completed', '{"result": true}')
expect(updated?.completed_at).toBeTruthy()
expect(updated?.result).toBe('{"result": true}')
@@ -255,7 +114,7 @@ describe('OrchestrationDb', () => {
it('completing a task frees its active dispatch context', () => {
const d = createDb()
const task = d.createTask({ spec: 'do it' })
const task = d.createTask({ runId, spec: 'do it' })
createRootDispatch(d, task.id, 'term_a')
d.updateTaskStatus(task.id, 'completed')
@@ -266,8 +125,8 @@ describe('OrchestrationDb', () => {
it('listTasks filters by status', () => {
const d = createDb()
d.createTask({ spec: 'ready task' })
const t2 = d.createTask({ spec: 'another' })
d.createTask({ runId, spec: 'ready task' })
const t2 = d.createTask({ runId, spec: 'another' })
d.updateTaskStatus(t2.id, 'completed')
expect(d.listTasks({ status: 'ready' })).toHaveLength(1)
@@ -277,15 +136,15 @@ describe('OrchestrationDb', () => {
it('listTasks returns all when no filter', () => {
const d = createDb()
d.createTask({ spec: 'one' })
d.createTask({ spec: 'two' })
d.createTask({ runId, spec: 'one' })
d.createTask({ runId, spec: 'two' })
expect(d.listTasks()).toHaveLength(2)
})
it('listTasksWithDispatch joins active dispatch metadata', () => {
const d = createDb()
const ready = d.createTask({ spec: 'ready task' })
const dispatched = d.createTask({ spec: 'active task' })
const ready = d.createTask({ runId, spec: 'ready task' })
const dispatched = d.createTask({ runId, spec: 'active task' })
const ctx = createRootDispatch(d, dispatched.id, 'term_worker')
const rows = d.listTasksWithDispatch()
@@ -300,7 +159,7 @@ describe('OrchestrationDb', () => {
it('listTasksWithDispatch does not surface completed dispatches', () => {
const d = createDb()
const task = d.createTask({ spec: 'work' })
const task = d.createTask({ runId, spec: 'work' })
createRootDispatch(d, task.id, 'term_worker')
d.updateTaskStatus(task.id, 'completed')
@@ -314,8 +173,8 @@ describe('OrchestrationDb', () => {
it('supports parent_id for task decomposition', () => {
const d = createDb()
const parent = d.createTask({ spec: 'parent' })
const child = d.createTask({ spec: 'child', parentId: parent.id })
const parent = d.createTask({ runId, spec: 'parent' })
const child = d.createTask({ runId, spec: 'child', parentId: parent.id })
expect(child.parent_id).toBe(parent.id)
})
})
@@ -323,7 +182,7 @@ describe('OrchestrationDb', () => {
describe('dispatch contexts', () => {
it('creates a dispatch context and marks task as dispatched', () => {
const d = createDb()
const task = d.createTask({ spec: 'work' })
const task = d.createTask({ runId, spec: 'work' })
const ctx = createRootDispatch(d, task.id, 'term_worker')
expect(ctx.id).toMatch(/^ctx_/)
@@ -335,8 +194,8 @@ describe('OrchestrationDb', () => {
it('rejects dispatch for non-ready tasks', () => {
const d = createDb()
const parent = d.createTask({ spec: 'parent' })
const child = d.createTask({ spec: 'child', deps: [parent.id] })
const parent = d.createTask({ runId, spec: 'parent' })
const child = d.createTask({ runId, spec: 'child', deps: [parent.id] })
expect(() => createRootDispatch(d, child.id, 'term_worker')).toThrow(
/only ready tasks can be dispatched/
@@ -345,8 +204,8 @@ describe('OrchestrationDb', () => {
it('rejects dispatch to an occupied terminal', () => {
const d = createDb()
const t1 = d.createTask({ spec: 'first' })
const t2 = d.createTask({ spec: 'second' })
const t1 = d.createTask({ runId, spec: 'first' })
const t2 = d.createTask({ runId, spec: 'second' })
createRootDispatch(d, t1.id, 'term_worker')
expect(() => createRootDispatch(d, t2.id, 'term_worker')).toThrow(
@@ -361,8 +220,8 @@ describe('OrchestrationDb', () => {
it('rejects dispatch to a reminted handle on a pane with an active dispatch', () => {
const d = createDb()
const t1 = d.createTask({ spec: 'first' })
const t2 = d.createTask({ spec: 'second' })
const t1 = d.createTask({ runId, spec: 'first' })
const t2 = d.createTask({ runId, spec: 'second' })
createRootDispatch(d, t1.id, 'term_old', `tab_1:${LEAF_A}`)
expect(() => createRootDispatch(d, t2.id, 'term_new', `tab_1:${LEAF_A}`)).toThrow(
@@ -372,8 +231,8 @@ describe('OrchestrationDb', () => {
it('rejects dispatch when pane keys share a leaf after break-out', () => {
const d = createDb()
const t1 = d.createTask({ spec: 'first' })
const t2 = d.createTask({ spec: 'second' })
const t1 = d.createTask({ runId, spec: 'first' })
const t2 = d.createTask({ runId, spec: 'second' })
createRootDispatch(d, t1.id, 'term_old', `tab_1:${LEAF_A}`)
expect(() => createRootDispatch(d, t2.id, 'term_new', `tab_2:${LEAF_A}`)).toThrow(
@@ -383,8 +242,8 @@ describe('OrchestrationDb', () => {
it('allows concurrent dispatches to different panes', () => {
const d = createDb()
const t1 = d.createTask({ spec: 'first' })
const t2 = d.createTask({ spec: 'second' })
const t1 = d.createTask({ runId, spec: 'first' })
const t2 = d.createTask({ runId, spec: 'second' })
createRootDispatch(d, t1.id, 'term_a', `tab_1:${LEAF_A}`)
expect(() => createRootDispatch(d, t2.id, 'term_b', `tab_1:${LEAF_B}`)).not.toThrow()
@@ -392,8 +251,8 @@ describe('OrchestrationDb', () => {
it('falls back to handle lock when pane keys are missing', () => {
const d = createDb()
const t1 = d.createTask({ spec: 'first' })
const t2 = d.createTask({ spec: 'second' })
const t1 = d.createTask({ runId, spec: 'first' })
const t2 = d.createTask({ runId, spec: 'second' })
createRootDispatch(d, t1.id, 'term_worker')
// New dispatch has a pane key but the active row is legacy (no pane key):
@@ -403,8 +262,8 @@ describe('OrchestrationDb', () => {
it('allows dispatch to a terminal after previous dispatch completes', () => {
const d = createDb()
const t1 = d.createTask({ spec: 'first' })
const t2 = d.createTask({ spec: 'second' })
const t1 = d.createTask({ runId, spec: 'first' })
const t2 = d.createTask({ runId, spec: 'second' })
const ctx1 = createRootDispatch(d, t1.id, 'term_worker')
d.completeDispatch(ctx1.id)
@@ -414,7 +273,7 @@ describe('OrchestrationDb', () => {
it('getDispatchContext returns latest for a task', () => {
const d = createDb()
const task = d.createTask({ spec: 'work' })
const task = d.createTask({ runId, spec: 'work' })
const ctx = createRootDispatch(d, task.id, 'term_a')
const found = d.getDispatchContext(task.id)
expect(found?.id).toBe(ctx.id)
@@ -422,7 +281,7 @@ describe('OrchestrationDb', () => {
it('getDispatchContext uses insertion order when timestamps tie', () => {
const d = createDb()
const task = d.createTask({ spec: 'work' })
const task = d.createTask({ runId, spec: 'work' })
const ctx1 = createRootDispatch(d, task.id, 'term_a')
d.failDispatch(ctx1.id, 'retry')
const ctx2 = createRootDispatch(d, task.id, 'term_a')
@@ -432,7 +291,7 @@ describe('OrchestrationDb', () => {
it('getActiveDispatchForTerminal returns active dispatch', () => {
const d = createDb()
const task = d.createTask({ spec: 'work' })
const task = d.createTask({ runId, spec: 'work' })
createRootDispatch(d, task.id, 'term_a')
const active = d.getActiveDispatchForTerminal('term_a')
@@ -442,10 +301,16 @@ describe('OrchestrationDb', () => {
it('getLatestDispatchForTerminal returns the most recent completed dispatch', () => {
const d = createDb()
const firstTask = d.createTask({ spec: 'first' })
const firstTask = d.createTask({
runId,
spec: 'first'
})
const first = createRootDispatch(d, firstTask.id, 'term_a')
d.completeDispatch(first.id)
const secondTask = d.createTask({ spec: 'second' })
const secondTask = d.createTask({
runId,
spec: 'second'
})
const second = createRootDispatch(d, secondTask.id, 'term_a')
d.completeDispatch(second.id)
@@ -457,7 +322,7 @@ describe('OrchestrationDb', () => {
it('circuit breaker trips after 3 failures', () => {
const d = createDb()
const task = d.createTask({ spec: 'flaky' })
const task = d.createTask({ runId, spec: 'flaky' })
const ctx = createRootDispatch(d, task.id, 'term_a')
const after1 = d.failDispatch(ctx.id, 'timeout')
@@ -480,7 +345,7 @@ describe('OrchestrationDb', () => {
it('completeDispatch sets completed_at', () => {
const d = createDb()
const task = d.createTask({ spec: 'work' })
const task = d.createTask({ runId, spec: 'work' })
const ctx = createRootDispatch(d, task.id, 'term_a')
d.completeDispatch(ctx.id)
@@ -493,7 +358,10 @@ describe('OrchestrationDb', () => {
describe('decision gates', () => {
it('creates a gate and blocks the task', () => {
const d = createDb()
const task = d.createTask({ spec: 'needs approval' })
const task = d.createTask({
runId,
spec: 'needs approval'
})
createRootDispatch(d, task.id, 'term_a')
const gate = d.createGate({
taskId: task.id,
@@ -513,7 +381,7 @@ describe('OrchestrationDb', () => {
it('resolves a gate and unblocks the task', () => {
const d = createDb()
const task = d.createTask({ spec: 'work' })
const task = d.createTask({ runId, spec: 'work' })
const gate = d.createGate({ taskId: task.id, question: 'ok?' })
const resolved = d.resolveGate(gate.id, 'yes')
@@ -526,7 +394,7 @@ describe('OrchestrationDb', () => {
it('times out a gate', () => {
const d = createDb()
const task = d.createTask({ spec: 'work' })
const task = d.createTask({ runId, spec: 'work' })
const gate = d.createGate({ taskId: task.id, question: 'ok?' })
const timedOut = d.timeoutGate(gate.id)
@@ -535,8 +403,8 @@ describe('OrchestrationDb', () => {
it('lists gates with filters', () => {
const d = createDb()
const t1 = d.createTask({ spec: 'a' })
const t2 = d.createTask({ spec: 'b' })
const t1 = d.createTask({ runId, spec: 'a' })
const t2 = d.createTask({ runId, spec: 'b' })
d.createGate({ taskId: t1.id, question: 'q1' })
const g2 = d.createGate({ taskId: t2.id, question: 'q2' })
d.resolveGate(g2.id, 'done')
@@ -599,8 +467,13 @@ describe('OrchestrationDb', () => {
describe('lifecycle', () => {
it('resetAll clears all tables', () => {
const d = createDb()
d.insertMessage({ from: 'a', to: 'b', subject: 'test' })
d.createTask({ spec: 'work' })
d.insertMessage({
runId,
from: 'a',
to: 'b',
subject: 'test'
})
d.createTask({ runId, spec: 'work' })
d.resetAll()
@@ -610,8 +483,13 @@ describe('OrchestrationDb', () => {
it('resetMessages clears only messages', () => {
const d = createDb()
d.insertMessage({ from: 'a', to: 'b', subject: 'test' })
d.createTask({ spec: 'work' })
d.insertMessage({
runId,
from: 'a',
to: 'b',
subject: 'test'
})
d.createTask({ runId, spec: 'work' })
d.resetMessages()
@@ -621,8 +499,13 @@ describe('OrchestrationDb', () => {
it('resetTasks clears tasks and dispatch contexts', () => {
const d = createDb()
d.insertMessage({ from: 'a', to: 'b', subject: 'test' })
const task = d.createTask({ spec: 'work' })
d.insertMessage({
runId,
from: 'a',
to: 'b',
subject: 'test'
})
const task = d.createTask({ runId, spec: 'work' })
createRootDispatch(d, task.id, 'term_a')
d.resetTasks()
@@ -636,6 +519,7 @@ describe('OrchestrationDb', () => {
it('insertMessage accepts type = heartbeat', () => {
const d = createDb()
const msg = d.insertMessage({
runId,
from: 'worker',
to: 'coord',
subject: 'alive',
@@ -647,7 +531,7 @@ describe('OrchestrationDb', () => {
it('recordHeartbeat updates last_heartbeat_at on dispatched rows', () => {
const d = createDb()
const task = d.createTask({ spec: 'work' })
const task = d.createTask({ runId, spec: 'work' })
const ctx = createRootDispatch(d, task.id, 'term_a')
d.recordHeartbeat(ctx.id, '2026-05-04T00:00:00.000Z')
@@ -662,10 +546,10 @@ describe('OrchestrationDb', () => {
// (b) dispatched, heartbeated 12 min ago → STALE (expected result)
// (c) dispatched, never heartbeated, dispatched 30s ago → not stale (grace)
// (d) completed, heartbeated 30 min ago → not stale (status filter)
const taskA = d.createTask({ spec: 'a' })
const taskB = d.createTask({ spec: 'b' })
const taskC = d.createTask({ spec: 'c' })
const taskD = d.createTask({ spec: 'd' })
const taskA = d.createTask({ runId, spec: 'a' })
const taskB = d.createTask({ runId, spec: 'b' })
const taskC = d.createTask({ runId, spec: 'c' })
const taskD = d.createTask({ runId, spec: 'd' })
const ctxA = createRootDispatch(d, taskA.id, 'term_a')
const ctxB = createRootDispatch(d, taskB.id, 'term_b')
const ctxC = createRootDispatch(d, taskC.id, 'term_c')
@@ -710,15 +594,19 @@ describe('OrchestrationDb', () => {
// Fresh worker: dispatched 12:00, heartbeat 12:05 (space-format), both
// after the 11:55 threshold → NOT stale.
const fresh = createRootDispatch(d, d.createTask({ spec: 'fresh' }).id, 'term_fresh')
const fresh = createRootDispatch(d, d.createTask({ runId, spec: 'fresh' }).id, 'term_fresh')
setDispatchTimes(d, fresh.id, '2026-07-12 12:00:00', '2026-07-12 12:05:00')
// Legacy ISO-format fresh row (mixed-format table) stays fresh too.
const legacy = createRootDispatch(d, d.createTask({ spec: 'legacy' }).id, 'term_legacy')
const legacy = createRootDispatch(
d,
d.createTask({ runId, spec: 'legacy' }).id,
'term_legacy'
)
setDispatchTimes(d, legacy.id, '2026-07-12T12:00:00.000Z', '2026-07-12T12:05:00.000Z')
// Genuinely hung: dispatched + heartbeated at 10:00, ~2h before threshold.
const hung = createRootDispatch(d, d.createTask({ spec: 'hung' }).id, 'term_hung')
const hung = createRootDispatch(d, d.createTask({ runId, spec: 'hung' }).id, 'term_hung')
setDispatchTimes(d, hung.id, '2026-07-12 10:00:00', '2026-07-12 10:00:00')
const stale = d.getStaleDispatches('2026-07-12T11:55:00.000Z')
@@ -730,7 +618,7 @@ describe('OrchestrationDb', () => {
// Space-format dispatched_at one minute after the threshold, no heartbeat
// yet → still inside the grace window, must not be flagged.
const ctx = createRootDispatch(d, d.createTask({ spec: 'x' }).id, 'term_x')
const ctx = createRootDispatch(d, d.createTask({ runId, spec: 'x' }).id, 'term_x')
setDispatchTimes(d, ctx.id, '2026-07-12 12:00:00')
const stale = d.getStaleDispatches('2026-07-12T11:59:00.000Z')
@@ -742,7 +630,11 @@ describe('OrchestrationDb', () => {
it('getStaleDispatches keeps a fresh row just after a UTC-midnight threshold (#8452)', () => {
const d = createDb()
const ctx = createRootDispatch(d, d.createTask({ spec: 'midnight' }).id, 'term_midnight')
const ctx = createRootDispatch(
d,
d.createTask({ runId, spec: 'midnight' }).id,
'term_midnight'
)
setDispatchTimes(d, ctx.id, '2026-05-04 00:04:00')
const stale = d.getStaleDispatches('2026-05-04T00:00:00.000Z')
@@ -755,7 +647,7 @@ describe('OrchestrationDb', () => {
it('getStaleDispatches keeps a live worker with a fresh space-format heartbeat (#8452)', () => {
const d = createDb()
const ctx = createRootDispatch(d, d.createTask({ spec: 'live' }).id, 'term_live')
const ctx = createRootDispatch(d, d.createTask({ runId, spec: 'live' }).id, 'term_live')
setDispatchTimes(d, ctx.id, '2026-07-12 10:00:00', '2026-07-12 11:59:00')
const stale = d.getStaleDispatches('2026-07-12T11:55:00.000Z')
@@ -765,6 +657,7 @@ describe('OrchestrationDb', () => {
it('getThreadMessagesFor returns only same-thread replies to a handle', () => {
const d = createDb()
const outbound = d.insertMessage({
runId,
from: 'worker',
to: 'coord',
subject: 'Question',
@@ -773,6 +666,7 @@ describe('OrchestrationDb', () => {
})
// Reply in the same thread addressed to the worker
const reply = d.insertMessage({
runId,
from: 'coord',
to: 'worker',
subject: 'Re: Question',
@@ -781,6 +675,7 @@ describe('OrchestrationDb', () => {
})
// Distractor: different thread, same recipient
d.insertMessage({
runId,
from: 'coord',
to: 'worker',
subject: 'other',
@@ -789,6 +684,7 @@ describe('OrchestrationDb', () => {
})
// Distractor: same thread but not addressed to worker
d.insertMessage({
runId,
from: 'coord',
to: 'someone_else',
subject: 'cc',
@@ -902,6 +798,7 @@ describe('OrchestrationDb', () => {
// (a) INSERT type='heartbeat' now succeeds
expect(() =>
d.insertMessage({
runId,
from: 'w',
to: 'c',
subject: 'alive',
@@ -911,7 +808,7 @@ describe('OrchestrationDb', () => {
).not.toThrow()
// (b) last_heartbeat_at column exists on dispatch_contexts
const task = d.createTask({ spec: 'work' })
const task = d.createTask({ runId, spec: 'work' })
const ctx = createRootDispatch(d, task.id, 'term_a')
d.recordHeartbeat(ctx.id, '2026-05-04T00:00:00.000Z')
expect(d.getDispatchContext(task.id)?.last_heartbeat_at).toBe('2026-05-04T00:00:00.000Z')
@@ -942,11 +839,12 @@ describe('OrchestrationDb', () => {
const d = new OrchestrationDb(path)
db = d
const task = d.createTask({ spec: 'work' })
const task = d.createTask({ runId, spec: 'work' })
const ctx = createRootDispatch(d, task.id, 'term_a', 'tab_1:leaf_1')
expect(d.getDispatchContextById(ctx.id)?.assignee_pane_key).toBe('tab_1:leaf_1')
const msg = d.insertMessage({
runId,
from: 'w',
to: 'c',
subject: 'done',
@@ -960,6 +858,7 @@ describe('OrchestrationDb', () => {
const path = createV1Snapshot()
const first = new OrchestrationDb(path)
first.insertMessage({
runId,
from: 'w',
to: 'c',
subject: 'alive',
@@ -972,6 +871,7 @@ describe('OrchestrationDb', () => {
db = second
expect(() =>
second.insertMessage({
runId,
from: 'w',
to: 'c',
subject: 'again',
@@ -48,7 +48,7 @@ describe('durable Attempt observation and outcome projection', () => {
function createAttempt(): { taskId: string; dispatchId: string } {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'observe outcome' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'observe outcome' })
const dispatch = createRootDispatch(db, task.id, 'term_observed')
return { taskId: task.id, dispatchId: dispatch.id }
}
@@ -162,7 +162,10 @@ describe('durable Attempt observation and outcome projection', () => {
const path = join(dir, 'orchestration.sqlite')
try {
db = new OrchestrationDb(path)
const task = db.createTask({ spec: 'durable observation' })
const task = db.createTask({
runId: 'run_legacy_local',
spec: 'durable observation'
})
const dispatch = createRootDispatch(db, task.id, 'term_durable')
db.recordAttemptObservation(
fact(dispatch.id, {
@@ -189,7 +192,10 @@ describe('durable Attempt observation and outcome projection', () => {
it('keeps worker_done settlement as the atomic success fast path', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'worker_done fast path' })
const task = db.createTask({
runId: 'run_legacy_local',
spec: 'worker_done fast path'
})
const started = db.createStartingWorkerDispatch({
creator: { kind: 'system' },
maxDepth: Number.MAX_SAFE_INTEGER,
@@ -7,4 +7,4 @@ export const LEGACY_CONTRACT_VERSION = 0
export const CURRENT_CONTRACT_VERSION = ORCHESTRATION_CONTRACT_VERSION
// Schema versions: v2 'heartbeat'+last_heartbeat_at, v3 delivered_at, v4 task-creator terminal, v5 task_title/display_name, v6 pane identity, v7 lightweight Runs, v8 crash-safe Run deliveries, v9 durable question threads, v10 Dispatch capabilities, v11 durable mutation receipts, v12 composed worker state, v18 post-v6 version-skew repair, v19 adopted legacy Runs and compatibility receipts, v20 legacy question backfill, v21 legacy scheduler-loss provenance, v22 dispatch assignee lookup, v23 worker terminal resource ownership, v24 creator-incarnation authority, v25 active Dispatch handle lookup, v26 indexed mutation receipt capacity, v27 durable federation acknowledgments, v28 durable local mutation caller identity, v31 dispatch/resource identity links, v32 bounded worker-terminal recovery metadata, v33 durable mailbox pointer Enter state, v34 role-addressed mailbox deliveries, v35 mailbox delivery default and index-predicate repair, v36 dispatch mailbox consumer generation, v37 recorded dispatch creator identity, v39 structured session journal archives.
export const SCHEMA_VERSION = 39
export const SCHEMA_VERSION = 40
@@ -9,7 +9,7 @@ describe('decision-gate lifecycle transitions', () => {
it('blocks the dispatched Task when creating a gate', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'gate blocks task' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'gate blocks task' })
createRootDispatch(db, task.id, 'term_gate')
expect(db.getTask(task.id)?.status).toBe('dispatched')
@@ -20,7 +20,7 @@ describe('decision-gate lifecycle transitions', () => {
it('rolls back the gate row when the Task transition cannot commit', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'atomic gate creation' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'atomic gate creation' })
const dispatch = createRootDispatch(db, task.id, 'term_gate')
db.db.exec(`
CREATE TRIGGER reject_gate_task_block
@@ -1,6 +1,5 @@
import type { DecisionGateRow, DispatchContextRow, GateStatus } from '../../types'
import { OrchestrationError } from '../../orchestration-error'
import { LEGACY_RUN_ID } from '../contract-constants'
import { generateId } from '../generated-id'
import type { OrchestrationDb } from '../orchestration-db'
import { transitionLifecycleWithDb } from '../lifecycle-transition'
@@ -18,6 +17,16 @@ export function createGate(
): DecisionGateRow {
this.db.exec('SAVEPOINT create_gate')
try {
const task = this.getTask(gate.taskId)
if (!task) {
throw new OrchestrationError(
'lifecycle_not_found',
`Task ${gate.taskId} was not found while creating a decision gate.`,
{ taskId: gate.taskId }
)
}
const runId = task.run_id
this.requireRun(runId)
const active = this.db
.prepare(
`SELECT * FROM dispatch_contexts
@@ -65,22 +74,8 @@ export function createGate(
.prepare(
'INSERT INTO decision_gates (id, run_id, task_id, question, options) VALUES (?, ?, ?, ?, ?)'
)
.run(
id,
this.getTask(gate.taskId)?.run_id ?? LEGACY_RUN_ID,
gate.taskId,
gate.question,
optionsJson
)
.run(id, runId, gate.taskId, gate.question, optionsJson)
this.completeActiveDispatchesForTask(gate.taskId)
const task = this.getTask(gate.taskId)
if (!task) {
throw new OrchestrationError(
'lifecycle_not_found',
`Task ${gate.taskId} was not found while creating a decision gate.`,
{ taskId: gate.taskId }
)
}
transitionLifecycleWithDb(this.db, {
entity: 'task',
id: gate.taskId,
@@ -16,7 +16,7 @@ describe('nested worker depth', () => {
function coordinatorDispatchesWorker(maxDepth = UNCAPPED) {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'root task' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'root task' })
const worker = db.createDispatchContext({
taskId: task.id,
assigneeHandle: 'term_worker',
@@ -33,7 +33,7 @@ describe('nested worker depth', () => {
it('refuses a worker dispatching a sub-worker at the default cap', () => {
coordinatorDispatchesWorker()
const nested = db.createTask({ spec: 'nested task' })
const nested = db.createTask({ runId: 'run_legacy_local', spec: 'nested task' })
expect(() =>
db.createDispatchContext({
taskId: nested.id,
@@ -51,7 +51,7 @@ describe('nested worker depth', () => {
it('tells the refused worker to complete the task itself', () => {
coordinatorDispatchesWorker()
const nested = db.createTask({ spec: 'nested task' })
const nested = db.createTask({ runId: 'run_legacy_local', spec: 'nested task' })
expect(() =>
db.createDispatchContext({
taskId: nested.id,
@@ -64,7 +64,7 @@ describe('nested worker depth', () => {
it('permits one more generation when the cap is raised, and records depth 2', () => {
coordinatorDispatchesWorker()
const nested = db.createTask({ spec: 'nested task' })
const nested = db.createTask({ runId: 'run_legacy_local', spec: 'nested task' })
const sub = db.createDispatchContext({
taskId: nested.id,
assigneeHandle: 'term_sub',
@@ -113,9 +113,9 @@ describe('nested worker depth', () => {
db.db
.prepare(
`INSERT INTO remote_dispatch_attachments
(dispatch_id, task_id, home_peer_fingerprint, protocol_version, runtime_epoch,
(dispatch_id, task_id, home_run_id, home_peer_fingerprint, protocol_version, runtime_epoch,
pane_key, process_incarnation, state, depth)
VALUES (?, ?, 'peer', 1, 'epoch', ?, ?, ?, ?)`
VALUES (?, ?, 'run_home', 'peer', 1, 'epoch', ?, ?, ?, ?)`
)
.run(`ctx_${state}_${depth}_${paneKey}_${inc}`, 'task_remote', paneKey, inc, state, depth)
}
@@ -182,7 +182,10 @@ describe('nested worker depth', () => {
it('takes the maximum when a process holds both a local and a remote role', () => {
// Query order must not decide the answer: the deeper role governs.
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'local role' })
const task = db.createTask({
runId: 'run_legacy_local',
spec: 'local role'
})
db.createDispatchContext({
taskId: task.id,
assigneeHandle: 'term_both',
@@ -220,13 +223,19 @@ describe('nested worker depth', () => {
it('stamps depth 1 for a root coordinator', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'root work' })
const task = db.createTask({
runId: 'run_legacy_local',
spec: 'root work'
})
expect(startWorker(task.id, SYSTEM, UNCAPPED).dispatch.depth).toBe(1)
})
it('refuses a worker starting a sub-worker at the default cap', () => {
coordinatorDispatchesWorker()
const nested = db.createTask({ spec: 'nested work' })
const nested = db.createTask({
runId: 'run_legacy_local',
spec: 'nested work'
})
expect(() =>
startWorker(
nested.id,
@@ -238,7 +247,10 @@ describe('nested worker depth', () => {
it('refuses a worker retrying into a sub-worker at the default cap', () => {
coordinatorDispatchesWorker()
const nested = db.createTask({ spec: 'nested retry work' })
const nested = db.createTask({
runId: 'run_legacy_local',
spec: 'nested retry work'
})
const first = startWorker(nested.id, SYSTEM, UNCAPPED)
db.failWorkerStart(first.dispatch.id, 'accepted', 'first attempt failed')
expect(() =>
@@ -257,7 +269,10 @@ describe('nested worker depth', () => {
// Context-only dispatch stores null on purpose; requiring an incarnation
// locally would silently drop real parents and fail open.
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'context only' })
const task = db.createTask({
runId: 'run_legacy_local',
spec: 'context only'
})
const row = db.createDispatchContext({
taskId: task.id,
assigneeHandle: 'term_ctx',
@@ -22,7 +22,7 @@ describe('dispatch mailbox consumer fencing', () => {
afterEach(() => db.close())
function dispatchWithMail(subjects: string[]): { id: string; runId: string } {
const task = db.createTask({ spec: 'fenced worker work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'fenced worker work' })
const dispatch = createRootDispatch(db, task.id, 'term_worker', PANE_A)
for (const subject of subjects) {
db.insertMessage({
@@ -116,7 +116,10 @@ describe('dispatch mailbox consumer fencing', () => {
})
it('bumps and fences on the worker-start attach path', () => {
const task = db.createTask({ spec: 'worker-start attach' })
const task = db.createTask({
runId: 'run_legacy_local',
spec: 'worker-start attach'
})
const started = db.createStartingWorkerDispatch({
creator: { kind: 'system' },
maxDepth: Number.MAX_SAFE_INTEGER,
@@ -149,6 +152,7 @@ describe('dispatch mailbox consumer fencing', () => {
it('gives a federated attachment its own generation on the worker host', () => {
const dispatchId = 'ctx_remote_fence'
db.createRemoteDispatchAttachment({
runId: 'run-home',
dispatchId,
taskId: 'task_remote',
homePeerFingerprint: 'home-peer',
@@ -187,7 +191,10 @@ describe('dispatch mailbox consumer fencing', () => {
})
it('starts a retry Dispatch on a fresh mailbox address rather than sharing the old one', () => {
const task = db.createTask({ spec: 'work that fails once' })
const task = db.createTask({
runId: 'run_legacy_local',
spec: 'work that fails once'
})
const first = db.createStartingWorkerDispatch({
creator: { kind: 'system' },
maxDepth: Number.MAX_SAFE_INTEGER,
@@ -49,8 +49,8 @@ const STARTING_DISPATCH_CONTEXT_SQL = `INSERT INTO dispatch_contexts (
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', datetime('now'))`
const REMOTE_DISPATCH_ATTACHMENT_SQL = `INSERT INTO remote_dispatch_attachments (
dispatch_id, task_id, home_peer_fingerprint, protocol_version, runtime_epoch, depth
) VALUES (?, ?, ?, ?, ?, ?)`
dispatch_id, home_run_id, task_id, home_peer_fingerprint, protocol_version, runtime_epoch, depth
) VALUES (?, ?, ?, ?, ?, ?, ?)`
/** Last line of defence: a row that reached here unstamped would read as a root. */
function assertStampedDepth(depth: number): void {
@@ -140,6 +140,7 @@ export function insertRemoteDispatchAttachmentRow(
db: Database.Database,
params: {
dispatchId: string
runId: string
taskId: string
homePeerFingerprint: string
protocolVersion: number
@@ -151,6 +152,7 @@ export function insertRemoteDispatchAttachmentRow(
assertStampedDepth(params.depth)
db.prepare(REMOTE_DISPATCH_ATTACHMENT_SQL).run(
params.dispatchId,
params.runId,
params.taskId,
params.homePeerFingerprint,
params.protocolVersion,
@@ -8,7 +8,10 @@ describe('federated Dispatch observation fence', () => {
it('rejects out-of-order epochs and observations captured before release', () => {
const database = (db = new OrchestrationDb(':memory:'))
const task = database.createTask({ spec: 'fenced federated observation' })
const task = database.createTask({
runId: 'run_legacy_local',
spec: 'fenced federated observation'
})
const started = database.createStartingWorkerDispatch({
creator: { kind: 'system' },
maxDepth: Number.MAX_SAFE_INTEGER,
@@ -8,6 +8,7 @@ export function createRemoteDispatchAttachment(
this: OrchestrationDb,
params: {
dispatchId: string
runId: string
taskId: string
homePeerFingerprint: string
protocolVersion: number
@@ -43,6 +44,16 @@ export function createRemoteDispatchAttachment(
`Remote attachment request ${params.mutationReceipt.requestId} already exists.`
)
}
if (!params.runId?.trim()) {
throw new OrchestrationError('invalid_argument', 'Missing Run ID')
}
this.db
.prepare(
`INSERT OR IGNORE INTO runs (id, objective, home_database, consumer_generation, legacy)
VALUES (?, ?, 'remote', 0, 0)`
)
.run(params.runId, `Coordinated from ${params.homePeerFingerprint}`)
this.requireRun(params.runId)
ensureMutationReceiptCapacity(this.db)
this.db
.prepare(
@@ -59,6 +70,7 @@ export function createRemoteDispatchAttachment(
)
insertRemoteDispatchAttachmentRow(this.db, {
dispatchId: params.dispatchId,
runId: params.runId,
taskId: params.taskId,
homePeerFingerprint: params.homePeerFingerprint,
protocolVersion: params.protocolVersion,
@@ -16,6 +16,7 @@ describe('the remote attachment release guard', () => {
function settledAttachment(dispatchId: string): void {
db.createRemoteDispatchAttachment({
runId: 'run-home',
dispatchId,
taskId: `task_${dispatchId}`,
homePeerFingerprint: 'home-peer',
@@ -8,7 +8,7 @@ describe('guarded lifecycle transitions', () => {
it('rejects a stale prior state without changing the projection', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'guarded transition' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'guarded transition' })
expect(() =>
db!.transitionLifecycle({
@@ -23,7 +23,7 @@ describe('guarded lifecycle transitions', () => {
it('composes its projection into the caller-owned transaction', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'caller-owned rollback' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'caller-owned rollback' })
db.db.exec('SAVEPOINT lifecycle_test')
expect(
@@ -49,7 +49,7 @@ describe('guarded lifecycle transitions', () => {
['completed', 'blocked']
] as const)('preserves public task updates from %s to %s', (from, to) => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'manual status correction' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'manual status correction' })
db.db.prepare('UPDATE tasks SET status = ? WHERE id = ?').run(from, task.id)
expect(db.updateTaskStatus(task.id, to)?.status).toBe(to)
@@ -1,5 +1,4 @@
import type { MessageType, MessagePriority, MessageDeliveryContract, MessageRow } from '../../types'
import { LEGACY_RUN_ID } from '../contract-constants'
import { generateId } from '../generated-id'
import { exposeMessageTimestamps } from '../utc-timestamp'
import type { OrchestrationDb } from '../orchestration-db'
@@ -26,7 +25,10 @@ export type MessageInsert = {
}
export function insertMessage(this: OrchestrationDb, msg: MessageInsert): MessageRow {
const runId = msg.runId ?? LEGACY_RUN_ID
const runId = msg.runId
if (!runId) {
throw new Error('Run is required')
}
const deliveryContract = msg.deliveryContract ?? 'current_delivery'
this.requireRun(runId)
const id = msg.id ?? generateId('msg')
@@ -38,6 +38,7 @@ CREATE TABLE IF NOT EXISTS federated_dispatches (
);
CREATE TABLE IF NOT EXISTS remote_dispatch_attachments (
home_run_id TEXT NOT NULL,
dispatch_id TEXT PRIMARY KEY,
task_id TEXT NOT NULL,
home_peer_fingerprint TEXT NOT NULL,
@@ -0,0 +1,26 @@
import { afterEach, describe, expect, it } from 'vitest'
import { OrchestrationDb } from '../orchestration-db'
import { migrateV40 } from './migrate-v40'
import { importFederatedControlMessage } from '../../federation-control-message'
describe('federated home Run migration', () => {
const db = new OrchestrationDb(':memory:')
afterEach(() => db.close())
it('adds the home Run column and refuses mail for a development placeholder', () => {
db.db.exec('ALTER TABLE remote_dispatch_attachments DROP COLUMN home_run_id')
db.db.exec(`INSERT INTO remote_dispatch_attachments
(dispatch_id, task_id, home_peer_fingerprint, runtime_epoch)
VALUES ('ctx_old', 'task_old', 'home', 'epoch')`)
migrateV40.call(db, 39)
expect(db.getRemoteDispatchAttachment('ctx_old')?.home_run_id).toBe('')
expect(() =>
importFederatedControlMessage(db, {
dispatchId: 'ctx_old',
messageId: 'message_old',
payload: JSON.stringify({ from: 'home', subject: 'Instruction', body: '', type: 'message' })
})
).toThrow('Run not found:')
expect(db.getMessageById('message_old')).toBeUndefined()
})
})
@@ -0,0 +1,11 @@
import type { OrchestrationDb } from '../orchestration-db'
export function migrateV40(this: OrchestrationDb, current: number): void {
if (current >= 40 || this.hasColumn('remote_dispatch_attachments', 'home_run_id')) {
return
}
// Federation is unreleased; any development-only rows fail Run validation until reattached.
this.db.exec(
"ALTER TABLE remote_dispatch_attachments ADD COLUMN home_run_id TEXT NOT NULL DEFAULT ''"
)
}
@@ -10,6 +10,7 @@ import { migrateV36 } from './migrate-v36'
import { migrateV37 } from './migrate-v37'
import { migrateV38 } from './migrate-v38'
import { migrateV39 } from './migrate-v39'
import { migrateV40 } from './migrate-v40'
// Why: CREATE TABLE IF NOT EXISTS won't alter existing DBs; migrate in a txn that bumps user_version only on success (atomic all-or-nothing).
export function migrate(this: OrchestrationDb): void {
@@ -30,6 +31,7 @@ export function migrate(this: OrchestrationDb): void {
migrateV37.call(this, current)
migrateV38.call(this, current)
migrateV39.call(this, current)
migrateV40.call(this, current)
this.createMailboxDeliveryIndexesIfPossible()
this.db.pragma(`user_version = ${SCHEMA_VERSION}`)
this.db.exec('COMMIT')
@@ -35,18 +35,24 @@ export function updateTaskStatus(
ORDER BY rowid DESC LIMIT 1`
)
.get(id) as { id: string } | undefined
const activeWorker = terminalStatus
? (this.db
.prepare(
`SELECT active.id
// Why: a supervised worker owns its Task for as long as it is alive. Every status this
// function lets past the active-Dispatch check must clear the same worker check, or the Task
// re-opens under a worker whose own lifecycle can no longer settle it (#16904 relay wedge).
// A no-op re-assert of `dispatched` re-opens nothing and stays legal.
const reopensUnderWorker = requiresActiveDispatch && task.status !== 'dispatched'
const activeWorker =
terminalStatus || reopensUnderWorker
? (this.db
.prepare(
`SELECT active.id
FROM dispatch_contexts active
JOIN worker_dispatches worker ON worker.dispatch_id = active.id
WHERE active.task_id = ? AND active.status IN ('pending', 'dispatched')
AND worker.state NOT IN ('failed', 'succeeded', 'stopped', 'abandoned')
ORDER BY active.rowid DESC LIMIT 1`
)
.get(id) as { id: string } | undefined)
: undefined
)
.get(id) as { id: string } | undefined)
: undefined
if (activeWorker) {
throw new OrchestrationError(
'task_not_startable',
@@ -1,7 +1,6 @@
import type Database from '../../../../sqlite/sync-database'
import type { TaskStatus, TaskRow } from '../../types'
import { buildOrchestrationTaskDisplayMetadata } from '../../../../../shared/orchestration-task-display'
import { LEGACY_RUN_ID } from '../contract-constants'
import { generateId } from '../generated-id'
import type { TaskRuntimeLineageRow } from '../run-list-page'
import type { OrchestrationDb } from '../orchestration-db'
@@ -25,7 +24,10 @@ export function createTask(
runId?: string
}
): TaskRow {
const runId = task.runId ?? LEGACY_RUN_ID
const runId = task.runId
if (!runId) {
throw new Error('Run is required')
}
this.requireRun(runId)
if (task.parentId) {
const parent = this.getTask(task.parentId)
@@ -59,7 +59,19 @@ export function beginWorkerStop(
this.db.exec('COMMIT')
return { disposition: 'already_settled', worker, dispatch }
}
if (!['ready', 'start_unknown'].includes(worker.state)) {
// Why `stopping` under a DIFFERENT epoch is accepted: a stop whose runtime died mid-flight
// leaves the row here forever, and refusing the re-issue was the only operator escape
// (#16904). Re-running the stop earns the honest outcome — settled, or `stop_unknown`, from
// which the worker can be abandoned. It never asserts an exit the runtime did not observe.
//
// Why the epoch and not just the state: this runtime's own `stopping` row means its stop is
// still in flight, and a second pass would record `stop_unknown` over it. The exit event that
// follows claims a clean stop only from `stopping` under its own epoch
// (failActiveDispatchOnExit), so it would then read the operator's stop as a crash and
// escalate it. Same predicate as that reader, so both agree on whose stop this is.
const stopStrandedByAnotherRuntime =
worker.state === 'stopping' && worker.runtime_epoch !== runtimeEpoch
if (!['ready', 'start_unknown'].includes(worker.state) && !stopStrandedByAnotherRuntime) {
throw new OrchestrationError(
'dispatch_inactive',
`Dispatch ${dispatchId} cannot stop from ${worker.state}.`
@@ -0,0 +1,40 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { OrchestrationDb } from './orchestration-db'
describe('writers require a Run', () => {
let db: OrchestrationDb
beforeEach(() => {
db = new OrchestrationDb(':memory:')
})
afterEach(() => db.close())
it('rejects a message without a Run instead of using the legacy Run', () => {
expect(() => db.insertMessage({ from: 'sender', to: 'worker', subject: 'mail' })).toThrow(
'Run is required'
)
expect(db.db.prepare('SELECT id FROM messages').all()).toEqual([])
})
it('rejects a Task without a Run instead of using the legacy Run', () => {
expect(() => db.createTask({ spec: 'work' })).toThrow('Run is required')
expect(db.listTasks()).toEqual([])
})
it('rejects a decision gate whose Task has no Run', () => {
const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' })
vi.spyOn(db, 'getTask').mockReturnValue({ ...task, run_id: undefined } as never)
expect(() => db.createGate({ taskId: task.id, question: 'Proceed?' })).toThrow()
expect(db.listGates()).toEqual([])
})
it('rejects a decision gate without a Task before writing', () => {
db.db.exec(`
CREATE TRIGGER reject_gate_insert BEFORE INSERT ON decision_gates
BEGIN SELECT RAISE(ABORT, 'gate insert reached'); END;
`)
expect(() => db.createGate({ taskId: 'missing', question: 'Proceed?' })).toThrow(
'Task missing was not found while creating a decision gate.'
)
expect(db.listGates()).toEqual([])
})
})
@@ -6,7 +6,7 @@ import { createRootDispatch } from './db/root-dispatch-test-fixture'
describe('dispatch failure idempotency', () => {
it('counts an active dispatch failure only once', () => {
const db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' })
const dispatch = createRootDispatch(db, task.id, 'term_worker')
expect(db.failDispatch(dispatch.id, 'exit')?.failure_count).toBe(1)
@@ -19,7 +19,7 @@ describe('dispatch failure idempotency', () => {
it('does not overwrite a completed dispatch', () => {
const db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' })
const dispatch = createRootDispatch(db, task.id, 'term_worker')
db.completeDispatch(dispatch.id)
@@ -33,7 +33,7 @@ describe('dispatch failure idempotency', () => {
it('rolls back the dispatch when the task update fails', () => {
const db = new OrchestrationDb(':memory:')
const sqlite = (db as unknown as { db: Database.Database }).db
const task = db.createTask({ spec: 'work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' })
const dispatch = createRootDispatch(db, task.id, 'term_worker')
sqlite.exec(`
CREATE TRIGGER reject_task_failure_update
@@ -17,7 +17,7 @@ describe('a start that fails before authority still owns the terminal it created
adoption?: Parameters<OrchestrationDb['failWorkerStart']>[3]
): { db: OrchestrationDb; dispatchId: string } {
const d = (db = new OrchestrationDb(':memory:'))
const task = d.createTask({ spec: 'residual terminal' })
const task = d.createTask({ runId: 'run_legacy_local', spec: 'residual terminal' })
const started = d.createStartingWorkerDispatch({
creator: { kind: 'system' },
maxDepth: Number.MAX_SAFE_INTEGER,
@@ -122,7 +122,7 @@ describe('a start that fails before authority still owns the terminal it created
const first = d.createStartingWorkerDispatch({
creator: { kind: 'system' },
maxDepth: Number.MAX_SAFE_INTEGER,
taskId: d.createTask({ spec: 'owner' }).id,
taskId: d.createTask({ runId: 'run_legacy_local', spec: 'owner' }).id,
startOptions: {}
})
d.prepareStartingWorkerAuthority({
@@ -138,7 +138,7 @@ describe('a start that fails before authority still owns the terminal it created
const second = d.createStartingWorkerDispatch({
creator: { kind: 'system' },
maxDepth: Number.MAX_SAFE_INTEGER,
taskId: d.createTask({ spec: 'claimant' }).id,
taskId: d.createTask({ runId: 'run_legacy_local', spec: 'claimant' }).id,
startOptions: {}
})
d.recordWorkerStage({
@@ -14,6 +14,7 @@ describe('federation acknowledgment integrity', () => {
db = new OrchestrationDb(':memory:')
const dispatchId = `ctx_protocol_${protocolVersion}`
db.createRemoteDispatchAttachment({
runId: 'run-home',
dispatchId,
taskId: `task_protocol_${protocolVersion}`,
homePeerFingerprint: 'home_peer',
@@ -58,11 +58,20 @@ export function importFederatedControlMessage(
payload: string
}
): { imported: boolean; type: MessageType } {
const attachment = db.getRemoteDispatchAttachment(params.dispatchId)
if (!attachment) {
throw new OrchestrationError(
'dispatch_not_found',
`Remote Dispatch ${params.dispatchId} was not found.`
)
}
db.requireRun(attachment.home_run_id)
const message = parseFederatedControlMessage(params.payload)
const recipient = `dispatch:${params.dispatchId}`
const existing = db.getMessageById(params.messageId)
if (existing) {
if (
existing.run_id !== attachment.home_run_id ||
existing.to_handle !== recipient ||
existing.from_handle !== message.from ||
existing.subject !== message.subject ||
@@ -81,6 +90,7 @@ export function importFederatedControlMessage(
}
db.insertMessage({
id: params.messageId,
runId: attachment.home_run_id,
from: message.from,
to: recipient,
subject: message.subject,
@@ -109,7 +109,10 @@ describe('lifecycle graph against its callers', () => {
it('settles a stopping worker whose PTY exits during the stop', () => {
const database = createDatabase()
const task = database.createTask({ spec: 'stopping exited worker' })
const task = database.createTask({
runId: 'run_legacy_local',
spec: 'stopping exited worker'
})
const dispatchId = startWorker(database, task.id, 'stopping_exited')
expect(database.beginWorkerStop(dispatchId, 'runtime_test').disposition).toBe('stopping')
@@ -127,9 +130,18 @@ describe('lifecycle graph against its callers', () => {
it('still lets a coordinator reopen or overturn a settled Task', () => {
const database = createDatabase()
const reopened = database.createTask({ spec: 'reopen me' })
const overturned = database.createTask({ spec: 'overturn me' })
const retried = database.createTask({ spec: 'retry me' })
const reopened = database.createTask({
runId: 'run_legacy_local',
spec: 'reopen me'
})
const overturned = database.createTask({
runId: 'run_legacy_local',
spec: 'overturn me'
})
const retried = database.createTask({
runId: 'run_legacy_local',
spec: 'retry me'
})
database.updateTaskStatus(reopened.id, 'completed', 'first result')
database.updateTaskStatus(overturned.id, 'completed', 'wrong result')
database.updateTaskStatus(retried.id, 'failed', 'boom')
@@ -10,10 +10,11 @@ describe('lifecycle reconciliation', () => {
it('rejects handle churn when neither side has stable pane identity', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' })
const dispatch = createRootDispatch(db, task.id, 'term_before_restart')
const logs: string[] = []
const message = db.insertMessage({
runId: 'run_legacy_local',
from: 'term_after_restart',
to: 'term_coordinator',
subject: 'Done',
@@ -37,9 +38,10 @@ describe('lifecycle reconciliation', () => {
it('completes worker_done from the dispatched pane after a handle remint', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' })
const dispatch = createRootDispatch(db, task.id, 'term_before_restart', `tab_w:${LEAF_A}`)
const message = db.insertMessage({
runId: 'run_legacy_local',
from: 'term_after_restart',
to: 'term_coordinator',
subject: 'Done',
@@ -54,7 +56,7 @@ describe('lifecycle reconciliation', () => {
it('completes an exact-authority worker_done after an uncertain worker start', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' })
const started = db.createStartingWorkerDispatch({
creator: { kind: 'system' },
maxDepth: Number.MAX_SAFE_INTEGER,
@@ -82,6 +84,7 @@ describe('lifecycle reconciliation', () => {
).toEqual({ valid: true })
const message = db.insertMessage({
runId: 'run_legacy_local',
from: 'term_worker',
to: 'term_coordinator',
subject: 'Done after reconnect',
@@ -106,9 +109,10 @@ describe('lifecycle reconciliation', () => {
it('fails both the dispatch and task from an authenticated failed worker report', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' })
const dispatch = createRootDispatch(db, task.id, 'term_worker', `tab_w:${LEAF_A}`)
const message = db.insertMessage({
runId: 'run_legacy_local',
from: 'term_worker',
to: 'term_coordinator',
subject: 'Failed: tests cannot start',
@@ -139,7 +143,7 @@ describe('lifecycle reconciliation', () => {
it('keeps worker report settlement nested in its caller transaction', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' })
const dispatch = createRootDispatch(db, task.id, 'term_worker')
db.db.exec('BEGIN IMMEDIATE')
@@ -160,19 +164,16 @@ describe('lifecycle reconciliation', () => {
it('replays an identical terminal outcome without mutating settled state', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' })
const dispatch = createRootDispatch(db, task.id, 'term_worker')
const makeMessage = () =>
db.insertMessage({
runId: 'run_legacy_local',
from: 'term_worker',
to: 'term_coordinator',
subject: 'Done',
type: 'worker_done',
payload: JSON.stringify({
taskId: task.id,
dispatchId: dispatch.id,
outcome: 'succeeded'
})
payload: JSON.stringify({ taskId: task.id, dispatchId: dispatch.id, outcome: 'succeeded' })
})
expect(reconcileLifecycleMessage(db, makeMessage()).action).toBe('completed')
@@ -199,6 +200,7 @@ describe('lifecycle reconciliation', () => {
])('rejects malformed worker reports with $code', ({ payload, code }) => {
db = new OrchestrationDb(':memory:')
const message = db.insertMessage({
runId: 'run_legacy_local',
from: 'term_worker',
to: 'term_coordinator',
subject: 'Done',
@@ -215,11 +217,12 @@ describe('lifecycle reconciliation', () => {
it('completes worker_done from the same leaf after a pane break-out changed the tab half', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' })
// Dispatch recorded the post-break-out pane key; the worker shell still
// holds the spawn-time key with the old tab id.
const dispatch = createRootDispatch(db, task.id, 'term_before_restart', `tab_new:${LEAF_A}`)
const message = db.insertMessage({
runId: 'run_legacy_local',
from: 'term_after_restart',
to: 'term_coordinator',
subject: 'Done',
@@ -234,9 +237,10 @@ describe('lifecycle reconciliation', () => {
it('rejects mismatched opaque pane keys instead of treating them as legacy', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' })
const dispatch = createRootDispatch(db, task.id, 'term_owner', `tab_w:${LEAF_A}`)
const message = db.insertMessage({
runId: 'run_legacy_local',
from: 'term_reminted',
to: 'term_coordinator',
subject: 'Done',
@@ -251,9 +255,10 @@ describe('lifecycle reconciliation', () => {
it('rejects worker_done from a foreign pane that claims the assignee handle', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' })
const dispatch = createRootDispatch(db, task.id, 'term_owner', `tab_w1:${LEAF_A}`)
const message = db.insertMessage({
runId: 'run_legacy_local',
from: 'term_owner',
to: 'term_coordinator',
subject: 'Done',
@@ -294,9 +299,10 @@ describe('lifecycle reconciliation', () => {
it('does not let a caller-supplied rejection marker turn completion into success', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' })
const dispatch = createRootDispatch(db, task.id, 'term_worker', `tab_w:${LEAF_A}`)
const message = db.insertMessage({
runId: 'run_legacy_local',
from: 'term_worker',
to: 'term_coordinator',
subject: 'Done',
@@ -323,9 +329,10 @@ describe('lifecycle reconciliation', () => {
it('rejects a coordinator completion for a pane-bound dispatch', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' })
const dispatch = createRootDispatch(db, task.id, 'term_worker', `tab_w:${LEAF_A}`)
const message = db.insertMessage({
runId: 'run_legacy_local',
from: 'term_coordinator',
to: 'term_coordinator',
subject: 'Done',
@@ -342,9 +349,13 @@ describe('lifecycle reconciliation', () => {
it('uses exact handle equality only for a legacy dispatch without a pane key', () => {
db = new OrchestrationDb(':memory:')
const acceptedTask = db.createTask({ spec: 'legacy work' })
const acceptedTask = db.createTask({
runId: 'run_legacy_local',
spec: 'legacy work'
})
const acceptedDispatch = createRootDispatch(db, acceptedTask.id, 'term_legacy')
const accepted = db.insertMessage({
runId: 'run_legacy_local',
from: 'term_legacy',
to: 'term_coordinator',
subject: 'Done',
@@ -357,9 +368,13 @@ describe('lifecycle reconciliation', () => {
})
expect(reconcileLifecycleMessage(db, accepted).action).toBe('completed')
const rejectedTask = db.createTask({ spec: 'other legacy work' })
const rejectedTask = db.createTask({
runId: 'run_legacy_local',
spec: 'other legacy work'
})
const rejectedDispatch = createRootDispatch(db, rejectedTask.id, 'term_other_legacy')
const rejected = db.insertMessage({
runId: 'run_legacy_local',
from: 'term_foreign',
to: 'term_coordinator',
subject: 'Done',
@@ -379,8 +394,12 @@ describe('lifecycle reconciliation', () => {
it('does not release a dependent when a foreign completion wins the arrival race', () => {
db = new OrchestrationDb(':memory:')
const parent = db.createTask({ spec: 'parent' })
const child = db.createTask({ spec: 'child', deps: [parent.id] })
const parent = db.createTask({ runId: 'run_legacy_local', spec: 'parent' })
const child = db.createTask({
runId: 'run_legacy_local',
spec: 'child',
deps: [parent.id]
})
const dispatch = createRootDispatch(db, parent.id, 'term_worker', `tab_w:${LEAF_A}`)
const payload = JSON.stringify({
taskId: parent.id,
@@ -389,6 +408,7 @@ describe('lifecycle reconciliation', () => {
})
const foreign = db.insertMessage({
runId: 'run_legacy_local',
from: 'term_coordinator',
to: 'term_coordinator',
subject: 'Done',
@@ -403,6 +423,7 @@ describe('lifecycle reconciliation', () => {
expect(db.getTask(child.id)?.status).toBe('pending')
const owner = db.insertMessage({
runId: 'run_legacy_local',
from: 'term_worker_reminted',
to: 'term_coordinator',
subject: 'Done',
@@ -416,7 +437,7 @@ describe('lifecycle reconciliation', () => {
it('does not let a foreign replay overwrite an authorized completion', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' })
const dispatch = createRootDispatch(db, task.id, 'term_worker', `tab_w:${LEAF_A}`)
const payload = JSON.stringify({
taskId: task.id,
@@ -424,6 +445,7 @@ describe('lifecycle reconciliation', () => {
outcome: 'succeeded'
})
const owner = db.insertMessage({
runId: 'run_legacy_local',
from: 'term_worker',
to: 'term_coordinator',
subject: 'Done',
@@ -435,6 +457,7 @@ describe('lifecycle reconciliation', () => {
const result = db.getTask(task.id)?.result
const replay = db.insertMessage({
runId: 'run_legacy_local',
from: 'term_foreign',
to: 'term_coordinator',
subject: 'Forged replay',
@@ -451,10 +474,11 @@ describe('lifecycle reconciliation', () => {
it('surfaces worker_done sent from a different pane as rejected', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' })
const dispatch = createRootDispatch(db, task.id, 'term_owner', `tab_w1:${LEAF_A}`)
const logs: string[] = []
const message = db.insertMessage({
runId: 'run_legacy_local',
from: 'term_other_worker',
to: 'term_coordinator',
subject: 'Done',
@@ -474,9 +498,10 @@ describe('lifecycle reconciliation', () => {
it('surfaces a heartbeat sent from a different pane without recording liveness', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' })
const dispatch = createRootDispatch(db, task.id, 'term_owner', `tab_w1:${LEAF_A}`)
const heartbeat = db.insertMessage({
runId: 'run_legacy_local',
from: 'term_other_worker',
to: 'term_coordinator',
subject: 'alive',
@@ -508,9 +533,10 @@ describe('lifecycle reconciliation', () => {
it('surfaces a foreign heartbeat that claims the assignee handle', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' })
const dispatch = createRootDispatch(db, task.id, 'term_owner', `tab_w1:${LEAF_A}`)
const heartbeat = db.insertMessage({
runId: 'run_legacy_local',
from: 'term_owner',
to: 'term_coordinator',
subject: 'alive',
@@ -528,9 +554,10 @@ describe('lifecycle reconciliation', () => {
it('records a heartbeat whose pane key drifted only in the tab half', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' })
const dispatch = createRootDispatch(db, task.id, 'term_owner', `tab_new:${LEAF_A}`)
const heartbeat = db.insertMessage({
runId: 'run_legacy_local',
from: 'term_owner',
to: 'term_coordinator',
subject: 'alive',
@@ -548,12 +575,16 @@ describe('lifecycle reconciliation', () => {
it('suppresses same-dispatch heartbeats once worker_done is reconciled', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'work' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'work' })
const dispatch = createRootDispatch(db, task.id, 'term_worker')
const otherTask = db.createTask({ spec: 'other work' })
const otherTask = db.createTask({
runId: 'run_legacy_local',
spec: 'other work'
})
const otherDispatch = createRootDispatch(db, otherTask.id, 'term_other')
const insertHeartbeat = (dispatchId: string, from: string) =>
db.insertMessage({
runId: 'run_legacy_local',
from,
to: 'term_coordinator',
subject: 'alive',
@@ -565,6 +596,7 @@ describe('lifecycle reconciliation', () => {
reconcileLifecycleMessage(db, staleHeartbeat)
reconcileLifecycleMessage(db, otherHeartbeat)
const done = db.insertMessage({
runId: 'run_legacy_local',
from: 'term_worker',
to: 'term_coordinator',
subject: 'Done',
@@ -412,7 +412,7 @@ describe('STA-4604 worker PTY exit escalation reaches the coordinator', () => {
}
})
it('falls back to the legacy gate when the dispatch owning Run row is gone', async () => {
it('preserves the dispatch Run when legacy coordinator routing is used', async () => {
const { runtime, workerHandle, coordinatorHandle } = makeRuntimeWithTwoPanes()
const insertMessage = vi.fn((message: { to: string }) => ({
...message,
@@ -435,8 +435,7 @@ describe('STA-4604 worker PTY exit escalation reaches the coordinator', () => {
expect(insertMessage).toHaveBeenCalledWith(
expect.objectContaining({ to: coordinatorHandle, type: 'escalation' })
)
// An orphaned dispatch has no Run mailbox to address, so it must not invent one.
expect(insertMessage.mock.calls[0]?.[0]).not.toHaveProperty('runId')
expect(insertMessage.mock.calls[0]?.[0]).toHaveProperty('runId', 'run-that-no-longer-exists')
})
it('still reaches the Run mailbox when the Run has no bound coordinator', async () => {
@@ -15,9 +15,9 @@ const MAILBOX = 'dispatch:d1'
function seeded(): OrchestrationDb {
const db = new OrchestrationDb(':memory:')
db.insertMessages([
{ from: 'coordinator', to: MAILBOX, subject: 'a', type: 'status' },
{ from: 'coordinator', to: MAILBOX, subject: 'b', type: 'question' },
{ from: 'coordinator', to: MAILBOX, subject: 'c', type: 'status' }
{ runId: 'run_legacy_local', from: 'coordinator', to: MAILBOX, subject: 'a', type: 'status' },
{ runId: 'run_legacy_local', from: 'coordinator', to: MAILBOX, subject: 'b', type: 'question' },
{ runId: 'run_legacy_local', from: 'coordinator', to: MAILBOX, subject: 'c', type: 'status' }
])
return db
}
@@ -59,7 +59,12 @@ function stageArgs(db: OrchestrationDb, state: OrchestrationMailboxPointerState)
describe('mailbox pointer staging watermark', () => {
it('leaves no watermark when the reservation claim is lost', () => {
const db = new OrchestrationDb(':memory:')
const message = db.insertMessage({ from: 'a', to: 'run:run-1', subject: 's' })
const message = db.insertMessage({
runId: 'run_legacy_local',
from: 'a',
to: 'run:run-1',
subject: 's'
})
// A concurrent flight already owns the reservation, so this claim cannot succeed.
expect(
db.stageMailboxPointerEnter([message.id], { ptyId: 'other-pty', processIncarnation: 'inc-x' })
@@ -79,7 +84,12 @@ describe('mailbox pointer staging watermark', () => {
it('leaves no watermark when the reservation write throws', () => {
const db = new OrchestrationDb(':memory:')
const message = db.insertMessage({ from: 'a', to: 'run:run-1', subject: 's' })
const message = db.insertMessage({
runId: 'run_legacy_local',
from: 'a',
to: 'run:run-1',
subject: 's'
})
const throwing = new Proxy(db, {
get(target, prop, receiver) {
if (prop === 'markMailboxPointerWriteAttempted') {
@@ -107,7 +117,12 @@ describe('mailbox pointer staging watermark', () => {
it('keeps the watermark for the flight that owns the reservation', () => {
const db = new OrchestrationDb(':memory:')
const message = db.insertMessage({ from: 'a', to: 'run:run-1', subject: 's' })
const message = db.insertMessage({
runId: 'run_legacy_local',
from: 'a',
to: 'run:run-1',
subject: 's'
})
const state = new OrchestrationMailboxPointerState()
const args = stageArgs(db, state)
stageOrchestrationMailboxPointer({
@@ -122,7 +137,12 @@ describe('mailbox pointer staging watermark', () => {
it('drains a delivery parked behind the watermark when the write is refused', () => {
const db = new OrchestrationDb(':memory:')
const message = db.insertMessage({ from: 'a', to: 'run:run-1', subject: 's' })
const message = db.insertMessage({
runId: 'run_legacy_local',
from: 'a',
to: 'run:run-1',
subject: 's'
})
const state = new OrchestrationMailboxPointerState()
const args = stageArgs(db, state)
const redrive = vi.fn()
@@ -148,7 +168,7 @@ describe('mailbox pointer staging watermark', () => {
it('still points new mail after a delivery lost its reservation claim', async () => {
const db = new OrchestrationDb(':memory:')
db.insertMessage({ from: 'a', to: 'run:run-1', subject: 'first' })
db.insertMessage({ runId: 'run_legacy_local', from: 'a', to: 'run:run-1', subject: 'first' })
let stealNextClaim = true
const contended = new Proxy(db, {
get(target, prop, receiver) {
@@ -172,7 +192,7 @@ describe('mailbox pointer staging watermark', () => {
expect(writePty).not.toHaveBeenCalled()
// Newer mail must still reach the agent; a leaked watermark used to park it forever.
db.insertMessage({ from: 'a', to: 'run:run-1', subject: 'second' })
db.insertMessage({ runId: 'run_legacy_local', from: 'a', to: 'run:run-1', subject: 'second' })
delivery.deliver(LEAF, { mailboxHandle: 'run:run-1', skipAbsenceProbe: true })
await new Promise((resolve) => setImmediate(resolve))
@@ -14,7 +14,12 @@ import type { WriteSettlement } from '../../../shared/pty-write-settlement'
describe('orchestration mailbox pointer submit', () => {
it('does not settle a replacement reservation after an old Enter write resolves', async () => {
const db = new OrchestrationDb(':memory:')
const message = db.insertMessage({ from: 'a', to: 'run:run-1', subject: 'staged' })
const message = db.insertMessage({
runId: 'run_legacy_local',
from: 'a',
to: 'run:run-1',
subject: 'staged'
})
const ptyId = 'pty-reused'
const oldReservation = { ptyId, processIncarnation: 'inc-old' }
const replacementReservation = { ptyId, processIncarnation: 'inc-new' }
@@ -86,8 +91,18 @@ describe('orchestration mailbox pointer submit', () => {
it('does not overwrite a message already reserved by another pointer flight', () => {
const db = new OrchestrationDb(':memory:')
const first = db.insertMessage({ from: 'a', to: 'run:run-1', subject: 'first' })
const second = db.insertMessage({ from: 'a', to: 'run:run-1', subject: 'second' })
const first = db.insertMessage({
runId: 'run_legacy_local',
from: 'a',
to: 'run:run-1',
subject: 'first'
})
const second = db.insertMessage({
runId: 'run_legacy_local',
from: 'a',
to: 'run:run-1',
subject: 'second'
})
const original = { ptyId: 'pty-a', processIncarnation: 'inc-a' }
const replacement = { ptyId: 'pty-b', processIncarnation: 'inc-b' }
@@ -108,8 +108,20 @@ describe('message batch atomicity', () => {
expect(() =>
db?.insertMessages([
{ id: 'inner_first', from: 'sender', to: 'recipient', subject: 'first' },
{ id: 'inner_second', from: 'sender', to: 'recipient', subject: 'second' }
{
runId: 'run_legacy_local',
id: 'inner_first',
from: 'sender',
to: 'recipient',
subject: 'first'
},
{
runId: 'run_legacy_local',
id: 'inner_second',
from: 'sender',
to: 'recipient',
subject: 'second'
}
])
).toThrow('blocked')
sqlite.exec('COMMIT')
@@ -133,6 +145,7 @@ describe('message batch atomicity', () => {
expect(() =>
db?.commitWorkerDoneMessageMutation(() => {
db?.insertMessage({
runId: 'run_legacy_local',
id: 'inner',
from: 'worker',
to: 'coordinator',
@@ -34,6 +34,7 @@ describe('nested worker depth migration (v30)', () => {
const oldDb = new Database(dbPath)
oldDb.exec('ALTER TABLE dispatch_contexts DROP COLUMN depth')
oldDb.exec('ALTER TABLE remote_dispatch_attachments DROP COLUMN depth')
oldDb.exec('ALTER TABLE remote_dispatch_attachments DROP COLUMN home_run_id')
oldDb.pragma('user_version = 29')
oldDb
.prepare(
@@ -78,7 +79,7 @@ describe('nested worker depth migration (v30)', () => {
)
.run()
const task = db.createTask({ spec: 'post-upgrade nesting attempt' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'post-upgrade nesting attempt' })
expect(() =>
db!.createDispatchContext({
taskId: task.id,
@@ -47,11 +47,13 @@ function createAdoptedFixture(options: { settleWork: boolean }): AdoptedFixture
const before = new OrchestrationDb(dbPath)
const task = before.createTask({
runId: 'run_legacy_local',
spec: 'legacy assignment',
createdByTerminalHandle: LEGACY_COORDINATOR_HANDLE
})
const dispatch = createRootDispatch(before, task.id, LEGACY_WORKER_HANDLE, LEGACY_WORKER_PANE)
const recovery = before.insertMessage({
runId: 'run_legacy_local',
from: LEGACY_WORKER_HANDLE,
to: LEGACY_COORDINATOR_HANDLE,
subject: 'recovered worker outcome',
@@ -36,7 +36,12 @@ describe('orchestration migration from every prior version stamp', () => {
expect(reopened.db.pragma('user_version', { simple: true }), `reopen v${version}`).toBe(
SCHEMA_VERSION
)
expect(() => reopened.createTask({ spec: `migration v${version}` })).not.toThrow()
expect(() =>
reopened.createTask({
runId: 'run_legacy_local',
spec: `migration v${version}`
})
).not.toThrow()
reopened.close()
}
})
@@ -103,6 +103,7 @@ describe('OrchestrationDb bounded mutation receipts', () => {
insertMutationReceipts(db, MUTATION_RECEIPT_MAX_ROWS, 'completed')
db.createRemoteDispatchAttachment({
runId: 'run-home',
dispatchId: 'ctx_remote_pruned',
taskId: 'task_remote_pruned',
homePeerFingerprint: 'caller',
@@ -131,6 +132,7 @@ describe('OrchestrationDb bounded mutation receipts', () => {
expect(() =>
db!.createRemoteDispatchAttachment({
runId: 'run-home',
dispatchId: 'ctx_remote_overflow',
taskId: 'task_remote_overflow',
homePeerFingerprint: 'caller',
@@ -151,7 +153,7 @@ describe('OrchestrationDb bounded mutation receipts', () => {
it('guards atomic worker acceptance without changing task state', () => {
db = new OrchestrationDb(':memory:')
const task = db.createTask({ spec: 'capacity check' })
const task = db.createTask({ runId: 'run_legacy_local', spec: 'capacity check' })
insertMutationReceipts(db, MUTATION_RECEIPT_MAX_ROWS, 'pending')
expect(() =>
@@ -226,7 +228,10 @@ describe('OrchestrationDb dispatch assignee index migration', () => {
tempDir = mkdtempSync(join(tmpdir(), 'orca-dispatch-index-migration-'))
const dbPath = join(tempDir, 'orchestration.db')
db = new OrchestrationDb(dbPath)
const task = db.createTask({ spec: 'indexed lookup' })
const task = db.createTask({
runId: 'run_legacy_local',
spec: 'indexed lookup'
})
const dispatch = createRootDispatch(db, task.id, 'term_worker')
db.close()
db = undefined
@@ -245,7 +250,9 @@ describe('OrchestrationDb dispatch assignee index migration', () => {
db = new OrchestrationDb(dbPath)
const sqlite = sqliteFor(db)
expect(sqlite.pragma('user_version', { simple: true })).toBe(SCHEMA_VERSION)
expect(db.getDispatchContextById(dispatch.id)).toMatchObject({ assignee_handle: 'term_worker' })
expect(db.getDispatchContextById(dispatch.id)).toMatchObject({
assignee_handle: 'term_worker'
})
expect(db.getTask(task.id)).toMatchObject({
created_by_pane_key: null,
created_by_process_incarnation: null,
@@ -0,0 +1,97 @@
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { LEGACY_RUN_ID, OrchestrationDb } from './db'
import { SCHEMA_VERSION } from './db/contract-constants'
import { resolveOrchestrationMigrationStartVersion } from './orchestration-schema-version-skew'
describe('federated mailbox legacy-adoption probe', () => {
let db: OrchestrationDb | undefined
let directory: string | undefined
afterEach(() => {
db?.close()
if (directory) {
rmSync(directory, { recursive: true, force: true })
}
})
function seedMailbox(handle: string, kind: 'message' | 'delivery'): string {
directory = mkdtempSync(join(tmpdir(), 'orca-federated-legacy-probe-'))
const path = join(directory, 'orchestration.db')
db = new OrchestrationDb(path)
db.db.exec(`
INSERT INTO remote_dispatch_attachments (
dispatch_id, task_id, home_peer_fingerprint, home_run_id, runtime_epoch, state
) VALUES ('ctx_remote', 'task_remote', 'peer_home', 'run_home', 'epoch', 'ready');
`)
if (kind === 'message') {
db.db
.prepare(
`INSERT INTO messages (
id, run_id, delivery_contract, from_handle, to_handle, subject, type
) VALUES ('msg_probe', ?, 'current_delivery', 'term_home', ?, 'continue', 'dispatch')`
)
.run(LEGACY_RUN_ID, handle)
} else {
db.db
.prepare(
`INSERT INTO deliveries (id, run_id, mailbox_handle, consumer_generation, message_ids)
VALUES ('delivery_probe', ?, ?, 0, '[]')`
)
.run(LEGACY_RUN_ID, handle)
}
return path
}
it.each(['message', 'delivery'] as const)(
'does not replay adoption for a misfiled federated %s',
(kind) => {
const path = seedMailbox('dispatch:ctx_remote', kind)
expect(
resolveOrchestrationMigrationStartVersion(db!.db, SCHEMA_VERSION, SCHEMA_VERSION)
).toBe(SCHEMA_VERSION)
db!.close()
db = new OrchestrationDb(path)
expect(db.getLegacyAdoption()).toBeUndefined()
if (kind === 'message') {
expect(db.getMessageById('msg_probe')).toMatchObject({
run_id: LEGACY_RUN_ID,
delivery_contract: 'current_delivery'
})
} else {
expect(
db.db.prepare("SELECT status FROM deliveries WHERE id = 'delivery_probe'").get()
).toEqual({
status: 'outstanding'
})
}
}
)
it.each(['message', 'delivery'] as const)(
'still replays adoption for a genuine legacy %s',
(kind) => {
const path = seedMailbox('term_legacy_coordinator', kind)
expect(
resolveOrchestrationMigrationStartVersion(db!.db, SCHEMA_VERSION, SCHEMA_VERSION)
).toBe(6)
db!.close()
db = new OrchestrationDb(path)
expect(db.getLegacyAdoption()).toBeDefined()
if (kind === 'message') {
expect(db.getMessageById('msg_probe')).toMatchObject({
run_id: db.getLegacyAdoption()!.adopted_run_id,
delivery_contract: 'legacy_direct'
})
} else {
expect(
db.db.prepare("SELECT status FROM deliveries WHERE id = 'delivery_probe'").get()
).toEqual({
status: 'fenced'
})
}
}
)
})

Some files were not shown because too many files have changed in this diff Show More