mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 16:03:49 +00:00
fix: preserve relay reconciliation diagnostics
This commit is contained in:
@@ -314,4 +314,67 @@ describe('AgentHookServer Codex subagent transcript polling', () => {
|
||||
first.stop()
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps the final reconciled roster when marking an unresolved child unverifiable', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'agent-hook-codex-final-reconcile-'))
|
||||
dirs.push(dir)
|
||||
const parentPath = join(dir, 'rollout-parent.jsonl')
|
||||
const completedLaterPath = join(dir, `rollout-child-${CHILD_ID}.jsonl`)
|
||||
writeFileSync(
|
||||
parentPath,
|
||||
`${spawnLine(CHILD_ID, '/root/completes-late')}${spawnLine(SECOND_CHILD_ID, '/root/missing')}`
|
||||
)
|
||||
writeFileSync(
|
||||
completedLaterPath,
|
||||
line({ type: 'event_msg', payload: { type: 'task_started' } })
|
||||
)
|
||||
const first = new AgentHookServer()
|
||||
let server: AgentHookServer | undefined
|
||||
await first.start({ env: 'production', userDataPath: dir })
|
||||
try {
|
||||
const env = first.buildPtyEnv()
|
||||
await fetch(`http://127.0.0.1:${env.ORCA_AGENT_HOOK_PORT}/hook/codex`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Orca-Agent-Hook-Token': env.ORCA_AGENT_HOOK_TOKEN
|
||||
},
|
||||
body: JSON.stringify({
|
||||
paneKey: PANE_KEY,
|
||||
tabId: 'tab-1',
|
||||
worktreeId: 'wt-1',
|
||||
payload: {
|
||||
hook_event_name: 'PostToolUse',
|
||||
session_id: 'root-session',
|
||||
transcript_path: parentPath,
|
||||
tool_name: 'collaborationspawn_agent'
|
||||
}
|
||||
})
|
||||
})
|
||||
first.stop()
|
||||
server = new AgentHookServer()
|
||||
await server.start({ env: 'production', userDataPath: dir })
|
||||
setTimeout(() => {
|
||||
appendFileSync(
|
||||
completedLaterPath,
|
||||
line({ type: 'event_msg', payload: { type: 'task_complete' } })
|
||||
)
|
||||
}, 3_500).unref()
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
const status = server?.getStatusSnapshot()[0]
|
||||
expect(status?.reconcileDiagnostic).toEqual({
|
||||
kind: 'unverifiable',
|
||||
reason: 'transcript-unreadable',
|
||||
observedAt: expect.any(Number)
|
||||
})
|
||||
expect(status?.subagents).toEqual([expect.objectContaining({ id: SECOND_CHILD_ID })])
|
||||
},
|
||||
{ timeout: 7_000, interval: 100 }
|
||||
)
|
||||
} finally {
|
||||
server?.stop()
|
||||
first.stop()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1730,6 +1730,7 @@ export class AgentHookServer {
|
||||
...(terminal ? { state: 'done' as const } : {}),
|
||||
...(subagents ? { subagents } : { subagents: undefined })
|
||||
}
|
||||
let latest = current
|
||||
if (
|
||||
JSON.stringify(nextPayload) !== JSON.stringify(current.payload) ||
|
||||
(terminal && current.restoredUnconfirmed)
|
||||
@@ -1742,6 +1743,7 @@ export class AgentHookServer {
|
||||
}
|
||||
this.state.lastStatusByPaneKey.set(paneKey, next)
|
||||
original = next
|
||||
latest = next
|
||||
this.scheduleStatusPersist()
|
||||
this.notifyStatusChangeListeners()
|
||||
this.emitEnrichedStatus(next)
|
||||
@@ -1751,10 +1753,10 @@ export class AgentHookServer {
|
||||
const unreadable = [...transcript.subagents.values()].some((child) => child.unresolvedSince)
|
||||
if (
|
||||
unreadable &&
|
||||
(current.reconcileDiagnostic === undefined || current.reconcileDiagnostic === null)
|
||||
(latest.reconcileDiagnostic === undefined || latest.reconcileDiagnostic === null)
|
||||
) {
|
||||
const diagnostic: EnrichedAgentHookEventPayload = {
|
||||
...current,
|
||||
...latest,
|
||||
reconcileDiagnostic: {
|
||||
kind: 'unverifiable',
|
||||
reason: 'transcript-unreadable',
|
||||
|
||||
@@ -597,6 +597,11 @@ describe('SshRelaySession agent hooks over a fake relay transport', () => {
|
||||
toolAgentType: 'Review',
|
||||
claudeRunningNonAgentTask: true,
|
||||
providerSessionOnly: true,
|
||||
reconcileDiagnostic: {
|
||||
kind: 'unverifiable',
|
||||
reason: 'remote-contact-lost',
|
||||
observedAt: 1234
|
||||
},
|
||||
providerSession: {
|
||||
key: 'session_id',
|
||||
id: 'ssh-relay-session-1',
|
||||
@@ -625,11 +630,23 @@ describe('SshRelaySession agent hooks over a fake relay transport', () => {
|
||||
key: 'session_id',
|
||||
id: 'ssh-relay-session-1',
|
||||
transcriptPath: '/tmp/ssh-relay-session-1.jsonl'
|
||||
},
|
||||
reconcileDiagnostic: {
|
||||
kind: 'unverifiable',
|
||||
reason: 'remote-contact-lost',
|
||||
observedAt: 1234
|
||||
}
|
||||
}),
|
||||
'conn-hook-metadata'
|
||||
)
|
||||
)
|
||||
expect(agentHookServer.getStatusSnapshot()[0]).toMatchObject({
|
||||
reconcileDiagnostic: {
|
||||
kind: 'unverifiable',
|
||||
reason: 'remote-contact-lost',
|
||||
observedAt: 1234
|
||||
}
|
||||
})
|
||||
ingestSpy.mockRestore()
|
||||
})
|
||||
|
||||
|
||||
@@ -1507,6 +1507,7 @@ export class SshRelaySession {
|
||||
isReplay?: unknown
|
||||
providerSession?: unknown
|
||||
providerSessionOnly?: unknown
|
||||
reconcileDiagnostic?: unknown
|
||||
shedFields?: unknown
|
||||
claudeRunningNonAgentTask?: unknown
|
||||
payload?: unknown
|
||||
@@ -1542,6 +1543,7 @@ export class SshRelaySession {
|
||||
isReplay: envelope.isReplay === true ? true : undefined,
|
||||
providerSession: envelope.providerSession,
|
||||
providerSessionOnly: envelope.providerSessionOnly === true ? true : undefined,
|
||||
reconcileDiagnostic: envelope.reconcileDiagnostic,
|
||||
// Why: names the fields the relay dropped to fit the frame; ingestRemote restores them.
|
||||
shedFields: envelope.shedFields,
|
||||
claudeRunningNonAgentTask:
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { createHookListenerState, type AgentHookEventPayload } from '../shared/agent-hook-listener'
|
||||
import { applyRelayHookEvent } from './agent-hook-status-cache'
|
||||
import type { AgentHookRelayEnvelope } from '../shared/agent-hook-relay'
|
||||
|
||||
const PANE_KEY = 'tab-1:11111111-1111-4111-8111-111111111111'
|
||||
|
||||
function event(
|
||||
hookEventName: string,
|
||||
reconcileDiagnostic?: AgentHookEventPayload['reconcileDiagnostic']
|
||||
): AgentHookEventPayload {
|
||||
return {
|
||||
paneKey: PANE_KEY,
|
||||
source: 'codex',
|
||||
connectionId: null,
|
||||
hookEventName,
|
||||
...(reconcileDiagnostic !== undefined ? { reconcileDiagnostic } : {}),
|
||||
payload: { state: 'working', prompt: 'prompt', agentType: 'codex' }
|
||||
}
|
||||
}
|
||||
|
||||
describe('relay agent-hook status cache', () => {
|
||||
it('clears an inherited Codex reconciliation diagnostic at SessionStart', () => {
|
||||
const state = createHookListenerState()
|
||||
const metadata = new Map<string, { source: 'codex' }>()
|
||||
const forward = vi.fn<(envelope: AgentHookRelayEnvelope) => void>()
|
||||
const options = {
|
||||
state,
|
||||
previous: undefined,
|
||||
source: 'codex' as const,
|
||||
metadata,
|
||||
persist: vi.fn(),
|
||||
clearPaneState: vi.fn(),
|
||||
forward
|
||||
}
|
||||
|
||||
applyRelayHookEvent({
|
||||
...options,
|
||||
event: event('PostToolUse', {
|
||||
kind: 'unverifiable',
|
||||
reason: 'transcript-unreadable',
|
||||
observedAt: 100
|
||||
})
|
||||
})
|
||||
const previous = state.lastStatusByPaneKey.get(PANE_KEY)
|
||||
expect(previous?.reconcileDiagnostic).toBeDefined()
|
||||
|
||||
applyRelayHookEvent({
|
||||
...options,
|
||||
previous,
|
||||
event: event('SessionStart')
|
||||
})
|
||||
|
||||
expect(state.lastStatusByPaneKey.get(PANE_KEY)?.reconcileDiagnostic).toBeNull()
|
||||
expect(forward.mock.calls.at(-1)?.[0].reconcileDiagnostic).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -34,10 +34,14 @@ export function applyRelayHookEvent(options: {
|
||||
clearPaneState: (paneKey: string) => void
|
||||
forward: (envelope: AgentHookRelayEnvelope) => void
|
||||
}): void {
|
||||
const cachedEvent = resolveCachedClaudeCompactOwnership(options.previous, options.event)
|
||||
options.state.lastStatusByPaneKey.delete(options.event.paneKey)
|
||||
options.state.lastStatusByPaneKey.set(options.event.paneKey, cachedEvent)
|
||||
options.metadata.set(options.event.paneKey, {
|
||||
const diagnosticAwareEvent =
|
||||
options.event.payload.agentType === 'codex' && options.event.hookEventName === 'SessionStart'
|
||||
? { ...options.event, reconcileDiagnostic: null }
|
||||
: options.event
|
||||
const cachedEvent = resolveCachedClaudeCompactOwnership(options.previous, diagnosticAwareEvent)
|
||||
options.state.lastStatusByPaneKey.delete(diagnosticAwareEvent.paneKey)
|
||||
options.state.lastStatusByPaneKey.set(diagnosticAwareEvent.paneKey, cachedEvent)
|
||||
options.metadata.set(diagnosticAwareEvent.paneKey, {
|
||||
source: options.source,
|
||||
env: options.env,
|
||||
version: options.version
|
||||
|
||||
Reference in New Issue
Block a user