/** * v3 gate wait store — file-backed approval waits. * * Codex's v1-review blocker #4: a workflow gate is RUNTIME state, not an * in-memory chat ask. Its pending/resolved status MUST persist to the runDir * so a daemon restart doesn't lose a pending approval. The journal already * records `gateDispatched` / `gateResolved` (audit truth); this module owns the * materialized, mutable wait files under `runDir/waits/.json` — the * active state the Lark card layer keys off and the restart-recovery scan reads. * * Split of concerns: * - THIS file: the file-wait store + a gate resolver that persists * pending → resolved around an injected decision source. Pure file IO, * bot-agnostic, testable without the daemon. * - daemon (later): supplies `awaitDecision` — posts the Lark approval card * (reusing v0.2's card-builder / card-handler UX) and resolves when the * button is clicked; on restart it re-arms pending waits via * `listPendingWaits`. * * The wait shape mirrors v0.2's `waitKind: 'human-gate'` lineage but is * deliberately scoped to v3's gate needs: no deadline, but options / * approveOptions / approvers are persisted for crash-safe card recovery. */ import type { GateResolver } from './runtime-host-contract.js'; export { canResolveGateWait, normalizeGateWaitInput, selectedResolution, } from './gate-policy.js'; export type GateWaitStatus = 'pending' | 'approved' | 'rejected'; export interface GateWait { waitId: string; nodeId: string; /** The runtime instance this gate belongs to (`A#001`). A revisit makes a * fresh instance + fresh gate; resolve-time validation rejects a stale card * whose instance is no longer the node's effective one (code review). */ instanceId?: string; prompt: string; options: string[]; approveOptions: string[]; approvers: string[]; status: GateWaitStatus; createdAt: number; resolvedAt?: number; /** open_id (or 'system') of the resolver, once resolved. */ by?: string; /** The concrete option selected by the reviewer. */ selected?: string; /** Host-only: the exact frozen provider input this approval covers. */ hostApproval?: { attemptId: string; approvalDigest: string; inputHash: string; }; } export type { GateResolver }; export declare function waitsDir(runDir: string): string; export declare function waitPath(runDir: string, waitId: string): string; /** Host approvals are per frozen attempt, while ordinary gates remain per * runtime instance. Including the attempt number prevents a safe pre-intent * retry from reusing an already-resolved approval file. */ export declare function v3GateWaitId(nodeId: string, instanceId?: string, hostApproval?: { attemptId: string; }): string; /** Write the initial `pending` wait file for a gate. Overwrites any stale * file at the same waitId (a re-dispatched gate). */ export declare function writePendingWait(runDir: string, input: { waitId: string; nodeId: string; prompt: string; } & Partial>): GateWait; /** Read a single wait file, or `undefined` if it doesn't exist. */ export declare function readWait(runDir: string, waitId: string): GateWait | undefined; /** Transition a wait to approved / rejected. Throws if the wait is missing * (a resolution for an unknown gate is a programming error, not a no-op). */ export declare function resolveWait(runDir: string, waitId: string, resolution: 'approved' | 'rejected', by: string, selected?: string): GateWait; /** Cross-process compare-and-set for a pending wait. Exactly one resolver may * transition it; every later contradictory click observes the first result. */ export declare function resolveWaitOnce(runDir: string, waitId: string, resolution: 'approved' | 'rejected', by: string, selected?: string): { wait: GateWait; changed: boolean; }; /** All still-pending waits in the runDir — the daemon's restart-recovery scan * uses this to re-post / re-arm approval cards after a crash. */ export declare function listPendingWaits(runDir: string): GateWait[]; /** * Build the `resolveGate` the runtime injects. Persists the wait as `pending`, * delegates to the daemon-supplied `awaitDecision` (post card + await the * click), then persists the resolution — so the file store is authoritative * for pending/resolved regardless of whether the in-memory decision promise * survives a restart (the daemon re-arms via `listPendingWaits`). * * `awaitDecision` is the only daemon-coupled seam; everything else here is file * IO, which is why this factory is unit-testable with a fake decision source. */ export declare function createFileGate(deps: { awaitDecision: (wait: GateWait) => Promise<{ resolution: 'approved' | 'rejected'; by: string; selected?: string; }>; }): GateResolver; //# sourceMappingURL=gate-wait-store.d.ts.map