mirror of
https://github.com/stablyai/orca.git
synced 2026-10-07 16:02:29 +00:00
* feat(pi): show Pi and OMP background children as rows Refs #22868 * fix(pi): derive the extension's roster cap from the host's The generated extension re-typed the 32-row limit as its own literal, so a change to AGENT_STATUS_MAX_SUBAGENTS would leave the extension posting rosters the host silently truncates on arrival. Interpolate the host constant instead, and cover the cap with a test that posts past it. * test(pi): pin the Pi cancel verdict beside a live child row Publishing a subagent roster for Pi puts its panes behind the same child-work guard Claude and Codex sit behind, so Ctrl+C beside a live child no longer settles a stopped row. The extension reports no main-agent state, so Orca cannot tell a cancelled turn from Ctrl+C at the idle prompt of a lead children alone hold open, and keeps the live row. Both halves of that are now pinned, as is the resume-placeholder guard on the model_select arm of the same code path. * fix(pi): end a run with its session and keep children across reload Pi re-runs the extension on a fresh event bus for /new, resume, fork and /reload, so the roster kept on that bus was lost and nothing ended the run the old session's children held open. * fix(pi): preserve pending completion and OMP pane ownership * test(pi): use the hook owner context type * fix(pi): retain completion timers and delivery across session boundaries * test(omp): preserve unsent completion at session boundaries * test(journal): retain provider handle import across main integration * test(startup): account for encoded Windows prompt lines * fix(pi): serialize status delivery across module reloads * test(compatibility): retain the released journal parser dependency --------- Co-authored-by: Neil <neil@stably.ai> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
140 lines
4.4 KiB
JavaScript
140 lines
4.4 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { once } from 'node:events'
|
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
import { createServer } from 'node:http'
|
|
import { createRequire } from 'node:module'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { setTimeout as delay } from 'node:timers/promises'
|
|
import { build, transform } from 'esbuild'
|
|
|
|
const scratch = await mkdtemp(join(tmpdir(), 'orca-omp-completion-'))
|
|
const received = []
|
|
let rejectCompletion = true
|
|
const server = createServer(async (request, response) => {
|
|
let body = ''
|
|
for await (const chunk of request) {
|
|
body += chunk
|
|
}
|
|
const payload = JSON.parse(body).payload
|
|
const event = payload.hook_event_name
|
|
received.push(payload)
|
|
const reject = event === 'agent_end' && rejectCompletion
|
|
if (reject) {
|
|
rejectCompletion = false
|
|
}
|
|
response.writeHead(reject ? 503 : 204)
|
|
response.end()
|
|
})
|
|
|
|
async function waitForRequests(count) {
|
|
const deadline = Date.now() + 5000
|
|
while (received.length < count && Date.now() < deadline) {
|
|
await delay(10)
|
|
}
|
|
assert.equal(received.length, count, 'Hook requests did not arrive before the deadline')
|
|
}
|
|
|
|
try {
|
|
const bundle = join(scratch, 'source.cjs')
|
|
await build({
|
|
entryPoints: ['src/main/pi/agent-status-extension-source.ts'],
|
|
bundle: true,
|
|
platform: 'node',
|
|
format: 'cjs',
|
|
outfile: bundle
|
|
})
|
|
const require = createRequire(import.meta.url)
|
|
const { getPiAgentStatusExtensionSource } = require(bundle)
|
|
const generated = await transform(getPiAgentStatusExtensionSource('omp'), {
|
|
loader: 'ts',
|
|
format: 'cjs',
|
|
target: 'node24'
|
|
})
|
|
const extension = join(scratch, 'extension.cjs')
|
|
await writeFile(extension, generated.code)
|
|
server.listen(0, '127.0.0.1')
|
|
await once(server, 'listening')
|
|
Object.assign(process.env, {
|
|
ORCA_BACKGROUND_LAUNCH: '1',
|
|
ORCA_PANE_KEY: 'completion-proof',
|
|
ORCA_TAB_ID: 'proof-tab',
|
|
ORCA_AGENT_HOOK_PORT: String(server.address().port),
|
|
ORCA_AGENT_HOOK_TOKEN: 'isolated-proof-token',
|
|
ORCA_AGENT_HOOK_ENDPOINT: '',
|
|
ORCA_PI_STATUS_OWNED: '',
|
|
WSL_DISTRO_NAME: ''
|
|
})
|
|
const handlers = new Map()
|
|
require(extension).default({ on: (event, handler) => handlers.set(event, handler) })
|
|
let sessionId = 'A'
|
|
const context = {
|
|
isIdle: () => false,
|
|
sessionManager: {
|
|
getSessionId: () => sessionId,
|
|
getSessionFile: () => join(scratch, `${sessionId}.jsonl`)
|
|
}
|
|
}
|
|
const emit = async (event, payload = {}) => {
|
|
assert.ok(handlers.has(event), `Missing lifecycle handler: ${event}`)
|
|
await handlers.get(event)(payload, context)
|
|
}
|
|
await emit('agent_start')
|
|
await waitForRequests(1)
|
|
await emit('agent_end')
|
|
await waitForRequests(3)
|
|
assert.deepEqual(
|
|
received.map((event) => event.hook_event_name),
|
|
['agent_start', 'agent_end', 'agent_end']
|
|
)
|
|
const recovered = [...received]
|
|
|
|
received.length = 0
|
|
rejectCompletion = true
|
|
await emit('agent_start')
|
|
await waitForRequests(1)
|
|
await emit('agent_end')
|
|
await waitForRequests(2)
|
|
await emit('agent_start')
|
|
await delay(600)
|
|
assert.equal(
|
|
received.filter((event) => event.hook_event_name === 'agent_end').length,
|
|
1,
|
|
'An obsolete completion retried after the next turn started'
|
|
)
|
|
|
|
for (const boundary of ['session_switch', 'session_shutdown']) {
|
|
received.length = 0
|
|
rejectCompletion = true
|
|
sessionId = 'A'
|
|
await emit('agent_start')
|
|
await waitForRequests(1)
|
|
await emit('agent_end')
|
|
await waitForRequests(2)
|
|
sessionId = 'B'
|
|
await emit(boundary, { reason: 'new' })
|
|
await emit('agent_start')
|
|
await waitForRequests(4)
|
|
await delay(600)
|
|
const completions = received.filter((event) => event.hook_event_name === 'agent_end')
|
|
assert.equal(completions.length, 2, `Unsent completion was lost at ${boundary}`)
|
|
assert.ok(completions.every((event) => event.session_id === 'A'))
|
|
assert.equal(received.at(-1).hook_event_name, 'agent_start')
|
|
assert.equal(received.at(-1).session_id, 'B', 'Old completion overwrote the new session')
|
|
}
|
|
console.log(
|
|
JSON.stringify({
|
|
platform: process.platform,
|
|
node: process.version,
|
|
recovered,
|
|
cancelledAt: ['agent_start'],
|
|
preservedAt: ['session_switch', 'session_shutdown'],
|
|
scope: 'Generated OMP extension, real native HTTP 503, synthetic lifecycle callbacks'
|
|
})
|
|
)
|
|
} finally {
|
|
server.closeAllConnections()
|
|
server.close()
|
|
await rm(scratch, { recursive: true, force: true })
|
|
}
|