export type IdempotencyState = 'reserved' | 'attempting'; /** Which dispatch seam a lease governs. `fresh` = a fresh async-virtual session * (one lease → one throwaway session, #776). `turn` = a follow-up turn on an * EXISTING, potentially long-lived SHARED session (#71). The kind is an * UNFORGEABLE domain separator baked into the on-disk key derivation, NOT a * user-supplied string: a caller cannot craft a `fresh` key that collides with * a `turn` lease (or vice-versa) because the kind participates in the file hash. * Reconcile also reads it to avoid fresh-only teardown (closeSession) on a turn * lease whose session is shared and must survive. Absent on disk = legacy * pre-#71 record → treated as `fresh` (the only kind that existed then). */ export type IdempotencyKind = 'fresh' | 'turn'; export interface IdempotencyRecord { ownerLarkAppId: string; sessionId: string; triggerId: string; requestHash: string; ownerBootId: string; revision: number; state: IdempotencyState; createdAt: number; updatedAt: number; /** Dispatch seam this lease governs (see IdempotencyKind). Omitted on legacy * pre-#71 records → read as 'fresh'. */ kind?: IdempotencyKind; } export type ClaimResult = { kind: 'won'; record: IdempotencyRecord; } | { kind: 'existing'; record: IdempotencyRecord; }; export declare class IdempotencyConflictError extends Error { readonly existing: IdempotencyRecord; constructor(existing: IdempotencyRecord); } /** Non-locking read for the pre-check in trigger-session (a fast reject before * creating a session). Owner mismatch → undefined. Corrupt → THROWS. The * authoritative decision is always re-taken under the lock in claim/takeover. */ export declare function lookup(ownerLarkAppId: string, key: string, kind?: IdempotencyKind): IdempotencyRecord | undefined; /** * Claim (owner, key) for a fresh `reserved` lease, or return the existing one — * all inside the per-key lock (read → decide → durable write is atomic wrt other * daemons/boots). Throws on corrupt/IO (fail-closed) and on payload conflict. */ export declare function claim(input: { ownerLarkAppId: string; sessionId: string; triggerId: string; requestHash: string; ownerBootId: string; key: string; now: number; kind?: IdempotencyKind; }): ClaimResult; /** * Take over an OLDER-boot `reserved` lease with a fresh reserved lease (new * session/trigger), OR return the existing record if it's no longer a takeover * target — all under the lock, re-reading current state (never trusting the * caller's stale `from`). Returns won|existing so the caller handles a loss like * a claim loss (close its new session, don't fork). Throws on conflict/IO. * * - absent now → won (fresh claim). * - present, still the SAME older-boot reserved (identity+revision match) → won (replace). * - present, same payload but changed (attempting / newer revision / different * boot) → existing (someone advanced it; reuse, don't take over). * - present, different payload → conflict. */ export declare function takeover(input: { ownerLarkAppId: string; key: string; expect: IdempotencyRecord; sessionId: string; triggerId: string; requestHash: string; ownerBootId: string; now: number; kind?: IdempotencyKind; }): ClaimResult; /** CAS a record to a new state under the lock. Verifies full identity + revision * before writing (rejects a stale/foreign writer). Returns the written record. */ export declare function transition(ownerLarkAppId: string, key: string, from: IdempotencyRecord, patch: { state: IdempotencyState; now: number; }, kind?: IdempotencyKind): IdempotencyRecord; /** Compare-and-remove: delete the lease ONLY if it still matches `expect` * (identity + revision + state) under the lock. Used to release a `reserved` * lease we created but abandoned before dispatch. Returns a discriminated * RemoveByPathResult (removed | absent | changed) — NOT a boolean — so the * barrier-release caller can tell a clean release (retryable) apart from "the * disk already advanced to attempting under me" (must durably terminalize, not * delete) instead of swallowing both (finding #1). A lock-internal re-read * corruption or an ambiguous unlink error (EIO/EROFS/…) THROWS — the caller * must be able to trust that `removed` means the lease is truly released. */ export declare function compareAndRemove(ownerLarkAppId: string, key: string, expect: IdempotencyRecord, kind?: IdempotencyKind): RemoveByPathResult; /** Enumerate the leases owned by a SINGLE bot (boot reconcile is owner-scoped). * Reads only `idempotency//` — a foreign bot's corrupt lease * lives under a different subdir and is never even opened here, so it can't * block this owner's startup (finding #4). By default a corrupt file under THIS * owner is logged + skipped; with `throwOnCorrupt`, a corrupt OWN lease THROWS * (the reconcile can't prove it converged, so it must fail-closed rather than * silently skip a possibly-unconverged attempting fence). */ export declare function listAllForOwner(ownerLarkAppId: string, opts?: { throwOnCorrupt?: boolean; }): Array<{ file: string; record: IdempotencyRecord; }>; /** Result of a reconcile-time compare-and-remove-by-path: * - removed: on-disk record matched the snapshot and was deleted (converged). * - absent: nothing on disk (already gone — converged, nothing to do). * - changed: the record advanced/changed under us (carries the CURRENT record * so the caller can RECLASSIFY it by its real identity/state/boot * instead of falsely declaring the sweep converged). `sameIdentity` * distinguishes "MY exact lease merely advanced its state/revision" * (e.g. reserved→attempting: a crossed commit-unknown fence I own) * from "a DIFFERENT winner replaced it" (takeover/re-claim: a new * session/trigger/boot). The two demand opposite handling — the * former is a local terminal, the latter must be deferred to the * actual winner and never faked as a local terminal (codex #776 * round-6 findings #2/#3). */ export type RemoveByPathResult = { kind: 'removed'; } | { kind: 'absent'; } | { kind: 'changed'; current: IdempotencyRecord; sameIdentity: boolean; }; /** Reconcile-only compare-and-remove BY PATH (reconcile enumerated the file via * listAllForOwner and holds a snapshot record; the plaintext key isn't * recoverable from the hashed filename). Re-reads under the lock and removes * ONLY if the on-disk record still matches the snapshot's full identity + * revision + state — so a stale reserved snapshot can NOT delete a fence that * has since advanced to `attempting` (finding: old sweep erasing a crossed * commit-unknown barrier). * * Returns a discriminated result rather than a boolean so the reconcile can * tell "converged (removed/absent)" apart from "changed under me" and act on * the latter (finding #2: a bare `false` folded both the changed case AND a * lock-internal corruption into a single value the caller ignored, declaring a * non-convergence a success). The `changed` result carries `sameIdentity` so * the caller never mistakes a DIFFERENT winner's record for its own advanced * fence (findings #2/#3). A lock-internal re-read corruption or an ambiguous * unlink error THROWS (fail-closed — the reconcile aborts startup rather than * bind while a lease is in an unprovable state). */ export declare function compareAndRemoveByPath(fp: string, expect: IdempotencyRecord): RemoveByPathResult; //# sourceMappingURL=idempotency-store.d.ts.map