mirror of
https://github.com/stablyai/orca.git
synced 2026-11-04 11:07:42 +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>
101 lines
4.9 KiB
TypeScript
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()',
|
|
' })',
|
|
'}',
|
|
''
|
|
]
|
|
}
|