Validate ORCA_TERMINAL_HANDLE and remint stale orchestration terminals via pane key (#7514)
* Validate ORCA_TERMINAL_HANDLE and fall back to active terminal if stale Long-lived shells can retain a stale ORCA_TERMINAL_HANDLE environment variable after the runtime remints a pane handle. This can cause commands to bake obsolete terminal handles into coordinator preambles or tasks. - Check if the environment-provided handle is live via terminal.show before using it in dispatch, task-create, or run operations. - Fall back to resolving the active terminal/implicit sender if the environment handle is stale. - Map raw "no_active_terminal" errors to a helpful user-facing error message suggesting the use of the "--from" flag. * Remint stale orchestration terminals via pane key instead of focus Resolve stale environment-provided terminal handles using the caller's pane key (ORCA_PANE_KEY) via terminal.resolvePane instead of falling back to the active focused terminal. This prevents commands from being dispatched from or credited to the wrong terminal pane if focus has changed. Additionally, handle graph or pane resolution failures gracefully during task creation since creator handles are best-effort lineage metadata. * refactor orchestration tests to use helper stubs for stale handles Consolidate repetitive mocking boilerplate for stale terminal handle reminting and failure flows using new helper functions.
This commit is contained in:
parent
61ecaaf521
commit
8e16ec1007
|
|
@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|||
const callMock = vi.fn()
|
||||
const getTerminalHandleMock = vi.hoisted(() => vi.fn())
|
||||
const originalTerminalHandle = process.env.ORCA_TERMINAL_HANDLE
|
||||
const originalPaneKey = process.env.ORCA_PANE_KEY
|
||||
function lifecycleGroupRecipientError(type: 'worker_done' | 'heartbeat'): string {
|
||||
return `${type} messages must be sent to a concrete coordinator terminal handle, not a group address.`
|
||||
}
|
||||
|
|
@ -12,6 +13,25 @@ vi.mock('../format', () => ({ printResult: vi.fn() }))
|
|||
vi.mock('../selectors', () => ({ getTerminalHandle: getTerminalHandleMock }))
|
||||
|
||||
import { ORCHESTRATION_HANDLERS } from './orchestration'
|
||||
import { RuntimeClientError } from '../runtime-client'
|
||||
|
||||
function staleHandleError(): RuntimeClientError {
|
||||
return new RuntimeClientError('terminal_handle_stale', 'terminal_handle_stale')
|
||||
}
|
||||
|
||||
// Queues the stale-handle remint chain shared by coordinator commands:
|
||||
// stale terminal.show → resolvePane returns liveHandle → downstream RPC result.
|
||||
function stubStaleHandleRemint(liveHandle: string, downstream: unknown): void {
|
||||
callMock
|
||||
.mockRejectedValueOnce(staleHandleError())
|
||||
.mockResolvedValueOnce({ result: { terminal: { handle: liveHandle } } })
|
||||
.mockResolvedValueOnce(downstream)
|
||||
}
|
||||
|
||||
// Queues a stale terminal.show followed by a resolvePane remint that fails with `error`.
|
||||
function stubStaleHandleRemintFailure(error: RuntimeClientError): void {
|
||||
callMock.mockRejectedValueOnce(staleHandleError()).mockRejectedValueOnce(error)
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
getTerminalHandleMock.mockReset()
|
||||
|
|
@ -20,6 +40,11 @@ afterEach(() => {
|
|||
} else {
|
||||
process.env.ORCA_TERMINAL_HANDLE = originalTerminalHandle
|
||||
}
|
||||
if (originalPaneKey === undefined) {
|
||||
delete process.env.ORCA_PANE_KEY
|
||||
} else {
|
||||
process.env.ORCA_PANE_KEY = originalPaneKey
|
||||
}
|
||||
})
|
||||
|
||||
describe('orchestration reset CLI handler', () => {
|
||||
|
|
@ -67,6 +92,7 @@ describe('orchestration send structured payload flags', () => {
|
|||
callMock.mockReset().mockResolvedValue({ result: { message: { id: 'msg_1' } } })
|
||||
getTerminalHandleMock.mockReset()
|
||||
delete process.env.ORCA_TERMINAL_HANDLE
|
||||
delete process.env.ORCA_PANE_KEY
|
||||
})
|
||||
|
||||
const invokeSend = (flags: Map<string, string | boolean>) =>
|
||||
|
|
@ -186,6 +212,382 @@ describe('orchestration send structured payload flags', () => {
|
|||
devMode: false
|
||||
})
|
||||
})
|
||||
|
||||
it('continues to use ORCA_TERMINAL_HANDLE as worker lifecycle sender authority', async () => {
|
||||
process.env.ORCA_TERMINAL_HANDLE = 'term_worker_env'
|
||||
|
||||
await invokeSend(
|
||||
new Map<string, string | boolean>([
|
||||
['to', 'term_coord'],
|
||||
['subject', 'done'],
|
||||
['type', 'worker_done']
|
||||
])
|
||||
)
|
||||
|
||||
expect(callMock).toHaveBeenCalledTimes(1)
|
||||
expect(callMock).toHaveBeenCalledWith('orchestration.send', {
|
||||
from: 'term_worker_env',
|
||||
to: 'term_coord',
|
||||
subject: 'done',
|
||||
body: undefined,
|
||||
type: 'worker_done',
|
||||
priority: undefined,
|
||||
threadId: undefined,
|
||||
payload: undefined,
|
||||
devMode: false
|
||||
})
|
||||
})
|
||||
|
||||
it('reports sender resolution failure instead of raw no_active_terminal', async () => {
|
||||
getTerminalHandleMock.mockRejectedValue(
|
||||
new RuntimeClientError('no_active_terminal', 'no_active_terminal')
|
||||
)
|
||||
|
||||
await expect(
|
||||
invokeSend(
|
||||
new Map<string, string | boolean>([
|
||||
['to', 'term_coord'],
|
||||
['subject', 'done'],
|
||||
['type', 'worker_done']
|
||||
])
|
||||
)
|
||||
).rejects.toMatchObject({
|
||||
code: 'no_active_sender_terminal',
|
||||
message: expect.stringContaining('Pass --from')
|
||||
})
|
||||
expect(callMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('orchestration dispatch coordinator handle', () => {
|
||||
beforeEach(() => {
|
||||
callMock.mockReset()
|
||||
getTerminalHandleMock.mockReset()
|
||||
delete process.env.ORCA_TERMINAL_HANDLE
|
||||
delete process.env.ORCA_PANE_KEY
|
||||
})
|
||||
|
||||
const invokeDispatch = (flags: Map<string, string | boolean>) =>
|
||||
ORCHESTRATION_HANDLERS['orchestration dispatch']({
|
||||
flags,
|
||||
client: { call: callMock },
|
||||
cwd: '/tmp/repo',
|
||||
json: true
|
||||
} as never)
|
||||
|
||||
const invokeDispatchShow = (flags: Map<string, string | boolean>) =>
|
||||
ORCHESTRATION_HANDLERS['orchestration dispatch-show']({
|
||||
flags,
|
||||
client: { call: callMock },
|
||||
cwd: '/tmp/repo',
|
||||
json: true
|
||||
} as never)
|
||||
|
||||
const invokeRun = (flags: Map<string, string | boolean>) =>
|
||||
ORCHESTRATION_HANDLERS['orchestration run']({
|
||||
flags,
|
||||
client: { call: callMock },
|
||||
cwd: '/tmp/repo',
|
||||
json: true
|
||||
} as never)
|
||||
|
||||
it('remints a stale coordinator env handle from the caller pane key', async () => {
|
||||
process.env.ORCA_TERMINAL_HANDLE = 'term_stale_coord'
|
||||
process.env.ORCA_PANE_KEY = 'tab_coord:leaf_coord'
|
||||
stubStaleHandleRemint('term_live_coord', {
|
||||
result: { dispatch: { id: 'ctx_1', task_id: 'task_1', status: 'dispatched' } }
|
||||
})
|
||||
getTerminalHandleMock.mockRejectedValue(new Error('active terminal fallback is unsafe'))
|
||||
|
||||
await invokeDispatch(
|
||||
new Map<string, string | boolean>([
|
||||
['task', 'task_1'],
|
||||
['to', 'term_worker'],
|
||||
['inject', true]
|
||||
])
|
||||
)
|
||||
|
||||
expect(callMock).toHaveBeenNthCalledWith(1, 'terminal.show', {
|
||||
terminal: 'term_stale_coord'
|
||||
})
|
||||
expect(callMock).toHaveBeenNthCalledWith(2, 'terminal.resolvePane', {
|
||||
paneKey: 'tab_coord:leaf_coord'
|
||||
})
|
||||
expect(getTerminalHandleMock).not.toHaveBeenCalled()
|
||||
expect(callMock).toHaveBeenNthCalledWith(3, 'orchestration.dispatch', {
|
||||
task: 'task_1',
|
||||
to: 'term_worker',
|
||||
from: 'term_live_coord',
|
||||
inject: true,
|
||||
dryRun: undefined,
|
||||
returnPreamble: undefined,
|
||||
devMode: false
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects stale coordinator env handles when the caller pane cannot be proven', async () => {
|
||||
process.env.ORCA_TERMINAL_HANDLE = 'term_stale_coord'
|
||||
callMock.mockRejectedValueOnce(staleHandleError())
|
||||
getTerminalHandleMock.mockResolvedValue('term_wrong_active')
|
||||
|
||||
await expect(
|
||||
invokeDispatch(
|
||||
new Map<string, string | boolean>([
|
||||
['task', 'task_1'],
|
||||
['to', 'term_worker']
|
||||
])
|
||||
)
|
||||
).rejects.toMatchObject({
|
||||
code: 'no_active_sender_terminal'
|
||||
})
|
||||
|
||||
expect(callMock).toHaveBeenCalledTimes(1)
|
||||
expect(getTerminalHandleMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('propagates unexpected caller pane remint failures for coordinator commands', async () => {
|
||||
process.env.ORCA_TERMINAL_HANDLE = 'term_stale_coord'
|
||||
process.env.ORCA_PANE_KEY = 'tab_coord:leaf_coord'
|
||||
stubStaleHandleRemintFailure(
|
||||
new RuntimeClientError('runtime_unavailable', 'runtime_unavailable')
|
||||
)
|
||||
getTerminalHandleMock.mockResolvedValue('term_wrong_active')
|
||||
|
||||
await expect(
|
||||
invokeDispatch(
|
||||
new Map<string, string | boolean>([
|
||||
['task', 'task_1'],
|
||||
['to', 'term_worker']
|
||||
])
|
||||
)
|
||||
).rejects.toMatchObject({
|
||||
code: 'runtime_unavailable'
|
||||
})
|
||||
|
||||
expect(callMock).toHaveBeenNthCalledWith(1, 'terminal.show', {
|
||||
terminal: 'term_stale_coord'
|
||||
})
|
||||
expect(callMock).toHaveBeenNthCalledWith(2, 'terminal.resolvePane', {
|
||||
paneKey: 'tab_coord:leaf_coord'
|
||||
})
|
||||
expect(callMock).toHaveBeenCalledTimes(2)
|
||||
expect(getTerminalHandleMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('uses a live coordinator handle for dispatch-show preamble previews', async () => {
|
||||
process.env.ORCA_TERMINAL_HANDLE = 'term_stale_coord'
|
||||
process.env.ORCA_PANE_KEY = 'tab_coord:leaf_coord'
|
||||
stubStaleHandleRemint('term_live_coord', {
|
||||
result: { dispatch: null, preamble: 'preamble' }
|
||||
})
|
||||
getTerminalHandleMock.mockRejectedValue(new Error('active terminal fallback is unsafe'))
|
||||
|
||||
await invokeDispatchShow(
|
||||
new Map<string, string | boolean>([
|
||||
['task', 'task_1'],
|
||||
['preamble', true]
|
||||
])
|
||||
)
|
||||
|
||||
expect(callMock).toHaveBeenNthCalledWith(1, 'terminal.show', {
|
||||
terminal: 'term_stale_coord'
|
||||
})
|
||||
expect(callMock).toHaveBeenNthCalledWith(2, 'terminal.resolvePane', {
|
||||
paneKey: 'tab_coord:leaf_coord'
|
||||
})
|
||||
expect(callMock).toHaveBeenNthCalledWith(3, 'orchestration.dispatchShow', {
|
||||
task: 'task_1',
|
||||
preamble: true,
|
||||
from: 'term_live_coord',
|
||||
devMode: false
|
||||
})
|
||||
})
|
||||
|
||||
it('uses a live coordinator handle for orchestration runs', async () => {
|
||||
process.env.ORCA_TERMINAL_HANDLE = 'term_stale_coord'
|
||||
process.env.ORCA_PANE_KEY = 'tab_coord:leaf_coord'
|
||||
stubStaleHandleRemint('term_live_coord', {
|
||||
result: { runId: 'run_1', status: 'running' }
|
||||
})
|
||||
getTerminalHandleMock.mockRejectedValue(new Error('active terminal fallback is unsafe'))
|
||||
|
||||
await invokeRun(new Map<string, string | boolean>([['spec', 'run the plan']]))
|
||||
|
||||
expect(callMock).toHaveBeenNthCalledWith(1, 'terminal.show', {
|
||||
terminal: 'term_stale_coord'
|
||||
})
|
||||
expect(callMock).toHaveBeenNthCalledWith(2, 'terminal.resolvePane', {
|
||||
paneKey: 'tab_coord:leaf_coord'
|
||||
})
|
||||
expect(callMock).toHaveBeenNthCalledWith(3, 'orchestration.run', {
|
||||
spec: 'run the plan',
|
||||
from: 'term_live_coord',
|
||||
pollIntervalMs: undefined,
|
||||
maxConcurrent: undefined,
|
||||
worktree: undefined
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('orchestration task-create caller handle', () => {
|
||||
beforeEach(() => {
|
||||
callMock.mockReset()
|
||||
getTerminalHandleMock.mockReset()
|
||||
delete process.env.ORCA_TERMINAL_HANDLE
|
||||
delete process.env.ORCA_PANE_KEY
|
||||
})
|
||||
|
||||
const invokeTaskCreate = (flags: Map<string, string | boolean>) =>
|
||||
ORCHESTRATION_HANDLERS['orchestration task-create']({
|
||||
flags,
|
||||
client: { call: callMock },
|
||||
cwd: '/tmp/repo',
|
||||
json: true
|
||||
} as never)
|
||||
|
||||
it('records a live env terminal handle as task creator', async () => {
|
||||
process.env.ORCA_TERMINAL_HANDLE = 'term_creator'
|
||||
callMock
|
||||
.mockResolvedValueOnce({ result: { terminal: { handle: 'term_creator' } } })
|
||||
.mockResolvedValueOnce({ result: { task: { id: 'task_1', status: 'ready' } } })
|
||||
|
||||
await invokeTaskCreate(new Map<string, string | boolean>([['spec', 'do work']]))
|
||||
|
||||
expect(callMock).toHaveBeenNthCalledWith(1, 'terminal.show', { terminal: 'term_creator' })
|
||||
expect(callMock).toHaveBeenNthCalledWith(2, 'orchestration.taskCreate', {
|
||||
spec: 'do work',
|
||||
taskTitle: undefined,
|
||||
displayName: undefined,
|
||||
deps: undefined,
|
||||
parent: undefined,
|
||||
callerTerminalHandle: 'term_creator'
|
||||
})
|
||||
})
|
||||
|
||||
it('does not persist a stale env terminal handle as task creator', async () => {
|
||||
process.env.ORCA_TERMINAL_HANDLE = 'term_stale'
|
||||
callMock
|
||||
.mockRejectedValueOnce(staleHandleError())
|
||||
.mockResolvedValueOnce({ result: { task: { id: 'task_1', status: 'ready' } } })
|
||||
getTerminalHandleMock.mockResolvedValue('term_wrong_active')
|
||||
|
||||
await invokeTaskCreate(new Map<string, string | boolean>([['spec', 'do work']]))
|
||||
|
||||
expect(callMock).toHaveBeenNthCalledWith(1, 'terminal.show', { terminal: 'term_stale' })
|
||||
expect(getTerminalHandleMock).not.toHaveBeenCalled()
|
||||
expect(callMock).toHaveBeenNthCalledWith(2, 'orchestration.taskCreate', {
|
||||
spec: 'do work',
|
||||
taskTitle: undefined,
|
||||
displayName: undefined,
|
||||
deps: undefined,
|
||||
parent: undefined,
|
||||
callerTerminalHandle: undefined
|
||||
})
|
||||
})
|
||||
|
||||
it('does not fail task creation when env handle validation cannot inspect the graph', async () => {
|
||||
process.env.ORCA_TERMINAL_HANDLE = 'term_creator'
|
||||
callMock
|
||||
.mockRejectedValueOnce(new RuntimeClientError('runtime_unavailable', 'runtime_unavailable'))
|
||||
.mockResolvedValueOnce({ result: { task: { id: 'task_1', status: 'ready' } } })
|
||||
|
||||
await invokeTaskCreate(new Map<string, string | boolean>([['spec', 'do work']]))
|
||||
|
||||
expect(callMock).toHaveBeenNthCalledWith(1, 'terminal.show', { terminal: 'term_creator' })
|
||||
expect(getTerminalHandleMock).not.toHaveBeenCalled()
|
||||
expect(callMock).toHaveBeenNthCalledWith(2, 'orchestration.taskCreate', {
|
||||
spec: 'do work',
|
||||
taskTitle: undefined,
|
||||
displayName: undefined,
|
||||
deps: undefined,
|
||||
parent: undefined,
|
||||
callerTerminalHandle: undefined
|
||||
})
|
||||
})
|
||||
|
||||
it('omits caller handle when pane reminting cannot inspect the graph', async () => {
|
||||
process.env.ORCA_TERMINAL_HANDLE = 'term_stale'
|
||||
process.env.ORCA_PANE_KEY = 'tab_creator:leaf_creator'
|
||||
stubStaleHandleRemintFailure(
|
||||
new RuntimeClientError('runtime_unavailable', 'runtime_unavailable')
|
||||
)
|
||||
callMock.mockResolvedValueOnce({ result: { task: { id: 'task_1', status: 'ready' } } })
|
||||
getTerminalHandleMock.mockResolvedValue('term_wrong_active')
|
||||
|
||||
await invokeTaskCreate(new Map<string, string | boolean>([['spec', 'do work']]))
|
||||
|
||||
expect(callMock).toHaveBeenNthCalledWith(1, 'terminal.show', { terminal: 'term_stale' })
|
||||
expect(callMock).toHaveBeenNthCalledWith(2, 'terminal.resolvePane', {
|
||||
paneKey: 'tab_creator:leaf_creator'
|
||||
})
|
||||
expect(getTerminalHandleMock).not.toHaveBeenCalled()
|
||||
expect(callMock).toHaveBeenNthCalledWith(3, 'orchestration.taskCreate', {
|
||||
spec: 'do work',
|
||||
taskTitle: undefined,
|
||||
displayName: undefined,
|
||||
deps: undefined,
|
||||
parent: undefined,
|
||||
callerTerminalHandle: undefined
|
||||
})
|
||||
})
|
||||
|
||||
it('propagates unexpected caller pane remint failures for task creation', async () => {
|
||||
process.env.ORCA_TERMINAL_HANDLE = 'term_stale'
|
||||
process.env.ORCA_PANE_KEY = 'tab_creator:leaf_creator'
|
||||
stubStaleHandleRemintFailure(new RuntimeClientError('permission_denied', 'denied'))
|
||||
getTerminalHandleMock.mockResolvedValue('term_wrong_active')
|
||||
|
||||
await expect(
|
||||
invokeTaskCreate(new Map<string, string | boolean>([['spec', 'do work']]))
|
||||
).rejects.toMatchObject({
|
||||
code: 'permission_denied'
|
||||
})
|
||||
|
||||
expect(callMock).toHaveBeenNthCalledWith(1, 'terminal.show', { terminal: 'term_stale' })
|
||||
expect(callMock).toHaveBeenNthCalledWith(2, 'terminal.resolvePane', {
|
||||
paneKey: 'tab_creator:leaf_creator'
|
||||
})
|
||||
expect(callMock).toHaveBeenCalledTimes(2)
|
||||
expect(getTerminalHandleMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('propagates unexpected env handle validation failures', async () => {
|
||||
process.env.ORCA_TERMINAL_HANDLE = 'term_creator'
|
||||
callMock.mockRejectedValueOnce(new RuntimeClientError('permission_denied', 'denied'))
|
||||
|
||||
await expect(
|
||||
invokeTaskCreate(new Map<string, string | boolean>([['spec', 'do work']]))
|
||||
).rejects.toMatchObject({
|
||||
code: 'permission_denied'
|
||||
})
|
||||
|
||||
expect(callMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('remints a stale task creator env handle from the caller pane key', async () => {
|
||||
process.env.ORCA_TERMINAL_HANDLE = 'term_stale'
|
||||
process.env.ORCA_PANE_KEY = 'tab_creator:leaf_creator'
|
||||
stubStaleHandleRemint('term_live', {
|
||||
result: { task: { id: 'task_1', status: 'ready' } }
|
||||
})
|
||||
getTerminalHandleMock.mockRejectedValue(new Error('active terminal fallback is unsafe'))
|
||||
|
||||
await invokeTaskCreate(new Map<string, string | boolean>([['spec', 'do work']]))
|
||||
|
||||
expect(callMock).toHaveBeenNthCalledWith(1, 'terminal.show', { terminal: 'term_stale' })
|
||||
expect(callMock).toHaveBeenNthCalledWith(2, 'terminal.resolvePane', {
|
||||
paneKey: 'tab_creator:leaf_creator'
|
||||
})
|
||||
expect(getTerminalHandleMock).not.toHaveBeenCalled()
|
||||
expect(callMock).toHaveBeenNthCalledWith(3, 'orchestration.taskCreate', {
|
||||
spec: 'do work',
|
||||
taskTitle: undefined,
|
||||
displayName: undefined,
|
||||
deps: undefined,
|
||||
parent: undefined,
|
||||
callerTerminalHandle: 'term_live'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('orchestration timeout flag validation', () => {
|
||||
|
|
@ -200,6 +602,7 @@ describe('orchestration timeout flag validation', () => {
|
|||
beforeEach(() => {
|
||||
callMock.mockReset()
|
||||
delete process.env.ORCA_TERMINAL_HANDLE
|
||||
delete process.env.ORCA_PANE_KEY
|
||||
})
|
||||
|
||||
const invokeCheck = (flags: Map<string, string | boolean>) =>
|
||||
|
|
|
|||
|
|
@ -127,7 +127,8 @@ async function resolveOrchestrationTerminalHandle(
|
|||
flags: Map<string, string | boolean>,
|
||||
cwd: string,
|
||||
client: Parameters<CommandHandler>[0]['client'],
|
||||
flagName: 'from' | 'terminal'
|
||||
flagName: 'from' | 'terminal',
|
||||
options: { validateEnvHandle?: boolean } = {}
|
||||
): Promise<string> {
|
||||
const explicit = getOptionalStringFlag(flags, flagName)
|
||||
if (explicit) {
|
||||
|
|
@ -135,11 +136,181 @@ async function resolveOrchestrationTerminalHandle(
|
|||
}
|
||||
const envHandle = process.env.ORCA_TERMINAL_HANDLE
|
||||
if (envHandle && envHandle.length > 0) {
|
||||
if (flagName === 'from' && options.validateEnvHandle) {
|
||||
// Why: long-lived shells can retain an ORCA_TERMINAL_HANDLE after the
|
||||
// runtime remints the pane handle; do not bake that stale id into
|
||||
// coordinator preambles.
|
||||
const live = await isLiveTerminalHandle(envHandle, client)
|
||||
if (!live) {
|
||||
return await resolveStaleOrchestrationSender(client)
|
||||
}
|
||||
}
|
||||
return envHandle
|
||||
}
|
||||
if (flagName === 'from') {
|
||||
return await resolveImplicitOrchestrationSender(flags, cwd, client)
|
||||
}
|
||||
return await getTerminalHandle(flags, cwd, client)
|
||||
}
|
||||
|
||||
async function resolveTaskCreatorTerminalHandle(
|
||||
client: Parameters<CommandHandler>[0]['client']
|
||||
): Promise<string | undefined> {
|
||||
const envHandle = process.env.ORCA_TERMINAL_HANDLE
|
||||
if (!envHandle || envHandle.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
let live: boolean
|
||||
try {
|
||||
live = await isLiveTerminalHandle(envHandle, client)
|
||||
} catch (err) {
|
||||
if (isOptionalTaskCreatorHandleError(err)) {
|
||||
// Why: creator handles are best-effort lineage metadata; graph
|
||||
// unavailability should not block task creation itself.
|
||||
return undefined
|
||||
}
|
||||
throw err
|
||||
}
|
||||
if (live) {
|
||||
return envHandle
|
||||
}
|
||||
return await resolveOrchestrationPaneTerminalHandle(client, { optional: true })
|
||||
}
|
||||
|
||||
async function isLiveTerminalHandle(
|
||||
handle: string,
|
||||
client: Parameters<CommandHandler>[0]['client']
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await client.call('terminal.show', { terminal: handle })
|
||||
return true
|
||||
} catch (err) {
|
||||
if (isStaleTerminalIdentityError(err)) {
|
||||
return false
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
function getClientErrorCode(err: unknown): string | undefined {
|
||||
if (!err || typeof err !== 'object') {
|
||||
return undefined
|
||||
}
|
||||
const code = (err as { code?: unknown }).code
|
||||
return typeof code === 'string' ? code : undefined
|
||||
}
|
||||
|
||||
function isStaleTerminalIdentityError(err: unknown): boolean {
|
||||
const code = getClientErrorCode(err)
|
||||
return code === 'terminal_handle_stale' || code === 'terminal_gone'
|
||||
}
|
||||
|
||||
function isNoActiveTerminalError(err: unknown): boolean {
|
||||
return getClientErrorCode(err) === 'no_active_terminal'
|
||||
}
|
||||
|
||||
function isOptionalTaskCreatorHandleError(err: unknown): boolean {
|
||||
const code = getClientErrorCode(err)
|
||||
return code === 'no_active_sender_terminal' || code === 'runtime_unavailable'
|
||||
}
|
||||
|
||||
async function resolveOrchestrationPaneTerminalHandle(
|
||||
client: Parameters<CommandHandler>[0]['client'],
|
||||
options: { optional?: boolean } = {}
|
||||
): Promise<string | undefined> {
|
||||
const paneKey = process.env.ORCA_PANE_KEY
|
||||
if (!paneKey || paneKey.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
try {
|
||||
// Why: pane key reminting preserves the caller identity; focus-based
|
||||
// active-terminal fallback can point at a different pane.
|
||||
const response = await client.call<{ terminal: { handle: string } }>('terminal.resolvePane', {
|
||||
paneKey
|
||||
})
|
||||
return response.result.terminal.handle
|
||||
} catch (err) {
|
||||
if (
|
||||
isPaneRemintUnavailableError(err) ||
|
||||
(options.optional === true && isOptionalPaneRemintUnavailableError(err))
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
function isPaneRemintUnavailableError(err: unknown): boolean {
|
||||
const code = getClientErrorCode(err)
|
||||
const message = getClientErrorMessage(err)
|
||||
return (
|
||||
code === 'terminal_not_found' ||
|
||||
code === 'terminal_handle_stale' ||
|
||||
code === 'terminal_gone' ||
|
||||
message === 'terminal_not_found' ||
|
||||
message === 'terminal_handle_stale' ||
|
||||
message === 'terminal_gone'
|
||||
)
|
||||
}
|
||||
|
||||
function isOptionalPaneRemintUnavailableError(err: unknown): boolean {
|
||||
return getClientErrorCode(err) === 'runtime_unavailable'
|
||||
}
|
||||
|
||||
function getClientErrorMessage(err: unknown): string | undefined {
|
||||
if (err instanceof Error) {
|
||||
return err.message
|
||||
}
|
||||
if (!err || typeof err !== 'object') {
|
||||
return undefined
|
||||
}
|
||||
const message = (err as { message?: unknown }).message
|
||||
return typeof message === 'string' ? message : undefined
|
||||
}
|
||||
|
||||
async function resolveStaleOrchestrationSender(
|
||||
client: Parameters<CommandHandler>[0]['client']
|
||||
): Promise<string> {
|
||||
const paneHandle = await resolveOrchestrationPaneTerminalHandle(client)
|
||||
if (paneHandle) {
|
||||
return paneHandle
|
||||
}
|
||||
throwNoActiveSenderTerminal()
|
||||
}
|
||||
|
||||
async function resolveCoordinatorTerminalHandle(
|
||||
flags: Map<string, string | boolean>,
|
||||
cwd: string,
|
||||
client: Parameters<CommandHandler>[0]['client']
|
||||
): Promise<string> {
|
||||
return await resolveOrchestrationTerminalHandle(flags, cwd, client, 'from', {
|
||||
validateEnvHandle: true
|
||||
})
|
||||
}
|
||||
|
||||
async function resolveImplicitOrchestrationSender(
|
||||
flags: Map<string, string | boolean>,
|
||||
cwd: string,
|
||||
client: Parameters<CommandHandler>[0]['client']
|
||||
): Promise<string> {
|
||||
try {
|
||||
return await getTerminalHandle(flags, cwd, client)
|
||||
} catch (err) {
|
||||
if (!isNoActiveTerminalError(err)) {
|
||||
throw err
|
||||
}
|
||||
throwNoActiveSenderTerminal()
|
||||
}
|
||||
}
|
||||
|
||||
function throwNoActiveSenderTerminal(): never {
|
||||
throw new RuntimeClientError(
|
||||
'no_active_sender_terminal',
|
||||
'Could not determine the sender terminal for this orchestration command. ' +
|
||||
'Pass --from <terminal-handle> or run the command inside a live Orca terminal with ORCA_TERMINAL_HANDLE set.'
|
||||
)
|
||||
}
|
||||
|
||||
function isDevCliInvocation(): boolean {
|
||||
return process.env.ORCA_USER_DATA_PATH?.includes('orca-dev') ?? false
|
||||
}
|
||||
|
|
@ -289,11 +460,7 @@ export const ORCHESTRATION_HANDLERS: Record<string, CommandHandler> = {
|
|||
},
|
||||
|
||||
'orchestration task-create': async ({ flags, client, json }) => {
|
||||
const callerTerminalHandle =
|
||||
typeof process.env.ORCA_TERMINAL_HANDLE === 'string' &&
|
||||
process.env.ORCA_TERMINAL_HANDLE.length > 0
|
||||
? process.env.ORCA_TERMINAL_HANDLE
|
||||
: undefined
|
||||
const callerTerminalHandle = await resolveTaskCreatorTerminalHandle(client)
|
||||
const result = await client.call<{ task: { id: string; status: string } }>(
|
||||
'orchestration.taskCreate',
|
||||
{
|
||||
|
|
@ -361,7 +528,7 @@ export const ORCHESTRATION_HANDLERS: Record<string, CommandHandler> = {
|
|||
},
|
||||
|
||||
'orchestration dispatch': async ({ flags, client, cwd, json }) => {
|
||||
const from = await resolveOrchestrationTerminalHandle(flags, cwd, client, 'from')
|
||||
const from = await resolveCoordinatorTerminalHandle(flags, cwd, client)
|
||||
const dryRun = flags.has('dry-run') ? true : undefined
|
||||
const returnPreamble = flags.has('return-preamble') ? true : undefined
|
||||
// Why: --to is only required for non-dry-run; the RPC handler re-enforces.
|
||||
|
|
@ -437,7 +604,7 @@ export const ORCHESTRATION_HANDLERS: Record<string, CommandHandler> = {
|
|||
// Why: resolve --from when previewing so the preamble embeds a real
|
||||
// coordinator handle, matching what an actual dispatch would produce.
|
||||
const from = showPreamble
|
||||
? await resolveOrchestrationTerminalHandle(flags, cwd, client, 'from')
|
||||
? await resolveCoordinatorTerminalHandle(flags, cwd, client)
|
||||
: undefined
|
||||
const result = await client.call<{
|
||||
dispatch: { id: string; task_id: string; status: string } | null
|
||||
|
|
@ -460,7 +627,7 @@ export const ORCHESTRATION_HANDLERS: Record<string, CommandHandler> = {
|
|||
},
|
||||
|
||||
'orchestration run': async ({ flags, client, cwd, json }) => {
|
||||
const from = await resolveOrchestrationTerminalHandle(flags, cwd, client, 'from')
|
||||
const from = await resolveCoordinatorTerminalHandle(flags, cwd, client)
|
||||
const result = await client.call<{
|
||||
runId: string
|
||||
status: string
|
||||
|
|
|
|||
Loading…
Reference in New Issue