fix(orchestration): preserve active workers across updates (#11271)

* fix(orchestration): preserve active workers across updates

* test(ssh): model absent legacy adoption

* test(orchestration): align compatibility contracts

* fix(windows): escape updater PowerShell booleans

* fix(windows): restore stock uninstall process check

* fix(orchestration): keep recovery off renderer startup barrier

* fix(orchestration): harden legacy recovery migration

* fix(orchestration): close recovery review gaps

* fix(orchestration): complete legacy worker cutover recovery

* fix(orchestration): preserve legacy workers across updates

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-07-29 20:25:44 -07:00
committed by Neil
co-authored by OrcaWin
parent 3240c26bce
commit 959e8d63b0
150 changed files with 19865 additions and 792 deletions
File diff suppressed because one or more lines are too long
@@ -71,4 +71,24 @@ describe('orchestration check identity', () => {
})
)
})
it('preserves the pinned legacy --inject check signature', async () => {
process.env.ORCA_TERMINAL_HANDLE = 'term_legacy_worker'
await invokeCheck(
new Map<string, string | boolean>([
['unread', true],
['inject', true]
])
)
expect(callMock).toHaveBeenCalledWith(
'orchestration.check',
expect.objectContaining({
terminal: 'term_legacy_worker',
unread: true,
inject: true
})
)
})
})
@@ -179,6 +179,7 @@ describe('legacy orchestration CLI inspection', () => {
id: 'msg_current',
run_id: 'run_current',
from_handle: 'term_current',
to_handle: 'run:run_current',
subject: 'current question',
type: 'question',
body: 'May I continue?',
@@ -217,7 +218,8 @@ describe('legacy orchestration CLI inspection', () => {
expect(response.result.formatted).not.toContain('RUNTIME_SENTINEL')
expect(
response.result.formatted.split('\n').filter((line) => line.startsWith('[Reply:'))
).toEqual(['[Reply: orca orchestration reply --id msg_current --from term_coord --body "..."]'])
).toEqual(['[Reply: orca orchestration reply --id msg_current --body "..."]'])
expect(response.result.formatted).not.toContain('--from run:run_current')
})
it('preserves runtime formatting when every message belongs to a current Run', async () => {
@@ -1,28 +1,44 @@
import { describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { ORCHESTRATION_HANDLERS } from './orchestration'
describe('orchestration CLI migration recovery', () => {
it('redirects worker_done without an outcome before resolving or calling the runtime', async () => {
const call = vi.fn()
const originalPaneKey = process.env.ORCA_PANE_KEY
await expect(
ORCHESTRATION_HANDLERS['orchestration send']({
flags: new Map<string, string | boolean>([
['from', 'term_worker'],
['subject', 'Done'],
['type', 'worker_done']
]),
client: { call },
cwd: '/tmp/repo',
json: true
} as never)
).rejects.toMatchObject({
code: 'invalid_argument',
data: {
effectsApplied: false,
nextCommandArgs: ['skills', 'get', 'orchestration', '--full']
}
afterEach(() => {
if (originalPaneKey === undefined) {
delete process.env.ORCA_PANE_KEY
} else {
process.env.ORCA_PANE_KEY = originalPaneKey
}
})
describe('orchestration CLI migration recovery', () => {
it('forwards legacy worker_done without inventing a completion outcome', async () => {
process.env.ORCA_PANE_KEY = 'tab-worker:leaf-worker'
const call = vi.fn().mockResolvedValue({ result: { message: { id: 'msg_done' } } })
await ORCHESTRATION_HANDLERS['orchestration send']({
flags: new Map<string, string | boolean>([
['from', 'term_worker'],
['subject', 'Done'],
['type', 'worker_done']
]),
client: { call },
cwd: '/tmp/repo',
json: true
} as never)
expect(call).toHaveBeenCalledWith('orchestration.send', {
from: 'term_worker',
to: undefined,
run: undefined,
subject: 'Done',
body: undefined,
type: 'worker_done',
priority: undefined,
threadId: undefined,
payload: undefined,
senderPaneKey: 'tab-worker:leaf-worker',
devMode: false
})
expect(call).not.toHaveBeenCalled()
})
})
@@ -62,6 +62,28 @@ describe('lightweight Run CLI handlers', () => {
from: 'term_coord'
})
})
it('passes explicit legacy takeover only when requested', async () => {
callMock.mockResolvedValue({
result: { run: { id: 'run_adopted', objective: 'Recovered work' } }
})
await ORCHESTRATION_HANDLERS['orchestration run-use']({
flags: new Map<string, string | boolean>([
['id', 'run_adopted'],
['from', 'term_current'],
['takeover-legacy', true]
]),
client: { call: callMock },
cwd: '/tmp/repo',
json: true
} as never)
expect(callMock).toHaveBeenCalledWith('orchestration.runUse', {
id: 'run_adopted',
from: 'term_current',
takeoverLegacy: true
})
})
})
describe('orchestration reset CLI handler', () => {
@@ -64,11 +64,13 @@ describe('orchestration timeout flag validation', () => {
// Why: unread:false makes pre-peek runtimes fall back to non-consuming all mode.
expect(callMock).toHaveBeenCalledWith('orchestration.check', {
terminal: 'term_worker',
terminalPaneKey: undefined,
unread: false,
peek: true,
all: undefined,
types: undefined,
format: undefined,
compatibilityCliCommand: expect.stringMatching(/^orca(?:-ide)?$/),
run: undefined,
ack: undefined,
wait: true,
@@ -182,7 +184,9 @@ describe('orchestration timeout flag validation', () => {
resume: undefined,
options: undefined,
timeoutMs: 123,
from: 'term_worker'
from: 'term_worker',
compatibilityCliCommand: expect.stringMatching(/^orca(?:-ide)?$/),
compatibilityWindowsCommand: undefined
},
{ timeoutMs: 5_123, orchestrationCapability: undefined }
)
@@ -209,7 +213,9 @@ describe('orchestration timeout flag validation', () => {
resume: 'msg_question',
options: undefined,
timeoutMs: undefined,
from: 'term_worker'
from: 'term_worker',
compatibilityCliCommand: expect.stringMatching(/^orca(?:-ide)?$/),
compatibilityWindowsCommand: undefined
},
{ timeoutMs: 605_000, orchestrationCapability: undefined }
)
+11 -3
View File
@@ -86,11 +86,15 @@ describe('orchestration timeout flag validation', () => {
expect(callMock).toHaveBeenCalledWith('orchestration.check', {
terminal: 'term_worker',
terminalPaneKey: undefined,
unread: false,
peek: true,
all: undefined,
types: undefined,
inject: undefined,
format: undefined,
compatibilityCliCommand: expect.stringMatching(/^orca(?:-ide)?$/),
run: undefined,
ack: undefined,
wait: true,
timeoutMs: 250
})
@@ -273,12 +277,16 @@ describe('orchestration timeout flag validation', () => {
'orchestration.ask',
{
to: 'term_coord',
run: undefined,
question: 'Proceed?',
resume: undefined,
options: undefined,
timeoutMs: 123,
from: 'term_worker'
from: 'term_worker',
compatibilityCliCommand: expect.stringMatching(/^orca(?:-ide)?$/),
compatibilityWindowsCommand: undefined
},
{ timeoutMs: 5_123 }
{ timeoutMs: 5_123, orchestrationCapability: undefined }
)
})
})
@@ -0,0 +1,110 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const callMock = vi.fn()
const originalCliCommand = process.env.ORCA_CLI_COMMAND
const originalPackagedLauncher = process.env.ORCA_WINDOWS_PACKAGED_CLI_LAUNCHER
const originalTerminalHandle = process.env.ORCA_TERMINAL_HANDLE
const originalExitCode = process.exitCode
vi.mock('../format', () => ({ printResult: vi.fn() }))
vi.mock('../selectors', () => ({ getTerminalHandle: vi.fn() }))
import { ORCHESTRATION_HANDLERS } from './orchestration'
describe('packaged Windows legacy ask protocol', () => {
beforeEach(() => {
callMock.mockReset()
process.env.ORCA_WINDOWS_PACKAGED_CLI_LAUNCHER = '1'
process.env.ORCA_TERMINAL_HANDLE = 'term_worker'
process.exitCode = undefined
})
afterEach(() => {
vi.restoreAllMocks()
process.exitCode = originalExitCode
restoreEnv('ORCA_CLI_COMMAND', originalCliCommand)
restoreEnv('ORCA_WINDOWS_PACKAGED_CLI_LAUNCHER', originalPackagedLauncher)
restoreEnv('ORCA_TERMINAL_HANDLE', originalTerminalHandle)
})
it.each(['orca', 'orca-ide'] as const)(
'commits with the %s launcher and exits 75 before resume',
async (command) => {
process.env.ORCA_CLI_COMMAND = command
callMock.mockResolvedValue({
result: {
answer: null,
messageId: 'msg_question',
threadId: 'msg_question',
timedOut: false,
legacyCompatibility: {
resumeRequired: true,
resumeCommand: `${command} orchestration ask --resume msg_question`
}
}
})
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
await invokeAsk(new Map([['question', 'Proceed?']]))
expect(callMock).toHaveBeenCalledWith(
'orchestration.ask',
expect.objectContaining({
question: 'Proceed?',
resume: undefined,
compatibilityWindowsCommand: command
}),
expect.any(Object)
)
expect(log.mock.calls.map(([line]) => line)).toEqual([
'Question msg_question committed.',
`Resume with: ${command} orchestration ask --resume msg_question`
])
expect(process.exitCode).toBe(75)
}
)
it('resumes the committed question without another exit-75 handoff', async () => {
process.env.ORCA_CLI_COMMAND = 'orca'
callMock.mockResolvedValue({
result: {
answer: 'yes',
messageId: 'msg_question',
threadId: 'msg_question',
timedOut: false
}
})
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
await invokeAsk(new Map([['resume', 'msg_question']]))
expect(callMock).toHaveBeenCalledWith(
'orchestration.ask',
expect.objectContaining({
question: undefined,
resume: 'msg_question',
compatibilityWindowsCommand: 'orca'
}),
expect.any(Object)
)
expect(log).toHaveBeenCalledWith('yes')
expect(process.exitCode).toBeUndefined()
})
})
function invokeAsk(flags: Map<string, string | boolean>): Promise<void> {
return ORCHESTRATION_HANDLERS['orchestration ask']({
flags,
client: { call: callMock },
cwd: '/tmp/repo',
json: false
} as never)
}
function restoreEnv(name: string, value: string | undefined): void {
if (value === undefined) {
delete process.env[name]
} else {
process.env[name] = value
}
}
+7 -2
View File
@@ -692,6 +692,7 @@ describe('orchestration timeout flag validation', () => {
all: undefined,
types: undefined,
format: undefined,
compatibilityCliCommand: expect.stringMatching(/^orca(?:-ide)?$/),
run: undefined,
ack: undefined,
wait: true,
@@ -819,7 +820,9 @@ describe('orchestration timeout flag validation', () => {
resume: undefined,
options: undefined,
timeoutMs: 123,
from: 'term_worker'
from: 'term_worker',
compatibilityCliCommand: expect.stringMatching(/^orca(?:-ide)?$/),
compatibilityWindowsCommand: undefined
},
{ timeoutMs: 5_123, orchestrationCapability: undefined }
)
@@ -848,7 +851,9 @@ describe('orchestration timeout flag validation', () => {
resume: 'msg_question',
options: undefined,
timeoutMs: undefined,
from: 'term_worker'
from: 'term_worker',
compatibilityCliCommand: expect.stringMatching(/^orca(?:-ide)?$/),
compatibilityWindowsCommand: undefined
},
{ timeoutMs: 605_000, orchestrationCapability: undefined }
)
+85 -120
View File
@@ -21,11 +21,14 @@ import type {
} from '../../shared/orchestration-worker-output'
import type { NativeChatMessage } from '../../shared/native-chat-types'
import type { RuntimeTerminalRead } from '../../shared/runtime-types'
import { orchestrationMigrationData } from '../../shared/orchestration-rpc-contract'
import {
ORCHESTRATION_LEGACY_RUN_ID,
orchestrationMigrationData,
orchestrationSkillRecoveryData
} from '../../shared/orchestration-rpc-contract'
formatMessageReadOnlyTag,
formatOrchestrationCheckText,
prepareOrchestrationCheckOutput,
type LegacyCompatibilityResult,
type OrchestrationMessageSummary as MessageSummary
} from '../../shared/orchestration-check-output'
// Why: 15 s is well under Claude Code's ~2 min Bash-tool silence budget while keeping log volume low. See design doc §3.4.
const DEFAULT_KEEPALIVE_INTERVAL_MS = 15_000
@@ -75,81 +78,6 @@ const TASK_STATUS_VALUES = [
'blocked'
] as const
type MessageSummary = {
id: string
run_id?: string
from_handle: string
to_handle?: string
subject?: string
type?: string
body?: string
payload?: string | null
priority?: string
read?: number
}
function formatMessageReadOnlyTag(message: MessageSummary): string {
return message.run_id === ORCHESTRATION_LEGACY_RUN_ID ? ' [legacy, read-only]' : ''
}
function isLegacyReadOnlyMessage(message: MessageSummary): boolean {
return message.run_id === ORCHESTRATION_LEGACY_RUN_ID
}
function formatMessagePriorityTag(message: MessageSummary): string {
return message.priority === 'urgent' ? ' [URGENT]' : message.priority === 'high' ? ' [HIGH]' : ''
}
function escapeTerminalControlCharacters(value: string): string {
return [...value]
.map((character) => {
const code = character.charCodeAt(0)
if (character === '\n' || (code >= 0x20 && code < 0x7f) || code > 0x9f) {
return character
}
return `\\x${code.toString(16).padStart(2, '0')}`
})
.join('')
}
function formatQuotedMessageField(label: string, value?: string): string {
return `[${label}]\n${escapeTerminalControlCharacters(value ?? '')
.split('\n')
.map((line) => ` ${line}`)
.join('\n')}`
}
function formatLegacyAwareCheckMessages(
messages: MessageSummary[],
checkedTerminal: string
): string {
return messages
.map((message) => {
const legacyReadOnly = isLegacyReadOnlyMessage(message)
const lines = [
`${message.id}${formatMessageReadOnlyTag(message)}${formatMessagePriorityTag(message)} [${message.type ?? 'status'}] from=${message.from_handle}`,
formatQuotedMessageField('subject', message.subject)
]
if (legacyReadOnly) {
lines.push('[Inspection only: reply and acknowledgment are unavailable.]')
}
if (message.body) {
lines.push(formatQuotedMessageField('body', message.body))
}
if (message.payload) {
lines.push(formatQuotedMessageField('payload', message.payload))
}
if (!legacyReadOnly) {
const replyFrom = message.to_handle ?? checkedTerminal
lines.push(
`[Reply: orca orchestration reply --id ${message.id} --from ${replyFrom} --body "..."]`
)
}
return lines.join('\n')
})
.join('\n\n')
}
type LifecycleSendRejection = {
action: 'rejected'
code: string
@@ -170,6 +98,40 @@ type OrchestrationSendResult =
lifecycle?: { action: 'completed' | 'failed' }
}
function resolveCompatibilityCliCommand(): 'orca' | 'orca-ide' | 'orca-dev' {
const configured = process.env.ORCA_CLI_COMMAND
if (configured === 'orca' || configured === 'orca-ide' || configured === 'orca-dev') {
return configured
}
return process.platform === 'linux' ? 'orca-ide' : 'orca'
}
function resolvePackagedWindowsCompatibilityCommand(): 'orca' | 'orca-ide' | undefined {
if (process.env.ORCA_WINDOWS_PACKAGED_CLI_LAUNCHER !== '1') {
return undefined
}
const command = process.env.ORCA_CLI_COMMAND
if (command === 'orca' || command === 'orca-ide') {
return command
}
throw new RuntimeClientError(
'invalid_argument',
'The packaged Orca launcher did not provide a valid resume command. No question was created.'
)
}
async function flushStdout(): Promise<void> {
await new Promise<void>((resolve, reject) => {
process.stdout.write('', (error) => {
if (error) {
reject(error)
} else {
resolve()
}
})
})
}
function getOptionalStructuredMessagePayload(
flags: Map<string, string | boolean>
): string | undefined {
@@ -485,7 +447,8 @@ export const ORCHESTRATION_HANDLERS: Record<string, CommandHandler> = {
run: { id: string; objective: string; consumer_generation: number }
}>(client, flags, 'orchestration.runUse', {
id: getRequiredStringFlag(flags, 'id'),
from
from,
...(flags.has('takeover-legacy') ? { takeoverLegacy: true } : {})
})
printResult(result, json, (r) => `Using Run ${r.run.id}: ${r.run.objective}`)
},
@@ -541,13 +504,6 @@ export const ORCHESTRATION_HANDLERS: Record<string, CommandHandler> = {
rejectLifecycleGroupRecipient(type, to)
}
const outcome = getOptionalStringFlag(flags, 'outcome')
if (type === 'worker_done' && outcome === undefined && !flags.has('payload')) {
throw new RuntimeClientError(
'invalid_argument',
'worker_done requires --outcome succeeded or --outcome failed. No effects were applied.',
orchestrationSkillRecoveryData()
)
}
if (type !== 'worker_done' && outcome !== undefined) {
throw new RuntimeClientError(
'invalid_argument',
@@ -634,6 +590,7 @@ export const ORCHESTRATION_HANDLERS: Record<string, CommandHandler> = {
timedOut?: boolean
cancelled?: boolean
connectionLost?: boolean
legacyCompatibility?: LegacyCompatibilityResult
}
let result: Awaited<ReturnType<typeof client.call<CheckResult>>>
try {
@@ -646,6 +603,8 @@ export const ORCHESTRATION_HANDLERS: Record<string, CommandHandler> = {
all: flags.has('all') ? true : undefined,
types: getOptionalStringFlag(flags, 'types'),
format: flags.has('format') ? true : undefined,
inject: flags.has('inject') ? true : undefined,
compatibilityCliCommand: resolveCompatibilityCliCommand(),
run: getOptionalStringFlag(flags, 'run'),
ack: getOptionalStringFlag(flags, 'ack'),
wait: wait ? true : undefined,
@@ -682,39 +641,25 @@ export const ORCHESTRATION_HANDLERS: Record<string, CommandHandler> = {
}
}
}
if (flags.has('format') && result.result.messages.some(isLegacyReadOnlyMessage)) {
// Why: formatted is one opaque batch with untrusted bodies, so selective banner parsing cannot safely remove legacy actions.
result = {
...result,
result: {
...result.result,
formatted: formatLegacyAwareCheckMessages(result.result.messages, terminal)
}
}
result = {
...result,
result: prepareOrchestrationCheckOutput(result.result, terminal, flags.has('format'))
}
printResult(result, json, (r) => formatOrchestrationCheckText(r, terminal))
const compatibilityAck = result.result.legacyCompatibility?.ackMessageIds
if (compatibilityAck && compatibilityAck.length > 0) {
await flushStdout()
await client.call('orchestration.check', {
terminal,
compatibilityAck: JSON.stringify({
messageIds: compatibilityAck,
types: getOptionalStringFlag(flags, 'types')
?.split(',')
.map((type) => type.trim())
.filter(Boolean)
})
})
}
printResult(result, json, (r) => {
if (r.formatted) {
return r.formatted
}
if (r.count === 0) {
if (r.timedOut) {
return 'Wait timed out; no messages were consumed.'
}
if (r.cancelled) {
return r.connectionLost
? 'Wait cancelled because the connection closed; no messages were consumed.'
: 'Wait cancelled; no messages were consumed.'
}
return 'No messages.'
}
const rendered = r.messages
.map(
(m) =>
`${m.id}${formatMessageReadOnlyTag(m)} [${m.type ?? 'status'}] from=${m.from_handle} "${m.subject}"`
)
.join('\n')
return r.deliveryId ? `Delivery ${r.deliveryId}\n${rendered}` : rendered
})
},
'orchestration reply': async ({ flags, client, cwd, json }) => {
@@ -1034,6 +979,8 @@ export const ORCHESTRATION_HANDLERS: Record<string, CommandHandler> = {
timeoutMs?: number
cancelled?: boolean
connectionLost?: boolean
answerMessageId?: string | null
legacyCompatibility?: LegacyCompatibilityResult
}>(
client,
flags,
@@ -1045,7 +992,9 @@ export const ORCHESTRATION_HANDLERS: Record<string, CommandHandler> = {
resume,
options: getOptionalStringFlag(flags, 'options'),
timeoutMs: parsedTimeoutMs === undefined ? undefined : timeoutMs,
from
from,
compatibilityCliCommand: resolveCompatibilityCliCommand(),
compatibilityWindowsCommand: resolvePackagedWindowsCompatibilityCommand()
},
// Why: extend past timeoutMs so the RPC transport's 60s default doesn't abort before the runtime's own timeout resolves.
{
@@ -1056,9 +1005,25 @@ export const ORCHESTRATION_HANDLERS: Record<string, CommandHandler> = {
// Why: bypass printResult so --json emits a bare JSON object (no envelope) pipeable via `jq -r .answer`, unlike other verbs.
if (json) {
console.log(JSON.stringify(result.result))
} else if (result.result.legacyCompatibility?.resumeRequired) {
console.log(`Question ${result.result.messageId} committed.`)
console.log(`Resume with: ${result.result.legacyCompatibility.resumeCommand}`)
} else if (result.result.answer !== null) {
console.log(result.result.answer)
}
if (result.result.legacyCompatibility?.resumeRequired) {
await flushStdout()
process.exitCode = 75
return
}
const answerAck = result.result.legacyCompatibility?.answerAcknowledgement
if (answerAck && result.result.answer !== null) {
await flushStdout()
await client.call('orchestration.check', {
terminal: from,
compatibilityQuestionAck: JSON.stringify(answerAck)
})
}
if (result.result.timedOut) {
if (!json) {
// Why: report the server's effective budget — it clamps large values, so the requested one would overstate the wait.
+27 -7
View File
@@ -103,20 +103,40 @@ describe.skipIf(process.platform === 'win32')('RuntimeClient', () => {
await new Promise<void>((resolve) => server.listen(endpoint, resolve))
writeMetadata(userDataPath, endpoint)
const priorLaunchToken = process.env.ORCA_AGENT_LAUNCH_TOKEN
process.env.ORCA_AGENT_LAUNCH_TOKEN = 'launch-secret'
const client = new RuntimeClient(userDataPath, 500)
await client.call(
'orchestration.send',
{ subject: 'hello' },
{
orchestrationRequestId: 'mutation_explicit'
try {
await client.call(
'orchestration.send',
{ subject: 'hello' },
{
orchestrationRequestId: 'mutation_explicit'
}
)
await client.call('orchestration.taskList', {})
const secondClient = new RuntimeClient(userDataPath, 500)
await secondClient.call('orchestration.taskList', {})
} finally {
if (priorLaunchToken === undefined) {
delete process.env.ORCA_AGENT_LAUNCH_TOKEN
} else {
process.env.ORCA_AGENT_LAUNCH_TOKEN = priorLaunchToken
}
)
await client.call('orchestration.taskList', {})
}
expect(requests[0]?.method).toBe('status.get')
expect(requests[0]?.compatibilityInvocationId).toBeUndefined()
expect(requests[1]?.orchestrationRequestId).toBe('mutation_explicit')
expect(requests[1]?.orchestrationContractVersion).toBe(1)
expect(requests[1]?.compatibilityInvocationId).toBe('mutation_explicit')
expect(requests[1]?.orchestrationCompatibilityEvidence).toMatchObject({
launchToken: 'launch-secret'
})
expect(requests[2]?.orchestrationRequestId).toBeUndefined()
expect(requests[2]?.compatibilityInvocationId).not.toBe(requests[1]?.compatibilityInvocationId)
expect(requests[3]?.method).toBe('orchestration.taskList')
expect(requests[3]?.compatibilityInvocationId).not.toBe(requests[1]?.compatibilityInvocationId)
})
it('rejects an old local runtime before sending an orchestration mutation', async () => {
+14 -17
View File
@@ -20,6 +20,8 @@ import {
ORCHESTRATION_CONTRACT_VERSION,
RUNTIME_PROTOCOL_VERSION
} from '../../shared/protocol-version'
import { createOrchestrationCompatibilityEnvelope } from './orchestration-compatibility-envelope'
import { getTimeoutMsParam, isWaitingCheck } from './runtime-request-timeout'
// Why: for long-poll methods the caller's method-level
// `params.timeoutMs` is the inner waiter budget; we extend the client-side
@@ -43,6 +45,9 @@ export class RuntimeClient {
private readonly environmentSelector: string | null
private remoteCompatChecked = false
private orchestrationContractCheck: Promise<void> | null = null
private readonly orchestrationCompatibility = createOrchestrationCompatibilityEnvelope(
process.env
)
// Why: browser commands trigger first-time session init (agent-browser connect +
// CDP proxy setup) which can take 15-30s. 60s accommodates cold start without
@@ -76,12 +81,20 @@ export class RuntimeClient {
const orchestrationRequestId = orchestrationMutation
? (options?.orchestrationRequestId ?? randomUUID())
: undefined
const compatibilityEnvelope = method.startsWith('orchestration.')
? {
...this.orchestrationCompatibility,
compatibilityInvocationId:
orchestrationRequestId ?? this.orchestrationCompatibility.compatibilityInvocationId
}
: {}
const envelope = {
orchestrationCapability: options?.orchestrationCapability,
orchestrationContractVersion: method.startsWith('orchestration.')
? ORCHESTRATION_CONTRACT_VERSION
: undefined,
orchestrationRequestId
orchestrationRequestId,
...compatibilityEnvelope
}
if (this.remotePairing) {
if (method !== 'status.get') {
@@ -327,19 +340,3 @@ function resolveRemotePairing(
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}
function isWaitingCheck(params: unknown): boolean {
return (
typeof params === 'object' &&
params !== null &&
'wait' in params &&
(params as { wait: unknown }).wait === true
)
}
function getTimeoutMsParam(params: unknown): unknown {
if (typeof params !== 'object' || params === null || !('timeoutMs' in params)) {
return undefined
}
return (params as { timeoutMs?: unknown }).timeoutMs
}
@@ -0,0 +1,12 @@
import { randomUUID } from 'node:crypto'
import { readOrchestrationCompatibilityEvidence } from '../../shared/orchestration-compatibility-evidence'
import type { RuntimeOrchestrationEnvelope } from '../../shared/runtime-rpc-envelope'
export function createOrchestrationCompatibilityEnvelope(
env: NodeJS.ProcessEnv
): RuntimeOrchestrationEnvelope {
return {
compatibilityInvocationId: randomUUID(),
orchestrationCompatibilityEvidence: readOrchestrationCompatibilityEvidence(env)
}
}
@@ -0,0 +1,15 @@
export function isWaitingCheck(params: unknown): boolean {
return (
typeof params === 'object' &&
params !== null &&
'wait' in params &&
(params as { wait: unknown }).wait === true
)
}
export function getTimeoutMsParam(params: unknown): unknown {
if (typeof params !== 'object' || params === null || !('timeoutMs' in params)) {
return undefined
}
return (params as { timeoutMs?: unknown }).timeoutMs
}
+3 -1
View File
@@ -188,7 +188,9 @@ export async function sendRequest<TResult>(
params,
orchestrationCapability: envelope?.orchestrationCapability,
orchestrationContractVersion: envelope?.orchestrationContractVersion,
orchestrationRequestId: envelope?.orchestrationRequestId
orchestrationRequestId: envelope?.orchestrationRequestId,
compatibilityInvocationId: envelope?.compatibilityInvocationId,
orchestrationCompatibilityEvidence: envelope?.orchestrationCompatibilityEvidence
})}\n`
)
})
+26
View File
@@ -0,0 +1,26 @@
import { describe, expect, it } from 'vitest'
import { RuntimeRpcFailureError } from './types'
describe('RuntimeRpcFailureError compatibility redaction', () => {
it('removes compatibility evidence from CLI JSON errors', () => {
const error = new RuntimeRpcFailureError({
id: 'request-1',
ok: false,
error: {
code: 'compatibility_rejected',
message: 'Rejected',
data: {
orchestrationCompatibilityEvidence: {
launchToken: 'launch-secret'
}
}
},
_meta: { runtimeId: 'runtime-1' }
})
expect(error.data).toEqual({
orchestrationCompatibilityEvidence: '[redacted]'
})
expect(JSON.stringify(error.response)).not.toContain('launch-secret')
})
})
+9 -2
View File
@@ -1,4 +1,5 @@
import type { RuntimeRpcFailure } from '../../shared/runtime-rpc-envelope'
import { redactOrchestrationCompatibilitySecrets } from '../../shared/orchestration-compatibility-evidence'
export type {
RuntimeRpcFailure,
@@ -15,7 +16,7 @@ export class RuntimeClientError extends Error {
constructor(code: string, message: string, data?: unknown) {
super(message)
this.code = code
this.data = data
this.data = redactOrchestrationCompatibilitySecrets(data)
}
}
@@ -25,6 +26,12 @@ export class RuntimeRpcFailureError extends RuntimeClientError {
constructor(response: RuntimeRpcFailure) {
// Why: all client errors expose recovery through the same inherited channel.
super(response.error.code, response.error.message, response.error.data)
this.response = response
this.response = {
...response,
error: {
...response.error,
...(response.error.data === undefined ? {} : { data: this.data })
}
}
}
}
+6 -2
View File
@@ -16,8 +16,12 @@ export const ORCHESTRATION_COMMAND_SPECS: CommandSpec[] = [
{
path: ['orchestration', 'run-use'],
summary: 'Bind this coordinator terminal to an existing Run',
usage: 'orca orchestration run-use --id <run_id> [--from <handle>] [--json]',
allowedFlags: [...GLOBAL_FLAGS, 'id', 'from', 'retry-request']
usage:
'orca orchestration run-use --id <run_id> [--from <handle>] [--takeover-legacy] [--json]',
allowedFlags: [...GLOBAL_FLAGS, 'id', 'from', 'takeover-legacy', 'retry-request'],
notes: [
'--takeover-legacy must run in the live coordinator agent terminal it binds; it preserves existing worker assignments.'
]
},
{
path: ['orchestration', 'run-current'],
@@ -0,0 +1,223 @@
import { createHash } from 'node:crypto'
import { afterEach, describe, expect, it } from 'vitest'
import type { AgentHookEventPayload } from '../../shared/agent-hook-listener'
import { makePaneKey } from '../../shared/stable-pane-id'
import { AgentHookServer } from './server'
const PANE_KEY = makePaneKey('tab-authority', '11111111-1111-4111-8111-111111111111')
const SECOND_PANE_KEY = makePaneKey('tab-authority-2', '22222222-2222-4222-8222-222222222222')
describe('AgentHookServer authority evidence', () => {
const servers: AgentHookServer[] = []
afterEach(() => {
for (const server of servers) {
server.stop()
}
servers.length = 0
})
it('freezes pre-listen commitments separately from current-runtime observations', async () => {
const server = new AgentHookServer()
servers.push(server)
const hydrated = {
paneKey: PANE_KEY,
launchToken: 'launch-before-restart',
tabId: 'tab-authority',
worktreeId: 'repo::before',
connectionId: 'ssh-target',
payload: { state: 'working', prompt: 'before', agentType: 'codex' },
receivedAt: 100,
stateStartedAt: 100
} satisfies AgentHookEventPayload & { receivedAt: number; stateStartedAt: number }
server._getStateForTests().lastStatusByPaneKey.set(PANE_KEY, hydrated)
await server.start()
const commitments = server.getHydratedAuthorityCommitments()
expect(commitments).toEqual([
{
paneKey: PANE_KEY,
launchTokenHash: expect.stringMatching(/^[a-f0-9]{64}$/),
tabId: 'tab-authority',
worktreeId: 'repo::before',
connectionId: 'ssh-target',
observedAt: 100
}
])
expect(Object.isFrozen(commitments)).toBe(true)
expect(Object.isFrozen(commitments[0])).toBe(true)
expect(server.getCurrentAuthorityObservations()).toEqual([])
expect(
server.attestCompatibilityAuthority({
paneKey: PANE_KEY,
launchTokenHash: createHash('sha256').update('launch-before-restart').digest('hex'),
connectionId: 'ssh-target',
terminalProvenance: 'restored'
})
).toEqual({ paneKey: PANE_KEY, source: 'hydrated_commitment' })
server.ingestRemote(
{
paneKey: PANE_KEY,
launchToken: 'launch-after-restart',
tabId: 'tab-authority',
worktreeId: 'repo::after',
payload: { state: 'working', prompt: 'after', agentType: 'codex' }
},
'ssh-target'
)
expect(server.getHydratedAuthorityCommitments()).toBe(commitments)
expect(JSON.stringify(commitments)).not.toContain('launch-before-restart')
expect(server.getCurrentAuthorityObservations()).toEqual([
expect.objectContaining({
paneKey: PANE_KEY,
launchTokenHash: expect.stringMatching(/^[a-f0-9]{64}$/),
connectionId: 'ssh-target',
worktreeId: 'repo::after'
})
])
expect(JSON.stringify(server.getCurrentAuthorityObservations())).not.toContain(
'launch-after-restart'
)
expect(
server.attestCompatibilityAuthority({
paneKey: PANE_KEY,
launchTokenHash: createHash('sha256').update('launch-before-restart').digest('hex'),
connectionId: 'ssh-target',
terminalProvenance: 'restored'
})
).toBeNull()
expect(
server.attestCompatibilityAuthority({
paneKey: PANE_KEY,
launchTokenHash: createHash('sha256').update('launch-before-restart').digest('hex'),
connectionId: 'ssh-target',
terminalProvenance: 'current_runtime'
})
).toBeNull()
expect(
server.attestCompatibilityAuthority({
paneKey: PANE_KEY,
launchTokenHash: createHash('sha256').update('launch-after-restart').digest('hex'),
connectionId: 'ssh-target',
terminalProvenance: 'restored'
})
).toBeNull()
expect(
server.attestCompatibilityAuthority({
paneKey: PANE_KEY,
launchTokenHash: createHash('sha256').update('launch-after-restart').digest('hex'),
connectionId: 'ssh-target',
terminalProvenance: 'current_runtime'
})
).toEqual({ paneKey: PANE_KEY, source: 'current_hook' })
server.ingestRemote(
{
paneKey: PANE_KEY,
launchToken: 'launch-before-restart',
tabId: 'tab-authority',
worktreeId: 'repo::current',
payload: { state: 'working', prompt: 'current', agentType: 'codex' }
},
'ssh-target'
)
expect(
server.attestCompatibilityAuthority({
paneKey: PANE_KEY,
launchTokenHash: createHash('sha256').update('launch-before-restart').digest('hex'),
connectionId: 'ssh-target',
terminalProvenance: 'restored'
})
).toEqual({ paneKey: PANE_KEY, source: 'current_hook' })
server.ingestRemote(
{
paneKey: SECOND_PANE_KEY,
launchToken: 'launch-before-restart',
tabId: 'tab-authority-2',
payload: { state: 'working', prompt: 'duplicate', agentType: 'codex' }
},
'ssh-target'
)
expect(
server.attestCompatibilityAuthority({
paneKey: PANE_KEY,
launchTokenHash: createHash('sha256').update('launch-before-restart').digest('hex'),
connectionId: 'ssh-target',
terminalProvenance: 'restored'
})
).toBeNull()
server.clearStatusEntriesForConnection('ssh-target')
expect(server.getHydratedAuthorityCommitments()).toBe(commitments)
expect(server.getCurrentAuthorityObservations()).toEqual([])
expect(
server.attestCompatibilityAuthority({
paneKey: PANE_KEY,
launchTokenHash: createHash('sha256').update('launch-before-restart').digest('hex'),
connectionId: 'ssh-target',
terminalProvenance: 'restored'
})
).toEqual({ paneKey: PANE_KEY, source: 'hydrated_commitment' })
server.clearPaneState(PANE_KEY)
expect(server.getHydratedAuthorityCommitments()).toBe(commitments)
expect(
server.attestCompatibilityAuthority({
paneKey: PANE_KEY,
launchTokenHash: createHash('sha256').update('launch-before-restart').digest('hex'),
connectionId: 'ssh-target',
terminalProvenance: 'restored'
})
).toBeNull()
})
it('preserves hydrated authority when stale alias cleanup does not own the stable pane', async () => {
const server = new AgentHookServer()
servers.push(server)
const launchToken = 'launch-before-restart'
const launchTokenHash = createHash('sha256').update(launchToken).digest('hex')
const hydrated = {
paneKey: PANE_KEY,
launchToken,
tabId: 'tab-authority',
worktreeId: 'repo::before',
connectionId: 'ssh-target',
payload: { state: 'working', prompt: 'before', agentType: 'codex' },
receivedAt: 100,
stateStartedAt: 100
} satisfies AgentHookEventPayload & { receivedAt: number; stateStartedAt: number }
server._getStateForTests().lastStatusByPaneKey.set(PANE_KEY, hydrated)
server.registerPaneKeyAlias('tab-authority:0', PANE_KEY, 'old-pty')
await server.start()
server.ingestRemote(
{
paneKey: PANE_KEY,
launchToken,
tabId: 'tab-authority',
worktreeId: 'repo::current',
payload: { state: 'working', prompt: 'current', agentType: 'codex' }
},
'ssh-target'
)
server.clearPaneKeyAliasesForPty('old-pty', { shouldClearStablePaneKey: () => false })
expect(
server.attestCompatibilityAuthority({
paneKey: PANE_KEY,
launchTokenHash,
connectionId: 'ssh-target',
terminalProvenance: 'restored'
})
).toEqual({ paneKey: PANE_KEY, source: 'current_hook' })
})
})
+143 -1
View File
@@ -1,6 +1,7 @@
/* eslint-disable max-lines -- Why: this suite exercises the full hook HTTP surface (Claude/Codex/Gemini parsing, transcript chunked scan, paneKey dispatch) and keeping the scenarios co-located avoids fixture drift across files. */
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { execFileSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import {
existsSync,
mkdirSync,
@@ -57,6 +58,7 @@ const TAB_A_PANE = makePaneKey('tab-A', LEAF_5)
type Body = {
paneKey: string
launchToken?: string
tabId?: string
worktreeId?: string
env?: string
@@ -6254,7 +6256,10 @@ describe('Last-status persistence', () => {
try {
await postHookEvent(
server,
buildBody({ hook_event_name: 'UserPromptSubmit', prompt: 'persist me' })
buildBody(
{ hook_event_name: 'UserPromptSubmit', prompt: 'persist me' },
{ launchToken: 'launch-bearer-must-not-persist' }
)
)
// Synchronous flush via stop() captures the trailing-debounced write.
server.flushStatusPersistSync()
@@ -6269,11 +6274,148 @@ describe('Last-status persistence', () => {
stateStartedAt: expect.any(Number),
payload: expect.objectContaining({ state: 'working', prompt: 'persist me' })
})
expect(file.entries[PANE].launchToken).toBeUndefined()
expect(file.entries[PANE].launchTokenHash).toBe(
createHash('sha256').update('launch-bearer-must-not-persist').digest('hex')
)
expect(readFileSync(lastStatusPath(), 'utf8')).not.toContain('launch-bearer-must-not-persist')
} finally {
server.stop()
}
})
it('scrubs a legacy persisted launch bearer while retaining its authority commitment', async () => {
mkdirSync(join(userDataPath, 'agent-hooks'), { recursive: true })
const receivedAt = recentTs()
const launchToken = 'legacy-launch-bearer'
writeFileSync(
lastStatusPath(),
JSON.stringify({
version: 2,
entries: {
[PANE]: {
paneKey: PANE,
launchToken,
tabId: 'tab-1',
worktreeId: 'wt-1',
connectionId: null,
receivedAt,
stateStartedAt: receivedAt,
payload: {
state: 'working',
prompt: 'legacy worker',
agentType: 'codex'
}
}
}
}),
'utf8'
)
const server = new AgentHookServer()
await server.start({ env: 'production', userDataPath })
try {
const launchTokenHash = createHash('sha256').update(launchToken).digest('hex')
expect(server.getHydratedAuthorityCommitments()).toEqual([
expect.objectContaining({ paneKey: PANE, launchTokenHash })
])
expect(server.getStatusSnapshotForPane(PANE)[0]?.launchToken).toBeUndefined()
const persisted = readFileSync(lastStatusPath(), 'utf8')
expect(persisted).not.toContain(launchToken)
expect(JSON.parse(persisted).entries[PANE]).toMatchObject({ launchTokenHash })
} finally {
server.stop()
}
const restartedServer = new AgentHookServer()
await restartedServer.start({ env: 'production', userDataPath })
try {
expect(restartedServer.getHydratedAuthorityCommitments()).toEqual([
expect.objectContaining({
paneKey: PANE,
launchTokenHash: createHash('sha256').update(launchToken).digest('hex')
})
])
expect(restartedServer.getStatusSnapshotForPane(PANE)[0]?.launchToken).toBeUndefined()
} finally {
restartedServer.stop()
}
})
it('persists SSH authority through transient clears until explicit retirement', async () => {
mkdirSync(join(userDataPath, 'agent-hooks'), { recursive: true })
const receivedAt = recentTs()
const launchToken = 'retained-ssh-launch-bearer'
const launchTokenHash = createHash('sha256').update(launchToken).digest('hex')
writeFileSync(
lastStatusPath(),
JSON.stringify({
version: 2,
entries: {
[PANE]: {
paneKey: PANE,
launchTokenHash,
tabId: 'tab-1',
worktreeId: 'wt-1',
connectionId: 'ssh-target',
receivedAt,
stateStartedAt: receivedAt,
payload: {
state: 'working',
prompt: 'retained SSH worker',
agentType: 'codex'
}
}
}
}),
'utf8'
)
const first = new AgentHookServer()
await first.start({ env: 'production', userDataPath })
first.clearStatusEntriesForConnection('ssh-target')
first.flushStatusPersistSync()
first.stop()
const afterClear = JSON.parse(readFileSync(lastStatusPath(), 'utf8'))
expect(afterClear.entries).toEqual({})
expect(afterClear.authorityCommitments[PANE]).toMatchObject({
paneKey: PANE,
launchTokenHash,
connectionId: 'ssh-target'
})
const restored = new AgentHookServer()
await restored.start({ env: 'production', userDataPath })
expect(
restored.attestCompatibilityAuthority({
paneKey: PANE,
launchTokenHash,
connectionId: 'ssh-target',
terminalProvenance: 'restored'
})
).toEqual({ paneKey: PANE, source: 'hydrated_commitment' })
restored.retirePaneAuthority(PANE)
restored.flushStatusPersistSync()
restored.stop()
const retired = new AgentHookServer()
await retired.start({ env: 'production', userDataPath })
try {
expect(
retired.attestCompatibilityAuthority({
paneKey: PANE,
launchTokenHash,
connectionId: 'ssh-target',
terminalProvenance: 'restored'
})
).toBeNull()
} finally {
retired.stop()
}
})
it('persists and hydrates Pi session identity without creating status telemetry', async () => {
const firstServer = new AgentHookServer()
const firstRendererListener = vi.fn()
+364 -18
View File
@@ -79,6 +79,22 @@ type EnrichedAgentHookEventPayload = AgentHookEventPayload & {
stateStartedAt: number
}
type PersistedAgentHookEventPayload = Omit<
EnrichedAgentHookEventPayload,
'launchToken' | 'promptInteractionKey'
> & {
launchTokenHash?: string
}
type PersistedAgentHookAuthorityCommitment = {
paneKey: string
launchTokenHash: string
connectionId: string | null
tabId?: string
worktreeId?: string
observedAt: number
}
export type AgentHookStatusChangeEntry = {
state: AgentStatusState
receivedAt: number
@@ -92,6 +108,20 @@ export type AgentHookProviderSessionIdentity = {
worktreeId?: string
}
export type AgentHookAuthorityEvidence = Readonly<{
paneKey: string
launchTokenHash: string
connectionId: string | null
tabId?: string
worktreeId?: string
observedAt: number
}>
export type AgentHookAuthorityAttestation = Readonly<{
paneKey: string
source: 'current_hook' | 'hydrated_commitment'
}>
type StatusChangeListener = (statuses: AgentHookStatusChangeEntry[]) => void
type ProviderSessionChangeListener = (providerSessions: AgentHookProviderSessionIdentity[]) => void
type PaneStatusClearListener = (clear: AgentStatusClearIpcPayload) => void
@@ -128,7 +158,8 @@ export const PANE_KEY_ALIASES_MAX = 1024
type LastStatusFile = {
version: number
entries: Record<string, EnrichedAgentHookEventPayload>
entries: Record<string, PersistedAgentHookEventPayload>
authorityCommitments?: Record<string, PersistedAgentHookAuthorityCommitment>
}
type AgentPromptSentDedupeEntry = {
@@ -251,7 +282,6 @@ function sanitizeHydratedEntry(
}
return {
paneKey,
launchToken: typeof record.launchToken === 'string' ? record.launchToken : undefined,
tabId: typeof tabId === 'string' ? tabId : undefined,
worktreeId: typeof worktreeId === 'string' ? worktreeId : undefined,
connectionId,
@@ -268,6 +298,63 @@ function sanitizeHydratedEntry(
}
}
function readPersistedLaunchTokenHash(rawEntry: unknown): string | null {
if (typeof rawEntry !== 'object' || rawEntry === null) {
return null
}
const record = rawEntry as Record<string, unknown>
const launchTokenHash =
typeof record.launchTokenHash === 'string' ? record.launchTokenHash.trim() : ''
if (/^[a-f0-9]{64}$/.test(launchTokenHash)) {
return launchTokenHash
}
const legacyLaunchToken = typeof record.launchToken === 'string' ? record.launchToken.trim() : ''
return legacyLaunchToken ? createHash('sha256').update(legacyLaunchToken).digest('hex') : null
}
function sanitizePersistedAuthorityCommitment(
paneKey: string,
value: unknown
): AgentHookAuthorityEvidence | null {
if (!isValidPaneKey(paneKey) || typeof value !== 'object' || value === null) {
return null
}
const record = value as Record<string, unknown>
const launchTokenHash =
typeof record.launchTokenHash === 'string' ? record.launchTokenHash.trim() : ''
const connectionId = record.connectionId
const observedAt = record.observedAt
if (
!/^[a-f0-9]{64}$/.test(launchTokenHash) ||
(connectionId !== null && typeof connectionId !== 'string') ||
typeof observedAt !== 'number' ||
!Number.isFinite(observedAt)
) {
return null
}
return Object.freeze({
paneKey,
launchTokenHash,
connectionId,
...(typeof record.tabId === 'string' ? { tabId: record.tabId } : {}),
...(typeof record.worktreeId === 'string' ? { worktreeId: record.worktreeId } : {}),
observedAt
})
}
function authorityCommitmentsMatch(
left: AgentHookAuthorityEvidence,
right: AgentHookAuthorityEvidence
): boolean {
return (
left.paneKey === right.paneKey &&
left.launchTokenHash === right.launchTokenHash &&
left.connectionId === right.connectionId &&
left.tabId === right.tabId &&
left.worktreeId === right.worktreeId
)
}
function toAgentStatusIpcPayload(entry: EnrichedAgentHookEventPayload): AgentStatusIpcPayload {
return {
paneKey: entry.paneKey,
@@ -479,6 +566,11 @@ export class AgentHookServer {
private state: HookListenerState = createHookListenerState()
// Why: hydrated rows give UI continuity but aren't evidence of live agent work in this runtime.
private runtimeObservedStatusPaneKeys = new Set<string>()
private hydratedAuthorityCommitments: readonly AgentHookAuthorityEvidence[] = Object.freeze([])
private hydratedLaunchTokenHashByPaneKey = new Map<string, string>()
private persistedAuthorityCommitmentsByPaneKey = new Map<string, AgentHookAuthorityEvidence>()
private revokedHydratedAuthorityCommitments = new WeakSet<AgentHookAuthorityEvidence>()
private currentAuthorityObservations = new Map<string, AgentHookAuthorityEvidence>()
private legacyPaneKeyAliases = new Map<string, PaneKeyAliasEntry>()
private paneKeyAliasPersistenceListener: PaneKeyAliasPersistenceListener | null = null
// Why: on-disk last-status cache path; null without a userDataPath (tests), where persistence is a no-op and only in-memory replay applies.
@@ -562,6 +654,53 @@ export class AgentHookServer {
return entry ? [toAgentStatusIpcPayload(entry as EnrichedAgentHookEventPayload)] : []
}
getHydratedAuthorityCommitments(): readonly AgentHookAuthorityEvidence[] {
return this.hydratedAuthorityCommitments
}
getCurrentAuthorityObservations(): readonly AgentHookAuthorityEvidence[] {
return Object.freeze(
Array.from(this.currentAuthorityObservations.values(), (entry) => Object.freeze({ ...entry }))
)
}
attestCompatibilityAuthority(candidate: {
paneKey: string
launchTokenHash: string
connectionId: string | null
terminalProvenance: 'current_runtime' | 'restored'
}): AgentHookAuthorityAttestation | null {
const paneKey = this.resolvePaneKeyAlias(candidate.paneKey)
const matchesCandidate = (entry: AgentHookAuthorityEvidence): boolean =>
entry.launchTokenHash === candidate.launchTokenHash &&
entry.connectionId === candidate.connectionId
const commitments = this.hydratedAuthorityCommitments.filter(
(entry) => matchesCandidate(entry) && !this.revokedHydratedAuthorityCommitments.has(entry)
)
const current = Array.from(this.currentAuthorityObservations.values())
const observations = current.filter(matchesCandidate)
const paneObservations = current.filter(
(entry) => this.resolvePaneKeyAlias(entry.paneKey) === paneKey
)
const hasUniqueCurrentObservation =
observations.length === 1 &&
paneObservations.length === 1 &&
this.resolvePaneKeyAlias(observations[0]!.paneKey) === paneKey
if (candidate.terminalProvenance === 'current_runtime') {
return hasUniqueCurrentObservation ? Object.freeze({ paneKey, source: 'current_hook' }) : null
}
if (commitments.length !== 1 || this.resolvePaneKeyAlias(commitments[0]!.paneKey) !== paneKey) {
return null
}
if (observations.length === 0 && paneObservations.length === 0) {
return Object.freeze({ paneKey, source: 'hydrated_commitment' })
}
if (!hasUniqueCurrentObservation) {
return null
}
return Object.freeze({ paneKey, source: 'current_hook' })
}
inferInterrupt(request: AgentInterruptInferenceRequest): boolean {
if (!isValidPaneKey(request.paneKey)) {
return false
@@ -1301,9 +1440,40 @@ export class AgentHookServer {
tabId: owner?.tabId
})
}
const hydratedLaunchTokenHash = this.hydratedLaunchTokenHashByPaneKey.get(previousOwnerPaneKey)
if (hydratedLaunchTokenHash) {
this.hydratedLaunchTokenHashByPaneKey.delete(previousOwnerPaneKey)
this.hydratedLaunchTokenHashByPaneKey.set(toPaneKey, hydratedLaunchTokenHash)
}
const persistedAuthority = this.persistedAuthorityCommitmentsByPaneKey.get(previousOwnerPaneKey)
if (persistedAuthority) {
const owner = parsePaneKey(toPaneKey)
this.persistedAuthorityCommitmentsByPaneKey.delete(previousOwnerPaneKey)
this.persistedAuthorityCommitmentsByPaneKey.set(
toPaneKey,
Object.freeze({
...persistedAuthority,
paneKey: toPaneKey,
...(owner?.tabId ? { tabId: owner.tabId } : {})
})
)
}
if (this.runtimeObservedStatusPaneKeys.delete(previousOwnerPaneKey)) {
this.runtimeObservedStatusPaneKeys.add(toPaneKey)
}
const authorityObservation = this.currentAuthorityObservations.get(previousOwnerPaneKey)
if (authorityObservation) {
const owner = parsePaneKey(toPaneKey)
this.currentAuthorityObservations.delete(previousOwnerPaneKey)
this.currentAuthorityObservations.set(
toPaneKey,
Object.freeze({
...authorityObservation,
paneKey: toPaneKey,
tabId: owner?.tabId
})
)
}
const promptDedupe = this.promptSentDedupeByPaneKey.get(previousOwnerPaneKey)
if (promptDedupe !== undefined) {
this.promptSentDedupeByPaneKey.delete(previousOwnerPaneKey)
@@ -1321,7 +1491,7 @@ export class AgentHookServer {
this.boundPaneKeyAliases()
this.closedAgentStatusPaneKeys.delete(toPaneKey)
this.notifyPaneKeyAliasPersistenceListener()
if (hadStatus) {
if (hadStatus || persistedAuthority) {
this.scheduleStatusPersist()
this.notifyStatusChangeListeners()
}
@@ -1339,6 +1509,7 @@ export class AgentHookServer {
aliasChanged = true
}
}
const authorityChanged = this.revokeHydratedAuthorityForPaneKeys(paneKeys)
const hadStatus = [...paneKeys].some((key) => this.state.lastStatusByPaneKey.has(key))
for (const key of paneKeys) {
this.markPaneClosedForAgentStatus(key)
@@ -1346,12 +1517,13 @@ export class AgentHookServer {
this.clearCodexSubagentPoll(key)
clearPaneCacheState(this.state, key)
this.runtimeObservedStatusPaneKeys.delete(key)
this.currentAuthorityObservations.delete(key)
this.promptSentDedupeByPaneKey.delete(key)
}
if (aliasChanged) {
this.notifyPaneKeyAliasPersistenceListener()
}
if (hadStatus) {
if (hadStatus || authorityChanged) {
this.scheduleStatusPersist()
this.notifyStatusChangeListeners()
}
@@ -1366,11 +1538,19 @@ export class AgentHookServer {
const clearedStatusPaneKeys = new Set<string>()
for (const [legacyPaneKey, entry] of this.legacyPaneKeyAliases) {
if (entry.ptyId === ptyId) {
this.legacyPaneKeyAliases.delete(legacyPaneKey)
clearPaneCacheState(this.state, legacyPaneKey)
this.promptSentDedupeByPaneKey.delete(legacyPaneKey)
const shouldClearStablePaneKey =
options?.shouldClearStablePaneKey?.(entry.stablePaneKey) ?? true
const revokedPaneKeys = new Set([legacyPaneKey])
if (shouldClearStablePaneKey) {
revokedPaneKeys.add(entry.stablePaneKey)
}
if (this.revokeHydratedAuthorityForPaneKeys(revokedPaneKeys)) {
statusChanged = true
}
this.legacyPaneKeyAliases.delete(legacyPaneKey)
clearPaneCacheState(this.state, legacyPaneKey)
this.currentAuthorityObservations.delete(legacyPaneKey)
this.promptSentDedupeByPaneKey.delete(legacyPaneKey)
if (shouldClearStablePaneKey && this.state.lastStatusByPaneKey.has(entry.stablePaneKey)) {
statusChanged = true
clearedStatusPaneKeys.add(entry.stablePaneKey)
@@ -1379,6 +1559,7 @@ export class AgentHookServer {
// Why: hydrated rows live under the stable key; if this PTY dies before ptyPaneKey rebuilds, alias cleanup is the only evictor.
clearPaneCacheState(this.state, entry.stablePaneKey)
this.runtimeObservedStatusPaneKeys.delete(entry.stablePaneKey)
this.currentAuthorityObservations.delete(entry.stablePaneKey)
this.promptSentDedupeByPaneKey.delete(entry.stablePaneKey)
}
aliasChanged = true
@@ -1400,6 +1581,27 @@ export class AgentHookServer {
return this.legacyPaneKeyAliases.get(paneKey)?.stablePaneKey ?? paneKey
}
private revokeHydratedAuthorityForPaneKeys(paneKeys: ReadonlySet<string>): boolean {
let changed = false
for (const commitment of this.hydratedAuthorityCommitments) {
if (
paneKeys.has(commitment.paneKey) ||
paneKeys.has(this.resolvePaneKeyAlias(commitment.paneKey))
) {
this.revokedHydratedAuthorityCommitments.add(commitment)
changed = true
}
}
for (const paneKey of paneKeys) {
const resolvedPaneKey = this.resolvePaneKeyAlias(paneKey)
changed = this.hydratedLaunchTokenHashByPaneKey.delete(paneKey) || changed
changed = this.hydratedLaunchTokenHashByPaneKey.delete(resolvedPaneKey) || changed
changed = this.persistedAuthorityCommitmentsByPaneKey.delete(paneKey) || changed
changed = this.persistedAuthorityCommitmentsByPaneKey.delete(resolvedPaneKey) || changed
}
return changed
}
private normalizeHookBodyPaneKeyAlias(body: unknown): unknown {
if (typeof body !== 'object' || body === null) {
return body
@@ -1602,6 +1804,7 @@ export class AgentHookServer {
isReplay: envelope.isReplay === true ? true : undefined,
payload: normalizedPayload
}
this.recordCurrentAuthorityObservation(event)
this.applyNormalizedStatus(event)
}
@@ -1632,6 +1835,7 @@ export class AgentHookServer {
if (this.lastStatusFilePath) {
this.hydrateLastStatusFromDisk()
}
this.captureHydratedAuthorityCommitments()
const handleRequest = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {
if (req.method !== 'POST') {
res.writeHead(404)
@@ -1673,6 +1877,7 @@ export class AgentHookServer {
const aliasedBody = this.normalizeHookBodyPaneKeyAlias(body)
const normalized = normalizeHookPayload(this.state, source, aliasedBody, this.env)
if (normalized && !this.shouldSuppressClosedTabStatus(normalized.paneKey)) {
this.recordCurrentAuthorityObservation(normalized)
const enriched = this.applyNormalizedStatus(normalized)
this.scheduleAssistantMessageRetry(source, aliasedBody, enriched)
this.scheduleCodexSubagentPoll(source, aliasedBody, enriched)
@@ -1739,6 +1944,11 @@ export class AgentHookServer {
this.lastStatusFilePath = null
this.lastWrittenJson = null
this.runtimeObservedStatusPaneKeys.clear()
this.hydratedAuthorityCommitments = Object.freeze([])
this.hydratedLaunchTokenHashByPaneKey.clear()
this.persistedAuthorityCommitmentsByPaneKey.clear()
this.revokedHydratedAuthorityCommitments = new WeakSet()
this.currentAuthorityObservations.clear()
this.promptSentDedupeByPaneKey.clear()
this.closedAgentStatusTabIds.clear()
this.closedAgentStatusPaneKeys.clear()
@@ -1750,7 +1960,7 @@ export class AgentHookServer {
/** Drop only the status row (user dismissal); do NOT wipe prompt/tool caches since the pane's agent may still be alive. Use clearPaneState for PTY-teardown. */
dropStatusEntry(paneKey: string): void {
if (!this.deleteStatusEntry(paneKey)) {
if (!this.deleteStatusEntry(paneKey, { preserveAuthority: true })) {
return
}
this.scheduleStatusPersist()
@@ -1775,7 +1985,7 @@ export class AgentHookServer {
if (entry.connectionId !== normalizedConnectionId) {
continue
}
const deleted = this.deleteStatusEntry(paneKey)
const deleted = this.deleteStatusEntry(paneKey, { preserveAuthority: true })
if (deleted) {
statusChanged = true
if (deleted.payload.agentType === 'codex') {
@@ -1785,6 +1995,11 @@ export class AgentHookServer {
}
}
}
for (const [paneKey, evidence] of this.currentAuthorityObservations) {
if (evidence.connectionId === normalizedConnectionId) {
this.currentAuthorityObservations.delete(paneKey)
}
}
if (statusChanged) {
// Why: persist/notify once — one disconnect can own many panes.
this.scheduleStatusPersist()
@@ -1798,7 +2013,10 @@ export class AgentHookServer {
})
}
private deleteStatusEntry(paneKey: string): EnrichedAgentHookEventPayload | null {
private deleteStatusEntry(
paneKey: string,
options?: { preserveAuthority?: boolean }
): EnrichedAgentHookEventPayload | null {
const resolvedPaneKey = this.resolvePaneKeyAlias(paneKey)
const existing = this.state.lastStatusByPaneKey.get(resolvedPaneKey) as
| EnrichedAgentHookEventPayload
@@ -1807,9 +2025,14 @@ export class AgentHookServer {
return null
}
this.state.lastStatusByPaneKey.delete(resolvedPaneKey)
if (!options?.preserveAuthority) {
this.hydratedLaunchTokenHashByPaneKey.delete(resolvedPaneKey)
this.persistedAuthorityCommitmentsByPaneKey.delete(resolvedPaneKey)
}
this.clearAssistantMessageRetry(resolvedPaneKey)
this.clearCodexSubagentPoll(resolvedPaneKey)
this.runtimeObservedStatusPaneKeys.delete(resolvedPaneKey)
this.currentAuthorityObservations.delete(resolvedPaneKey)
if (existing.payload.state === 'done') {
this.promptSentDedupeByPaneKey.delete(resolvedPaneKey)
}
@@ -1854,6 +2077,11 @@ export class AgentHookServer {
paneKeysToClear.add(paneKey)
}
}
for (const commitment of this.hydratedAuthorityCommitments) {
if (paneCacheKeyMatchesTab(commitment.paneKey, tabId)) {
paneKeysToClear.add(commitment.paneKey)
}
}
let aliasChanged = false
for (const [legacyPaneKey, entry] of this.legacyPaneKeyAliases) {
@@ -1867,6 +2095,7 @@ export class AgentHookServer {
aliasChanged = true
}
}
const authorityChanged = this.revokeHydratedAuthorityForPaneKeys(paneKeysToClear)
let statusChanged = false
for (const paneKey of paneKeysToClear) {
@@ -1877,12 +2106,13 @@ export class AgentHookServer {
this.clearCodexSubagentPoll(paneKey)
clearPaneCacheState(this.state, paneKey)
this.runtimeObservedStatusPaneKeys.delete(paneKey)
this.currentAuthorityObservations.delete(paneKey)
this.promptSentDedupeByPaneKey.delete(paneKey)
}
if (aliasChanged) {
this.notifyPaneKeyAliasPersistenceListener()
}
if (statusChanged) {
if (statusChanged || authorityChanged) {
this.scheduleStatusPersist()
this.notifyStatusChangeListeners()
}
@@ -1890,25 +2120,31 @@ export class AgentHookServer {
clearPaneState(paneKey: string): void {
const resolvedPaneKey = this.resolvePaneKeyAlias(paneKey)
const paneKeys = new Set([paneKey, resolvedPaneKey])
// Why: only persist when a status entry was actually evicted; dropping prompt/tool caches doesn't change the file.
const hadStatus = this.state.lastStatusByPaneKey.has(resolvedPaneKey)
this.clearAssistantMessageRetry(resolvedPaneKey)
this.clearCodexSubagentPoll(resolvedPaneKey)
clearPaneCacheState(this.state, resolvedPaneKey)
this.currentAuthorityObservations.delete(resolvedPaneKey)
this.promptSentDedupeByPaneKey.delete(resolvedPaneKey)
let clearedAlias = false
for (const [legacyPaneKey, stablePaneKey] of this.legacyPaneKeyAliases) {
if (stablePaneKey.stablePaneKey === resolvedPaneKey) {
this.legacyPaneKeyAliases.delete(legacyPaneKey)
paneKeys.add(legacyPaneKey)
paneKeys.add(stablePaneKey.stablePaneKey)
clearPaneCacheState(this.state, legacyPaneKey)
this.currentAuthorityObservations.delete(legacyPaneKey)
this.promptSentDedupeByPaneKey.delete(legacyPaneKey)
clearedAlias = true
}
}
const authorityChanged = this.revokeHydratedAuthorityForPaneKeys(paneKeys)
if (clearedAlias) {
this.notifyPaneKeyAliasPersistenceListener()
}
if (hadStatus) {
if (hadStatus || authorityChanged) {
this.runtimeObservedStatusPaneKeys.delete(resolvedPaneKey)
this.scheduleStatusPersist()
this.notifyStatusChangeListeners()
@@ -1963,6 +2199,8 @@ export class AgentHookServer {
}
// Why: keep hydrate idempotent so a future re-start path can't merge prior-session state.
this.state.lastStatusByPaneKey.clear()
this.hydratedLaunchTokenHashByPaneKey.clear()
this.persistedAuthorityCommitmentsByPaneKey.clear()
let raw: string
try {
raw = readFileSync(this.lastStatusFilePath, 'utf8')
@@ -2001,6 +2239,7 @@ export class AgentHookServer {
let hydrated = 0
let dropped = 0
let prunedLegacyClaudeSubagents = 0
let scrubbedLegacyLaunchTokens = 0
// Why: drop entries older than HYDRATE_MAX_AGE_MS to bound disk growth (one Date.now() for a consistent cutoff).
const ttlCutoff = Date.now() - HYDRATE_MAX_AGE_MS
for (const [paneKey, rawEntry] of Object.entries(entries)) {
@@ -2011,6 +2250,21 @@ export class AgentHookServer {
: { ...(rawEntry as Record<string, unknown>), paneKey: resolvedPaneKey }
const entry = sanitizeHydratedEntry(resolvedPaneKey, rawResolvedEntry)
if (entry && entry.receivedAt >= ttlCutoff) {
const launchTokenHash = readPersistedLaunchTokenHash(rawResolvedEntry)
if (launchTokenHash) {
this.hydratedLaunchTokenHashByPaneKey.set(resolvedPaneKey, launchTokenHash)
const evidence = this.toAuthorityEvidence(entry, launchTokenHash)
if (evidence) {
this.persistedAuthorityCommitmentsByPaneKey.set(resolvedPaneKey, evidence)
}
}
if (
typeof rawResolvedEntry === 'object' &&
rawResolvedEntry !== null &&
typeof (rawResolvedEntry as Record<string, unknown>).launchToken === 'string'
) {
scrubbedLegacyLaunchTokens += 1
}
const hydratedPayload = dropHydratedIdleClaudeSubagents(entry.payload)
if (hydratedPayload !== entry.payload) {
prunedLegacyClaudeSubagents +=
@@ -2041,13 +2295,30 @@ export class AgentHookServer {
dropped += 1
}
}
for (const [paneKey, rawCommitment] of Object.entries(file.authorityCommitments ?? {})) {
const resolvedPaneKey = this.resolvePaneKeyAlias(paneKey)
const commitment = sanitizePersistedAuthorityCommitment(resolvedPaneKey, rawCommitment)
if (!commitment || commitment.observedAt < ttlCutoff) {
dropped += 1
continue
}
const existing = this.persistedAuthorityCommitmentsByPaneKey.get(resolvedPaneKey)
if (existing && !authorityCommitmentsMatch(existing, commitment)) {
this.persistedAuthorityCommitmentsByPaneKey.delete(resolvedPaneKey)
this.hydratedLaunchTokenHashByPaneKey.delete(resolvedPaneKey)
dropped += 1
continue
}
this.persistedAuthorityCommitmentsByPaneKey.set(resolvedPaneKey, commitment)
this.hydratedLaunchTokenHashByPaneKey.set(resolvedPaneKey, commitment.launchTokenHash)
}
if (dropped > 0) {
console.warn(
`[agent-hooks] last-status hydrate dropped ${dropped} entries (kept ${hydrated})`
)
}
if (dropped > 0 || prunedLegacyClaudeSubagents > 0) {
// Why: persist load-time pruning once so legacy idle rows aren't re-parsed every launch.
if (dropped > 0 || prunedLegacyClaudeSubagents > 0 || scrubbedLegacyLaunchTokens > 0) {
// Why: persist load-time pruning and bearer scrubbing once.
this.runStatusPersist()
} else if (hydrated > 0) {
// Why: prime dedup from raw bytes (not re-serialized) only when hydration was lossless.
@@ -2055,17 +2326,92 @@ export class AgentHookServer {
}
}
private captureHydratedAuthorityCommitments(): void {
this.revokedHydratedAuthorityCommitments = new WeakSet()
for (const entry of this.state.lastStatusByPaneKey.values()) {
const evidence = this.toAuthorityEvidence(
entry as EnrichedAgentHookEventPayload,
this.hydratedLaunchTokenHashByPaneKey.get(entry.paneKey)
)
if (evidence && !this.persistedAuthorityCommitmentsByPaneKey.has(entry.paneKey)) {
this.persistedAuthorityCommitmentsByPaneKey.set(entry.paneKey, evidence)
}
}
this.hydratedAuthorityCommitments = Object.freeze(
Array.from(this.persistedAuthorityCommitmentsByPaneKey.values())
)
}
private recordCurrentAuthorityObservation(payload: AgentHookEventPayload): void {
const evidence = this.toAuthorityEvidence(payload)
if (evidence) {
this.currentAuthorityObservations.set(evidence.paneKey, evidence)
this.persistedAuthorityCommitmentsByPaneKey.set(evidence.paneKey, evidence)
this.hydratedLaunchTokenHashByPaneKey.set(evidence.paneKey, evidence.launchTokenHash)
}
}
private toAuthorityEvidence(
payload: AgentHookEventPayload | EnrichedAgentHookEventPayload,
launchTokenHashOverride?: string
): AgentHookAuthorityEvidence | null {
const launchToken = payload.launchToken?.trim()
const launchTokenHash =
launchTokenHashOverride ??
(launchToken ? createHash('sha256').update(launchToken).digest('hex') : null)
if (!launchTokenHash) {
return null
}
return Object.freeze({
paneKey: payload.paneKey,
launchTokenHash,
connectionId: payload.connectionId,
...(payload.tabId ? { tabId: payload.tabId } : {}),
...(payload.worktreeId ? { worktreeId: payload.worktreeId } : {}),
observedAt: 'receivedAt' in payload ? payload.receivedAt : Date.now()
})
}
private serializeStatusFile(): string {
const entries: Record<string, EnrichedAgentHookEventPayload> = {}
const entries: Record<string, PersistedAgentHookEventPayload> = {}
const authorityCommitments: Record<string, PersistedAgentHookAuthorityCommitment> = {}
const conflictedCommitments = new Set<string>()
for (const [paneKey, commitment] of this.persistedAuthorityCommitmentsByPaneKey) {
authorityCommitments[paneKey] = { ...commitment }
}
for (const [paneKey, payload] of this.state.lastStatusByPaneKey) {
// Why: never persist invalid keys (matches the hydrate-path invariant).
if (!isValidPaneKey(paneKey)) {
continue
}
const { promptInteractionKey: _promptInteractionKey, ...persistedPayload } = payload
entries[paneKey] = persistedPayload as EnrichedAgentHookEventPayload
const {
promptInteractionKey: _promptInteractionKey,
launchToken,
...persistedPayload
} = payload as EnrichedAgentHookEventPayload
const launchTokenHash = launchToken?.trim()
? createHash('sha256').update(launchToken.trim()).digest('hex')
: this.hydratedLaunchTokenHashByPaneKey.get(paneKey)
entries[paneKey] = {
...persistedPayload,
...(launchTokenHash ? { launchTokenHash } : {})
}
const commitment = this.toAuthorityEvidence(payload, launchTokenHash)
if (commitment && !conflictedCommitments.has(paneKey)) {
const existing = authorityCommitments[paneKey]
if (existing && !authorityCommitmentsMatch(existing, commitment)) {
delete authorityCommitments[paneKey]
conflictedCommitments.add(paneKey)
} else {
authorityCommitments[paneKey] = { ...commitment }
}
}
}
const file: LastStatusFile = {
version: LAST_STATUS_FILE_VERSION,
entries,
authorityCommitments
}
const file: LastStatusFile = { version: LAST_STATUS_FILE_VERSION, entries }
return JSON.stringify(file)
}
@@ -11,4 +11,41 @@ describe('packaged Windows CLI launcher asset', () => {
expect(launcher).toContain('orca.cmd cannot safely forward orchestration message bodies')
expect(launcher).not.toContain('"%ELECTRON%" "%CLI%" %*')
})
it('marks the packaged child and propagates its exact exit status', () => {
const sourcePath = join(process.cwd(), 'native', 'windows-cli-launcher', 'OrcaCliLauncher.cs')
const source = readFileSync(sourcePath, 'utf8')
expect(source).toContain(
'startInfo.EnvironmentVariables["ORCA_WINDOWS_PACKAGED_CLI_LAUNCHER"] = "1";'
)
expect(source).toContain('Environment.GetEnvironmentVariable("ORCA_CLI_COMMAND") == "orca-ide"')
expect(source).toContain('? "orca-ide"')
expect(source).toContain(': "orca";')
expect(source).toContain('child.WaitForExit();')
expect(source).toContain('return child.ExitCode;')
})
it('requires exact updater process and executable release proof', () => {
const includePath = join(process.cwd(), 'config', 'nsis', 'daemon-host-uninstall.nsh')
const source = readFileSync(includePath, 'utf8')
expect(source).toContain('!macro customCheckAppRunning')
expect(source).toContain('!include "getProcessInfo.nsh"')
expect(source).toContain('Var pid')
expect(source).toContain('[String]::Equals')
expect(source).toContain('"$INSTDIR\\Orca.exe" "$INSTDIR\\resources\\bin\\orca.exe"')
expect(source).toContain('CreationDate=[string]$$parent.CreationDate')
expect(source).toContain("'ProcessId=' + $$proof.ProcessId")
expect(source).toContain('[IO.FileShare]::None')
expect(source).toContain('$$released=$$false')
expect(source).toContain('$$released=$$true')
expect(source).not.toMatch(/(?<!\$)(?:\$\$)*\$(?:false|true)\b/)
expect(source).toContain(
' ${else}\n !insertmacro IS_POWERSHELL_AVAILABLE\n !insertmacro _CHECK_APP_RUNNING\n ${endIf}'
)
expect(source).toContain('SetErrorLevel 2')
expect(source).toContain('Quit')
expect(source).not.toContain('taskkill /IM Orca.exe')
})
})
+10
View File
@@ -299,6 +299,16 @@ export class HeadlessEmulator {
return lines
}
getBufferTailLines(limit: number): string[] {
const buffer = this.terminal.buffer.active
const start = Math.max(0, buffer.length - Math.max(0, Math.floor(limit)))
const lines: string[] = []
for (let row = start; row < buffer.length; row += 1) {
lines.push(buffer.getLine(row)?.translateToString(true) ?? '')
}
return lines
}
getCwd(): string | null {
return this.oscText.cwd
}
@@ -1,4 +1,11 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import type * as WslModule from '../wsl'
vi.mock('../wsl', async (importOriginal) => ({
...(await importOriginal<typeof WslModule>()),
getDefaultWslDistro: () => 'Ubuntu'
}))
import { TerminalHost } from './terminal-host'
import type { SubprocessHandle } from './session'
import { resolveWslSessionContext } from './wsl-session-context'
@@ -60,6 +67,40 @@ describe('TerminalHost WSL context', () => {
}
})
it('persists the resolved default distro across daemon attaches', async () => {
const spawnSubprocess = vi.fn(() => createSubprocess())
host = new TerminalHost({ spawnSubprocess })
const platform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
try {
const created = await host.createOrAttach({
sessionId: 'session-default-wsl',
cols: 80,
rows: 24,
cwd: 'C:\\Users\\jin\\repo',
shellOverride: 'wsl.exe',
terminalWindowsWslDistro: null,
streamClient: { onData: vi.fn(), onExit: vi.fn() }
})
const attached = await host.createOrAttach({
sessionId: 'session-default-wsl',
cols: 80,
rows: 24,
shellOverride: 'wsl.exe',
terminalWindowsWslDistro: 'Debian',
streamClient: { onData: vi.fn(), onExit: vi.fn() }
})
expect(created.wslDistro).toBe('Ubuntu')
expect(attached.wslDistro).toBe('Ubuntu')
expect(spawnSubprocess).toHaveBeenCalledOnce()
} finally {
if (platform) {
Object.defineProperty(process, 'platform', platform)
}
}
})
it('returns authoritative null when a native session is attached with a WSL preference', async () => {
const spawnSubprocess = vi.fn(() => createSubprocess())
host = new TerminalHost({ spawnSubprocess })
@@ -118,6 +159,13 @@ describe('TerminalHost WSL context', () => {
terminalWindowsWslDistro: ' Ubuntu '
})
).toEqual({ distro: 'Ubuntu', treatPosixCwdAsWsl: true })
expect(
resolveWslSessionContext({
cwd: 'C:\\Users\\jin\\repo',
shellOverride: 'wsl.exe',
terminalWindowsWslDistro: null
})
).toEqual({ distro: 'Ubuntu', treatPosixCwdAsWsl: true })
} finally {
if (platform) {
Object.defineProperty(process, 'platform', platform)
+3 -2
View File
@@ -1,6 +1,6 @@
import { splitWorktreeIdForFilesystem } from '../../shared/worktree-id'
import { isWslShellName } from '../../shared/local-windows-terminal-runtime'
import { parseWslPath } from '../wsl'
import { getDefaultWslDistro, parseWslPath } from '../wsl'
import { parsePtySessionId } from './pty-session-id'
import { parseWslUncPath } from '../../shared/wsl-paths'
@@ -41,7 +41,8 @@ export function resolveWslSessionContext(args: {
return (
(args.sessionId ? getWslContextFromSessionId(args.sessionId) : undefined) ??
(isWslShellName(args.shellOverride)
? getWslContextFromPreferredDistro(args.terminalWindowsWslDistro)
? (getWslContextFromPreferredDistro(args.terminalWindowsWslDistro) ??
getWslContextFromPreferredDistro(getDefaultWslDistro()))
: undefined)
)
}
+30 -2
View File
@@ -30,7 +30,12 @@ import {
getSshPtyProvider,
registerHeadlessPtyRuntime
} from './ipc/pty'
import { initDaemonPtyProvider, disconnectDaemon, shutdownDaemon } from './daemon/daemon-init'
import {
initDaemonPtyProvider,
disconnectDaemon,
getDaemonProvider,
shutdownDaemon
} from './daemon/daemon-init'
import { closeAllWatchers } from './ipc/filesystem-watcher'
import { disposeWorktreeBaseDirectoryWatchers } from './ipc/worktree-base-directory-watcher'
import { registerCoreHandlers } from './ipc/register-core-handlers'
@@ -132,6 +137,7 @@ import {
import { maybeRedirectAppImageCliLaunch } from './startup/appimage-cli-redirect'
import { maybeRedirectPackagedCliEntryLaunch } from './startup/packaged-cli-entry-redirect'
import { startFirstWindowStartupServices } from './startup/first-window-startup-services'
import { recoverLegacyWorkerTerminalsForRendererStartup } from './startup/legacy-worker-renderer-recovery'
import { createWslCliReconciliationStartupBarrier } from './startup/wsl-cli-reconciliation-startup-barrier'
import { getDevInstanceIdentity } from './startup/dev-instance-identity'
import { hydrateShellPath, mergePathSegments } from './startup/hydrate-shell-path'
@@ -724,10 +730,24 @@ if (hasSingleInstanceLock) {
}
ipcMain.handle('app:awaitFirstWindowStartupServices', async () => {
// Why: restored WSL terminals get a bounded chance to receive launcher repairs before window rendering proceeds.
await Promise.all([firstWindowStartupServicesReady, managedWslCliStartupBarrierReady])
})
ipcMain.handle('app:recoverLegacyWorkerTerminalsForRendererStartup', () =>
recoverLegacyWorkerTerminalsForRendererStartup({
firstWindowStartupServicesReady,
managedWslCliStartupBarrierReady,
localPtyProviderStartupReady,
reconcile: async () => {
await runtime?.refreshRestoredOrchestrationAuthority()
return runtime?.reconcileLegacyWorkerTerminals({ materializeRenderer: true })
},
onDeferredRecoveryError: (error) => {
console.warn('[orchestration] legacy worker provider-ready recovery failed', error)
}
})
)
// Why: the renderer pulls this once its ui:openSettings listener attaches, so a Settings request queued before mount isn't lost.
ipcMain.handle('ui:consumePendingOpenSettings', (event) =>
pendingOpenSettings.matches(event.sender.id, { consume: true })
@@ -2207,6 +2227,11 @@ void app.whenReady().then(async () => {
getAgentProviderSessionSnapshot: () => agentHookServer.getStatusSnapshot(),
getAgentProviderSessionRowsForPane: (paneKey) =>
agentHookServer.getStatusSnapshotForPane(paneKey),
attestAgentHookCompatibilityAuthority: (candidate) =>
agentHookServer.attestCompatibilityAuthority(candidate),
retireAgentHookCompatibilityAuthority: (paneKey) =>
agentHookServer.retirePaneAuthority(paneKey),
canRecoverPersistentLocalPtys: () => getDaemonProvider() !== null,
// Why: source codex-home here (runs in window AND serve) so aiVault.listSessions includes managed-Codex sessions; registerCoreHandlers is window-only.
getAdditionalAiVaultCodexHomePaths: () =>
codexRuntimeHome ? codexRuntimeHome.getHostCodexHomePathsForSessionDiscovery() : [],
@@ -2222,6 +2247,7 @@ void app.whenReady().then(async () => {
orchestrationEnvironmentTransport
})
runtime = runtimeService
runtimeService.prepareLegacyWorkerTerminalRecovery()
publishProviderSessionChanges(agentHookServer.getProviderSessionIdentities())
browserManager.setBrowserGuestStateChangedListener((worktreeId) => {
runtimeService.notifyMobileSessionTabsChanged(worktreeId)
@@ -2664,6 +2690,8 @@ void app.whenReady().then(async () => {
store,
prepareCodexSessionResumeForLaunch
)
await runtime.refreshRestoredOrchestrationAuthority()
await runtime.reconcileLegacyWorkerTerminals()
// Why: headless servers can't mount <webview> panes; use offscreen WebContents, gated on a real display so browser.headless.v1 stays honest.
if (headlessBrowserDisplayAvailable) {
runtime.setOffscreenBrowserBackend(new OffscreenBrowserBackend(browserManager))
+89
View File
@@ -3599,6 +3599,64 @@ describe('registerPtyHandlers', () => {
})
})
it('resolves default WSL authority before daemon host env and spawn metadata', async () => {
await withWin32Platform(async () => {
_setWslCachesForTests({ available: true, distros: ['Ubuntu'] })
const daemonSpawn = setupDaemonAdapter()
const runtime = {
setPtyController: vi.fn(),
registerPty: vi.fn(),
onPtySpawned: vi.fn(),
onPtyExit: vi.fn(),
onPtyData: vi.fn(),
preparePtyExecutionContext: vi.fn().mockReturnValue(true),
getOrchestrationCompatibilityHostId: vi.fn(() => 'compat-host')
}
const settings = {
terminalWindowsShell: 'wsl.exe',
terminalWindowsWslDistro: null,
terminalWindowsPowerShellImplementation: 'auto'
}
handlers.clear()
registerPtyHandlers(
mainWindow as never,
runtime as never,
undefined,
(() => settings) as never
)
const controller = runtime.setPtyController.mock.calls[0]?.[0] as {
spawn(args: {
cols: number
rows: number
cwd?: string
worktreeId?: string
env?: Record<string, string>
}): Promise<{ id: string }>
}
await controller.spawn({
cols: 80,
rows: 24,
cwd: 'C:\\repo',
worktreeId: 'repo-1::C:\\repo',
env: {}
})
const spawnOptions = daemonSpawn.mock.calls.at(-1)?.[0] as DaemonSpawnCall
expect(spawnOptions.terminalWindowsWslDistro).toBe('Ubuntu')
expect(spawnOptions.env).toMatchObject({
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_KIND: 'wsl',
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_ID: 'compat-host',
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION: 'Ubuntu'
})
expect(runtime.preparePtyExecutionContext).toHaveBeenCalledWith(
expect.any(String),
'Ubuntu',
expect.objectContaining({ resetIncarnation: true })
)
})
})
it('distinguishes an attached native context from an older daemon fallback', async () => {
await withWin32Platform(async () => {
_setWslCachesForTests({ available: true, distros: ['Ubuntu'] })
@@ -5374,6 +5432,37 @@ describe('registerPtyHandlers', () => {
expect(listProcesses).not.toHaveBeenCalled()
})
it('scopes runtime inventories to the requested provider', async () => {
const localList = vi
.spyOn(getLocalPtyProvider(), 'listProcesses')
.mockResolvedValue([{ id: 'local-pty', title: 'Local', cwd: '/local' }])
const sshAList = vi.fn(async () => [{ id: 'ssh-a-pty' }])
const sshBList = vi.fn(async () => {
throw new Error('ssh-b unavailable')
})
registerSshPtyProvider('ssh-a', { listProcesses: sshAList } as never)
registerSshPtyProvider('ssh-b', { listProcesses: sshBList } as never)
const runtime = { setPtyController: vi.fn() }
handlers.clear()
registerPtyHandlers(mainWindow as never, runtime as never)
const controller = runtime.setPtyController.mock.calls[0]?.[0] as {
listProcesses(connectionId?: string | null): Promise<{ id: string }[]>
}
await expect(controller.listProcesses(null)).resolves.toEqual([
{ id: 'local-pty', title: 'Local', cwd: '/local' }
])
expect(localList).toHaveBeenCalledOnce()
expect(sshAList).not.toHaveBeenCalled()
expect(sshBList).not.toHaveBeenCalled()
await expect(controller.listProcesses('ssh-a')).resolves.toEqual([{ id: 'ssh-a-pty' }])
expect(sshAList).toHaveBeenCalledOnce()
expect(sshBList).not.toHaveBeenCalled()
await expect(controller.listProcesses()).rejects.toThrow('ssh-b unavailable')
})
it('returns unavailable runtime confirmation for unsupported or missing providers', async () => {
registerSshPtyProvider('ssh-1', {} as never)
setPtyOwnership('unsupported-pty', 'ssh-1')
+72 -54
View File
@@ -140,7 +140,7 @@ import {
} from '../agent-hooks/migration-unsupported-pty-state'
import { parseWslPath } from '../wsl'
import { mergePersistedWindowsPath } from '../pty/windows-environment-path'
import { addOrcaWslInteropEnv } from '../pty/wsl-orca-env'
import { addOrcaWslInteropEnv, stampWslOrchestrationCompatibilityHost } from '../pty/wsl-orca-env'
import { PtyProducerFlowController } from './pty-producer-flow-control'
import { beginTerminalInstall } from './watcher-removal-gate'
import {
@@ -1787,6 +1787,11 @@ export function registerPtyHandlers(
if (preAllocatedHandle) {
env.ORCA_TERMINAL_HANDLE = preAllocatedHandle
}
stampWslOrchestrationCompatibilityHost(
env,
runtime?.getOrchestrationCompatibilityHostId?.(),
ctx?.isWsl === true ? ctx.wslDistro : null
)
if (ctx?.isWsl === true) {
addOrcaWslInteropEnv(env)
}
@@ -3362,10 +3367,35 @@ export function registerPtyHandlers(
})
: { shellOverride: undefined, terminalWindowsWslDistro: null }
const daemonShellOverride = terminalRuntimeOptions.shellOverride
const isDaemonHostSpawn =
!args.connectionId &&
!(provider instanceof LocalPtyProvider) &&
!routesFreshSpawnsToLocalProvider(provider)
const callerRequestedSessionId = args.sessionId?.trim()
const requestedSessionId =
callerRequestedSessionId ??
(isDaemonHostSpawn && args.agentSessionCreateOperationId
? ptySessionIdForAgentCreateOperation(args.worktreeId, args.agentSessionCreateOperationId)
: undefined)
const sessionId =
requestedSessionId ?? (isDaemonHostSpawn ? mintPtySessionId(args.worktreeId) : undefined)
const effectiveSessionRelayId =
sessionId !== undefined ? getRelayPtyId(args.connectionId, sessionId) : undefined
const effectiveSessionAppId =
sessionId !== undefined ? getAppPtyId(args.connectionId, sessionId) : undefined
const isMintedSessionId = callerRequestedSessionId === undefined && isDaemonHostSpawn
const expectedWslDistro = !args.connectionId
? (resolveWslSessionContext({
cwd,
sessionId,
shellOverride: terminalRuntimeOptions.shellOverride,
terminalWindowsWslDistro: terminalRuntimeOptions.terminalWindowsWslDistro
})?.distro ?? null)
: null
const codexSelectionTarget = getCodexSelectionTargetForPty(
daemonShellOverride,
cwd,
terminalRuntimeOptions.terminalWindowsWslDistro ?? null
expectedWslDistro
)
const codexResumePreparation = prepareCodexResumeHome({
connectionId: args.connectionId,
@@ -3393,31 +3423,6 @@ export function registerPtyHandlers(
)
}
const isDaemonHostSpawn =
!args.connectionId &&
!(provider instanceof LocalPtyProvider) &&
!routesFreshSpawnsToLocalProvider(provider)
const callerRequestedSessionId = args.sessionId?.trim()
const requestedSessionId =
callerRequestedSessionId ??
(isDaemonHostSpawn && args.agentSessionCreateOperationId
? ptySessionIdForAgentCreateOperation(args.worktreeId, args.agentSessionCreateOperationId)
: undefined)
const sessionId =
requestedSessionId ?? (isDaemonHostSpawn ? mintPtySessionId(args.worktreeId) : undefined)
const effectiveSessionRelayId =
sessionId !== undefined ? getRelayPtyId(args.connectionId, sessionId) : undefined
const effectiveSessionAppId =
sessionId !== undefined ? getAppPtyId(args.connectionId, sessionId) : undefined
const isMintedSessionId = callerRequestedSessionId === undefined && isDaemonHostSpawn
const expectedWslDistro = !args.connectionId
? (resolveWslSessionContext({
cwd,
sessionId,
shellOverride: terminalRuntimeOptions.shellOverride,
terminalWindowsWslDistro: terminalRuntimeOptions.terminalWindowsWslDistro
})?.distro ?? null)
: null
const shouldPersistHostSessionBinding = args.persistHostSessionBinding === true
let hostSessionBinding: {
store: NonNullable<typeof store>
@@ -3492,11 +3497,16 @@ export function registerPtyHandlers(
launchAgent: isTuiAgent(args.launchAgent) ? args.launchAgent : undefined,
shellPath: daemonShellOverride ?? process.env.COMSPEC,
isWsl: shouldSkipCodexHomeEnvForWindowsShell(daemonShellOverride, cwd),
wslDistro: codexSelectionTarget.runtime === 'wsl' ? codexSelectionTarget.wslDistro : null,
wslDistro: codexSelectionTarget.runtime === 'wsl' ? expectedWslDistro : null,
agentStatusHooksEnabled: isAgentStatusHooksEnabled(getSettings?.()),
networkProxySettings: getSettings?.(),
deferGitConfigGuardToDaemon: provider.supportsGitCredentialGuardHost?.(sessionId) === true
})
stampWslOrchestrationCompatibilityHost(
env,
runtime?.getOrchestrationCompatibilityHostId?.(),
codexSelectionTarget.runtime === 'wsl' ? expectedWslDistro : null
)
promoteAgentTeamsShimPath(env, requestedAgentTeamsPath)
}
@@ -3596,8 +3606,7 @@ export function registerPtyHandlers(
}
if (process.platform === 'win32' && !args.connectionId) {
spawnOptions.shellOverride = terminalRuntimeOptions.shellOverride
spawnOptions.terminalWindowsWslDistro =
terminalRuntimeOptions.terminalWindowsWslDistro ?? null
spawnOptions.terminalWindowsWslDistro = expectedWslDistro
spawnOptions.terminalWindowsPowerShellImplementation = getSettings
? (getSettings()?.terminalWindowsPowerShellImplementation ?? 'auto')
: undefined
@@ -4243,7 +4252,13 @@ export function registerPtyHandlers(
return null
}
},
listProcesses: async () => {
listProcesses: async (connectionId) => {
if (connectionId === null) {
return localProvider.listProcesses()
}
if (connectionId !== undefined) {
return getProvider(connectionId).listProcesses()
}
const providerSessions = await Promise.all([
localProvider.listProcesses(),
...Array.from(sshProviders.values(), (provider) => provider.listProcesses())
@@ -4450,24 +4465,6 @@ export function registerPtyHandlers(
})
: { shellOverride: args.shellOverride, terminalWindowsWslDistro: null }
const initialShellOverride = terminalRuntimeOptions.shellOverride
const initialSelectionTarget = getCodexSelectionTargetForPty(
initialShellOverride,
cwd,
terminalRuntimeOptions.terminalWindowsWslDistro ?? null
)
const claudeAuth =
isClaudeLaunch && prepareClaudeAuth ? await prepareClaudeAuth(initialSelectionTarget) : null
spawnTiming.mark('auth')
if (isClaudeLaunch && isClaudeAuthSwitchInProgress()) {
throw new Error('A Claude account switch is in progress. Try again after it finishes.')
}
if (claudeAuth?.stripAuthEnv && hasClaudeAuthEnvConflict(args.env)) {
throw new Error(
'This Claude launch defines explicit Anthropic auth environment variables. Remove those overrides before using a managed Claude account.'
)
}
// Why: the daemon-backed provider skips LocalPtyProvider's buildSpawnEnv, so assemble the same host-local env here for parity.
// Safety: skip entirely for SSH — every injection is a loopback secret or a local path that leaks or misleads on the remote host.
const isDaemonHostSpawn =
!args.connectionId &&
!(provider instanceof LocalPtyProvider) &&
@@ -4494,6 +4491,24 @@ export function registerPtyHandlers(
terminalWindowsWslDistro: terminalRuntimeOptions.terminalWindowsWslDistro
})?.distro ?? null)
: null
const initialSelectionTarget = getCodexSelectionTargetForPty(
initialShellOverride,
cwd,
expectedWslDistro
)
const claudeAuth =
isClaudeLaunch && prepareClaudeAuth ? await prepareClaudeAuth(initialSelectionTarget) : null
spawnTiming.mark('auth')
if (isClaudeLaunch && isClaudeAuthSwitchInProgress()) {
throw new Error('A Claude account switch is in progress. Try again after it finishes.')
}
if (claudeAuth?.stripAuthEnv && hasClaudeAuthEnvConflict(args.env)) {
throw new Error(
'This Claude launch defines explicit Anthropic auth environment variables. Remove those overrides before using a managed Claude account.'
)
}
// Why: the daemon-backed provider skips LocalPtyProvider's buildSpawnEnv, so assemble the same host-local env here for parity.
// Safety: skip entirely for SSH — every injection is a loopback secret or a local path that leaks or misleads on the remote host.
const startupTerminalColorQueryReplyColors = getStartupTerminalColorQueryReplyColors(args)
// Why: forward pane env to SSH only when the relay hook path is enabled, or a newer relay could emit statuses this build can't route.
const sshSourceEnv = stripRemotePaneEnvWhenHooksDisabled(args.connectionId, args.env)
@@ -4603,7 +4618,7 @@ export function registerPtyHandlers(
const codexSelectionTarget = getCodexSelectionTargetForPty(
effectiveShellOverride,
cwd,
terminalRuntimeOptions.terminalWindowsWslDistro ?? null
expectedWslDistro
)
const codexResumePreparation = prepareCodexResumeHome({
connectionId: args.connectionId,
@@ -4669,13 +4684,17 @@ export function registerPtyHandlers(
launchAgent: isTuiAgent(args.launchAgent) ? args.launchAgent : undefined,
shellPath: effectiveShellOverride ?? process.env.COMSPEC,
isWsl: shouldSkipCodexHomeEnvForWindowsShell(effectiveShellOverride, cwd),
wslDistro:
codexSelectionTarget.runtime === 'wsl' ? codexSelectionTarget.wslDistro : null,
wslDistro: codexSelectionTarget.runtime === 'wsl' ? expectedWslDistro : null,
agentStatusHooksEnabled: isAgentStatusHooksEnabled(getSettings?.()),
networkProxySettings: getSettings?.(),
deferGitConfigGuardToDaemon:
provider.supportsGitCredentialGuardHost?.(effectiveSessionId) === true
})
stampWslOrchestrationCompatibilityHost(
env,
runtime?.getOrchestrationCompatibilityHostId?.(),
codexSelectionTarget.runtime === 'wsl' ? expectedWslDistro : null
)
promoteAgentTeamsShimPath(env, requestedAgentTeamsPath)
} catch (err) {
// Why: buildPtyHostEnv has fs side-effects (Pi/OMP install); clear per-PTY state on throw, but only minted ids — caller ids may name existing PTYs.
@@ -4762,8 +4781,7 @@ export function registerPtyHandlers(
}
if (process.platform === 'win32' && !args.connectionId) {
// Why: the renderer models PowerShell as one shell family; thread the implementation choice so both PTY paths resolve the same executable.
spawnOptions.terminalWindowsWslDistro =
terminalRuntimeOptions.terminalWindowsWslDistro ?? null
spawnOptions.terminalWindowsWslDistro = expectedWslDistro
spawnOptions.terminalWindowsPowerShellImplementation = getSettings
? (getSettings()?.terminalWindowsPowerShellImplementation ?? 'auto')
: undefined
+15
View File
@@ -3691,6 +3691,21 @@ describe('Store', () => {
expect(hostSession.lastVisitedAtByWorktreeId?.['r1::/path/wt1']).toBeUndefined()
})
it('lists only persisted workspace-session host partitions', async () => {
const store = await createStore()
expect(store.getWorkspaceSessionHostIds()).toEqual(['local'])
store.getWorkspaceSession('ssh:not-persisted')
store.setWorkspaceSession(getDefaultWorkspaceSession(), 'ssh:ssh-a')
store.setWorkspaceSession(getDefaultWorkspaceSession(), 'runtime:environment-a')
expect(store.getWorkspaceSessionHostIds()).toEqual([
'local',
'ssh:ssh-a',
'runtime:environment-a'
])
})
it('removeProject removes the derived project host setup compatibility record', async () => {
const store = await createStore()
store.addRepo(makeRepo({ id: 'r1' }))
+11
View File
@@ -5769,6 +5769,17 @@ export class Store {
return this.state.workspaceSessionsByHostId?.[resolved] ?? getDefaultWorkspaceSession()
}
getWorkspaceSessionHostIds(): ExecutionHostId[] {
const hostIds = new Set<ExecutionHostId>([LOCAL_EXECUTION_HOST_ID])
for (const key of Object.keys(this.state.workspaceSessionsByHostId ?? {})) {
const hostId = normalizeExecutionHostId(key)
if (hostId) {
hostIds.add(hostId)
}
}
return [...hostIds]
}
readTerminalScrollbackSnapshot(ref: string): string | null {
return readTerminalScrollbackSnapshotSync(ref, this.terminalScrollbackSnapshotStorage)
}
@@ -96,6 +96,7 @@ vi.mock('../wsl', () => ({
toLinuxPath: (path: string) => path.replace(/^C:\\/i, '/mnt/c/').replace(/\\/g, '/'),
toWindowsWslPath: (path: string, distro: string) =>
`\\\\wsl.localhost\\${distro}${path.replace(/\//g, '\\')}`,
getDefaultWslDistro: () => 'Ubuntu',
isWslAvailable: () => true,
// Why: WSL worktree validation now asks the distro; these tests use WSL UNC
// cwds that are meant to exist, so report them present without spawning wsl.exe.
@@ -880,6 +881,45 @@ describe('LocalPtyProvider', () => {
expect(spawnCall[2].env.HISTFILE).toContain('terminal-history-wsl/Debian')
})
it('resolves and persists the default distro for Windows cwd WSL terminals', async () => {
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const buildSpawnEnv = vi.fn(
(
_id: string,
env: Record<string, string>,
_ctx?: { isWsl?: boolean; wslDistro?: string | null }
) => env
)
provider.configure({ buildSpawnEnv })
const result = await provider.spawn({
cols: 80,
rows: 24,
worktreeId: 'repo-1::C:\\Users\\jin\\repo',
cwd: 'C:\\Users\\jin\\repo',
shellOverride: 'wsl.exe',
terminalWindowsWslDistro: null
})
const spawnCall = spawnMock.mock.calls.at(-1)!
expect(spawnCall[1]).toEqual([
'-d',
'Ubuntu',
'--',
'sh',
'-c',
expect.stringContaining("cd '/mnt/c/Users/jin/repo'")
])
expect(buildSpawnEnv.mock.calls[0]?.[2]).toMatchObject({
isWsl: true,
wslDistro: 'Ubuntu'
})
expect(result.wslDistro).toBe('Ubuntu')
expect(
(await provider.listProcesses()).find((entry) => entry.id === result.id)?.wslDistro
).toBe('Ubuntu')
})
it('repro: keeps explicit PowerShell 7 selection when the pwsh probe is cold-false', async () => {
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const pwshAvailable = vi.fn(() => false)
+12 -6
View File
@@ -11,7 +11,7 @@ import { buildWindowsPowerShellSpawnAttempts } from './windows-shell-fallback-ch
import { resolveProcessCwd } from './process-cwd'
import { existsSync } from 'node:fs'
import * as pty from 'node-pty'
import { parseWslPath, isWslAvailable } from '../wsl'
import { getDefaultWslDistro, parseWslPath, isWslAvailable } from '../wsl'
import { splitWorktreeIdForFilesystem } from '../../shared/worktree-id'
import {
injectHistoryEnv,
@@ -547,6 +547,10 @@ export class LocalPtyProvider implements IPtyProvider {
process.platform === 'win32'
? getWslContextFromPreferredDistro(args.terminalWindowsWslDistro)
: undefined
let launchWslContext =
wslInfo !== null
? getWslContextFromPreferredDistro(wslInfo.distro)
: (worktreeWslContext ?? preferredWslContext)
let shellPath: string
let shellArgs: string[]
@@ -572,6 +576,9 @@ export class LocalPtyProvider implements IPtyProvider {
process.env.COMSPEC ||
'powershell.exe'
const shellFamily = worktreeWslContext ? 'wsl.exe' : requestedShellFamily
if (!launchWslContext && pathWin32.basename(shellFamily).toLowerCase() === 'wsl.exe') {
launchWslContext = getWslContextFromPreferredDistro(getDefaultWslDistro())
}
const normalizedShellFamily = pathWin32.basename(shellFamily).toLowerCase()
const resolvedGitBashPath = resolveWindowsGitBashShellPath(shellFamily)
// Why: normalize setting-value and path forms to the PowerShell family so the resolver can fall back to inbox powershell.exe.
@@ -606,7 +613,7 @@ export class LocalPtyProvider implements IPtyProvider {
shellPath,
cwd,
defaultCwd,
wslContext: worktreeWslContext ?? preferredWslContext,
wslContext: launchWslContext,
startupCommand: args.command
})
const primaryAttempt = windowsFallbackAttempts[0]
@@ -621,7 +628,7 @@ export class LocalPtyProvider implements IPtyProvider {
shellPath,
cwd,
defaultCwd,
worktreeWslContext ?? preferredWslContext,
launchWslContext,
args.command
)
shellArgs = resolved.shellArgs
@@ -672,8 +679,7 @@ export class LocalPtyProvider implements IPtyProvider {
}
const isWslShell = Boolean(wslInfo) || pathWin32.basename(shellPath).toLowerCase() === 'wsl.exe'
const launchWslDistro =
wslInfo?.distro ?? worktreeWslContext?.distro ?? preferredWslContext?.distro ?? null
const launchWslDistro = isWslShell ? (launchWslContext?.distro ?? null) : null
const finalEnv = this.opts.buildSpawnEnv
? this.opts.buildSpawnEnv(id, spawnEnv, {
command: args.command,
@@ -796,7 +802,7 @@ export class LocalPtyProvider implements IPtyProvider {
let historyResult: ReturnType<typeof injectHistoryEnv> | null = null
if (historyEnabled) {
historyResult = injectHistoryEnv(finalEnv, worktreeId, effectiveShellPath, cwd, {
wslDistro: preferredWslContext?.distro ?? worktreeWslContext?.distro ?? null
wslDistro: launchWslDistro
})
logHistoryInjection(worktreeId, historyResult)
}
+44 -2
View File
@@ -1,5 +1,9 @@
import { describe, expect, it } from 'vitest'
import { addOrcaWslInteropEnv, addWorktreeSetupWslInteropEnv } from './wsl-orca-env'
import {
addOrcaWslInteropEnv,
addWorktreeSetupWslInteropEnv,
stampWslOrchestrationCompatibilityHost
} from './wsl-orca-env'
describe('addOrcaWslInteropEnv', () => {
it('marks the Orca terminal handle for Windows to WSL env import', () => {
@@ -29,10 +33,14 @@ describe('addOrcaWslInteropEnv', () => {
ORCA_PANE_KEY: 'tab-1:leaf-1',
ORCA_TAB_ID: 'tab-1',
ORCA_WORKTREE_ID: 'repo::\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo',
ORCA_AGENT_LAUNCH_TOKEN: 'launch-secret',
ORCA_AGENT_HOOK_PORT: '4567',
ORCA_AGENT_HOOK_TOKEN: 'token',
ORCA_AGENT_HOOK_ENV: 'dev',
ORCA_AGENT_HOOK_VERSION: '1'
ORCA_AGENT_HOOK_VERSION: '1',
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_KIND: 'wsl',
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_ID: 'local',
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION: 'Ubuntu'
}
addOrcaWslInteropEnv(env)
@@ -44,10 +52,44 @@ describe('addOrcaWslInteropEnv', () => {
expect(env.WSLENV).toContain('ORCA_PANE_KEY/u')
expect(env.WSLENV).toContain('ORCA_TAB_ID/u')
expect(env.WSLENV).toContain('ORCA_WORKTREE_ID/u')
expect(env.WSLENV).toContain('ORCA_AGENT_LAUNCH_TOKEN/u')
expect(env.WSLENV).toContain('ORCA_AGENT_HOOK_PORT/u')
expect(env.WSLENV).toContain('ORCA_AGENT_HOOK_TOKEN/u')
expect(env.WSLENV).toContain('ORCA_AGENT_HOOK_ENV/u')
expect(env.WSLENV).toContain('ORCA_AGENT_HOOK_VERSION/u')
expect(env.WSLENV).toContain('ORCA_ORCHESTRATION_COMPATIBILITY_HOST_KIND/u')
expect(env.WSLENV).toContain('ORCA_ORCHESTRATION_COMPATIBILITY_HOST_ID/u')
expect(env.WSLENV).toContain('ORCA_ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION/u')
})
it('overwrites caller host evidence with native runtime WSL authority', () => {
const env = {
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_KIND: 'ssh',
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_ID: 'caller-host',
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION: 'caller-incarnation',
ORCA_ORCHESTRATION_COMPATIBILITY_ATTACHMENT: 'caller-attachment'
}
stampWslOrchestrationCompatibilityHost(env, 'local', 'Ubuntu')
expect(env).toEqual({
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_KIND: 'wsl',
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_ID: 'local',
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION: 'Ubuntu'
})
})
it('clears inherited host evidence outside a runtime-owned WSL scope', () => {
const env = {
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_KIND: 'ssh',
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_ID: 'caller-host',
ORCA_ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION: 'caller-incarnation',
ORCA_ORCHESTRATION_COMPATIBILITY_ATTACHMENT: 'caller-attachment'
}
stampWslOrchestrationCompatibilityHost(env, 'local', null)
expect(env).toEqual({})
})
it('path-translates a Windows hook endpoint but passes a guest-side one untouched', () => {
+29
View File
@@ -1,3 +1,10 @@
import {
ORCHESTRATION_COMPATIBILITY_ATTACHMENT_ENV,
ORCHESTRATION_COMPATIBILITY_HOST_ID_ENV,
ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION_ENV,
ORCHESTRATION_COMPATIBILITY_HOST_KIND_ENV
} from '../../shared/orchestration-compatibility-evidence'
const WSLENV_ENTRY_SEPARATOR = ':'
function parseWslenvEntries(value: string | undefined): string[] {
@@ -70,6 +77,9 @@ export function addOrcaWslInteropEnv(env: Record<string, string>): void {
'ORCA_TAB_ID/u',
'ORCA_WORKTREE_ID/u',
'ORCA_AGENT_LAUNCH_TOKEN/u',
'ORCA_ORCHESTRATION_COMPATIBILITY_HOST_KIND/u',
'ORCA_ORCHESTRATION_COMPATIBILITY_HOST_ID/u',
'ORCA_ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION/u',
'ORCA_AGENT_HOOK_PORT/u',
'ORCA_AGENT_HOOK_TOKEN/u',
'ORCA_AGENT_HOOK_ENV/u',
@@ -84,3 +94,22 @@ export function addOrcaWslInteropEnv(env: Record<string, string>): void {
]
applyWslenvPassthrough(env, passthroughEntries)
}
export function stampWslOrchestrationCompatibilityHost(
env: Record<string, string>,
hostId: string | null | undefined,
distro: string | null | undefined
): void {
delete env[ORCHESTRATION_COMPATIBILITY_HOST_KIND_ENV]
delete env[ORCHESTRATION_COMPATIBILITY_HOST_ID_ENV]
delete env[ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION_ENV]
delete env[ORCHESTRATION_COMPATIBILITY_ATTACHMENT_ENV]
const normalizedHostId = hostId?.trim()
const normalizedDistro = distro?.trim()
if (!normalizedHostId || !normalizedDistro) {
return
}
env[ORCHESTRATION_COMPATIBILITY_HOST_KIND_ENV] = 'wsl'
env[ORCHESTRATION_COMPATIBILITY_HOST_ID_ENV] = normalizedHostId
env[ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION_ENV] = normalizedDistro
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,350 @@
import { createHash } from 'node:crypto'
import { describe, expect, it, vi } from 'vitest'
import { AgentHookServer } from '../agent-hooks/server'
import { OrcaRuntimeService } from './orca-runtime'
const PANE_KEY = '11111111-1111-4111-8111-111111111111:22222222-2222-4222-8222-222222222222'
const TOKEN = 'launch-secret'
const TOKEN_HASH = createHash('sha256').update(TOKEN).digest('hex')
type TerminalAuthorityResolver = {
getOrchestrationDispatchAuthority: (terminalHandle: string) => unknown
restoredOrchestrationAuthorityByPtyId: Map<string, Record<string, unknown>>
}
function createRuntime(
hostScope:
| { kind: 'local'; hostId: 'local' }
| { kind: 'wsl'; hostId: 'local'; distro: string }
| { kind: 'ssh'; targetId: string },
launchTokenHash: string | null = TOKEN_HASH
) {
const runtime = new OrcaRuntimeService(null, undefined, {
attestAgentHookCompatibilityAuthority: ({ paneKey, launchTokenHash, connectionId }) =>
paneKey === PANE_KEY &&
launchTokenHash === TOKEN_HASH &&
connectionId === (hostScope.kind === 'ssh' ? hostScope.targetId : null)
? { paneKey, source: 'hydrated_commitment' }
: null
})
const resolveTerminal = vi.fn(() => ({
runtimeId: 'runtime-1',
terminalHandle: 'term-1',
ptyId: 'pty-1',
worktreeId: 'repo-1::/worktree',
processIncarnation: 'incarnation-1',
paneKey: PANE_KEY,
launchTokenHash,
hostScope
}))
;(runtime as unknown as TerminalAuthorityResolver).getOrchestrationDispatchAuthority =
resolveTerminal
if (launchTokenHash === null) {
;(runtime as unknown as TerminalAuthorityResolver).restoredOrchestrationAuthorityByPtyId.set(
'pty-1',
{
ptyId: 'pty-1',
worktreeId: 'repo-1::/worktree',
terminalHandle: 'term-1',
paneKey: PANE_KEY,
processIncarnation: 'incarnation-1',
hostScope
}
)
}
return runtime
}
describe('orchestration compatibility runtime authority', () => {
it('returns only attested local identity and its token hash', () => {
const runtime = createRuntime({ kind: 'local', hostId: 'local' })
const authority = runtime.verifyOrchestrationCompatibilityCaller({
terminalHandle: 'term-1',
paneKey: PANE_KEY,
launchToken: TOKEN
})
expect(authority).toEqual({
hostScope: { kind: 'local', hostId: 'local' },
paneKey: PANE_KEY,
terminalHandle: 'term-1',
processIncarnation: 'incarnation-1',
launchTokenHash: TOKEN_HASH
})
expect(JSON.stringify(authority)).not.toContain(TOKEN)
expect(
runtime.verifyOrchestrationCompatibilityCaller({
terminalHandle: 'term-1',
paneKey: PANE_KEY,
launchToken: 'wrong'
})
).toBeNull()
expect(
runtime.verifyOrchestrationCompatibilityCaller({
terminalHandle: 'term-1',
paneKey: PANE_KEY,
launchToken: TOKEN_HASH
})
).toBeNull()
})
it('keeps local and WSL authority stable across app runtime generations', () => {
const firstLocal = createRuntime({ kind: 'local', hostId: 'local' })
const secondLocal = createRuntime({ kind: 'local', hostId: 'local' })
const wsl = createRuntime({ kind: 'wsl', hostId: 'local', distro: 'Ubuntu' })
const localEvidence = {
terminalHandle: 'term-1',
paneKey: PANE_KEY,
launchToken: TOKEN
}
const wslEvidence = {
...localEvidence,
host: { kind: 'wsl', hostId: 'local', distro: 'Ubuntu' }
} as const
expect(firstLocal.verifyOrchestrationCompatibilityCaller(localEvidence)?.hostScope).toEqual(
secondLocal.verifyOrchestrationCompatibilityCaller(localEvidence)?.hostScope
)
expect(wsl.verifyOrchestrationCompatibilityCaller(wslEvidence)?.hostScope).toEqual({
kind: 'wsl',
hostId: 'local',
distro: 'Ubuntu'
})
expect(wsl.verifyOrchestrationCompatibilityCaller(localEvidence)).toBeNull()
expect(
wsl.verifyOrchestrationCompatibilityCaller({
...wslEvidence,
host: { kind: 'wsl', hostId: 'runtime-before-restart', distro: 'Ubuntu' }
})
).toBeNull()
})
it('requires a live exact terminal even when the hook proof is hydrated', () => {
const runtime = createRuntime({ kind: 'local', hostId: 'local' })
;(runtime as unknown as TerminalAuthorityResolver).getOrchestrationDispatchAuthority = () =>
null
expect(
runtime.verifyOrchestrationCompatibilityCaller({
terminalHandle: 'term-1',
paneKey: PANE_KEY,
launchToken: TOKEN
})
).toBeNull()
})
it('uses the hydrated hook commitment for a restored exact PTY', () => {
const restored = createRuntime({ kind: 'local', hostId: 'local' }, null)
const uncommitted = new OrcaRuntimeService()
;(uncommitted as unknown as TerminalAuthorityResolver).getOrchestrationDispatchAuthority =
() => ({
runtimeId: 'runtime-1',
terminalHandle: 'term-1',
ptyId: 'pty-1',
worktreeId: 'repo-1::/worktree',
processIncarnation: 'incarnation-1',
paneKey: PANE_KEY,
launchTokenHash: null,
hostScope: { kind: 'local', hostId: 'local' }
})
const evidence = {
terminalHandle: 'term-1',
paneKey: PANE_KEY,
launchToken: TOKEN
}
expect(restored.verifyOrchestrationCompatibilityCaller(evidence)).toMatchObject({
processIncarnation: 'incarnation-1',
launchTokenHash: TOKEN_HASH
})
expect(uncommitted.verifyOrchestrationCompatibilityCaller(evidence)).toBeNull()
expect(
restored.verifyOrchestrationCompatibilityCaller({
...evidence,
launchToken: 'wrong'
})
).toBeNull()
})
it.each([
['PTY', { ptyId: 'pty-other' }],
['worktree', { worktreeId: 'repo-1::/other' }],
['terminal handle', { terminalHandle: 'term-other' }],
[
'pane',
{ paneKey: '33333333-3333-4333-8333-333333333333:44444444-4444-4444-8444-444444444444' }
],
['process incarnation', { processIncarnation: 'incarnation-other' }]
])('rejects a restored receipt with mismatched %s identity', (_field, mismatch) => {
const runtime = createRuntime({ kind: 'local', hostId: 'local' }, null)
const internals = runtime as unknown as TerminalAuthorityResolver
const receipt = internals.restoredOrchestrationAuthorityByPtyId.get('pty-1')!
internals.restoredOrchestrationAuthorityByPtyId.set('pty-1', { ...receipt, ...mismatch })
expect(
runtime.verifyOrchestrationCompatibilityCaller({
terminalHandle: 'term-1',
paneKey: PANE_KEY,
launchToken: TOKEN
})
).toBeNull()
})
it('rejects restored receipts from a different WSL distro or SSH target', () => {
const wsl = createRuntime({ kind: 'wsl', hostId: 'local', distro: 'Ubuntu' }, null)
const wslInternals = wsl as unknown as TerminalAuthorityResolver
const wslReceipt = wslInternals.restoredOrchestrationAuthorityByPtyId.get('pty-1')!
wslInternals.restoredOrchestrationAuthorityByPtyId.set('pty-1', {
...wslReceipt,
hostScope: { kind: 'wsl', hostId: 'local', distro: 'Debian' }
})
expect(
wsl.verifyOrchestrationCompatibilityCaller({
terminalHandle: 'term-1',
paneKey: PANE_KEY,
launchToken: TOKEN,
host: { kind: 'wsl', hostId: 'local', distro: 'Ubuntu' }
})
).toBeNull()
const ssh = createRuntime({ kind: 'ssh', targetId: 'saved-target' }, null)
const sshInternals = ssh as unknown as TerminalAuthorityResolver
const sshReceipt = sshInternals.restoredOrchestrationAuthorityByPtyId.get('pty-1')!
sshInternals.restoredOrchestrationAuthorityByPtyId.set('pty-1', {
...sshReceipt,
hostScope: { kind: 'ssh', targetId: 'other-target' }
})
const host = ssh.registerOrchestrationCompatibilitySshAttachment('saved-target', 'connection-1')
expect(
ssh.verifyOrchestrationCompatibilityCaller({
terminalHandle: 'term-1',
paneKey: PANE_KEY,
launchToken: TOKEN,
host
})
).toBeNull()
})
it('does not fall back to a restored receipt when a fresh launch token mismatches', () => {
const runtime = createRuntime({ kind: 'local', hostId: 'local' })
;(runtime as unknown as TerminalAuthorityResolver).restoredOrchestrationAuthorityByPtyId.set(
'pty-1',
{
ptyId: 'pty-1',
worktreeId: 'repo-1::/worktree',
terminalHandle: 'term-1',
paneKey: PANE_KEY,
processIncarnation: 'incarnation-1',
hostScope: { kind: 'local', hostId: 'local' }
}
)
expect(
runtime.verifyOrchestrationCompatibilityCaller({
terminalHandle: 'term-1',
paneKey: PANE_KEY,
launchToken: 'wrong'
})
).toBeNull()
})
it('attests current coordinators immediately without upgrading restored legacy callers', () => {
const server = new AgentHookServer()
server.ingestRemote(
{
paneKey: PANE_KEY,
launchToken: TOKEN,
payload: { state: 'working', prompt: 'coordinate', agentType: 'codex' }
},
'saved-target'
)
const createIntegratedRuntime = (launchTokenHash: string | null): OrcaRuntimeService => {
const runtime = new OrcaRuntimeService(null, undefined, {
attestAgentHookCompatibilityAuthority: (candidate) =>
server.attestCompatibilityAuthority(candidate)
})
;(runtime as unknown as TerminalAuthorityResolver).getOrchestrationDispatchAuthority = vi.fn(
() => ({
runtimeId: 'runtime-1',
terminalHandle: 'term-1',
ptyId: 'pty-1',
worktreeId: 'repo-1::/worktree',
processIncarnation: 'incarnation-1',
paneKey: PANE_KEY,
launchTokenHash,
hostScope: { kind: 'ssh', targetId: 'saved-target' }
})
)
if (launchTokenHash === null) {
;(
runtime as unknown as TerminalAuthorityResolver
).restoredOrchestrationAuthorityByPtyId.set('pty-1', {
ptyId: 'pty-1',
worktreeId: 'repo-1::/worktree',
terminalHandle: 'term-1',
paneKey: PANE_KEY,
processIncarnation: 'incarnation-1',
hostScope: { kind: 'ssh', targetId: 'saved-target' }
})
}
return runtime
}
const evidenceFor = (runtime: OrcaRuntimeService, launchToken = TOKEN) => ({
terminalHandle: 'term-1',
paneKey: PANE_KEY,
launchToken,
host: runtime.registerOrchestrationCompatibilitySshAttachment('saved-target', 'connection-1')
})
const current = createIntegratedRuntime(TOKEN_HASH)
expect(current.verifyOrchestrationCompatibilityCaller(evidenceFor(current))).not.toBeNull()
const restored = createIntegratedRuntime(null)
expect(restored.verifyOrchestrationCompatibilityCaller(evidenceFor(restored))).toBeNull()
server.ingestRemote(
{
paneKey: '33333333-3333-4333-8333-333333333333:44444444-4444-4444-8444-444444444444',
launchToken: TOKEN,
payload: { state: 'working', prompt: 'duplicate', agentType: 'codex' }
},
'saved-target'
)
expect(current.verifyOrchestrationCompatibilityCaller(evidenceFor(current))).toBeNull()
const mismatchedToken = 'different-launch-secret'
const mismatched = createIntegratedRuntime(
createHash('sha256').update(mismatchedToken).digest('hex')
)
expect(
mismatched.verifyOrchestrationCompatibilityCaller(evidenceFor(mismatched, mismatchedToken))
).toBeNull()
})
it('accepts only a live runtime-issued SSH attachment', () => {
const runtime = createRuntime({ kind: 'ssh', targetId: 'saved-target' })
const host = runtime.registerOrchestrationCompatibilitySshAttachment(
'saved-target',
'connection-1'
)
const evidence = {
terminalHandle: 'term-1',
paneKey: PANE_KEY,
launchToken: TOKEN,
host
} as const
expect(runtime.verifyOrchestrationCompatibilityCaller(evidence)).not.toBeNull()
expect(
runtime.verifyOrchestrationCompatibilityCaller({
...evidence,
host: { ...host, attachmentId: 'caller-chosen' }
})
).toBeNull()
runtime.releaseOrchestrationCompatibilitySshAttachment(host.attachmentId)
expect(runtime.verifyOrchestrationCompatibilityCaller(evidence)).toBeNull()
})
})
+1 -1
View File
@@ -942,7 +942,7 @@ describe('OrchestrationDb', () => {
// v1 data preserved
expect(d.getMessageById('msg_v1')?.subject).toBe('pre-migration')
expect(d.getMessageById('msg_v1')?.run_id).toBe(LEGACY_RUN_ID)
expect(d.getMessageById('msg_v1')?.run_id).toBe(d.getLegacyAdoption()?.adopted_run_id)
expect(d.getRun(LEGACY_RUN_ID)).toMatchObject({ legacy: 1 })
})
File diff suppressed because it is too large Load Diff
@@ -77,6 +77,14 @@ describe('formatMessageBanner', () => {
)
})
it('lets the CLI resolve the live sender for Run and Dispatch addresses', () => {
for (const to_handle of ['run:run_test', 'dispatch:dispatch_test']) {
const banner = formatMessageBanner(makeMessage({ to_handle }))
expect(banner).toContain('[Reply: orca orchestration reply --id msg_test1 --body "..."]')
expect(banner).not.toContain(`--from ${to_handle}`)
}
})
it('marks legacy messages read-only without reply or acknowledgment affordances', () => {
const banner = formatMessageBanner(
makeMessage({ id: 'msg_legacy', run_id: 'run_legacy_local' })
@@ -88,6 +96,63 @@ describe('formatMessageBanner', () => {
expect(banner).not.toContain('orchestration reply')
})
it('renders only attested actions for legacy compatibility authority', () => {
const banner = formatMessageBanner(makeMessage({ id: 'msg_legacy' }), {
authority: 'legacy_compatibility',
supportedActionHints: [
'orca orchestration reply --id msg_legacy --from term_coord --body "..."'
]
})
expect(banner).toContain('[LEGACY COMPATIBILITY]')
expect(banner).toContain(
'[Supported action: orca orchestration reply --id msg_legacy --from term_coord --body "..."]'
)
expect(banner).not.toContain('[Reply:')
expect(banner).not.toContain('acknowledgment')
})
it('warns that a bounded legacy recovery replay may already have been seen', () => {
const banner = formatMessageBanner(makeMessage(), {
authority: 'legacy_recovery_replay',
supportedActionHints: ['orca orchestration check --ack delivery_legacy']
})
expect(banner).toContain('[LEGACY RECOVERY REPLAY — MAY HAVE BEEN SEEN]')
expect(banner).toContain('bounded recovery replay may already have been seen')
expect(banner).toContain('[Supported action: orca orchestration check --ack delivery_legacy]')
expect(banner).not.toContain('[Reply:')
})
it('does not infer live compatibility from legacy database provenance', () => {
const banner = formatMessageBanner(makeMessage({ run_id: 'run_legacy_local' }), {
supportedActionHints: ['orca orchestration check --ack delivery_legacy']
})
expect(banner).toContain('[LEGACY READ-ONLY]')
expect(banner).not.toContain('Supported action:')
expect(banner).not.toContain('check --ack')
})
it('keeps adopted legacy and audit messages read-only without runtime authority', () => {
for (const deliveryContract of ['legacy_direct', 'audit_only'] as const) {
const banner = formatMessageBanner(
makeMessage({ run_id: 'run_adopted', delivery_contract: deliveryContract })
)
expect(banner).toContain('[LEGACY READ-ONLY]')
expect(banner).not.toContain('[Reply:')
}
})
it('keeps current formatting unchanged when authority is explicit', () => {
const message = makeMessage({ id: 'msg_current' })
expect(formatMessageBanner(message, { authority: 'current' })).toBe(
formatMessageBanner(message)
)
})
it('ends with a separator line', () => {
const banner = formatMessageBanner(makeMessage())
const lines = banner.split('\n')
+69 -10
View File
@@ -4,19 +4,77 @@ import { ORCHESTRATION_LEGACY_RUN_ID } from '../../../shared/orchestration-rpc-c
const BANNER_WIDTH = 60
const SEPARATOR = '─'.repeat(BANNER_WIDTH)
export function formatMessageBanner(msg: MessageRow): string {
export type MessageFormattingAuthority =
| 'current'
| 'legacy_compatibility'
| 'legacy_recovery_replay'
| 'legacy_read_only'
export type MessageFormattingOptions = {
authority?: MessageFormattingAuthority
supportedActionHints?: readonly string[]
}
function resolveAuthority(
msg: MessageRow,
authority: MessageFormattingAuthority | undefined
): MessageFormattingAuthority {
if (authority) {
return authority
}
return msg.run_id === ORCHESTRATION_LEGACY_RUN_ID ||
msg.delivery_contract === 'legacy_direct' ||
msg.delivery_contract === 'audit_only'
? 'legacy_read_only'
: 'current'
}
function appendLegacyGuidance(
lines: string[],
authority: MessageFormattingAuthority,
supportedActionHints: readonly string[]
): void {
if (authority === 'legacy_read_only') {
lines.push('[Inspection only: reply and acknowledgment are unavailable.]')
return
}
if (authority === 'legacy_compatibility') {
lines.push('[Use only the supported legacy action shown below.]')
} else if (authority === 'legacy_recovery_replay') {
lines.push(
'[This bounded recovery replay may already have been seen. Use only the action shown below.]'
)
}
for (const hint of supportedActionHints) {
lines.push(`[Supported action: ${hint}]`)
}
}
export function formatMessageBanner(msg: MessageRow, options: MessageFormattingOptions): string
export function formatMessageBanner(msg: MessageRow): string
export function formatMessageBanner(
msg: MessageRow,
options: MessageFormattingOptions = {}
): string {
const priorityTag =
msg.priority === 'urgent' ? ' [URGENT]' : msg.priority === 'high' ? ' [HIGH]' : ''
const legacyReadOnly = msg.run_id === ORCHESTRATION_LEGACY_RUN_ID
const authorityTag = legacyReadOnly ? ' [LEGACY READ-ONLY]' : ''
const authority = resolveAuthority(msg, options.authority)
const authorityTag =
authority === 'legacy_compatibility'
? ' [LEGACY COMPATIBILITY]'
: authority === 'legacy_recovery_replay'
? ' [LEGACY RECOVERY REPLAY — MAY HAVE BEEN SEEN]'
: authority === 'legacy_read_only'
? ' [LEGACY READ-ONLY]'
: ''
const senderName = msg.from_handle.toUpperCase()
const header = `──── From: ${senderName} (${msg.from_handle})${priorityTag}${authorityTag} (${msg.type}) ────`
const lines: string[] = [header]
lines.push(`Subject: ${msg.subject}`)
if (legacyReadOnly) {
lines.push('[Inspection only: reply and acknowledgment are unavailable.]')
if (authority !== 'current') {
appendLegacyGuidance(lines, authority, options.supportedActionHints ?? [])
}
if (msg.body) {
@@ -27,11 +85,12 @@ export function formatMessageBanner(msg: MessageRow): string {
lines.push(`[Payload: ${msg.payload}]`)
}
if (!legacyReadOnly) {
// Why: older shells can lack Orca's terminal identity environment.
lines.push(
`[Reply: orca orchestration reply --id ${msg.id} --from ${msg.to_handle} --body "..."]`
)
if (authority === 'current') {
const explicitFrom =
msg.to_handle.startsWith('run:') || msg.to_handle.startsWith('dispatch:')
? ''
: ` --from ${msg.to_handle}`
lines.push(`[Reply: orca orchestration reply --id ${msg.id}${explicitFrom} --body "..."]`)
}
lines.push(SEPARATOR)
@@ -0,0 +1,381 @@
import { rmSync } from 'node:fs'
import { afterEach, describe, expect, it } from 'vitest'
import Database from '../../sqlite/sync-database'
import { LEGACY_RUN_ID, OrchestrationDb } from './db'
import {
createLegacyStorageCutoverFixture,
type LegacyStorageCutoverFixture
} from './orchestration-legacy-storage-test-fixture'
describe('OrchestrationDb legacy coordinator authority', () => {
let db: OrchestrationDb | undefined
let tempDir: string | undefined
afterEach(() => {
db?.close()
db = undefined
if (tempDir) {
rmSync(tempDir, { recursive: true, force: true })
tempDir = undefined
}
})
function createCutoverFixture(): LegacyStorageCutoverFixture {
const created = createLegacyStorageCutoverFixture()
tempDir = created.tempDir
return created.fixture
}
function openAdoptedFixture(): {
fixture: LegacyStorageCutoverFixture
adoptedRunId: string
workerPrincipalId: string
} {
const fixture = createCutoverFixture()
db = new OrchestrationDb(fixture.dbPath)
const adoptedRunId = db.getLegacyAdoption()?.adopted_run_id as string
const worker = db.commitLegacyCompatibilityPrincipal({
runId: adoptedRunId,
dispatchId: fixture.legacyDispatchId,
role: 'worker',
hostScope: 'local:runtime_1',
terminalHandle: 'term_legacy_worker',
paneKey: 'tab_legacy:33333333-3333-4333-8333-333333333333',
launchTokenHash: 'legacy_launch_hash',
processIncarnation: 'process_1'
})
return {
fixture,
adoptedRunId,
workerPrincipalId: worker.principal.id
}
}
it('records exact scheduler loss without admitting naturally settled coordinators', () => {
const fixture = createCutoverFixture()
const raw = new Database(fixture.dbPath)
raw.exec(`
UPDATE tasks
SET created_by_terminal_handle = 'term_manual_coord'
WHERE run_id = '${LEGACY_RUN_ID}';
UPDATE messages
SET from_handle = CASE
WHEN from_handle = 'term_legacy_coord' THEN 'term_manual_coord'
ELSE from_handle
END,
to_handle = CASE
WHEN to_handle = 'term_legacy_coord' THEN 'term_manual_coord'
ELSE to_handle
END
WHERE run_id = '${LEGACY_RUN_ID}';
INSERT INTO coordinator_runs (
id, spec, status, coordinator_handle, created_at
) VALUES (
'coordinator_manual', 'manual legacy coordinator', 'running',
'term_manual_coord', datetime('now', '-1 minute')
);
INSERT INTO coordinator_runs (
id, spec, status, coordinator_handle, created_at, completed_at
) VALUES (
'coordinator_natural', 'naturally completed coordinator', 'completed',
'term_natural_coord', datetime('now', '-2 minutes'),
'2099-01-01T00:00:00.000Z'
);
`)
raw.close()
db = new OrchestrationDb(fixture.dbPath)
const adoptedRunId = db.getLegacyAdoption()?.adopted_run_id as string
expect(db.getActiveCoordinatorRun()).toBeUndefined()
const adoption = db.getLegacyAdoption()!
const migratedCoordinator = db.getCoordinatorRun('coordinator_manual')!
expect(migratedCoordinator).toMatchObject({
status: 'failed',
completed_at: adoption.adopted_at,
scheduler_lost_at: adoption.adopted_at
})
expect(db.getCoordinatorRun('coordinator_natural')).toMatchObject({
status: 'completed',
completed_at: '2099-01-01T00:00:00.000Z',
scheduler_lost_at: null
})
expect(
db.resolveLegacyCoordinatorCandidate({
runId: adoptedRunId,
terminalHandle: 'term_manual_coord',
paneKey: 'tab_manual:44444444-4444-4444-8444-444444444444'
})
).toMatchObject({ terminalHandle: 'term_manual_coord' })
expect(db.isLegacyCoordinatorHandle(adoptedRunId, 'term_natural_coord')).toBe(false)
db.close()
db = new OrchestrationDb(fixture.dbPath)
expect(db.getLegacyAdoption()).toEqual(adoption)
expect(db.getCoordinatorRun('coordinator_manual')).toEqual(migratedCoordinator)
expect(
db.resolveLegacyCoordinatorCandidate({
runId: adoptedRunId,
terminalHandle: 'term_manual_coord',
paneKey: 'tab_manual:44444444-4444-4444-8444-444444444444'
})
).toMatchObject({ terminalHandle: 'term_manual_coord' })
})
it('fails closed when multiple pre-cutover schedulers lose authority', () => {
const fixture = createCutoverFixture()
const raw = new Database(fixture.dbPath)
raw.exec(`
INSERT INTO coordinator_runs (
id, spec, status, coordinator_handle, created_at
) VALUES
('coordinator_first', 'first', 'running', 'term_first_coord', datetime('now', '-2 minutes')),
('coordinator_second', 'second', 'running', 'term_second_coord', datetime('now', '-1 minute'));
`)
raw.close()
db = new OrchestrationDb(fixture.dbPath)
const adoption = db.getLegacyAdoption()!
const adoptedRunId = adoption.adopted_run_id
expect(db.getActiveCoordinatorRun()).toBeUndefined()
expect(db.getCoordinatorRun('coordinator_first')).toMatchObject({
status: 'failed',
scheduler_lost_at: adoption.adopted_at
})
expect(db.getCoordinatorRun('coordinator_second')).toMatchObject({
status: 'failed',
scheduler_lost_at: adoption.adopted_at
})
expect(
db.resolveLegacyCoordinatorCandidate({
runId: adoptedRunId,
terminalHandle: 'term_legacy_coord',
paneKey: 'tab_manual:44444444-4444-4444-8444-444444444444'
})
).toBeUndefined()
})
it('does not infer a coordinator from worker-to-worker legacy mail', () => {
const fixture = createCutoverFixture()
const raw = new Database(fixture.dbPath)
raw.exec(`
INSERT INTO tasks (
id, run_id, spec, status, created_by_terminal_handle
) VALUES (
'task_second_worker', '${LEGACY_RUN_ID}', 'second worker',
'dispatched', 'term_legacy_coord'
);
INSERT INTO dispatch_contexts (
id, run_id, task_id, contract_version, assignee_handle,
assignee_pane_key, status, dispatched_at
) VALUES (
'dispatch_second_worker', '${LEGACY_RUN_ID}', 'task_second_worker', 0,
'term_second_worker', 'tab_second:55555555-5555-4555-8555-555555555555',
'dispatched', datetime('now')
);
INSERT INTO messages (
id, run_id, from_handle, to_handle, subject
) VALUES (
'message_worker_to_worker', '${LEGACY_RUN_ID}', 'term_legacy_worker',
'term_second_worker', 'peer coordination'
);
`)
raw.close()
db = new OrchestrationDb(fixture.dbPath)
const adoptedRunId = db.getLegacyAdoption()?.adopted_run_id as string
expect(db.isLegacyCoordinatorHandle(adoptedRunId, 'term_second_worker')).toBe(false)
expect(db.isLegacyCoordinatorHandle(adoptedRunId, 'term_legacy_worker')).toBe(false)
expect(db.isLegacyCoordinatorHandle(adoptedRunId, 'term_legacy_coord')).toBe(true)
})
it('promotes only the unacknowledged coordinator recovery cohort during takeover', () => {
const fixture = createCutoverFixture()
db = new OrchestrationDb(fixture.dbPath)
const adoptedRunId = db.getLegacyAdoption()?.adopted_run_id as string
const unacknowledged = db.insertMessage({
runId: adoptedRunId,
deliveryContract: 'legacy_direct',
from: 'term_legacy_worker',
to: 'term_legacy_coord',
subject: 'second recovered coordinator outcome'
})
db.markAsRead([unacknowledged.id])
const coordinator = db.commitLegacyCompatibilityPrincipal({
runId: adoptedRunId,
role: 'coordinator',
hostScope: 'local:runtime_1',
terminalHandle: 'term_legacy_coord',
paneKey: 'tab_coord:44444444-4444-4444-8444-444444444444',
launchTokenHash: 'coord_launch_hash',
processIncarnation: 'process_coord'
}).principal
const firstPage = db.getLegacyMailPage({ principalId: coordinator.id, limit: 1 })
expect(firstPage.recovery).toBe(true)
db.acknowledgeLegacyMail({
principalId: coordinator.id,
messageIds: firstPage.messages.map((message) => message.id)
})
const acknowledgedId = firstPage.messages[0].id
db.bindRun({
runId: adoptedRunId,
coordinatorHandle: 'term_current_coord',
coordinatorPaneKey: 'tab_current:11111111-1111-4111-8111-111111111111',
takeoverLegacy: true
})
expect(db.getMessageById(acknowledgedId)).toMatchObject({
to_handle: 'term_legacy_coord',
delivery_contract: 'legacy_direct',
read: 1
})
expect(db.getMessageById(unacknowledged.id)).toMatchObject({
to_handle: `run:${adoptedRunId}`,
delivery_contract: 'current_delivery',
read: 0
})
})
it('promotes a pre-cutover read recovery message when takeover precedes attestation', () => {
const fixture = createCutoverFixture()
db = new OrchestrationDb(fixture.dbPath)
const adoptedRunId = db.getLegacyAdoption()?.adopted_run_id as string
const recoveryMessageId = fixture.legacyMessageIds[1]
expect(db.getLegacyCoordinatorPrincipal(adoptedRunId)).toBeUndefined()
expect(db.getMessageById(recoveryMessageId)).toMatchObject({
to_handle: 'term_legacy_coord',
delivery_contract: 'legacy_direct',
read: 1
})
const run = db.bindRun({
runId: adoptedRunId,
coordinatorHandle: 'term_current_coord',
coordinatorPaneKey: 'tab_current:11111111-1111-4111-8111-111111111111',
takeoverLegacy: true
})!
expect(db.getMessageById(recoveryMessageId)).toMatchObject({
to_handle: `run:${adoptedRunId}`,
delivery_contract: 'current_delivery',
read: 0
})
const first = db.getOrCreateRunDelivery({
runId: adoptedRunId,
consumerGeneration: run.consumer_generation
})!
const replay = db.getOrCreateRunDelivery({
runId: adoptedRunId,
consumerGeneration: run.consumer_generation
})!
expect(first.messages.map((message) => message.id)).toContain(recoveryMessageId)
expect(replay).toMatchObject({ replayed: true, delivery: { id: first.delivery.id } })
db.acknowledgeRunDelivery({
runId: adoptedRunId,
consumerGeneration: run.consumer_generation,
deliveryId: first.delivery.id
})
expect(
db
.getOrCreateRunDelivery({
runId: adoptedRunId,
consumerGeneration: run.consumer_generation
})
?.messages.map((message) => message.id) ?? []
).not.toContain(recoveryMessageId)
})
it('promotes retained mail on an ordinary post-settlement coordinator replacement', () => {
const state = openAdoptedFixture()
const coordinator = db!.commitLegacyCompatibilityPrincipal({
runId: state.adoptedRunId,
role: 'coordinator',
hostScope: 'local:runtime_1',
terminalHandle: 'term_legacy_coord',
paneKey: 'tab_coord:44444444-4444-4444-8444-444444444444',
launchTokenHash: 'coord_launch_hash',
processIncarnation: 'process_coord'
}).principal
const completion = db!.insertMessage({
runId: state.adoptedRunId,
deliveryContract: 'legacy_direct',
from: 'term_legacy_worker',
to: 'term_legacy_coord',
subject: 'Completed before ordinary run-use',
type: 'worker_done'
})
db!.settleWorkerReport({
taskId: state.fixture.legacyTaskId,
dispatchId: state.fixture.legacyDispatchId,
outcome: 'succeeded',
result: 'done'
})
const run = db!.bindRun({
runId: state.adoptedRunId,
coordinatorHandle: 'term_current_coord',
coordinatorPaneKey: 'tab_current:11111111-1111-4111-8111-111111111111'
})!
expect(db!.getLegacyCompatibilityPrincipal(coordinator.id)?.status).toBe('revoked')
expect(db!.getMessageById(completion.id)).toMatchObject({
to_handle: `run:${state.adoptedRunId}`,
delivery_contract: 'current_delivery',
read: 0
})
expect(
db!
.getOrCreateRunDelivery({
runId: state.adoptedRunId,
consumerGeneration: run.consumer_generation
})
?.messages.map((message) => message.id)
).toContain(completion.id)
})
it('acknowledges the exact current Run answer to a legacy ask after takeover', () => {
const state = openAdoptedFixture()
const ask = db!.commitLegacyAskOperation({
principalId: state.workerPrincipalId,
operationKey: 'ask_before_takeover',
method: 'orchestration.ask',
payloadHash: 'ask_before_takeover_payload',
question: 'Continue after takeover?',
recipientHandle: 'term_legacy_coord'
})
const run = db!.bindRun({
runId: state.adoptedRunId,
coordinatorHandle: 'term_current_coord',
coordinatorPaneKey: 'tab_current:11111111-1111-4111-8111-111111111111',
takeoverLegacy: true
})!
const answered = db!.answerQuestion({
messageId: ask.question.message_id,
runId: state.adoptedRunId,
consumerGeneration: run.consumer_generation,
body: 'continue'
})
expect(answered.message).toMatchObject({
from_handle: `run:${state.adoptedRunId}`,
to_handle: `dispatch:${state.fixture.legacyDispatchId}`,
delivery_contract: 'current_delivery'
})
const acknowledged = db!.acknowledgeLegacyQuestionAnswer({
principalId: state.workerPrincipalId,
questionId: ask.question.message_id,
answerMessageId: answered.message.id
})
expect(acknowledged.duplicate).toBe(false)
expect(db!.getMessageById(answered.message.id)?.read).toBe(1)
expect(
db!.acknowledgeLegacyQuestionAnswer({
principalId: state.workerPrincipalId,
questionId: ask.question.message_id,
answerMessageId: answered.message.id
}).duplicate
).toBe(true)
})
})
@@ -0,0 +1,255 @@
import { rmSync } from 'node:fs'
import { afterEach, describe, expect, it } from 'vitest'
import Database from '../../sqlite/sync-database'
import { LEGACY_RUN_ID, OrchestrationDb } from './db'
import { createLegacyStorageCutoverFixture } from './orchestration-legacy-storage-test-fixture'
describe('OrchestrationDb legacy question migration', () => {
let db: OrchestrationDb | undefined
let tempDir: string | undefined
afterEach(() => {
db?.close()
if (tempDir) {
rmSync(tempDir, { recursive: true, force: true })
}
})
function prepareTakenOverSchema19Question(readAndAcknowledge: boolean): {
dbPath: string
adoptedRunId: string
questionId: string
} {
const created = createLegacyStorageCutoverFixture()
tempDir = created.tempDir
db = new OrchestrationDb(created.fixture.dbPath)
const adoptedRunId = db.getLegacyAdoption()?.adopted_run_id as string
const questionId = readAndAcknowledge ? 'msg_schema19_acknowledged' : 'msg_schema19_unread'
db.insertMessage({
id: questionId,
runId: adoptedRunId,
deliveryContract: 'legacy_direct',
from: 'term_legacy_worker',
to: 'term_legacy_coord',
subject: 'Question',
body: 'Continue after takeover?',
type: 'decision_gate',
threadId: questionId,
payload: JSON.stringify({
taskId: created.fixture.legacyTaskId,
dispatchId: created.fixture.legacyDispatchId
})
})
if (readAndAcknowledge) {
db.markAsRead([questionId])
}
const coordinator = db.commitLegacyCompatibilityPrincipal({
runId: adoptedRunId,
role: 'coordinator',
hostScope: 'local:runtime_1',
terminalHandle: 'term_legacy_coord',
paneKey: 'tab_coord:44444444-4444-4444-8444-444444444444',
launchTokenHash: 'coord_launch_hash',
processIncarnation: 'process_coord'
}).principal
if (readAndAcknowledge) {
const recovery = db.getLegacyMailPage({ principalId: coordinator.id })
expect(recovery.messages.map((message) => message.id)).toContain(questionId)
db.acknowledgeLegacyMail({
principalId: coordinator.id,
messageIds: recovery.messages.map((message) => message.id)
})
}
db.bindRun({
runId: adoptedRunId,
coordinatorHandle: 'term_current_coord',
coordinatorPaneKey: 'tab_current:11111111-1111-4111-8111-111111111111',
takeoverLegacy: true
})
expect(db.getMessageById(questionId)).toMatchObject(
readAndAcknowledge
? { delivery_contract: 'legacy_direct', read: 1 }
: { delivery_contract: 'current_delivery', read: 0 }
)
db.close()
db = undefined
const raw = new Database(created.fixture.dbPath)
raw.exec('DROP TABLE question_threads')
raw.pragma('user_version = 19')
raw.close()
return { dbPath: created.fixture.dbPath, adoptedRunId, questionId }
}
it('backfills a pre-question-thread ask and its accepted answer', () => {
const created = createLegacyStorageCutoverFixture()
tempDir = created.tempDir
db = new OrchestrationDb(created.fixture.dbPath)
const adoptedRunId = db.getLegacyAdoption()?.adopted_run_id as string
db.close()
db = undefined
const raw = new Database(created.fixture.dbPath)
raw.exec(`
DROP TABLE question_threads;
INSERT INTO messages (
id, run_id, delivery_contract, from_handle, to_handle,
subject, body, type, thread_id, payload
) VALUES (
'msg_cutover', '${adoptedRunId}', 'legacy_direct',
'term_legacy_worker', 'term_legacy_coord', 'Question',
'Continue?', 'decision_gate', 'msg_cutover',
'{"taskId":"${created.fixture.legacyTaskId}","dispatchId":"${created.fixture.legacyDispatchId}"}'
);
INSERT INTO messages (
id, run_id, delivery_contract, from_handle, to_handle,
subject, body, thread_id
) VALUES
(
'msg_cross_run_decoy', 'run_decoy', 'legacy_direct',
'term_legacy_coord', 'term_legacy_worker',
'Re: Question', 'cross-run decoy', 'msg_cutover'
),
(
'msg_self_sent_decoy', '${adoptedRunId}', 'legacy_direct',
'term_legacy_worker', 'term_legacy_worker',
'Re: Question', 'self-sent decoy', 'msg_cutover'
),
(
'msg_cutover_answer', '${adoptedRunId}', 'legacy_direct',
'term_legacy_coord', 'term_legacy_worker',
'Re: Question', 'yes', 'msg_cutover'
);
`)
raw.pragma('user_version = 19')
raw.close()
db = new OrchestrationDb(created.fixture.dbPath)
expect(db.getQuestion('msg_cutover')).toMatchObject({
run_id: adoptedRunId,
dispatch_id: created.fixture.legacyDispatchId,
asker_handle: 'term_legacy_worker',
status: 'answered',
answer_message_id: 'msg_cutover_answer',
answer_body: 'yes'
})
})
it('leaves a question pending when only cross-run and self-sent decoys exist', () => {
const created = createLegacyStorageCutoverFixture()
tempDir = created.tempDir
db = new OrchestrationDb(created.fixture.dbPath)
const adoptedRunId = db.getLegacyAdoption()?.adopted_run_id as string
db.close()
db = undefined
const raw = new Database(created.fixture.dbPath)
raw.exec(`
DROP TABLE question_threads;
INSERT INTO messages (
id, run_id, delivery_contract, from_handle, to_handle,
subject, body, type, thread_id, payload
) VALUES (
'msg_pending_cutover', '${adoptedRunId}', 'legacy_direct',
'term_legacy_worker', 'term_legacy_coord', 'Question',
'Continue?', 'decision_gate', 'msg_pending_cutover',
'{"taskId":"${created.fixture.legacyTaskId}","dispatchId":"${created.fixture.legacyDispatchId}"}'
);
INSERT INTO messages (
id, run_id, delivery_contract, from_handle, to_handle,
subject, body, thread_id
) VALUES
(
'msg_pending_cross_run_decoy', 'run_decoy', 'legacy_direct',
'term_legacy_coord', 'term_legacy_worker',
'Re: Question', 'cross-run decoy', 'msg_pending_cutover'
),
(
'msg_pending_self_sent_decoy', '${adoptedRunId}', 'legacy_direct',
'term_legacy_worker', 'term_legacy_worker',
'Re: Question', 'self-sent decoy', 'msg_pending_cutover'
);
`)
raw.pragma('user_version = 19')
raw.close()
db = new OrchestrationDb(created.fixture.dbPath)
expect(db.getQuestion('msg_pending_cutover')).toMatchObject({
run_id: adoptedRunId,
dispatch_id: created.fixture.legacyDispatchId,
asker_handle: 'term_legacy_worker',
status: 'pending',
answer_message_id: null,
answer_body: null
})
})
it('uses task provenance to disambiguate same-handle same-second asks', () => {
const created = createLegacyStorageCutoverFixture()
tempDir = created.tempDir
const raw = new Database(created.fixture.dbPath)
raw.exec(`
INSERT INTO tasks (
id, run_id, spec, status, created_by_terminal_handle, created_at
) VALUES
('task_question_target', '${LEGACY_RUN_ID}', 'target', 'dispatched',
'term_legacy_coord', '2026-01-01 00:00:00'),
('task_question_other', '${LEGACY_RUN_ID}', 'other', 'dispatched',
'term_legacy_coord', '2026-01-01 00:00:00');
INSERT INTO dispatch_contexts (
id, run_id, task_id, contract_version, assignee_handle,
assignee_pane_key, status, dispatched_at, created_at
) VALUES
('dispatch_question_target', '${LEGACY_RUN_ID}', 'task_question_target', 0,
'term_legacy_worker', 'tab_target:11111111-1111-4111-8111-111111111111',
'dispatched', '2026-01-01 00:00:00', '2026-01-01 00:00:00'),
('dispatch_question_other', '${LEGACY_RUN_ID}', 'task_question_other', 0,
'term_legacy_worker', 'tab_other:22222222-2222-4222-8222-222222222222',
'dispatched', '2026-01-01 00:00:00', '2026-01-01 00:00:00');
INSERT INTO messages (
id, run_id, delivery_contract, from_handle, to_handle,
subject, body, type, payload, created_at
) VALUES (
'msg_question_task_only', '${LEGACY_RUN_ID}', 'legacy_direct',
'term_legacy_worker', 'term_legacy_coord', 'Question', 'Continue?',
'decision_gate', '{"taskId":"task_question_target","question":"Continue?","options":[]}',
'2026-01-01 00:00:00'
);
`)
raw.close()
db = new OrchestrationDb(created.fixture.dbPath)
expect(db.getQuestion('msg_question_task_only')).toMatchObject({
dispatch_id: 'dispatch_question_target',
asker_handle: 'term_legacy_worker',
status: 'pending'
})
})
it.each([
['promoted unread', false],
['acknowledged recovery', true]
] as const)('backfills and routes a %s v19 question after takeover', (_label, acknowledged) => {
const state = prepareTakenOverSchema19Question(acknowledged)
db = new OrchestrationDb(state.dbPath)
expect(db.getQuestion(state.questionId)).toMatchObject({
run_id: state.adoptedRunId,
status: 'pending'
})
expect(db.getMessageById(state.questionId)).toMatchObject({
to_handle: `run:${state.adoptedRunId}`,
delivery_contract: 'current_delivery',
read: 0
})
const run = db.getRun(state.adoptedRunId)!
expect(
db
.getOrCreateRunDelivery({
runId: state.adoptedRunId,
consumerGeneration: run.consumer_generation
})
?.messages.map((message) => message.id)
).toContain(state.questionId)
})
})
@@ -0,0 +1,751 @@
import { rmSync } from 'node:fs'
import { afterEach, describe, expect, it } from 'vitest'
import Database from '../../sqlite/sync-database'
import {
CURRENT_CONTRACT_VERSION,
LEGACY_CONTRACT_VERSION,
LEGACY_RUN_ID,
OrchestrationDb
} from './db'
import {
createLegacyStorageCutoverFixture,
type LegacyStorageCutoverFixture
} from './orchestration-legacy-storage-test-fixture'
describe('OrchestrationDb legacy contract storage', () => {
let db: OrchestrationDb | undefined
let tempDir: string | undefined
afterEach(() => {
db?.close()
db = undefined
if (tempDir) {
rmSync(tempDir, { recursive: true, force: true })
tempDir = undefined
}
})
function createCutoverFixture(): LegacyStorageCutoverFixture {
const created = createLegacyStorageCutoverFixture()
tempDir = created.tempDir
return created.fixture
}
function openAdoptedFixture(): {
fixture: LegacyStorageCutoverFixture
adoptedRunId: string
workerPrincipalId: string
coordinatorPrincipalId: string
} {
const fixture = createCutoverFixture()
db = new OrchestrationDb(fixture.dbPath)
const adoptedRunId = db.getLegacyAdoption()?.adopted_run_id as string
const worker = db.commitLegacyCompatibilityPrincipal({
runId: adoptedRunId,
dispatchId: fixture.legacyDispatchId,
role: 'worker',
hostScope: 'local:runtime_1',
terminalHandle: 'term_legacy_worker',
paneKey: 'tab_legacy:33333333-3333-4333-8333-333333333333',
launchTokenHash: 'legacy_launch_hash',
processIncarnation: 'process_1'
})
const coordinator = db.commitLegacyCompatibilityPrincipal({
runId: adoptedRunId,
role: 'coordinator',
hostScope: 'local:runtime_1',
terminalHandle: 'term_legacy_coord',
paneKey: 'tab_coord:44444444-4444-4444-8444-444444444444',
launchTokenHash: 'coord_launch_hash',
processIncarnation: 'process_coord'
})
return {
fixture,
adoptedRunId,
workerPrincipalId: worker.principal.id,
coordinatorPrincipalId: coordinator.principal.id
}
}
it('atomically rehomes the full graph, fences legacy Delivery, and preserves current rows', () => {
const fixture = createCutoverFixture()
db = new OrchestrationDb(fixture.dbPath)
const adoption = db.getLegacyAdoption()
const adoptedRunId = adoption?.adopted_run_id as string
const sqlite = (db as unknown as { db: Database.Database }).db
expect(adoption).toMatchObject({
source_run_id: LEGACY_RUN_ID,
scheduler_state_lost: 1
})
expect(db.getRun(adoptedRunId)).toMatchObject({ legacy: 0, consumer_generation: 0 })
expect(db.listTasks({ runId: LEGACY_RUN_ID })).toEqual([])
expect(db.getDispatchContextById(fixture.legacyDispatchId)).toMatchObject({
run_id: adoptedRunId,
contract_version: LEGACY_CONTRACT_VERSION,
launch_token_hash: null
})
expect(db.getGate(fixture.legacyGateId)).toMatchObject({ run_id: adoptedRunId })
expect(db.getQuestion(fixture.legacyQuestionId)).toMatchObject({ run_id: adoptedRunId })
expect(db.getMessageById(fixture.legacyMessageIds[0])).toMatchObject({
run_id: adoptedRunId,
delivery_contract: 'legacy_direct'
})
expect(db.getMessageById(fixture.rejectionMessageId)).toMatchObject({
run_id: adoptedRunId,
delivery_contract: 'audit_only'
})
expect(db.getMessageById(fixture.lookalikeMessageId)).toMatchObject({
run_id: adoptedRunId,
delivery_contract: 'legacy_direct'
})
for (const messageId of fixture.malformedRejectionMessageIds) {
expect(db.getMessageById(messageId)).toMatchObject({
run_id: adoptedRunId,
delivery_contract: 'legacy_direct'
})
}
expect(
sqlite.prepare('SELECT * FROM deliveries WHERE id = ?').get(fixture.legacyDeliveryId)
).toMatchObject({ run_id: adoptedRunId, status: 'fenced' })
expect(db.getDispatchContextById(fixture.currentDispatchId)).toMatchObject({
run_id: fixture.currentRunId,
contract_version: CURRENT_CONTRACT_VERSION,
launch_token_hash: 'current_launch_hash'
})
db.close()
const partial = new Database(fixture.dbPath)
partial
.prepare('UPDATE tasks SET run_id = ? WHERE id = ?')
.run(LEGACY_RUN_ID, fixture.legacyTaskId)
partial.pragma('user_version = 19')
partial.close()
db = new OrchestrationDb(fixture.dbPath)
expect(db.getLegacyAdoption()?.adopted_run_id).toBe(adoptedRunId)
expect(db.getTask(fixture.legacyTaskId)?.run_id).toBe(adoptedRunId)
expect(db.getDispatchContextById(fixture.currentDispatchId)?.contract_version).toBe(
CURRENT_CONTRACT_VERSION
)
expect(db.listTasks({ runId: LEGACY_RUN_ID })).toEqual([])
})
it('ignores unrelated cross-Run anomalies while validating adopted rows', () => {
const fixture = createCutoverFixture()
const raw = new Database(fixture.dbPath)
raw
.prepare('UPDATE dispatch_contexts SET run_id = ? WHERE id = ?')
.run(fixture.unrelatedRunId, fixture.currentDispatchId)
raw.close()
db = new OrchestrationDb(fixture.dbPath)
expect(db.getLegacyAdoption()).toBeDefined()
expect(db.getDispatchContextById(fixture.currentDispatchId)?.run_id).toBe(
fixture.unrelatedRunId
)
})
it('does not synthesize an adopted Run or compatibility authority for a fresh database', () => {
db = new OrchestrationDb(':memory:')
expect(db.getLegacyAdoption()).toBeUndefined()
expect(db.listLegacyCompatibilityPrincipals(LEGACY_RUN_ID)).toEqual([])
expect(db.listRuns()).toEqual([expect.objectContaining({ id: LEGACY_RUN_ID, legacy: 1 })])
})
it('keeps current Delivery disjoint from adopted direct and audit-only mail', () => {
const state = openAdoptedFixture()
const run = db!.getRun(state.adoptedRunId) as NonNullable<ReturnType<OrchestrationDb['getRun']>>
db!.insertMessage({
runId: state.adoptedRunId,
from: 'current_worker',
to: `run:${state.adoptedRunId}`,
subject: 'current retry mail'
})
const delivery = db!.getOrCreateRunDelivery({
runId: state.adoptedRunId,
consumerGeneration: run.consumer_generation
})
expect(delivery?.messages.map((message) => message.subject)).toEqual(['current retry mail'])
})
it('drains a durable recovery cohort in bounded replaying pages before unread mail', () => {
const state = openAdoptedFixture()
const first = db!.getLegacyMailPage({ principalId: state.workerPrincipalId, limit: 1 })
const replay = db!.getLegacyMailPage({ principalId: state.workerPrincipalId, limit: 1 })
expect(first.recovery).toBe(true)
expect(replay.messages.map((message) => message.id)).toEqual(
first.messages.map((message) => message.id)
)
expect(() =>
db!.acknowledgeLegacyMail({
principalId: state.workerPrincipalId,
messageIds: [state.fixture.legacyMessageIds[2]]
})
).toThrow(/current replay page/)
db!.acknowledgeLegacyMail({
principalId: state.workerPrincipalId,
messageIds: first.messages.map((message) => message.id)
})
const second = db!.getLegacyMailPage({ principalId: state.workerPrincipalId, limit: 1 })
expect(second.recovery).toBe(true)
expect(second.messages[0].id).not.toBe(first.messages[0].id)
db!.acknowledgeLegacyMail({
principalId: state.workerPrincipalId,
messageIds: second.messages.map((message) => message.id)
})
expect(db!.getLegacyMailPage({ principalId: state.workerPrincipalId, limit: 1 }).recovery).toBe(
false
)
})
it('returns complete addressed legacy history without changing read state', () => {
const state = openAdoptedFixture()
const unread = db!.insertMessage({
runId: state.adoptedRunId,
deliveryContract: 'legacy_direct',
from: 'term_legacy_coord',
to: 'term_legacy_worker',
subject: 'unread history'
})
db!.insertMessage({
runId: state.adoptedRunId,
deliveryContract: 'legacy_direct',
from: 'other',
to: 'other',
subject: 'not addressed'
})
const history = db!.getLegacyMailHistory({ principalId: state.workerPrincipalId })
expect(history.recovery).toBe(false)
expect(history.messages.map((message) => message.subject)).toEqual([
'read worker mail',
'second worker page',
'unread history'
])
expect(db!.getMessageById(unread.id)?.read).toBe(0)
})
it('resolves legacy principals and completion evidence only within exact assignments', () => {
const state = openAdoptedFixture()
const taskId = db!.getDispatchContextById(state.fixture.legacyDispatchId)!.task_id
const payload = JSON.stringify({ taskId, dispatchId: state.fixture.legacyDispatchId })
const completion = db!.insertMessage({
runId: state.adoptedRunId,
deliveryContract: 'legacy_direct',
from: 'term_legacy_worker',
to: 'term_legacy_coord',
subject: 'Completed',
body: 'done',
type: 'worker_done',
payload
})
expect(
db!.resolveLegacyWorkerCandidate({
runId: state.adoptedRunId,
terminalHandle: 'term_legacy_worker',
paneKey: 'tab_reminted:33333333-3333-4333-8333-333333333333',
taskId
})
).toMatchObject({ dispatch: { id: state.fixture.legacyDispatchId } })
expect(
db!.resolveLegacyWorkerCandidate({
runId: state.adoptedRunId,
terminalHandle: 'term_legacy_worker',
paneKey: 'tab_wrong:99999999-9999-4999-8999-999999999999',
taskId
})
).toBeUndefined()
expect(
db!.resolveLegacyCoordinatorCandidate({
runId: state.adoptedRunId,
terminalHandle: 'term_legacy_coord',
paneKey: 'tab_coord:44444444-4444-4444-8444-444444444444'
})
).toMatchObject({ terminalHandle: 'term_legacy_coord' })
expect(
db!.findLegacyWorkerCompletion({
principalId: state.workerPrincipalId,
taskId,
recipientHandle: 'term_legacy_coord',
subject: 'Completed',
body: 'done',
payload
})
).toMatchObject({ id: completion.id })
})
it('prevents an unproven coordinator from taking over an active adopted Run', () => {
const state = openAdoptedFixture()
expect(() =>
db!.bindRun({
runId: state.adoptedRunId,
coordinatorHandle: 'term_other',
coordinatorPaneKey: 'tab_other:55555555-5555-4555-8555-555555555555'
})
).toThrow(/attested coordinator may rebind/)
const bound = db!.bindRun({
runId: state.adoptedRunId,
coordinatorHandle: 'term_legacy_coord',
coordinatorPaneKey: 'tab_coord:44444444-4444-4444-8444-444444444444',
legacyCoordinatorAuthority: {
runId: state.adoptedRunId,
principalId: state.coordinatorPrincipalId,
terminalHandle: 'term_legacy_coord',
paneKey: 'tab_coord:44444444-4444-4444-8444-444444444444',
consumerGeneration: 0
}
})
expect(bound).toMatchObject({ coordinator_handle: 'term_legacy_coord' })
expect(
db!.bindRun({
runId: state.adoptedRunId,
coordinatorHandle: 'term_legacy_coord',
coordinatorPaneKey: 'tab_coord:44444444-4444-4444-8444-444444444444'
})
).toMatchObject({ coordinator_handle: 'term_legacy_coord' })
})
it.each([
['generation', { consumerGeneration: 1 }],
['principal', { principalId: 'legacy_principal_wrong' }],
['handle', { terminalHandle: 'term_wrong' }],
['pane', { paneKey: 'tab_wrong:99999999-9999-4999-8999-999999999999' }]
] as const)('rejects stale legacy coordinator %s proof inside Run binding', (_label, patch) => {
const state = openAdoptedFixture()
const before = db!.getRun(state.adoptedRunId)
expect(() =>
db!.bindRun({
runId: state.adoptedRunId,
coordinatorHandle: 'term_legacy_coord',
coordinatorPaneKey: 'tab_coord:44444444-4444-4444-8444-444444444444',
legacyCoordinatorAuthority: {
runId: state.adoptedRunId,
principalId: state.coordinatorPrincipalId,
terminalHandle: 'term_legacy_coord',
paneKey: 'tab_coord:44444444-4444-4444-8444-444444444444',
consumerGeneration: 0,
...patch
}
})
).toThrow(/no longer has lifecycle authority/)
expect(db!.getRun(state.adoptedRunId)).toEqual(before)
})
it('revokes the legacy coordinator when a current coordinator takes over after settlement', () => {
const state = openAdoptedFixture()
db!.settleWorkerReport({
taskId: state.fixture.legacyTaskId,
dispatchId: state.fixture.legacyDispatchId,
outcome: 'succeeded',
result: 'done'
})
expect(
db!.bindRun({
runId: state.adoptedRunId,
coordinatorHandle: 'term_current_coord',
coordinatorPaneKey: 'tab_current:11111111-1111-4111-8111-111111111111'
})
).toMatchObject({ coordinator_handle: 'term_current_coord' })
expect(db!.getLegacyCompatibilityPrincipal(state.coordinatorPrincipalId)?.status).toBe(
'revoked'
)
expect(() =>
db!.commitLegacyCompatibilityPrincipal({
runId: state.adoptedRunId,
role: 'coordinator',
hostScope: 'local:runtime_1',
terminalHandle: 'term_legacy_coord',
paneKey: 'tab_coord:44444444-4444-4444-8444-444444444444',
launchTokenHash: 'coord_launch_hash',
processIncarnation: 'process_coord'
})
).toThrow(/revoked/)
})
it('promotes only mail addressed to the replaced legacy coordinator', () => {
const state = openAdoptedFixture()
const coordinatorMail = db!.insertMessage({
runId: state.adoptedRunId,
deliveryContract: 'legacy_direct',
from: 'term_legacy_worker',
to: 'term_legacy_coord',
subject: 'coordinator outcome'
})
const workerMail = db!.insertMessage({
runId: state.adoptedRunId,
deliveryContract: 'legacy_direct',
from: 'term_legacy_worker',
to: 'term_other_worker',
subject: 'worker-only guidance'
})
db!.bindRun({
runId: state.adoptedRunId,
coordinatorHandle: 'term_current_coord',
coordinatorPaneKey: 'tab_current:11111111-1111-4111-8111-111111111111',
takeoverLegacy: true
})
expect(db!.getMessageById(coordinatorMail.id)).toMatchObject({
to_handle: `run:${state.adoptedRunId}`,
delivery_contract: 'current_delivery'
})
expect(db!.getMessageById(workerMail.id)).toMatchObject({
to_handle: 'term_other_worker',
delivery_contract: 'legacy_direct'
})
})
it('commits legacy messages, lifecycle effects, and invocation receipts exactly once', () => {
const state = openAdoptedFixture()
const params = {
principalId: state.workerPrincipalId,
operationKey: 'invocation_1',
method: 'orchestration.send',
payloadHash: 'payload_1',
message: {
to: 'term_legacy_coord',
subject: 'alive',
type: 'heartbeat' as const
},
lifecycle: { kind: 'heartbeat' as const, at: '2026-07-28T12:00:00.000Z' }
}
const first = db!.commitLegacyLifecycleOperation(params)
const replay = db!.commitLegacyLifecycleOperation(params)
expect(replay).toMatchObject({
duplicate: true,
message: { id: first.message.id },
receipt: { effect_id: first.message.id }
})
expect(db!.getDispatchContextById(state.fixture.legacyDispatchId)?.last_heartbeat_at).toBe(
'2026-07-28T12:00:00.000Z'
)
expect(db!.getUnreadMessages('term_legacy_coord')).toEqual([])
expect(db!.getUndeliveredUnreadMessages('term_legacy_coord')).toEqual([])
expect(() =>
db!.commitLegacyLifecycleOperation({ ...params, payloadHash: 'different' })
).toThrow(/different input/)
})
it('reconstructs a matching pre-receipt settlement without changing its persisted outcome', () => {
const state = openAdoptedFixture()
const accepted = db!.insertMessage({
runId: state.adoptedRunId,
deliveryContract: 'legacy_direct',
from: 'term_legacy_worker',
to: 'term_legacy_coord',
subject: 'Completed',
type: 'worker_done'
})
expect(
db!.settleWorkerReport({
taskId: db!.getDispatchContextById(state.fixture.legacyDispatchId)!.task_id,
dispatchId: state.fixture.legacyDispatchId,
outcome: 'succeeded',
result: 'accepted by A'
})
).toMatchObject({ action: 'settled', duplicate: false })
const reconstructed = db!.commitLegacyLifecycleOperation({
principalId: state.workerPrincipalId,
operationKey: `settlement:${state.fixture.legacyDispatchId}`,
method: 'orchestration.send',
payloadHash: 'settlement_payload',
message: {
existingId: accepted.id,
to: 'term_legacy_coord',
subject: 'Completed',
type: 'worker_done'
},
lifecycle: {
kind: 'worker_report',
taskId: db!.getDispatchContextById(state.fixture.legacyDispatchId)!.task_id,
outcome: 'succeeded',
result: 'accepted by A'
}
})
expect(reconstructed).toMatchObject({
duplicate: false,
message: { id: accepted.id },
settlement: { action: 'settled', outcome: 'succeeded', duplicate: true }
})
expect(db!.getLegacyCompatibilityPrincipal(state.workerPrincipalId)?.status).toBe('settled')
})
it('reconstructs a read pre-takeover completion through its original legacy route', () => {
const state = openAdoptedFixture()
const taskId = db!.getDispatchContextById(state.fixture.legacyDispatchId)!.task_id
const accepted = db!.insertMessage({
runId: state.adoptedRunId,
deliveryContract: 'legacy_direct',
from: 'term_legacy_worker',
to: 'term_legacy_coord',
subject: 'Completed',
body: 'accepted before takeover',
type: 'worker_done'
})
db!.markAsRead([accepted.id])
db!.settleWorkerReport({
taskId,
dispatchId: state.fixture.legacyDispatchId,
outcome: 'succeeded',
result: 'accepted before takeover'
})
db!.bindRun({
runId: state.adoptedRunId,
coordinatorHandle: 'term_current_coord',
coordinatorPaneKey: 'tab_current:11111111-1111-4111-8111-111111111111',
takeoverLegacy: true
})
const beforeIds = db!.getInbox(100).map((message) => message.id)
const reconstructed = db!.commitLegacyLifecycleOperation({
principalId: state.workerPrincipalId,
operationKey: 'read_completion_after_takeover',
method: 'orchestration.send',
payloadHash: 'read_completion_after_takeover_payload',
message: {
existingId: accepted.id,
to: 'term_legacy_coord',
subject: 'Completed',
body: 'accepted before takeover',
type: 'worker_done'
},
lifecycle: {
kind: 'worker_report',
taskId,
outcome: 'succeeded',
result: 'accepted before takeover'
}
})
expect(reconstructed).toMatchObject({
duplicate: false,
message: {
id: accepted.id,
to_handle: 'term_legacy_coord',
delivery_contract: 'legacy_direct'
},
settlement: { action: 'settled', outcome: 'succeeded', duplicate: true }
})
expect(db!.getInbox(100).map((message) => message.id)).toEqual(beforeIds)
})
it('rejects cross-cutover completion reconstruction for another recipient', () => {
const state = openAdoptedFixture()
const taskId = db!.getDispatchContextById(state.fixture.legacyDispatchId)!.task_id
const payload = JSON.stringify({ taskId, dispatchId: state.fixture.legacyDispatchId })
const foreign = db!.insertMessage({
runId: state.adoptedRunId,
deliveryContract: 'legacy_direct',
from: 'term_legacy_worker',
to: 'term_other_coord',
subject: 'Completed',
body: 'accepted elsewhere',
type: 'worker_done',
payload
})
db!.settleWorkerReport({
taskId,
dispatchId: state.fixture.legacyDispatchId,
outcome: 'succeeded',
result: 'accepted elsewhere'
})
const beforeIds = db!.getInbox(100).map((message) => message.id)
expect(
db!.findLegacyWorkerCompletion({
principalId: state.workerPrincipalId,
taskId,
recipientHandle: 'term_legacy_coord',
subject: 'Completed',
body: 'accepted elsewhere',
payload
})
).toBeUndefined()
expect(() =>
db!.commitLegacyLifecycleOperation({
principalId: state.workerPrincipalId,
operationKey: 'wrong_recipient_retry',
method: 'orchestration.send',
payloadHash: 'wrong_recipient_retry_payload',
message: {
to: 'term_legacy_coord',
subject: 'Completed',
body: 'accepted elsewhere',
type: 'worker_done',
payload
},
lifecycle: {
kind: 'worker_report',
taskId,
outcome: 'succeeded',
result: 'accepted elsewhere'
}
})
).toThrow(/settled/)
expect(() =>
db!.commitLegacyLifecycleOperation({
principalId: state.workerPrincipalId,
operationKey: 'wrong_recipient_reconstruction',
method: 'orchestration.send',
payloadHash: 'wrong_recipient_payload',
message: {
existingId: foreign.id,
to: 'term_legacy_coord',
subject: 'Completed',
body: 'accepted elsewhere',
type: 'worker_done',
payload
},
lifecycle: {
kind: 'worker_report',
taskId,
outcome: 'succeeded',
result: 'accepted elsewhere'
}
})
).toThrow(/does not match this principal/)
expect(db!.getInbox(100).map((message) => message.id)).toEqual(beforeIds)
expect(
db!.getLegacyOperationReceipt(state.workerPrincipalId, 'wrong_recipient_reconstruction')
).toBeUndefined()
expect(
db!.getLegacyOperationReceipt(state.workerPrincipalId, 'wrong_recipient_retry')
).toBeUndefined()
expect(db!.getTask(taskId)).toMatchObject({
status: 'completed',
result: 'accepted elsewhere'
})
})
it('uses invocation identity for repeated asks and atomically conflicts divergent replies', () => {
const state = openAdoptedFixture()
const ask = {
principalId: state.workerPrincipalId,
operationKey: 'ask_invocation_1',
method: 'orchestration.ask',
payloadHash: 'ask_payload',
question: 'Same text?',
options: ['yes', 'no'],
recipientHandle: 'term_legacy_coord'
}
const first = db!.commitLegacyAskOperation(ask)
const replay = db!.commitLegacyAskOperation(ask)
const repeated = db!.commitLegacyAskOperation({
...ask,
operationKey: 'ask_invocation_2'
})
expect(replay).toMatchObject({ duplicate: true, question: { message_id: first.message.id } })
expect(repeated.message.id).not.toBe(first.message.id)
expect(db!.findPendingLegacyQuestions(ask)).toHaveLength(2)
expect(
db!.findPendingLegacyQuestions({
...ask,
question: ' Same text?\r\n',
options: ['yes ', ' no']
})
).toHaveLength(2)
const inherited = db!.createQuestion({
runId: state.adoptedRunId,
dispatchId: state.fixture.legacyDispatchId,
askerHandle: 'term_legacy_worker',
question: 'Inherited?'
})
const sqlite = (db as unknown as { db: Database.Database }).db
sqlite
.prepare(
`UPDATE messages
SET from_handle = 'term_legacy_worker', to_handle = 'term_legacy_coord',
delivery_contract = 'legacy_direct'
WHERE id = ?`
)
.run(inherited.message.id)
const inheritedAsk = {
...ask,
operationKey: 'ask_inherited_1',
payloadHash: 'ask_inherited_payload_1',
question: 'Inherited?',
options: [],
existingQuestionId: inherited.message.id
}
const adopted = db!.commitLegacyAskOperation(inheritedAsk)
const distinct = db!.commitLegacyAskOperation({
...inheritedAsk,
operationKey: 'ask_inherited_2',
payloadHash: 'ask_inherited_payload_2'
})
expect(adopted.message.id).toBe(inherited.message.id)
expect(distinct.message.id).not.toBe(inherited.message.id)
const reply = {
principalId: state.coordinatorPrincipalId,
operationKey: 'reply_invocation_1',
method: 'orchestration.reply',
payloadHash: 'reply_payload',
questionId: first.message.id,
body: 'yes'
}
const answered = db!.commitLegacyReplyOperation(reply)
const answerReplay = db!.commitLegacyReplyOperation(reply)
expect(answerReplay).toMatchObject({
duplicate: true,
message: { id: answered.message.id }
})
expect(
db!
.findLegacyQuestionsBySemanticIdentity(ask)
.find((row) => row.question.message_id === first.message.id)
).toMatchObject({ question: { status: 'answered' }, answerAcknowledged: false })
db!.acknowledgeLegacyQuestionAnswer({
principalId: state.workerPrincipalId,
questionId: first.message.id,
answerMessageId: answered.message.id
})
expect(
db!
.findLegacyQuestionsBySemanticIdentity(ask)
.find((row) => row.question.message_id === first.message.id)
).toMatchObject({ answerAcknowledged: true })
expect(() =>
db!.commitLegacyReplyOperation({
...reply,
operationKey: 'reply_invocation_2',
payloadHash: 'different_reply',
body: 'no'
})
).toThrow(/different answer/)
const currentTask = db!.createTask({ runId: state.adoptedRunId, spec: 'current retry' })
const currentDispatch = db!.createDispatchContext(currentTask.id, 'term_current_retry')
const currentQuestion = db!.createQuestion({
runId: state.adoptedRunId,
dispatchId: currentDispatch.id,
askerHandle: 'term_current_retry',
question: 'Current question?'
})
expect(() =>
db!.commitLegacyReplyOperation({
...reply,
operationKey: 'reply_current_question',
payloadHash: 'current_question',
questionId: currentQuestion.message.id
})
).toThrow(/not actionable/)
})
})
@@ -0,0 +1,206 @@
import { mkdtempSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import Database from '../../sqlite/sync-database'
import { LEGACY_RUN_ID, OrchestrationDb } from './db'
export type LegacyStorageCutoverFixture = {
dbPath: string
currentRunId: string
unrelatedRunId: string
currentDispatchId: string
legacyTaskId: string
legacyDispatchId: string
legacyGateId: string
legacyMessageIds: string[]
legacyQuestionId: string
legacyDeliveryId: string
rejectionMessageId: string
lookalikeMessageId: string
malformedRejectionMessageIds: string[]
}
export function createLegacyStorageCutoverFixture(): {
fixture: LegacyStorageCutoverFixture
tempDir: string
} {
const tempDir = mkdtempSync(join(tmpdir(), 'orca-legacy-storage-'))
const dbPath = join(tempDir, 'orchestration.db')
const first = new OrchestrationDb(dbPath)
const currentRun = first.createRun({
objective: 'Current work',
coordinatorHandle: 'term_current_coord',
coordinatorPaneKey: 'tab_current:11111111-1111-4111-8111-111111111111'
})
const currentTask = first.createTask({ spec: 'current', runId: currentRun.id })
const unrelatedRun = first.createRun({
objective: 'Unrelated current work',
coordinatorHandle: 'term_unrelated_coord',
coordinatorPaneKey: 'tab_unrelated:55555555-5555-4555-8555-555555555555'
})
const currentDispatch = first.createDispatchContext(
currentTask.id,
'term_current_worker',
'tab_current:22222222-2222-4222-9222-222222222222',
'current_launch_hash'
)
first.insertMessage({
runId: currentRun.id,
from: 'term_current_worker',
to: `run:${currentRun.id}`,
subject: 'current mail'
})
const legacyTask = first.createTask({
spec: 'legacy',
createdByTerminalHandle: 'term_legacy_coord'
})
first.createDispatchContext(
legacyTask.id,
'term_legacy_worker',
'tab_legacy:33333333-3333-4333-8333-333333333333'
)
const legacyGate = first.createGate({
taskId: legacyTask.id,
question: 'Retained gate?'
})
first.resolveGate(legacyGate.id, 'continue')
const retryDispatch = first.createDispatchContext(
legacyTask.id,
'term_legacy_worker',
'tab_legacy:33333333-3333-4333-8333-333333333333'
)
const legacyMessages = [
first.insertMessage({
from: 'term_legacy_coord',
to: 'term_legacy_worker',
subject: 'read worker mail'
}),
first.insertMessage({
from: 'term_legacy_worker',
to: 'term_legacy_coord',
subject: 'read coordinator mail'
}),
first.insertMessage({
from: 'term_legacy_coord',
to: 'term_legacy_worker',
subject: 'second worker page'
})
]
first.markAsRead(legacyMessages.map((message) => message.id))
const question = first.createQuestion({
runId: LEGACY_RUN_ID,
dispatchId: retryDispatch.id,
askerHandle: 'term_legacy_worker',
question: 'Retained question?'
})
const rejection = first.insertMessage({
from: 'term_legacy_worker',
to: 'term_legacy_coord',
subject: 'Rejected heartbeat',
type: 'heartbeat',
payload: JSON.stringify({ _orcaLifecycleRejection: { code: 'migration', reason: 'cutover' } })
})
const lookalike = first.insertMessage({
from: 'term_legacy_worker',
to: 'term_legacy_coord',
subject: 'Ordinary legacy mail',
payload: JSON.stringify({
userData: { _orcaLifecycleRejection: { code: 'not-a-top-level-audit-marker' } }
})
})
const malformedRejections = [
first.insertMessage({
from: 'term_legacy_worker',
to: 'term_legacy_coord',
subject: 'Invalid JSON marker',
payload: '{"_orcaLifecycleRejection":'
}),
first.insertMessage({
from: 'term_legacy_worker',
to: 'term_legacy_coord',
subject: 'Array marker',
payload: JSON.stringify({ _orcaLifecycleRejection: [] })
}),
first.insertMessage({
from: 'term_legacy_worker',
to: 'term_legacy_coord',
subject: 'String marker',
payload: JSON.stringify({ _orcaLifecycleRejection: 'migration' })
}),
first.insertMessage({
from: 'term_legacy_worker',
to: 'term_legacy_coord',
subject: 'Incomplete marker',
payload: JSON.stringify({ _orcaLifecycleRejection: { code: 'migration' } })
}),
first.insertMessage({
from: 'term_legacy_worker',
to: 'term_legacy_coord',
subject: 'Non-string marker fields',
payload: JSON.stringify({ _orcaLifecycleRejection: { code: 19, reason: false } })
}),
first.insertMessage({
from: 'term_legacy_worker',
to: 'term_legacy_coord',
subject: 'Array root',
payload: JSON.stringify([
{ _orcaLifecycleRejection: { code: 'migration', reason: 'nested' } }
])
}),
first.insertMessage({
from: 'term_legacy_worker',
to: 'term_legacy_coord',
subject: 'String root',
payload: JSON.stringify('_orcaLifecycleRejection')
})
]
first.close()
const raw = new Database(dbPath)
const legacyDeliveryId = 'delivery_legacy_outstanding'
raw
.prepare(
`INSERT INTO deliveries (
id, run_id, consumer_generation, message_ids, status
) VALUES (?, ?, 0, ?, 'outstanding')`
)
.run(legacyDeliveryId, LEGACY_RUN_ID, JSON.stringify([legacyMessages[0].id]))
raw
.prepare("UPDATE messages SET delivery_contract = 'legacy_direct' WHERE id = ?")
.run(rejection.id)
const seedAuditOnly = raw.prepare(
"UPDATE messages SET delivery_contract = 'audit_only' WHERE id = ?"
)
for (const message of [lookalike, ...malformedRejections]) {
seedAuditOnly.run(message.id)
}
raw.exec(`
DROP INDEX IF EXISTS idx_messages_delivery_contract;
DROP TABLE legacy_mail_receipts;
DROP TABLE legacy_operation_receipts;
DROP TABLE legacy_compatibility_principals;
DROP TABLE legacy_adoptions;
`)
raw.pragma('user_version = 18')
raw.close()
return {
tempDir,
fixture: {
dbPath,
currentRunId: currentRun.id,
unrelatedRunId: unrelatedRun.id,
currentDispatchId: currentDispatch.id,
legacyTaskId: legacyTask.id,
legacyDispatchId: retryDispatch.id,
legacyGateId: legacyGate.id,
legacyMessageIds: legacyMessages.map((message) => message.id),
legacyQuestionId: question.message.id,
legacyDeliveryId,
rejectionMessageId: rejection.id,
lookalikeMessageId: lookalike.id,
malformedRejectionMessageIds: malformedRejections.map((message) => message.id)
}
}
}
@@ -0,0 +1,88 @@
import { describe, expect, it } from 'vitest'
import type { LegacyWorkerTerminalRecoveryRow } from './types'
import { planLegacyWorkerTerminalRecovery } from './orchestration-legacy-worker-terminal-recovery'
const LEAF_ID = '11111111-1111-4111-8111-111111111111'
const INCARNATION_ID = '22222222-2222-4222-8222-222222222222'
function recoveryRow(
overrides: Partial<LegacyWorkerTerminalRecoveryRow> = {}
): LegacyWorkerTerminalRecoveryRow {
return {
dispatch_id: 'dispatch-1',
task_id: 'task-1',
dispatch_status: 'completed',
contract_version: 0,
assignee_handle: 'term-worker',
assignee_pane_key: `tab-worker:${LEAF_ID}`,
process_incarnation: `ssh:ssh-1@@pty-worker:${INCARNATION_ID}`,
worker_state: 'ready',
worktree_id: 'repo::/workspace',
agent_terminal_handle: 'term-worker',
...overrides
}
}
describe('legacy worker terminal recovery planning', () => {
it('retains completed Dispatches when the worker process row is still live', () => {
expect(planLegacyWorkerTerminalRecovery([recoveryRow()])).toEqual({
blockedPanes: [
{
worktreeId: 'repo::/workspace',
paneKey: `tab-worker:${LEAF_ID}`,
contractVersion: 0
}
],
candidates: [
expect.objectContaining({
dispatchId: 'dispatch-1',
ptyId: 'ssh:ssh-1@@pty-worker',
incarnationId: INCARNATION_ID
})
],
ambiguousDispatchIds: []
})
})
it('blocks resume but refuses recovery when durable handles disagree', () => {
expect(
planLegacyWorkerTerminalRecovery([recoveryRow({ agent_terminal_handle: 'term-replacement' })])
).toEqual({
blockedPanes: [
{
worktreeId: 'repo::/workspace',
paneKey: `tab-worker:${LEAF_ID}`,
contractVersion: 0
}
],
candidates: [],
ambiguousDispatchIds: []
})
})
it('fails closed when two Dispatches claim one terminal identity', () => {
const plan = planLegacyWorkerTerminalRecovery([
recoveryRow(),
recoveryRow({ dispatch_id: 'dispatch-2', task_id: 'task-2' })
])
expect(plan.candidates).toEqual([])
expect(plan.ambiguousDispatchIds).toEqual(['dispatch-1', 'dispatch-2'])
expect(plan.blockedPanes).toHaveLength(1)
})
it('does not trust malformed pane or process identities', () => {
const plan = planLegacyWorkerTerminalRecovery([
recoveryRow({
assignee_pane_key: 'tab-worker:1',
process_incarnation: 'runtime:pty:generation'
})
])
expect(plan).toEqual({
blockedPanes: [],
candidates: [],
ambiguousDispatchIds: []
})
})
})
@@ -0,0 +1,119 @@
import { isPtyIncarnationId, type PtyIncarnationId } from '../../../shared/pty-incarnation'
import { parsePaneKey } from '../../../shared/stable-pane-id'
import type { LegacyWorkerTerminalRecoveryRow } from './types'
export type LegacyWorkerTerminalRecoveryCandidate = {
dispatchId: string
dispatchStatus: LegacyWorkerTerminalRecoveryRow['dispatch_status']
contractVersion: number
taskId: string
worktreeId: string
terminalHandle: string
paneKey: string
tabId: string
leafId: string
processIncarnation: string
ptyId: string
incarnationId: PtyIncarnationId
}
export type LegacyWorkerTerminalRecoveryPlan = {
blockedPanes: { worktreeId: string; paneKey: string; contractVersion: number }[]
candidates: LegacyWorkerTerminalRecoveryCandidate[]
ambiguousDispatchIds: string[]
}
function parseProcessIncarnation(
value: string
): { ptyId: string; incarnationId: PtyIncarnationId } | null {
const separator = value.lastIndexOf(':')
if (separator <= 0) {
return null
}
const ptyId = value.slice(0, separator)
const incarnationId = value.slice(separator + 1)
return ptyId && isPtyIncarnationId(incarnationId) ? { ptyId, incarnationId } : null
}
function countCandidateKeys(
candidates: readonly LegacyWorkerTerminalRecoveryCandidate[],
select: (candidate: LegacyWorkerTerminalRecoveryCandidate) => string
): Map<string, number> {
const counts = new Map<string, number>()
for (const candidate of candidates) {
const key = select(candidate)
counts.set(key, (counts.get(key) ?? 0) + 1)
}
return counts
}
export function planLegacyWorkerTerminalRecovery(
rows: readonly LegacyWorkerTerminalRecoveryRow[]
): LegacyWorkerTerminalRecoveryPlan {
const blockedPanes = new Map<
string,
{ worktreeId: string; paneKey: string; contractVersion: number }
>()
const parsedCandidates: LegacyWorkerTerminalRecoveryCandidate[] = []
for (const row of rows) {
const worktreeId = row.worktree_id?.trim()
const paneKey = row.assignee_pane_key?.trim()
const pane = paneKey ? parsePaneKey(paneKey) : null
if (worktreeId && paneKey && pane) {
blockedPanes.set(`${worktreeId}\0${paneKey}`, {
worktreeId,
paneKey,
contractVersion: row.contract_version
})
}
const terminalHandle = row.assignee_handle?.trim()
const workerHandle = row.agent_terminal_handle?.trim()
const processIncarnation = row.process_incarnation?.trim()
const process = processIncarnation ? parseProcessIncarnation(processIncarnation) : null
if (
!worktreeId ||
!paneKey ||
!pane ||
!terminalHandle ||
terminalHandle !== workerHandle ||
!processIncarnation ||
!process
) {
continue
}
parsedCandidates.push({
dispatchId: row.dispatch_id,
dispatchStatus: row.dispatch_status,
contractVersion: row.contract_version,
taskId: row.task_id,
worktreeId,
terminalHandle,
paneKey,
tabId: pane.tabId,
leafId: pane.leafId,
processIncarnation,
ptyId: process.ptyId,
incarnationId: process.incarnationId
})
}
const identityCounts = [
countCandidateKeys(parsedCandidates, (candidate) => candidate.terminalHandle),
countCandidateKeys(parsedCandidates, (candidate) => candidate.paneKey),
countCandidateKeys(parsedCandidates, (candidate) => candidate.processIncarnation)
]
const ambiguousDispatchIds = new Set<string>()
const candidates = parsedCandidates.filter((candidate) => {
const keys = [candidate.terminalHandle, candidate.paneKey, candidate.processIncarnation]
const ambiguous = keys.some((key, index) => (identityCounts[index]?.get(key) ?? 0) !== 1)
if (ambiguous) {
ambiguousDispatchIds.add(candidate.dispatchId)
}
return !ambiguous
})
return {
blockedPanes: [...blockedPanes.values()],
candidates,
ambiguousDispatchIds: [...ambiguousDispatchIds]
}
}
@@ -2,8 +2,12 @@ import type Database from '../../sqlite/sync-database'
const POST_V6_COLUMNS = [
['messages', 'run_id'],
['messages', 'delivery_contract'],
['coordinator_runs', 'scheduler_lost_at'],
['tasks', 'run_id'],
['dispatch_contexts', 'run_id'],
['dispatch_contexts', 'contract_version'],
['dispatch_contexts', 'launch_token_hash'],
['dispatch_contexts', 'capability_hash'],
['dispatch_contexts', 'process_incarnation'],
['dispatch_contexts', 'capability_revoked_at'],
@@ -14,11 +18,16 @@ const POST_V6_COLUMNS = [
['remote_dispatch_attachments', 'to_worker_imported_sequence'],
['remote_dispatch_attachments', 'protocol_version'],
['federation_relay_items', 'dispatch_id'],
['remote_questions', 'message_id']
['remote_questions', 'message_id'],
['legacy_adoptions', 'source_run_id'],
['legacy_compatibility_principals', 'id'],
['legacy_operation_receipts', 'principal_id'],
['legacy_mail_receipts', 'principal_id']
] as const
const POST_V6_INDEXES = [
'idx_messages_run_sequence',
'idx_messages_delivery_contract',
'idx_tasks_run_status',
'idx_dispatch_run_status',
'idx_gates_run_status',
@@ -46,11 +55,39 @@ function messagesAllowQuestions(db: Database.Database): boolean {
return !!row && row.sql.includes("'question'")
}
function hasConsistentLegacyAdoption(db: Database.Database): boolean {
const sourceRunId = 'run_legacy_local'
const sourceGraph = db
.prepare(
`SELECT 1
WHERE EXISTS(SELECT 1 FROM tasks WHERE run_id = ?)
OR EXISTS(SELECT 1 FROM dispatch_contexts WHERE run_id = ?)
OR EXISTS(SELECT 1 FROM decision_gates WHERE run_id = ?)
OR EXISTS(SELECT 1 FROM messages WHERE run_id = ?)
OR EXISTS(SELECT 1 FROM question_threads WHERE run_id = ?)
OR EXISTS(SELECT 1 FROM deliveries WHERE run_id = ?)`
)
.get(sourceRunId, sourceRunId, sourceRunId, sourceRunId, sourceRunId, sourceRunId)
const adoption = db
.prepare('SELECT adopted_run_id FROM legacy_adoptions WHERE source_run_id = ?')
.get(sourceRunId) as { adopted_run_id: string } | undefined
if (sourceGraph) {
return false
}
if (adoption) {
return Boolean(
db.prepare('SELECT 1 FROM runs WHERE id = ? AND legacy = 0').get(adoption.adopted_run_id)
)
}
return true
}
function hasCompletePostV6Schema(db: Database.Database): boolean {
return (
POST_V6_COLUMNS.every(([table, column]) => hasOrchestrationColumn(db, table, column)) &&
POST_V6_INDEXES.every((index) => hasOrchestrationIndex(db, index)) &&
messagesAllowQuestions(db)
messagesAllowQuestions(db) &&
hasConsistentLegacyAdoption(db)
)
}
@@ -59,9 +96,12 @@ export function resolveOrchestrationMigrationStartVersion(
storedVersion: number,
schemaVersion: number
): number {
if (storedVersion >= schemaVersion || hasCompletePostV6Schema(db)) {
if (storedVersion > schemaVersion) {
return storedVersion
}
if (hasCompletePostV6Schema(db)) {
return storedVersion
}
// Why: version-skewed pre-Run databases can claim the post-v6 range while retaining v6 tables.
return Math.min(storedVersion, 6)
return Math.min(storedVersion, schemaVersion, 6)
}
@@ -3,7 +3,8 @@ import { tmpdir } from 'node:os'
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 { LEGACY_CONTRACT_VERSION, LEGACY_RUN_ID, OrchestrationDb } from './db'
import { resolveOrchestrationMigrationStartVersion } from './orchestration-schema-version-skew'
describe('OrchestrationDb version-skew migration', () => {
let db: OrchestrationDb | undefined
@@ -11,12 +12,14 @@ describe('OrchestrationDb version-skew migration', () => {
afterEach(() => {
db?.close()
db = undefined
if (tempDir) {
rmSync(tempDir, { recursive: true, force: true })
tempDir = undefined
}
})
function createLegacySchemaClaimingVersion17(): string {
function createLegacySchemaClaimingVersion(claimedVersion = 17): string {
tempDir = mkdtempSync(join(tmpdir(), 'orca-db-version-skew-'))
const dbPath = join(tempDir, 'orchestration.db')
const raw = new Database(dbPath)
@@ -126,20 +129,28 @@ describe('OrchestrationDb version-skew migration', () => {
'gate_legacy', 'task_legacy', 'retained gate'
);
`)
raw.pragma('user_version = 17')
raw.pragma(`user_version = ${claimedVersion}`)
raw.close()
return dbPath
}
it('repairs retained v6 rows when the database already claims v17', () => {
const dbPath = createLegacySchemaClaimingVersion17()
const dbPath = createLegacySchemaClaimingVersion()
db = new OrchestrationDb(dbPath)
const adoptedRunId = db.getLegacyAdoption()?.adopted_run_id
expect(adoptedRunId).toBeTruthy()
expect(db.getRun(LEGACY_RUN_ID)).toMatchObject({ legacy: 1 })
expect(db.getMessageById('msg_legacy')).toMatchObject({ run_id: LEGACY_RUN_ID })
expect(db.getTask('task_legacy')).toMatchObject({ run_id: LEGACY_RUN_ID })
expect(db.getDispatchContextById('ctx_legacy')).toMatchObject({ run_id: LEGACY_RUN_ID })
expect(db.getGate('gate_legacy')).toMatchObject({ run_id: LEGACY_RUN_ID })
expect(db.getMessageById('msg_legacy')).toMatchObject({
run_id: adoptedRunId,
delivery_contract: 'legacy_direct'
})
expect(db.getTask('task_legacy')).toMatchObject({ run_id: adoptedRunId })
expect(db.getDispatchContextById('ctx_legacy')).toMatchObject({
run_id: adoptedRunId,
contract_version: LEGACY_CONTRACT_VERSION
})
expect(db.getGate('gate_legacy')).toMatchObject({ run_id: adoptedRunId })
const run = db.createRun({
objective: 'verify repaired orchestration',
@@ -164,8 +175,18 @@ describe('OrchestrationDb version-skew migration', () => {
db.close()
db = undefined
db = new OrchestrationDb(dbPath)
expect(db.listTasks({ runId: LEGACY_RUN_ID }).map((row) => row.id)).toEqual(['task_legacy'])
expect(db.listTasks({ runId: adoptedRunId }).map((row) => row.id)).toEqual(['task_legacy'])
expect(db.getRun(run.id)).toBeDefined()
expect(db.getQuestion(question.message.id)).toMatchObject({ status: 'pending' })
})
it('does not repair an incomplete schema written by a future binary', () => {
const dbPath = createLegacySchemaClaimingVersion(20)
const raw = new Database(dbPath)
expect(resolveOrchestrationMigrationStartVersion(raw, 20, 19)).toBe(20)
expect(raw.pragma('user_version', { simple: true })).toBe(20)
raw.close()
})
})
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it } from 'vitest'
import { OrchestrationDb } from './db'
import { CURRENT_CONTRACT_VERSION, OrchestrationDb } from './db'
describe('OrchestrationDb worker Dispatch state', () => {
let db: OrchestrationDb | undefined
@@ -44,6 +44,51 @@ describe('OrchestrationDb worker Dispatch state', () => {
status: 'dispatched',
assignee_handle: 'term_worker'
})
expect(d.listLegacyWorkerTerminalRecoveryRows()).toEqual([
expect.objectContaining({
dispatch_id: started.dispatch.id,
contract_version: CURRENT_CONTRACT_VERSION,
worker_state: 'ready',
agent_terminal_handle: 'term_worker'
})
])
})
it('requeues an active Task before settling a worker whose terminal is missing', () => {
const d = createDb()
const task = d.createTask({ spec: 'recover missing worker' })
const started = d.createStartingWorkerDispatch({
taskId: task.id,
startOptions: { topology: 'current', agent: 'codex' }
})
d.prepareStartingWorkerAuthority({
dispatchId: started.dispatch.id,
handle: 'term_missing',
paneKey: 'tab_missing:11111111-1111-4111-8111-111111111111',
processIncarnation: 'pty-missing:22222222-2222-4222-8222-222222222222',
worktreeId: 'repo::worktree',
setupState: 'not_applicable',
effects: []
})
d.markWorkerDispatchReady(started.dispatch.id)
expect(
d.reconcileMissingWorkerTerminal(started.dispatch.id, 'worker terminal is no longer live')
).toMatchObject({
state: 'abandoned',
stage: 'terminal_missing',
last_error: 'worker terminal is no longer live'
})
expect(d.getDispatchContextById(started.dispatch.id)).toMatchObject({
status: 'failed',
failure_count: 1,
last_failure: 'worker terminal is no longer live'
})
expect(d.getTask(task.id)?.status).toBe('ready')
d.reconcileMissingWorkerTerminal(started.dispatch.id, 'duplicate recovery')
expect(d.getDispatchContextById(started.dispatch.id)?.failure_count).toBe(1)
expect(d.getTask(task.id)?.status).toBe('ready')
})
it('commits worker-start mutation acceptance with the starting Dispatch', () => {
+59
View File
@@ -14,6 +14,8 @@ export type MessageType = (typeof MESSAGE_TYPES)[number]
export type MessagePriority = 'normal' | 'high' | 'urgent'
export type MessageDeliveryContract = 'legacy_direct' | 'current_delivery' | 'audit_only'
export type TaskStatus = 'pending' | 'ready' | 'dispatched' | 'completed' | 'failed' | 'blocked'
export type DispatchStatus = 'pending' | 'dispatched' | 'completed' | 'failed' | 'circuit_broken'
@@ -61,6 +63,46 @@ export type DeliveryRow = {
acknowledged_at: string | null
}
export type LegacyAdoptionRow = {
source_run_id: string
adopted_run_id: string
scheduler_state_lost: number
adopted_at: string
}
export type LegacyPrincipalRole = 'worker' | 'coordinator'
export type LegacyPrincipalStatus = 'committed' | 'settled' | 'revoked'
export type LegacyCompatibilityPrincipalRow = {
id: string
run_id: string
dispatch_id: string | null
role: LegacyPrincipalRole
host_scope: string
terminal_handle: string
pane_key: string
launch_token_hash: string
process_incarnation: string | null
status: LegacyPrincipalStatus
}
export type LegacyOperationReceiptRow = {
principal_id: string
operation_key: string
method: string
payload_hash: string
effect_id: string
response_json: string
completed_at: string
}
export type LegacyMailReceiptRow = {
principal_id: string
message_id: string
acknowledged_at: string | null
}
export type QuestionStatus = 'pending' | 'answered' | 'closed'
export type QuestionRow = {
@@ -117,6 +159,19 @@ export type WorkerDispatchRow = {
updated_at: string
}
export type LegacyWorkerTerminalRecoveryRow = {
dispatch_id: string
task_id: string
dispatch_status: DispatchStatus
contract_version: number
assignee_handle: string | null
assignee_pane_key: string | null
process_incarnation: string | null
worker_state: WorkerDispatchState
worktree_id: string | null
agent_terminal_handle: string | null
}
export type FederatedDispatchRow = {
dispatch_id: string
environment_id: string
@@ -170,6 +225,7 @@ export type FederationRelayItemRow = {
export type MessageRow = {
id: string
run_id: string
delivery_contract?: MessageDeliveryContract
from_handle: string
to_handle: string
subject: string
@@ -204,6 +260,8 @@ export type DispatchContextRow = {
id: string
run_id: string
task_id: string
contract_version: number
launch_token_hash: string | null
assignee_handle: string | null
assignee_pane_key: string | null
capability_hash: string | null
@@ -238,4 +296,5 @@ export type CoordinatorRun = {
poll_interval_ms: number
created_at: string
completed_at: string | null
scheduler_lost_at: string | null
}
+17 -1
View File
@@ -1,7 +1,7 @@
// Why: single boundary between raw RPC frames and OrcaRuntimeService; keeps schema, handler, and result type on one object.
import { ZodError, type ZodType } from 'zod'
import type { TerminalStreamFrame } from '../../../shared/terminal-stream-protocol'
import type { OrcaRuntimeService } from '../orca-runtime'
import type { OrcaRuntimeService, OrchestrationCompatibilityCallerAuthority } from '../orca-runtime'
import type {
DeviceCredentialInstalled,
PairingGetEndpointsParams,
@@ -9,6 +9,7 @@ import type {
PairingProvisionRelayParams
} from '../../../shared/mobile-relay-credential-contract'
import type { RuntimeCapability } from '../../../shared/protocol-version'
import type { OrchestrationCompatibilityEvidence } from '../../../shared/orchestration-compatibility-evidence'
export type PairingRpcContext = {
getEndpoints(params: PairingGetEndpointsParams): Promise<PairingGetEndpointsResult>
@@ -48,8 +49,18 @@ export type RpcRequest = {
orchestrationCapability?: string
orchestrationContractVersion?: number
orchestrationRequestId?: string
compatibilityInvocationId?: string
orchestrationCompatibilityEvidence?: OrchestrationCompatibilityEvidence
}
export type LegacyCoordinatorAuthorityProof = Readonly<{
runId: string
principalId: string | null
terminalHandle: string
paneKey: string
consumerGeneration: number
}>
export type RpcContext = {
runtime: OrcaRuntimeService
// Why: lets long-poll handlers release immediately on client disconnect instead of running down timeoutMs. See design doc §3.1.
@@ -77,6 +88,11 @@ export type RpcContext = {
method: string
payloadHash: string
}
// Why: only the compatibility authority router can set this trusted scope; user params cannot bypass Run consumer binding.
legacyCoordinatorRunId?: string
legacyCoordinatorAuthority?: LegacyCoordinatorAuthorityProof
revalidateLegacyCoordinator?: () => string
orchestrationCompatibilityCallerAuthority?: OrchestrationCompatibilityCallerAuthority
// Why: federation pins the authenticated saved-environment caller without exposing its token to handlers or storage.
authenticatedCallerFingerprint?: string
pairing?: PairingRpcContext
@@ -0,0 +1,43 @@
import { InvalidArgumentError, ZodError, formatZodError } from './core'
import type { RpcEnvelopeMeta, RpcRequest, RpcResponse } from './core'
import {
computerErrorData,
errorResponse,
mapBrowserError,
mapEmulatorError,
mapRuntimeError
} from './errors'
export function invalidArgumentResponse(
request: RpcRequest,
meta: RpcEnvelopeMeta,
message: string
): RpcResponse {
return errorResponse(
request.id,
meta,
'invalid_argument',
message,
request.method.startsWith('computer.') ? computerErrorData('invalid_argument') : undefined
)
}
export function mapDispatcherError(
request: RpcRequest,
meta: RpcEnvelopeMeta,
error: unknown
): RpcResponse {
if (error instanceof ZodError) {
return invalidArgumentResponse(request, meta, formatZodError(error))
}
if (error instanceof InvalidArgumentError) {
return invalidArgumentResponse(request, meta, error.message)
}
if (request.method.startsWith('browser.')) {
return mapBrowserError(request.id, meta, error)
}
if (request.method.startsWith('emulator.')) {
return mapEmulatorError(request.id, meta, error)
}
return mapRuntimeError(request.id, meta, error)
}
@@ -46,6 +46,10 @@ function makeRuntime(ui: PersistedUIState = getDefaultUIState()): OrcaRuntimeSer
updateUIState: vi.fn((updates: Partial<PersistedUIState>) => {
currentUI = { ...currentUI, ...updates }
return currentUI
}),
getOrchestrationDb: () => ({
getLegacyAdoption: () => undefined,
resolveLegacyWorkerCandidate: () => undefined
})
} as unknown as OrcaRuntimeService
}
@@ -0,0 +1,18 @@
import type { RuntimeCapability } from '../../../shared/protocol-version'
import type { TerminalStreamFrame } from '../../../shared/terminal-stream-protocol'
import type { PairingRpcContext } from './core'
export type RpcDispatchStreamingOptions = {
connectionId?: string
signal?: AbortSignal
clientId?: string
pairedDeviceId?: string
clientKind?: 'mobile' | 'runtime'
clientCapabilities?: readonly RuntimeCapability[]
pairing?: PairingRpcContext
sendBinary?: (bytes: Uint8Array<ArrayBufferLike>) => boolean | void
registerBinaryStreamHandler?: (
streamId: number,
handler: (frame: TerminalStreamFrame) => void
) => () => void
}
+100 -118
View File
@@ -1,57 +1,44 @@
// Why: the dispatcher is the one place that knows how to turn a validated
// RPC request into a response envelope. Splitting it from the transport
// makes it unit-testable without spinning up a socket, and keeps
// runtime-rpc.ts focused on framing/auth/connection bookkeeping.
import {
ZodError,
InvalidArgumentError,
buildRegistry,
formatZodError,
isStreamingMethod,
type RpcAnyMethod,
type RpcEnvelopeMeta,
type PairingRpcContext,
type RpcRegistry,
type RpcRequest,
type RpcResponse
} from './core'
import type { TerminalStreamFrame } from '../../../shared/terminal-stream-protocol'
import type { FeatureInteractionId } from '../../../shared/feature-interactions'
import {
computerErrorData,
errorResponse,
mapBrowserError,
mapEmulatorError,
mapRuntimeError,
successResponse
} from './errors'
import { errorResponse, successResponse } from './errors'
import { ALL_RPC_METHODS } from './methods'
import { emulatorProbe, emulatorProbeError } from '../../emulator/emulator-probe'
import type { OrcaRuntimeService } from '../orca-runtime'
import type { RuntimeCapability } from '../../../shared/protocol-version'
import {
OrchestrationMutationExecutor,
authenticatedCallerFingerprint,
getOrchestrationMutationExecutor,
type OrchestrationMutationExecutor,
type DurableMutationInvocation
} from './orchestration-mutation-executor'
import { orchestrationMigrationFence } from './orchestration-contract-fence'
import { getRuntimeFeatureInteractionId } from './runtime-feature-interaction'
import { recordRuntimeFeatureInteraction } from './runtime-feature-interaction'
import { OrchestrationLegacyCompatibility } from './orchestration-legacy-compatibility'
import type { RpcDispatchStreamingOptions } from './dispatcher-stream-options'
import { invalidArgumentResponse, mapDispatcherError } from './dispatcher-error-response'
export type DispatcherOptions = {
runtime: OrcaRuntimeService
methods?: readonly RpcAnyMethod[]
}
export type DispatcherOptions = { runtime: OrcaRuntimeService; methods?: readonly RpcAnyMethod[] }
export class RpcDispatcher {
private readonly runtime: OrcaRuntimeService
private readonly registry: RpcRegistry
private readonly orchestrationMutations: OrchestrationMutationExecutor
private readonly legacyOrchestration: OrchestrationLegacyCompatibility
constructor({ runtime, methods = ALL_RPC_METHODS }: DispatcherOptions) {
this.runtime = runtime
this.registry = buildRegistry(methods)
this.orchestrationMutations = new OrchestrationMutationExecutor(runtime)
this.orchestrationMutations = getOrchestrationMutationExecutor(runtime)
this.legacyOrchestration = new OrchestrationLegacyCompatibility(runtime)
}
async dispatch(request: RpcRequest, options?: { signal?: AbortSignal }): Promise<RpcResponse> {
@@ -76,9 +63,6 @@ export class RpcDispatcher {
return parsedParams.error
}
// Why: streaming methods are not supported over one-shot transports like
// Unix sockets. They require a reply function that can be called multiple
// times, which is only available via dispatchStreaming.
if (isStreamingMethod(method)) {
return errorResponse(
request.id,
@@ -88,29 +72,60 @@ export class RpcDispatcher {
)
}
const isEmulator = request.method.startsWith('emulator.')
if (isEmulator) {
if (request.method.startsWith('emulator.')) {
emulatorProbe(`rpc ${request.method}`, request.params)
}
try {
const invoke = (mutation?: DurableMutationInvocation) =>
method.handler(parsedParams.value, {
const compatibility = await this.legacyOrchestration.tryHandle(
request,
parsedParams.value,
options?.signal
)
if (compatibility.handled) {
return successResponse(request.id, meta, compatibility.result)
}
const effectiveParams = compatibility.params ?? parsedParams.value
const legacyCoordinator = this.legacyOrchestration.createCoordinatorInvocation(
request,
compatibility.legacyCoordinatorAuthority
)
const invoke = (mutation?: DurableMutationInvocation) => {
const legacyCoordinatorRunId = legacyCoordinator?.revalidate()
return method.handler(effectiveParams, {
runtime: this.runtime,
signal: options?.signal,
requestId: request.id,
orchestrationCapability: request.orchestrationCapability,
authenticatedCallerFingerprint: authenticatedCallerFingerprint(request),
authenticatedCallerFingerprint:
mutation?.identity.callerFingerprint ?? authenticatedCallerFingerprint(request),
recordMutationReceipt: mutation?.recordReceipt,
orchestrationMutation: mutation?.identity
orchestrationMutation: mutation?.identity,
legacyCoordinatorRunId,
legacyCoordinatorAuthority: legacyCoordinator?.authority,
revalidateLegacyCoordinator: legacyCoordinator?.revalidate,
orchestrationCompatibilityCallerAuthority:
compatibility.orchestrationCompatibilityCallerAuthority
})
const result = await this.orchestrationMutations.run(request, parsedParams.value, invoke)
this.recordRuntimeFeatureInteraction(request.method, result, undefined, request.params)
}
const result = await this.orchestrationMutations.run(
request,
effectiveParams,
invoke,
legacyCoordinator?.mutationCallerFingerprint
)
recordRuntimeFeatureInteraction(
this.runtime,
request.method,
result,
undefined,
request.params
)
return successResponse(request.id, meta, result)
} catch (error) {
if (isEmulator) {
if (request.method.startsWith('emulator.')) {
emulatorProbeError(`rpc ${request.method}`, error, { params: request.params })
}
return this.mapError(request, meta, error)
return mapDispatcherError(request, meta, error)
}
}
@@ -120,20 +135,7 @@ export class RpcDispatcher {
async dispatchStreaming(
request: RpcRequest,
reply: (response: string) => void,
options?: {
connectionId?: string
signal?: AbortSignal
clientId?: string
pairedDeviceId?: string
clientKind?: 'mobile' | 'runtime'
clientCapabilities?: readonly RuntimeCapability[]
pairing?: PairingRpcContext
sendBinary?: (bytes: Uint8Array<ArrayBufferLike>) => boolean | void
registerBinaryStreamHandler?: (
streamId: number,
handler: (frame: TerminalStreamFrame) => void
) => () => void
}
options?: RpcDispatchStreamingOptions
): Promise<void> {
const meta = this.meta()
const method = this.registry.get(request.method)
@@ -160,8 +162,23 @@ export class RpcDispatcher {
if (!isStreamingMethod(method)) {
try {
const invoke = (mutation?: DurableMutationInvocation) =>
method.handler(parsedParams.value, {
const compatibility = await this.legacyOrchestration.tryHandle(
request,
parsedParams.value,
options?.signal
)
if (compatibility.handled) {
reply(JSON.stringify(successResponse(request.id, meta, compatibility.result)))
return
}
const effectiveParams = compatibility.params ?? parsedParams.value
const legacyCoordinator = this.legacyOrchestration.createCoordinatorInvocation(
request,
compatibility.legacyCoordinatorAuthority
)
const invoke = (mutation?: DurableMutationInvocation) => {
const legacyCoordinatorRunId = legacyCoordinator?.revalidate()
return method.handler(effectiveParams, {
runtime: this.runtime,
signal: options?.signal,
requestId: request.id,
@@ -171,25 +188,44 @@ export class RpcDispatcher {
clientKind: options?.clientKind,
clientCapabilities: options?.clientCapabilities,
orchestrationCapability: request.orchestrationCapability,
authenticatedCallerFingerprint: authenticatedCallerFingerprint(request),
authenticatedCallerFingerprint:
mutation?.identity.callerFingerprint ?? authenticatedCallerFingerprint(request),
recordMutationReceipt: mutation?.recordReceipt,
orchestrationMutation: mutation?.identity,
pairing: options?.pairing,
sendBinary: options?.sendBinary,
registerBinaryStreamHandler: options?.registerBinaryStreamHandler
registerBinaryStreamHandler: options?.registerBinaryStreamHandler,
legacyCoordinatorRunId,
legacyCoordinatorAuthority: legacyCoordinator?.authority,
revalidateLegacyCoordinator: legacyCoordinator?.revalidate,
orchestrationCompatibilityCallerAuthority:
compatibility.orchestrationCompatibilityCallerAuthority
})
const result = await this.orchestrationMutations.run(request, parsedParams.value, invoke)
this.recordRuntimeFeatureInteraction(request.method, result, undefined, request.params)
}
const result = await this.orchestrationMutations.run(
request,
effectiveParams,
invoke,
legacyCoordinator?.mutationCallerFingerprint
)
recordRuntimeFeatureInteraction(
this.runtime,
request.method,
result,
undefined,
request.params
)
reply(JSON.stringify(successResponse(request.id, meta, result)))
} catch (error) {
reply(JSON.stringify(this.mapError(request, meta, error)))
reply(JSON.stringify(mapDispatcherError(request, meta, error)))
}
return
}
const recordedStreamingFeatureInteractions = new Set<FeatureInteractionId>()
const emit = (result: unknown): void => {
this.recordRuntimeFeatureInteraction(
recordRuntimeFeatureInteraction(
this.runtime,
request.method,
result,
recordedStreamingFeatureInteractions,
@@ -212,20 +248,22 @@ export class RpcDispatcher {
pairedDeviceId: options?.pairedDeviceId,
clientKind: options?.clientKind,
clientCapabilities: options?.clientCapabilities,
orchestrationCapability: request.orchestrationCapability,
pairing: options?.pairing,
sendBinary: options?.sendBinary,
registerBinaryStreamHandler: options?.registerBinaryStreamHandler
},
emit
)
this.recordRuntimeFeatureInteraction(
recordRuntimeFeatureInteraction(
this.runtime,
request.method,
result,
recordedStreamingFeatureInteractions,
request.params
)
} catch (error) {
reply(JSON.stringify(this.mapError(request, meta, error)))
reply(JSON.stringify(mapDispatcherError(request, meta, error)))
}
}
@@ -241,69 +279,13 @@ export class RpcDispatcher {
const result = method.params.safeParse(rawParams)
if (!result.success) {
return {
error: this.invalidArgumentResponse(request, meta, formatZodError(result.error))
error: invalidArgumentResponse(request, meta, formatZodError(result.error))
}
}
return { value: result.data }
}
private mapError(request: RpcRequest, meta: RpcEnvelopeMeta, error: unknown): RpcResponse {
if (error instanceof ZodError) {
return this.invalidArgumentResponse(request, meta, formatZodError(error))
}
if (error instanceof InvalidArgumentError) {
return this.invalidArgumentResponse(request, meta, error.message)
}
// Why: browser methods throw BrowserError with a structured `code`;
// every other runtime error has a plain-message code. Routing by method
// prefix keeps the mapping a single decision rather than a per-method
// flag callers must remember to set.
if (request.method.startsWith('browser.')) {
return mapBrowserError(request.id, meta, error)
}
if (request.method.startsWith('emulator.')) {
return mapEmulatorError(request.id, meta, error)
}
return mapRuntimeError(request.id, meta, error)
}
private invalidArgumentResponse(
request: RpcRequest,
meta: RpcEnvelopeMeta,
message: string
): RpcResponse {
return errorResponse(
request.id,
meta,
'invalid_argument',
message,
request.method.startsWith('computer.') ? computerErrorData('invalid_argument') : undefined
)
}
private meta(): RpcEnvelopeMeta {
return { runtimeId: this.runtime.getRuntimeId() }
}
private recordRuntimeFeatureInteraction(
method: string,
result: unknown,
alreadyRecorded?: Set<FeatureInteractionId>,
rawParams?: unknown
): void {
const id = getRuntimeFeatureInteractionId(method, result, rawParams)
if (!id) {
return
}
if (alreadyRecorded?.has(id)) {
return
}
try {
this.runtime.recordFeatureInteraction(id)
alreadyRecorded?.add(id)
} catch {
// Best-effort education state must not break runtime tools.
}
}
}
+1
View File
@@ -88,6 +88,7 @@ const STRUCTURED_RUNTIME_PASSTHROUGH_CODES: ReadonlySet<string> = new Set([
'agent_unconfigured',
'terminal_worktree_mismatch',
'request_mismatch',
'legacy_read_only',
'orchestration_migration_required',
'operation_unknown',
'question_not_found',
@@ -265,7 +265,7 @@ describe('orchestration federation control mail', () => {
})
await expect(waiting).resolves.toMatchObject({
ok: true,
result: { count: 0, timedOut: true }
result: { count: 0 }
})
})
@@ -224,6 +224,7 @@ describe('orchestration federation', () => {
])
)
expect(workerDb.listTasks()).toHaveLength(0)
expect(vi.mocked(workerRuntime.createManagedWorktree).mock.calls[0]?.[0].activate).toBe(false)
expect(workerRuntime.sendTerminalAgentPrompt).toHaveBeenCalledWith(
'term_windows_worker',
expect.stringContaining(`Your task ID is: ${task.id}`)
@@ -103,8 +103,12 @@ export const ORCHESTRATION_GATE_METHODS: RpcMethod[] = [
defineMethod({
name: 'orchestration.gateCreate',
params: GateCreateParams,
handler: (params, { runtime }) => {
handler: (params, { runtime, legacyCoordinatorRunId }) => {
const db = runtime.getOrchestrationDb()
const task = db.getTask(params.task)
if (legacyCoordinatorRunId && task?.run_id !== legacyCoordinatorRunId) {
throw new Error(`Task not found: ${params.task}`)
}
let options: string[] | undefined
if (params.options) {
try {
@@ -129,8 +133,12 @@ export const ORCHESTRATION_GATE_METHODS: RpcMethod[] = [
defineMethod({
name: 'orchestration.gateResolve',
params: GateResolveParams,
handler: (params, { runtime }) => {
handler: (params, { runtime, legacyCoordinatorRunId }) => {
const db = runtime.getOrchestrationDb()
const existing = db.getGate(params.id)
if (legacyCoordinatorRunId && existing?.run_id !== legacyCoordinatorRunId) {
throw new Error(`Gate not found: ${params.id}`)
}
const gate = db.resolveGate(params.id, params.resolution)
if (!gate) {
throw new Error(`Gate not found: ${params.id}`)
@@ -1,6 +1,6 @@
import { z } from 'zod'
import { defineMethod, type RpcMethod } from '../core'
import { OptionalString, requiredString } from '../schemas'
import { OptionalBoolean, OptionalString, requiredString } from '../schemas'
import type { OrcaRuntimeService } from '../../orca-runtime'
import { OrchestrationError } from '../../orchestration/orchestration-error'
@@ -11,7 +11,8 @@ const RunCreateParams = z.object({
const RunUseParams = z.object({
id: requiredString('Missing --id'),
from: requiredString('Missing coordinator terminal')
from: requiredString('Missing coordinator terminal'),
takeoverLegacy: OptionalBoolean
})
const RunCurrentParams = z.object({ from: requiredString('Missing coordinator terminal') })
@@ -51,14 +52,33 @@ export const ORCHESTRATION_RUN_METHODS: RpcMethod[] = [
defineMethod({
name: 'orchestration.runUse',
params: RunUseParams,
handler: (params, { runtime }) => {
handler: (
params,
{
runtime,
legacyCoordinatorAuthority,
orchestrationCompatibilityCallerAuthority: callerAuthority
}
) => {
const paneKey = requireCallerPane(runtime, params.from)
if (
params.takeoverLegacy &&
(callerAuthority?.terminalHandle !== params.from || callerAuthority.paneKey !== paneKey)
) {
throw new OrchestrationError(
'legacy_read_only',
'Legacy takeover must be invoked by the live coordinator agent terminal it will bind. No effects were applied.',
{ effectsApplied: false }
)
}
const db = runtime.getOrchestrationDb()
const priorRun = db.getCurrentRunForPane(paneKey)
const run = db.bindRun({
runId: params.id,
coordinatorHandle: params.from,
coordinatorPaneKey: paneKey
coordinatorPaneKey: paneKey,
takeoverLegacy: params.takeoverLegacy,
legacyCoordinatorAuthority
})
if (!run) {
throw new OrchestrationError(
@@ -36,6 +36,21 @@ export type WorkerSetupReceipt = {
| 'not_applicable'
}
export function requireWorkerAuthority(runtime: OrcaRuntimeService, terminalHandle: string) {
const authority = runtime.getOrchestrationDispatchAuthority(terminalHandle)
const paneKey = authority?.paneKey ?? runtime.getTerminalPaneKey(terminalHandle)
const processIncarnation =
authority?.processIncarnation ?? runtime.getTerminalProcessIncarnation(terminalHandle)
if (!paneKey || !processIncarnation) {
throw new Error('stable_pane_required')
}
return {
paneKey,
processIncarnation,
...(authority?.launchTokenHash ? { launchTokenHash: authority.launchTokenHash } : {})
}
}
export async function createExistingWorktreeWorkerTerminal(args: {
runtime: OrcaRuntimeService
worktreeId: string
@@ -10,6 +10,7 @@ import {
createExistingWorktreeWorkerTerminal,
createWorkerWorktree,
monitorWorkerSetup,
requireWorkerAuthority,
type WorkerEffect,
type WorkerSetupReceipt
} from './orchestration-worker-topology'
@@ -235,16 +236,11 @@ export const ORCHESTRATION_WORKER_START_METHODS: RpcMethod[] = [
: `Agent did not become ready (${wait.status}).`
)
}
const paneKey = runtime.getTerminalPaneKey(terminalHandle)
const processIncarnation = runtime.getTerminalProcessIncarnation(terminalHandle)
if (!paneKey || !processIncarnation) {
throw new Error('stable_pane_required')
}
const terminalAuthority = requireWorkerAuthority(runtime, terminalHandle)
const capability = db.prepareStartingWorkerAuthority({
dispatchId: started.dispatch.id,
handle: terminalHandle,
paneKey,
processIncarnation,
...terminalAuthority,
worktreeId: resolvedWorktree.id,
effects,
setupState: setupReceipt.state
@@ -1918,6 +1918,30 @@ describe('orchestration RPC methods', () => {
expect(db.getDispatchContextById(result.dispatch.id)?.assignee_pane_key).toBe('tab_w:leaf_w')
})
it('commits the target process launch token on a manual dispatch', async () => {
setup()
vi.spyOn(runtime, 'getOrchestrationDispatchAuthority').mockReturnValue({
runtimeId: runtime.getRuntimeId(),
terminalHandle: 'term_a',
ptyId: 'pty_a',
worktreeId: 'repo::worktree',
paneKey: 'tab_w:leaf_w',
processIncarnation: 'runtime_test:term_a:1',
launchTokenHash: 'launch-token-hash',
hostScope: { kind: 'local', hostId: 'local' }
})
const task = db.createTask({ spec: 'work' })
const result = (await call('orchestration.dispatch', {
task: task.id,
to: 'term_a'
})) as { dispatch: { id: string } }
expect(db.getDispatchContextById(result.dispatch.id)?.launch_token_hash).toBe(
'launch-token-hash'
)
})
it('rejects dispatch for a pending task', async () => {
setup()
const parent = db.createTask({ spec: 'parent' })
@@ -2162,6 +2186,32 @@ describe('orchestration RPC methods', () => {
)
})
it('commits the launched worker token with its durable authority', async () => {
setup()
mockCurrentWorkerStart()
vi.spyOn(runtime, 'getOrchestrationDispatchAuthority').mockReturnValue({
runtimeId: runtime.getRuntimeId(),
terminalHandle: 'term_worker',
ptyId: 'pty_worker',
worktreeId: 'repo::worktree',
paneKey: 'tab_worker:leaf_worker',
processIncarnation: 'runtime_test:term_worker:1',
launchTokenHash: 'worker-launch-token-hash',
hostScope: { kind: 'local', hostId: 'local' }
})
const task = db.createTask({ spec: 'persist worker identity' })
const result = (await call('orchestration.workerStart', {
task: task.id,
from: 'term_coord',
agent: 'codex'
})) as { dispatchId: string }
expect(db.getDispatchContextById(result.dispatchId)?.launch_token_hash).toBe(
'worker-launch-token-hash'
)
})
it('surfaces a worker terminal reveal failure without discarding the live worker', async () => {
setup()
mockCurrentWorkerStart()
+167 -21
View File
@@ -2,7 +2,12 @@
import { z } from 'zod'
import { defineMethod, type RpcMethod } from '../core'
import { OptionalFiniteNumber, OptionalString, OptionalBoolean, requiredString } from '../schemas'
import type { MessageType, MessagePriority, TaskStatus } from '../../orchestration/db'
import {
LEGACY_CONTRACT_VERSION,
type MessageType,
type MessagePriority,
type TaskStatus
} from '../../orchestration/db'
import { MESSAGE_TYPES } from '../../orchestration/types'
import { buildDispatchPreamble } from '../../orchestration/preamble'
import { formatMessageBanner } from '../../orchestration/formatter'
@@ -51,6 +56,10 @@ function parseRemoteWorkerPayload(payload: string | undefined): Record<string, u
}
}
function isWorkerReportOutcome(value: unknown): value is 'succeeded' | 'failed' {
return value === 'succeeded' || value === 'failed'
}
const SendParams = z
.object({
to: OptionalString,
@@ -107,6 +116,9 @@ const CheckParams = z
// Why: one-release RPC compatibility only; the public CLI uses --format because no terminal input is injected.
inject: OptionalBoolean,
ack: OptionalString,
compatibilityAck: OptionalString,
compatibilityQuestionAck: OptionalString,
compatibilityCliCommand: z.enum(['orca', 'orca-ide', 'orca-dev']).optional(),
run: OptionalString,
wait: OptionalBoolean,
timeoutMs: OptionalFiniteNumber
@@ -205,7 +217,9 @@ const AskParams = z
options: OptionalString,
timeoutMs: OptionalFiniteNumber,
from: OptionalString,
run: OptionalString
run: OptionalString,
compatibilityCliCommand: z.enum(['orca', 'orca-ide', 'orca-dev']).optional(),
compatibilityWindowsCommand: z.enum(['orca', 'orca-ide']).optional()
})
.superRefine((params, ctx) => {
if ((params.question ? 1 : 0) + (params.resume ? 1 : 0) !== 1) {
@@ -241,6 +255,7 @@ function resolveRunScope(
callerTerminalHandle?: string
callerPaneKey?: string
requireCurrentConsumer: boolean
legacyCoordinatorRunId?: string
}
): RunRow {
const db = runtime.getOrchestrationDb()
@@ -252,6 +267,9 @@ function resolveRunScope(
if (!params.requireCurrentConsumer && explicit) {
return explicit
}
if (explicit && params.legacyCoordinatorRunId === explicit.id) {
return explicit
}
if (!params.callerTerminalHandle) {
throw new OrchestrationError(
'run_required',
@@ -366,6 +384,49 @@ function resolveMessageRun(
return { run, dispatchId: dispatch?.id ?? dispatchId }
}
function legacyWorkerDeliveryContract(
runtime: OrcaRuntimeService,
runId: string | undefined,
recipient: string
): 'legacy_direct' | undefined {
if (!runId) {
return undefined
}
if (!recipient.startsWith('dispatch:')) {
return runtime
.getOrchestrationDb()
.resolveLegacyWorkerCandidate({ runId, terminalHandle: recipient })
? 'legacy_direct'
: undefined
}
const dispatch = runtime
.getOrchestrationDb()
.getDispatchContextById(recipient.slice('dispatch:'.length))
return dispatch?.run_id === runId &&
dispatch.contract_version === LEGACY_CONTRACT_VERSION &&
(dispatch.status === 'pending' || dispatch.status === 'dispatched')
? 'legacy_direct'
: undefined
}
function interruptedAcknowledgedCheck(
runId: string,
acknowledged: string,
reason: 'consumer_fenced' | 'outcome_unknown' | 'waiter_exists'
): Record<string, unknown> {
return {
runId,
deliveryId: null,
messages: [],
count: 0,
acknowledged,
timedOut: false,
cancelled: false,
connectionLost: false,
waitInterrupted: reason
}
}
function rejectFederatedExplicitTarget(params: { to?: string; run?: string }): void {
if (params.to || params.run) {
throw new OrchestrationError(
@@ -382,7 +443,10 @@ export const ORCHESTRATION_METHODS: RpcMethod[] = [
defineMethod({
name: 'orchestration.send',
params: SendParams,
handler: async (params, { runtime, orchestrationCapability }) => {
handler: async (
params,
{ runtime, orchestrationCapability, legacyCoordinatorRunId, revalidateLegacyCoordinator }
) => {
const db = runtime.getOrchestrationDb()
const from = params.from ?? 'unknown'
// Why: caller-supplied pane fields are only compatibility metadata; lifecycle authority uses the runtime-observed pane plus capability.
@@ -463,6 +527,15 @@ export const ORCHESTRATION_METHODS: RpcMethod[] = [
runId: params.run,
payload: params.payload
})
if (
params.type === 'worker_done' &&
!isWorkerReportOutcome(parseRemoteWorkerPayload(params.payload).outcome)
) {
throw new OrchestrationError(
'invalid_argument',
'worker_done requires outcome=succeeded|failed for a current Dispatch.'
)
}
if (params.to?.startsWith('task:')) {
throw new OrchestrationError(
'invalid_argument',
@@ -514,6 +587,7 @@ export const ORCHESTRATION_METHODS: RpcMethod[] = [
'Coordinator-to-worker control mail cannot report worker lifecycle.'
)
}
revalidateLegacyCoordinator?.()
const relay = db.enqueueFederationRelay({
dispatchId,
direction: 'to_worker',
@@ -540,6 +614,7 @@ export const ORCHESTRATION_METHODS: RpcMethod[] = [
}
}
// Point-to-point — existing single-recipient behavior
revalidateLegacyCoordinator?.()
const msg = db.insertMessage({
from,
to,
@@ -550,7 +625,12 @@ export const ORCHESTRATION_METHODS: RpcMethod[] = [
threadId: params.threadId,
payload: params.payload,
senderPaneKey,
runId: routing.run?.id
runId: routing.run?.id,
deliveryContract: legacyWorkerDeliveryContract(
runtime,
routing.run?.id ?? legacyCoordinatorRunId,
to
)
})
const dispatch = routing.dispatchId
? db.getDispatchContextById(routing.dispatchId)
@@ -607,6 +687,7 @@ export const ORCHESTRATION_METHODS: RpcMethod[] = [
throw new Error(`No recipients resolved for group address: ${to}`)
}
revalidateLegacyCoordinator?.()
const threadId = params.threadId ?? `thread_${Date.now()}`
const messages = handles.map((handle) =>
db.insertMessage({
@@ -619,7 +700,12 @@ export const ORCHESTRATION_METHODS: RpcMethod[] = [
threadId,
payload: params.payload,
senderPaneKey,
runId: routing.run?.id
runId: routing.run?.id,
deliveryContract: legacyWorkerDeliveryContract(
runtime,
routing.run?.id ?? legacyCoordinatorRunId,
handle
)
})
)
for (const message of messages) {
@@ -633,7 +719,16 @@ export const ORCHESTRATION_METHODS: RpcMethod[] = [
defineMethod({
name: 'orchestration.check',
params: CheckParams,
handler: async (params, { runtime, signal }) => {
handler: async (
params,
{
runtime,
signal,
legacyCoordinatorRunId,
revalidateLegacyCoordinator,
recordMutationReceipt
}
) => {
const db = runtime.getOrchestrationDb()
const handle = params.terminal ?? 'unknown'
const typeFilter = parseMessageTypes(params.types)
@@ -646,7 +741,8 @@ export const ORCHESTRATION_METHODS: RpcMethod[] = [
runId: params.run,
callerTerminalHandle: handle,
callerPaneKey: paneKey ?? undefined,
requireCurrentConsumer: true
requireCurrentConsumer: true,
legacyCoordinatorRunId
})
const generation = run.consumer_generation
const address = `run:${run.id}`
@@ -659,6 +755,11 @@ export const ORCHESTRATION_METHODS: RpcMethod[] = [
deliveryId: params.ack
})
: undefined
if (acknowledged) {
recordMutationReceipt?.(
interruptedAcknowledgedCheck(run.id, acknowledged.delivery.id, 'outcome_unknown')
)
}
if (params.peek || params.all || params.unread === false) {
const history = db.getRunMailboxHistory(run.id, 100, typeFilter)
const messages =
@@ -722,14 +823,28 @@ export const ORCHESTRATION_METHODS: RpcMethod[] = [
signal,
exclusive: true
})
try {
revalidateLegacyCoordinator?.()
} catch (error) {
if (!acknowledged) {
throw error
}
return interruptedAcknowledgedCheck(run.id, acknowledged.delivery.id, 'consumer_fenced')
}
const latestRun = db.getRun(run.id)
if (!latestRun || latestRun.consumer_generation !== generation) {
if (acknowledged) {
return interruptedAcknowledgedCheck(run.id, acknowledged.delivery.id, 'consumer_fenced')
}
throw new OrchestrationError(
'consumer_fenced',
'This mailbox consumer was replaced while waiting.'
)
}
if (waitResult === 'waiter_exists') {
if (acknowledged) {
return interruptedAcknowledgedCheck(run.id, acknowledged.delivery.id, 'waiter_exists')
}
throw new OrchestrationError(
'waiter_exists',
`Run ${run.id} already has an active actionable waiter.`
@@ -911,13 +1026,28 @@ export const ORCHESTRATION_METHODS: RpcMethod[] = [
defineMethod({
name: 'orchestration.reply',
params: ReplyParams,
handler: async (params, { runtime }) => {
handler: async (params, { runtime, legacyCoordinatorRunId }) => {
const db = runtime.getOrchestrationDb()
const original = db.getMessageById(params.id)
if (!original) {
throw new Error(`Message not found: ${params.id}`)
}
if (original.run_id === ORCHESTRATION_LEGACY_RUN_ID) {
if (
legacyCoordinatorRunId &&
(original.run_id !== legacyCoordinatorRunId ||
(params.run !== undefined && params.run !== legacyCoordinatorRunId))
) {
throw new OrchestrationError(
'request_mismatch',
`Message ${params.id} does not belong to this adopted Run.`,
{ effectsApplied: false }
)
}
if (
original.run_id === ORCHESTRATION_LEGACY_RUN_ID ||
original.delivery_contract === 'legacy_direct' ||
original.delivery_contract === 'audit_only'
) {
throw new OrchestrationError(
'legacy_read_only',
'Legacy orchestration messages are inspect-only; no reply was applied.',
@@ -930,7 +1060,8 @@ export const ORCHESTRATION_METHODS: RpcMethod[] = [
const run = resolveRunScope(runtime, {
runId: params.run ?? question.run_id,
callerTerminalHandle: params.from,
requireCurrentConsumer: true
requireCurrentConsumer: true,
legacyCoordinatorRunId
})
const answered = db.answerQuestion({
messageId: question.message_id,
@@ -993,7 +1124,7 @@ export const ORCHESTRATION_METHODS: RpcMethod[] = [
defineMethod({
name: 'orchestration.taskCreate',
params: TaskCreateParams,
handler: (params, { runtime }) => {
handler: (params, { runtime, legacyCoordinatorRunId }) => {
const db = runtime.getOrchestrationDb()
let deps: string[] | undefined
if (params.deps) {
@@ -1017,7 +1148,8 @@ export const ORCHESTRATION_METHODS: RpcMethod[] = [
runId: resolveRunScope(runtime, {
runId: params.run,
callerTerminalHandle: params.callerTerminalHandle,
requireCurrentConsumer: true
requireCurrentConsumer: true,
legacyCoordinatorRunId
}).id
})
return { task }
@@ -1027,7 +1159,7 @@ export const ORCHESTRATION_METHODS: RpcMethod[] = [
defineMethod({
name: 'orchestration.taskList',
params: TaskListParams,
handler: (params, { runtime }) => {
handler: (params, { runtime, legacyCoordinatorRunId }) => {
const db = runtime.getOrchestrationDb()
const explicitRun = params.run ? db.getRun(params.run) : undefined
const run =
@@ -1036,7 +1168,8 @@ export const ORCHESTRATION_METHODS: RpcMethod[] = [
: resolveRunScope(runtime, {
runId: params.run,
callerTerminalHandle: params.callerTerminalHandle,
requireCurrentConsumer: params.run === undefined
requireCurrentConsumer: params.run === undefined,
legacyCoordinatorRunId
})
// Why: listTasksWithDispatch adds assignee_handle + dispatch_id (NULL for non-dispatched), so legacy-shape consumers are unaffected.
const joined = db.listTasksWithDispatch({
@@ -1063,12 +1196,13 @@ export const ORCHESTRATION_METHODS: RpcMethod[] = [
defineMethod({
name: 'orchestration.taskUpdate',
params: TaskUpdateParams,
handler: (params, { runtime }) => {
handler: (params, { runtime, legacyCoordinatorRunId }) => {
const db = runtime.getOrchestrationDb()
const run = resolveRunScope(runtime, {
runId: params.run,
callerTerminalHandle: params.callerTerminalHandle,
requireCurrentConsumer: true
requireCurrentConsumer: true,
legacyCoordinatorRunId
})
const existing = db.getTask(params.id)
if (!existing || existing.run_id !== run.id) {
@@ -1088,7 +1222,7 @@ export const ORCHESTRATION_METHODS: RpcMethod[] = [
defineMethod({
name: 'orchestration.dispatch',
params: DispatchParams,
handler: async (params, { runtime }) => {
handler: async (params, { runtime, legacyCoordinatorRunId, revalidateLegacyCoordinator }) => {
const db = runtime.getOrchestrationDb()
const task = db.getTask(params.task)
if (!task) {
@@ -1097,7 +1231,8 @@ export const ORCHESTRATION_METHODS: RpcMethod[] = [
const run = resolveRunScope(runtime, {
runId: params.run,
callerTerminalHandle: params.from,
requireCurrentConsumer: true
requireCurrentConsumer: true,
legacyCoordinatorRunId
})
if (task.run_id !== run.id) {
throw new OrchestrationError(
@@ -1143,8 +1278,13 @@ export const ORCHESTRATION_METHODS: RpcMethod[] = [
}
}
const assigneePaneKey = runtime.getTerminalPaneKey(to) ?? undefined
const processIncarnation = runtime.getTerminalProcessIncarnation(to) ?? undefined
const dispatchAuthority = runtime.getOrchestrationDispatchAuthority(to)
const assigneePaneKey =
dispatchAuthority?.paneKey ?? runtime.getTerminalPaneKey(to) ?? undefined
const processIncarnation =
dispatchAuthority?.processIncarnation ??
runtime.getTerminalProcessIncarnation(to) ??
undefined
if (params.inject && (!assigneePaneKey || !processIncarnation)) {
throw new OrchestrationError(
'stable_pane_required',
@@ -1152,7 +1292,13 @@ export const ORCHESTRATION_METHODS: RpcMethod[] = [
)
}
const ctx = db.createDispatchContext(params.task, to, assigneePaneKey)
revalidateLegacyCoordinator?.()
const ctx = db.createDispatchContext(
params.task,
to,
assigneePaneKey,
dispatchAuthority?.launchTokenHash ?? undefined
)
const dispatchCapability = params.inject
? db.mintDispatchCapability({
dispatchId: ctx.id,
@@ -0,0 +1,257 @@
import type { OrchestrationCompatibilityEvidence } from '../../../shared/orchestration-compatibility-evidence'
import type {
DispatchContextRow,
LegacyCompatibilityPrincipalRow,
LegacyPrincipalRole
} from '../orchestration/types'
import { OrchestrationError } from '../orchestration/orchestration-error'
import type { OrcaRuntimeService, OrchestrationCompatibilityCallerAuthority } from '../orca-runtime'
import { LEGACY_CONTRACT_VERSION } from '../orchestration/db'
import type { RpcRequest } from './core'
import {
equivalentLegacyPaneKey,
legacyCoordinatorReadOnly,
legacyReadOnlyError
} from './orchestration-legacy-process-identity'
export type LegacyPrincipalCandidate = {
runId: string
role: LegacyPrincipalRole
dispatchId?: string
terminalHandle: string
paneKey: string
}
export function resolveAttestedLegacyPrincipal(args: {
runtime: OrcaRuntimeService
evidence?: OrchestrationCompatibilityEvidence
candidate: LegacyPrincipalCandidate
authority?: OrchestrationCompatibilityCallerAuthority
}): LegacyCompatibilityPrincipalRow {
const authority = args.authority ?? verifyAttestedLegacyCandidate(args)
return args.runtime.getOrchestrationDb().commitLegacyCompatibilityPrincipal({
runId: args.candidate.runId,
dispatchId: args.candidate.dispatchId,
role: args.candidate.role,
hostScope: JSON.stringify(authority.hostScope),
terminalHandle: args.candidate.terminalHandle,
paneKey: args.candidate.paneKey,
launchTokenHash: authority.launchTokenHash,
processIncarnation: authority.processIncarnation
}).principal
}
export function verifyAttestedLegacyCandidate(args: {
runtime: OrcaRuntimeService
evidence?: OrchestrationCompatibilityEvidence
candidate: LegacyPrincipalCandidate
}) {
const authority = args.runtime.verifyOrchestrationCompatibilityCaller(args.evidence)
if (
!authority ||
!equivalentLegacyPaneKey(args.candidate.paneKey, authority.paneKey) ||
args.candidate.terminalHandle !== authority.terminalHandle
) {
throw legacyReadOnlyError()
}
if (
args.candidate.role === 'worker' &&
args.candidate.dispatchId &&
!args.runtime.getOrchestrationDb().isDispatchProcessCurrent({
dispatchId: args.candidate.dispatchId,
paneKey: authority.paneKey,
processIncarnation: authority.processIncarnation
})
) {
throw legacyReadOnlyError()
}
return authority
}
export class LegacyCompatibilityAuthority {
constructor(private readonly runtime: OrcaRuntimeService) {}
resolveWorkerDispatch(
request: RpcRequest,
target: { terminalHandle?: string; dispatchId?: string; taskId?: string }
): DispatchContextRow | undefined {
const db = this.runtime.getOrchestrationDb()
const evidence = request.orchestrationCompatibilityEvidence
const adoptedRunId = db.getLegacyAdoption()?.adopted_run_id
const existing = adoptedRunId
? db.resolveLegacyCompatibilityPrincipalByIdentity({
runId: adoptedRunId,
role: 'worker',
terminalHandle: evidence?.terminalHandle,
paneKey: evidence?.paneKey
})
: undefined
if (existing?.dispatch_id) {
verifyAttestedLegacyCandidate({
runtime: this.runtime,
evidence,
candidate: candidateFromPrincipal(existing)
})
const settledDispatch = db.getDispatchContextById(existing.dispatch_id)
if (
settledDispatch &&
(!target.dispatchId || target.dispatchId === settledDispatch.id) &&
(!target.taskId || target.taskId === settledDispatch.task_id)
) {
return settledDispatch
}
}
const dispatch = db.resolveLegacyWorkerCandidate({
runId: adoptedRunId,
terminalHandle: evidence?.terminalHandle ?? target.terminalHandle,
paneKey: evidence?.paneKey,
dispatchId: target.dispatchId,
taskId: target.taskId
})?.dispatch
if (!dispatch && evidence) {
const retained = db.resolveLegacyWorkerCandidate({
runId: adoptedRunId,
terminalHandle: target.terminalHandle,
dispatchId: target.dispatchId,
taskId: target.taskId
})
if (retained) {
throw legacyReadOnlyError()
}
}
return dispatch
}
resolveAskDispatch(
request: RpcRequest,
params: { from?: string; resume?: string }
): DispatchContextRow | undefined {
const db = this.runtime.getOrchestrationDb()
if (params.resume) {
const question = db.getQuestion(params.resume)
return question ? db.getDispatchContextById(question.dispatch_id) : undefined
}
return this.resolveWorkerDispatch(request, { terminalHandle: params.from })
}
resolveCheckPrincipal(
request: RpcRequest,
terminalHandle?: string
): LegacyCompatibilityPrincipalRow | undefined {
const db = this.runtime.getOrchestrationDb()
const adoption = db.getLegacyAdoption()
if (!adoption) {
return undefined
}
const evidence = request.orchestrationCompatibilityEvidence
const existingWorker = db.resolveLegacyCompatibilityPrincipalByIdentity({
runId: adoption.adopted_run_id,
role: 'worker',
terminalHandle: evidence?.terminalHandle,
paneKey: evidence?.paneKey
})
if (existingWorker) {
verifyAttestedLegacyCandidate({
runtime: this.runtime,
evidence,
candidate: candidateFromPrincipal(existingWorker)
})
return existingWorker
}
const worker = db.resolveLegacyWorkerCandidate({
runId: adoption.adopted_run_id,
terminalHandle: evidence?.terminalHandle ?? terminalHandle,
paneKey: evidence?.paneKey
})
if (worker) {
return this.attestWorker(request, worker.dispatch)
}
if (evidence) {
const retained = db.resolveLegacyWorkerCandidate({
runId: adoption.adopted_run_id,
terminalHandle
})
const settled = db.resolveLegacyCompatibilityPrincipalByIdentity({
runId: adoption.adopted_run_id,
role: 'worker',
terminalHandle
})
if (retained || settled) {
throw legacyReadOnlyError()
}
}
return this.attestCoordinator(request, adoption.adopted_run_id, false)
}
attestWorker(request: RpcRequest, dispatch: DispatchContextRow): LegacyCompatibilityPrincipalRow {
if (dispatch.contract_version !== LEGACY_CONTRACT_VERSION) {
throw new OrchestrationError(
'request_mismatch',
`Dispatch ${dispatch.id} does not use the legacy contract.`
)
}
return resolveAttestedLegacyPrincipal({
runtime: this.runtime,
evidence: request.orchestrationCompatibilityEvidence,
candidate: candidateFromDispatch(dispatch)
})
}
attestCoordinator(
request: RpcRequest,
runId: string,
required = true
): LegacyCompatibilityPrincipalRow | undefined {
const db = this.runtime.getOrchestrationDb()
const candidate = db.resolveLegacyCoordinatorCandidate({
runId,
terminalHandle: request.orchestrationCompatibilityEvidence?.terminalHandle,
paneKey: request.orchestrationCompatibilityEvidence?.paneKey
})
if (!candidate) {
if (required) {
throw legacyCoordinatorReadOnly()
}
return undefined
}
const principal = resolveAttestedLegacyPrincipal({
runtime: this.runtime,
evidence: request.orchestrationCompatibilityEvidence,
candidate: {
runId,
role: 'coordinator',
terminalHandle: candidate.terminalHandle,
paneKey: candidate.paneKey
}
})
return principal
}
}
function candidateFromDispatch(dispatch: DispatchContextRow): LegacyPrincipalCandidate {
if (!dispatch.assignee_handle || !dispatch.assignee_pane_key) {
throw new OrchestrationError(
'legacy_read_only',
`Dispatch ${dispatch.id} lacks durable process identity. No effects were applied.`
)
}
return {
runId: dispatch.run_id,
role: 'worker',
dispatchId: dispatch.id,
terminalHandle: dispatch.assignee_handle,
paneKey: dispatch.assignee_pane_key
}
}
function candidateFromPrincipal(
principal: LegacyCompatibilityPrincipalRow
): LegacyPrincipalCandidate {
return {
runId: principal.run_id,
role: principal.role,
dispatchId: principal.dispatch_id ?? undefined,
terminalHandle: principal.terminal_handle,
paneKey: principal.pane_key
}
}
@@ -0,0 +1,176 @@
import { createHash } from 'node:crypto'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { expect, vi } from 'vitest'
import type { OrchestrationCompatibilityEvidence } from '../../../shared/orchestration-compatibility-evidence'
import { ORCHESTRATION_CONTRACT_VERSION } from '../../../shared/protocol-version'
import Database from '../../sqlite/sync-database'
import { OrcaRuntimeService } from '../orca-runtime'
import { OrchestrationDb } from '../orchestration/db'
import type { RpcRequest, RpcResponse } from './core'
import { RpcDispatcher } from './dispatcher'
import { ORCHESTRATION_METHODS } from './methods/orchestration'
export const WORKER_HANDLE = 'term_legacy_worker'
export const WORKER_PANE = 'tab_worker:33333333-3333-4333-8333-333333333333'
export const COORDINATOR_HANDLE = 'term_legacy_coord'
export const COORDINATOR_PANE = 'tab_coord:44444444-4444-4444-8444-444444444444'
type Transport = 'dispatch' | 'websocket'
export type LegacyCompatibilityDispatcherHarness = {
db: OrchestrationDb
dispatcher: RpcDispatcher
runtime: OrcaRuntimeService
adoptedRunId: string
taskId: string
dispatchId: string
notify: ReturnType<typeof vi.spyOn>
verify: ReturnType<typeof vi.spyOn>
}
const tempDirs: string[] = []
const databases: OrchestrationDb[] = []
export function cleanupLegacyCompatibilityDispatcherHarnesses(): void {
for (const database of databases.splice(0)) {
database.close()
}
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true })
}
}
export function createHarness(): LegacyCompatibilityDispatcherHarness {
const dir = mkdtempSync(join(tmpdir(), 'orca-legacy-dispatcher-'))
tempDirs.push(dir)
const dbPath = join(dir, 'orchestration.db')
const before = new OrchestrationDb(dbPath)
const task = before.createTask({
spec: 'legacy assignment',
createdByTerminalHandle: COORDINATOR_HANDLE
})
const dispatch = before.createDispatchContext(task.id, WORKER_HANDLE, WORKER_PANE)
before.close()
const raw = new Database(dbPath)
raw.exec(`
UPDATE dispatch_contexts SET process_incarnation = 'process-1';
DROP INDEX IF EXISTS idx_messages_delivery_contract;
DROP TABLE legacy_mail_receipts;
DROP TABLE legacy_operation_receipts;
DROP TABLE legacy_compatibility_principals;
DROP TABLE legacy_adoptions;
`)
raw.pragma('user_version = 18')
raw.close()
const db = new OrchestrationDb(dbPath)
databases.push(db)
const adoptedRunId = db.getLegacyAdoption()?.adopted_run_id as string
const runtime = new OrcaRuntimeService()
runtime.setOrchestrationDb(db)
vi.spyOn(runtime, 'getTerminalPaneKey').mockImplementation((handle) =>
handle === COORDINATOR_HANDLE ? COORDINATOR_PANE : handle === WORKER_HANDLE ? WORKER_PANE : null
)
const verify = vi
.spyOn(runtime, 'verifyOrchestrationCompatibilityCaller')
.mockImplementation((evidence) => {
const validWorker =
evidence?.terminalHandle === WORKER_HANDLE && evidence.paneKey === WORKER_PANE
const validCoordinator =
evidence?.terminalHandle === COORDINATOR_HANDLE && evidence.paneKey === COORDINATOR_PANE
if ((!validWorker && !validCoordinator) || !evidence?.launchToken) {
return null
}
return {
hostScope: { kind: 'local', hostId: 'local' },
terminalHandle: evidence.terminalHandle as string,
paneKey: evidence.paneKey as string,
processIncarnation: 'process-1',
launchTokenHash: createHash('sha256').update(evidence.launchToken).digest('hex')
}
})
const notify = vi.spyOn(runtime, 'notifyMessageArrived').mockImplementation(() => {})
return {
db,
dispatcher: new RpcDispatcher({ runtime, methods: ORCHESTRATION_METHODS }),
runtime,
adoptedRunId,
taskId: task.id,
dispatchId: dispatch.id,
notify,
verify
}
}
export function evidence(
role: 'worker' | 'coordinator',
valid = true
): OrchestrationCompatibilityEvidence {
const worker = role === 'worker'
return {
terminalHandle: worker ? WORKER_HANDLE : COORDINATOR_HANDLE,
paneKey: valid ? (worker ? WORKER_PANE : COORDINATOR_PANE) : 'tab_wrong:wrong-leaf',
launchToken: `${role}-token`
}
}
export function request(
method: string,
params: unknown,
proof: OrchestrationCompatibilityEvidence,
invocationId: string
): RpcRequest {
return {
id: `rpc_${invocationId}`,
authToken: 'caller-token',
method,
params,
orchestrationContractVersion: ORCHESTRATION_CONTRACT_VERSION,
orchestrationRequestId: invocationId,
compatibilityInvocationId: invocationId,
orchestrationCompatibilityEvidence: proof
}
}
export async function invoke(
dispatcher: RpcDispatcher,
rpcRequest: RpcRequest,
transport: Transport
): Promise<RpcResponse> {
if (transport === 'dispatch') {
return await dispatcher.dispatch(rpcRequest)
}
const replies: string[] = []
await dispatcher.dispatchStreaming(rpcRequest, (reply) => replies.push(reply))
expect(replies).toHaveLength(1)
return JSON.parse(replies[0]) as RpcResponse
}
export function counts(db: OrchestrationDb): Record<string, number> {
const sqlite = (db as unknown as { db: Database.Database }).db
return Object.fromEntries(
[
'messages',
'legacy_compatibility_principals',
'legacy_operation_receipts',
'legacy_mail_receipts',
'mutation_receipts'
].map((table) => [
table,
(sqlite.prepare(`SELECT COUNT(*) AS count FROM ${table}`).get() as { count: number }).count
])
)
}
export function escalationParams(harness: LegacyCompatibilityDispatcherHarness) {
return {
from: WORKER_HANDLE,
to: COORDINATOR_HANDLE,
subject: 'Blocked',
type: 'escalation',
payload: JSON.stringify({ taskId: harness.taskId, dispatchId: harness.dispatchId })
}
}
@@ -0,0 +1,755 @@
import { createHash } from 'node:crypto'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type Database from '../../sqlite/sync-database'
import {
cleanupLegacyCompatibilityDispatcherHarnesses,
COORDINATOR_HANDLE,
COORDINATOR_PANE,
counts,
createHarness,
escalationParams,
evidence,
invoke,
request,
WORKER_HANDLE,
WORKER_PANE
} from './orchestration-legacy-compatibility-dispatcher-test-fixture'
afterEach(() => {
cleanupLegacyCompatibilityDispatcherHarnesses()
})
describe('legacy compatibility through RpcDispatcher', () => {
it('rejects malformed current-contract input before compatibility attestation', async () => {
const harness = createHarness()
const before = counts(harness.db)
const response = await harness.dispatcher.dispatch(
request(
'orchestration.send',
{ from: WORKER_HANDLE, type: 'escalation' },
evidence('worker'),
'malformed'
)
)
expect(response).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
expect(harness.verify).not.toHaveBeenCalled()
expect(counts(harness.db)).toEqual(before)
})
it.each([
['dispatch', true],
['dispatch', false],
['websocket', true],
['websocket', false]
] as const)(
'%s routes task-only escalation with valid proof=%s and zero partial effects',
async (transport, valid) => {
const harness = createHarness()
const before = counts(harness.db)
const response = await invoke(
harness.dispatcher,
request(
'orchestration.send',
escalationParams(harness),
evidence('worker', valid),
`${transport}-${valid}`
),
transport
)
if (!valid) {
expect(response).toMatchObject({
ok: false,
error: { code: 'legacy_read_only' }
})
expect(counts(harness.db)).toEqual(before)
expect(harness.notify).not.toHaveBeenCalled()
return
}
expect(response).toMatchObject({
ok: true,
result: {
message: { type: 'escalation', delivery_contract: 'legacy_direct' },
legacyCompatibility: { replayed: false }
}
})
expect(counts(harness.db)).toEqual({
...before,
messages: before.messages + 1,
legacy_compatibility_principals: before.legacy_compatibility_principals + 1,
legacy_operation_receipts: before.legacy_operation_receipts + 1
})
expect(harness.notify).toHaveBeenCalledOnce()
}
)
it('validates, infers, settles, and replays legacy worker completion exactly once', async () => {
const harness = createHarness()
const baseParams = {
from: WORKER_HANDLE,
to: COORDINATOR_HANDLE,
type: 'worker_done',
body: 'legacy result',
payload: JSON.stringify({ taskId: harness.taskId, dispatchId: harness.dispatchId })
}
const before = counts(harness.db)
const invalid = await harness.dispatcher.dispatch(
request(
'orchestration.send',
{ ...baseParams, subject: 'Completed', payload: JSON.stringify({ outcome: 'maybe' }) },
evidence('worker'),
'invalid-outcome'
)
)
expect(invalid).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
expect(counts(harness.db)).toEqual(before)
const firstRequest = request(
'orchestration.send',
{ ...baseParams, subject: 'Completed' },
evidence('worker'),
'completion'
)
const first = await harness.dispatcher.dispatch(firstRequest)
const replay = await harness.dispatcher.dispatch({ ...firstRequest, id: 'rpc_replay' })
const mismatch = await harness.dispatcher.dispatch({
...firstRequest,
id: 'rpc_mismatch',
params: { ...baseParams, subject: 'Changed completion' }
})
expect(first).toMatchObject({
ok: true,
result: {
lifecycle: { action: 'settled', outcome: 'succeeded' },
legacyCompatibility: { replayed: false }
}
})
expect(replay).toMatchObject({
ok: true,
result: { legacyCompatibility: { replayed: true } }
})
expect(mismatch).toMatchObject({ ok: false, error: { code: 'request_mismatch' } })
expect(harness.db.getTask(harness.taskId)?.status).toBe('completed')
expect(harness.db.getDispatchContextById(harness.dispatchId)?.status).toBe('completed')
expect(counts(harness.db)).toEqual({
...before,
messages: before.messages + 1,
legacy_compatibility_principals: before.legacy_compatibility_principals + 1,
legacy_operation_receipts: before.legacy_operation_receipts + 1
})
expect(harness.notify).toHaveBeenCalledOnce()
})
it('replays an A-era completion without touching a newer current attempt', async () => {
const harness = createHarness()
const payload = JSON.stringify({
taskId: harness.taskId,
dispatchId: harness.dispatchId
})
const completion = harness.db.insertMessage({
runId: harness.adoptedRunId,
from: WORKER_HANDLE,
to: COORDINATOR_HANDLE,
subject: 'Failed: pinned A treated this as complete',
body: 'persisted result',
type: 'worker_done',
payload,
senderPaneKey: WORKER_PANE,
deliveryContract: 'legacy_direct'
})
harness.db.settleWorkerReport({
taskId: harness.taskId,
dispatchId: harness.dispatchId,
outcome: 'succeeded',
result: 'persisted result'
})
harness.db.commitLegacyCompatibilityPrincipal({
runId: harness.adoptedRunId,
dispatchId: harness.dispatchId,
role: 'worker',
hostScope: JSON.stringify({ kind: 'local', hostId: 'local' }),
terminalHandle: WORKER_HANDLE,
paneKey: WORKER_PANE,
launchTokenHash: createHash('sha256').update('worker-token').digest('hex'),
processIncarnation: 'process-1'
})
harness.db.updateTaskStatus(harness.taskId, 'ready')
const currentDispatch = harness.db.createDispatchContext(
harness.taskId,
'term_current_worker',
'tab_current_worker:77777777-7777-4777-8777-777777777777',
'current-launch-hash'
)
const currentTaskBefore = harness.db.getTask(harness.taskId)
const currentDispatchBefore = harness.db.getDispatchContextById(currentDispatch.id)
const before = counts(harness.db)
const response = await harness.dispatcher.dispatch(
request(
'orchestration.send',
{
from: WORKER_HANDLE,
to: COORDINATOR_HANDLE,
subject: completion.subject,
body: completion.body,
type: 'worker_done',
payload
},
evidence('worker'),
'reconstruct-pinned-a-completion'
)
)
expect(response).toMatchObject({
ok: true,
result: {
message: { id: completion.id },
lifecycle: { action: 'settled', outcome: 'succeeded', duplicate: true }
}
})
expect(harness.db.getTask(harness.taskId)).toEqual(currentTaskBefore)
expect(harness.db.getDispatchContextById(harness.dispatchId)?.status).toBe('completed')
expect(harness.db.getDispatchContextById(currentDispatch.id)).toEqual(currentDispatchBefore)
expect(counts(harness.db)).toEqual({
...before,
legacy_operation_receipts: before.legacy_operation_receipts + 1
})
})
it('rejects a legacy lifecycle recipient outside the adopted Run with zero effects', async () => {
const harness = createHarness()
const before = counts(harness.db)
const response = await harness.dispatcher.dispatch(
request(
'orchestration.send',
{
...escalationParams(harness),
to: 'term_unrelated'
},
evidence('worker'),
'wrong-recipient'
)
)
expect(response).toMatchObject({
ok: false,
error: { code: 'request_mismatch' }
})
expect(counts(harness.db)).toEqual(before)
expect(harness.verify).not.toHaveBeenCalled()
expect(harness.notify).not.toHaveBeenCalled()
})
it('rejects a reused pane whose live process incarnation is not the legacy worker', async () => {
const harness = createHarness()
const sqlite = (harness.db as unknown as { db: Database.Database }).db
sqlite
.prepare('UPDATE dispatch_contexts SET process_incarnation = ? WHERE id = ?')
.run('different-process', harness.dispatchId)
const before = counts(harness.db)
const response = await harness.dispatcher.dispatch(
request('orchestration.send', escalationParams(harness), evidence('worker'), 'reused-pane')
)
expect(response).toMatchObject({ ok: false, error: { code: 'legacy_read_only' } })
expect(counts(harness.db)).toEqual(before)
expect(harness.notify).not.toHaveBeenCalled()
})
it('rejects a legacy question recipient outside the adopted Run with zero effects', async () => {
const harness = createHarness()
const before = counts(harness.db)
const response = await harness.dispatcher.dispatch(
request(
'orchestration.ask',
{
from: WORKER_HANDLE,
to: 'term_unrelated',
question: 'Proceed?',
timeoutMs: 1
},
evidence('worker'),
'wrong-question-recipient'
)
)
expect(response).toMatchObject({
ok: false,
error: { code: 'request_mismatch' }
})
expect(counts(harness.db)).toEqual(before)
expect(harness.verify).not.toHaveBeenCalled()
expect(harness.notify).not.toHaveBeenCalled()
})
it('keeps distinct compatibility ask invocations on distinct questions', async () => {
const harness = createHarness()
const ask = {
from: WORKER_HANDLE,
to: COORDINATOR_HANDLE,
question: 'Same question?',
options: ['yes', 'no'],
timeoutMs: 1
}
const first = await harness.dispatcher.dispatch(
request('orchestration.ask', ask, evidence('worker'), 'ask-one')
)
const replay = await harness.dispatcher.dispatch(
request('orchestration.ask', ask, evidence('worker'), 'ask-one')
)
const second = await harness.dispatcher.dispatch(
request('orchestration.ask', ask, evidence('worker'), 'ask-two')
)
expect(first).toMatchObject({ ok: true, result: { legacyCompatibility: { replayed: false } } })
expect(replay).toMatchObject({ ok: true, result: { legacyCompatibility: { replayed: true } } })
expect(second).toMatchObject({ ok: true, result: { legacyCompatibility: { replayed: false } } })
const firstId = (first as { result: { messageId: string } }).result.messageId
const replayId = (replay as { result: { messageId: string } }).result.messageId
const secondId = (second as { result: { messageId: string } }).result.messageId
expect(replayId).toBe(firstId)
expect(secondId).not.toBe(firstId)
expect(harness.db.getMessageById(firstId)?.type).toBe('decision_gate')
const coordinatorCheck = await harness.dispatcher.dispatch(
request(
'orchestration.check',
{
terminal: COORDINATOR_HANDLE,
types: 'worker_done,escalation,decision_gate',
format: true
},
evidence('coordinator'),
'coordinator-check-decision-gates'
)
)
expect(coordinatorCheck).toMatchObject({
ok: true,
result: {
messages: [
{ id: firstId, type: 'decision_gate' },
{ id: secondId, type: 'decision_gate' }
],
count: 2
}
})
expect((coordinatorCheck as { result: { formatted: string } }).result.formatted).toContain(
`orca orchestration reply --id ${firstId}`
)
})
it('keeps the normal wait budget when legacy check omits timeout', async () => {
const harness = createHarness()
const waitForMessage = vi
.spyOn(harness.runtime, 'waitForMessage')
.mockResolvedValue('timed_out')
let clockReads = 0
const now = vi.spyOn(Date, 'now').mockImplementation(() => {
clockReads += 1
return clockReads === 1 ? 0 : clockReads === 2 ? 1 : 120_001
})
try {
const response = await harness.dispatcher.dispatch(
request(
'orchestration.check',
{ terminal: WORKER_HANDLE, wait: true },
evidence('worker'),
'legacy-check-default-timeout'
)
)
expect(response).toMatchObject({
ok: true,
result: { messages: [], count: 0, timedOut: true }
})
expect(waitForMessage).toHaveBeenCalledOnce()
} finally {
now.mockRestore()
}
})
it('does not infer an outcome for a current Dispatch when legacy adoption exists', async () => {
const harness = createHarness()
const run = harness.db.createRun({
objective: 'current work',
coordinatorHandle: 'term_current_coord',
coordinatorPaneKey: 'tab_current_coord:55555555-5555-4555-8555-555555555555'
})
const task = harness.db.createTask({ spec: 'current assignment', runId: run.id })
const dispatch = harness.db.createDispatchContext(
task.id,
'term_current_worker',
'tab_current_worker:66666666-6666-4666-8666-666666666666',
'current-launch-hash'
)
const before = counts(harness.db)
const response = await harness.dispatcher.dispatch(
request(
'orchestration.send',
{
from: 'term_current_worker',
to: COORDINATOR_HANDLE,
subject: 'Completed',
type: 'worker_done',
payload: JSON.stringify({ taskId: task.id, dispatchId: dispatch.id })
},
{
terminalHandle: 'term_current_worker',
paneKey: 'tab_current_worker:66666666-6666-4666-8666-666666666666',
launchToken: 'current-token'
},
'current-missing-outcome'
)
)
expect(response).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
expect(harness.db.getTask(task.id)?.status).toBe('dispatched')
expect(harness.db.getDispatchContextById(dispatch.id)?.status).toBe('dispatched')
expect(counts(harness.db)).toEqual(before)
})
it('rejects invalid typed ACKs and consumes only the filtered legacy page', async () => {
const harness = createHarness()
await harness.dispatcher.dispatch(
request(
'orchestration.send',
{
...escalationParams(harness),
type: 'worker_done',
subject: 'Completed',
payload: JSON.stringify({ taskId: harness.taskId, dispatchId: harness.dispatchId })
},
evidence('worker'),
'settle-for-mail'
)
)
const status = harness.db.insertMessage({
runId: harness.adoptedRunId,
from: COORDINATOR_HANDLE,
to: WORKER_HANDLE,
subject: 'status first',
type: 'status',
deliveryContract: 'legacy_direct'
})
const question = harness.db.insertMessage({
runId: harness.adoptedRunId,
from: COORDINATOR_HANDLE,
to: WORKER_HANDLE,
subject: 'question second',
type: 'question',
deliveryContract: 'legacy_direct'
})
const check = await harness.dispatcher.dispatch(
request(
'orchestration.check',
{ terminal: WORKER_HANDLE, types: 'question' },
evidence('worker'),
'question-check'
)
)
expect(check).toMatchObject({
ok: true,
result: {
messages: [{ id: question.id }],
legacyCompatibility: { ackMessageIds: [question.id] }
}
})
const invalid = await harness.dispatcher.dispatch(
request(
'orchestration.check',
{
terminal: WORKER_HANDLE,
compatibilityAck: JSON.stringify({
messageIds: [question.id],
types: ['not-a-message-type']
})
},
evidence('worker'),
'invalid-ack'
)
)
expect(invalid).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
expect(harness.db.getMessageById(status.id)?.read).toBe(0)
expect(harness.db.getMessageById(question.id)?.read).toBe(0)
const acknowledged = await harness.dispatcher.dispatch(
request(
'orchestration.check',
{
terminal: WORKER_HANDLE,
compatibilityAck: JSON.stringify({
messageIds: [question.id],
types: ['question']
})
},
evidence('worker'),
'valid-ack'
)
)
expect(acknowledged).toMatchObject({
ok: true,
result: { acknowledged: [question.id], legacyCompatibility: { acknowledged: true } }
})
expect(harness.db.getMessageById(status.id)?.read).toBe(0)
expect(harness.db.getMessageById(question.id)?.read).toBe(1)
})
it('rejects invalid legacy check types before attestation or mail consumption', async () => {
const harness = createHarness()
const message = harness.db.insertMessage({
runId: harness.adoptedRunId,
from: COORDINATOR_HANDLE,
to: WORKER_HANDLE,
subject: 'retained status',
type: 'status',
deliveryContract: 'legacy_direct'
})
const before = counts(harness.db)
const response = await harness.dispatcher.dispatch(
request(
'orchestration.check',
{ terminal: WORKER_HANDLE, types: 'status,not-a-message-type' },
evidence('worker'),
'invalid-types'
)
)
expect(response).toMatchObject({
ok: false,
error: { code: 'invalid_argument', message: 'Invalid --types: not-a-message-type' }
})
expect(counts(harness.db)).toEqual(before)
expect(harness.db.getMessageById(message.id)?.read).toBe(0)
expect(harness.verify).not.toHaveBeenCalled()
expect(harness.notify).not.toHaveBeenCalled()
})
it('advertises only Run-addressed current delivery to a legacy coordinator', async () => {
const harness = createHarness()
harness.db.insertMessage({
runId: harness.adoptedRunId,
from: 'term_current_worker',
to: WORKER_HANDLE,
subject: 'worker-only current mail',
deliveryContract: 'current_delivery'
})
const workerOnly = await harness.dispatcher.dispatch(
request(
'orchestration.check',
{ terminal: COORDINATOR_HANDLE },
evidence('coordinator'),
'worker-current-mail'
)
)
expect(workerOnly).toMatchObject({
ok: true,
result: { legacyCompatibility: { currentDelivery: undefined } }
})
harness.db.insertMessage({
runId: harness.adoptedRunId,
from: 'term_current_worker',
to: `run:${harness.adoptedRunId}`,
subject: 'Run current mail',
deliveryContract: 'current_delivery'
})
const runMail = await harness.dispatcher.dispatch(
request(
'orchestration.check',
{ terminal: COORDINATOR_HANDLE },
evidence('coordinator'),
'run-current-mail'
)
)
expect(runMail).toMatchObject({
ok: true,
result: {
legacyCompatibility: {
currentDelivery: {
runId: harness.adoptedRunId,
checkCommand: expect.stringContaining(`--run ${harness.adoptedRunId}`)
}
}
}
})
})
it('rejects a legacy coordinator reply outside the adopted Run with zero effects', async () => {
const harness = createHarness()
const unrelatedRun = harness.db.createRun({
objective: 'unrelated current work',
coordinatorHandle: 'term_unrelated_coord',
coordinatorPaneKey: 'tab_unrelated_coord:55555555-5555-4555-8555-555555555555'
})
const unrelated = harness.db.insertMessage({
runId: unrelatedRun.id,
from: 'term_unrelated_worker',
to: `run:${unrelatedRun.id}`,
subject: 'Unrelated current status',
deliveryContract: 'current_delivery'
})
const before = counts(harness.db)
const response = await harness.dispatcher.dispatch(
request(
'orchestration.reply',
{
id: unrelated.id,
body: 'Forged cross-Run reply',
from: COORDINATOR_HANDLE,
run: harness.adoptedRunId
},
evidence('coordinator'),
'cross-run-reply'
)
)
expect(response).toMatchObject({
ok: false,
error: {
code: 'request_mismatch',
data: { effectsApplied: false }
}
})
expect(counts(harness.db)).toEqual(before)
expect(harness.db.getMessageById(unrelated.id)?.read).toBe(0)
expect(harness.notify).not.toHaveBeenCalled()
})
it('keeps explicit adopted Run task inspection available to unrelated callers', async () => {
const harness = createHarness()
const before = counts(harness.db)
const response = await harness.dispatcher.dispatch(
request(
'orchestration.taskList',
{ run: harness.adoptedRunId, callerTerminalHandle: 'term_unrelated' },
{
terminalHandle: 'term_unrelated',
paneKey: 'tab_unrelated:55555555-5555-4555-8555-555555555555',
launchToken: 'unrelated-token'
},
'explicit-adopted-run-inspection'
)
)
expect(response).toMatchObject({
ok: true,
result: {
runId: harness.adoptedRunId,
tasks: [expect.objectContaining({ id: harness.taskId })]
}
})
expect(counts(harness.db)).toEqual(before)
expect(harness.verify).not.toHaveBeenCalled()
})
it.each(['dispatch', 'websocket'] as const)(
'%s binds an attested coordinator once and rejects contradictory proof before binding',
async (transport) => {
const invalidHarness = createHarness()
const invalidBefore = counts(invalidHarness.db)
const rejected = await invoke(
invalidHarness.dispatcher,
request(
'orchestration.runUse',
{ id: invalidHarness.adoptedRunId, from: COORDINATOR_HANDLE },
evidence('coordinator', false),
`run-use-invalid-${transport}`
),
transport
)
expect(rejected).toMatchObject({ ok: false, error: { code: 'legacy_read_only' } })
expect(invalidHarness.db.getRun(invalidHarness.adoptedRunId)?.consumer_generation).toBe(0)
expect(counts(invalidHarness.db)).toEqual(invalidBefore)
const harness = createHarness()
const runUse = request(
'orchestration.runUse',
{ id: harness.adoptedRunId, from: COORDINATOR_HANDLE },
evidence('coordinator'),
`run-use-${transport}`
)
const first = await invoke(harness.dispatcher, runUse, transport)
const replay = await invoke(
harness.dispatcher,
{ ...runUse, id: 'rpc_run-use-replay' },
transport
)
expect(first).toMatchObject({
ok: true,
result: { binding: { consumerGeneration: 1 }, mutation: { replayed: false } }
})
expect(replay).toMatchObject({
ok: true,
result: { binding: { consumerGeneration: 1 }, mutation: { replayed: true } }
})
expect(harness.db.getRun(harness.adoptedRunId)?.consumer_generation).toBe(1)
}
)
it('passes trusted coordinator scope to a failing handler without binding or receipts', async () => {
const harness = createHarness()
const before = counts(harness.db)
const response = await harness.dispatcher.dispatch(
request(
'orchestration.taskUpdate',
{
id: 'task_missing',
status: 'completed',
callerTerminalHandle: COORDINATOR_HANDLE
},
evidence('coordinator'),
'missing-task'
)
)
expect(response).toMatchObject({ ok: false, error: { code: 'task_not_found' } })
expect(harness.db.getRun(harness.adoptedRunId)?.consumer_generation).toBe(0)
expect(counts(harness.db)).toEqual(before)
})
it('rejects stale legacy coordinator proof after current takeover', async () => {
const harness = createHarness()
const principal = harness.db.commitLegacyCompatibilityPrincipal({
runId: harness.adoptedRunId,
role: 'coordinator',
hostScope: JSON.stringify({ kind: 'local', hostId: 'local' }),
terminalHandle: COORDINATOR_HANDLE,
paneKey: COORDINATOR_PANE,
launchTokenHash: createHash('sha256').update('coordinator-token').digest('hex'),
processIncarnation: 'process-1'
}).principal
harness.db.settleWorkerReport({
taskId: harness.taskId,
dispatchId: harness.dispatchId,
outcome: 'succeeded',
result: 'done'
})
harness.db.bindRun({
runId: harness.adoptedRunId,
coordinatorHandle: 'term_current_coord',
coordinatorPaneKey: 'tab_current:55555555-5555-4555-8555-555555555555'
})
expect(harness.db.getLegacyCompatibilityPrincipal(principal.id)?.status).toBe('revoked')
const before = counts(harness.db)
const response = await harness.dispatcher.dispatch(
request(
'orchestration.taskList',
{},
evidence('coordinator'),
'stale-coordinator-after-takeover'
)
)
expect(response).toMatchObject({ ok: false, error: { code: 'legacy_read_only' } })
expect(counts(harness.db)).toEqual(before)
})
})
@@ -0,0 +1,170 @@
import { createHash } from 'node:crypto'
import type { LegacyCoordinatorAuthorityProof, RpcRequest } from './core'
import type { OrcaRuntimeService, OrchestrationCompatibilityCallerAuthority } from '../orca-runtime'
import { LegacyCompatibilityAuthority } from './orchestration-legacy-authority'
import { handleLegacyLifecycleSend } from './orchestration-legacy-lifecycle'
import { handleLegacyCheck, handleLegacyReply } from './orchestration-legacy-mail'
import { handleLegacyAsk } from './orchestration-legacy-question'
import type {
LegacyAskParams,
LegacyCheckParams,
LegacyReplyParams,
LegacySendParams
} from './orchestration-legacy-operation'
import { LegacyCoordinatorAuthority } from './orchestration-legacy-coordinator-authority'
const COORDINATOR_PREFLIGHT_METHODS = new Set([
'orchestration.taskCreate',
'orchestration.taskList',
'orchestration.taskUpdate',
'orchestration.dispatch',
'orchestration.gateCreate',
'orchestration.gateResolve',
'orchestration.runUse',
'orchestration.send',
'orchestration.check',
'orchestration.reply'
])
export type LegacyCompatibilityRoute =
| { handled: true; result: unknown }
| {
handled: false
params?: unknown
legacyCoordinatorAuthority?: LegacyCoordinatorAuthorityProof
orchestrationCompatibilityCallerAuthority?: OrchestrationCompatibilityCallerAuthority
}
export type LegacyCoordinatorInvocation = Readonly<{
authority: LegacyCoordinatorAuthorityProof
mutationCallerFingerprint: string
revalidate: () => string
}>
export class OrchestrationLegacyCompatibility {
private readonly authority: LegacyCompatibilityAuthority
private readonly coordinatorAuthority: LegacyCoordinatorAuthority
constructor(private readonly runtime: OrcaRuntimeService) {
this.authority = new LegacyCompatibilityAuthority(runtime)
this.coordinatorAuthority = new LegacyCoordinatorAuthority(runtime)
}
async tryHandle(
request: RpcRequest,
params: unknown,
signal?: AbortSignal
): Promise<LegacyCompatibilityRoute> {
if (!request.method.startsWith('orchestration.')) {
return { handled: false }
}
const result = await this.route(request, params, signal)
if (result !== undefined) {
return { handled: true, result }
}
if (!COORDINATOR_PREFLIGHT_METHODS.has(request.method)) {
return { handled: false }
}
const values = params as Record<string, unknown>
if (request.method === 'orchestration.runUse' && values.takeoverLegacy === true) {
const callerAuthority = this.runtime.verifyOrchestrationCompatibilityCaller(
request.orchestrationCompatibilityEvidence
)
return {
handled: false,
...(callerAuthority ? { orchestrationCompatibilityCallerAuthority: callerAuthority } : {})
}
}
const requestedRunId =
request.method === 'orchestration.runUse' ? stringValue(values.id) : stringValue(values.run)
if (request.method === 'orchestration.taskList' && requestedRunId) {
return { handled: false }
}
const authority = this.coordinatorAuthority.resolve(request, requestedRunId)
return authority
? {
handled: false,
params: { ...values, run: authority.runId },
legacyCoordinatorAuthority: authority
}
: { handled: false }
}
revalidateCoordinatorAuthority(
request: RpcRequest,
proof: LegacyCoordinatorAuthorityProof
): string {
return this.coordinatorAuthority.revalidate(request, proof)
}
createCoordinatorInvocation(
request: RpcRequest,
authority?: LegacyCoordinatorAuthorityProof
): LegacyCoordinatorInvocation | undefined {
return authority
? {
authority,
mutationCallerFingerprint: legacyCoordinatorMutationCallerFingerprint(authority),
revalidate: () => this.revalidateCoordinatorAuthority(request, authority)
}
: undefined
}
private async route(
request: RpcRequest,
params: unknown,
signal?: AbortSignal
): Promise<unknown | undefined> {
if (request.method === 'orchestration.send') {
return await handleLegacyLifecycleSend({
runtime: this.runtime,
authority: this.authority,
request,
params: params as LegacySendParams
})
}
if (request.method === 'orchestration.check') {
return await handleLegacyCheck({
runtime: this.runtime,
authority: this.authority,
request,
params: params as LegacyCheckParams,
signal
})
}
if (request.method === 'orchestration.ask') {
return await handleLegacyAsk({
runtime: this.runtime,
authority: this.authority,
request,
params: params as LegacyAskParams,
signal
})
}
if (request.method === 'orchestration.reply') {
return await handleLegacyReply({
runtime: this.runtime,
authority: this.authority,
request,
params: params as LegacyReplyParams
})
}
return undefined
}
}
function legacyCoordinatorMutationCallerFingerprint(
authority: LegacyCoordinatorAuthorityProof
): string {
return createHash('sha256')
.update(
['legacy-coordinator-v1', authority.runId, authority.terminalHandle, authority.paneKey].join(
'\0'
)
)
.digest('hex')
}
function stringValue(value: unknown): string | undefined {
return typeof value === 'string' && value.length > 0 ? value : undefined
}
@@ -0,0 +1,170 @@
import type { OrcaRuntimeService, OrchestrationCompatibilityCallerAuthority } from '../orca-runtime'
import type { LegacyCompatibilityPrincipalRow } from '../orchestration/types'
import type { LegacyCoordinatorAuthorityProof, RpcRequest } from './core'
import {
resolveAttestedLegacyPrincipal,
verifyAttestedLegacyCandidate,
type LegacyPrincipalCandidate
} from './orchestration-legacy-authority'
import {
equivalentLegacyPaneKey,
legacyCoordinatorReadOnly
} from './orchestration-legacy-process-identity'
export class LegacyCoordinatorAuthority {
constructor(private readonly runtime: OrcaRuntimeService) {}
resolve(
request: RpcRequest,
requestedRunId?: string
): LegacyCoordinatorAuthorityProof | undefined {
const db = this.runtime.getOrchestrationDb()
const adoption = db.getLegacyAdoption()
const requestedRun = requestedRunId ?? adoption?.adopted_run_id
if (!adoption || requestedRun !== adoption.adopted_run_id) {
return undefined
}
const candidate = db.resolveLegacyCoordinatorCandidate({
runId: adoption.adopted_run_id,
terminalHandle: request.orchestrationCompatibilityEvidence?.terminalHandle,
paneKey: request.orchestrationCompatibilityEvidence?.paneKey
})
if (!candidate) {
if (request.orchestrationCompatibilityEvidence) {
const caller = this.runtime.verifyOrchestrationCompatibilityCaller(
request.orchestrationCompatibilityEvidence
)
const run = db.getRun(adoption.adopted_run_id)
if (
caller &&
run?.coordinator_handle === caller.terminalHandle &&
run.coordinator_pane_key &&
equivalentLegacyPaneKey(run.coordinator_pane_key, caller.paneKey)
) {
return undefined
}
throw legacyCoordinatorReadOnly()
}
return undefined
}
const existing = db.getLegacyCoordinatorPrincipal(adoption.adopted_run_id)
const proofCandidate = existing
? this.candidate(adoption.adopted_run_id, existing)
: this.candidate(adoption.adopted_run_id, candidate)
const attestation = verifyAttestedLegacyCandidate({
runtime: this.runtime,
evidence: request.orchestrationCompatibilityEvidence,
candidate: proofCandidate
})
const run = db.getRun(adoption.adopted_run_id)
if (!run || !this.bindingMatches(run, proofCandidate)) {
throw legacyCoordinatorReadOnly()
}
if (existing && !this.principalMatchesAttestation(existing, attestation)) {
throw legacyCoordinatorReadOnly()
}
const principal = existing
? existing
: request.method === 'orchestration.runUse'
? resolveAttestedLegacyPrincipal({
runtime: this.runtime,
evidence: request.orchestrationCompatibilityEvidence,
candidate: proofCandidate,
authority: attestation
})
: undefined
return {
runId: run.id,
principalId: principal?.id ?? null,
terminalHandle: proofCandidate.terminalHandle,
paneKey: proofCandidate.paneKey,
consumerGeneration: run.consumer_generation
}
}
revalidate(request: RpcRequest, proof: LegacyCoordinatorAuthorityProof): string {
const db = this.runtime.getOrchestrationDb()
const adoption = db.getLegacyAdoption()
const run = db.getRun(proof.runId)
if (
adoption?.adopted_run_id !== proof.runId ||
!run ||
run.consumer_generation !== proof.consumerGeneration
) {
throw legacyCoordinatorReadOnly()
}
const candidate = this.candidate(proof.runId, proof)
const principal = db.getLegacyCoordinatorPrincipal(proof.runId)
if (proof.principalId) {
if (
principal?.id !== proof.principalId ||
principal.status !== 'committed' ||
principal.terminal_handle !== proof.terminalHandle ||
!equivalentLegacyPaneKey(principal.pane_key, proof.paneKey)
) {
throw legacyCoordinatorReadOnly()
}
} else if (
principal &&
(principal.status !== 'committed' ||
principal.terminal_handle !== proof.terminalHandle ||
!equivalentLegacyPaneKey(principal.pane_key, proof.paneKey))
) {
throw legacyCoordinatorReadOnly()
}
const attestation = verifyAttestedLegacyCandidate({
runtime: this.runtime,
evidence: request.orchestrationCompatibilityEvidence,
candidate
})
if (principal && !this.principalMatchesAttestation(principal, attestation)) {
throw legacyCoordinatorReadOnly()
}
if (!this.bindingMatches(run, candidate)) {
throw legacyCoordinatorReadOnly()
}
return run.id
}
private candidate(
runId: string,
identity: {
terminalHandle?: string
terminal_handle?: string
paneKey?: string
pane_key?: string
}
): LegacyPrincipalCandidate {
return {
runId,
role: 'coordinator',
terminalHandle: identity.terminalHandle ?? (identity.terminal_handle as string),
paneKey: identity.paneKey ?? (identity.pane_key as string)
}
}
private bindingMatches(
run: { coordinator_handle: string | null; coordinator_pane_key: string | null },
candidate: LegacyPrincipalCandidate
): boolean {
return (
!run.coordinator_pane_key ||
(run.coordinator_handle === candidate.terminalHandle &&
equivalentLegacyPaneKey(run.coordinator_pane_key, candidate.paneKey))
)
}
private principalMatchesAttestation(
principal: LegacyCompatibilityPrincipalRow,
attestation: OrchestrationCompatibilityCallerAuthority
): boolean {
return (
principal.status === 'committed' &&
principal.host_scope === JSON.stringify(attestation.hostScope) &&
principal.terminal_handle === attestation.terminalHandle &&
equivalentLegacyPaneKey(principal.pane_key, attestation.paneKey) &&
principal.launch_token_hash === attestation.launchTokenHash &&
principal.process_incarnation === attestation.processIncarnation
)
}
}
@@ -0,0 +1,664 @@
import { createHash } from 'node:crypto'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { ORCHESTRATION_CONTRACT_VERSION } from '../../../shared/protocol-version'
import type { OrchestrationCompatibilityEvidence } from '../../../shared/orchestration-compatibility-evidence'
import Database from '../../sqlite/sync-database'
import { OrcaRuntimeService } from '../orca-runtime'
import { OrchestrationDb } from '../orchestration/db'
import type { RpcRequest, RpcResponse } from './core'
import { RpcDispatcher } from './dispatcher'
import { ORCHESTRATION_METHODS } from './methods/orchestration'
const COORDINATOR_HANDLE = 'term_legacy_coord'
const COORDINATOR_PANE = 'tab_coord:44444444-4444-4444-8444-444444444444'
const WORKER_HANDLE = 'term_legacy_worker'
const WORKER_PANE = 'tab_worker:33333333-3333-4333-8333-333333333333'
type Transport = 'dispatch' | 'websocket'
type Harness = {
db: OrchestrationDb
dispatcher: RpcDispatcher
runtime: OrcaRuntimeService
adoptedRunId: string
taskId: string
dispatchId: string
}
const tempDirs: string[] = []
const databases: OrchestrationDb[] = []
afterEach(() => {
databases.splice(0).forEach((database) => database.close())
tempDirs.splice(0).forEach((dir) => rmSync(dir, { recursive: true, force: true }))
})
function createHarness(): Harness {
const dir = mkdtempSync(join(tmpdir(), 'orca-legacy-coordinator-race-'))
tempDirs.push(dir)
const dbPath = join(dir, 'orchestration.db')
const before = new OrchestrationDb(dbPath)
const task = before.createTask({
spec: 'legacy assignment',
createdByTerminalHandle: COORDINATOR_HANDLE
})
const dispatch = before.createDispatchContext(task.id, WORKER_HANDLE, WORKER_PANE)
before.close()
const raw = new Database(dbPath)
raw.exec(`
UPDATE dispatch_contexts SET process_incarnation = 'process-1';
DROP INDEX IF EXISTS idx_messages_delivery_contract;
DROP TABLE legacy_mail_receipts;
DROP TABLE legacy_operation_receipts;
DROP TABLE legacy_compatibility_principals;
DROP TABLE legacy_adoptions;
`)
raw.pragma('user_version = 18')
raw.close()
const db = new OrchestrationDb(dbPath)
databases.push(db)
const adoptedRunId = db.getLegacyAdoption()?.adopted_run_id as string
const runtime = new OrcaRuntimeService()
runtime.setOrchestrationDb(db)
vi.spyOn(runtime, 'getTerminalPaneKey').mockImplementation((handle) =>
handle === COORDINATOR_HANDLE ? COORDINATOR_PANE : handle === WORKER_HANDLE ? WORKER_PANE : null
)
vi.spyOn(runtime, 'verifyOrchestrationCompatibilityCaller').mockImplementation(
(compatibilityEvidence) => {
const coordinator =
compatibilityEvidence?.terminalHandle === COORDINATOR_HANDLE &&
compatibilityEvidence.paneKey === COORDINATOR_PANE &&
compatibilityEvidence.launchToken === 'coordinator-token'
const worker =
compatibilityEvidence?.terminalHandle === WORKER_HANDLE &&
compatibilityEvidence.paneKey === WORKER_PANE &&
compatibilityEvidence.launchToken === 'worker-token'
return coordinator || worker
? {
hostScope: { kind: 'local', hostId: 'local' },
terminalHandle: coordinator ? COORDINATOR_HANDLE : WORKER_HANDLE,
paneKey: coordinator ? COORDINATOR_PANE : WORKER_PANE,
processIncarnation: 'process-1',
launchTokenHash: createHash('sha256')
.update(coordinator ? 'coordinator-token' : 'worker-token')
.digest('hex')
}
: null
}
)
return {
db,
dispatcher: new RpcDispatcher({ runtime, methods: ORCHESTRATION_METHODS }),
runtime,
adoptedRunId,
taskId: task.id,
dispatchId: dispatch.id
}
}
function evidence(
role: 'coordinator' | 'worker' = 'coordinator'
): OrchestrationCompatibilityEvidence {
return {
terminalHandle: role === 'coordinator' ? COORDINATOR_HANDLE : WORKER_HANDLE,
paneKey: role === 'coordinator' ? COORDINATOR_PANE : WORKER_PANE,
launchToken: role === 'coordinator' ? 'coordinator-token' : 'worker-token'
}
}
function request(
method: string,
params: unknown,
invocationId: string,
callerEvidence = evidence()
): RpcRequest {
return {
id: `rpc_${invocationId}`,
authToken: 'caller-token',
method,
params,
orchestrationContractVersion: ORCHESTRATION_CONTRACT_VERSION,
orchestrationRequestId: invocationId,
compatibilityInvocationId: invocationId,
orchestrationCompatibilityEvidence: callerEvidence
}
}
async function invoke(
dispatcher: RpcDispatcher,
rpcRequest: RpcRequest,
transport: Transport
): Promise<RpcResponse> {
if (transport === 'dispatch') {
return await dispatcher.dispatch(rpcRequest)
}
const replies: string[] = []
await dispatcher.dispatchStreaming(rpcRequest, (reply) => replies.push(reply))
expect(replies).toHaveLength(1)
return JSON.parse(replies[0]) as RpcResponse
}
function installTakeoverDuringMutationPreflight(harness: Harness): void {
const originalBegin = harness.db.beginMutationReceipt.bind(harness.db)
vi.spyOn(harness.db, 'beginMutationReceipt').mockImplementation((identity) => {
const begun = originalBegin(identity)
settleLegacyWorkerAndTakeOver(harness, 'settled during takeover')
return begun
})
}
function settleLegacyWorkerAndTakeOver(harness: Harness, result: string): void {
harness.db.settleWorkerReport({
taskId: harness.taskId,
dispatchId: harness.dispatchId,
outcome: 'succeeded',
result
})
harness.db.bindRun({
runId: harness.adoptedRunId,
coordinatorHandle: 'term_current_coord',
coordinatorPaneKey: 'tab_current:55555555-5555-4555-8555-555555555555'
})
}
function mutationReceiptCount(db: OrchestrationDb): number {
const sqlite = (db as unknown as { db: Database.Database }).db
return (
sqlite.prepare('SELECT COUNT(*) AS count FROM mutation_receipts').get() as { count: number }
).count
}
function messageCount(db: OrchestrationDb): number {
const sqlite = (db as unknown as { db: Database.Database }).db
return (sqlite.prepare('SELECT COUNT(*) AS count FROM messages').get() as { count: number }).count
}
describe('legacy coordinator takeover races', () => {
it.each(['dispatch', 'websocket'] as const)(
'%s routes a pre-bind coordinator send to the retained worker mailbox',
async (transport) => {
const harness = createHarness()
const response = await invoke(
harness.dispatcher,
request(
'orchestration.send',
{ from: COORDINATOR_HANDLE, to: WORKER_HANDLE, subject: 'continue safely' },
`send-direct-${transport}`
),
transport
)
expect(response).toMatchObject({
ok: true,
result: {
message: {
run_id: harness.adoptedRunId,
to_handle: WORKER_HANDLE,
delivery_contract: 'legacy_direct'
}
}
})
const workerCheck = await harness.dispatcher.dispatch(
request(
'orchestration.check',
{ terminal: WORKER_HANDLE },
`worker-check-${transport}`,
evidence('worker')
)
)
expect(workerCheck).toMatchObject({
ok: true,
result: {
messages: [
{
subject: 'continue safely',
run_id: harness.adoptedRunId,
delivery_contract: 'legacy_direct'
}
],
legacyCompatibility: { ackMessageIds: [expect.any(String)] }
}
})
expect(
harness.db.getInbox(100).some((message) => message.run_id === 'run_legacy_local')
).toBe(false)
}
)
it('replays coordinator mutations after transport authentication rotates', async () => {
const harness = createHarness()
const mutation = request(
'orchestration.send',
{ from: COORDINATOR_HANDLE, to: WORKER_HANDLE, subject: 'send once' },
'coordinator-restart-send'
)
mutation.authToken = 'before-restart'
const first = await harness.dispatcher.dispatch(mutation)
const restartedDispatcher = new RpcDispatcher({
runtime: harness.runtime,
methods: ORCHESTRATION_METHODS
})
const replay = await restartedDispatcher.dispatch({
...mutation,
id: 'rpc_coordinator-restart-send-retry',
authToken: 'after-restart'
})
expect(first).toMatchObject({
ok: true,
result: {
message: { subject: 'send once' },
mutation: { requestId: 'coordinator-restart-send', replayed: false }
}
})
expect(replay).toMatchObject({
ok: true,
result: {
message: { id: (first as { result: { message: { id: string } } }).result.message.id },
mutation: { requestId: 'coordinator-restart-send', replayed: true }
}
})
expect(messageCount(harness.db)).toBe(1)
expect(mutationReceiptCount(harness.db)).toBe(1)
})
it('routes an exact legacy Dispatch recipient to compatibility delivery', async () => {
const harness = createHarness()
const response = await harness.dispatcher.dispatch(
request(
'orchestration.send',
{
from: COORDINATOR_HANDLE,
to: `dispatch:${harness.dispatchId}`,
subject: 'dispatch guidance'
},
'send-dispatch'
)
)
expect(response).toMatchObject({
ok: true,
result: {
message: {
run_id: harness.adoptedRunId,
to_handle: `dispatch:${harness.dispatchId}`,
delivery_contract: 'legacy_direct'
}
}
})
const workerCheck = await harness.dispatcher.dispatch(
request(
'orchestration.check',
{ terminal: WORKER_HANDLE },
'worker-check-dispatch',
evidence('worker')
)
)
expect(workerCheck).toMatchObject({
ok: true,
result: { messages: [{ subject: 'dispatch guidance' }] }
})
})
it('partitions a coordinator group send by legacy recipient contract', async () => {
const harness = createHarness()
vi.spyOn(harness.runtime, 'listTerminals').mockResolvedValue({
terminals: [
{ handle: COORDINATOR_HANDLE },
{ handle: WORKER_HANDLE },
{ handle: 'term_current_worker' }
],
totalCount: 3,
truncated: false
} as never)
const response = await harness.dispatcher.dispatch(
request(
'orchestration.send',
{ from: COORDINATOR_HANDLE, to: '@all', subject: 'group guidance' },
'send-group'
)
)
expect(response).toMatchObject({ ok: true, result: { recipients: 2 } })
const messages = harness.db
.getInbox(100)
.filter((message) => message.subject === 'group guidance')
expect(messages).toEqual(
expect.arrayContaining([
expect.objectContaining({
run_id: harness.adoptedRunId,
to_handle: WORKER_HANDLE,
delivery_contract: 'legacy_direct'
}),
expect.objectContaining({
run_id: harness.adoptedRunId,
to_handle: 'term_current_worker',
delivery_contract: 'current_delivery'
})
])
)
})
it.each(['dispatch', 'websocket'] as const)(
'%s rejects a coordinator send after takeover during durable preflight',
async (transport) => {
const harness = createHarness()
installTakeoverDuringMutationPreflight(harness)
const before = messageCount(harness.db)
const response = await invoke(
harness.dispatcher,
request(
'orchestration.send',
{ from: COORDINATOR_HANDLE, to: WORKER_HANDLE, subject: 'must not send' },
`send-takeover-${transport}`
),
transport
)
expect(response).toMatchObject({ ok: false, error: { code: 'legacy_read_only' } })
expect(messageCount(harness.db)).toBe(before)
expect(mutationReceiptCount(harness.db)).toBe(0)
}
)
it('revalidates a group send after asynchronous terminal discovery', async () => {
const harness = createHarness()
let resolveTerminals:
| ((value: {
terminals: { handle: string }[]
totalCount: number
truncated: boolean
}) => void)
| undefined
let signalListingStarted: (() => void) | undefined
const listingStarted = new Promise<void>((resolve) => {
signalListingStarted = resolve
})
vi.spyOn(harness.runtime, 'listTerminals').mockImplementation(
() =>
new Promise((resolve) => {
resolveTerminals = resolve as typeof resolveTerminals
signalListingStarted?.()
}) as never
)
const before = messageCount(harness.db)
const pending = harness.dispatcher.dispatch(
request(
'orchestration.send',
{ from: COORDINATOR_HANDLE, to: '@all', subject: 'must remain unsent' },
'send-group-takeover'
)
)
await listingStarted
settleLegacyWorkerAndTakeOver(harness, 'settled during terminal discovery')
resolveTerminals?.({
terminals: [{ handle: COORDINATOR_HANDLE }, { handle: WORKER_HANDLE }],
totalCount: 2,
truncated: false
})
await expect(pending).resolves.toMatchObject({
ok: false,
error: { code: 'legacy_read_only' }
})
expect(messageCount(harness.db)).toBe(before)
expect(mutationReceiptCount(harness.db)).toBe(0)
})
it('reports a committed ACK when coordinator takeover interrupts its wait', async () => {
const harness = createHarness()
const incoming = harness.db.insertMessage({
runId: harness.adoptedRunId,
from: WORKER_HANDLE,
to: `run:${harness.adoptedRunId}`,
subject: 'ready to acknowledge'
})
const first = await harness.dispatcher.dispatch(
request(
'orchestration.check',
{ terminal: COORDINATOR_HANDLE, run: harness.adoptedRunId },
'check-delivery-before-takeover'
)
)
expect(first).toMatchObject({
ok: true,
result: { deliveryId: expect.any(String), messages: [{ id: incoming.id }] }
})
const deliveryId = (first as { result: { deliveryId: string } }).result.deliveryId
let resolveWait: (() => void) | undefined
let signalWaitStarted: (() => void) | undefined
const waitStarted = new Promise<void>((resolve) => {
signalWaitStarted = resolve
})
vi.spyOn(harness.runtime, 'waitForMessage').mockImplementation(
() =>
new Promise((resolve) => {
resolveWait = () => resolve('notified')
signalWaitStarted?.()
})
)
const pending = harness.dispatcher.dispatch(
request(
'orchestration.check',
{
terminal: COORDINATOR_HANDLE,
run: harness.adoptedRunId,
ack: deliveryId,
wait: true,
timeoutMs: 10_000
},
'check-ack-takeover'
)
)
await waitStarted
expect(harness.db.getMessageById(incoming.id)?.read).toBe(1)
settleLegacyWorkerAndTakeOver(harness, 'settled during acknowledged wait')
resolveWait?.()
await expect(pending).resolves.toMatchObject({
ok: true,
result: {
acknowledged: deliveryId,
count: 0,
waitInterrupted: 'consumer_fenced',
mutation: { requestId: 'check-ack-takeover', replayed: false }
}
})
const sqlite = (harness.db as unknown as { db: Database.Database }).db
expect(sqlite.prepare('SELECT state FROM mutation_receipts').get()).toEqual({
state: 'completed'
})
})
it('keeps concurrent duplicates on the live ACK-and-wait result', async () => {
const harness = createHarness()
harness.db.insertMessage({
runId: harness.adoptedRunId,
from: WORKER_HANDLE,
to: `run:${harness.adoptedRunId}`,
subject: 'first delivery'
})
const firstCheck = await harness.dispatcher.dispatch(
request(
'orchestration.check',
{ terminal: COORDINATOR_HANDLE, run: harness.adoptedRunId },
'check-first-delivery'
)
)
const deliveryId = (firstCheck as { result: { deliveryId: string } }).result.deliveryId
let resolveWait: (() => void) | undefined
let signalWaitStarted: (() => void) | undefined
const waitStarted = new Promise<void>((resolve) => {
signalWaitStarted = resolve
})
vi.spyOn(harness.runtime, 'waitForMessage').mockImplementation(
() =>
new Promise((resolve) => {
resolveWait = () => resolve('notified')
signalWaitStarted?.()
})
)
const ackRequest = request(
'orchestration.check',
{
terminal: COORDINATOR_HANDLE,
run: harness.adoptedRunId,
ack: deliveryId,
wait: true,
timeoutMs: 10_000
},
'check-ack-concurrent'
)
const original = harness.dispatcher.dispatch(ackRequest)
await waitStarted
const secondLiveDispatcher = new RpcDispatcher({
runtime: harness.runtime,
methods: ORCHESTRATION_METHODS
})
const duplicate = secondLiveDispatcher.dispatch({
...ackRequest,
id: 'rpc_check-ack-concurrent-duplicate'
})
let duplicateSettled = false
void duplicate.then(() => {
duplicateSettled = true
})
await Promise.resolve()
expect(duplicateSettled).toBe(false)
const arrived = harness.db.insertMessage({
runId: harness.adoptedRunId,
from: WORKER_HANDLE,
to: `run:${harness.adoptedRunId}`,
subject: 'arrived while waiting'
})
resolveWait?.()
const [originalResult, duplicateResult] = await Promise.all([original, duplicate])
expect(originalResult).toMatchObject({
ok: true,
result: {
messages: [{ id: arrived.id }],
mutation: { requestId: 'check-ack-concurrent', replayed: false }
}
})
expect(duplicateResult).toMatchObject({
ok: true,
result: {
messages: [{ id: arrived.id }],
mutation: { requestId: 'check-ack-concurrent', replayed: true }
}
})
})
it.each(['dispatch', 'websocket'] as const)(
'%s rejects run-use after takeover during durable preflight',
async (transport) => {
const harness = createHarness()
installTakeoverDuringMutationPreflight(harness)
const response = await invoke(
harness.dispatcher,
request(
'orchestration.runUse',
{ id: harness.adoptedRunId, from: COORDINATOR_HANDLE },
`run-use-takeover-${transport}`
),
transport
)
expect(response).toMatchObject({ ok: false, error: { code: 'legacy_read_only' } })
expect(harness.db.getRun(harness.adoptedRunId)).toMatchObject({
coordinator_handle: 'term_current_coord',
consumer_generation: 1
})
expect(mutationReceiptCount(harness.db)).toBe(0)
expect(harness.db.getLegacyCoordinatorPrincipal(harness.adoptedRunId)?.status).toBe('revoked')
}
)
it.each(['dispatch', 'websocket'] as const)(
'%s rejects a mutation after takeover during durable preflight',
async (transport) => {
const harness = createHarness()
const target = harness.db.createTask({
spec: 'must remain ready',
runId: harness.adoptedRunId
})
installTakeoverDuringMutationPreflight(harness)
const response = await invoke(
harness.dispatcher,
request(
'orchestration.taskUpdate',
{
id: target.id,
status: 'completed',
callerTerminalHandle: COORDINATOR_HANDLE
},
`task-update-takeover-${transport}`
),
transport
)
expect(response).toMatchObject({ ok: false, error: { code: 'legacy_read_only' } })
expect(harness.db.getTask(target.id)?.status).toBe('ready')
expect(mutationReceiptCount(harness.db)).toBe(0)
}
)
it('revalidates after asynchronous agent detection', async () => {
const harness = createHarness()
const target = harness.db.createTask({
spec: 'must not dispatch after takeover',
runId: harness.adoptedRunId
})
const targetHandle = 'term_current_worker'
const targetPane = 'tab_current_worker:77777777-7777-4777-8777-777777777777'
vi.spyOn(harness.runtime, 'getTerminalPaneKey').mockImplementation((handle: string) =>
handle === COORDINATOR_HANDLE ? COORDINATOR_PANE : handle === targetHandle ? targetPane : null
)
vi.spyOn(harness.runtime, 'getTerminalProcessIncarnation').mockReturnValue('current-process')
let resolveDetection: ((detected: boolean) => void) | undefined
let signalDetectionStarted: (() => void) | undefined
const detectionStarted = new Promise<void>((resolve) => {
signalDetectionStarted = resolve
})
vi.spyOn(harness.runtime, 'isTerminalRunningAgent').mockImplementation(
() =>
new Promise<boolean>((resolve) => {
resolveDetection = resolve
signalDetectionStarted?.()
})
)
const sendPrompt = vi
.spyOn(harness.runtime, 'sendTerminalAgentPrompt')
.mockResolvedValue({ handle: targetHandle, accepted: true, bytesWritten: 1 })
const pending = harness.dispatcher.dispatch(
request(
'orchestration.dispatch',
{ task: target.id, to: targetHandle, from: COORDINATOR_HANDLE, inject: true },
'dispatch-detection-takeover'
)
)
await detectionStarted
settleLegacyWorkerAndTakeOver(harness, 'settled during detection')
resolveDetection?.(true)
const response = await pending
expect(response).toMatchObject({ ok: false, error: { code: 'legacy_read_only' } })
expect(harness.db.getTask(target.id)?.status).toBe('ready')
expect(harness.db.getDispatchContext(target.id)).toBeUndefined()
expect(sendPrompt).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,114 @@
import type { RpcRequest } from './core'
import type { OrcaRuntimeService } from '../orca-runtime'
import type { MessageType } from '../orchestration/db'
import { OrchestrationError } from '../orchestration/orchestration-error'
import type { LegacyCompatibilityAuthority } from './orchestration-legacy-authority'
import {
inferLegacyWorkerOutcome,
isWorkerOutcome,
operationIdentity,
parseLegacyPayload,
stringValue,
type LegacySendParams
} from './orchestration-legacy-operation'
export async function handleLegacyLifecycleSend(args: {
runtime: OrcaRuntimeService
authority: LegacyCompatibilityAuthority
request: RpcRequest
params: LegacySendParams
}): Promise<unknown | undefined> {
const { runtime, authority, request, params } = args
if (!['heartbeat', 'worker_done', 'escalation'].includes(params.type ?? '')) {
return undefined
}
const payload = parseLegacyPayload(params.payload)
const dispatch = authority.resolveWorkerDispatch(request, {
terminalHandle: params.from,
dispatchId: stringValue(payload.dispatchId),
taskId: stringValue(payload.taskId)
})
if (!dispatch) {
if (params.type === 'worker_done' && !isWorkerOutcome(payload.outcome)) {
throw new OrchestrationError(
'invalid_argument',
'worker_done requires outcome=succeeded|failed for a current Dispatch.'
)
}
return undefined
}
if (!params.to) {
throw new OrchestrationError('invalid_argument', 'Legacy lifecycle mail requires --to.')
}
const db = runtime.getOrchestrationDb()
if (!db.isLegacyCoordinatorHandle(dispatch.run_id, params.to)) {
throw new OrchestrationError(
'request_mismatch',
`Terminal ${params.to} is not a retained coordinator for this legacy Dispatch.`
)
}
if (
params.type === 'worker_done' &&
Object.hasOwn(payload, 'outcome') &&
!isWorkerOutcome(payload.outcome)
) {
throw new OrchestrationError(
'invalid_argument',
'Legacy worker_done outcome must be succeeded or failed when provided.'
)
}
const principal = authority.attestWorker(request, dispatch)
const operation = operationIdentity(request, params.type as string, { ...params, payload })
const inferredOutcome =
params.type === 'worker_done'
? isWorkerOutcome(payload.outcome)
? payload.outcome
: inferLegacyWorkerOutcome(params.subject)
: undefined
const existingId =
params.type === 'worker_done'
? db.findLegacyWorkerCompletion({
principalId: principal.id,
taskId: dispatch.task_id,
recipientHandle: params.to,
subject: params.subject,
body: params.body ?? '',
payload: params.payload ?? null
})?.id
: undefined
const committed = db.commitLegacyLifecycleOperation({
principalId: principal.id,
operationKey: operation.key,
method: request.method,
payloadHash: operation.payloadHash,
message: {
existingId,
to: params.to,
subject: params.subject,
body: params.body,
type: params.type as MessageType,
priority: params.priority,
payload: params.payload
},
lifecycle:
params.type === 'heartbeat'
? { kind: 'heartbeat', at: new Date().toISOString() }
: params.type === 'worker_done'
? {
kind: 'worker_report',
taskId: dispatch.task_id,
outcome: inferredOutcome as 'succeeded' | 'failed',
result: params.body || params.subject
}
: { kind: 'message_only' }
})
if (!committed.duplicate) {
runtime.notifyMessageArrived(committed.message.to_handle, committed.message.type)
}
return {
message: committed.message,
...(committed.settlement ? { lifecycle: committed.settlement } : {}),
legacyCompatibility: { replayed: committed.duplicate }
}
}
@@ -0,0 +1,181 @@
import type { RpcRequest } from './core'
import type { OrcaRuntimeService } from '../orca-runtime'
import { OrchestrationError } from '../orchestration/orchestration-error'
import { formatMessageBanner } from '../orchestration/formatter'
import { ORCHESTRATION_MESSAGE_WAIT_DEFAULT_TIMEOUT_MS } from '../../../shared/orchestration-message-wait-timeout'
import type { LegacyCompatibilityAuthority } from './orchestration-legacy-authority'
import {
operationIdentity,
parseLegacyMailAck,
parseLegacyQuestionAck,
parseLegacyTypes,
supportedLegacyHints,
type LegacyCheckParams,
type LegacyReplyParams
} from './orchestration-legacy-operation'
export async function handleLegacyCheck(args: {
runtime: OrcaRuntimeService
authority: LegacyCompatibilityAuthority
request: RpcRequest
params: LegacyCheckParams
signal?: AbortSignal
}): Promise<unknown | undefined> {
const { runtime, authority, request, params, signal } = args
if (params.run) {
return undefined
}
const typeFilter = parseLegacyTypes(params.types)
const principal = authority.resolveCheckPrincipal(request, params.terminal)
if (!principal) {
return undefined
}
const db = runtime.getOrchestrationDb()
if (params.compatibilityQuestionAck) {
const answer = parseLegacyQuestionAck(params.compatibilityQuestionAck)
const acknowledged = db.acknowledgeLegacyQuestionAnswer({
principalId: principal.id,
...answer
})
return {
messages: [],
count: 0,
acknowledged: [answer.answerMessageId],
duplicate: acknowledged.duplicate,
legacyCompatibility: { acknowledged: true }
}
}
if (params.compatibilityAck) {
const ack = parseLegacyMailAck(params.compatibilityAck)
const acknowledged = db.acknowledgeLegacyMail({
principalId: principal.id,
messageIds: ack.messageIds,
types: ack.types
})
return {
messages: [],
count: 0,
acknowledged: ack.messageIds,
duplicate: acknowledged.duplicate,
legacyCompatibility: { acknowledged: true }
}
}
const history = params.all || (params.unread === false && !params.peek)
const readOnly = Boolean(params.peek || history)
const read = () =>
history
? db.getLegacyMailHistory({ principalId: principal.id, types: typeFilter })
: db.getLegacyMailPage({ principalId: principal.id, types: typeFilter })
let page = read()
const deadline =
Date.now() + Math.max(params.timeoutMs ?? ORCHESTRATION_MESSAGE_WAIT_DEFAULT_TIMEOUT_MS, 0)
while (page.messages.length === 0 && params.wait && !signal?.aborted && Date.now() < deadline) {
if (principal.role === 'coordinator' && db.hasPendingCurrentDelivery(principal.run_id)) {
break
}
await runtime.waitForMessage(
principal.role === 'worker'
? `dispatch:${principal.dispatch_id as string}`
: principal.terminal_handle,
{
typeFilter,
timeoutMs: Math.min(1_000, Math.max(deadline - Date.now(), 1)),
signal
}
)
page = read()
}
const consuming = !readOnly
const messageIds = consuming ? page.messages.map((message) => message.id) : []
const formattingAuthority = readOnly
? 'legacy_read_only'
: page.recovery
? 'legacy_recovery_replay'
: 'legacy_compatibility'
const formatted =
params.format || params.inject
? page.messages
.map((message) =>
formatMessageBanner(message, {
authority: formattingAuthority,
supportedActionHints: readOnly
? []
: supportedLegacyHints(message, principal, params.compatibilityCliCommand ?? 'orca')
})
)
.join('\n\n')
: undefined
const currentDelivery =
principal.role === 'coordinator' && db.hasPendingCurrentDelivery(principal.run_id)
? {
runId: principal.run_id,
checkCommand: `${params.compatibilityCliCommand ?? 'orca'} orchestration check --run ${principal.run_id}`,
ackCommand: `${params.compatibilityCliCommand ?? 'orca'} orchestration check --run ${principal.run_id} --ack <delivery-id>`
}
: undefined
return {
runId: principal.run_id,
dispatchId: principal.dispatch_id,
messages: page.messages,
count: page.messages.length,
formatted,
timedOut: Boolean(params.wait && page.messages.length === 0 && !signal?.aborted),
cancelled: signal?.aborted === true,
connectionLost: signal?.aborted === true,
legacyCompatibility: {
recovery: page.recovery,
readOnly,
ackMessageIds: messageIds,
currentDelivery
}
}
}
export async function handleLegacyReply(args: {
runtime: OrcaRuntimeService
authority: LegacyCompatibilityAuthority
request: RpcRequest
params: LegacyReplyParams
}): Promise<unknown | undefined> {
const { runtime, authority, request, params } = args
const db = runtime.getOrchestrationDb()
const original = db.getMessageById(params.id)
if (!original || original.delivery_contract !== 'legacy_direct') {
return undefined
}
const principal = authority.attestCoordinator(request, original.run_id)
if (!principal) {
throw legacyCoordinatorReadOnly()
}
const operation = operationIdentity(request, 'reply', {
questionId: params.id,
body: params.body
})
const committed = db.commitLegacyReplyOperation({
principalId: principal.id,
operationKey: operation.key,
method: request.method,
payloadHash: operation.payloadHash,
questionId: params.id,
body: params.body
})
if (!committed.duplicate) {
runtime.notifyMessageArrived(committed.question.asker_handle, 'status')
}
return {
message: committed.message,
question: committed.question,
duplicate: committed.duplicate,
legacyCompatibility: { replayed: committed.duplicate }
}
}
function legacyCoordinatorReadOnly(): OrchestrationError {
return new OrchestrationError(
'legacy_read_only',
'This retained legacy coordinator could not prove its original process identity. No effects were applied.',
{ effectsApplied: false }
)
}
@@ -0,0 +1,209 @@
import { createHash, randomUUID } from 'node:crypto'
import type { RpcRequest } from './core'
import type { MessageType } from '../orchestration/db'
import { MESSAGE_TYPES } from '../orchestration/types'
import type { LegacyCompatibilityPrincipalRow, MessageRow } from '../orchestration/types'
import { OrchestrationError } from '../orchestration/orchestration-error'
const MESSAGE_TYPE_SET = new Set<string>(MESSAGE_TYPES)
export type LegacySendParams = {
from?: string
to?: string
subject: string
body?: string
type?: string
priority?: 'normal' | 'high' | 'urgent'
payload?: string
}
export type LegacyCheckParams = {
terminal?: string
run?: string
unread?: boolean
peek?: boolean
all?: boolean
types?: string
format?: boolean
inject?: boolean
wait?: boolean
timeoutMs?: number
compatibilityAck?: string
compatibilityQuestionAck?: string
compatibilityCliCommand?: 'orca' | 'orca-ide' | 'orca-dev'
}
export type LegacyAskParams = {
from?: string
to?: string
run?: string
question?: string
resume?: string
options?: string
timeoutMs?: number
compatibilityCliCommand?: 'orca' | 'orca-ide' | 'orca-dev'
compatibilityWindowsCommand?: 'orca' | 'orca-ide'
}
export type LegacyReplyParams = {
id: string
body: string
from?: string
run?: string
}
export function operationIdentity(
request: RpcRequest,
method: string,
payload: unknown
): { key: string; payloadHash: string } {
const canonical = JSON.stringify(canonicalize(payload))
const payloadHash = createHash('sha256').update(canonical).digest('hex')
const semantic =
method === 'worker_done' || method === 'reply' || method === 'ask'
? `${method}:${payloadHash}`
: `${method}:${randomUUID()}`
return {
key: request.compatibilityInvocationId
? `invocation:${request.compatibilityInvocationId}`
: `semantic:${semantic}`,
payloadHash
}
}
export function parseLegacyPayload(raw: string | undefined): Record<string, unknown> {
if (!raw) {
return {}
}
try {
const parsed: unknown = JSON.parse(raw)
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return parsed as Record<string, unknown>
}
} catch {
// Fall through to one stable validation error.
}
throw new OrchestrationError('invalid_argument', 'Message payload must be valid JSON.')
}
export function parseLegacyMailAck(raw: string): { messageIds: string[]; types?: MessageType[] } {
try {
const parsed: unknown = JSON.parse(raw)
const messageIds = Array.isArray(parsed)
? parsed
: parsed && typeof parsed === 'object' && !Array.isArray(parsed)
? (parsed as { messageIds?: unknown }).messageIds
: undefined
const types =
parsed && typeof parsed === 'object' && !Array.isArray(parsed)
? (parsed as { types?: unknown }).types
: undefined
if (
Array.isArray(messageIds) &&
messageIds.length > 0 &&
messageIds.every((value) => typeof value === 'string' && value.length > 0) &&
(types === undefined ||
(Array.isArray(types) &&
types.every((value) => typeof value === 'string' && MESSAGE_TYPE_SET.has(value))))
) {
return {
messageIds,
types: types && types.length > 0 ? (types as MessageType[]) : undefined
}
}
} catch {
// Fall through to one stable validation error.
}
throw new OrchestrationError('invalid_argument', 'Invalid compatibility acknowledgment.')
}
export function parseLegacyQuestionAck(raw: string): {
questionId: string
answerMessageId: string
} {
try {
const parsed: unknown = JSON.parse(raw)
if (
parsed &&
typeof parsed === 'object' &&
!Array.isArray(parsed) &&
typeof (parsed as { questionId?: unknown }).questionId === 'string' &&
typeof (parsed as { answerMessageId?: unknown }).answerMessageId === 'string'
) {
return parsed as { questionId: string; answerMessageId: string }
}
} catch {
// Fall through to one stable validation error.
}
throw new OrchestrationError('invalid_argument', 'Invalid legacy question acknowledgment.')
}
export function parseLegacyTypes(raw: string | undefined): MessageType[] | undefined {
const values = raw
?.split(',')
.map((value) => value.trim())
.filter(Boolean)
const invalidValues = values?.filter((value) => !MESSAGE_TYPE_SET.has(value))
if (invalidValues && invalidValues.length > 0) {
throw new OrchestrationError('invalid_argument', `Invalid --types: ${invalidValues.join(',')}`)
}
return values && values.length > 0 ? (values as MessageType[]) : undefined
}
export function parseLegacyOptions(raw: string | undefined): string[] {
return (
raw
?.split(',')
.map((value) => value.trim())
.filter(Boolean) ?? []
)
}
export function supportedLegacyHints(
message: MessageRow,
principal: LegacyCompatibilityPrincipalRow,
cliCommand: 'orca' | 'orca-ide' | 'orca-dev'
): string[] {
if (
principal.role !== 'coordinator' ||
(message.type !== 'question' && message.type !== 'decision_gate')
) {
return []
}
return [
`${cliCommand} orchestration reply --id ${message.id} --from ${principal.terminal_handle} --body "..."`
]
}
export function inferLegacyWorkerOutcome(subject: string): 'succeeded' | 'failed' {
const normalized = subject.trim()
return normalized === 'Failed' || normalized.startsWith('Failed:') ? 'failed' : 'succeeded'
}
export function isWorkerOutcome(value: unknown): value is 'succeeded' | 'failed' {
return value === 'succeeded' || value === 'failed'
}
export function stringValue(value: unknown): string | undefined {
return typeof value === 'string' && value.length > 0 ? value : undefined
}
export function normalizeLegacyText(value: string): string {
return value.replace(/\r\n/g, '\n').trim()
}
function canonicalize(value: unknown): unknown {
if (Array.isArray(value)) {
return value.map(canonicalize)
}
if (!value || typeof value !== 'object') {
return value
}
const source = value as Record<string, unknown>
return Object.fromEntries(
Object.keys(source)
.sort()
.filter((key) => source[key] !== undefined)
.map((key) => [key, canonicalize(source[key])])
)
}
@@ -0,0 +1,33 @@
import { parsePaneKey } from '../../../shared/stable-pane-id'
import { OrchestrationError } from '../orchestration/orchestration-error'
export function equivalentLegacyPaneKey(
a: string | null | undefined,
b: string | null | undefined
): boolean {
if (!a || !b) {
return false
}
if (a === b) {
return true
}
const aLeaf = parsePaneKey(a)?.leafId
const bLeaf = parsePaneKey(b)?.leafId
return Boolean(aLeaf && bLeaf && aLeaf === bLeaf)
}
export function legacyReadOnlyError(): OrchestrationError {
return new OrchestrationError(
'legacy_read_only',
'This retained legacy assignment could not prove authority from its original live process. No effects were applied.',
{ effectsApplied: false }
)
}
export function legacyCoordinatorReadOnly(): OrchestrationError {
return new OrchestrationError(
'legacy_read_only',
'This retained legacy coordinator could not prove its original process identity. No effects were applied.',
{ effectsApplied: false }
)
}
@@ -0,0 +1,364 @@
import { createHash } from 'node:crypto'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { OrchestrationCompatibilityEvidence } from '../../../shared/orchestration-compatibility-evidence'
import { ORCHESTRATION_CONTRACT_VERSION } from '../../../shared/protocol-version'
import Database from '../../sqlite/sync-database'
import { OrchestrationDb } from '../orchestration/db'
import { OrcaRuntimeService } from '../orca-runtime'
import type { RpcRequest } from './core'
import { RpcDispatcher } from './dispatcher'
import { ORCHESTRATION_METHODS } from './methods/orchestration'
const COORDINATOR_HANDLE = 'term_legacy_coord'
const CURRENT_COORDINATOR_HANDLE = 'term_current_coord'
const CURRENT_COORDINATOR_PANE = 'tab_current:55555555-5555-4555-8555-555555555555'
const WORKER_HANDLE = 'term_legacy_worker'
const WORKER_PANE = 'tab_worker:33333333-3333-4333-8333-333333333333'
const tempDirs: string[] = []
const databases: OrchestrationDb[] = []
afterEach(() => {
databases.splice(0).forEach((database) => database.close())
tempDirs.splice(0).forEach((dir) => rmSync(dir, { recursive: true, force: true }))
})
function createHarness(options?: { seedCutoverQuestion?: boolean; seedCutoverAnswer?: boolean }): {
db: OrchestrationDb
dispatcher: RpcDispatcher
adoptedRunId: string
dispatchId: string
cutoverQuestionId?: string
cutoverAnswerId?: string
} {
const dir = mkdtempSync(join(tmpdir(), 'orca-legacy-question-takeover-'))
tempDirs.push(dir)
const dbPath = join(dir, 'orchestration.db')
const before = new OrchestrationDb(dbPath)
const task = before.createTask({
spec: 'legacy assignment',
createdByTerminalHandle: COORDINATOR_HANDLE
})
const dispatch = before.createDispatchContext(task.id, WORKER_HANDLE, WORKER_PANE)
const cutoverQuestion = options?.seedCutoverQuestion
? before.insertMessage({
from: WORKER_HANDLE,
to: COORDINATOR_HANDLE,
subject: 'Question',
body: 'Continue after update?',
type: 'decision_gate',
payload: JSON.stringify({
question: 'Continue after update?',
options: ['yes', 'no']
})
})
: undefined
const cutoverAnswer =
cutoverQuestion && options?.seedCutoverAnswer
? before.insertMessage({
from: COORDINATOR_HANDLE,
to: WORKER_HANDLE,
subject: 'Re: Question',
body: 'Continue.',
threadId: cutoverQuestion.id
})
: undefined
before.close()
const raw = new Database(dbPath)
raw.exec(`
UPDATE dispatch_contexts SET process_incarnation = 'process-1';
DROP INDEX IF EXISTS idx_messages_delivery_contract;
DROP TABLE legacy_mail_receipts;
DROP TABLE legacy_operation_receipts;
DROP TABLE legacy_compatibility_principals;
DROP TABLE legacy_adoptions;
`)
raw.pragma('user_version = 18')
raw.close()
const db = new OrchestrationDb(dbPath)
databases.push(db)
const runtime = new OrcaRuntimeService()
runtime.setOrchestrationDb(db)
vi.spyOn(runtime, 'getTerminalPaneKey').mockImplementation((handle) =>
handle === WORKER_HANDLE
? WORKER_PANE
: handle === CURRENT_COORDINATOR_HANDLE
? CURRENT_COORDINATOR_PANE
: null
)
vi.spyOn(runtime, 'verifyOrchestrationCompatibilityCaller').mockImplementation((evidence) => {
const worker =
evidence?.terminalHandle === WORKER_HANDLE &&
evidence.paneKey === WORKER_PANE &&
evidence.launchToken === 'worker-token'
const coordinator =
evidence?.terminalHandle === CURRENT_COORDINATOR_HANDLE &&
evidence.paneKey === CURRENT_COORDINATOR_PANE &&
evidence.launchToken === 'current-coordinator-token'
const launchToken = worker
? 'worker-token'
: coordinator
? 'current-coordinator-token'
: undefined
return launchToken
? {
hostScope: { kind: 'local', hostId: 'local' },
terminalHandle: evidence?.terminalHandle as string,
paneKey: evidence?.paneKey as string,
processIncarnation: 'process-1',
launchTokenHash: createHash('sha256').update(launchToken).digest('hex')
}
: null
})
return {
db,
dispatcher: new RpcDispatcher({ runtime, methods: ORCHESTRATION_METHODS }),
adoptedRunId: db.getLegacyAdoption()?.adopted_run_id as string,
dispatchId: dispatch.id,
...(cutoverQuestion ? { cutoverQuestionId: cutoverQuestion.id } : {}),
...(cutoverAnswer ? { cutoverAnswerId: cutoverAnswer.id } : {})
}
}
function request(
method: string,
params: unknown,
invocationId: string,
evidence?: OrchestrationCompatibilityEvidence
): RpcRequest {
return {
id: `rpc_${invocationId}`,
authToken: 'caller-token',
method,
params,
orchestrationContractVersion: ORCHESTRATION_CONTRACT_VERSION,
orchestrationRequestId: invocationId,
compatibilityInvocationId: invocationId,
orchestrationCompatibilityEvidence: evidence
}
}
const workerEvidence: OrchestrationCompatibilityEvidence = {
terminalHandle: WORKER_HANDLE,
paneKey: WORKER_PANE,
launchToken: 'worker-token'
}
describe('legacy question takeover compatibility', () => {
it('reuses a pending ask that crossed the schema cutover', async () => {
const harness = createHarness({ seedCutoverQuestion: true })
const asked = await harness.dispatcher.dispatch(
request(
'orchestration.ask',
{
from: WORKER_HANDLE,
to: COORDINATOR_HANDLE,
question: 'Continue after update?',
options: 'yes,no',
timeoutMs: 0
},
'retry-cutover-ask',
workerEvidence
)
)
expect(asked).toMatchObject({
ok: true,
result: {
messageId: harness.cutoverQuestionId,
legacyCompatibility: { replayed: true }
}
})
const sqlite = (harness.db as unknown as { db: Database.Database }).db
expect(
(
sqlite
.prepare(
`SELECT COUNT(*) AS count
FROM messages
WHERE type IN ('decision_gate', 'question')`
)
.get() as { count: number }
).count
).toBe(1)
expect(
(sqlite.prepare('SELECT COUNT(*) AS count FROM question_threads').get() as { count: number })
.count
).toBe(1)
})
it('resumes an answered pinned-A ask without duplicating it', async () => {
const harness = createHarness({
seedCutoverQuestion: true,
seedCutoverAnswer: true
})
const resumed = await harness.dispatcher.dispatch(
request(
'orchestration.ask',
{
from: WORKER_HANDLE,
resume: harness.cutoverQuestionId,
timeoutMs: 0
},
'resume-answered-cutover-ask',
workerEvidence
)
)
expect(resumed).toMatchObject({
ok: true,
result: {
answer: 'Continue.',
answerMessageId: harness.cutoverAnswerId,
messageId: harness.cutoverQuestionId,
legacyCompatibility: {
answerAcknowledgement: {
questionId: harness.cutoverQuestionId,
answerMessageId: harness.cutoverAnswerId
}
}
}
})
const sqlite = (harness.db as unknown as { db: Database.Database }).db
expect(
(sqlite.prepare('SELECT COUNT(*) AS count FROM question_threads').get() as { count: number })
.count
).toBe(1)
})
it('resumes and idempotently acknowledges the exact current Run answer', async () => {
const harness = createHarness()
const asked = await harness.dispatcher.dispatch(
request(
'orchestration.ask',
{
from: WORKER_HANDLE,
to: COORDINATOR_HANDLE,
question: 'Continue after takeover?',
timeoutMs: 0
},
'ask-before-takeover',
workerEvidence
)
)
const questionId = (asked as { result: { messageId: string } }).result.messageId
await expect(
harness.dispatcher.dispatch(
request(
'orchestration.runUse',
{
id: harness.adoptedRunId,
from: CURRENT_COORDINATOR_HANDLE,
takeoverLegacy: true
},
'take-over',
{
terminalHandle: CURRENT_COORDINATOR_HANDLE,
paneKey: CURRENT_COORDINATOR_PANE,
launchToken: 'current-coordinator-token'
}
)
)
).resolves.toMatchObject({ ok: true })
await expect(
harness.dispatcher.dispatch(
request(
'orchestration.reply',
{
id: questionId,
body: 'Continue.',
from: CURRENT_COORDINATOR_HANDLE,
run: harness.adoptedRunId
},
'answer-after-takeover'
)
)
).resolves.toMatchObject({ ok: true })
const resumed = await harness.dispatcher.dispatch(
request(
'orchestration.ask',
{ from: WORKER_HANDLE, resume: questionId, timeoutMs: 100 },
'resume-after-takeover',
workerEvidence
)
)
expect(resumed).toMatchObject({
ok: true,
result: {
answer: 'Continue.',
legacyCompatibility: {
answerAcknowledgement: {
questionId,
answerMessageId: expect.any(String)
}
}
}
})
const acknowledgement = (
resumed as {
result: {
legacyCompatibility: {
answerAcknowledgement: { questionId: string; answerMessageId: string }
}
}
}
).result.legacyCompatibility.answerAcknowledgement
expect(harness.db.getMessageById(acknowledgement.answerMessageId)).toMatchObject({
run_id: harness.adoptedRunId,
from_handle: `run:${harness.adoptedRunId}`,
to_handle: `dispatch:${harness.dispatchId}`,
delivery_contract: 'current_delivery',
thread_id: questionId
})
const acknowledge = (invocationId: string) =>
harness.dispatcher.dispatch(
request(
'orchestration.check',
{
terminal: WORKER_HANDLE,
compatibilityQuestionAck: JSON.stringify(acknowledgement)
},
invocationId,
workerEvidence
)
)
const sqlite = (harness.db as unknown as { db: Database.Database }).db
sqlite
.prepare('UPDATE messages SET to_handle = ? WHERE id = ?')
.run('dispatch:foreign', acknowledgement.answerMessageId)
await expect(acknowledge('ack-wrong-answer-route')).resolves.toMatchObject({
ok: false,
error: { code: 'request_mismatch' }
})
sqlite
.prepare('UPDATE messages SET to_handle = ? WHERE id = ?')
.run(`dispatch:${harness.dispatchId}`, acknowledgement.answerMessageId)
await expect(acknowledge('ack-answer')).resolves.toMatchObject({
ok: true,
result: {
acknowledged: [acknowledgement.answerMessageId],
duplicate: false,
legacyCompatibility: { acknowledged: true }
}
})
await expect(acknowledge('ack-answer-repeat')).resolves.toMatchObject({
ok: true,
result: {
acknowledged: [acknowledgement.answerMessageId],
duplicate: true,
legacyCompatibility: { acknowledged: true }
}
})
})
})
@@ -0,0 +1,169 @@
import { clampOrchestrationAskTimeoutMs } from '../../../shared/orchestration-ask-timeout'
import type { RpcRequest } from './core'
import type { OrcaRuntimeService } from '../orca-runtime'
import { OrchestrationError } from '../orchestration/orchestration-error'
import { LEGACY_CONTRACT_VERSION } from '../orchestration/db'
import type { LegacyCompatibilityAuthority } from './orchestration-legacy-authority'
import {
normalizeLegacyText,
operationIdentity,
parseLegacyOptions,
type LegacyAskParams
} from './orchestration-legacy-operation'
export async function handleLegacyAsk(args: {
runtime: OrcaRuntimeService
authority: LegacyCompatibilityAuthority
request: RpcRequest
params: LegacyAskParams
signal?: AbortSignal
}): Promise<unknown | undefined> {
const { runtime, authority, request, params, signal } = args
const dispatch = authority.resolveAskDispatch(request, params)
if (!dispatch || dispatch.contract_version !== LEGACY_CONTRACT_VERSION) {
return undefined
}
const db = runtime.getOrchestrationDb()
if (!params.resume) {
if (!params.to) {
throw new OrchestrationError('invalid_argument', 'Legacy ask requires --to.')
}
if (!db.isLegacyCoordinatorHandle(dispatch.run_id, params.to)) {
throw new OrchestrationError(
'request_mismatch',
`Terminal ${params.to} is not a retained coordinator for this legacy Dispatch.`
)
}
}
const principal = authority.attestWorker(request, dispatch)
const timeoutMs = clampOrchestrationAskTimeoutMs(params.timeoutMs)
let questionId = params.resume
let duplicate = true
if (questionId) {
const question = db.getQuestion(questionId)
if (!question || question.dispatch_id !== dispatch.id) {
throw new OrchestrationError(
'question_not_found',
`Question ${questionId} does not belong to this legacy Dispatch.`
)
}
} else {
const question = params.question as string
const options = parseLegacyOptions(params.options)
const recipient = params.to as string
const operation = operationIdentity(request, 'ask', {
question: normalizeLegacyText(question),
options: options.map(normalizeLegacyText),
recipient
})
const priorReceipt = db.getLegacyOperationReceipt(principal.id, operation.key)
let existingQuestionId: string | undefined
if (!priorReceipt) {
const matches = db.findLegacyQuestionsBySemanticIdentity({
principalId: principal.id,
question,
options,
recipientHandle: recipient
})
const unclaimed = matches.filter((match) => !match.claimedByOperation)
const pending = unclaimed.filter((match) => match.question.status === 'pending')
const lostAnswer = unclaimed.find(
(match) => match.question.status === 'answered' && !match.answerAcknowledged
)
if (lostAnswer) {
const cliCommand =
params.compatibilityCliCommand ?? params.compatibilityWindowsCommand ?? 'orca'
throw new OrchestrationError(
'operation_unknown',
`A matching legacy answer may have been accepted before the update. Run ${cliCommand} orchestration check --terminal ${principal.terminal_handle} before asking again.`
)
}
if (pending.length > 1) {
throw new OrchestrationError(
'operation_unknown',
'Multiple matching pending legacy questions exist; drain legacy check before retrying.'
)
}
existingQuestionId = pending[0]?.question.message_id
}
const committed = db.commitLegacyAskOperation({
question,
principalId: principal.id,
operationKey: operation.key,
method: request.method,
payloadHash: operation.payloadHash,
options,
recipientHandle: recipient,
existingQuestionId
})
questionId = committed.question.message_id
duplicate = committed.duplicate || Boolean(existingQuestionId)
if (!duplicate) {
runtime.notifyMessageArrived(committed.message.to_handle, committed.message.type)
}
}
if (params.compatibilityWindowsCommand && !params.resume) {
return {
answer: null,
messageId: questionId,
threadId: questionId,
timedOut: false,
cancelled: false,
connectionLost: false,
timeoutMs,
legacyCompatibility: {
resumeRequired: true,
resumeCommand: `${params.compatibilityWindowsCommand} orchestration ask --resume ${questionId}`
}
}
}
const deadline = Date.now() + timeoutMs
while (true) {
const current = db.getQuestion(questionId as string)
if (!current || current.status === 'closed') {
throw new OrchestrationError(
'dispatch_inactive',
`Question ${questionId as string} closed because its Dispatch is inactive.`
)
}
if (current.status === 'answered') {
return {
answer: current.answer_body,
answerMessageId: current.answer_message_id,
messageId: current.message_id,
threadId: current.message_id,
timedOut: false,
cancelled: false,
connectionLost: false,
timeoutMs,
legacyCompatibility: {
replayed: duplicate,
answerAcknowledgement: current.answer_message_id
? {
questionId: current.message_id,
answerMessageId: current.answer_message_id
}
: undefined
}
}
}
if (signal?.aborted || Date.now() >= deadline) {
return {
answer: null,
messageId: questionId,
threadId: questionId,
timedOut: !signal?.aborted,
cancelled: signal?.aborted === true,
connectionLost: signal?.aborted === true,
timeoutMs,
legacyCompatibility: { replayed: duplicate, ackMessageIds: [] }
}
}
await runtime.waitForMessage(principal.terminal_handle, {
timeoutMs: Math.min(1_000, Math.max(deadline - Date.now(), 1)),
signal
})
}
}
@@ -0,0 +1,565 @@
import { createHash } from 'node:crypto'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { OrchestrationCompatibilityEvidence } from '../../../shared/orchestration-compatibility-evidence'
import { ORCHESTRATION_CONTRACT_VERSION } from '../../../shared/protocol-version'
import Database from '../../sqlite/sync-database'
import { OrcaRuntimeService } from '../orca-runtime'
import { OrchestrationDb } from '../orchestration/db'
import type { RpcRequest } from './core'
import { RpcDispatcher } from './dispatcher'
import { ORCHESTRATION_METHODS } from './methods/orchestration'
const WORKER_HANDLE = 'term_legacy_worker'
const WORKER_PANE = 'tab_worker:33333333-3333-4333-8333-333333333333'
const COORDINATOR_HANDLE = 'term_legacy_coord'
const COORDINATOR_PANE = 'tab_coord:44444444-4444-4444-8444-444444444444'
const CURRENT_COORDINATOR_HANDLE = 'term_current_coord'
const CURRENT_COORDINATOR_PANE = 'tab_current:55555555-5555-4555-8555-555555555555'
type Harness = {
db: OrchestrationDb
dispatcher: RpcDispatcher
adoptedRunId: string
taskId: string
dispatchId: string
}
const tempDirs: string[] = []
const databases: OrchestrationDb[] = []
afterEach(() => {
for (const database of databases.splice(0)) {
database.close()
}
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true })
}
})
function createHarness(): Harness {
const dir = mkdtempSync(join(tmpdir(), 'orca-legacy-takeover-'))
tempDirs.push(dir)
const dbPath = join(dir, 'orchestration.db')
const before = new OrchestrationDb(dbPath)
const task = before.createTask({
spec: 'legacy assignment',
createdByTerminalHandle: COORDINATOR_HANDLE
})
const dispatch = before.createDispatchContext(task.id, WORKER_HANDLE, WORKER_PANE)
before.close()
const raw = new Database(dbPath)
raw.exec(`
UPDATE dispatch_contexts SET process_incarnation = 'process-1';
DROP INDEX IF EXISTS idx_messages_delivery_contract;
DROP TABLE legacy_mail_receipts;
DROP TABLE legacy_operation_receipts;
DROP TABLE legacy_compatibility_principals;
DROP TABLE legacy_adoptions;
`)
raw.pragma('user_version = 18')
raw.close()
const db = new OrchestrationDb(dbPath)
databases.push(db)
const adoptedRunId = db.getLegacyAdoption()?.adopted_run_id as string
const runtime = new OrcaRuntimeService()
runtime.setOrchestrationDb(db)
vi.spyOn(runtime, 'getTerminalPaneKey').mockImplementation((handle) =>
handle === COORDINATOR_HANDLE
? COORDINATOR_PANE
: handle === WORKER_HANDLE
? WORKER_PANE
: handle === CURRENT_COORDINATOR_HANDLE
? CURRENT_COORDINATOR_PANE
: null
)
vi.spyOn(runtime, 'verifyOrchestrationCompatibilityCaller').mockImplementation((proof) => {
const validWorker = proof?.terminalHandle === WORKER_HANDLE && proof.paneKey === WORKER_PANE
const validCoordinator =
proof?.terminalHandle === COORDINATOR_HANDLE && proof.paneKey === COORDINATOR_PANE
const validCurrentCoordinator =
proof?.terminalHandle === CURRENT_COORDINATOR_HANDLE &&
proof.paneKey === CURRENT_COORDINATOR_PANE
if ((!validWorker && !validCoordinator && !validCurrentCoordinator) || !proof?.launchToken) {
return null
}
return {
hostScope: { kind: 'local', hostId: 'local' },
terminalHandle: proof.terminalHandle as string,
paneKey: proof.paneKey as string,
processIncarnation: 'process-1',
launchTokenHash: createHash('sha256').update(proof.launchToken).digest('hex')
}
})
vi.spyOn(runtime, 'notifyMessageArrived').mockImplementation(() => {})
return {
db,
dispatcher: new RpcDispatcher({ runtime, methods: ORCHESTRATION_METHODS }),
adoptedRunId,
taskId: task.id,
dispatchId: dispatch.id
}
}
function evidence(
role: 'worker' | 'coordinator' | 'current-coordinator'
): OrchestrationCompatibilityEvidence {
const worker = role === 'worker'
const currentCoordinator = role === 'current-coordinator'
return {
terminalHandle: worker
? WORKER_HANDLE
: currentCoordinator
? CURRENT_COORDINATOR_HANDLE
: COORDINATOR_HANDLE,
paneKey: worker
? WORKER_PANE
: currentCoordinator
? CURRENT_COORDINATOR_PANE
: COORDINATOR_PANE,
launchToken: `${role}-token`
}
}
function request(
method: string,
params: unknown,
proof: OrchestrationCompatibilityEvidence,
invocationId: string
): RpcRequest {
return {
id: `rpc_${invocationId}`,
authToken: 'caller-token',
method,
params,
orchestrationContractVersion: ORCHESTRATION_CONTRACT_VERSION,
orchestrationRequestId: invocationId,
compatibilityInvocationId: invocationId,
orchestrationCompatibilityEvidence: proof
}
}
function counts(db: OrchestrationDb): Record<string, number> {
const sqlite = (db as unknown as { db: Database.Database }).db
return Object.fromEntries(
[
'messages',
'legacy_compatibility_principals',
'legacy_operation_receipts',
'legacy_mail_receipts',
'mutation_receipts'
].map((table) => [
table,
(sqlite.prepare(`SELECT COUNT(*) AS count FROM ${table}`).get() as { count: number }).count
])
)
}
function escalationParams(harness: Harness) {
return {
from: WORKER_HANDLE,
to: COORDINATOR_HANDLE,
subject: 'Blocked',
type: 'escalation',
payload: JSON.stringify({ taskId: harness.taskId, dispatchId: harness.dispatchId })
}
}
describe('legacy compatibility after explicit takeover', () => {
it('binds takeover to the authenticated invoking coordinator pane', async () => {
const harness = createHarness()
const before = harness.db.getRun(harness.adoptedRunId)
const spoofed = await harness.dispatcher.dispatch(
request(
'orchestration.runUse',
{
id: harness.adoptedRunId,
from: COORDINATOR_HANDLE,
takeoverLegacy: true
},
evidence('current-coordinator'),
'spoofed-takeover'
)
)
expect(spoofed).toMatchObject({
ok: false,
error: { code: 'legacy_read_only', data: { effectsApplied: false } }
})
expect(harness.db.getRun(harness.adoptedRunId)).toEqual(before)
const bound = await harness.dispatcher.dispatch(
request(
'orchestration.runUse',
{
id: harness.adoptedRunId,
from: CURRENT_COORDINATOR_HANDLE,
takeoverLegacy: true
},
evidence('current-coordinator'),
'authenticated-takeover'
)
)
expect(bound).toMatchObject({
ok: true,
result: {
run: {
coordinator_handle: CURRENT_COORDINATOR_HANDLE,
coordinator_pane_key: CURRENT_COORDINATOR_PANE
}
}
})
})
it('does not let an uncommitted legacy coordinator attest after explicit takeover', async () => {
const harness = createHarness()
harness.db.bindRun({
runId: harness.adoptedRunId,
coordinatorHandle: 'term_current_coord',
coordinatorPaneKey: 'tab_current:55555555-5555-4555-8555-555555555555',
takeoverLegacy: true
})
expect(harness.db.getLegacyCoordinatorPrincipal(harness.adoptedRunId)).toBeUndefined()
const before = counts(harness.db)
const response = await harness.dispatcher.dispatch(
request(
'orchestration.taskList',
{},
evidence('coordinator'),
'uncommitted-coordinator-after-takeover'
)
)
expect(response).toMatchObject({ ok: false, error: { code: 'legacy_read_only' } })
expect(harness.db.getLegacyCoordinatorPrincipal(harness.adoptedRunId)).toBeUndefined()
expect(counts(harness.db)).toEqual(before)
})
it.each(['escalation', 'worker_done'] as const)(
'keeps the retained coordinator address routable for post-takeover %s',
async (type) => {
const harness = createHarness()
harness.db.bindRun({
runId: harness.adoptedRunId,
coordinatorHandle: 'term_current_coord',
coordinatorPaneKey: 'tab_current:55555555-5555-4555-8555-555555555555',
takeoverLegacy: true
})
const response = await harness.dispatcher.dispatch(
request(
'orchestration.send',
{
...escalationParams(harness),
type,
subject: type === 'worker_done' ? 'Completed' : 'Blocked'
},
evidence('worker'),
`post-takeover-${type}`
)
)
expect(response).toMatchObject({
ok: true,
result: {
message: {
to_handle: `run:${harness.adoptedRunId}`,
delivery_contract: 'current_delivery',
type
}
}
})
}
)
it('routes future worker mail to Run Delivery after same-handle takeover', async () => {
const harness = createHarness()
harness.db.bindRun({
runId: harness.adoptedRunId,
coordinatorHandle: COORDINATOR_HANDLE,
coordinatorPaneKey: COORDINATOR_PANE,
takeoverLegacy: true
})
const response = await harness.dispatcher.dispatch(
request(
'orchestration.send',
escalationParams(harness),
evidence('worker'),
'same-handle-takeover-mail'
)
)
expect(response).toMatchObject({
ok: true,
result: {
message: {
to_handle: `run:${harness.adoptedRunId}`,
delivery_contract: 'current_delivery',
type: 'escalation'
}
}
})
})
it('keeps post-takeover legacy ask routed to the adopted Run', async () => {
const harness = createHarness()
harness.db.bindRun({
runId: harness.adoptedRunId,
coordinatorHandle: 'term_current_coord',
coordinatorPaneKey: 'tab_current:55555555-5555-4555-8555-555555555555',
takeoverLegacy: true
})
const response = await harness.dispatcher.dispatch(
request(
'orchestration.ask',
{
from: WORKER_HANDLE,
to: COORDINATOR_HANDLE,
question: 'Continue after takeover?',
timeoutMs: 1
},
evidence('worker'),
'post-takeover-ask'
)
)
const messageId = (response as { result?: { messageId?: string } }).result?.messageId
expect(response).toMatchObject({
ok: true,
result: { timedOut: true, messageId: expect.any(String) }
})
expect(harness.db.getMessageById(messageId as string)).toMatchObject({
to_handle: `run:${harness.adoptedRunId}`,
delivery_contract: 'current_delivery',
type: 'question'
})
})
it('keeps an active legacy worker usable across authenticated takeover', async () => {
const harness = createHarness()
await expect(
harness.dispatcher.dispatch(
request(
'orchestration.runUse',
{ id: harness.adoptedRunId, from: COORDINATOR_HANDLE },
evidence('coordinator'),
'bind-original-coordinator'
)
)
).resolves.toMatchObject({ ok: true })
const pendingAsk = await harness.dispatcher.dispatch(
request(
'orchestration.ask',
{
from: WORKER_HANDLE,
to: COORDINATOR_HANDLE,
question: 'Continue with the migration?',
timeoutMs: 0
},
evidence('worker'),
'pending-before-takeover'
)
)
const questionId = (pendingAsk as { result: { messageId: string } }).result.messageId
const unrelated = harness.db.insertMessage({
runId: harness.adoptedRunId,
from: WORKER_HANDLE,
to: 'term_peer_worker',
subject: 'peer-only',
deliveryContract: 'legacy_direct'
})
const currentRequest = (method: string, params: unknown, invocationId: string) =>
request(method, params, evidence('current-coordinator'), invocationId)
const takeoverParams = {
id: harness.adoptedRunId,
from: CURRENT_COORDINATOR_HANDLE,
takeoverLegacy: true
}
const takeover = await harness.dispatcher.dispatch(
currentRequest('orchestration.runUse', takeoverParams, 'explicit-takeover')
)
const repeated = await harness.dispatcher.dispatch(
currentRequest('orchestration.runUse', takeoverParams, 'explicit-takeover-repeat')
)
expect(takeover).toMatchObject({
ok: true,
result: { binding: { consumerGeneration: 2 } }
})
expect(repeated).toMatchObject({
ok: true,
result: { binding: { consumerGeneration: 2 } }
})
expect(harness.db.getDispatchContextById(harness.dispatchId)?.status).toBe('dispatched')
expect(harness.db.getLegacyCoordinatorPrincipal(harness.adoptedRunId)?.status).toBe('revoked')
expect(harness.db.getMessageById(unrelated.id)).toMatchObject({
to_handle: 'term_peer_worker',
delivery_contract: 'legacy_direct'
})
const promoted = await harness.dispatcher.dispatch(
currentRequest(
'orchestration.check',
{
terminal: CURRENT_COORDINATOR_HANDLE,
run: harness.adoptedRunId,
format: true
},
'read-promoted-question'
)
)
expect(promoted).toMatchObject({
ok: true,
result: {
messages: [
{
id: questionId,
to_handle: `run:${harness.adoptedRunId}`,
delivery_contract: 'current_delivery'
}
],
formatted: expect.not.stringContaining(`--from run:${harness.adoptedRunId}`)
}
})
const deliveryId = (promoted as { result: { deliveryId: string } }).result.deliveryId
await expect(
harness.dispatcher.dispatch(
currentRequest(
'orchestration.reply',
{
id: questionId,
body: 'Yes, continue.',
from: CURRENT_COORDINATOR_HANDLE,
run: harness.adoptedRunId
},
'reply-after-takeover'
)
)
).resolves.toMatchObject({ ok: true, result: { question: { status: 'answered' } } })
await expect(
harness.dispatcher.dispatch(
request(
'orchestration.ask',
{ from: WORKER_HANDLE, resume: questionId, timeoutMs: 100 },
evidence('worker'),
'resume-after-takeover'
)
)
).resolves.toMatchObject({ ok: true, result: { answer: 'Yes, continue.' } })
const followUp = await harness.dispatcher.dispatch(
currentRequest(
'orchestration.send',
{
from: CURRENT_COORDINATOR_HANDLE,
to: `dispatch:${harness.dispatchId}`,
subject: 'Continue from the new coordinator'
},
'current-coordinator-to-legacy-worker'
)
)
expect(followUp).toMatchObject({
ok: true,
result: {
message: {
run_id: harness.adoptedRunId,
to_handle: `dispatch:${harness.dispatchId}`,
delivery_contract: 'legacy_direct'
}
}
})
const workerFollowUp = await harness.dispatcher.dispatch(
request(
'orchestration.check',
{ terminal: WORKER_HANDLE },
evidence('worker'),
'legacy-worker-check-after-takeover'
)
)
expect(workerFollowUp).toMatchObject({
ok: true,
result: {
messages: [{ subject: 'Continue from the new coordinator' }],
legacyCompatibility: { ackMessageIds: [expect.any(String)] }
}
})
const [followUpMessageId] = (
workerFollowUp as { result: { legacyCompatibility: { ackMessageIds: string[] } } }
).result.legacyCompatibility.ackMessageIds
await expect(
harness.dispatcher.dispatch(
request(
'orchestration.check',
{
terminal: WORKER_HANDLE,
compatibilityAck: JSON.stringify({ messageIds: [followUpMessageId] })
},
evidence('worker'),
'legacy-worker-ack-after-takeover'
)
)
).resolves.toMatchObject({
ok: true,
result: {
acknowledged: [followUpMessageId],
legacyCompatibility: { acknowledged: true }
}
})
await harness.dispatcher.dispatch(
currentRequest(
'orchestration.check',
{
terminal: CURRENT_COORDINATOR_HANDLE,
run: harness.adoptedRunId,
ack: deliveryId
},
'ack-promoted-question'
)
)
await expect(
harness.dispatcher.dispatch(
request(
'orchestration.send',
{
from: WORKER_HANDLE,
to: COORDINATOR_HANDLE,
subject: 'Need current coordinator',
type: 'escalation',
payload: JSON.stringify({ taskId: harness.taskId })
},
evidence('worker'),
'escalation-after-takeover'
)
)
).resolves.toMatchObject({
ok: true,
result: {
message: {
to_handle: `run:${harness.adoptedRunId}`,
delivery_contract: 'current_delivery'
}
}
})
await expect(
harness.dispatcher.dispatch(
request(
'orchestration.taskList',
{ callerTerminalHandle: COORDINATOR_HANDLE },
evidence('coordinator'),
'old-coordinator-fenced'
)
)
).resolves.toMatchObject({ ok: false, error: { code: 'legacy_read_only' } })
})
})
@@ -22,13 +22,14 @@ export class OrchestrationMutationExecutor {
async run(
request: RpcRequest,
params: unknown,
invoke: (mutation?: DurableMutationInvocation) => Promise<unknown> | unknown
invoke: (mutation?: DurableMutationInvocation) => Promise<unknown> | unknown,
callerFingerprintOverride?: string
): Promise<unknown> {
const requestId = request.orchestrationRequestId
if (!requestId || !isOrchestrationMutation(request.method, params)) {
return await invoke()
}
const callerFingerprint = authenticatedCallerFingerprint(request)
const callerFingerprint = callerFingerprintOverride ?? authenticatedCallerFingerprint(request)
const payloadHash = createHash('sha256')
.update(JSON.stringify(canonicalize({ method: request.method, params })))
.digest('hex')
@@ -55,6 +56,10 @@ export class OrchestrationMutationExecutor {
: db.beginMutationReceipt(identity)
if (begun.disposition === 'completed') {
const active = this.inFlight.get(key)
if (active) {
return attachMutationReceipt(await active, requestId, true)
}
return attachMutationReceipt(JSON.parse(begun.row.receipt ?? 'null'), requestId, true)
}
if (begun.disposition === 'pending') {
@@ -102,6 +107,20 @@ export class OrchestrationMutationExecutor {
}
}
const executorsByRuntime = new WeakMap<OrcaRuntimeService, OrchestrationMutationExecutor>()
export function getOrchestrationMutationExecutor(
runtime: OrcaRuntimeService
): OrchestrationMutationExecutor {
const existing = executorsByRuntime.get(runtime)
if (existing) {
return existing
}
const executor = new OrchestrationMutationExecutor(runtime)
executorsByRuntime.set(runtime, executor)
return executor
}
export function authenticatedCallerFingerprint(request: RpcRequest): string {
const callerToken =
request.authToken ||
@@ -252,7 +252,10 @@ describe('durable orchestration mutation ledger', () => {
})
it('recovers a lost ask acceptance without creating a second question', async () => {
const db = new OrchestrationDb(':memory:')
const dir = mkdtempSync(join(tmpdir(), 'orca-mutation-ask-recovery-'))
paths.push(dir)
const dbPath = join(dir, 'orchestration.db')
const db = new OrchestrationDb(dbPath)
const runtime = new OrcaRuntimeService()
runtime.setOrchestrationDb(db)
vi.spyOn(runtime, 'getTerminalPaneKey').mockReturnValue('tab_worker:leaf_worker')
@@ -290,7 +293,16 @@ describe('durable orchestration mutation ledger', () => {
const first = firstDispatcher.dispatch(askRequest, { signal: controller.signal })
await vi.waitFor(() => expect(db.getInbox(10)).toHaveLength(1))
const restartedDispatcher = new RpcDispatcher({ runtime, methods: ORCHESTRATION_METHODS })
const restartedDb = new OrchestrationDb(dbPath)
const restartedRuntime = new OrcaRuntimeService()
restartedRuntime.setOrchestrationDb(restartedDb)
vi.spyOn(restartedRuntime, 'getTerminalPaneKey').mockReturnValue('tab_worker:leaf_worker')
vi.spyOn(restartedRuntime, 'getTerminalProcessIncarnation').mockReturnValue('runtime:pty:1')
vi.spyOn(restartedRuntime, 'notifyMessageArrived').mockImplementation(() => {})
const restartedDispatcher = new RpcDispatcher({
runtime: restartedRuntime,
methods: ORCHESTRATION_METHODS
})
const recovered = await restartedDispatcher.dispatch({ ...askRequest, id: 'rpc_ask_2' })
expect(recovered).toMatchObject({
ok: true,
@@ -304,6 +316,7 @@ describe('durable orchestration mutation ledger', () => {
controller.abort()
await first
restartedDb.close()
db.close()
})
})
@@ -1,5 +1,6 @@
import type { FeatureInteractionId } from '../../../shared/feature-interactions'
import { isBrowserPaneUiRuntimeRpcParams } from '../../../shared/runtime-rpc-feature-interaction-source'
import type { OrcaRuntimeService } from '../orca-runtime'
export function getRuntimeFeatureInteractionId(
method: string,
@@ -37,6 +38,25 @@ export function getRuntimeFeatureInteractionId(
return method.startsWith('orchestration.') ? 'agent-orchestration' : null
}
export function recordRuntimeFeatureInteraction(
runtime: OrcaRuntimeService,
method: string,
result: unknown,
alreadyRecorded?: Set<FeatureInteractionId>,
rawParams?: unknown
): void {
const id = getRuntimeFeatureInteractionId(method, result, rawParams)
if (!id || alreadyRecorded?.has(id)) {
return
}
try {
runtime.recordFeatureInteraction(id)
alreadyRecorded?.add(id)
} catch {
// Best-effort education state must not break runtime tools.
}
}
function hasBooleanResult(value: unknown, key: string): boolean {
return (
value !== null && typeof value === 'object' && (value as Record<string, unknown>)[key] === true
@@ -1,20 +1,47 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { randomUUID } from 'node:crypto'
import type * as NodeCrypto from 'node:crypto'
import { SshRelaySession } from './ssh-relay-session'
import { createMockDeps, mockDeploySuccess } from './ssh-relay-session-test-fixtures'
const { muxRequestMock } = vi.hoisted(() => ({ muxRequestMock: vi.fn() }))
type MockMuxInstance = {
requestHandlers: Map<string, (params: Record<string, unknown>) => Promise<unknown>>
}
const { muxRequestMock, muxInstancesRaw } = vi.hoisted(() => ({
muxRequestMock: vi.fn(),
muxInstancesRaw: [] as unknown[]
}))
const muxInstances = muxInstancesRaw as MockMuxInstance[]
vi.mock('./ssh-relay-deploy', () => ({ deployAndLaunchRelay: vi.fn() }))
vi.mock('./ssh-relay-deploy-helpers', () => ({ execCommand: vi.fn().mockResolvedValue('') }))
vi.mock('node:crypto', async (importOriginal) => {
const actual = await importOriginal<typeof NodeCrypto>()
return { ...actual, randomUUID: vi.fn() }
})
vi.mock('./ssh-remote-orca-cli', () => ({
runRemoteOrcaCli: vi.fn().mockResolvedValue({ exitCode: 0, stdout: '', stderr: '' })
}))
vi.mock('./ssh-channel-multiplexer', () => ({
SshChannelMultiplexer: class MockSshChannelMultiplexer {
requestHandlers = new Map<string, (params: Record<string, unknown>) => Promise<unknown>>()
notify = vi.fn()
request = muxRequestMock
onNotification = vi.fn().mockReturnValue(() => {})
onRequest = vi.fn().mockReturnValue(() => {})
onRequest = vi.fn(
(method: string, handler: (params: Record<string, unknown>) => Promise<unknown>) => {
this.requestHandlers.set(method, handler)
return () => this.requestHandlers.delete(method)
}
)
onDispose = vi.fn().mockReturnValue(() => {})
dispose = vi.fn()
isDisposed = vi.fn().mockReturnValue(false)
constructor() {
muxInstancesRaw.push(this)
}
}
}))
vi.mock('../agent-hooks/remote-managed-hook-installers', () => ({
@@ -98,13 +125,74 @@ function emitExitDuringAttach(payload: { id: string; code: number; incarnationId
describe('SshRelaySession reconnect incarnation ordering', () => {
beforeEach(() => {
vi.clearAllMocks()
muxInstances.splice(0)
delete process.env.ORCA_FEATURE_REMOTE_AGENT_HOOKS
muxRequestMock.mockReset()
muxRequestMock.mockResolvedValue([])
vi.mocked(randomUUID).mockReset()
vi.mocked(randomUUID).mockReturnValue('00000000-0000-4000-8000-000000000001')
mockDeploySuccess()
vi.mocked(getPtyIdsForConnection).mockReturnValue([])
})
it('keeps the winning reconnect incarnation when a stale health check resolves last', async () => {
const initialIncarnation = '00000000-0000-4000-8000-000000000001'
const winningIncarnation = '00000000-0000-4000-8000-000000000002'
const staleIncarnation = '00000000-0000-4000-8000-000000000003'
let resolveStaleHealthCheck!: (value: unknown) => void
const staleHealthCheck = new Promise((resolve) => {
resolveStaleHealthCheck = resolve
})
let resolveHomeCalls = 0
muxRequestMock.mockImplementation((method: string) => {
if (method !== 'session.resolveHome') {
return Promise.resolve([])
}
resolveHomeCalls += 1
return resolveHomeCalls === 2 ? staleHealthCheck : Promise.resolve('/')
})
vi.mocked(randomUUID)
.mockReturnValueOnce(initialIncarnation)
.mockReturnValueOnce(winningIncarnation)
.mockReturnValue(staleIncarnation)
const { mockConn, mockStore, mockPortForward, getMainWindow } = createMockDeps()
const runtime = {
registerOrchestrationCompatibilitySshAttachment: vi.fn(
(_targetId: string, connectionIncarnation: string) => ({
attachmentId: `attachment-${connectionIncarnation}`,
connectionIncarnation
})
),
releaseOrchestrationCompatibilitySshAttachment: vi.fn()
}
const session = new SshRelaySession(
'target-1',
getMainWindow,
mockStore,
mockPortForward,
runtime as never
)
await session.establish(mockConn)
const staleReconnect = session.reconnect(mockConn)
await vi.waitFor(() => expect(resolveHomeCalls).toBe(2))
await session.reconnect(mockConn)
expect(session.getState()).toBe('ready')
resolveStaleHealthCheck('/')
await staleReconnect
const winningCliHandler = muxInstances[2]?.requestHandlers.get('orca.cli')
expect(winningCliHandler).toBeDefined()
await winningCliHandler?.({ argv: ['status'], cwd: '/', env: {} })
expect(runtime.registerOrchestrationCompatibilitySshAttachment).toHaveBeenCalledWith(
'target-1',
winningIncarnation
)
expect(randomUUID).toHaveBeenCalledTimes(2)
})
it('restores and persists exact incarnation proof from reconnect attach', async () => {
const { mockConn, mockStore, mockPortForward, getMainWindow } = createMockDeps()
const incarnationId = 'incarnation-reconnect'
+72 -11
View File
@@ -2,6 +2,7 @@
// Why: single authority for all relay lifecycle state per SSH target (previously scattered across module Maps/Sets with duplicated paths).
import type { BrowserWindow } from 'electron'
import { randomUUID } from 'node:crypto'
import { deployAndLaunchRelay } from './ssh-relay-deploy'
import { execCommand } from './ssh-relay-deploy-helpers'
import { isRelayVersionMismatchError } from './ssh-relay-version-mismatch-error'
@@ -65,6 +66,10 @@ import {
import type { Store } from '../persistence'
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
import { runRemoteOrcaCli } from './ssh-remote-orca-cli'
import {
acknowledgeRemoteOrcaCliPostOutput,
parseRemoteOrcaCliPostOutput
} from './ssh-remote-orchestration-post-output'
import { toSshExecutionHostId, type ExecutionHostId } from '../../shared/execution-host'
import { isTerminalLeafId, makePaneKey } from '../../shared/stable-pane-id'
import { isValidTerminalTabId } from '../../shared/terminal-tab-id'
@@ -151,6 +156,7 @@ export class SshRelaySession {
private remoteCliBridgeEnv: RemoteCliBridgeEnv | null = null
private forwardedReattachReplayByPty = new Map<string, ForwardedReplayFingerprint>()
private pendingPtyReattaches = new Map<string, PendingPtyReattach>()
private activeCompatibilityAttachmentIds = new Set<string>()
constructor(
readonly targetId: string,
@@ -278,8 +284,15 @@ export class SshRelaySession {
// Why: round-trip the relay before registering providers so a closed --connect bridge fails fast instead of leaving a 'ready' session on a dead mux.
await mux.request('session.resolveHome', { path: '~' })
if (!ownsAttempt()) {
if (!mux.isDisposed()) {
mux.dispose()
}
throw new Error('Session disposed during establish')
}
const connectionIncarnation = randomUUID()
const registered = await this.registerProviders(mux, ownsAttempt)
const registered = await this.registerProviders(mux, ownsAttempt, connectionIncarnation)
if (!registered) {
if (!mux.isDisposed()) {
mux.dispose()
@@ -387,8 +400,9 @@ export class SshRelaySession {
}
return
}
const connectionIncarnation = randomUUID()
const registered = await this.registerProviders(mux, ownsAttempt)
const registered = await this.registerProviders(mux, ownsAttempt, connectionIncarnation)
if (!registered) {
if (!mux.isDisposed()) {
mux.dispose()
@@ -499,7 +513,8 @@ export class SshRelaySession {
// Why: shared by establish() and reconnect() so both use the exact same registration sequence.
private async registerProviders(
mux: SshChannelMultiplexer,
shouldContinue?: () => boolean
shouldContinue: (() => boolean) | undefined,
connectionIncarnation: string
): Promise<boolean> {
await this.registerRelayRoots(mux)
if (shouldContinue && !shouldContinue()) {
@@ -530,7 +545,7 @@ export class SshRelaySession {
return false
}
this.wireUpRemoteOrcaCli(mux)
this.wireUpRemoteOrcaCli(mux, connectionIncarnation)
const ptyProvider = new SshPtyProvider(this.targetId, mux, this.remoteCliBridgeEnv ?? undefined)
registerSshPtyProvider(this.targetId, ptyProvider)
@@ -667,7 +682,7 @@ export class SshRelaySession {
}
}
private wireUpRemoteOrcaCli(mux: SshChannelMultiplexer): void {
private wireUpRemoteOrcaCli(mux: SshChannelMultiplexer, connectionIncarnation: string): void {
mux.onRequest('orca.cli', async (params) => {
if (!this.runtime) {
throw new Error('Orca runtime is unavailable')
@@ -687,12 +702,54 @@ export class SshRelaySession {
)
: {}
const stdin = typeof params.stdin === 'string' ? params.stdin : undefined
return await runRemoteOrcaCli(this.runtime, {
argv,
cwd,
env,
...(stdin !== undefined ? { stdin } : {})
})
const runtimeAuthority = this.runtime.registerOrchestrationCompatibilitySshAttachment(
this.targetId,
connectionIncarnation
)
this.activeCompatibilityAttachmentIds.add(runtimeAuthority.attachmentId)
try {
return await runRemoteOrcaCli(this.runtime, {
argv,
cwd,
env,
...(stdin !== undefined ? { stdin } : {}),
runtimeAuthority
})
} finally {
this.activeCompatibilityAttachmentIds.delete(runtimeAuthority.attachmentId)
this.runtime.releaseOrchestrationCompatibilitySshAttachment(runtimeAuthority.attachmentId)
}
})
mux.onRequest('orca.cli.postOutput', async (params) => {
if (!this.runtime) {
throw new Error('Orca runtime is unavailable')
}
const rawEnv = params.env
const env =
rawEnv && typeof rawEnv === 'object' && !Array.isArray(rawEnv)
? Object.fromEntries(
Object.entries(rawEnv).filter(
(entry): entry is [string, string] =>
typeof entry[0] === 'string' && typeof entry[1] === 'string'
)
)
: {}
const runtimeAuthority = this.runtime.registerOrchestrationCompatibilitySshAttachment(
this.targetId,
connectionIncarnation
)
this.activeCompatibilityAttachmentIds.add(runtimeAuthority.attachmentId)
try {
await acknowledgeRemoteOrcaCliPostOutput(this.runtime, {
postOutput: parseRemoteOrcaCliPostOutput(params.postOutput),
env,
runtimeAuthority
})
return { acknowledged: true }
} finally {
this.activeCompatibilityAttachmentIds.delete(runtimeAuthority.attachmentId)
this.runtime.releaseOrchestrationCompatibilitySshAttachment(runtimeAuthority.attachmentId)
}
})
}
@@ -826,6 +883,10 @@ export class SshRelaySession {
this.mux.dispose(reason)
}
this.mux = null
for (const attachmentId of this.activeCompatibilityAttachmentIds) {
this.runtime?.releaseOrchestrationCompatibilitySshAttachment(attachmentId)
}
this.activeCompatibilityAttachmentIds.clear()
if (reason === 'shutdown') {
clearPtyOwnershipForConnection(this.targetId)
@@ -22,6 +22,12 @@ import {
import { resolveOrchestrationAskClientTimeoutMs } from '../../shared/orchestration-ask-timeout'
import { remoteCliRequestTimeoutMs } from '../../relay/remote-cli-timeout'
import { MAX_TIMER_DELAY_MS } from '../../shared/timer-delay'
import {
ORCHESTRATION_COMPATIBILITY_ATTACHMENT_ENV,
ORCHESTRATION_COMPATIBILITY_HOST_ID_ENV,
ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION_ENV,
ORCHESTRATION_COMPATIBILITY_HOST_KIND_ENV
} from '../../shared/orchestration-compatibility-evidence'
type FakeChild = EventEmitter & {
stdout: EventEmitter
@@ -65,7 +71,12 @@ describe('buildHostCliEnv', () => {
ORCA_TERMINAL_HANDLE: 'term_remote',
ORCA_WORKTREE_ID: 'repo::/home/alice/wt',
ORCA_PANE_KEY: 'pane-9',
ORCA_AGENT_LAUNCH_TOKEN: 'launch-secret',
ORCA_WORKSPACE_ID: 'ws-1',
[ORCHESTRATION_COMPATIBILITY_HOST_KIND_ENV]: 'wsl',
[ORCHESTRATION_COMPATIBILITY_HOST_ID_ENV]: 'caller-host',
[ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION_ENV]: 'caller-incarnation',
[ORCHESTRATION_COMPATIBILITY_ATTACHMENT_ENV]: 'caller-attachment',
// Why: these are remote-machine paths and must not leak into the host
// subprocess (PATH would break host binary lookup; user-data would
// retarget the CLI at a different local instance).
@@ -73,13 +84,24 @@ describe('buildHostCliEnv', () => {
ORCA_USER_DATA_PATH: '/remote/user-data'
},
userDataPath: '/host/user-data',
remoteCwd: '/home/alice/wt/sub'
remoteCwd: '/home/alice/wt/sub',
runtimeAuthority: {
kind: 'ssh',
targetId: 'saved-target',
connectionIncarnation: 'connection-incarnation',
attachmentId: 'runtime-attachment'
}
})
expect(env.ORCA_TERMINAL_HANDLE).toBe('term_remote')
expect(env.ORCA_WORKTREE_ID).toBe('repo::/home/alice/wt')
expect(env.ORCA_PANE_KEY).toBe('pane-9')
expect(env.ORCA_AGENT_LAUNCH_TOKEN).toBe('launch-secret')
expect(env.ORCA_WORKSPACE_ID).toBe('ws-1')
expect(env[ORCHESTRATION_COMPATIBILITY_HOST_KIND_ENV]).toBe('ssh')
expect(env[ORCHESTRATION_COMPATIBILITY_HOST_ID_ENV]).toBe('saved-target')
expect(env[ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION_ENV]).toBe('connection-incarnation')
expect(env[ORCHESTRATION_COMPATIBILITY_ATTACHMENT_ENV]).toBe('runtime-attachment')
expect(env.PATH).toBe('/host/bin')
expect(env.ORCA_USER_DATA_PATH).toBe('/host/user-data')
expect(env.ORCA_CLI_CWD).toBe('/home/alice/wt/sub')
@@ -17,20 +17,49 @@ import {
parsePositiveSafeIntegerNumericText,
parsePositiveSafeIntegerText
} from '../../shared/timer-delay'
import {
ORCHESTRATION_COMPATIBILITY_ATTACHMENT_ENV,
ORCHESTRATION_COMPATIBILITY_HOST_ID_ENV,
ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION_ENV,
ORCHESTRATION_COMPATIBILITY_HOST_KIND_ENV
} from '../../shared/orchestration-compatibility-evidence'
export type SshCliRuntimeAuthority = {
kind: 'ssh'
targetId: string
connectionIncarnation: string
attachmentId: string
}
export type RemoteOrcaCliRequest = {
argv: string[]
cwd: string
env: Record<string, string>
stdin?: string
runtimeAuthority?: SshCliRuntimeAuthority
}
export type RemoteOrcaCliResult = {
stdout: string
stderr: string
exitCode: number
postOutput?: RemoteOrcaCliPostOutput
}
export type RemoteOrcaCliPostOutput =
| {
kind: 'legacy_check_ack'
terminal: string
messageIds: string[]
types?: string[]
}
| {
kind: 'legacy_question_ack'
terminal: string
questionId: string
answerMessageId: string
}
export type HostCliPassthroughOptions = {
execPath?: string
cliEntryPath?: string
@@ -54,6 +83,7 @@ const REMOTE_CONTEXT_ENV_VARS = [
'ORCA_TERMINAL_HANDLE',
'ORCA_WORKTREE_ID',
'ORCA_PANE_KEY',
'ORCA_AGENT_LAUNCH_TOKEN',
'ORCA_WORKSPACE_ID'
] as const
@@ -106,6 +136,7 @@ export function buildHostCliEnv(args: {
remoteEnv: Record<string, string>
userDataPath: string
remoteCwd: string
runtimeAuthority?: SshCliRuntimeAuthority
}): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = { ...args.hostEnv }
for (const key of REMOTE_CONTEXT_ENV_VARS) {
@@ -127,6 +158,17 @@ export function buildHostCliEnv(args: {
env.ORCA_NODE_REPL_EXTERNAL_MODULE = args.hostEnv.NODE_REPL_EXTERNAL_MODULE ?? ''
delete env.NODE_OPTIONS
delete env.NODE_REPL_EXTERNAL_MODULE
delete env[ORCHESTRATION_COMPATIBILITY_HOST_KIND_ENV]
delete env[ORCHESTRATION_COMPATIBILITY_HOST_ID_ENV]
delete env[ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION_ENV]
delete env[ORCHESTRATION_COMPATIBILITY_ATTACHMENT_ENV]
if (args.runtimeAuthority) {
env[ORCHESTRATION_COMPATIBILITY_HOST_KIND_ENV] = 'ssh'
env[ORCHESTRATION_COMPATIBILITY_HOST_ID_ENV] = args.runtimeAuthority.targetId
env[ORCHESTRATION_COMPATIBILITY_HOST_INCARNATION_ENV] =
args.runtimeAuthority.connectionIncarnation
env[ORCHESTRATION_COMPATIBILITY_ATTACHMENT_ENV] = args.runtimeAuthority.attachmentId
}
env.ELECTRON_RUN_AS_NODE = '1'
return env
}
@@ -177,7 +219,8 @@ export async function runHostOrcaCliPassthrough(
hostEnv,
remoteEnv: request.env,
userDataPath,
remoteCwd: request.cwd
remoteCwd: request.cwd,
runtimeAuthority: request.runtimeAuthority
})
return await new Promise<RemoteOrcaCliResult>((resolve, reject) => {
@@ -0,0 +1,40 @@
import type { RpcResponse } from '../runtime/rpc/core'
import type { ParsedRemoteCli } from './ssh-remote-cli-argument-error'
import type { RemoteOrcaCliResult } from './ssh-remote-cli-host-passthrough'
import { formatRemoteCli } from './ssh-remote-cli-format'
import {
formatRemoteOrchestrationAsk,
getRemoteCliExitCode
} from './ssh-remote-orchestration-ask-output'
import { formatRemoteOrchestrationCheck } from './ssh-remote-orchestration-check-output'
import { getRemoteCliPostOutput } from './ssh-remote-orchestration-post-output'
import { resolveRemoteCliHandle } from './ssh-remote-cli-args'
export function formatInProcessRemoteCliResult(
parsed: ParsedRemoteCli,
env: Record<string, string>,
response: RpcResponse,
json: boolean
): RemoteOrcaCliResult {
const command = parsed.commandPath.join(' ')
const formatted =
command === 'orchestration check'
? formatRemoteOrchestrationCheck(
response,
json,
resolveRemoteCliHandle(parsed.flags, env, 'terminal'),
parsed.flags.has('format')
)
: command === 'orchestration ask'
? formatRemoteOrchestrationAsk(response, json)
: json
? { stdout: `${JSON.stringify(response, null, 2)}\n`, stderr: '' }
: formatRemoteCli(response)
const postOutput = getRemoteCliPostOutput(parsed, env, response)
return {
stdout: formatted.stdout,
stderr: formatted.stderr,
exitCode: getRemoteCliExitCode(command, response),
...(postOutput ? { postOutput } : {})
}
}
+1 -1
View File
@@ -24,7 +24,6 @@ const LEGACY_FALLBACK_OPTIONS: HostCliPassthroughOptions = {
userDataPath: '/host/user-data',
entryExists: () => false
}
type FakeChild = EventEmitter & {
stdout: EventEmitter
stderr: EventEmitter
@@ -80,6 +79,7 @@ describe('runRemoteOrcaCli', () => {
}
}
}),
getLegacyAdoption: vi.fn(() => undefined),
getActiveDispatchForIdentity: vi.fn(() => undefined),
getCurrentRunForPane: vi.fn(() => undefined),
findActiveRemoteAttachmentForPane: vi.fn(() => undefined)
+99 -36
View File
@@ -1,10 +1,11 @@
import type { CliStatusResult, RuntimeStatus } from '../../shared/runtime-types'
import { randomUUID } from 'node:crypto'
import type { RuntimeOrchestrationEnvelope } from '../../shared/runtime-rpc-envelope'
import { readOrchestrationCompatibilityEvidence } from '../../shared/orchestration-compatibility-evidence'
import { ORCHESTRATION_CONTRACT_VERSION } from '../../shared/protocol-version'
import { RpcDispatcher } from '../runtime/rpc/dispatcher'
import type { RpcResponse } from '../runtime/rpc/core'
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
import { formatRemoteCli } from './ssh-remote-cli-format'
import {
HostCliUnavailableError,
runHostOrcaCliPassthrough,
@@ -24,9 +25,9 @@ import { buildRemoteCliError } from './ssh-remote-cli-error-response'
import { getRemoteLinearHelp, tryDispatchRemoteLinearCli } from './ssh-remote-linear-cli'
import {
getRemoteOrchestrationPayload,
hasRemoteLifecycleRejection,
resolveRemoteOrchestrationSender
} from './ssh-remote-orchestration-send'
import { formatInProcessRemoteCliResult } from './ssh-remote-cli-in-process-result'
export type { RemoteOrcaCliRequest, RemoteOrcaCliResult } from './ssh-remote-cli-host-passthrough'
@@ -49,6 +50,7 @@ export async function runRemoteOrcaCli(
): Promise<RemoteOrcaCliResult> {
const parsed = parseRemoteCliArgs(request.argv)
const json = parsed.flags.has('json')
const command = parsed.commandPath.join(' ')
const interactiveMessage = HOST_INTERACTIVE_COMMANDS[parsed.commandPath[0] ?? '']
if (interactiveMessage) {
@@ -62,6 +64,17 @@ export async function runRemoteOrcaCli(
return { stdout: '', stderr: `${interactiveMessage}\n`, exitCode: 1 }
}
if (command === 'orchestration check' || command === 'orchestration ask') {
// Why: compatibility ACKs must wait until relay stdout is observable; a host CLI child can only flush into main's capture pipe.
return await runLegacyRemoteOrcaCli(
runtime,
request,
parsed,
json,
new HostCliUnavailableError('output-ordered orchestration bridge required')
)
}
let passthroughFailure: HostCliUnavailableError | null = null
try {
return await runHostOrcaCliPassthrough(request, passthroughOptions)
@@ -96,18 +109,10 @@ async function runLegacyRemoteOrcaCli(
parsed,
request.env,
request.stdin,
passthroughFailure.message
passthroughFailure.message,
request.runtimeAuthority
)
const formatted = json
? { stdout: `${JSON.stringify(response, null, 2)}\n`, stderr: '' }
: formatRemoteCli(response)
return {
stdout: formatted.stdout,
stderr: formatted.stderr,
// Why: the legacy SSH bridge bypasses the local CLI handler that turns
// a persisted lifecycle rejection into an unsuccessful command.
exitCode: response.ok && !hasRemoteLifecycleRejection(response.result) ? 0 : 1
}
return formatInProcessRemoteCliResult(parsed, request.env, response, json)
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
const code =
@@ -134,9 +139,23 @@ async function dispatchRemoteCli(
parsed: ParsedRemoteCli,
env: Record<string, string>,
stdin: string | undefined,
passthroughFailureReason: string
passthroughFailureReason: string,
runtimeAuthority: RemoteOrcaCliRequest['runtimeAuthority']
): Promise<RpcResponse> {
const command = parsed.commandPath.join(' ')
const inheritedEvidence = readOrchestrationCompatibilityEvidence(env)
const orchestrationCompatibilityEvidence = runtimeAuthority
? { ...inheritedEvidence, host: runtimeAuthority }
: inheritedEvidence
const compatibilityEnvelope: RuntimeOrchestrationEnvelope = {
compatibilityInvocationId: randomUUID(),
orchestrationRequestId:
optionalRemoteCliString(parsed.flags, 'retry-request') ??
(command === 'orchestration check' || command === 'orchestration ask'
? randomUUID()
: undefined),
orchestrationCompatibilityEvidence
}
const linearResponse = await tryDispatchRemoteLinearCli(dispatcher, parsed, env, stdin)
if (linearResponse) {
return linearResponse
@@ -187,33 +206,74 @@ async function dispatchRemoteCli(
senderPaneKey: env.ORCA_PANE_KEY || undefined
},
{
orchestrationCapability: optionalRemoteCliString(parsed.flags, 'dispatch-capability'),
orchestrationRequestId: optionalRemoteCliString(parsed.flags, 'retry-request')
...compatibilityEnvelope,
orchestrationCapability: optionalRemoteCliString(parsed.flags, 'dispatch-capability')
}
)
}
case 'orchestration check':
return await call(dispatcher, 'orchestration.check', {
terminal: resolveRemoteCliHandle(parsed.flags, env, 'terminal'),
terminalPaneKey: parsed.flags.has('terminal') ? undefined : env.ORCA_PANE_KEY || undefined,
unread: parsed.flags.has('unread') ? true : undefined,
all: parsed.flags.has('all') ? true : undefined,
types: optionalRemoteCliString(parsed.flags, 'types'),
inject: parsed.flags.has('inject') ? true : undefined,
wait: parsed.flags.has('wait') ? true : undefined,
timeoutMs: optionalRemoteCliNumber(parsed.flags, 'timeout-ms')
})
return await call(
dispatcher,
'orchestration.check',
{
terminal: resolveRemoteCliHandle(parsed.flags, env, 'terminal'),
terminalPaneKey: parsed.flags.has('terminal')
? undefined
: env.ORCA_PANE_KEY || undefined,
unread: parsed.flags.has('unread') ? true : parsed.flags.has('peek') ? false : undefined,
peek: parsed.flags.has('peek') ? true : undefined,
all: parsed.flags.has('all') ? true : undefined,
types: optionalRemoteCliString(parsed.flags, 'types'),
format: parsed.flags.has('format') ? true : undefined,
inject: parsed.flags.has('inject') ? true : undefined,
compatibilityCliCommand: 'orca',
run: optionalRemoteCliString(parsed.flags, 'run'),
ack: optionalRemoteCliString(parsed.flags, 'ack'),
wait: parsed.flags.has('wait') ? true : undefined,
timeoutMs: optionalRemoteCliNumber(parsed.flags, 'timeout-ms')
},
compatibilityEnvelope
)
case 'orchestration ask':
return await call(
dispatcher,
'orchestration.ask',
{
to: optionalRemoteCliString(parsed.flags, 'to'),
question: optionalRemoteCliString(parsed.flags, 'question'),
resume: optionalRemoteCliString(parsed.flags, 'resume'),
options: optionalRemoteCliString(parsed.flags, 'options'),
timeoutMs: optionalRemoteCliNumber(parsed.flags, 'timeout-ms'),
from: resolveRemoteCliHandle(parsed.flags, env, 'from'),
run: optionalRemoteCliString(parsed.flags, 'run'),
compatibilityCliCommand: 'orca'
},
{
...compatibilityEnvelope,
orchestrationCapability: optionalRemoteCliString(parsed.flags, 'dispatch-capability')
}
)
case 'orchestration reply':
return await call(dispatcher, 'orchestration.reply', {
id: requiredRemoteCliString(parsed.flags, 'id'),
body: requiredRemoteCliString(parsed.flags, 'body'),
from: resolveRemoteCliHandle(parsed.flags, env, 'from')
})
return await call(
dispatcher,
'orchestration.reply',
{
id: requiredRemoteCliString(parsed.flags, 'id'),
body: requiredRemoteCliString(parsed.flags, 'body'),
from: resolveRemoteCliHandle(parsed.flags, env, 'from')
},
compatibilityEnvelope
)
case 'orchestration inbox':
return await call(dispatcher, 'orchestration.inbox', {
limit: optionalRemoteCliNumber(parsed.flags, 'limit'),
terminal: optionalRemoteCliString(parsed.flags, 'terminal')
})
return await call(
dispatcher,
'orchestration.inbox',
{
limit: optionalRemoteCliNumber(parsed.flags, 'limit'),
terminal: optionalRemoteCliString(parsed.flags, 'terminal')
},
compatibilityEnvelope
)
default:
// Why: only reachable when the full host CLI could not be launched;
// include that root cause so users can fix the install instead of
@@ -239,6 +299,9 @@ async function call(
orchestrationContractVersion: method.startsWith('orchestration.')
? ORCHESTRATION_CONTRACT_VERSION
: undefined,
orchestrationRequestId: envelope?.orchestrationRequestId
orchestrationRequestId: envelope?.orchestrationRequestId,
compatibilityInvocationId:
envelope?.orchestrationRequestId ?? envelope?.compatibilityInvocationId,
orchestrationCompatibilityEvidence: envelope?.orchestrationCompatibilityEvidence
})
}
@@ -0,0 +1,66 @@
import type { RpcResponse } from '../runtime/rpc/core'
import { formatRemoteCli } from './ssh-remote-cli-format'
import { hasRemoteLifecycleRejection } from './ssh-remote-orchestration-send'
export function formatRemoteOrchestrationAsk(
response: RpcResponse,
json: boolean
): { stdout: string; stderr: string } {
if (!response.ok || !isRecord(response.result)) {
return json
? { stdout: `${JSON.stringify(response, null, 2)}\n`, stderr: '' }
: formatRemoteCli(response)
}
if (json) {
return { stdout: `${JSON.stringify(response.result)}\n`, stderr: '' }
}
if (isRecord(response.result.legacyCompatibility)) {
const compatibility = response.result.legacyCompatibility
if (compatibility.resumeRequired === true && typeof compatibility.resumeCommand === 'string') {
return {
stdout:
`Question ${String(response.result.messageId ?? 'unknown')} committed.\n` +
`Resume with: ${compatibility.resumeCommand}\n`,
stderr: ''
}
}
}
const answer = typeof response.result.answer === 'string' ? response.result.answer : ''
const thread = typeof response.result.threadId === 'string' ? response.result.threadId : 'unknown'
const timeoutMs =
typeof response.result.timeoutMs === 'number' ? response.result.timeoutMs : undefined
const stderr = response.result.timedOut
? `ask timeout after ${timeoutMs ?? 0}ms (thread ${thread})\n`
: response.result.cancelled
? response.result.connectionLost
? `ask connection closed (question ${String(response.result.messageId ?? 'unknown')})\n`
: `ask cancelled (question ${String(response.result.messageId ?? 'unknown')})\n`
: ''
return { stdout: answer ? `${answer}\n` : '', stderr }
}
export function getRemoteCliExitCode(command: string, response: RpcResponse): number {
if (!response.ok || hasRemoteLifecycleRejection(response.result)) {
return 1
}
if (
command === 'orchestration ask' &&
isRecord(response.result) &&
isRecord(response.result.legacyCompatibility) &&
response.result.legacyCompatibility.resumeRequired === true
) {
return 75
}
if (
command === 'orchestration ask' &&
isRecord(response.result) &&
(response.result.timedOut === true || response.result.cancelled === true)
) {
return 1
}
return 0
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}
@@ -0,0 +1,48 @@
import {
formatOrchestrationCheckText,
prepareOrchestrationCheckOutput,
type OrchestrationCheckOutput,
type OrchestrationMessageSummary
} from '../../shared/orchestration-check-output'
import type { RpcResponse } from '../runtime/rpc/core'
import { formatRemoteCli } from './ssh-remote-cli-format'
export function formatRemoteOrchestrationCheck(
response: RpcResponse,
json: boolean,
terminal: string,
formattedRequested: boolean
): { stdout: string; stderr: string } {
if (!response.ok || !isRecord(response.result)) {
return json
? { stdout: `${JSON.stringify(response, null, 2)}\n`, stderr: '' }
: formatRemoteCli(response)
}
const messages = Array.isArray(response.result.messages)
? response.result.messages.filter(isMessageSummary)
: []
const count = typeof response.result.count === 'number' ? response.result.count : messages.length
const result = prepareOrchestrationCheckOutput(
{ ...(response.result as OrchestrationCheckOutput), messages, count },
terminal,
formattedRequested
)
if (json) {
return {
stdout: `${JSON.stringify({ ...response, result }, null, 2)}\n`,
stderr: ''
}
}
return {
stdout: `${formatOrchestrationCheckText(result, terminal)}\n`,
stderr: ''
}
}
function isMessageSummary(value: unknown): value is OrchestrationMessageSummary {
return isRecord(value) && typeof value.id === 'string' && typeof value.from_handle === 'string'
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}
@@ -0,0 +1,401 @@
import { createHash } from 'node:crypto'
import { describe, expect, it, vi } from 'vitest'
vi.mock('electron', () => ({
app: {
isPackaged: false,
getAppPath: () => '/host/app'
}
}))
vi.mock('../persistence', () => ({
getCanonicalUserDataPath: () => '/host/user-data'
}))
import { OrchestrationDb } from '../runtime/orchestration/db'
import { OrcaRuntimeService } from '../runtime/orca-runtime'
import type Database from '../sqlite/sync-database'
import type { HostCliPassthroughOptions } from './ssh-remote-cli-host-passthrough'
import { runRemoteOrcaCli } from './ssh-remote-orca-cli'
import { acknowledgeRemoteOrcaCliPostOutput } from './ssh-remote-orchestration-post-output'
const LEGACY_FALLBACK_OPTIONS: HostCliPassthroughOptions = {
execPath: '/host/electron',
cliEntryPath: '/host/app/out/cli/index.js',
userDataPath: '/host/user-data',
entryExists: () => false
}
const WORKER_HANDLE = 'term_legacy_ssh_worker'
const WORKER_PANE = 'tab_legacy_ssh:33333333-3333-4333-8333-333333333333'
const COORDINATOR_HANDLE = 'term_legacy_ssh_coord'
const COORDINATOR_PANE = 'tab_legacy_coord:44444444-4444-4444-8444-444444444444'
const WORKER_ENV = {
ORCA_TERMINAL_HANDLE: WORKER_HANDLE,
ORCA_PANE_KEY: WORKER_PANE,
ORCA_AGENT_LAUNCH_TOKEN: 'legacy-ssh-token'
}
const COORDINATOR_ENV = {
ORCA_TERMINAL_HANDLE: COORDINATOR_HANDLE,
ORCA_PANE_KEY: COORDINATOR_PANE,
ORCA_AGENT_LAUNCH_TOKEN: 'legacy-ssh-coordinator-token'
}
const RUNTIME_AUTHORITY = {
kind: 'ssh' as const,
targetId: 'saved-target',
connectionIncarnation: 'connection-1',
attachmentId: 'attachment-1'
}
function createLegacyRuntime() {
const db = new OrchestrationDb(':memory:')
const run = db.createRun({
objective: 'Adopted legacy SSH work',
coordinatorHandle: COORDINATOR_HANDLE,
coordinatorPaneKey: COORDINATOR_PANE
})
const task = db.createTask({
spec: 'legacy SSH assignment',
runId: run.id,
createdByTerminalHandle: COORDINATOR_HANDLE
})
const dispatch = db.createDispatchContext(task.id, WORKER_HANDLE, WORKER_PANE)
const sqlite = (db as unknown as { db: Database.Database }).db
sqlite
.prepare(
`UPDATE dispatch_contexts
SET contract_version = 0, process_incarnation = 'process-1'
WHERE id = ?`
)
.run(dispatch.id)
sqlite
.prepare(
`INSERT INTO legacy_adoptions (source_run_id, adopted_run_id, scheduler_state_lost)
VALUES ('run_legacy_local', ?, 1)`
)
.run(run.id)
sqlite
.prepare(
`UPDATE runs
SET coordinator_handle = NULL, coordinator_pane_key = NULL, consumer_generation = 0
WHERE id = ?`
)
.run(run.id)
const runtime = new OrcaRuntimeService()
runtime.setOrchestrationDb(db)
vi.spyOn(runtime, 'getTerminalPaneKey').mockImplementation((handle) =>
handle === WORKER_HANDLE ? WORKER_PANE : handle === COORDINATOR_HANDLE ? COORDINATOR_PANE : null
)
vi.spyOn(runtime, 'verifyOrchestrationCompatibilityCaller').mockImplementation((evidence) => {
const worker =
evidence?.terminalHandle === WORKER_HANDLE &&
evidence.paneKey === WORKER_PANE &&
evidence.launchToken === WORKER_ENV.ORCA_AGENT_LAUNCH_TOKEN
const coordinator =
evidence?.terminalHandle === COORDINATOR_HANDLE &&
evidence.paneKey === COORDINATOR_PANE &&
evidence.launchToken === COORDINATOR_ENV.ORCA_AGENT_LAUNCH_TOKEN
if (
(!worker && !coordinator) ||
evidence.host?.kind !== 'ssh' ||
evidence.host.targetId !== RUNTIME_AUTHORITY.targetId ||
evidence.host.connectionIncarnation !== RUNTIME_AUTHORITY.connectionIncarnation
) {
return null
}
const identity = worker
? {
terminalHandle: WORKER_HANDLE,
paneKey: WORKER_PANE,
launchToken: WORKER_ENV.ORCA_AGENT_LAUNCH_TOKEN
}
: {
terminalHandle: COORDINATOR_HANDLE,
paneKey: COORDINATOR_PANE,
launchToken: COORDINATOR_ENV.ORCA_AGENT_LAUNCH_TOKEN
}
return {
hostScope: { kind: 'ssh', targetId: RUNTIME_AUTHORITY.targetId },
terminalHandle: identity.terminalHandle,
paneKey: identity.paneKey,
processIncarnation: 'process-1',
launchTokenHash: createHash('sha256').update(identity.launchToken).digest('hex')
}
})
vi.spyOn(runtime, 'notifyMessageArrived').mockImplementation(() => {})
return { db, runtime, run: db.getRun(run.id)!, dispatch }
}
describe('legacy SSH orchestration fallback', () => {
it('acknowledges a consuming check only after remote output', async () => {
const { db, runtime, run } = createLegacyRuntime()
const message = db.insertMessage({
runId: run.id,
deliveryContract: 'legacy_direct',
from: COORDINATOR_HANDLE,
to: WORKER_HANDLE,
subject: 'retained SSH mail'
})
const request = {
argv: ['orchestration', 'check', '--unread', '--inject', '--json'],
cwd: '/home/alice/repo',
env: WORKER_ENV,
runtimeAuthority: RUNTIME_AUTHORITY
}
try {
const first = await runRemoteOrcaCli(runtime, request, LEGACY_FALLBACK_OPTIONS)
const replay = await runRemoteOrcaCli(runtime, request, LEGACY_FALLBACK_OPTIONS)
expect(JSON.parse(first.stdout)).toMatchObject({
result: {
messages: [{ id: message.id }],
formatted: expect.stringContaining('retained SSH mail')
}
})
expect(first.postOutput).toEqual({
kind: 'legacy_check_ack',
terminal: WORKER_HANDLE,
messageIds: [message.id]
})
expect(JSON.parse(replay.stdout)).toMatchObject({
result: { messages: [{ id: message.id }] }
})
expect(db.getMessageById(message.id)?.read).toBe(0)
await acknowledgeRemoteOrcaCliPostOutput(runtime, {
postOutput: first.postOutput!,
env: WORKER_ENV,
runtimeAuthority: RUNTIME_AUTHORITY
})
const afterAck = await runRemoteOrcaCli(runtime, request, LEGACY_FALLBACK_OPTIONS)
expect(JSON.parse(afterAck.stdout)).toMatchObject({ result: { messages: [], count: 0 } })
expect(afterAck.postOutput).toBeUndefined()
expect(db.getMessageById(message.id)?.read).toBe(1)
} finally {
db.close()
}
})
it('keeps peek formatted and non-consuming', async () => {
const { db, runtime, run } = createLegacyRuntime()
const message = db.insertMessage({
runId: run.id,
deliveryContract: 'legacy_direct',
from: COORDINATOR_HANDLE,
to: WORKER_HANDLE,
subject: 'inspect retained SSH mail'
})
try {
const peek = await runRemoteOrcaCli(
runtime,
{
argv: ['orchestration', 'check', '--peek', '--format', '--json'],
cwd: '/home/alice/repo',
env: WORKER_ENV,
runtimeAuthority: RUNTIME_AUTHORITY
},
LEGACY_FALLBACK_OPTIONS
)
const output = JSON.parse(peek.stdout) as {
result: { formatted: string }
}
expect(output).toMatchObject({
result: {
messages: [{ id: message.id }],
formatted: expect.stringContaining('inspect retained SSH mail'),
legacyCompatibility: { readOnly: true }
}
})
expect(output.result.formatted).toContain(`${message.id} [legacy, read-only]`)
expect(output.result.formatted).toContain(
'Inspection only: reply and acknowledgment are unavailable.'
)
expect(output.result.formatted).not.toContain('orchestration reply')
expect(peek.postOutput).toBeUndefined()
expect(db.getMessageById(message.id)?.read).toBe(0)
} finally {
db.close()
}
})
it('reads and acknowledges current Run delivery', async () => {
const { db, runtime, run } = createLegacyRuntime()
const message = db.insertMessage({
runId: run.id,
from: WORKER_HANDLE,
to: `run:${run.id}`,
subject: 'current Run mail'
})
const baseRequest = {
cwd: '/home/alice/repo',
env: COORDINATOR_ENV,
runtimeAuthority: RUNTIME_AUTHORITY
}
const spawn = vi.fn()
const hostCliAvailable = {
...LEGACY_FALLBACK_OPTIONS,
entryExists: () => true,
spawn: spawn as HostCliPassthroughOptions['spawn']
}
try {
const checked = await runRemoteOrcaCli(
runtime,
{
...baseRequest,
argv: ['orchestration', 'check', '--run', run.id]
},
hostCliAvailable
)
expect(checked.stdout).toContain('Delivery ')
expect(checked.stdout).toContain(`${message.id} [status] from=${WORKER_HANDLE}`)
expect(spawn).not.toHaveBeenCalled()
const checkedJson = await runRemoteOrcaCli(
runtime,
{
...baseRequest,
argv: ['orchestration', 'check', '--run', run.id, '--json']
},
hostCliAvailable
)
const result = JSON.parse(checkedJson.stdout) as {
result: { deliveryId: string; messages: { id: string }[] }
}
expect(result.result.messages).toEqual([expect.objectContaining({ id: message.id })])
const acknowledged = await runRemoteOrcaCli(
runtime,
{
...baseRequest,
argv: [
'orchestration',
'check',
'--run',
run.id,
'--ack',
result.result.deliveryId,
'--json'
]
},
hostCliAvailable
)
expect(JSON.parse(acknowledged.stdout)).toMatchObject({
result: { acknowledged: result.result.deliveryId }
})
expect(db.getMessageById(message.id)?.read).toBe(1)
} finally {
db.close()
}
})
it('replays an SSH legacy ask by retry request without creating another question', async () => {
const { db, runtime } = createLegacyRuntime()
const argv = [
'orchestration',
'ask',
'--to',
COORDINATOR_HANDLE,
'--question',
'Continue?',
'--timeout-ms',
'1',
'--retry-request',
'ssh-question-1',
'--json'
]
const request = {
argv,
cwd: '/home/alice/repo',
env: WORKER_ENV,
runtimeAuthority: RUNTIME_AUTHORITY
}
try {
const first = await runRemoteOrcaCli(runtime, request, LEGACY_FALLBACK_OPTIONS)
const replay = await runRemoteOrcaCli(runtime, request, LEGACY_FALLBACK_OPTIONS)
const sqlite = (db as unknown as { db: Database.Database }).db
const firstResult = JSON.parse(first.stdout) as { messageId: string; timedOut: boolean }
const replayResult = JSON.parse(replay.stdout) as { messageId: string; timedOut: boolean }
expect(first).toMatchObject({ exitCode: 1 })
expect(firstResult).toMatchObject({ messageId: expect.any(String), timedOut: true })
expect(replay).toMatchObject({ exitCode: 1 })
expect(replayResult).toMatchObject({ messageId: firstResult.messageId, timedOut: true })
expect(
(
sqlite.prepare('SELECT COUNT(*) AS count FROM question_threads').get() as {
count: number
}
).count
).toBe(1)
} finally {
db.close()
}
})
it('resumes and acknowledges a question answer', async () => {
const { db, runtime, run, dispatch } = createLegacyRuntime()
const pending = db.createQuestion({
runId: run.id,
dispatchId: dispatch.id,
askerHandle: WORKER_HANDLE,
question: 'Proceed?'
})
const answer = db.answerQuestion({
messageId: pending.question.message_id,
runId: run.id,
consumerGeneration: run.consumer_generation,
body: 'yes'
})
const sqlite = (db as unknown as { db: Database.Database }).db
sqlite
.prepare(
`UPDATE messages
SET delivery_contract = 'legacy_direct', read = 0
WHERE id = ?`
)
.run(answer.message.id)
try {
const result = await runRemoteOrcaCli(
runtime,
{
argv: ['orchestration', 'ask', '--resume', pending.question.message_id, '--json'],
cwd: '/home/alice/repo',
env: WORKER_ENV,
runtimeAuthority: RUNTIME_AUTHORITY
},
LEGACY_FALLBACK_OPTIONS
)
expect(result.exitCode).toBe(0)
expect(JSON.parse(result.stdout)).toMatchObject({
answer: 'yes',
messageId: pending.question.message_id,
timedOut: false
})
expect(result.postOutput).toEqual({
kind: 'legacy_question_ack',
terminal: WORKER_HANDLE,
questionId: pending.question.message_id,
answerMessageId: answer.message.id
})
expect(db.getMessageById(answer.message.id)?.read).toBe(0)
await acknowledgeRemoteOrcaCliPostOutput(runtime, {
postOutput: result.postOutput!,
env: WORKER_ENV,
runtimeAuthority: RUNTIME_AUTHORITY
})
expect(db.getMessageById(answer.message.id)?.read).toBe(1)
} finally {
db.close()
}
})
})
@@ -0,0 +1,143 @@
import { randomUUID } from 'node:crypto'
import { readOrchestrationCompatibilityEvidence } from '../../shared/orchestration-compatibility-evidence'
import { ORCHESTRATION_CONTRACT_VERSION } from '../../shared/protocol-version'
import type { OrcaRuntimeService } from '../runtime/orca-runtime'
import type { RpcResponse } from '../runtime/rpc/core'
import { RpcDispatcher } from '../runtime/rpc/dispatcher'
import type {
RemoteOrcaCliPostOutput,
RemoteOrcaCliRequest
} from './ssh-remote-cli-host-passthrough'
import { RemoteCliArgumentError, type ParsedRemoteCli } from './ssh-remote-cli-argument-error'
import { optionalRemoteCliString, resolveRemoteCliHandle } from './ssh-remote-cli-args'
export async function acknowledgeRemoteOrcaCliPostOutput(
runtime: OrcaRuntimeService,
args: {
postOutput: RemoteOrcaCliPostOutput
env: Record<string, string>
runtimeAuthority?: RemoteOrcaCliRequest['runtimeAuthority']
}
): Promise<void> {
const inheritedEvidence = readOrchestrationCompatibilityEvidence(args.env)
const orchestrationCompatibilityEvidence = args.runtimeAuthority
? { ...inheritedEvidence, host: args.runtimeAuthority }
: inheritedEvidence
const params =
args.postOutput.kind === 'legacy_check_ack'
? {
terminal: args.postOutput.terminal,
compatibilityAck: JSON.stringify({
messageIds: args.postOutput.messageIds,
types: args.postOutput.types
})
}
: {
terminal: args.postOutput.terminal,
compatibilityQuestionAck: JSON.stringify({
questionId: args.postOutput.questionId,
answerMessageId: args.postOutput.answerMessageId
})
}
const response = await new RpcDispatcher({ runtime }).dispatch({
id: `remote-cli-post-output-${randomUUID()}`,
authToken: 'remote-cli',
method: 'orchestration.check',
params,
orchestrationContractVersion: ORCHESTRATION_CONTRACT_VERSION,
compatibilityInvocationId: randomUUID(),
orchestrationCompatibilityEvidence
})
if (!response.ok) {
throw new Error(response.error.message)
}
}
export function parseRemoteOrcaCliPostOutput(value: unknown): RemoteOrcaCliPostOutput {
if (!isRecord(value) || typeof value.kind !== 'string' || typeof value.terminal !== 'string') {
throw invalidPostOutput()
}
if (
value.kind === 'legacy_check_ack' &&
Array.isArray(value.messageIds) &&
value.messageIds.every((id) => typeof id === 'string') &&
(value.types === undefined ||
(Array.isArray(value.types) && value.types.every((type) => typeof type === 'string')))
) {
return {
kind: value.kind,
terminal: value.terminal,
messageIds: value.messageIds,
...(value.types === undefined ? {} : { types: value.types })
}
}
if (
value.kind === 'legacy_question_ack' &&
typeof value.questionId === 'string' &&
typeof value.answerMessageId === 'string'
) {
return {
kind: value.kind,
terminal: value.terminal,
questionId: value.questionId,
answerMessageId: value.answerMessageId
}
}
throw invalidPostOutput()
}
export function getRemoteCliPostOutput(
parsed: ParsedRemoteCli,
env: Record<string, string>,
response: RpcResponse
): RemoteOrcaCliPostOutput | undefined {
if (!response.ok || !isRecord(response.result)) {
return undefined
}
const compatibility = response.result.legacyCompatibility
if (!isRecord(compatibility)) {
return undefined
}
const command = parsed.commandPath.join(' ')
if (
command === 'orchestration check' &&
Array.isArray(compatibility.ackMessageIds) &&
compatibility.ackMessageIds.length > 0 &&
compatibility.ackMessageIds.every((id) => typeof id === 'string')
) {
const types = optionalRemoteCliString(parsed.flags, 'types')
?.split(',')
.map((type) => type.trim())
.filter(Boolean)
return {
kind: 'legacy_check_ack',
terminal: resolveRemoteCliHandle(parsed.flags, env, 'terminal'),
messageIds: compatibility.ackMessageIds,
...(types ? { types } : {})
}
}
const acknowledgement = compatibility.answerAcknowledgement
if (
command === 'orchestration ask' &&
response.result.answer !== null &&
isRecord(acknowledgement) &&
typeof acknowledgement.questionId === 'string' &&
typeof acknowledgement.answerMessageId === 'string'
) {
return {
kind: 'legacy_question_ack',
terminal: resolveRemoteCliHandle(parsed.flags, env, 'from'),
questionId: acknowledgement.questionId,
answerMessageId: acknowledgement.answerMessageId
}
}
return undefined
}
function invalidPostOutput(): RemoteCliArgumentError {
return new RemoteCliArgumentError('invalid_argument', 'Invalid SSH CLI post-output action.')
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
}
@@ -73,6 +73,13 @@ describe('startup ordering', () => {
expect(desktopStartup).not.toContain('await managedWslCliReconciliationReady')
expect(barrier).toContain('managedWslCliStartupBarrierReady')
expect(barrier).not.toContain('managedWslCliReconciliationReady')
expect(barrier).toContain("ipcMain.handle('app:recoverLegacyWorkerTerminalsForRendererStartup'")
expect(barrier).toContain('recoverLegacyWorkerTerminalsForRendererStartup({')
expect(barrier).toContain('localPtyProviderStartupReady,')
expect(barrier).toContain('await runtime?.refreshRestoredOrchestrationAuthority()')
expect(barrier).toContain(
'return runtime?.reconcileLegacyWorkerTerminals({ materializeRenderer: true })'
)
})
it('exposes managed WSL reconciliation status to headless serve clients and diagnostics', () => {
@@ -0,0 +1,117 @@
import { describe, expect, it, vi } from 'vitest'
import { recoverLegacyWorkerTerminalsForRendererStartup } from './legacy-worker-renderer-recovery'
describe('legacy worker renderer recovery', () => {
it('hydrates after bounded barriers while provider startup remains pending', async () => {
let resolveFirstWindow!: () => void
let resolveWslBarrier!: () => void
let resolveProvider!: () => void
const firstWindowReady = new Promise<void>((resolve) => {
resolveFirstWindow = resolve
})
const wslBarrierReady = new Promise<void>((resolve) => {
resolveWslBarrier = resolve
})
const providerReady = new Promise<void>((resolve) => {
resolveProvider = resolve
})
const reconcile = vi.fn().mockResolvedValue(undefined)
const startup = recoverLegacyWorkerTerminalsForRendererStartup({
firstWindowStartupServicesReady: firstWindowReady,
managedWslCliStartupBarrierReady: wslBarrierReady,
localPtyProviderStartupReady: providerReady,
reconcile,
onDeferredRecoveryError: vi.fn()
})
resolveFirstWindow()
await Promise.resolve()
expect(reconcile).not.toHaveBeenCalled()
resolveWslBarrier()
await startup
expect(reconcile).toHaveBeenCalledTimes(1)
resolveProvider()
await vi.waitFor(() => expect(reconcile).toHaveBeenCalledTimes(2))
})
it('retries after initial recovery when the provider is already ready', async () => {
const reconcile = vi.fn().mockResolvedValue(undefined)
await recoverLegacyWorkerTerminalsForRendererStartup({
firstWindowStartupServicesReady: Promise.resolve(),
managedWslCliStartupBarrierReady: Promise.resolve(),
localPtyProviderStartupReady: Promise.resolve(),
reconcile,
onDeferredRecoveryError: vi.fn()
})
await vi.waitFor(() => expect(reconcile).toHaveBeenCalledTimes(2))
})
it('contains provider startup rejection after initial recovery', async () => {
const providerError = new Error('provider failed')
const reconcile = vi.fn().mockResolvedValue(undefined)
let reportError!: (error: unknown) => void
const reportedError = new Promise<unknown>((resolve) => {
reportError = resolve
})
await recoverLegacyWorkerTerminalsForRendererStartup({
firstWindowStartupServicesReady: Promise.resolve(),
managedWslCliStartupBarrierReady: Promise.resolve(),
localPtyProviderStartupReady: Promise.reject(providerError),
reconcile,
onDeferredRecoveryError: reportError
})
await expect(reportedError).resolves.toBe(providerError)
expect(reconcile).toHaveBeenCalledTimes(1)
})
it('contains deferred recovery rejection', async () => {
const recoveryError = new Error('recovery failed')
const reconcile = vi.fn().mockResolvedValueOnce(undefined).mockRejectedValueOnce(recoveryError)
let reportError!: (error: unknown) => void
const reportedError = new Promise<unknown>((resolve) => {
reportError = resolve
})
await recoverLegacyWorkerTerminalsForRendererStartup({
firstWindowStartupServicesReady: Promise.resolve(),
managedWslCliStartupBarrierReady: Promise.resolve(),
localPtyProviderStartupReady: Promise.resolve(),
reconcile,
onDeferredRecoveryError: reportError
})
await expect(reportedError).resolves.toBe(recoveryError)
expect(reconcile).toHaveBeenCalledTimes(2)
})
it('contains initial recovery rejection and still retries when the provider becomes ready', async () => {
const initialError = new Error('initial recovery failed')
let resolveProvider!: () => void
const providerReady = new Promise<void>((resolve) => {
resolveProvider = resolve
})
const reconcile = vi.fn().mockRejectedValueOnce(initialError).mockResolvedValueOnce(undefined)
const onDeferredRecoveryError = vi.fn()
await expect(
recoverLegacyWorkerTerminalsForRendererStartup({
firstWindowStartupServicesReady: Promise.resolve(),
managedWslCliStartupBarrierReady: Promise.resolve(),
localPtyProviderStartupReady: providerReady,
reconcile,
onDeferredRecoveryError
})
).resolves.toBeUndefined()
expect(onDeferredRecoveryError).toHaveBeenCalledWith(initialError)
expect(reconcile).toHaveBeenCalledTimes(1)
resolveProvider()
await vi.waitFor(() => expect(reconcile).toHaveBeenCalledTimes(2))
})
})
@@ -0,0 +1,33 @@
type LegacyWorkerRendererRecoveryOptions = {
firstWindowStartupServicesReady: Promise<void>
managedWslCliStartupBarrierReady: Promise<void>
localPtyProviderStartupReady: Promise<void>
reconcile: () => Promise<unknown> | undefined
onDeferredRecoveryError: (error: unknown) => void
}
export async function recoverLegacyWorkerTerminalsForRendererStartup(
options: LegacyWorkerRendererRecoveryOptions
): Promise<void> {
const providerStartupResult = options.localPtyProviderStartupReady.then(
() => ({ ok: true as const }),
(error: unknown) => ({ ok: false as const, error })
)
await Promise.all([
options.firstWindowStartupServicesReady,
options.managedWslCliStartupBarrierReady
])
void providerStartupResult
.then(async (result) => {
if (!result.ok) {
throw result.error
}
await options.reconcile()
})
.catch(options.onDeferredRecoveryError)
try {
await options.reconcile()
} catch (error) {
options.onDeferredRecoveryError(error)
}
}
@@ -829,4 +829,73 @@ describe('attachMainWindowServices', () => {
await expect(revealPromise).resolves.toEqual({ tabId: 'tab-1', title: 'SSH tmux' })
expect(removeListenerMock).toHaveBeenCalledWith('terminal:tabCreateReply', handler)
})
it('requires an exact renderer identity receipt for recovered worker reveals', async () => {
const sendMock = vi.fn()
const mainWindow = createMainWindow({ send: sendMock })
const runtime = createRuntime()
attachMainWindowServices(mainWindow as never, createStore(), runtime as never)
const notifier = runtime.setNotifier.mock.calls[0][0] as {
revealTerminalSession: (
worktreeId: string,
opts: {
ptyId: string
tabId: string
leafId: string
expectedProcessIdentity: { terminalHandle: string; incarnationId: string }
}
) => Promise<unknown>
}
const opts = {
ptyId: 'pty-worker',
tabId: 'tab-worker',
leafId: 'leaf-worker',
expectedProcessIdentity: {
terminalHandle: 'term_worker',
incarnationId: 'inc-worker'
}
}
const mismatch = notifier.revealTerminalSession('worktree-1', opts)
const mismatchPayload = sendMock.mock.calls.at(-1)?.[1]
const mismatchHandler = onMock.mock.calls.findLast(
([channel]) => channel === 'terminal:tabCreateReply'
)?.[1]
mismatchHandler?.(
{ sender: mainWindow.webContents },
{
requestId: mismatchPayload.requestId,
tabId: 'tab-worker',
identity: {
worktreeId: 'worktree-1',
tabId: 'tab-worker',
leafId: 'leaf-worker',
ptyId: 'pty-replacement'
}
}
)
await expect(mismatch).rejects.toThrow('terminal_reveal_identity_mismatch')
const exact = notifier.revealTerminalSession('worktree-1', opts)
const exactPayload = sendMock.mock.calls.at(-1)?.[1]
const exactHandler = onMock.mock.calls.findLast(
([channel]) => channel === 'terminal:tabCreateReply'
)?.[1]
const identity = {
worktreeId: 'worktree-1',
tabId: 'tab-worker',
leafId: 'leaf-worker',
ptyId: 'pty-worker'
}
exactHandler?.(
{ sender: mainWindow.webContents },
{ requestId: exactPayload.requestId, tabId: 'tab-worker', identity }
)
await expect(exact).resolves.toEqual({
tabId: 'tab-worker',
title: undefined,
identity
})
})
})
+35 -6
View File
@@ -49,6 +49,7 @@ import type {
RuntimeMarkdownSaveTabResult
} from '../../shared/mobile-markdown-document'
import type { RuntimeMobileSessionTabMove } from '../../shared/runtime-types'
import type { TerminalTabCreateReply } from '../../shared/terminal-reveal-identity'
import { isNativeFileDropPayload, type NativeFileDropPayload } from '../../shared/native-file-drop'
import { requestMobileMarkdownFromRenderer } from './mobile-markdown-request-relay'
import { requestTerminalTabCloseFromRenderer } from './terminal-tab-close-request-relay'
@@ -347,14 +348,20 @@ function registerRuntimeWindowLifecycle(
revealTerminalSession: (worktreeId, opts) =>
new Promise((resolve, reject) => {
const requestId = randomUUID()
const expectedIdentity = opts.expectedProcessIdentity
? opts.tabId && opts.leafId
? { worktreeId, tabId: opts.tabId, leafId: opts.leafId, ptyId: opts.ptyId }
: null
: undefined
if (expectedIdentity === null) {
reject(new Error('terminal_reveal_identity_required'))
return
}
const timer = setTimeout(() => {
ipcMain.removeListener('terminal:tabCreateReply', handler)
reject(new Error('Terminal reveal timed out'))
}, 10_000)
const handler = (
event: Electron.IpcMainEvent,
reply: { requestId: string; tabId?: string; title?: string; error?: string }
): void => {
const handler = (event: Electron.IpcMainEvent, reply: TerminalTabCreateReply): void => {
// Why: requestId is renderer-supplied, so only the targeted main window may satisfy the reveal.
if (event.sender !== mainWindow.webContents || reply.requestId !== requestId) {
return
@@ -365,7 +372,22 @@ function registerRuntimeWindowLifecycle(
reject(new Error(reply.error))
return
}
resolve({ tabId: reply.tabId!, title: reply.title })
if (
expectedIdentity &&
(!reply.identity ||
reply.identity.worktreeId !== expectedIdentity.worktreeId ||
reply.identity.tabId !== expectedIdentity.tabId ||
reply.identity.leafId !== expectedIdentity.leafId ||
reply.identity.ptyId !== expectedIdentity.ptyId)
) {
reject(new Error('terminal_reveal_identity_mismatch'))
return
}
resolve({
tabId: reply.tabId!,
title: reply.title,
...(reply.identity ? { identity: reply.identity } : {})
})
}
ipcMain.on('terminal:tabCreateReply', handler)
send('ui:createTerminal', {
@@ -387,9 +409,16 @@ function registerRuntimeWindowLifecycle(
...(opts.splitDirection !== undefined ? { splitDirection: opts.splitDirection } : {}),
...(opts.splitTelemetrySource !== undefined
? { splitTelemetrySource: opts.splitTelemetrySource }
: {})
: {}),
...(opts.focus !== undefined ? { focus: opts.focus } : {})
})
}),
resolveLegacyWorkerTerminalRecovery: (paneKey, resolution, ptyId) =>
send('agentStatus:legacyWorkerTerminalRecovery', {
paneKey,
resolution,
...(ptyId ? { ptyId } : {})
}),
splitTerminal: (tabId, paneRuntimeId, opts) => {
send('ui:splitTerminal', {
tabId,
+12 -6
View File
@@ -18,6 +18,7 @@ import type {
TerminalTabCloseRequest,
TerminalTabCloseResponse
} from '../shared/terminal-tab-close'
import type { TerminalTabCreateReply } from '../shared/terminal-reveal-identity'
import type {
LocalLogTailChangedPayload,
LocalLogTailReadArgs,
@@ -971,6 +972,8 @@ export type AppApi = {
/** Resolves when the daemon PTY provider and hook receiver have either
* started or failed open for the first BrowserWindow. */
awaitFirstWindowStartupServices: () => Promise<void>
/** Reconciles legacy worker authority around persisted terminal reconnect. */
recoverLegacyWorkerTerminalsForRendererStartup: () => Promise<void>
/** Emits a startup benchmark marker when ORCA_STARTUP_DIAGNOSTICS is enabled. */
startupDiagnostic: (event: string, details?: Record<string, unknown>) => Promise<void>
/** macOS active input mode, or layout ID when no IME is selected (e.g. `com.apple.keylayout.PolishPro`).
@@ -3135,6 +3138,7 @@ export type PreloadApi = {
title?: string
ptyId?: string
activate?: boolean
focus?: boolean
presentation?: RuntimeTerminalPresentation
tabId?: string
leafId?: string
@@ -3149,12 +3153,7 @@ export type PreloadApi = {
onRequestTerminalTabMount: (
callback: (data: { worktreeId: string; tabId?: string; ptyId?: string }) => void
) => () => void
replyTerminalCreate: (reply: {
requestId: string
tabId?: string
title?: string
error?: string
}) => void
replyTerminalCreate: (reply: TerminalTabCreateReply) => void
onSplitTerminal: (
callback: (data: {
tabId: string
@@ -3517,6 +3516,13 @@ export type PreloadApi = {
/** Listen for PTYs on a legacy numeric pane key that have registry-backed UUID pane proof. */
onMigrationUnsupported: (callback: (entry: MigrationUnsupportedPtyEntry) => void) => () => void
onMigrationUnsupportedClear: (callback: (data: { ptyId: string }) => void) => () => void
onLegacyWorkerTerminalRecovery: (
callback: (data: {
paneKey: string
resolution: 'adopted' | 'exited' | 'rolled_back'
ptyId?: string
}) => void
) => () => void
getMigrationUnsupportedSnapshot: () => Promise<MigrationUnsupportedPtyEntry[]>
/** Drop a paneKey from the main-process hook cache and on-disk last-status file. Fire-and-forget. */
drop: (paneKey: string) => void
+24 -6
View File
@@ -13,6 +13,7 @@ import type { CliInstallStatus } from '../shared/cli-install-types'
import type { AgentHookInstallStatus } from '../shared/agent-hook-types'
import type { CodexConfigSyncStatus } from '../shared/codex-config-sync-types'
import type { TerminalPaneSplitSource } from '../shared/feature-education-telemetry'
import type { TerminalTabCreateReply } from '../shared/terminal-reveal-identity'
import type { ProjectExecutionRuntimeResolution } from '../shared/project-execution-runtime'
import type { StartupCommandDelivery } from '../shared/codex-startup-delivery'
import type {
@@ -487,6 +488,8 @@ const api = {
},
awaitFirstWindowStartupServices: (): Promise<void> =>
ipcRenderer.invoke('app:awaitFirstWindowStartupServices'),
recoverLegacyWorkerTerminalsForRendererStartup: (): Promise<void> =>
ipcRenderer.invoke('app:recoverLegacyWorkerTerminalsForRendererStartup'),
startupDiagnostic: (event: string, details?: Record<string, unknown>): Promise<void> =>
startupDiagnosticsEnabled
? ipcRenderer.invoke('app:startupDiagnostic', event, details)
@@ -3696,6 +3699,7 @@ const api = {
title?: string
ptyId?: string
activate?: boolean
focus?: boolean
presentation?: RuntimeTerminalPresentation
tabId?: string
leafId?: string
@@ -3720,6 +3724,7 @@ const api = {
title?: string
ptyId?: string
activate?: boolean
focus?: boolean
presentation?: RuntimeTerminalPresentation
tabId?: string
leafId?: string
@@ -3751,12 +3756,7 @@ const api = {
ipcRenderer.on('terminal:requestTabMount', listener)
return () => ipcRenderer.removeListener('terminal:requestTabMount', listener)
},
replyTerminalCreate: (reply: {
requestId: string
tabId?: string
title?: string
error?: string
}): void => {
replyTerminalCreate: (reply: TerminalTabCreateReply): void => {
ipcRenderer.send('terminal:tabCreateReply', reply)
},
onSplitTerminal: (
@@ -4646,6 +4646,24 @@ const api = {
ipcRenderer.on('agentStatus:migrationUnsupportedClear', listener)
return () => ipcRenderer.removeListener('agentStatus:migrationUnsupportedClear', listener)
},
onLegacyWorkerTerminalRecovery: (
callback: (data: {
paneKey: string
resolution: 'adopted' | 'exited' | 'rolled_back'
ptyId?: string
}) => void
): (() => void) => {
const listener = (
_event: Electron.IpcRendererEvent,
data: {
paneKey: string
resolution: 'adopted' | 'exited' | 'rolled_back'
ptyId?: string
}
) => callback(data)
ipcRenderer.on('agentStatus:legacyWorkerTerminalRecovery', listener)
return () => ipcRenderer.removeListener('agentStatus:legacyWorkerTerminalRecovery', listener)
},
getMigrationUnsupportedSnapshot: (): Promise<MigrationUnsupportedPtyEntry[]> =>
ipcRenderer.invoke('agentStatus:getMigrationUnsupportedSnapshot'),
/** Drop the cached hook status for a paneKey on both sides (memory + on-disk) so a relaunch can't resurrect a dismissed row. */
+85 -11
View File
@@ -198,6 +198,8 @@ async function runOrcaCliMode(sockPath: string, argv: string[]): Promise<void> {
let nextSeq = 1
let highestReceivedSeq = 0
const requestId = 1
const postOutputRequestId = 2
let initialExitCode = 0
const sendRequest = (): void => {
const env = pickRemoteCliEnv(process.env)
@@ -219,6 +221,59 @@ async function runOrcaCliMode(sockPath: string, argv: string[]): Promise<void> {
sock.write(frame)
}
const finish = (exitCode: number): void => {
sock.destroy()
process.exit(exitCode)
}
const sendPostOutput = (postOutput: unknown): void => {
sock.write(
encodeJsonRpcFrame(
{
jsonrpc: '2.0',
id: postOutputRequestId,
method: 'orca.cli.postOutput',
params: { postOutput, env: pickRemoteCliEnv(process.env) }
},
nextSeq++,
highestReceivedSeq
)
)
}
const writeOutput = (
result: { stdout?: unknown; stderr?: unknown },
onFlushed: (error?: Error) => void
): void => {
const writes = [
[process.stdout, result.stdout],
[process.stderr, result.stderr]
].filter((entry): entry is [NodeJS.WriteStream, string] => typeof entry[1] === 'string')
if (writes.length === 0) {
onFlushed()
return
}
let pending = writes.length
let completed = false
for (const [stream, output] of writes) {
stream.write(output, (error) => {
if (completed) {
return
}
if (error) {
completed = true
onFlushed(error)
return
}
pending -= 1
if (pending === 0) {
completed = true
onFlushed()
}
})
}
}
const decoder = new FrameDecoder((frame: DecodedFrame) => {
if (frame.id > highestReceivedSeq) {
highestReceivedSeq = frame.id
@@ -227,28 +282,41 @@ async function runOrcaCliMode(sockPath: string, argv: string[]): Promise<void> {
return
}
const msg = parseJsonRpcMessage(frame.payload)
if (!('id' in msg) || msg.id !== requestId || !('result' in msg || 'error' in msg)) {
if (
!('id' in msg) ||
(msg.id !== requestId && msg.id !== postOutputRequestId) ||
!('result' in msg || 'error' in msg)
) {
return
}
const response = msg as JsonRpcResponse
if (response.error) {
process.stderr.write(`${response.error.message}\n`)
sock.destroy()
process.exit(1)
finish(1)
return
}
if (response.id === postOutputRequestId) {
finish(initialExitCode)
return
}
const result = (response.result ?? {}) as {
stdout?: unknown
stderr?: unknown
exitCode?: unknown
postOutput?: unknown
}
if (typeof result.stdout === 'string' && result.stdout.length > 0) {
process.stdout.write(result.stdout)
}
if (typeof result.stderr === 'string' && result.stderr.length > 0) {
process.stderr.write(result.stderr)
}
sock.destroy()
process.exit(typeof result.exitCode === 'number' ? result.exitCode : 0)
initialExitCode = typeof result.exitCode === 'number' ? result.exitCode : 0
writeOutput(result, (error) => {
if (error) {
finish(1)
return
}
if (result.postOutput === undefined) {
finish(initialExitCode)
return
}
sendPostOutput(result.postOutput)
})
})
const connectTimeout = setTimeout(() => {
@@ -443,6 +511,12 @@ async function main(): Promise<void> {
timeoutMs: remoteCliRequestTimeoutMs(params)
})
})
dispatcher.onRequest('orca.cli.postOutput', async (params, context) => {
return await dispatcher.requestAnyClient('orca.cli.postOutput', params, {
excludeClientId: context.clientId,
timeoutMs: remoteCliRequestTimeoutMs(params)
})
})
function configureRelayGraceTime(params: Record<string, unknown>): { graceTimeMs: number } {
const seconds = Number(params.graceTimeSeconds)

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