@sema-agent/core
Version:
Stateless, task-oriented AI agent core
50 lines (49 loc) • 3.23 kB
TypeScript
/**
* knob-hygiene #98 — one validator for every RETENTION policy a bundled store accepts.
*
* These knobs share a property nothing else in the engine does: they are the only numbers whose job is
* to DELETE, so an unevaluable one is not merely ignored, it decides. The shapes actually reachable:
*
* - `keep` is consumed as `rows.slice(Math.max(0, keep))` / `i >= keep`. `Math.max(0, NaN)` is `NaN`
* and `slice(NaN)` means `slice(0)` — so a policy whose `keep` arrived as NaN (the ordinary
* `Number(cfg.keep)` on a missing or misspelled field) deleted EVERY terminal row instead of
* keeping N of them. A negative `keep` reaches the same place by a different route.
* - `maxAgeMs` on the roster feeds `now - createdAt <= maxAgeMs`, which is false for NaN — so every
* row reads as expired, the next write persists the survivors, and the whole roster is gone. A
* roster row is a durable ADDRESS; losing one turns a live agent unreachable.
* - the remaining `*MaxAgeMs` arms fail the other way (NaN comparisons are false ⇒ nothing is
* reaped), which is safe but equally silent: retention that quietly stopped running is how a disk
* fills up. They are validated for the same reason — an operator deserves the same answer either way.
*
* Fail-loud rather than fall back to a default: a retention policy is the deployment saying what it
* wants kept, and quietly substituting a different policy is exactly the class of silence this sweep
* exists to remove. Every store passes its options through here BEFORE touching a row, so no
* implementation can drift from another (the RB-74 / RB-87 / RB-111 history in the store family is
* three consecutive "fixed one of the two implementations" misses).
*/
export interface RetentionPolicyKnobs {
/** Age bound in ms. */
maxAgeMs?: number;
/** Row-count bound, whole rows. */
keep?: number;
/** Age bound in ms for the stale-running flip arm. */
staleRunningMaxAgeMs?: number;
/** Row-count bound, whole rows (roster's write-time cap). */
maxEntries?: number;
}
/**
* The family's typed refusal, exported so a seam whose bound has its own ADMISSIBLE RANGE refuses in
* the same grammar under the same code instead of minting a second one. The one in-tree case:
* file-history's boundary cap requires `>= 1` where the row stores accept `0` ("keep no rows" is a
* coherent policy for a ledger; for a history whose every commit mints a boundary it is not — the
* cap would reap the boundary its own commit just published), so that door cannot reuse
* {@link assertRetentionPolicy}'s "0 or more" wording without telling an operator something false.
*/
export declare function refuseRetentionPolicy(label: string, knob: string, value: unknown, requirement: string): never;
/**
* Refuse a malformed retention policy at the door. `undefined` on any field keeps its documented
* meaning — that arm is simply not applied — so a deployment that sets nothing is untouched.
*
* @param label the caller's own name, so the error names the seam the operator configured.
*/
export declare function assertRetentionPolicy(label: string, opts: RetentionPolicyKnobs | undefined): void;