/** * Quota auto-resume watchdog for dynamic-workflow runs. * * Why: when a workflow subagent hits a usage cap (GLM 1308 …), upstream stops * the WHOLE run as `stopped(provider)` with a `ProviderStopDetails{kind: * "quota"}` and its notification instructs the agent to call * ResumeWorkflowRun AFTER the reset (workflow-notification-copy.ts:55-64). * Nothing schedules that "after" — the run sits stopped until a human acts. * The bridge owns the process that can wait, so it does: a small persisted * registry of pending resumes, a periodic scan over the project's run journal, * and a timer per entry. * * Detection is deliberately journal-first: the v4 session summary carries the * failure message (with the provider's business code in brackets) but NOT the * structured providerStop/resetAt, and the workflow progress poller gives up * after 10 minutes — long runs would never be seen there. The scan reads * `workflows/runs` (one session-less RPC), keeps rows that are * `stopped(provider)`, and only then resolves the few parent sessions through * `v4/conversation/workflowRuns` to read their failure messages. * * Firing is defensive at every step: quota card first (a known future reset * defers without burning an attempt), then a live state read (a run that is * running again / finished / no longer resumable drops the entry), then the * same `resumeWorkflowRun` command the App's Resume button uses. An accepted * resume is re-verified after {@link VERIFY_AFTER_MS}; only a stopped-again run * with no known reset burns an attempt and backs off. * * Store: `~/.zcode/v2/acp-quota-resumes.json`, shared by every bridge process * and written only via temp+rename with merge-at-write (the dismissals-store * discipline — a torn write would lose every pending resume). */ import type { QuotaResult } from "../quota/types.js"; import type { ZcodeAcpServer } from "../server.js"; /** Scan cadence for newly quota-stopped runs. */ export declare const QUOTA_RESUME_SCAN_INTERVAL_MS: number; /** One pending auto-resume. `attempts` counts resume attempts that did not stick. */ export interface QuotaResumeEntry { runId: string; /** Parent backend session (the run's journal parentSessionId). */ zcodeSid: string; /** Workspace the run belongs to — bridges re-arm only their own project's entries. */ cwd: string; /** ACP alias when known at schedule time (re-resolved at fire time anyway). */ acpSid?: string; name?: string; /** Epoch ms of the next fire. */ resumeAt: number; /** Failed-attempt counter (drives the backoff ladder). */ attempts: number; /** True while the previous resume attempt is inside its verify window. */ awaitingVerify?: boolean; /** * Terminal give-up reason. The journal row stays `stopped(provider)+quota` * forever, so without this marker the scan would re-schedule the same run * every cycle — a retry/give-up loop. A gave-up entry is skipped by scans, * re-arms, and the idle-exit predicate; it expires via the TTL, by which * time the row is out of the backfill window anyway. */ gaveUp?: string; /** * In-flight fire claim (cross-process single-flight). Written under the * settings file lock by {@link claimFire}; every normal mutation (updateEntry * / reschedule / markGaveUp) clears it. Without it, two bridges of the same * workspace (editor + serve/TUI) could both resume one run — a money-level * double execution. */ firing?: { pid: number; at: number; }; scheduledAt: number; updatedAt: number; } export interface QuotaResumeInput { runId: string; zcodeSid: string; cwd: string; name?: string; acpSid?: string; /** Provider-reported reset (Retry-After). Absent → the first fire re-derives. */ resetAt?: number; } /** * Register (or merge) a pending auto-resume. Idempotent per runId; an earlier * resumeAt wins so a duplicate detection never delays an armed timer. * * The whole read-decide-write runs under the store file lock: the merge must * see the CURRENT record (a sibling's in-flight fire claim included — which it * must never stomp) rather than a snapshot read before the lock was taken. */ export declare function scheduleQuotaResume(server: ZcodeAcpServer, input: QuotaResumeInput): Promise; /** * One scan over the project's journal for quota-stopped runs. Best-effort and * side-effect free when nothing matches; never spawns a backend (the scan is * discovery — only a fire may act). */ export declare function scanQuotaStoppedRuns(server: ZcodeAcpServer): Promise; export interface QuotaResumeDeps { /** Injectable for tests; defaults to the real quota card. */ queryQuota?: () => Promise; } /** * Fire one pending entry. Everything is re-checked here because the state may * have moved since scheduling (user resumed/stopped/dismissed the run, quota * already recovered, bridge slept past the reset). * * Cross-process single-flight: the side-effecting tail runs only under a * locked fire claim (see {@link claimFire}); a sibling bridge that loses the * claim re-arms its local timer and lets the winner's store write (reschedule * / retire) become the truth. */ export declare function fire(server: ZcodeAcpServer, runId: string, deps?: QuotaResumeDeps): Promise; /** * Arm the watchdog for one bridge: re-arm its project's persisted entries and * start the periodic scan. Idempotent per server. Scans never spawn a backend * and never throw; a bridge that never sees a backend stays silent. */ export declare function armQuotaResumeWatchdog(server: ZcodeAcpServer): void; /** Is a resume armed for this run? (test/observability hook) */ export declare function quotaResumePending(runId: string): boolean; /** * Is any resume pending for a workspace? The headless serve bridge consults * this in its idle decision: exiting would kill the backend the pending * resume must act on, breaking "the bridge owns the wait" exactly when the * app is asleep (the case the watchdog exists for). Retired (gave-up) entries * do not hold a serve bridge alive. */ export declare function hasQuotaResumePendingForCwd(cwd: string): boolean; /** Read one entry (test hook). */ export declare function quotaResumeEntryForTest(runId: string): QuotaResumeEntry | undefined; /** Patch an entry and drop its timer (test hook; patches may include `firing`). */ export declare function updateQuotaResumeEntryForTest(runId: string, patch: Partial): void; /** Drop every timer and the store file (test hook). */ export declare function resetQuotaResumeForTest(): void; //# sourceMappingURL=quota-resume.d.ts.map