/** * WorkOrderConsumer - the single host-code consumer of system workorders * (Stage 2, plan S2-T3). * * A dedicated interval timer (60s default) claims pending workorders from the * TaskLedger and runs each through workerRun on the operator lane. It runs * UNCONDITIONALLY of MAMA_TRIGGER_LOOP (the publishers are unconditional, so * coupling consumption to an opt-in loop would strand every workorder - plan * A1 BLOCKER). Since v0.28.0 this is the ONLY system run path. * * Serial consumption: one claim at a time, awaited to completion, with a tick * re-entrancy guard (a 260s board run spans 4+ ticks - overlapping ticks skip, * plan G4). Blocking bound = the runner's per-request timeout x maxTurns; no * consumer-level watchdog (plan N2). * * Failure policy (plan G5/M4): ordinary kinds use failWorkOrder plus per-kind * retry limits. Temporal attempts instead run durable generation arbitration, * so a committed effect wins over runner transport failure and retries remain * tied to one generation. Boot recovery routes stale in_progress claims * through the matching policy and emits a separate stale-claim alarm. * * Completion hooks (plan E3/E4): per-kind before/after seams re-home the * post-run host effects the legacy closures owned (board bracket * verification, promotion event re-emission, wiki noUpdate reading). Hook * errors remain observe-only for existing kinds. Temporal work opts into a * blocking verdict, with its durable receipt still authoritative over runner * or verifier transport failures. */ import { type WorkOrderKind, type WorkOrderRecord, type EnqueueWorkOrderInput, type BoardCandidateAttemptState, type TemporalAttemptState, type TemporalWorkFailureResult } from './task-ledger.js'; import { type WorkerRunner } from './worker-run.js'; export interface WorkOrderLedgerPort { claimNextWorkOrder(): WorkOrderRecord | null; completeWorkOrder(id: number): void; failWorkOrder(id: number, reason: string): void; /** Atomic fail+replacement (retry) - one transaction (PR bot round). */ requeueWorkOrder(wo: WorkOrderRecord, reason: string): WorkOrderRecord; inspectTemporalAttempt(attemptId: number): TemporalAttemptState; inspectBoardCandidateAttempt(attemptId: number): BoardCandidateAttemptState; failTemporalWorkOrder(attemptId: number, reason: string, allowRetry?: boolean): TemporalWorkFailureResult; enqueueWorkOrder(order: EnqueueWorkOrderInput): WorkOrderRecord; listStaleClaims(): WorkOrderRecord[]; countPendingWorkOrders(): number; } /** Active owner alarm channel (telegram via the ops sink; may be unconfigured). */ export interface OpsAlarmSink { configured: boolean; send(line: string): Promise; } export type WorkOrderEffectVerdict = { disposition: 'complete'; } | { disposition: 'fail'; reason: string; }; export interface WorkOrderHook { /** Bracket 'before' state (e.g. verifier snapshot at claim time). */ before?: (wo: WorkOrderRecord) => unknown | Promise; /** Post-run effects (verification, event re-emission, outcome reading). */ after?: (wo: WorkOrderRecord, response: string, beforeState: unknown) => WorkOrderEffectVerdict | void | Promise; /** Opt-in only: a missing, malformed, or negative verdict blocks completion. */ verdictRequired?: boolean; } export interface WorkOrderConsumerEvent { type: 'complete' | 'failed' | 'requeued' | 'exhausted' | 'stale-claim' | 'superseded'; workKind: WorkOrderKind; workOrderId: number; reason?: string; /** input+output tokens of the completed run, when the runner reported usage. * Restores the tokens_used telemetry the legacy persona path had * (executeValidatedRun) and the Stage-2 cutover lost. */ tokensUsed?: number; } export interface WorkOrderConsumerDeps { ledger: WorkOrderLedgerPort; runner: WorkerRunner; /** null = brief missing -> the workorder fails loudly (never a silent skip). */ loadBrief: (kind: WorkOrderKind) => string | null; /** Passive owner surface (AgentNoticeQueue via MessageRouter accessor). */ noticeOwner: (summary: string) => void; opsAlarm: OpsAlarmSink; /** Telemetry seam (agent_activity / eventBus) - optional. */ onEvent?: (event: WorkOrderConsumerEvent) => void; /** * Per-order extra run options (Stage-2: per-run envelope issuance). May be * async - envelope issuance persists to the DB. A THROW/REJECT here fails * the order loudly - a run without an envelope would have every model_tool * call denied 'envelope_missing'. */ runOptionsFor?: (wo: WorkOrderRecord) => Record | undefined | Promise | undefined>; log?: (line: string) => void; tickMs?: number; now?: () => number; } /** Per-kind retry budget: attempts start at 1; board/promotion self-heal on * the next publish cycle, wiki events do not re-fire so it retries once. */ export declare const WORKORDER_MAX_ATTEMPTS: Record; export interface SafeCandidateRetryEvidence { readonly phase: 'before_runner_call'; readonly code: 'before_hook_failed' | 'run_options_failed'; } /** * An API failure the CLI printed as response text. Bounded to the head of the * response so a report that merely QUOTES an old error is not misclassified - * the CLI emits the error as (nearly) the whole output, optionally behind the * turns-counter prefix. */ export declare function detectTransportErrorResponse(response: string): string | null; /** * A transient upstream model error the CLI THREW (not in-band). "Selected model * is at capacity", rate limits, overload and 5xx are upstream capacity signals - * the same class as an in-band 529, but delivered as a thrown CLI error rather * than response text. detectTransportErrorResponse only sees in-band bytes; this * names the thrown ones so the operator reads "model-at-capacity" instead of an * anonymous sha256 digest for what is an Anthropic capacity blip, not a MAMA bug. */ export declare function classifyTransientModelError(reason: string): string | null; /** Exported so the boot-time leg declaration and the timer share one number. */ export declare const DEFAULT_TICK_MS = 60000; export declare class WorkOrderConsumer { private readonly deps; private readonly hooks; private readonly lastAlarmAt; private readonly unresolvedTemporalEffects; private readonly unresolvedBoardCandidateEffects; private timer; private consuming; private stopping; private activeTick; constructor(deps: WorkOrderConsumerDeps); registerHook(kind: WorkOrderKind, hook: WorkOrderHook): void; /** * Boot recovery (plan C4/M4): in_progress system rows are crash artifacts * (single serial consumer). Each routes through the SAME failure policy * (a crashed wiki batch requeues once; board/promotion do not), plus a * separate stale-claim alarm. */ bootRecover(): void; start(): void; isStarted(): boolean; /** Graceful: awaits an in-flight tick so shutdown does not race the * operator-DB close into "database is not open" noise (review m4). */ stop(): Promise; /** * Drain pending workorders serially: claim -> await -> next claim. Returns * 'skipped' when a previous tick is still consuming (re-entrancy guard, * plan G4) - long runs span multiple tick firings. */ tick(): Promise<'drained' | 'skipped'>; private runOne; /** * Failure policy layer (plan G5): mark failed, then requeue (attempts+1, * fresh row, same occurrence key - the terminal row freed it) or declare * retries-exhausted with an owner alarm. */ private handleFailure; private handleOrdinaryFailure; /** Durable receipts are the board-candidate completion authority. */ private arbitrateBoardCandidateAttempt; private deferBoardCandidateArbitration; private recheckUnresolvedBoardCandidateEffects; /** Durable row+generation+receipt state always wins over runner prose/errors. */ private arbitrateTemporalAttempt; private deferTemporalArbitration; private recheckUnresolvedTemporalEffects; /** Owner alarm: passive notice + active telegram, deduped per kind (6h). */ private alarm; private emitEvent; private log; } /** The failure shape, or null when none of the known ones match. */ export declare function classifyTemporalFailure(reason: string): string | null; //# sourceMappingURL=workorder-consumer.d.ts.map