mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 08:03:58 +00:00
fix(agent-status): preserve latest Codex parent lifecycle
This commit is contained in:
@@ -210,7 +210,10 @@ describe('AgentHookServer Codex subagent transcript polling', () => {
|
||||
dirs.push(dir)
|
||||
const parentPath = join(dir, 'rollout-parent.jsonl')
|
||||
const childPath = join(dir, `rollout-child-${CHILD_ID}.jsonl`)
|
||||
writeFileSync(parentPath, spawnLine(CHILD_ID, '/root/restart_repro'))
|
||||
writeFileSync(
|
||||
parentPath,
|
||||
`${line({ type: 'event_msg', payload: { type: 'task_started' } })}${line({ type: 'event_msg', payload: { type: 'task_complete' } })}${line({ type: 'event_msg', payload: { type: 'task_started' } })}${spawnLine(CHILD_ID, '/root/restart_repro')}`
|
||||
)
|
||||
writeFileSync(childPath, line({ type: 'event_msg', payload: { type: 'task_started' } }))
|
||||
const post = async (server: AgentHookServer): Promise<void> => {
|
||||
const env = server.buildPtyEnv()
|
||||
|
||||
@@ -1726,7 +1726,11 @@ export class AgentHookServer {
|
||||
const terminal = transcript.parentTerminalObserved === true
|
||||
const nextPayload = {
|
||||
...current.payload,
|
||||
...(terminal ? { state: 'done' as const } : {}),
|
||||
...(terminal
|
||||
? { state: 'done' as const }
|
||||
: transcript.parentTerminalObserved === false
|
||||
? { state: 'working' as const }
|
||||
: {}),
|
||||
...(subagents ? { subagents } : { subagents: undefined })
|
||||
}
|
||||
let latest = current
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import type { AgentHookRelayEnvelope } from '../shared/agent-hook-relay'
|
||||
|
||||
export type RelayHookForward = (envelope: AgentHookRelayEnvelope) => void
|
||||
export type RelayHookServerOptions = {
|
||||
endpointDir?: string
|
||||
env?: string
|
||||
token?: string
|
||||
preferredPort?: number
|
||||
forward: RelayHookForward
|
||||
}
|
||||
export type RelayHookServerStartOptions = { publishEndpoint?: boolean }
|
||||
@@ -11,6 +11,11 @@ import * as agentHookListener from '../shared/agent-hook-listener/grok-result-di
|
||||
|
||||
const LEAF_ID = '11111111-1111-4111-8111-111111111111'
|
||||
const PANE_KEY = makePaneKey('tab-1', LEAF_ID)
|
||||
const MALFORMED_LEAF_IDS = [
|
||||
'22222222-2222-4222-8222-222222222222',
|
||||
'33333333-3333-4333-8333-333333333333',
|
||||
'44444444-4444-4444-8444-444444444444'
|
||||
] as const
|
||||
|
||||
type RelayServerInternals = {
|
||||
state: { lastStatusByPaneKey: Map<string, unknown> }
|
||||
@@ -372,17 +377,20 @@ describe('RelayAgentHookServer', () => {
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
entries: [
|
||||
{ event: { paneKey: PANE_KEY }, meta: { source: 'claude' } },
|
||||
{
|
||||
event: { paneKey: makePaneKey('tab-1', MALFORMED_LEAF_IDS[0]) },
|
||||
meta: { source: 'claude' }
|
||||
},
|
||||
{
|
||||
event: {
|
||||
paneKey: PANE_KEY,
|
||||
paneKey: makePaneKey('tab-1', MALFORMED_LEAF_IDS[1]),
|
||||
payload: { state: 'working', prompt: 'missing agent type' }
|
||||
},
|
||||
meta: { source: 'claude' }
|
||||
},
|
||||
{
|
||||
event: {
|
||||
paneKey: PANE_KEY,
|
||||
paneKey: makePaneKey('tab-1', MALFORMED_LEAF_IDS[2]),
|
||||
payload: null
|
||||
},
|
||||
meta: { source: 'claude' }
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
|
||||
import { createHookListenerState } from '../shared/agent-hook-listener/listener-state'
|
||||
import type { AgentHookEventPayload } from '../shared/agent-hook-listener/listener-event'
|
||||
import { applyRelayHookEvent } from './agent-hook-status-cache'
|
||||
import { applyRelayHookEvent, reconcileRelayCodexEvent } from './agent-hook-status-cache'
|
||||
import type { AgentHookRelayEnvelope } from '../shared/agent-hook-relay'
|
||||
|
||||
const PANE_KEY = 'tab-1:11111111-1111-4111-8111-111111111111'
|
||||
@@ -22,6 +25,39 @@ function event(
|
||||
}
|
||||
|
||||
describe('relay agent-hook status cache', () => {
|
||||
it('restores working when a newer parent turn follows a historical completion', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'relay-hook-status-cache-'))
|
||||
try {
|
||||
const transcriptPath = join(dir, 'rollout-parent.jsonl')
|
||||
writeFileSync(
|
||||
transcriptPath,
|
||||
`${JSON.stringify({ type: 'event_msg', payload: { type: 'task_complete' } })}\n${JSON.stringify({ type: 'event_msg', payload: { type: 'task_started' } })}\n`
|
||||
)
|
||||
const state = createHookListenerState()
|
||||
|
||||
const reconciled = reconcileRelayCodexEvent(state, {
|
||||
...event('PostToolUse'),
|
||||
providerSession: { key: 'session_id', id: 'session-1', transcriptPath },
|
||||
payload: {
|
||||
state: 'done',
|
||||
prompt: 'prompt',
|
||||
agentType: 'codex',
|
||||
subagents: [
|
||||
{
|
||||
id: '019fa65f-3144-7151-9c02-cff7a28f316f',
|
||||
state: 'working',
|
||||
startedAt: 1234
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
expect(reconciled.payload.state).toBe('working')
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('clears an inherited Codex reconciliation diagnostic at SessionStart', () => {
|
||||
const state = createHookListenerState()
|
||||
const metadata = new Map<string, { source: 'codex' }>()
|
||||
|
||||
@@ -149,7 +149,11 @@ export function reconcileRelayCodexEvent(
|
||||
const payload = {
|
||||
...event.payload,
|
||||
...(subagents ? { subagents } : { subagents: undefined }),
|
||||
...(transcript.parentTerminalObserved ? { state: 'done' as const } : {})
|
||||
...(transcript.parentTerminalObserved === true
|
||||
? { state: 'done' as const }
|
||||
: transcript.parentTerminalObserved === false
|
||||
? { state: 'working' as const }
|
||||
: {})
|
||||
}
|
||||
return [...transcript.subagents.values()].some((child) => child.unresolvedSince)
|
||||
? { ...event, payload }
|
||||
|
||||
@@ -78,6 +78,7 @@ describe('agent hook listener relay dependency boundary', () => {
|
||||
const listenerPathPrefix = resolve(sharedRoot, 'agent-hook-listener')
|
||||
const relayConsumers = [
|
||||
resolve(sharedRoot, '../relay/agent-hook-server.ts'),
|
||||
resolve(sharedRoot, '../relay/agent-hook-request.ts'),
|
||||
resolve(sharedRoot, '../relay/agent-hook-result-retry-scheduler.ts')
|
||||
]
|
||||
const pending = relayConsumers.flatMap((consumer) =>
|
||||
|
||||
@@ -23,9 +23,7 @@ type JsonlCursor = {
|
||||
|
||||
type TrackedTranscriptSubagent = JsonlCursor & {
|
||||
description?: string
|
||||
/** Latest model seen in the child's own rollout. Retained across polls
|
||||
* because the cursor is incremental: `turn_context` is emitted once per
|
||||
* turn, so a later read usually carries no model at all. */
|
||||
/** Retained because incremental reads usually omit the earlier `turn_context`. */
|
||||
model?: string
|
||||
startedAt: number
|
||||
unresolvedSince?: number
|
||||
@@ -203,9 +201,7 @@ function readActivity(recordValue: JsonRecord):
|
||||
}
|
||||
}
|
||||
|
||||
/** Latest model from the child's own `turn_context` records. A child can be
|
||||
* launched on a different model than its parent, so this is read from the
|
||||
* child rollout rather than inherited. */
|
||||
/** Read from the child's rollout because its model can differ from the parent's. */
|
||||
function readChildModel(records: JsonRecord[]): string | undefined {
|
||||
let model: string | undefined
|
||||
for (const recordValue of records) {
|
||||
@@ -222,19 +218,15 @@ function readChildModel(records: JsonRecord[]): string | undefined {
|
||||
}
|
||||
|
||||
function childIsComplete(records: JsonRecord[]): boolean {
|
||||
let complete = false
|
||||
let lifecycle: unknown
|
||||
for (const recordValue of records) {
|
||||
if (recordValue.type !== 'event_msg') {
|
||||
continue
|
||||
}
|
||||
const payload = record(recordValue.payload)
|
||||
if (payload?.type === 'task_started') {
|
||||
complete = false
|
||||
} else if (payload?.type === 'task_complete') {
|
||||
complete = true
|
||||
const eventType =
|
||||
recordValue.type === 'event_msg' ? record(recordValue.payload)?.type : undefined
|
||||
if (eventType === 'task_started' || eventType === 'task_complete') {
|
||||
lifecycle = eventType
|
||||
}
|
||||
}
|
||||
return complete
|
||||
return lifecycle === 'task_complete'
|
||||
}
|
||||
|
||||
export function createCodexSubagentTranscriptState(): CodexSubagentTranscriptState {
|
||||
@@ -265,11 +257,14 @@ export function reconcileCodexSubagentTranscript(
|
||||
}
|
||||
state.parent = { filePath: normalizedPath, offset: 0, carry: '' }
|
||||
state.subagents.clear()
|
||||
state.parentTerminalObserved = false
|
||||
state.parentTerminalObserved = undefined
|
||||
}
|
||||
for (const recordValue of readJsonlCursor(state.parent) ?? []) {
|
||||
if (recordValue.type === 'event_msg' && record(recordValue.payload)?.type === 'task_complete') {
|
||||
state.parentTerminalObserved = true
|
||||
if (recordValue.type === 'event_msg') {
|
||||
const eventType = record(recordValue.payload)?.type
|
||||
if (eventType === 'task_started' || eventType === 'task_complete') {
|
||||
state.parentTerminalObserved = eventType === 'task_complete'
|
||||
}
|
||||
}
|
||||
const activity = readActivity(recordValue)
|
||||
if (!activity) {
|
||||
|
||||
Reference in New Issue
Block a user