Files
orca/src/main/ssh/ssh-remote-orchestration-send.ts
T
Jinjing 52388c1ab7 Fix worker done pane identity (#8473)
* 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.
2026-07-12 22:45:33 -07:00

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
}