import type { AgentMessage } from "@gajae-code/agent-core"; import { logger } from "@gajae-code/utils"; export interface YieldDispatcher

{ /** Drop entries already delivered through another path. Called per-entry at flush time. */ isStale?(entry: P): boolean; /** * Optional ownership-origin key: when provided, the flush builds ONE * message per distinct key instead of one message for the whole batch, so * a later scope:"owned" drop of one origin never suppresses entries of * another origin (review thread P2). */ groupKey?(entry: P): string; /** Produce one batched AgentMessage from non-stale entries. Return null to skip. */ build(survivors: P[]): AgentMessage | null; } export interface YieldQueueOptions { isStreaming: () => boolean; injectStreaming(msg: AgentMessage): void; injectIdle(messages: AgentMessage[]): Promise; scheduleIdleFlush(run: () => Promise): void; } type YieldFlushMode = "streaming" | "idle"; interface StoredDispatcher { isStale?: (entry: unknown) => boolean; groupKey?: (entry: unknown) => string; build: (survivors: unknown[]) => AgentMessage | null; } function formatError(error: unknown): string { return error instanceof Error ? error.message : String(error); } export class YieldQueue { readonly #options: YieldQueueOptions; readonly #dispatchers = new Map(); readonly #entries = new Map(); #idleFlushPending = false; constructor(options: YieldQueueOptions) { this.#options = options; } register

(kind: string, dispatcher: YieldDispatcher

): () => void { const stored: StoredDispatcher = { ...(dispatcher.isStale ? { isStale: entry => dispatcher.isStale?.(entry as P) ?? false } : {}), ...(dispatcher.groupKey ? { groupKey: entry => dispatcher.groupKey?.(entry as P) ?? "default" } : {}), build: survivors => dispatcher.build(survivors as P[]), }; this.#dispatchers.set(kind, stored); return () => { if (this.#dispatchers.get(kind) !== stored) return; this.#dispatchers.delete(kind); this.#entries.delete(kind); }; } enqueue

(kind: string, entry: P): void { if (!this.#dispatchers.has(kind)) { logger.warn("Yield queue entry ignored for unregistered kind", { kind }); return; } let entries = this.#entries.get(kind); if (!entries) { entries = []; this.#entries.set(kind, entries); } entries.push(entry); if (!this.#options.isStreaming()) { this.#scheduleIdleFlush(); } } has(kind?: string): boolean { if (kind !== undefined) return (this.#entries.get(kind)?.length ?? 0) > 0; for (const entries of this.#entries.values()) { if (entries.length > 0) return true; } return false; } async flush(mode: YieldFlushMode): Promise { if (mode === "idle") { this.#idleFlushPending = false; } const idleMessages: AgentMessage[] = []; for (const [kind, dispatcher] of this.#dispatchers) { const entries = this.#drain(kind); if (entries.length === 0) continue; const messages = this.#build(kind, dispatcher, entries) ?? []; for (const message of messages) { if (mode === "streaming") { try { this.#options.injectStreaming(message); } catch (error) { logger.warn("Yield queue streaming dispatch failed", { kind, error: formatError(error) }); } } else { idleMessages.push(message); } } } if (mode === "idle" && idleMessages.length > 0) { try { await this.#options.injectIdle(idleMessages); } catch (error) { logger.warn("Yield queue idle dispatch failed", { error: formatError(error) }); } } } clear(): void { this.#entries.clear(); this.#idleFlushPending = false; } /** Drop only the queued entries of a single kind, leaving other kinds intact. */ clearKind(kind: string): void { this.#entries.delete(kind); } /** * Re-schedule an idle flush if work remains and the session is idle. Used after * a transition (e.g. handoff) releases a delivery fence so entries queued while * fenced are not stranded until an unrelated enqueue or agent yield. */ rearmIdle(): void { if (this.#options.isStreaming()) return; for (const entries of this.#entries.values()) { if (entries.length > 0) { this.#scheduleIdleFlush(); return; } } } #scheduleIdleFlush(): void { if (this.#idleFlushPending) return; this.#idleFlushPending = true; try { this.#options.scheduleIdleFlush(async () => { this.#idleFlushPending = false; if (this.#options.isStreaming()) return; await this.flush("idle"); }); } catch (error) { this.#idleFlushPending = false; logger.warn("Yield queue idle flush scheduling failed", { error: formatError(error) }); } } #drain(kind: string): unknown[] { const entries = this.#entries.get(kind); if (!entries || entries.length === 0) return []; this.#entries.delete(kind); return entries; } #build(kind: string, dispatcher: StoredDispatcher, entries: unknown[]): AgentMessage[] | null { // Corrected turn semantics (terminal abort): turn-scope abort blocks only // deliveries whose origin is a continuation of the aborted turn. // Owned-completion deliveries from work deliberately left running are // intentionally allowed to resume the agent through the normal // followUp/prompt path and receive a fresh turn attempt. A closed // terminal record must never make an allowed owned-completion entry // stale merely because it is closed; stale filtering below applies only // to ordinary manager state (e.g. isDeliverySuppressed) or explicit // blocked-continuation/owned-cleanup entries. const survivors: unknown[] = []; for (const entry of entries) { if (dispatcher.isStale) { let stale: boolean; try { stale = dispatcher.isStale(entry); } catch (error) { logger.warn("Yield queue stale check failed", { kind, error: formatError(error) }); continue; } if (stale) continue; } survivors.push(entry); } if (survivors.length === 0) return null; // Build one message per ownership-origin group (when the dispatcher // declares a groupKey) so a later owned-scope drop of one group never // suppresses another group's entries. Groups are partitioned into // CONTIGUOUS origin runs (preserving the queued FIFO chronology): with // entries A1, B1, A2, a map grouping every A together would deliver A2 // before the earlier B1, changing the observable order of async results // (review thread P2). const groups: unknown[][] = []; let currentGroupKey: string | undefined; for (const entry of survivors) { const key = dispatcher.groupKey ? dispatcher.groupKey(entry) : "default"; const last = groups[groups.length - 1]; if (last !== undefined && currentGroupKey === key) { last.push(entry); } else { groups.push([entry]); currentGroupKey = key; } } const messages: AgentMessage[] = []; for (const group of groups.values()) { try { const message = dispatcher.build(group); if (message) messages.push(message); } catch (error) { logger.warn("Yield queue build failed", { kind, error: formatError(error) }); } } return messages.length > 0 ? messages : null; } }