mirror of
https://github.com/stablyai/orca.git
synced 2026-09-26 00:02:34 +00:00
* fix(orchestration): typed error codes for dispatch and worker-start refusals orchestration dispatch (and worker-start, which composes it) surfaced task not found, task not ready, and inject rejected as the same bare runtime_error, so an agent reading the receipt could not choose between creating the task, waiting on dependencies, or picking another terminal. Add task_not_found (data.taskId), task_not_ready (data.status, data.unmetDependencies), and inject_rejected (data.terminal, data.reason), each carrying data.nextSteps so every shipped CLI already prints the recovery. worker-start's not-ready refusal moves from task_not_startable to task_not_ready with the same detail. runtime_error stays for genuinely unexpected failures. Proven red-first from RpcDispatcher through the CLI's own failure formatting, plus an SSH bridge test that the host CLI's typed refusal relays unchanged. * test(orchestration): load CLI formatter at runtime in the dispatch-code test The composite node typecheck (config/tsconfig.node.json without --composite false, as CI runs it) rejects a static import of src/cli from a main test with TS6307. Load the formatter and error class dynamically behind narrow structural types, as the CLI/runtime boundary test does. * fix(orchestration): keep task_not_startable and split the CLI-format proof Review on #18902: - Drop task_not_ready. worker-start already published task_not_startable for a not-ready Task, so renaming it would change an existing receipt value under old clients. dispatch now emits task_not_startable too (it was a bare runtime_error before, so this is purely additive), with the new data.status / data.unmetDependencies / data.nextSteps. - Move the refusal receipts (code, message, data) into src/shared/orchestration-dispatch-refusal-contract.ts so the runtime emits them and the CLI test formats the identical envelope. The RPC test under src/main asserts toEqual against the contract; the new src/cli/orchestration-dispatch-refusal-format.test.ts feeds those same receipts to formatCliError / reportCliError. Neither tsconfig widens and the composite typecheck CI runs is clean. * fix(orchestration): keep published refusal messages and type the DB claim guards Codex review of #18902: - Every call site keeps the exact message it published on main ("Task not found: <id>", "only a ready Task can start.", "cannot retry from Dispatch"); the shared contract now takes the message per site and only owns the code and data. Baseline strings are pinned as literals. - createDispatchContext's own missing/non-ready guards, including the atomic-claim loser, now emit the same typed receipt instead of a bare Error, so a dispatch that races a status change no longer flattens to runtime_error. Covered by a dispatcher-level race test. - Invalid --retry-of keeps task_not_startable but now carries status, unmetDependencies, retryOf, and a retry-specific next step. - Dependency recovery text distinguishes waiting on running deps from retrying/unblocking failed ones. - CLI test adds an unknown-code case so the old-client claim rests on an assertion, not a comment; SSH test asserts exact stdout. - Guide table narrowed to the covered preflight cases; occupancy stays runtime_error and is named as such.
103 lines
3.6 KiB
TypeScript
103 lines
3.6 KiB
TypeScript
import { TUI_AGENT_CONFIG } from './tui-agent-config'
|
|
|
|
// Why: one source for each dispatch refusal's code, message, and data, so the runtime emits and
|
|
// the CLI test formats the identical envelope. Messages are supplied per call site because each
|
|
// existing string is a published receipt an old consumer may match on.
|
|
|
|
export type DispatchRefusalReceipt = {
|
|
code: 'task_not_found' | 'task_not_startable' | 'inject_rejected'
|
|
message: string
|
|
data: Record<string, unknown> & { nextSteps: string[] }
|
|
}
|
|
|
|
export function taskNotFoundRefusal(
|
|
message: string,
|
|
detail: { taskId: string; runId?: string }
|
|
): DispatchRefusalReceipt {
|
|
return {
|
|
code: 'task_not_found',
|
|
message,
|
|
data: {
|
|
...detail,
|
|
nextSteps: [
|
|
'Run orca orchestration task-list --json in the bound Run to find the intended Task id.',
|
|
'If the Task does not exist yet, create it with orca orchestration task-create --spec <text> --json.'
|
|
]
|
|
}
|
|
}
|
|
}
|
|
|
|
export type TaskNotStartableDetail = {
|
|
taskId: string
|
|
status: string
|
|
unmetDependencies: string[]
|
|
retryOf?: string
|
|
}
|
|
|
|
export function taskNotStartableRefusal(
|
|
message: string,
|
|
detail: TaskNotStartableDetail
|
|
): DispatchRefusalReceipt {
|
|
return {
|
|
code: 'task_not_startable',
|
|
message,
|
|
data: { ...detail, nextSteps: taskNotStartableNextSteps(detail) }
|
|
}
|
|
}
|
|
|
|
function taskNotStartableNextSteps(detail: TaskNotStartableDetail): string[] {
|
|
if (detail.retryOf) {
|
|
return [
|
|
`--retry-of must name the latest settled Dispatch of a failed or blocked Task; check orca orchestration dispatch-show --task ${detail.taskId} --json and orca orchestration worker-show --dispatch ${detail.retryOf} --json.`
|
|
]
|
|
}
|
|
if (detail.unmetDependencies.length > 0) {
|
|
return [
|
|
`Dependencies ${detail.unmetDependencies.join(', ')} are not completed. Wait for running ones with orca orchestration check --wait --json; retry or unblock failed ones before dispatching again.`
|
|
]
|
|
}
|
|
if (detail.status === 'dispatched') {
|
|
return [
|
|
`The Task already has an active Dispatch; inspect it with orca orchestration dispatch-show --task ${detail.taskId} --json.`
|
|
]
|
|
}
|
|
return [
|
|
`A ${detail.status} Task cannot be dispatched; create a new Task or use worker-start --retry-of for a failed attempt.`
|
|
]
|
|
}
|
|
|
|
// Why: the old five-name example read as an allowlist (#15125); derive from the field detection keys on so it cannot drift.
|
|
// Not filtered by `disabledTuiAgents` — that gates Orca's launchers, not detection, so a hand-started disabled agent still injects.
|
|
const RECOGNIZED_AGENT_PROCESS_NAMES = [
|
|
...new Set(Object.values(TUI_AGENT_CONFIG).map((config) => config.expectedProcess))
|
|
].sort()
|
|
|
|
export function buildInjectRejectionMessage(terminal: string): string {
|
|
return (
|
|
`Cannot dispatch --inject to terminal ${terminal}: no recognized agent detected. ` +
|
|
`Orca detects these agent CLIs (${RECOGNIZED_AGENT_PROCESS_NAMES.join(', ')}). ` +
|
|
'Start one in the terminal and let it finish launching, ' +
|
|
'or dispatch without --inject and send the prompt manually.'
|
|
)
|
|
}
|
|
|
|
export type InjectRejectionReason = 'no_agent_detected'
|
|
|
|
export function injectRejectedRefusal(
|
|
terminal: string,
|
|
reason: InjectRejectionReason
|
|
): DispatchRefusalReceipt {
|
|
return {
|
|
code: 'inject_rejected',
|
|
message: buildInjectRejectionMessage(terminal),
|
|
data: {
|
|
terminal,
|
|
reason,
|
|
nextSteps: [
|
|
'Start a recognized agent CLI in that terminal and wait for it to finish launching, or pick a terminal that already runs one.',
|
|
'Alternatively dispatch without --inject and deliver the prompt with orca terminal send --terminal <handle> --text <prompt> --enter --json.'
|
|
]
|
|
}
|
|
}
|
|
}
|