import type { CreateLoopInput, CreateWorkflowInvocationInput, CreateWorkflowInput, Goal, GoalAutoExecute, GoalPlanNode, GoalRun, GoalStatus, Loop, LoopRun, LoopStatus, RecoveredLeaseRunSnapshotEntry, RunReceipt, RunStatus, TimeoutMs, StoredWorkflowEvent, WriteRunReceiptInput, WorkflowInvocation, WorkflowRun, WorkflowRunStatus, WorkflowSpec, WorkflowStepRun, WorkflowWorkItem, WorkflowWorkItemStatus, UpsertWorkflowWorkItemInput } from "../types.js"; import { type InitialAgentSessionContractEvent } from "./workflow-provenance.js"; import { type LoopMutationEnvelope, type LoopMutationLookupCaps, type LoopMutationResult, type OperationAuthorityBinding } from "./operation-contract.js"; interface DaemonLeaseFence { daemonLeaseId?: string; now?: Date; claimToken?: string; recoveredRun?: RecoveredLeaseRunSnapshotEntry; } export interface WorkflowRecoveryContext { mode?: "internal" | "operator" | "runner"; now?: Date; loopRunId?: string; claimedBy?: string; claimToken?: string; } export type LoopSchedulingState = Pick; export interface CircuitBreakerTransitionResult { loop: Loop; marker: LoopRun; } export declare const LIVE_EXPIRED_RUN_GRACE_MS = 60000; /** * Ceiling on CONSECUTIVE lease-recovery deferrals for a single run. Past it the * run is abandoned regardless of how alive its process still looks. * * A grace that cannot expire is not a grace. Without this ceiling a run whose * process merely *looks* alive is re-deferred every * {@link LIVE_EXPIRED_RUN_GRACE_MS} forever: never abandoned, never advanced, * and blocking everything queued behind it (station01, 2026-07-31 — a wall of * codewith "Loop run deferred" toasts once a minute and a stalled publish). * * A run only reaches recovery when its lease has ALREADY expired, which means * the runner stopped renewing it — healthy work renews and never enters this * path at all. So the ceiling costs a genuinely-live run nothing, and bounds * total grace at MAX x GRACE (10 min), after which a wedged or * recycled-pid run is released instead of wedging the queue. */ export declare const MAX_LIVE_EXPIRED_RUN_DEFERRALS = 10; export declare const GENERATED_ROUTE_TEMPLATE_IDS: Set; export declare const GENERATED_ROUTE_KEYS: Set; export declare function isGeneratedRouteTemplate(routeKey: string, templateId: string): boolean; export interface LoopRow { id: string; name: string; description: string | null; labels_json: string | null; status: string; archived_at: string | null; archived_from_status: string | null; schedule_json: string; target_json: string; goal_json: string | null; machine_json: string | null; next_run_at: string | null; retry_scheduled_for: string | null; catch_up: string; catch_up_limit: number; overlap: string; max_attempts: number; retry_delay_ms: number; lease_ms: number; expires_at: string | null; expires_after_runs: number | null; created_at: string; updated_at: string; } export interface RunRow { id: string; loop_id: string; loop_name: string; scheduled_for: string; attempt: number; status: string; started_at: string | null; finished_at: string | null; claimed_by: string | null; claim_token: string | null; lease_expires_at: string | null; pid: number | null; pgid: number | null; process_started_at: string | null; /** Nullable for rows read before migration 0014 backfills the column. */ defer_count: number | null; exit_code: number | null; duration_ms: number | null; stdout: string | null; stderr: string | null; error: string | null; goal_run_id: string | null; created_at: string; updated_at: string; } export interface RunReceiptRow { loop_id: string; run_id: string; machine_json: string; repo: string; task_ids_json: string; knowledge_ids_json: string; digest_id: string; started_at: string | null; finished_at: string | null; status: string; exit_code: number | null; summary_json: string; evidence_paths_json: string; created_at: string; updated_at: string; } export interface WorkflowRow { id: string; name: string; description: string | null; version: number; status: string; goal_json: string | null; steps_json: string; created_at: string; updated_at: string; } export interface WorkflowRunRow { id: string; workflow_id: string; workflow_name: string; loop_id: string | null; loop_run_id: string | null; invocation_id: string | null; work_item_id: string | null; scheduled_for: string | null; idempotency_key: string | null; workflow_definition_hash: string | null; manifest_path: string | null; status: string; started_at: string | null; finished_at: string | null; duration_ms: number | null; error: string | null; goal_run_id: string | null; created_at: string; updated_at: string; } export interface WorkflowInvocationRow { id: string; workflow_id: string | null; template_id: string | null; source_kind: string; source_id: string | null; source_dedupe_key: string | null; source_json: string; subject_kind: string; subject_id: string | null; subject_path: string | null; subject_url: string | null; subject_json: string; intent: string; scope_json: string | null; output_policy_json: string | null; created_at: string; updated_at: string; } export interface WorkflowWorkItemRow { id: string; route_key: string; idempotency_key: string; invocation_id: string; source_type: string; source_ref: string; subject_ref: string; project_key: string | null; project_group: string | null; machine_id: string | null; route_scope: string | null; priority: number; status: string; attempts: number; /** Nullable for rows read before migration 0011 backfills the column. */ gate_deaths: number | null; next_attempt_at: string | null; lease_expires_at: string | null; workflow_id: string | null; loop_id: string | null; workflow_run_id: string | null; last_reason: string | null; created_at: string; updated_at: string; } export interface WorkflowStepRunRow { id: string; workflow_run_id: string; step_id: string; sequence: number; status: string; started_at: string | null; finished_at: string | null; exit_code: number | null; pid: number | null; /** Nullable for rows written before migration 0014 added the fingerprint. */ process_started_at: string | null; duration_ms: number | null; stdout: string | null; stderr: string | null; error: string | null; account_profile: string | null; account_tool: string | null; goal_run_id: string | null; created_at: string; updated_at: string; } export interface WorkflowEventRow { id: string; workflow_run_id: string; sequence: number; event_type: string; step_id: string | null; payload_json: string | null; created_at: string; } export interface GoalRow { id: string; plan_id: string; objective: string; status: string; token_budget: number | null; tokens_used: number; time_used_seconds: number; auto_execute: string; max_tokens: number | null; source_type: string | null; source_id: string | null; loop_id: string | null; loop_run_id: string | null; workflow_id: string | null; workflow_run_id: string | null; workflow_step_id: string | null; created_at: string; updated_at: string; } export interface GoalPlanNodeRow { id: string; goal_id: string; plan_id: string; key: string; sequence: number; priority: number; objective: string; status: string; ready: number; token_budget: number | null; tokens_used: number; time_used_seconds: number; depends_on_json: string; created_at: string; updated_at: string; } export interface GoalRunRow { id: string; goal_id: string; plan_id: string; loop_id: string | null; loop_run_id: string | null; workflow_id: string | null; workflow_run_id: string | null; workflow_step_id: string | null; turn: number; phase: string; status: string; node_key: string | null; tokens_used: number; evidence_json: string | null; raw_response_json: string | null; created_at: string; updated_at: string; } export interface DaemonLease { id: string; pid: number; hostname: string; heartbeatAt: string; expiresAt: string; createdAt: string; updatedAt: string; } export interface LeaseRow { id: string; pid: number; hostname: string; heartbeat_at: string; expires_at: string; created_at: string; updated_at: string; } export declare function rowToLoop(row: LoopRow): Loop; export declare function rowToRun(row: RunRow): LoopRun; export declare function rowToRunReceipt(row: RunReceiptRow): RunReceipt; export declare function rowToWorkflow(row: WorkflowRow): WorkflowSpec; export declare function rowToWorkflowRun(row: WorkflowRunRow): WorkflowRun; export declare function rowToWorkflowInvocation(row: WorkflowInvocationRow): WorkflowInvocation; export declare function rowToWorkflowWorkItem(row: WorkflowWorkItemRow): WorkflowWorkItem; export declare function rowToWorkflowStepRun(row: WorkflowStepRunRow): WorkflowStepRun; export declare function rowToGoal(row: GoalRow): Goal; export declare function rowToGoalPlanNode(row: GoalPlanNodeRow): GoalPlanNode; export declare function rowToGoalRun(row: GoalRunRow): GoalRun; export declare function rowToWorkflowEvent(row: WorkflowEventRow): StoredWorkflowEvent; /** * Whether a workflow step's recorded pid is still the step's own process. * * Two paths, because the answer is only as strong as the evidence on the row: * * - **Fingerprinted** (migration 0014 onward): `processStartedAt` holds the * child's real start time, so identity is a TWO-SIDED match via * {@link verifiedProcessStart} — the same strict comparison * `isRecordedProcessAlive` uses for runs, and it fails closed. This is what * rejects a recycled pid: the OS handing that number to an unrelated process * yields a start time that does not match, in either direction. * * - **Legacy** (rows written before 0014, no fingerprint): fall back to the * step's `started_at` as a lower bound. This is a guess, not an identity * check — it rejects a pid older than the step but cannot reject a newer * one — so it stays lenient on unresolvable data rather than killing live * work mid-upgrade. Leniency here is safe ONLY because * {@link MAX_LIVE_EXPIRED_RUN_DEFERRALS} bounds how long a "possibly alive" * answer can hold a run open. Before that ceiling existed, this branch * returning `true` on one unreadable timestamp wedged the runner forever. */ export declare function isLiveStepProcess(pid: number, stepStartedAt: string | null | undefined, processStartedAt?: string | null): boolean; export declare function rowToLease(row: LeaseRow): DaemonLease; export interface ClaimRunResult { run: LoopRun; loop: Loop; claimToken: string; } export interface CreateWorkflowRunInput { workflow: WorkflowSpec; loop?: Loop; loopRun?: LoopRun; scheduledFor?: string; idempotencyKey?: string; invocationId?: string; workItemId?: string; daemonLeaseId?: string; operationAuthority?: OperationAuthorityBinding; /** Internal deterministic fault-injection seam used to verify atomic initial event persistence. */ beforeInitialWorkflowEventPersist?: (event: InitialAgentSessionContractEvent) => void; } export interface CreateGoalInput { objective: string; tokenBudget?: number; autoExecute?: GoalAutoExecute; maxTokens?: number; sourceType?: string; sourceId?: string; loopId?: string; loopRunId?: string; workflowId?: string; workflowRunId?: string; workflowStepId?: string; } export interface CreateGoalPlanNodeInput { key: string; objective: string; dependsOn?: string[]; priority?: number; tokenBudget?: number; } export interface RecordRunProcessInput { pid: number; pgid?: number; processStartedAt?: string; } export interface RecoverExpiredRunLeasesResult { /** Runs whose lease expired with no live process; marked abandoned. */ abandoned: LoopRun[]; /** Runs whose lease expired while their process (group) is still alive; lease deferred. */ deferred: LoopRun[]; /** Runs left unchanged because an admitted private operation has no terminal receipt. */ operationReconciliationRequired: LoopRun[]; } export interface ExpiredRunLeaseCandidate { runId: string; loopId: string; leaseExpiresAt: string; updatedAt: string; } export interface ExpiredRunLeaseCandidatePage { candidates: ExpiredRunLeaseCandidate[]; truncated: boolean; } export interface RecoveredLeaseRunPage { runs: LoopRun[]; snapshot?: RecoveredLeaseRunSnapshotEntry[]; nextOffset?: number; } export interface PruneHistoryOptions { /** Delete terminal runs whose created_at is older than this many days. */ maxAgeDays?: number; /** Always retain at least this many of the most recent runs per loop. */ keepPerLoop?: number; /** Report what would be deleted without deleting anything. */ dryRun?: boolean; /** Injectable clock for tests. */ now?: Date; } export interface PruneHistorySummary { dryRun: boolean; cutoff?: string; keepPerLoop?: number; loopRuns: number; workflowRuns: number; goalRuns: number; } export interface StoreMigrationRows { schemaVersion: number; workflows: WorkflowSpec[]; loops: Loop[]; runs: LoopRun[]; checks: StoreMigrationChecks; } export interface StoreMigrationUnsupportedCounts { workflowInvocations: number; workflowWorkItems: number; workflowRuns: number; workflowStepRuns: number; workflowEvents: number; goals: number; goalPlanNodes: number; goalRuns: number; } export interface StoreMigrationVolatileCounts { daemonLeases: number; activeDaemonLeases: number; runningLoopRuns: number; runningWorkflowRuns: number; runningWorkflowStepRuns: number; leasedWorkflowWorkItems: number; } export interface StoreMigrationChecks { unsupportedCounts: StoreMigrationUnsupportedCounts; volatileCounts: StoreMigrationVolatileCounts; } export interface StoreMigrationRowsOptions { includeRuns?: boolean; } export interface StoreMigrationUpsertOptions { replace?: boolean; } export interface RecordGoalEventInput { goalId: string; turn?: number; phase: GoalRun["phase"]; status: GoalRun["status"]; nodeKey?: string; tokensUsed?: number; evidence?: Record; rawResponse?: unknown; } export declare function workItemStatusForLoopRun(status: RunStatus, attempt: number, maxAttempts: number | undefined): WorkflowWorkItemStatus | undefined; /** * `exit(75)` = `EX_TEMPFAIL`: the sysexits.h "temporary failure, retry later" * code. A gate/worker step that exits 75 (e.g. an account-quota probe that is * still dry) is signalling "not now", not a real failed attempt — so it must * not burn the todos-task redispatch cap and must leave the work item * requeueable rather than persisting as a terminal, dedupe-forever row. */ export declare const WORK_ITEM_TEMPFAIL_EXIT_CODE = 75; /** * Workflow step ids that run BEFORE the worker does any real work. A failure in * one of these (triage/planner gate, or the pre-step worktree preparation) that * dies quickly is a "gate death": the run never executed the worker, so it must * not count toward the redispatch cap (otherwise a purely infrastructural fault * — e.g. a stale worktree registration — silently dead-letters a task that * never actually ran). */ export declare const GATE_STEP_IDS: ReadonlySet; /** A gate-step failure only counts as a gate death when it dies before any real * work could have happened. Worktree-prep deaths are always gate deaths (they * fail before the agent is spawned) regardless of this bound. */ export declare const GATE_DEATH_MAX_DURATION_MS = 60000; /** * Secondary ceiling for CONSECUTIVE gate deaths. Gate deaths refund their * redispatch attempt (the worker never ran), which is correct for transient * infrastructure faults — but a deterministic fault (e.g. a permanently broken * repo path) would otherwise retry forever at the backoff floor. After this * many consecutive gate deaths the work item is dead-lettered (visible in * drain reports) instead of spinning; any run that reaches the worker resets * the streak, and an operator requeue (attempts reset) re-arms it. At the * ~2–4 minute refunded-attempts backoff this bounds a deterministic fault to * roughly an hour of auto-retry before it demands an operator. */ export declare const GATE_DEATH_CEILING = 20; export type NonProductiveFailureKind = "tempfail" | "gate-death"; type ClassifiableStepRun = Pick; /** * Classify a just-finalized *failed* workflow run's decisive failing step. A * non-productive finish (a tempfail retry-signal, or a gate death before the * worker ran) must not count toward the todos-task redispatch cap. Returns the * non-productive kind, or `undefined` when the run represents a real worker * attempt that legitimately counts toward the cap. Pure/exported for tests. */ export declare function classifyNonProductiveStepFailure(steps: ClassifiableStepRun[]): NonProductiveFailureKind | undefined; export declare function scrubbedOrNull(value: string | undefined | null): string | null; /** Scrub secrets then bound size before persisting run stdout/stderr. */ export declare function persistedRunOutput(value: string | undefined | null): string | null; /** Scrub structured string leaves before stringify, then scrub the encoded JSON. */ export declare function persistedJson(value: unknown): string; export declare function persistedWorkflowEventPayload(payload: Record | undefined | null): string | null; export declare class Store { private db; private rootDir; /** Temp dir created for a `:memory:` store, removed in close() so tests/short-lived instances don't leak it. */ private memoryRootDir?; constructor(path?: string); private migrate; private migrations; private createBaseSchema; private createRunReceiptsSchema; private createLoopMutationSchema; /** * Add a column only if it does not already exist. Idempotent — avoids the * "duplicate column name" error that SQLite logs (via libsqlite3, before any * JS try/catch) when re-running an additive migration on a database that has * already been upgraded. Table/column/definition come from hardcoded literals * in {@link migrate}, never user input, so interpolation here is safe. */ private addColumnIfMissing; private createWorkflowRunBackfillIndexes; /** Run `fn` inside a write transaction unless the caller already opened one. */ private transact; private assertDaemonLeaseFence; private assertNoNestedWorkflowGoal; createLoop(input: CreateLoopInput, from?: Date): Loop; getLoop(id: string): Loop | undefined; findLoopByName(name: string): Loop | undefined; requireUniqueLoop(idOrName: string): Loop; private requireArchiveMutationLoop; requireLoop(idOrName: string): Loop; listLoops(opts?: { status?: LoopStatus; labels?: string[]; limit?: number; offset?: number; archived?: boolean; includeArchived?: boolean; name?: string; }): Loop[]; private withLatestRunSummaries; dueLoops(now: Date, limit?: number): Loop[]; updateLoop(id: string, patch: Partial>, opts?: DaemonLeaseFence): Loop; mutateLoop(envelope: LoopMutationEnvelope, authority: OperationAuthorityBinding, opts?: { now?: Date; leaseMs?: number; }): LoopMutationResult; getLoopMutationResult(authority: OperationAuthorityBinding, operationId: string, stepId: string, caps?: LoopMutationLookupCaps): LoopMutationResult | undefined; advanceLoopIfCurrent(id: string, expected: LoopSchedulingState, patch: Partial>, opts?: DaemonLeaseFence): Loop | undefined; tripCircuitBreakerIfCurrent(id: string, expected: LoopSchedulingState, patch: Partial>, marker: { scheduledFor: string; reason: string; }, opts?: DaemonLeaseFence): CircuitBreakerTransitionResult | undefined; private activeLoopReferenceCount; private archiveWorkflowIfUnreferenced; retargetWorkflowLoop(idOrName: string, workflowId: string, opts?: DaemonLeaseFence & { workflowTimeoutMs?: TimeoutMs; }): Loop; updateAgentLoopTimeout(idOrName: string, timeoutMs: TimeoutMs, opts?: DaemonLeaseFence): Loop; createAndRetargetWorkflowLoop(idOrName: string, workflowInput: CreateWorkflowInput, opts?: DaemonLeaseFence & { workflowTimeoutMs?: TimeoutMs; archiveOld?: boolean; }): { loop: Loop; workflow: WorkflowSpec; previousWorkflow: WorkflowSpec; archivedOld?: WorkflowSpec; }; cloneWorkflowWithoutGoalAndRetargetLoop(idOrName: string, opts: DaemonLeaseFence & { workflowName: string; workflowTimeoutMs?: TimeoutMs; archiveOld?: boolean; }): { loop: Loop; workflow: WorkflowSpec; previousWorkflow: WorkflowSpec; archivedOld?: WorkflowSpec; }; renameLoop(id: string, name: string, opts?: DaemonLeaseFence): Loop; archiveLoop(idOrName: string): Loop; unarchiveLoop(idOrName: string): Loop; deleteLoop(idOrName: string): boolean; createWorkflow(input: CreateWorkflowInput): WorkflowSpec; getWorkflow(id: string): WorkflowSpec | undefined; findWorkflowByName(name: string): WorkflowSpec | undefined; requireWorkflow(idOrName: string): WorkflowSpec; listWorkflows(opts?: { status?: WorkflowSpec["status"]; limit?: number; offset?: number; }): WorkflowSpec[]; countWorkflows(opts?: { status?: WorkflowSpec["status"]; }): number; archiveWorkflow(idOrName: string): WorkflowSpec; private generatedRouteArchiveContext; private maybeArchiveGeneratedRouteWorkflow; private maybeArchiveTerminalGeneratedRouteWorkflow; private taskLifecycleTodosPointerContext; private syncSuccessfulTaskLifecycleTodosPointers; createWorkflowInvocation(input: CreateWorkflowInvocationInput): WorkflowInvocation; refreshWorkflowInvocationForWorkItem(workItemId: string, input: CreateWorkflowInvocationInput): WorkflowInvocation; getWorkflowInvocation(id: string): WorkflowInvocation | undefined; listWorkflowInvocations(opts?: { limit?: number; }): WorkflowInvocation[]; upsertWorkflowWorkItem(input: UpsertWorkflowWorkItemInput): WorkflowWorkItem; getWorkflowWorkItem(id: string): WorkflowWorkItem | undefined; findWorkflowWorkItem(routeKey: string, idempotencyKey: string): WorkflowWorkItem | undefined; listWorkflowWorkItems(opts?: { status?: WorkflowWorkItemStatus; routeKey?: string; limit?: number; }): WorkflowWorkItem[]; countActiveWorkflowWorkItems(args?: { projectKey?: string; projectGroup?: string; routeScope?: string; }): { global: number; project: number; projectGroup?: number; }; /** * Number of currently-running workflow steps per resolved auth profile * (account_profile). Drives least-loaded pool selection and the * `--max-per-profile` guard so concurrency spreads across subscription * accounts instead of stacking on one (the provider-side 429 wall). Only * `running` steps are counted: within a workflow steps run sequentially, so a * profile's running count is the number of concurrent workflows executing a * step on that account right now — exactly the concurrency to bound. */ countRunningWorkflowStepsByAuthProfile(): Record; /** * Requeue a terminal admission work item for the next task/event delivery. * By default `attempts` is preserved (used by the bounded stale-terminal * re-admission on the route path, which must keep counting toward the cap). * Pass `resetAttempts: true` for the operator unwedge (`loops routes requeue`) * so a manual requeue is DURABLE rather than one-shot: without the reset a * capped item that finishes terminal once more re-caps instantly. */ requeueWorkflowWorkItem(id: string, patch?: { reason?: string; resetAttempts?: boolean; }): WorkflowWorkItem; /** * Transition a terminal admission work item to `dead_letter`. Used by the * route path when a still-actionable todos task has exhausted the redispatch * cap: instead of silently deduping the same terminal row forever (the "black * hole" — `considered=N created=0` with no signal), the item is moved to a * visible `dead_letter` state so drain reports can surface + count it and an * operator can `loops routes requeue` it. Idempotent: a no-op on an item that * is already dead-lettered. */ deadLetterWorkflowWorkItem(id: string, patch?: { reason?: string; }): WorkflowWorkItem; /** * Refund a redispatch attempt for a *failed* run that never did real work. * Called from {@link finalizeWorkflowRun} the moment a work item is set * `failed`, so the todos-task redispatch cap only ever counts real worker * attempts. A tempfail (`exit 75`) is additionally made requeueable (dropped * back to `queued`, bindings cleared) so its "retry later" contract fires on * the next drain instead of persisting as a terminal, dedupe-forever row. A * gate death stays `failed` (the bounded re-admission picks it up after * backoff once the underlying infra fault clears). Both floor attempts at 0. */ private demoteNonProductiveWorkItems; admitWorkflowWorkItem(id: string, patch: { workflowId: string; loopId: string; reason?: string; }): WorkflowWorkItem; private setWorkflowWorkItemsForLoop; private setWorkflowWorkItemsForWorkflowRun; private setWorkflowWorkItemsForLoopRun; createGoal(input: CreateGoalInput, opts?: DaemonLeaseFence): Goal; getGoal(id: string): Goal | undefined; requireGoal(id: string): Goal; findGoalByLoop(idOrName: string): Goal | undefined; findGoalByRunId(id: string): Goal | undefined; findGoalByContext(context: { loopRunId?: string; workflowRunId?: string; workflowStepId?: string; sourceType?: string; sourceId?: string; }): Goal | undefined; listGoals(opts?: { status?: GoalStatus; limit?: number; }): Goal[]; createGoalPlanNodes(goalId: string, nodes: CreateGoalPlanNodeInput[], opts?: DaemonLeaseFence): GoalPlanNode[]; /** * Insert a plan node, detecting (rather than silently ignoring) conflicts: * an existing (plan_id, key) row means the node is already planned and is * kept; a primary-key collision retries with a fresh id instead of dropping * the node on the floor. */ private insertGoalPlanNode; listGoalPlanNodes(goalIdOrPlanId: string): GoalPlanNode[]; updateGoalStatus(goalId: string, status: GoalStatus, opts?: DaemonLeaseFence): Goal; addGoalUsage(goalId: string, tokens: number, timeUsedSeconds?: number, opts?: DaemonLeaseFence): Goal; updateGoalPlanNode(goalId: string, key: string, patch: Partial>, opts?: DaemonLeaseFence): GoalPlanNode; recordGoalEvent(input: RecordGoalEventInput, opts?: DaemonLeaseFence): GoalRun; listGoalRuns(opts?: { goalId?: string; runId?: string; limit?: number; }): GoalRun[]; createWorkflowRun(input: CreateWorkflowRunInput): WorkflowRun; getWorkflowRun(id: string): WorkflowRun | undefined; requireWorkflowRun(id: string): WorkflowRun; listWorkflowRuns(opts?: { workflowId?: string; loopRunId?: string; limit?: number; }): WorkflowRun[]; listWorkflowStepRuns(workflowRunId: string): WorkflowStepRun[]; getWorkflowStepRun(workflowRunId: string, stepId: string): WorkflowStepRun | undefined; isWorkflowRunTerminal(workflowRunId: string): boolean; startWorkflowStepRun(workflowRunId: string, stepId: string, opts?: DaemonLeaseFence): WorkflowStepRun; markWorkflowStepPid(workflowRunId: string, stepId: string, pid: number, opts?: DaemonLeaseFence): WorkflowStepRun; recordWorkflowStepProgress(workflowRunId: string, stepId: string, progress: { stdout?: string; stderr?: string; payload?: Record; }, opts?: DaemonLeaseFence): WorkflowStepRun; recoverWorkflowRun(workflowRunId: string, reason?: string, _context?: WorkflowRecoveryContext): { run: WorkflowRun; recoveredSteps: WorkflowStepRun[]; }; finalizeWorkflowStepRun(workflowRunId: string, stepId: string, patch: Pick & Partial>, opts?: DaemonLeaseFence): WorkflowStepRun; skipWorkflowStepRun(workflowRunId: string, stepId: string, reason: string, opts?: DaemonLeaseFence): WorkflowStepRun; finalizeWorkflowRun(workflowRunId: string, status: WorkflowRunStatus, patch?: Partial>, opts?: DaemonLeaseFence): WorkflowRun; cancelWorkflowRun(workflowRunId: string, reason?: string): WorkflowRun; appendWorkflowEvent(workflowRunId: string, eventType: string, stepId?: string, payload?: Record): StoredWorkflowEvent; listWorkflowEvents(workflowRunId: string, limit?: number): StoredWorkflowEvent[]; hasRunningRun(loopId: string): boolean; hasRunningRunForSlot(loopId: string, scheduledFor: string): boolean; private hasBlockingRunningRunForOtherSlot; markRunPid(id: string, pid: number, claimedBy?: string, opts?: DaemonLeaseFence): LoopRun | undefined; /** * Record the spawned child's process identity (pid, process group id, start * time) so recovery can signal the whole process group later. */ recordRunProcess(runId: string, info: RecordRunProcessInput, opts?: DaemonLeaseFence): LoopRun | undefined; private hasLiveWorkflowStepProcesses; createSkippedRun(loop: Loop, scheduledFor: string, reason: string, opts?: DaemonLeaseFence): LoopRun; getRun(id: string): LoopRun | undefined; getRunBySlot(loopId: string, scheduledFor: string): LoopRun | undefined; nextRetryableRun(loopId: string, maxAttempts: number, afterScheduledFor?: string): LoopRun | undefined; claimRun(loop: Loop, scheduledFor: string, runnerId: string, now?: Date, opts?: DaemonLeaseFence): ClaimRunResult | undefined; finalizeRun(id: string, patch: Pick & Partial>, opts?: { claimedBy?: string; now?: Date; daemonLeaseId?: string; claimToken?: string; }): LoopRun; heartbeatRunLease(id: string, claimedBy: string, leaseMs: number, now?: Date, opts?: DaemonLeaseFence): LoopRun | undefined; listRuns(opts?: { loopId?: string; status?: RunStatus; labels?: string[]; limit?: number; offset?: number; }): LoopRun[]; listRecoveredLeaseRunsPage(opts?: { snapshot?: RecoveredLeaseRunSnapshotEntry[]; offset?: number; limit?: number; }): RecoveredLeaseRunPage; writeRunReceipt(input: WriteRunReceiptInput, opts?: { now?: Date; }): RunReceipt; getRunReceipt(runId: string): RunReceipt | undefined; listRunReceipts(opts?: { loopId?: string; repo?: string; taskId?: string; knowledgeId?: string; status?: string; limit?: number; }): RunReceipt[]; private deferLiveExpiredRun; recoverExpiredRunLeases(now?: Date, opts?: DaemonLeaseFence & { limit?: number; scanLimit?: number; runId?: string; expectedLeaseExpiresAt?: string; expectedUpdatedAt?: string; }): LoopRun[]; listExpiredRunLeaseCandidates(expiredBefore?: Date, opts?: { limit?: number; }): ExpiredRunLeaseCandidatePage; /** * Read-only counterpart to {@link recoverExpiredRunLeasesDetailed}: runs the * identical SELECT and the identical two-part classification — liveness * (`isRecordedProcessAlive` / `hasLiveWorkflowStepProcesses`) AND the * `defer_count` grace ceiling (`MAX_LIVE_EXPIRED_RUN_DEFERRALS`) — but issues * no UPDATE. `reclaimable` is exactly the set a same-parameters call to * `recoverExpiredRunLeasesDetailed` (without `preserveLiveProcesses`) would * mark abandoned: dead now, OR alive but already past the grace ceiling. * `liveDeferred` is exactly the set it would instead defer: alive AND still * under the ceiling. * * CORRECTED (P1, PR #182 review; superseded fix in #182 itself did not * address this — see hygiene.ts `buildStuckRunReport` for the paired half). * This previously classified any row that "looks alive" as `liveDeferred` * unconditionally, ignoring `defer_count` entirely. Because callers gate the * real mutating call on `reclaimable.length > 0`, that made a live-looking * run's grace-ceiling escalation permanently unreachable — * `recoverExpiredRunLeasesDetailed`, the only place `defer_count` is ever * incremented, was never invoked for such a run, so it sat at * `defer_count=0` forever rather than ever accumulating toward the ceiling * and being abandoned. Reusing the exact * `looksAlive && deferralsSoFar < MAX_LIVE_EXPIRED_RUN_DEFERRALS` predicate * (not a re-derived one) is what keeps this method from drifting from * `recoverExpiredRunLeasesDetailed` again. */ previewExpiredRunLeases(now?: Date, opts?: { limit?: number; scanLimit?: number; runId?: string; }): { reclaimable: LoopRun[]; liveDeferred: LoopRun[]; }; /** * Recover expired run leases and report both outcomes: runs abandoned (no * live process) and runs deferred because their process (group) is still * alive. Entries carry pid/pgid/processStartedAt so the daemon can signal * orphaned process groups (SIGTERM then SIGKILL) after recovery. */ recoverExpiredRunLeasesDetailed(now?: Date, opts?: DaemonLeaseFence & { limit?: number; scanLimit?: number; runId?: string; expectedLeaseExpiresAt?: string; expectedUpdatedAt?: string; refuseAdmittedPrivateOperations?: boolean; excludeClaimedBy?: string; /** * Leave one runner's runs untouched, but only within an explicit set of * loops. Unlike `excludeClaimedBy`, which protects everything a runner * owns unconditionally, this protects only the loops the caller has * established are still recoverable by other means (for Loops: loops a * poll never examined, whose runs that runner is about to take over on * its next poll). A runner's own run on a loop that WAS examined is not * recoverable by takeover, so blanket-excluding it strands it in * `running` with a dead lease indefinitely. * * Expressed as a loop-id set rather than a run-id set on purpose: a * run-id set has to be enumerated by the caller, which both caps * silently at one page and costs a query per loop. It is applied inside * the scan query, BEFORE `LIMIT`, so protected rows never consume the * scan window and starve an unrelated reapable run. */ protectClaimedByInLoops?: { claimedBy: string; loopIds: readonly string[]; }; /** Leave every currently live process untouched, even after the daemon recovery grace ceiling. */ preserveLiveProcesses?: boolean; }): RecoverExpiredRunLeasesResult; /** * Atomically transition a loop to "expired" after N consecutive successful * runs and write the expiry marker run (status "skipped", error = reason). * * Mirrors {@link tripCircuitBreakerIfCurrent}: the expected-state guard makes * a concurrent conflicting mutation a no-op, and the marker is the watermark * that gives a manual resume a fresh success streak. Guarded by the daemon * lease fence like every scheduler transition. */ expireLoopIfCurrent(id: string, expected: LoopSchedulingState, patch: Partial>, marker: { scheduledFor: string; reason: string; }, opts?: DaemonLeaseFence): CircuitBreakerTransitionResult | undefined; expireLoops(now?: Date, opts?: DaemonLeaseFence): Loop[]; countLoops(status?: LoopStatus, opts?: { archived?: boolean; includeArchived?: boolean; }): number; countRuns(opts?: { loopId?: string; status?: RunStatus; labels?: string[]; }): number; exportMigrationRows(opts?: StoreMigrationRowsOptions): StoreMigrationRows; /** * Page through loop_runs for a streaming export. A full `exportMigrationRows` * loads every run's stdout/stderr into memory at once (hundreds of MB on a * busy host); a self-hosted backfill instead pulls stable ordered pages so * peak memory stays bounded. Order is deterministic (created_at, id) so * offset paging over an immutable snapshot never skips or repeats a row. */ exportMigrationRunPage(opts: { limit: number; offset: number; }): LoopRun[]; private countTable; private migrationChecks; upsertMigrationWorkflow(workflow: WorkflowSpec, opts?: StoreMigrationUpsertOptions): WorkflowSpec; upsertMigrationLoop(loop: Loop, opts?: StoreMigrationUpsertOptions): Loop; upsertMigrationRun(run: LoopRun, opts?: StoreMigrationUpsertOptions): LoopRun; /** * Delete old terminal run history: loop runs plus their attached workflow * runs (step runs and events cascade), goal run events, and per-run manifest * directories. At least one of maxAgeDays / keepPerLoop must be provided; * when both are given a run is only deleted when it is older than the cutoff * AND beyond the per-loop retention floor. Running runs are never touched. */ pruneHistory(opts: PruneHistoryOptions): PruneHistorySummary; acquireDaemonLease(input: { id: string; pid: number; hostname: string; ttlMs: number; now?: Date; }): DaemonLease | undefined; heartbeatDaemonLease(id: string, ttlMs: number, now?: Date): DaemonLease | undefined; releaseDaemonLease(id: string): void; getDaemonLease(): DaemonLease | undefined; writeTransaction(fn: () => T): T; close(): void; } export {};