Files
orca/src/main/pi/agent-status-post-queue-source.ts
T
a259616f7c feat(pi): show Pi and OMP background children as rows (#23201)
* 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>
2026-10-05 20:04:48 -07:00

101 lines
4.9 KiB
TypeScript

import type { PiAgentKind } from '../../shared/pi-agent-kind'
export function getPiAgentStatusPostQueueSourceLines(kind: PiAgentKind): string[] {
return [
'type HookPost = { hookEventName: string; extra: Record<string, unknown>; metadata: Record<string, unknown>; ompRuntime: boolean; revision: number; attempts: number; delivered: boolean; final?: boolean }',
'type HookPostQueue = { activePost: HookPost | null; pendingPost: HookPost | null; latestPost: HookPost | null; finalPosts: HookPost[]; finalRetryTimer: ReturnType<typeof setTimeout> | null; postRevision: number; retryTimer: ReturnType<typeof setTimeout> | null }',
...(kind === 'pi'
? [
'declare global { var __orcaPiStatusPostQueue: HookPostQueue | undefined }',
// Why: old delivery continuations and a reloaded registration must serialize through one queue.
'const postQueue: HookPostQueue = globalThis.__orcaPiStatusPostQueue ??= { activePost: null, pendingPost: null, latestPost: null, finalPosts: [], finalRetryTimer: null, postRevision: 0, retryTimer: null }'
]
: [
'const postQueue: HookPostQueue = { activePost: null, pendingPost: null, latestPost: null, finalPosts: [], finalRetryTimer: null, postRevision: 0, retryTimer: null }'
]),
'',
'function cancelPostRetry(): void {',
' if (postQueue.retryTimer !== null) clearTimeout(postQueue.retryTimer)',
' postQueue.retryTimer = null',
'}',
'',
'// Why: a queued or scheduled post builds its body later, so it already carries newer state.',
'function hasQueuedPost(): boolean {',
' return postQueue.pendingPost !== null || postQueue.retryTimer !== null',
'}',
'',
'function resetPostQueue(): void {',
' cancelPostRetry()',
' postQueue.postRevision++',
// Why: preserve the original object so an in-flight acknowledgment also retires its final queue entry.
' for (const post of [postQueue.activePost, postQueue.pendingPost, postQueue.latestPost]) {',
" if (!post || post.hookEventName !== 'agent_end' || post.delivered || post.final) continue",
' post.final = true',
' postQueue.finalPosts.push(post)',
' }',
' postQueue.pendingPost = null',
' postQueue.latestPost = null',
' drainPosts()',
'}',
'',
'function retireTurnCompletionPosts(metadata: Record<string, unknown>): void {',
' const first = postQueue.finalPosts[0]',
' for (const post of postQueue.finalPosts) {',
' if (post.extra.session_boundary !== true && post.metadata.session_id === metadata.session_id) post.final = false',
' }',
' postQueue.finalPosts = postQueue.finalPosts.filter((post) => post.final)',
' if (first && !first.final && postQueue.finalRetryTimer !== null) {',
' clearTimeout(postQueue.finalRetryTimer)',
' postQueue.finalRetryTimer = null',
' }',
'}',
'',
'function drainPosts(): void {',
// Why: a final post waiting to retry holds newer posts back, so the old session's row is never written last.
' if (postQueue.activePost || postQueue.finalRetryTimer !== null) return',
' const next = postQueue.finalPosts[0] ?? postQueue.pendingPost',
' if (!next) return',
' if (!next.final) postQueue.pendingPost = null',
' postQueue.activePost = next',
' void postOnce(next.hookEventName, next.extra, next.metadata, next.ompRuntime, next.final === true)',
' .then(() => {',
' next.delivered = true',
' if (next.final) postQueue.finalPosts.shift()',
' })',
' .catch(() => {',
' if (next.final) {',
' if (next.attempts >= 3) {',
' postQueue.finalPosts.shift()',
" console.warn('[orca-pi-status] hook delivery failed after retries:', next.hookEventName)",
' return',
' }',
' postQueue.finalRetryTimer = setTimeout(() => {',
' postQueue.finalRetryTimer = null',
' drainPosts()',
' }, 250 * 2 ** next.attempts++)',
" if (typeof postQueue.finalRetryTimer.unref === 'function') postQueue.finalRetryTimer.unref()",
' return',
' }',
' if (!next.ompRuntime || next.revision !== postQueue.postRevision) return',
' if (next.attempts >= 3) {',
" console.warn('[orca-pi-status] hook delivery failed after retries:', next.hookEventName)",
' return',
' }',
' const delay = 250 * 2 ** next.attempts++',
' postQueue.retryTimer = setTimeout(() => {',
' postQueue.retryTimer = null',
' if (next.revision !== postQueue.postRevision) return',
' postQueue.pendingPost = next',
' drainPosts()',
' }, delay)',
" if (typeof postQueue.retryTimer.unref === 'function') postQueue.retryTimer.unref()",
' })',
' .finally(() => {',
' postQueue.activePost = null',
' drainPosts()',
' })',
'}',
''
]
}