UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

107 lines (106 loc) • 7.67 kB
/** * design/381 §片2 — the engine's OWN bound on file-history growth, and the door its one knob passes * through. * * The hole this closes: `FileHistoryStore.reap` is a complete, contract-tested GC verb that nothing * in the engine ever called. Every turn of a wired session commits one more boundary, each boundary * is a complete mapping over the tracked set, and no seat anywhere dropped the old ones — so a long * session's history grew without bound, silently, which is precisely the shape the bad-value * loudness rule exists to forbid (a limit nobody states is still a limit; it is just the disk's). * The bundled backends therefore carry the bound THEMSELVES: each one commits a boundary and then * trims its scope back to the retention bound, and a host that wants the GC schedule for itself * says so (`keep: "unbounded"`) rather than discovering the default by running out of disk. * * PROVENANCE OF THE DEFAULT (not a taste call, and not a JSDoc-only constraint): the reference * implementation caps its own file-history at the newest 100 snapshots and evicts the rest — CC * 2.1.250 holds the cap as a module constant (`R1e = 100`) and applies it in the snapshot reducer * (`if (E.length > R1e) R = E.slice(-R1e), <evict>(E.slice(0, E.length - R1e), R, …).catch(…)`), * i.e. keep the newest N by commit order, hand the evicted ones to a best-effort eviction pass, and * never let that pass fail the turn that triggered it. Its eviction unlinks a backup only when no * surviving snapshot references it AND its version is not 1 — the same v1-permanence law this * store's `reap` already implements. design/381 §片2 carries the number over as "边界上限(默认 * 100,store 旋钮)", which is the shape below: engine default, store-level, host-overridable. * * @see {@link import("./retention-policy.js").assertRetentionPolicy} — the sibling row stores' door. * This one is separate because its admissible range is different (see {@link refuseRetentionPolicy}). */ export declare const DEFAULT_FILE_HISTORY_BOUNDARY_KEEP = 100; /** * The file-history retention knob, as a deployment writes it. ONE knob on purpose — the graph has * exactly one unbounded dimension (boundaries per scope); version records and blobs are already * bounded by it, because `reap` drops every version no surviving boundary references (v1 and the * base-unavailable markers excepted, which are per tracked file, not per turn). * * Absent ⇒ {@link DEFAULT_FILE_HISTORY_BOUNDARY_KEEP}. Every other reading of a badly-formed value * is a REFUSAL at construction, never a quiet fallback — see {@link resolveFileHistoryRetention}. */ export interface FileHistoryRetentionPolicy { /** * How many of a scope's NEWEST boundaries survive each retention pass. A whole number `>= 1`, or * the literal `"unbounded"` to switch the store's self-trim OFF and take the GC schedule over * (the host then owns calling {@link import("./file-history-store.js").FileHistoryStore.reap}; * nothing else collects boundaries). * * `0` is REFUSED rather than accepted as "keep nothing": a boundary is minted by the very commit * that would then reap it, so a store configured that way would answer `ok` to every `snapshot` * and `false` to every `canRestore` — a rewind seat that is on, reports success, and can never * restore anything. `"unbounded"` is the ONE spelling of "do not trim"; omitting the knob is NOT * that spelling — it selects the engine default and therefore trims (cross-review round 3 caught * this said the opposite here and in the refusal text, which would have sent an operator who * wanted everything kept down the path that deletes). */ keep?: number | "unbounded"; } /** * The retention door: resolve a deployment's policy to a boundary cap, or REFUSE loudly. * * Returns the cap (a whole number `>= 1`), or `null` for the explicit `"unbounded"` opt-out. Called * at CONSTRUCTION by every bundled backend, so a malformed policy fails where the operator wired it * rather than mid-session inside a GC pass they cannot see. * * The three ways a value can be bad each get their own named exit — none of them falls back to the * default, because a retention knob that quietly became a different number is the failure this whole * seam exists to remove: * 1. GARBAGE — the policy is not a knob bag, a knob is misspelled, or `keep` is not a whole number * (NaN from a `Number(cfg.keep)` on a missing field, `Infinity`, `2.5`, `"100"`, `null`). * 2. OUT OF RANGE — a negative `keep`. (Reached by the same route as NaN in the sibling stores' * history: `slice(Math.max(0, keep))` turns both into `slice(0)` = delete everything.) * 3. CONTRADICTORY — `keep: 0`, well-formed and in the range its siblings accept, but self-cancelling * here; refused with the ONE spelling of "do not trim" named, and with what omission really does * said out loud so the remediation cannot be followed into the opposite outcome. * * @param label the seam the operator configured, so the refusal names it. */ export declare function resolveFileHistoryRetention(label: string, policy: FileHistoryRetentionPolicy | undefined): number | null; /** * Which boundaries survive the pass that `justCommitted`'s commit triggered — the SELECTION half, * shared so the two bundled backends (and an out-of-tree twin) cannot drift on which boundary dies. * * Returns `null` when the scope is still within its bound, which is the answer the caller wants * cheaply: no `reap` call at all, so a session under the cap pays one directory listing per turn and * nothing else. Otherwise returns the keep set for * {@link import("./file-history-store.js").FileHistoryStore.reap} — the newest `keep` ids, with * `justCommitted` PINNED into it unconditionally. * * The pin is not belt-and-braces. `orderedOldestFirst` is whatever commit order its backend can * PROVE, and the two bundled ones prove different amounts: the in-memory reference reads an * insertion-ordered map (exact), while the durable one reads each boundary's own publication stamp, * falling back to file mtime for records written before that field or rebuilt from an import, and * to the entry id for ties. A clock stepped backwards, or a host minting non-monotonic ids of its * own, can still invert two neighbours there. * An ordering that is off by one place costs a different-but-equally-legal retention choice; an * ordering that put the JUST-COMMITTED boundary at the bottom would cost this turn its rewind seat. * Pinning makes the second outcome unreachable by construction rather than by trusting the clock. * * One more place the order is only as good as its source, said plainly rather than assumed away: an * envelope round trip (`exportHistory` → `importHistory`) writes boundaries in CANONICAL — i.e. * lexicographic — entry-id order, so a scope rebuilt from one has entry-id order where it used to * have commit order. For engine-minted uuidv7 ids those are the same order; for a foreign graph's * ids they need not be, and such a scope's oldest-first reading is by id. The consequence is again * only WHICH legal boundary is retained, and the just-committed pin still holds. * * @param orderedOldestFirst every committed boundary id in the scope, oldest first. * @param justCommitted the boundary whose commit triggered the pass. * @param keep the resolved cap (`>= 1`). */ export declare function fileHistoryBoundariesToKeep(orderedOldestFirst: readonly string[], justCommitted: string, keep: number): string[] | null;