import { type AftProjectTransport, type BgNudgeRef } from "@cortexkit/aft-bridge"; import type { PluginContext } from "./types.js"; export interface BgCompletion { task_id: string; status: string; exit_code: number | null; command: string; duration_ms?: number; runtime_ms?: number; runtime?: number; /** * Exit-aware preview of stdout+stderr captured at completion (from Rust): * success = short tail (≤600 B), failure = small head + larger tail * (≤2.25 KiB). Full output stays recoverable via bash_status / file pointers. */ output_preview?: string; /** True when the captured tail is shorter than the actual output. */ output_truncated?: boolean; original_tokens?: number; compressed_tokens?: number; tokens_skipped?: boolean; status_reason?: string; mode?: "pipes" | "pty" | string; output_path?: string; } export interface PatternMatchEntry { task_id: string; session_id: string; watch_id: string; match_text: string; match_offset: number; context: string; once: boolean; reason?: "pattern_match" | "task_exit"; /** Ack the underlying bash completion after this task-exit reminder is delivered. */ ackCompletionOnDelivery?: boolean; } export interface BgLongRunningReminder { task_id: string; session_id: string; command: string; elapsed_ms: number; mode?: "pipes" | "pty" | string; } type SessionBgState = { outstandingTaskIds: Set; pendingCompletions: BgCompletion[]; pendingLongRunning: BgLongRunningReminder[]; pendingPatternMatches: PatternMatchEntry[]; explicitControlTasks: Set; debounceTimer: NodeJS.Timeout | null; firstCompletionAt: number | null; scheduledFireAt: number | null; scheduledCompletionCount: number; retryDelayMs: number | null; wakeRetryAttempts: number; wakeHardStopped: boolean; forcedDrainCompleted: boolean; unknownCompletions: Array<{ completion: BgCompletion; receivedAt: number; }>; /** * Task IDs spawned since the last session.idle event. Push completions for * these tasks are kept pending but do not promptAsync-wake immediately: the * agent may still be in the same assistant turn and about to call sync * bash_watch, whose inline result should be the only delivery. In-turn * append and the next session.idle still deliver normally. */ wakeDeferredTaskIds: Set; /** * Task IDs whose completions were consumed inline by an explicit * `bash_status({ exit: true, ... })` wait. The bash_completed push * frame for these tasks may arrive AFTER the wait poll loop returned * (the Rust→plugin frame is async); without this set, the late frame * would land in `pendingCompletions` and the next `appendInTurnBgCompletions` * or wake would deliver a duplicate reminder. We dedupe at the ingest * boundary so `pendingCompletions` stays a clean source of truth. * * Bounded by `CONSUMED_TASKIDS_CAP` (FIFO eviction) so a session that * runs thousands of bg tasks doesn't grow this set without bound. */ consumedTaskIds: Set; consumedTaskOrder: string[]; /** * Task IDs whose delivery is IN FLIGHT (removed from pendingCompletions / * pattern queues, prompt not yet resolved). Ingest skips these so a subc forced * drain in the removal→delivery window cannot re-accept and double-schedule an * already-departing completion. Bounded by the in-flight wake batch (cleared on * delivery success → moves to awaitingAck, or on delivery failure → re-pended). */ deliveringTaskIds: Set; /** * Task IDs DELIVERED to the agent but whose `bash_ack_completions` has not yet * confirmed (so the Rust registry still holds them and, over subc, re-nudges). * Ingest skips these for fresh delivery AND a forced drain RE-ACKs them (the * self-terminating close of the re-nudge loop, C-#3). Entries are removed by * DAEMON RECONCILIATION, not a timer: a forced drain that no longer returns a * task proves the daemon dropped it, so it is safe to forget (R2-T3 — a * time-based TTL could evict a task the daemon still holds, reopening the * double-deliver). Self-drains: the module re-nudges (→ drain → re-ack) until * ack confirms. Insertion-ordered for the FIFO OOM backstop cap. */ deliveredAwaitingAckTaskIds: Set; lastSeenAt: number; }; export declare const sessionBgStates: Map; export declare const SESSION_BG_STATE_IDLE_TTL_MS: number; interface DrainContext { ctx: PluginContext; directory: string; sessionID: string; /** * Plugin-provided OpenCode SDK client (`input.client`). Wake prompts are * sent through this canonical in-process client; OpenCode fixed the * runner-state split that previously required a live HTTP listener workaround. * * Typed `unknown` because the real `@opencode-ai/sdk` `OpencodeClient` * has a generated `promptAsync` signature. The wake closure asserts to the * loose structural `OpenCodeClient` shape after checking it. */ client?: unknown; /** Complete provenance for a subc bg_events nudge. */ nudgeRef?: BgNudgeRef; /** Cached bridge so one nudge resolves drain and ack through one authority. */ resolvedBridge?: AftProjectTransport; } /** * Mark a bg task's completion as consumed by an explicit bash_status wait. * Removes it from pendingCompletions so the next wake/in-turn drain * doesn't double-notify the agent. */ export declare function consumeBgCompletion(sessionID: string | undefined, taskId: string): void; export declare function markBgCompletionDelivered(drainContext: DrainContext, taskId: string): Promise; /** * Pre-mark a task as expected to be consumed inline before the wait loop * starts polling. This is the key suppression mechanism: ingestBgCompletions * will skip push frames for tasks already in consumedTaskIds, so a wake is * never scheduled in the first place. The consume-after-detection path * loses a race when push frame arrives faster than the wait loop's next poll. * * Caller MUST balance with `unmarkTaskWaiting` if the wait loop returns * without seeing terminal status (timeout or pattern-match-without-exit), * so future push frames deliver normally. */ export declare function markTaskWaiting(sessionID: string | undefined, taskId: string): void; /** * Remove a task from the consumed set when the wait loop returned without * seeing terminal status (e.g. timeout or pattern-only match). Without * this, future push frames for the task would be permanently suppressed. */ export declare function unmarkTaskWaiting(sessionID: string | undefined, taskId: string): void; export declare function trackBgTask(sessionID: string | undefined, taskId: string): void; export declare function markExplicitControl(sessionID: string | undefined, taskId: string, trackOutstanding?: boolean): void; export declare function unmarkExplicitControl(sessionID: string | undefined, taskId: string): void; export declare function handlePushedPatternMatch(drainContext: DrainContext & { client: unknown; }, frame: PatternMatchEntry): Promise; export declare function ingestBgCompletions(sessionID: string | undefined, completions: unknown): BgCompletion[]; export declare function handlePushedBgCompletion(drainContext: DrainContext & { client: unknown; }, completion: unknown): Promise; export declare function handlePushedBgLongRunning(drainContext: DrainContext & { client: unknown; }, reminder: BgLongRunningReminder): Promise; export declare function appendInTurnBgCompletions(drainContext: DrainContext, output: { output?: string; } | undefined): Promise; export declare function handleIdleBgCompletions(drainContext: DrainContext & { client: unknown; }): Promise; /** * Subc bg_events wake entrypoint. Over subc, an idle-completion WAKE is a thin * payload-less nudge: the module only nudges while it holds pending completions * (re-armed each tick until acked), so a nudge ALWAYS means "drain me now". This * differs from {@link handleIdleBgCompletions}, whose drain is GATED — once * `forcedDrainCompleted` is set and nothing is locally outstanding, it skips the * drain. A subc completion can be for a task this process never tracked (a prior * session, or one whose outstanding entry was already cleared), so the gated * drain would skip it and the module would re-arm and nudge forever. The * module-side loop is * `crates/aft/src/subc/push.rs::{emit_bg_event_wakes,clear_stale_bg_wakes_for_empty_sessions}`: * it re-emits pending wakes until ack empties the queue, so coalescing a duplicate * while this handler is in flight is safe. This path forces an UNCONDITIONAL drain * so the completion is fetched, delivered, and acked. */ export declare function handleSubcBgEventsNudge(drainContext: DrainContext & { client: unknown; }): Promise; export declare function formatSystemReminder(completions: readonly BgCompletion[]): string; export declare function formatLongRunningReminder(reminders: readonly BgLongRunningReminder[]): string; export declare function formatPatternMatchReminder(matches: readonly PatternMatchEntry[]): string; export declare function extractSessionID(value: unknown): string | undefined; export declare function __resetBgNotificationStateForTests(): void; export {}; //# sourceMappingURL=bg-notifications.d.ts.map