mirror of
https://github.com/stablyai/orca.git
synced 2026-10-29 16:17:51 +00:00
* fix(orchestration): bind lifecycle sends to dispatched pane * fix(orchestration): bind injected worker messaging * Fix worker_done pane identity gating and rename SSH linear CLI files - Lifecycle reconciliation now returns an explicit `rejected` action (instead of `ignored`) for persisted sender_not_assignee markers, and only treats a payload's `_orcaLifecycleRejection` as trusted state when it exactly matches that reserved shape, preventing caller- supplied markers from spoofing a rejected send as success. - SSH legacy fallback CLI now fails closed with `no_active_sender_terminal` when a worker_done/heartbeat send has no resolvable sender identity, rejects mixed raw/structured payload flags, and reconstructs structured lifecycle payload fields (task/dispatch id, files-modified, report-path, phase) instead of dropping them — extracted into ssh-remote-orchestration-send.ts. - Renamed ssh-remote-linear-argument-error.ts to ssh-remote-cli-argument-error.ts since RemoteCliArgumentError is now shared beyond the Linear CLI. * Add re-read logging for already-converted lifecycle rejections Send-path reconcile converts worker_done/heartbeat rejections with a no-op logger, so the coordinator's later re-read is the only chance to surface the rejection message. Log it there instead of dropping it.
84 lines
2.8 KiB
TypeScript
84 lines
2.8 KiB
TypeScript
import { RemoteCliArgumentError } from './ssh-remote-cli-argument-error'
|
|
|
|
type RemoteFlags = Map<string, string | boolean>
|
|
|
|
export function hasRemoteLifecycleRejection(result: unknown): boolean {
|
|
if (!result || typeof result !== 'object') {
|
|
return false
|
|
}
|
|
const lifecycle = (result as { lifecycle?: unknown }).lifecycle
|
|
return (
|
|
lifecycle !== null &&
|
|
typeof lifecycle === 'object' &&
|
|
(lifecycle as { action?: unknown }).action === 'rejected'
|
|
)
|
|
}
|
|
|
|
export function resolveRemoteOrchestrationSender(
|
|
flags: RemoteFlags,
|
|
env: Record<string, string>,
|
|
type: string | undefined
|
|
): string {
|
|
const explicit = optionalString(flags, 'from')
|
|
const envHandle = env.ORCA_TERMINAL_HANDLE || undefined
|
|
if ((type === 'worker_done' || type === 'heartbeat') && !explicit && !envHandle) {
|
|
// Why: the fallback must not turn missing remote identity into the
|
|
// synthetic "unknown" handle for lifecycle authority decisions.
|
|
throw new RemoteCliArgumentError(
|
|
'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.'
|
|
)
|
|
}
|
|
return explicit ?? envHandle ?? 'unknown'
|
|
}
|
|
|
|
export function getRemoteOrchestrationPayload(flags: RemoteFlags): string | undefined {
|
|
const rawPayload = optionalString(flags, 'payload')
|
|
const taskId = optionalString(flags, 'task-id')
|
|
const dispatchId = optionalString(flags, 'dispatch-id')
|
|
const filesModified = optionalString(flags, 'files-modified')
|
|
const reportPath = optionalString(flags, 'report-path')
|
|
const phase = optionalString(flags, 'phase')
|
|
const hasStructuredPayload = [taskId, dispatchId, filesModified, reportPath, phase].some(
|
|
(value) => value !== undefined
|
|
)
|
|
if (!hasStructuredPayload) {
|
|
return rawPayload
|
|
}
|
|
if (rawPayload !== undefined) {
|
|
throw new RemoteCliArgumentError(
|
|
'invalid_argument',
|
|
'Use either --payload or structured payload flags, not both.'
|
|
)
|
|
}
|
|
|
|
// Why: the fallback receives the same preamble commands as the full CLI;
|
|
// preserving these flags keeps lifecycle payloads valid over broken installs.
|
|
const payload: Record<string, string | string[]> = {}
|
|
if (taskId) {
|
|
payload.taskId = taskId
|
|
}
|
|
if (dispatchId) {
|
|
payload.dispatchId = dispatchId
|
|
}
|
|
if (filesModified) {
|
|
payload.filesModified = filesModified
|
|
.split(',')
|
|
.map((file) => file.trim())
|
|
.filter(Boolean)
|
|
}
|
|
if (reportPath) {
|
|
payload.reportPath = reportPath
|
|
}
|
|
if (phase) {
|
|
payload.phase = phase
|
|
}
|
|
return JSON.stringify(payload)
|
|
}
|
|
|
|
function optionalString(flags: RemoteFlags, name: string): string | undefined {
|
|
const value = flags.get(name)
|
|
return typeof value === 'string' && value.length > 0 ? value : undefined
|
|
}
|