mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 08:03:12 +00:00
fix(orchestration): restore terminal identity on fleet status rows
The hook server publishes status rows carrying only a pane key, but the fleet matcher compares terminal handle and dispatch id. Every local worker therefore failed identity and projected unverifiable/missing_status while running. Apply the same enrichment the renderer IPC boundary already applies, so the shared matcher sees the identity it was written against.
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
} from './orchestration/cli-command'
|
||||
import { getAppEnvironment } from '../../shared/app-environment'
|
||||
import type { AgentStatusIpcPayload } from '../../shared/agent-status-ipc-payload'
|
||||
import { enrichAgentStatusIpcPayload } from '../ipc/agent-status-ipc-boundary'
|
||||
|
||||
export class OrcaRuntimeWithGetOrchestrationDispatchAuthority extends OrcaRuntimeWithVerifyOrchestrationCompatibilityCaller {
|
||||
/** Every pane key this PTY could be addressed by, including restored receipts. */
|
||||
@@ -195,7 +196,12 @@ export class OrcaRuntimeWithGetOrchestrationDispatchAuthority extends OrcaRuntim
|
||||
|
||||
/** Push-fed hook rows for local read-only fleet projection; callers must redact payload text. */
|
||||
getOrchestrationFleetAgentStatusSnapshot(): readonly AgentStatusIpcPayload[] {
|
||||
return this.getAgentStatusSnapshotFn?.() ?? []
|
||||
// Why: hook rows carry only a pane key, but the fleet matcher compares terminal handle and
|
||||
// dispatch id. Without the same enrichment the renderer boundary applies, every local worker
|
||||
// fails identity and projects `missing_status` while it is demonstrably running.
|
||||
return (this.getAgentStatusSnapshotFn?.() ?? []).map((entry) =>
|
||||
enrichAgentStatusIpcPayload(entry, this)
|
||||
)
|
||||
}
|
||||
|
||||
getTerminalOrchestrationCliCommand(handle: string): OrchestrationCliCommand {
|
||||
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { OrcaRuntimeService } from '../../../../orca-runtime'
|
||||
import type { OrchestrationDb } from '../../../../orchestration/db'
|
||||
import { OrcaRuntimeWithGetOrchestrationDispatchAuthority } from '../../../../orca-runtime-get-orchestration-dispatch-authority'
|
||||
import { toAgentStatusIpcPayload } from '../../../../../agent-hooks/server/server-status-identity'
|
||||
import type { EnrichedAgentHookEventPayload } from '../../../../../agent-hooks/server/server-types'
|
||||
import type { AgentStatusOrchestrationContext } from '../../../../../../shared/agent-status-types'
|
||||
import { projectFleetWorkerPage } from './worker-observation'
|
||||
|
||||
const PANE_KEY = 'tab-fleet:leaf-fleet'
|
||||
const TERMINAL_HANDLE = 'term_fleet'
|
||||
const DISPATCH_ID = 'disp-fleet'
|
||||
/** `projectFleetWorkerPage` stamps `Date.now()` itself, so the fixture must ride the wall clock. */
|
||||
const observedAt = (): number => Date.now() - 1_000
|
||||
|
||||
/** Exactly what `agentHookServer.getStatusSnapshot()` publishes: pane identity, no terminal identity. */
|
||||
function hookRowAsPublished(): ReturnType<typeof toAgentStatusIpcPayload> {
|
||||
return toAgentStatusIpcPayload({
|
||||
paneKey: PANE_KEY,
|
||||
tabId: 'tab-fleet',
|
||||
worktreeId: 'wt-fleet',
|
||||
connectionId: null,
|
||||
receivedAt: observedAt(),
|
||||
stateStartedAt: observedAt(),
|
||||
payload: { state: 'working', agentType: 'claude' }
|
||||
} as unknown as EnrichedAgentHookEventPayload)
|
||||
}
|
||||
|
||||
function createRuntime(args: {
|
||||
handleForPane?: string
|
||||
orchestration?: AgentStatusOrchestrationContext
|
||||
}): OrcaRuntimeService {
|
||||
const host = {
|
||||
getAgentStatusSnapshotFn: () => [hookRowAsPublished()],
|
||||
getAgentStatusTerminalHandleForPaneKey: (paneKey: string) =>
|
||||
paneKey === PANE_KEY ? args.handleForPane : undefined,
|
||||
getAgentStatusOrchestrationContextForPaneKey: (paneKey: string) =>
|
||||
paneKey === PANE_KEY ? args.orchestration : undefined
|
||||
}
|
||||
return {
|
||||
// Drive the shipping accessor, not a copy of it: the identity loss was in this method.
|
||||
getOrchestrationFleetAgentStatusSnapshot: () =>
|
||||
OrcaRuntimeWithGetOrchestrationDispatchAuthority.prototype.getOrchestrationFleetAgentStatusSnapshot.call(
|
||||
host as never
|
||||
)
|
||||
} as unknown as OrcaRuntimeService
|
||||
}
|
||||
|
||||
function createDb(): OrchestrationDb {
|
||||
return {
|
||||
listWorkerTerminalResources: () => [
|
||||
{
|
||||
dispatchId: DISPATCH_ID,
|
||||
taskId: 'task-fleet',
|
||||
runId: 'run-fleet',
|
||||
parentTaskId: 'task-parent',
|
||||
workerState: 'ready',
|
||||
dispatchStatus: 'dispatched',
|
||||
workerStage: 'input_accepted',
|
||||
agentTerminalHandle: TERMINAL_HANDLE,
|
||||
paneKey: PANE_KEY,
|
||||
worktreeId: 'wt-fleet',
|
||||
terminalState: 'active',
|
||||
pendingInput: false,
|
||||
pendingApproval: false,
|
||||
terminationReason: null,
|
||||
resource: {
|
||||
id: 'res-fleet',
|
||||
owner_dispatch_id: DISPATCH_ID,
|
||||
worktree_id: 'wt-fleet',
|
||||
pane_key: PANE_KEY,
|
||||
process_incarnation: 'pty-fleet:inc-1',
|
||||
endpoint_id: null,
|
||||
endpoint_incarnation: null,
|
||||
host_scope: JSON.stringify({ kind: 'local', hostId: 'local' }),
|
||||
ownership_state: 'owned',
|
||||
release_state: 'none',
|
||||
updated_at: new Date().toISOString()
|
||||
},
|
||||
createdAt: new Date(Date.now() - 60_000).toISOString(),
|
||||
databaseId: 1
|
||||
}
|
||||
],
|
||||
getWorkerAttentionFactsForDispatches: () => new Map()
|
||||
} as unknown as OrchestrationDb
|
||||
}
|
||||
|
||||
describe('local fleet liveness from a hook row that carries only a pane key', () => {
|
||||
it('publishes hook rows without terminal identity', () => {
|
||||
// Guards the premise: the fix must add identity, not assume the hook server already does.
|
||||
expect(hookRowAsPublished().terminalHandle).toBeUndefined()
|
||||
expect(hookRowAsPublished().orchestration).toBeUndefined()
|
||||
})
|
||||
|
||||
it('reads live for a running local worker whose pane still owns its handle', () => {
|
||||
const page = projectFleetWorkerPage(
|
||||
createRuntime({ handleForPane: TERMINAL_HANDLE }),
|
||||
createDb(),
|
||||
DISPATCH_ID
|
||||
)
|
||||
|
||||
expect(page?.workers[0]).toMatchObject({
|
||||
liveness: { verdict: 'live', source: 'agent_status' },
|
||||
evidence: { liveStatus: 'fresh' },
|
||||
stage: { activity: 'working' },
|
||||
nextAction: { kind: 'none' },
|
||||
attention: { requiresAction: false }
|
||||
})
|
||||
})
|
||||
|
||||
it('carries the dispatch context the renderer boundary attaches', () => {
|
||||
const page = projectFleetWorkerPage(
|
||||
createRuntime({
|
||||
handleForPane: TERMINAL_HANDLE,
|
||||
orchestration: { dispatchId: DISPATCH_ID } as AgentStatusOrchestrationContext
|
||||
}),
|
||||
createDb(),
|
||||
DISPATCH_ID
|
||||
)
|
||||
|
||||
expect(page?.workers[0]?.liveness.verdict).toBe('live')
|
||||
})
|
||||
|
||||
it('refuses a pane whose handle now belongs to another terminal', () => {
|
||||
const page = projectFleetWorkerPage(
|
||||
createRuntime({ handleForPane: 'term_reused' }),
|
||||
createDb(),
|
||||
DISPATCH_ID
|
||||
)
|
||||
|
||||
expect(page?.workers[0]).toMatchObject({
|
||||
liveness: { verdict: 'unverifiable', reason: 'missing_status' },
|
||||
evidence: { liveStatus: 'unavailable' }
|
||||
})
|
||||
})
|
||||
|
||||
it('refuses a pane whose handle now belongs to another dispatch', () => {
|
||||
const page = projectFleetWorkerPage(
|
||||
createRuntime({
|
||||
handleForPane: TERMINAL_HANDLE,
|
||||
orchestration: { dispatchId: 'disp-other' } as AgentStatusOrchestrationContext
|
||||
}),
|
||||
createDb(),
|
||||
DISPATCH_ID
|
||||
)
|
||||
|
||||
expect(page?.workers[0]?.liveness).toMatchObject({
|
||||
verdict: 'unverifiable',
|
||||
reason: 'missing_status'
|
||||
})
|
||||
})
|
||||
|
||||
it('refuses a pane that no longer resolves to a terminal', () => {
|
||||
const page = projectFleetWorkerPage(createRuntime({}), createDb(), DISPATCH_ID)
|
||||
|
||||
expect(page?.workers[0]?.liveness).toMatchObject({
|
||||
verdict: 'unverifiable',
|
||||
reason: 'missing_status'
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user