import { createHash, randomUUID } from 'node:crypto'; import * as fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'; import { OperationJournal } from './operation-journal.js'; import { registerPlanExecRpc } from './plan-exec-rpc.js'; import { singleChildWorkflowScript } from './workflow-spawn.js'; // Protocol evidence (installed sources verified against pi-subagents@0.76.0 // and @tintinweb/pi-tasks@0.9.0): // - @tintinweb/pi-tasks src/index.ts:103-119 reply channel/envelope, // 126-133 spawn/stop params, 137-157 strict PROTOCOL_VERSION=2, // 207-260 completed/failed/stopped fields. // - pi-subagents src/extension/rpc.js exposes the v1 channel and async-only spawn; // inline scripts use the public script field, not the internal workflowScript carrier. // - pi-subagents src/runs/background/result-watcher.ts:49-57 result file fields, // 141-164 child output/status normalization, 193-204 async-complete payload; // subagent-runner.ts:3066-3073 complete/failed/paused state values. // - pi-subagents src/agents/agents.ts:31-40 builtin names and // src/agents/agent-selection.ts:4-19 exact-name merge; pi-tasks examples need aliases. const PING_CHANNEL = 'subagents:rpc:ping'; const SPAWN_CHANNEL = 'subagents:rpc:spawn'; const STOP_CHANNEL = 'subagents:rpc:stop'; const COMPLETED_EVENT = 'subagents:completed'; const FAILED_EVENT = 'subagents:failed'; const READY_EVENT = 'subagents:ready'; const WARNING_EVENT = 'subagents:warning'; const NB_REQUEST_CHANNEL = 'subagents:rpc:v1:request'; const NB_COMPLETE_EVENT = 'subagent:async-complete'; const NB_REPLY_PREFIX = 'subagents:rpc:v1:reply:'; const DEFAULT_SPAWN_TIMEOUT_MS = 24_000; const DEFAULT_COMPLETION_POLL_INTERVAL_MS = 2_000; const DEFAULT_MAX_ACTIVE_RUNS = 2; const DEFAULT_MAX_TURNS = 12; const DEFAULT_TERMINAL_RESULT_GRACE_MS = 5_000; const DEFAULT_SPAWN_REPLY_CACHE_TTL_MS = 60_000; const DEFAULT_ACCEPTED_RUN_LEASE_MS = 30_000; const MAX_FAILURE_PARTIAL_OUTPUT_CHARS = 4_000; const AGENT_TYPE_ALIASES = new Map([ ['general-purpose', 'delegate'], ['Explore', 'scout'], ['explore', 'scout'], ]); const BRIDGE_ACCEPTANCE_CONFIG = { level: 'none', reason: 'pi-tasks bridge manages task lifecycle and result propagation; do not require pi-subagents acceptance reports.', } as const; const BRIDGE_CONTROL_CONFIG = { enabled: false, } as const; interface BridgeOptions { spawnTimeoutMs?: number; planExecJournalPath?: string; completionPollIntervalMs?: number; maxActiveRuns?: number; defaultMaxTurns?: number; terminalResultGraceMs?: number; spawnReplyCacheTtlMs?: number; acceptedRunLeaseMs?: number; acceptedRunReconcileIntervalMs?: number; getSessionId?: () => string | undefined; operationJournal?: OperationJournal; } type BridgeHost = Pick; type Unsubscribe = () => void; type RpcReply = | { success: true; data: T } | { success: false; error: string }; type CompletionKind = 'completed' | 'failed' | 'stopped'; interface BridgeRegistration { dispose(): void; } interface SpawnReplyCacheEntry { identity: string; reply: RpcReply<{ id: string }>; expiresAt: number; } interface InFlightSpawnReply { identity: string; reply: Promise>; } interface PendingLegacyBinding { requestId: string; requestDigest: string; sessionId: string; runId: string; } interface BridgeState { ownedRunIds: Set; completedRunIds: Set; stoppingRunIds: Set; pendingSpawnCount: number; inFlightSpawnReplies: Map; spawnReplyCache: Map; terminalResultDeadlines: Map; pendingLegacyBindings: Map; journal?: OperationJournal; registration?: BridgeRegistration; } const bridgeStates = new WeakMap(); const PROCESS_OWNER_KEY = Symbol.for( 'pi-subagents-bridge.accepted-run-owner.v1', ); interface ProcessOwnerStore { pid: number; instanceId: string; } function acceptedRunOwner(): ProcessOwnerStore { const target = globalThis as typeof globalThis & { [PROCESS_OWNER_KEY]?: ProcessOwnerStore; }; const existing = target[PROCESS_OWNER_KEY]; if (existing?.pid === process.pid) return existing; const created = { pid: process.pid, instanceId: randomUUID() }; target[PROCESS_OWNER_KEY] = created; return created; } interface SpawnOptionsRaw { model?: unknown; maxTurns?: unknown; } interface AsyncCompleteRaw { runId?: unknown; mode?: unknown; asyncId?: unknown; id?: unknown; success?: unknown; state?: unknown; summary?: unknown; output?: unknown; error?: unknown; results?: unknown; } function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } function text(value: unknown): string | undefined { return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined; } function replyChannel(channel: string, requestId: string): string { return `${channel}:reply:${requestId}`; } function nbReplyChannel(requestId: string): string { return `${NB_REPLY_PREFIX}${requestId}`; } function emitReply( events: BridgeHost['events'], channel: string, requestId: string, reply: RpcReply, ): void { events.emit(replyChannel(channel, requestId), reply); } function extractSpawnRunId(reply: unknown): string | undefined { if (!isRecord(reply)) return undefined; const details = isRecord(reply.details) ? reply.details : undefined; return ( text(details?.runId) ?? text(details?.asyncId) ?? text(reply.runId) ?? text(reply.asyncId) ); } function extractRunId(payload: AsyncCompleteRaw): string | undefined { return text(payload.runId) ?? text(payload.asyncId) ?? text(payload.id); } function extractChildOutputs(payload: AsyncCompleteRaw): string[] { if (!Array.isArray(payload.results)) return []; const outputs: string[] = []; for (const result of payload.results) { if (!isRecord(result)) continue; const output = text(result.output) ?? text(result.error); if (output) outputs.push(output); } return outputs; } function extractCompletedResult(payload: AsyncCompleteRaw): string | undefined { const childOutput = extractChildOutputs(payload).join('\n\n') || undefined; if (text(payload.mode) === 'workflow' && childOutput) return childOutput; return text(payload.summary) ?? text(payload.output) ?? childOutput; } function extractStoppedResult(payload: AsyncCompleteRaw): string | undefined { return ( (extractChildOutputs(payload).join('\n\n') || undefined) ?? text(payload.output) ?? text(payload.summary) ); } function extractFailureError(payload: AsyncCompleteRaw): string { let error = text(payload.error); if (!error && Array.isArray(payload.results)) { for (const result of payload.results) { if (!isRecord(result)) continue; error = text(result.error); if (error) break; } } const partialOutput = text(payload.output) ?? (Array.isArray(payload.results) ? payload.results .filter(isRecord) .map((result) => text(result.output)) .filter((output): output is string => output !== undefined) .join('\n\n') : undefined); if (!partialOutput) return error ?? 'Agent failed'; const truncatedOutput = partialOutput.slice( 0, MAX_FAILURE_PARTIAL_OUTPUT_CHARS, ); const suffix = partialOutput.length > truncatedOutput.length ? '\n[truncated]' : ''; return `${error ?? 'Agent failed'}\n\nPartial output:\n${truncatedOutput}${suffix}`; } function classifyCompleteEvent(payload: AsyncCompleteRaw): CompletionKind { const state = text(payload.state); if (state === 'paused' || state === 'stopped') return 'stopped'; if (state === 'failed' || state === 'aborted' || payload.success === false) return 'failed'; if (state === 'complete' || payload.success === true) return 'completed'; return 'failed'; } function resolveAgentType(type: string): string { return AGENT_TYPE_ALIASES.get(type) ?? type; } function requestNicobailonRpc( events: BridgeHost['events'], method: 'spawn' | 'status', params: Record, timeoutMs: number, signal?: AbortSignal, ): Promise { const requestId = randomUUID(); return new Promise((resolve, reject) => { let settled = false; const cleanup = (): void => { if (settled) return; settled = true; unsubscribe(); clearTimeout(timeout); signal?.removeEventListener('abort', onAbort); }; const fail = (error: Error): void => { if (settled) return; cleanup(); reject(error); }; const succeed = (value: T): void => { if (settled) return; cleanup(); resolve(value); }; const onAbort = (): void => { fail(new Error('Bridge disposed')); }; const timeout = setTimeout(() => { fail( new Error(`nicobailon "${method}" RPC timed out after ${timeoutMs}ms`), ); }, timeoutMs); const unsubscribe = events.on(nbReplyChannel(requestId), (raw: unknown) => { if (!isRecord(raw) || typeof raw.success !== 'boolean') { fail(new Error('Malformed nicobailon RPC reply.')); return; } if (raw.success) { succeed(raw.data as T); return; } const message = isRecord(raw.error) ? text(raw.error.message) : text(raw.error); fail(new Error(message ?? 'nicobailon RPC error')); }); signal?.addEventListener('abort', onAbort, { once: true }); if (signal?.aborted) { onAbort(); return; } events.emit(NB_REQUEST_CHANNEL, { version: 1, requestId, method, params, }); }); } function normalizeSpawnOptions(raw: unknown): SpawnOptionsRaw | undefined { return isRecord(raw) ? raw : undefined; } function extractRpcText(reply: unknown): string | undefined { return isRecord(reply) ? text(reply.text) : undefined; } function parseStatusState(statusText: string): string | undefined { const match = /^State:\s+(.+)$/im.exec(statusText); return match?.[1]?.trim().toLowerCase(); } function parseResultPath(statusText: string): string | undefined { const match = /^Result:\s+(.+)$/im.exec(statusText); return match?.[1]?.trim(); } function classifyStatusText(statusText: string): CompletionKind | undefined { const state = parseStatusState(statusText); if (!state) return undefined; if (state === 'paused' || state === 'stopped') return 'stopped'; if (state === 'failed' || state === 'aborted') return 'failed'; if (state === 'complete') return 'completed'; return undefined; } function readResultPayload(resultPath: string | undefined): { payload?: AsyncCompleteRaw; error?: string; } { if (!resultPath) return {}; try { const parsed: unknown = JSON.parse(fs.readFileSync(resultPath, 'utf8')); if (!isRecord(parsed)) { return { error: 'result payload is not a JSON object' }; } return { payload: parsed }; } catch { return { error: 'result payload is not readable yet' }; } } function positiveIntegerOrDefault( value: number | undefined, fallback: number, ): number { return value !== undefined && Number.isInteger(value) && value > 0 ? value : fallback; } function getBridgeState(events: BridgeHost['events']): BridgeState { const existing = bridgeStates.get(events); if (existing) return existing; const created: BridgeState = { ownedRunIds: new Set(), completedRunIds: new Set(), stoppingRunIds: new Set(), pendingSpawnCount: 0, inFlightSpawnReplies: new Map(), spawnReplyCache: new Map(), terminalResultDeadlines: new Map(), pendingLegacyBindings: new Map(), }; bridgeStates.set(events, created); return created; } export function registerBridge( pi: BridgeHost, options: BridgeOptions = {}, ): { dispose: () => void } { const state = getBridgeState(pi.events); if (state.registration) return state.registration; const processOwner = acceptedRunOwner(); const spawnTimeoutMs = positiveIntegerOrDefault( options.spawnTimeoutMs, DEFAULT_SPAWN_TIMEOUT_MS, ); const completionPollIntervalMs = positiveIntegerOrDefault( options.completionPollIntervalMs, DEFAULT_COMPLETION_POLL_INTERVAL_MS, ); const maxActiveRuns = positiveIntegerOrDefault( options.maxActiveRuns, DEFAULT_MAX_ACTIVE_RUNS, ); const defaultMaxTurns = positiveIntegerOrDefault( options.defaultMaxTurns, DEFAULT_MAX_TURNS, ); const terminalResultGraceMs = positiveIntegerOrDefault( options.terminalResultGraceMs, DEFAULT_TERMINAL_RESULT_GRACE_MS, ); const spawnReplyCacheTtlMs = positiveIntegerOrDefault( options.spawnReplyCacheTtlMs, DEFAULT_SPAWN_REPLY_CACHE_TTL_MS, ); const acceptedRunLeaseMs = positiveIntegerOrDefault( options.acceptedRunLeaseMs, DEFAULT_ACCEPTED_RUN_LEASE_MS, ); const fallbackSessionId = `process:${processOwner.instanceId}`; const currentSessionId = (): string | undefined => { const sessionId = options.getSessionId?.()?.trim(); if (sessionId) return sessionId; return options.getSessionId ? undefined : fallbackSessionId; }; const { ownedRunIds, completedRunIds, stoppingRunIds, inFlightSpawnReplies, spawnReplyCache, terminalResultDeadlines, pendingLegacyBindings, } = state; const pendingRpcControllers = new Set(); const completionPollTimers = new Map>(); const volatileOwnedRuns = new Map(); const unsubscribes: Unsubscribe[] = []; let disposed = false; if (options.operationJournal) { if (state.journal && state.journal !== options.operationJournal) { throw new Error( 'bridge was already registered with a different operation journal', ); } state.journal = options.operationJournal; } else if (options.planExecJournalPath) { const journalPath = path.resolve(options.planExecJournalPath); if (state.journal && state.journal.filePath !== journalPath) { throw new Error( 'bridge was already registered with a different operation journal', ); } state.journal ??= new OperationJournal(journalPath); } const bridgeJournal = state.journal; const planExecRpc = registerPlanExecRpc(pi.events, { timeoutMs: spawnTimeoutMs, ...(options.planExecJournalPath ? { journalPath: options.planExecJournalPath } : {}), }); const track = (unsubscribe: Unsubscribe | undefined): void => { if (typeof unsubscribe === 'function') unsubscribes.push(unsubscribe); }; const clearCompletionPoll = (runId: string): void => { const timer = completionPollTimers.get(runId); if (timer) clearTimeout(timer); completionPollTimers.delete(runId); }; const shouldStopPolling = (runId: string): boolean => disposed || completedRunIds.has(runId) || !ownedRunIds.has(runId); const emitCompletion = ( runId: string, kind: CompletionKind, payload?: AsyncCompleteRaw, ): void => { if (!ownedRunIds.has(runId) || completedRunIds.has(runId)) return; const sessionId = currentSessionId(); if (!sessionId) return; if (bridgeJournal && volatileOwnedRuns.get(runId) !== sessionId) { let renewed: boolean; try { renewed = bridgeJournal.renewAcceptedRun( runId, processOwner.instanceId, sessionId, ); } catch (error: unknown) { console.error(`Failed to renew accepted bridge run '${runId}':`, error); return; } if (!renewed) { ownedRunIds.delete(runId); clearCompletionPoll(runId); return; } } completedRunIds.add(runId); ownedRunIds.delete(runId); stoppingRunIds.delete(runId); terminalResultDeadlines.delete(runId); clearCompletionPoll(runId); if (kind === 'stopped') { const result = payload ? extractStoppedResult(payload) : undefined; pi.events.emit(FAILED_EVENT, { id: runId, ...(result ? { result } : {}), status: 'stopped', }); } else if (kind === 'failed') { pi.events.emit(FAILED_EVENT, { id: runId, error: payload ? extractFailureError(payload) : 'Agent failed', status: 'failed', }); } else { const result = payload ? extractCompletedResult(payload) : undefined; pi.events.emit(COMPLETED_EVENT, { id: runId, ...(result ? { result } : {}), }); } try { bridgeJournal?.completeRun(runId, processOwner.instanceId, sessionId); volatileOwnedRuns.delete(runId); } catch (error: unknown) { console.error( `Failed to record delivered bridge completion for '${runId}':`, error, ); } }; const pollRunCompletion = async (runId: string): Promise => { completionPollTimers.delete(runId); if (shouldStopPolling(runId)) { clearCompletionPoll(runId); return; } const controller = new AbortController(); pendingRpcControllers.add(controller); try { const reply = await requestNicobailonRpc( pi.events, 'status', { id: runId }, spawnTimeoutMs, controller.signal, ); if (shouldStopPolling(runId)) { clearCompletionPoll(runId); return; } const statusText = extractRpcText(reply); const kind = statusText ? classifyStatusText(statusText) : undefined; if (kind && statusText) { const resultPath = parseResultPath(statusText); const result = readResultPayload(resultPath); if (result.payload) { terminalResultDeadlines.delete(runId); emitCompletion(runId, kind, result.payload); return; } if (resultPath) { const now = Date.now(); const deadline = terminalResultDeadlines.get(runId) ?? now + terminalResultGraceMs; terminalResultDeadlines.set(runId, deadline); if (now < deadline) { const timer = setTimeout( () => { void pollRunCompletion(runId); }, Math.min(completionPollIntervalMs, deadline - now), ); timer.unref(); completionPollTimers.set(runId, timer); return; } emitCompletion(runId, kind, { [kind === 'failed' ? 'error' : 'summary']: `Bridge warning: ${result.error ?? 'result payload was unavailable'} after ${terminalResultGraceMs}ms.`, }); return; } emitCompletion(runId, kind); return; } } catch { if (disposed) return; } finally { pendingRpcControllers.delete(controller); } if (shouldStopPolling(runId)) { clearCompletionPoll(runId); return; } const timer = setTimeout(() => { void pollRunCompletion(runId); }, completionPollIntervalMs); timer.unref(); completionPollTimers.set(runId, timer); }; const ensureCompletionPoll = (runId: string): void => { if (completionPollTimers.has(runId)) return; const timer = setTimeout(() => { void pollRunCompletion(runId); }, completionPollIntervalMs); timer.unref(); completionPollTimers.set(runId, timer); }; const rearmAcceptedRun = (runId: string): string | undefined => { const sessionId = currentSessionId(); if (!sessionId) return 'Pi session identity is not initialized'; if (bridgeJournal && volatileOwnedRuns.get(runId) !== sessionId) { try { if (!bridgeJournal.acceptRun(runId, processOwner, sessionId)) { const claimed = bridgeJournal.claimAcceptedRuns( processOwner, sessionId, acceptedRunLeaseMs, ); if (!claimed.some((run) => run.runId === runId)) { return `native run '${runId}' is already owned by another bridge session`; } } } catch (error: unknown) { return error instanceof Error ? error.message : String(error); } } completedRunIds.delete(runId); ownedRunIds.add(runId); ensureCompletionPoll(runId); return undefined; }; const reconcileAcceptedRuns = (): void => { if (!bridgeJournal || disposed) return; const sessionId = currentSessionId(); if (!sessionId) return; for (const [runId, pending] of pendingLegacyBindings) { if (pending.sessionId !== sessionId) continue; try { bridgeJournal.bindLegacySpawn( pending.requestId, pending.requestDigest, pending.sessionId, pending.runId, ); pendingLegacyBindings.delete(runId); } catch (error: unknown) { console.error( `Failed to retry legacy bridge run '${runId}' binding:`, error, ); } } try { const claimed = bridgeJournal.claimAcceptedRuns( processOwner, sessionId, acceptedRunLeaseMs, ); const claimedIds = new Set(claimed.map((run) => run.runId)); for (const runId of [...ownedRunIds]) { const volatileSessionId = volatileOwnedRuns.get(runId); if (volatileSessionId === sessionId) { try { if (bridgeJournal.acceptRun(runId, processOwner, sessionId)) { volatileOwnedRuns.delete(runId); claimedIds.add(runId); continue; } volatileOwnedRuns.delete(runId); } catch (error: unknown) { console.error( `Failed to persist volatile bridge run '${runId}':`, error, ); continue; } } else if (volatileSessionId) { volatileOwnedRuns.delete(runId); } else if (claimedIds.has(runId)) { continue; } ownedRunIds.delete(runId); clearCompletionPoll(runId); pi.events.emit(WARNING_EVENT, { code: 'accepted_run_ownership_lost', id: runId, message: 'Bridge stopped delivering this run because another session owns its completion record.', }); } for (const accepted of claimed) { ownedRunIds.add(accepted.runId); ensureCompletionPoll(accepted.runId); } } catch (error: unknown) { console.error('Failed to reconcile accepted bridge runs:', error); } }; reconcileAcceptedRuns(); const acceptedRunReconcileInterval = bridgeJournal ? setInterval( reconcileAcceptedRuns, positiveIntegerOrDefault( options.acceptedRunReconcileIntervalMs, Math.max( 1_000, Math.min( completionPollIntervalMs, Math.floor(acceptedRunLeaseMs / 3), ), ), ), ) : undefined; acceptedRunReconcileInterval?.unref(); // pi-subagents/src/agents/agents.ts exposes exact runtime names and has no // general-purpose/Explore builtins. Keep only these pi-tasks compatibility aliases. track( pi.events.on(PING_CHANNEL, (raw: unknown) => { if (!isRecord(raw)) return; const requestId = text(raw.requestId); if (!requestId) return; emitReply(pi.events, PING_CHANNEL, requestId, { success: true, data: { version: 2 }, } satisfies RpcReply<{ version: number }>); }), ); const pruneSpawnReplyCache = (): void => { const now = Date.now(); for (const [requestId, entry] of spawnReplyCache) { if (entry.expiresAt <= now) spawnReplyCache.delete(requestId); } }; const handleSpawn = (raw: unknown): void => { if (!isRecord(raw)) return; const requestId = text(raw.requestId); if (!requestId) return; const agentType = text(raw.type); const prompt = text(raw.prompt); if (!agentType || !prompt) { emitReply(pi.events, SPAWN_CHANNEL, requestId, { success: false, error: 'spawn requires string type and prompt', }); return; } const sessionId = currentSessionId(); if (!sessionId) { emitReply(pi.events, SPAWN_CHANNEL, requestId, { success: false, error: 'Pi session identity is not initialized', }); return; } const optionsRaw = normalizeSpawnOptions(raw.options); const model = text(optionsRaw?.model); const maxTurns = typeof optionsRaw?.maxTurns === 'number' && Number.isInteger(optionsRaw.maxTurns) && optionsRaw.maxTurns > 0 ? optionsRaw.maxTurns : defaultMaxTurns; const spawnParams: Record = { script: singleChildWorkflowScript(resolveAgentType(agentType), prompt, { control: BRIDGE_CONTROL_CONFIG, }), async: true, context: 'fresh', acceptance: BRIDGE_ACCEPTANCE_CONFIG, control: BRIDGE_CONTROL_CONFIG, ...(model ? { model } : {}), turnBudget: { maxTurns }, }; const legacyRequestDigest = `sha256:${createHash('sha256') .update(JSON.stringify({ agentType, prompt, spawnParams })) .digest('hex')}`; const requestIdentity = `${sessionId}:${legacyRequestDigest}`; pruneSpawnReplyCache(); const cached = spawnReplyCache.get(requestId); if (cached) { if (cached.identity !== requestIdentity) { emitReply(pi.events, SPAWN_CHANNEL, requestId, { success: false, error: 'spawn requestId was already used by another request', }); return; } if (cached.reply.success) { const error = rearmAcceptedRun(cached.reply.data.id); if (error) { emitReply(pi.events, SPAWN_CHANNEL, requestId, { success: false, error, }); return; } } emitReply(pi.events, SPAWN_CHANNEL, requestId, cached.reply); return; } const inFlight = inFlightSpawnReplies.get(requestId); if (inFlight) { if (inFlight.identity !== requestIdentity) { emitReply(pi.events, SPAWN_CHANNEL, requestId, { success: false, error: 'spawn requestId was already used by another request', }); return; } void inFlight.reply.then((reply) => { if (!disposed) emitReply(pi.events, SPAWN_CHANNEL, requestId, reply); }); return; } const spawn = async (): Promise> => { if (bridgeJournal) { try { const durable = bridgeJournal.getLegacySpawn(requestId); if (durable) { if ( durable.requestDigest !== legacyRequestDigest || durable.sessionId !== sessionId ) { return { success: false, error: 'spawn requestId was already used by another request', }; } if (durable.binding === 'bound' && durable.runId) { const error = rearmAcceptedRun(durable.runId); return error ? { success: false, error } : { success: true, data: { id: durable.runId } }; } return { success: false, error: durable.error ?? 'legacy spawn outcome is unknown; manual recovery is required', }; } } catch (error: unknown) { return { success: false, error: error instanceof Error ? error.message : String(error), }; } } if (ownedRunIds.size + state.pendingSpawnCount >= maxActiveRuns) { return { success: false, error: `bridge capacity reached: at most ${maxActiveRuns} active runs`, }; } let durableOperationStarted = false; if (bridgeJournal) { try { const begun = bridgeJournal.beginLegacySpawn( requestId, legacyRequestDigest, sessionId, ); if (!begun.created) { return { success: false, error: 'spawn requestId was concurrently claimed', }; } durableOperationStarted = true; } catch (error: unknown) { return { success: false, error: error instanceof Error ? error.message : String(error), }; } } const controller = new AbortController(); state.pendingSpawnCount += 1; pendingRpcControllers.add(controller); try { const reply = await requestNicobailonRpc( pi.events, 'spawn', spawnParams, spawnTimeoutMs, controller.signal, ); if (disposed || controller.signal.aborted) { throw new Error('Bridge disposed after native dispatch'); } const runId = extractSpawnRunId(reply); if (!runId) { throw new Error('nicobailon spawn reply did not include a run id'); } try { bridgeJournal?.bindLegacySpawn( requestId, legacyRequestDigest, sessionId, runId, ); pendingLegacyBindings.delete(runId); durableOperationStarted = false; } catch (error: unknown) { pendingLegacyBindings.set(runId, { requestId, requestDigest: legacyRequestDigest, sessionId, runId, }); console.error( `Failed to persist legacy bridge run '${runId}' binding:`, error, ); } try { const accepted = bridgeJournal?.acceptRun( runId, processOwner, sessionId, ); if (accepted === false) { return { success: false, error: `native run '${runId}' is already owned by another bridge session`, }; } } catch (error: unknown) { // The native run already exists. Returning a failed spawn would invite // TaskExecute to launch a duplicate, so keep session-scoped ownership // in memory and report the durability loss explicitly. volatileOwnedRuns.set(runId, sessionId); console.error( `Failed to persist accepted bridge run '${runId}':`, error, ); } ownedRunIds.add(runId); ensureCompletionPoll(runId); return { success: true, data: { id: runId } }; } catch (error: unknown) { if (durableOperationStarted) { try { bridgeJournal?.markLegacySpawnUnknown( requestId, legacyRequestDigest, sessionId, error instanceof Error ? error.message : String(error), ); } catch (journalError: unknown) { console.error( `Failed to persist unknown legacy spawn '${requestId}':`, journalError, ); } } return { success: false, error: error instanceof Error ? error.message : String(error), }; } finally { state.pendingSpawnCount -= 1; pendingRpcControllers.delete(controller); } }; const spawnReply = spawn(); const inFlightEntry: InFlightSpawnReply = { identity: requestIdentity, reply: spawnReply, }; inFlightSpawnReplies.set(requestId, inFlightEntry); void spawnReply.then((reply) => { if (inFlightSpawnReplies.get(requestId) === inFlightEntry) { inFlightSpawnReplies.delete(requestId); } if ( reply.success || !reply.error.startsWith('bridge capacity reached:') ) { spawnReplyCache.set(requestId, { identity: requestIdentity, reply, expiresAt: Date.now() + spawnReplyCacheTtlMs, }); } if (!disposed) emitReply(pi.events, SPAWN_CHANNEL, requestId, reply); }); }; track(pi.events.on(SPAWN_CHANNEL, handleSpawn)); track( pi.events.on(STOP_CHANNEL, (raw: unknown) => { if (!isRecord(raw)) return; const requestId = text(raw.requestId); if (!requestId) return; const agentId = text(raw.agentId) ?? text(raw.id) ?? text(raw.runId); if (agentId && ownedRunIds.has(agentId) && !stoppingRunIds.has(agentId)) { stoppingRunIds.add(agentId); try { // pi-subagents rpc.ts normalizes stop targets from id/runId; use canonical id here. pi.events.emit(NB_REQUEST_CHANNEL, { version: 1, requestId: randomUUID(), method: 'stop', params: { id: agentId }, }); } catch { // pi-tasks ignores stop failures; still acknowledge success locally. } } emitReply(pi.events, STOP_CHANNEL, requestId, { success: true, data: undefined, } satisfies RpcReply); }), ); track( pi.events.on(NB_COMPLETE_EVENT, (raw: unknown) => { if (!isRecord(raw)) return; const payload = raw as AsyncCompleteRaw; const runId = extractRunId(payload); if (!runId || !ownedRunIds.has(runId) || completedRunIds.has(runId)) return; emitCompletion(runId, classifyCompleteEvent(payload), payload); }), ); for (const runId of ownedRunIds) ensureCompletionPoll(runId); pi.events.emit(READY_EVENT, {}); const registration: BridgeRegistration = { dispose() { if (disposed) return; disposed = true; planExecRpc.dispose(); if (state.registration === registration) { delete state.registration; } for (const controller of pendingRpcControllers) { controller.abort(); } pendingRpcControllers.clear(); for (const timer of completionPollTimers.values()) { clearTimeout(timer); } completionPollTimers.clear(); if (acceptedRunReconcileInterval) { clearInterval(acceptedRunReconcileInterval); } while (unsubscribes.length > 0) { const unsubscribe = unsubscribes.pop(); try { unsubscribe?.(); } catch { // Best effort cleanup. } } }, }; state.registration = registration; return registration; } export default function bridgeExtension(pi: ExtensionAPI): void { let sessionId: string | undefined; let registration: BridgeRegistration | undefined; const register = (): void => { registration ??= registerBridge(pi, { planExecJournalPath: path.join( os.homedir(), '.pi', 'pi-subagents-bridge', 'plan-exec-operations.sqlite', ), getSessionId: () => sessionId, }); }; pi.on('session_start', (_event, ctx) => { sessionId = ctx.sessionManager.getSessionId(); register(); }); pi.on('session_shutdown', () => { registration?.dispose(); registration = undefined; sessionId = undefined; }); }