mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 16:02:11 +00:00
* fix(agent-status): track Codex rollout subagents * fix(agent-status): resolve cross-day Codex child rollouts and unblock CI gate Codex files each rollout under its own local start date, so a session that runs past midnight spawns children into a sibling day directory. Scanning only the parent's directory left 13% of real subagent spawns (48/371 across local rollouts) permanently unresolved, which pinned a phantom "working" row and re-ran readdirSync every poll tick forever. Resolve the child's own day directory from occurred_at_ms, and time-box a child whose rollout stays unreadable so a deleted or never-written file can't leak a working row. Also make the hook HTTP handler return void: the changed-code quality gate keys findings by span overlap, so this PR's added line inside the pre-existing async createServer callback resurfaced no-misused-promises as a new finding. Tests cover cross-day resolution, grace-period retirement, and that the poll re-arms across successive roster changes (the prior tests passed even when the poll died after its first change). * fix(agent-status): keep the Codex subagent poll alive across nested hooks A nested non-codex CLI inherits its parent's ORCA_PANE_KEY, so its hook POST reached scheduleCodexSubagentPoll and tore the timer down before the source guard, silently ending polling while a rollout child was still live.
208 lines
7.3 KiB
TypeScript
208 lines
7.3 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { appendFileSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
|
|
import { AgentHookServer } from './server'
|
|
import { makePaneKey } from '../../shared/stable-pane-id'
|
|
|
|
const PANE_KEY = makePaneKey('tab-1', '11111111-1111-4111-8111-111111111111')
|
|
const CHILD_ID = '019fa65f-3144-7151-9c02-cff7a28f316f'
|
|
const SECOND_CHILD_ID = '019fa65f-3144-7151-9c02-cff7a28f3170'
|
|
|
|
function line(record: unknown): string {
|
|
return `${JSON.stringify(record)}\n`
|
|
}
|
|
|
|
function spawnLine(threadId: string, agentPath: string): string {
|
|
return line({
|
|
type: 'event_msg',
|
|
payload: {
|
|
type: 'sub_agent_activity',
|
|
occurred_at_ms: 1234,
|
|
agent_thread_id: threadId,
|
|
agent_path: agentPath,
|
|
kind: 'started'
|
|
}
|
|
})
|
|
}
|
|
|
|
describe('AgentHookServer Codex subagent transcript polling', () => {
|
|
const dirs: string[] = []
|
|
|
|
afterEach(() => {
|
|
for (const dir of dirs) {
|
|
rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
dirs.length = 0
|
|
})
|
|
|
|
it('publishes rollout-only children and removes them after their task completes', async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'agent-hook-codex-subagent-'))
|
|
dirs.push(dir)
|
|
const parentPath = join(dir, 'rollout-parent.jsonl')
|
|
const childPath = join(dir, `rollout-child-${CHILD_ID}.jsonl`)
|
|
writeFileSync(
|
|
parentPath,
|
|
line({
|
|
type: 'event_msg',
|
|
payload: {
|
|
type: 'sub_agent_activity',
|
|
occurred_at_ms: 1234,
|
|
agent_thread_id: CHILD_ID,
|
|
agent_path: '/root/pr_review',
|
|
kind: 'started'
|
|
}
|
|
})
|
|
)
|
|
writeFileSync(childPath, line({ type: 'event_msg', payload: { type: 'task_started' } }))
|
|
const server = new AgentHookServer()
|
|
await server.start({ env: 'production' })
|
|
try {
|
|
const env = server.buildPtyEnv()
|
|
const response = 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'
|
|
}
|
|
})
|
|
})
|
|
|
|
expect(response.status).toBe(204)
|
|
expect(server.getStatusSnapshot()[0]?.subagents).toEqual([
|
|
expect.objectContaining({ id: CHILD_ID, description: '/root/pr_review' })
|
|
])
|
|
|
|
appendFileSync(childPath, line({ type: 'event_msg', payload: { type: 'task_complete' } }))
|
|
await vi.waitFor(
|
|
() => {
|
|
expect(server.getStatusSnapshot()[0]?.subagents).toBeUndefined()
|
|
},
|
|
{ timeout: 2_000, interval: 50 }
|
|
)
|
|
} finally {
|
|
server.stop()
|
|
}
|
|
})
|
|
|
|
// Why: the poll re-arms off the object it just stored; if that identity ever drifts it stops after the first change.
|
|
it('keeps polling across successive roster changes', async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'agent-hook-codex-subagent-'))
|
|
dirs.push(dir)
|
|
const parentPath = join(dir, 'rollout-parent.jsonl')
|
|
const childPath = join(dir, `rollout-child-${CHILD_ID}.jsonl`)
|
|
const secondChildPath = join(dir, `rollout-child-${SECOND_CHILD_ID}.jsonl`)
|
|
const started = line({ type: 'event_msg', payload: { type: 'task_started' } })
|
|
writeFileSync(parentPath, spawnLine(CHILD_ID, '/root/pr_review'))
|
|
writeFileSync(childPath, started)
|
|
writeFileSync(secondChildPath, started)
|
|
const server = new AgentHookServer()
|
|
await server.start({ env: 'production' })
|
|
try {
|
|
const env = server.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'
|
|
}
|
|
})
|
|
})
|
|
expect(server.getStatusSnapshot()[0]?.subagents).toHaveLength(1)
|
|
|
|
appendFileSync(parentPath, spawnLine(SECOND_CHILD_ID, '/root/perf_audit'))
|
|
await vi.waitFor(
|
|
() => {
|
|
expect(server.getStatusSnapshot()[0]?.subagents).toHaveLength(2)
|
|
},
|
|
{ timeout: 3_000, interval: 50 }
|
|
)
|
|
|
|
const complete = line({ type: 'event_msg', payload: { type: 'task_complete' } })
|
|
appendFileSync(childPath, complete)
|
|
appendFileSync(secondChildPath, complete)
|
|
await vi.waitFor(
|
|
() => {
|
|
expect(server.getStatusSnapshot()[0]?.subagents).toBeUndefined()
|
|
},
|
|
{ timeout: 3_000, interval: 50 }
|
|
)
|
|
} finally {
|
|
server.stop()
|
|
}
|
|
})
|
|
|
|
// Why: a nested non-codex CLI inherits the pane's ORCA_PANE_KEY, so its hook must not tear down the codex poll.
|
|
it('keeps polling when a nested non-codex hook lands on the same pane', async () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'agent-hook-codex-subagent-'))
|
|
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/pr_review'))
|
|
writeFileSync(childPath, line({ type: 'event_msg', payload: { type: 'task_started' } }))
|
|
const server = new AgentHookServer()
|
|
await server.start({ env: 'production' })
|
|
try {
|
|
const env = server.buildPtyEnv()
|
|
const post = (path: string, payload: unknown): Promise<Response> =>
|
|
fetch(`http://127.0.0.1:${env.ORCA_AGENT_HOOK_PORT}${path}`, {
|
|
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 })
|
|
})
|
|
|
|
await post('/hook/codex', {
|
|
hook_event_name: 'PostToolUse',
|
|
session_id: 'root-session',
|
|
transcript_path: parentPath,
|
|
tool_name: 'collaborationspawn_agent'
|
|
})
|
|
expect(server.getStatusSnapshot()[0]?.subagents).toHaveLength(1)
|
|
|
|
const nested = await post('/hook/copilot', {
|
|
hook_event_name: 'Stop',
|
|
session_id: 'nested-session',
|
|
transcript_path: join(dir, 'nested-copilot-transcript.jsonl')
|
|
})
|
|
expect(nested.status).toBe(204)
|
|
// The nested completion is suppressed, so the pane is still the same live codex turn.
|
|
expect(server.getStatusSnapshot()[0]?.agentType).toBe('codex')
|
|
expect(server.getStatusSnapshot()[0]?.subagents).toHaveLength(1)
|
|
|
|
appendFileSync(childPath, line({ type: 'event_msg', payload: { type: 'task_complete' } }))
|
|
await vi.waitFor(
|
|
() => {
|
|
expect(server.getStatusSnapshot()[0]?.subagents).toBeUndefined()
|
|
},
|
|
{ timeout: 3_000, interval: 50 }
|
|
)
|
|
} finally {
|
|
server.stop()
|
|
}
|
|
})
|
|
})
|