import type { HostAdapter } from "#src/host/adapter"; import type { SubagentObserver } from "#src/observer"; import type { SubagentRecord } from "#src/types"; /** * NudgeManager — wakes up the parent agent when a subagent reaches a * terminal state (completed, failed, interrupted, crashed). * * Background: * Subagent spawns are fire-and-forget. The parent agent only retrieves * results by calling `get_subagent_result` — but it has no signal that * a subagent has finished, so it sits idle forever after spawning. * * Delivery goes through the injected `HostAdapter.notifyParent()`, gated * by the adapter's declared `nudgeCapability` (ADR 0001): * - "steer" — interrupts the parent's current turn (pi's default — * "followUp" would queue behind an in-progress turn, * which could delay the nudge indefinitely). * - "followUp" — same text, delivered non-interruptingly. * - "unsupported"— never call notifyParent; log once (not per completion) * that completion must be discovered via * get_subagent_result polling. * Core must never assume a specific capability — only branch on it. * * Batching: * When multiple subagents complete in quick succession, we batch * their nudges into a single message rather than sending N separate * turns. This avoids: * - Wasting model turns on N "the subagent completed" messages * - The parent responding to each nudge one-by-one (e.g., calling * get_subagent_result, then waiting, then being nudged again) * - Queue contention with any user-typed input * * The batch window is adaptive (floor / tail / cap — see `scheduleBatch`): * a lone completion waits `MIN_WINDOW_MS`; a genuinely-later straggler * extends the deadline by `TAIL_MS`, bounded by `MAX_WINDOW_MS`. */ export declare class NudgeManager implements SubagentObserver { private adapter; /** Track which agentIds have already been nudged to avoid duplicates. */ private nudged; /** * Agent IDs the parent actually spawned or resumed in THIS extension process * (this pi session). Only these are eligible for nudges. * * Why this exists: on startup the `StartupReconciler` calls * `tracker.loadFromDisk()` (which populates the cache directly, WITHOUT * `createTask`) and then marks any still-`running` records as `crashed` — * firing `onSubagentCompleted`. Those records belong to a PREVIOUS pi process * (or, historically, to test runs that shared the `.pif/tasks` state dir), not * to the fresh parent that just launched. Nudging the new parent about * subagents it never spawned produces the "storm of stale nudges on restart" * bug. Membership in this set is what distinguishes a live, this-session agent * (seen via `onSubagentCreated`/`onSubagentStarted`) from a reconciled ghost. */ private liveAgents; /** Pending records waiting to be batched into the next nudge. */ private pending; /** Timer handle for the debounced batch send. */ private batchTimer; /** When the current batch window opened (adaptive floor/tail/cap window). */ private windowStartAt; /** Current flush deadline for the open window. */ private deadlineAt; /** Injectable clock (fake-timer friendly) for the adaptive window math. */ private now; /** Whether the "nudges unavailable" notice has already been logged this process. */ private loggedUnsupported; constructor(adapter: HostAdapter, now?: () => number); onSubagentCreated(record: SubagentRecord): void; onSubagentStarted(record: SubagentRecord): void; onSubagentCompleted(record: SubagentRecord): void; onSubagentActivity(_record: SubagentRecord, _activity: unknown): void; /** * Schedule / extend the adaptive batch window (floor / tail / cap). * * On the first pending completion the window opens with a deadline of * `now + MIN_WINDOW_MS` (a floor, so a lone completion still waits briefly * for stragglers to coalesce). Each subsequent completion extends the * deadline to `min(windowStart + MAX_WINDOW_MS, max(currentDeadline, * now + TAIL_MS))`: a near-simultaneous burst all resolves to essentially * the same value (no per-agent creep), a genuine straggler pushes the * deadline by `TAIL_MS`, and the cap bounds the total window. */ private scheduleBatch; /** * Send the current batch of pending records as a single nudge. * * If `pending` is empty (race condition), this is a no-op. Batching and * dedup state (above) update unconditionally regardless of capability, so * a capability change can never cause a double-nudge. */ private flushBatch; /** * Send a structured nudge to the parent agent via the adapter. */ private sendNudge; /** * Send a nudge with up to MAX_RETRY_ATTEMPTS retries on failure. * Each retry waits RETRY_BACKOFF_MS before re-attempting. * On terminal failure, logs an error the user can see in the widget. */ private sendWithRetry; } //# sourceMappingURL=nudge-manager.d.ts.map