fix: preserve relay reconciliation diagnostics

This commit is contained in:
Brennan Benson
2026-08-27 06:25:13 -07:00
parent ca46080c8c
commit a93bfd06c3
6 changed files with 152 additions and 6 deletions
@@ -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()
}
})
})
+4 -2
View File
@@ -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()
})
+2
View File
@@ -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:
+58
View File
@@ -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()
})
})
+8 -4
View File
@@ -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