/** * #2661 — Global AI launch budget (emergency cost fuse). * * Every autonomous `claude --print` launch across ALL ruflo daemons in ALL * worktrees/workspaces owned by the current user must pass through this * user-global budget before a process is created. Without it, N worktree * daemons each schedule their own AI workers and aggregate launch volume * scales linearly with worktree count — enough to exhaust a user's Claude * hourly quota silently (13 launches/hour/daemon under the legacy schedule). * * The ledger lives under the user's home directory (NOT the workspace) so * daemons started from different worktrees of the same repository — or from * unrelated repositories — all share one budget: * * ~/.claude-flow/ai-budget.json launch ledger + circuit breaker * ~/.claude-flow/ai-budget.lock O_EXCL mutation lock * ~/.claude-flow/ai-budget-receipts.jsonl launch/deny/pause receipts * * Files are owner-only (0700 dir / 0600 files) and symlinks are rejected * (invariant 9 of #2661). All checks happen BEFORE process creation and the * ledger mutation is atomic under the lock, so two daemons racing for the * last hourly slot cannot both win. * * Default limits (issue #2661 containment): * maxConcurrentGlobal 1 at most one autonomous claude child, user-wide * maxLaunchesPerHour 2 * maxLaunchesPerDay 12 * pauseOnQuotaErrorMinutes 60 circuit breaker on 429/quota responses */ export interface AiBudgetLimits { maxConcurrentGlobal: number; maxLaunchesPerHour: number; maxLaunchesPerDay: number; pauseOnQuotaErrorMinutes: number; } export declare const DEFAULT_AI_BUDGET_LIMITS: AiBudgetLimits; export interface AiBudgetRequest { workerType: string; model: string; /** Worktree/workspace root requesting the launch (recorded for receipts only). */ workspace: string; } export interface AiBudgetPermit { allowed: boolean; permitId?: string; reason?: string; } /** * Heuristic match for Anthropic quota / rate-limit failures. Only ever * applied to ERROR output of a FAILED launch (never to successful analysis * output, which may legitimately discuss "rate limiting" in the user's code). */ export declare function isQuotaErrorText(text: string | undefined): boolean; export declare class GlobalAiBudget { private readonly dir; private readonly ledgerFile; private readonly lockFile; private readonly receiptsFile; private readonly limits; constructor(options?: { baseDir?: string; limits?: Partial; }); getLimits(): AiBudgetLimits; /** * Atomically reserve one launch slot. Denials carry a machine-readable * reason and are receipted. The reservation is counted as a launch * immediately (the hourly/daily invariant is on launches, not completions); * `release()` only frees the concurrency slot. * * Fails CLOSED: if the ledger cannot be read or locked, the launch is * denied — an unaccountable launch is exactly what this fuse exists to * prevent. `RUFLO_AI_BUDGET_DISABLE=1` is the explicit escape hatch. */ reserve(req: AiBudgetRequest): Promise; /** Free the concurrency slot held by a permit. Best-effort. */ release(permitId: string | undefined): Promise; /** * Open the user-global circuit breaker: a quota/429 response from ANY * daemon pauses ALL autonomous Claude launches for the cooldown window. */ recordQuotaError(detail: string): Promise; /** * #2661 root-fix — manual pause, via `ruflo daemon budget pause`. Distinct * from the automatic quota-error circuit breaker only in duration (open- * ended, until explicitly resumed, instead of a fixed cooldown) and * reason text — the enforcement path in reserve() is identical, so a * manual pause is just as hard a stop as a quota-triggered one. */ pause(reason?: string): Promise; /** #2661 root-fix — `ruflo daemon budget resume`. Clears ANY pause (manual or quota-triggered). */ resume(): Promise; /** * #2661 root-fix — structured per-launch token telemetry. Best-effort, * receipt-only: usage is recorded as a distinct receipt keyed by permitId * rather than mutated into the launch ledger, so a usage-recording failure * can never corrupt the budget-enforcement ledger. Only operational * metadata — never prompts or source content. */ recordUsage(permitId: string | undefined, usage: { workerType: string; model: string; inputTokens?: number; outputTokens?: number; durationMs?: number; costUsd?: number; }): void; /** Snapshot for `daemon status` / diagnostics. */ getUsage(): { lastHour: number; lastDay: number; active: number; pausedUntil?: number; pauseReason?: string; /** #2661 — 24h launch counts per worktree/workspace, most active first. */ byWorkspace: Array<{ workspace: string; launches: number; }>; }; private ensureDir; private acquireLock; /** Read + prune the ledger. Caller must hold the lock for read-modify-write. */ private readLedger; private writeLedger; /** * Invariant 10: every launch, denial, and pause emits a receipt. Only * operational metadata is persisted — never prompts or source content. */ private appendReceipt; } export declare function getGlobalAiBudget(): GlobalAiBudget; /** Test hook: reset the singleton (e.g. after changing RUFLO_AI_BUDGET_DIR). */ export declare function resetGlobalAiBudgetForTests(): void; //# sourceMappingURL=global-ai-budget.d.ts.map