UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

573 lines (572 loc) • 37.1 kB
/** * design/339 — the v3 consolidation WRITE PROTOCOL's engine-side module: active-set derivation * (§1.4.3), the gate store / plan store control-plane sidecars (§2.3), option screening (§6.1) and * the model-visible wording the read-side supersession treatment mints (§3.4). * * The derivation here is the ONE predicate every read face consumes (index mechanical rows, * memory_search's default filter, memory_get's disclosure, and the v2 retention/recall knives when * they land) — a second implementation would be a drift seam, exactly the class the challenge * exclusion view closed. */ import type { MemoryEntry, MemoryEntryHeader, MemoryEntryOrigin, NotePatch } from "./types.js"; /** * Derive the SUPERSEDED set from a committed header listing: superseded entry id → the standing * carrier (product) id whose edge suppresses it. `active(x) ⇔ !returned.has(x.id)` over non- * tombstoned entries (tombstones never reach a header listing). * * Edge validity (§1.4.3, the r1-2/r3-2 amended law) — an edge (target, rev) carried by product `p` * is VALID ⇔ all of: * - `p` stands in the listing (a tombstoned carrier lists nothing — its edges die with it); * - `p` is not excluded by the local governance overlay (`excluded`: challenge/lineage-latch — * a QUESTIONED replacement must not keep suppressing its net inputs; the inputs revive, the * information never falls into a double black hole). The overlay is deliberately LOCAL (the * challenge ledger does not travel) — a deployment without the account derives from committed * facts alone and shows MORE evidence, the safe direction; * - `p`'s committed rev still equals the block's `carrierRev` (the CARRIER-side rev anchor, * r3-2): a legitimate edit of the product suspends its edges — the edited product may have * rewritten or retracted what it integrated, and the only accurate copy must not hide outside * the default face. Deep-carrying idempotent touches move no rev and keep the edges; * - the TARGET stands in the listing at exactly the edge's anchored rev (the input-side rev * anchor, D-2a): a later legitimate edit of the superseded entry REVIVES it (new information * is never suppressed by an edge frozen over old bytes); an absent/tombstoned target makes the * edge inert (nothing to suppress — the partial-apply direction is safe by the self-containment * law: an edge exists only on a landed product); * - the target lives in the CARRIER'S OWN SCOPE (§3.3 内闭合, read side). The freeze and the retry * enforce "one plan, one scope" on the WRITE side, but every read consumer here is fed a * MULTI-scope listing (the tool face lists a plane's scopes, index rebuild and the collapse fuse * list every mounted scope), and an edge frozen legally inside scope A can later face a target * in scope B: `computeEntryRev` deliberately excludes scope/slug, so a supported cross-scope * MOVE leaves the anchored rev intact, and an adopted carrier (sync/bundle) is validated for its * OWN scope with its edge list checked for SHAPE only. Consolidation authority is per-scope * (seat + lease), so a scope-A carrier must not pull a scope-B entry out of B's default face. * A whole-scope migration moves carrier and target together and keeps this leg satisfied; only * the split/partial case changes, and it degrades toward REVIVAL — this module's stated safe * direction (a deployment shows MORE evidence, never less). * * CHAIN RULE (§1.4.3, explicit): a carrier that is ITSELF superseded (by a later product) keeps * its edges — supersession is convergence, not discredit; the chain p2→p1→x keeps x suppressed. * Only integrity questioning (the overlay) and the tombstone suspend a carrier's edges. * * NON-EMPTINESS (§1.4.3 定理, r2-1): edges only point at a product's own inputs, and inputs are * committed before the product exists — the supersession relation is a DAG, its maximal elements * are always active, so a non-empty library can never derive an empty active set (pinned by test * with the dual-plan full-coverage construction). */ export declare function deriveSupersededSet(headers: readonly MemoryEntryHeader[], opts?: { excluded?: (id: string) => boolean; }): Map<string, string>; /** memory_get head note for a superseded entry (delivery proceeds — disclosure, never refusal). */ export declare function memorySupersededNote(carrierId: string): string; /** The tag suffixed to a search hit line when a superseded entry is retrieved explicitly. */ export declare const MEMORY_SEARCH_SUPERSEDED_TAG = "[superseded]"; /** * §1.7/§2.3 — the host-injected GLOBAL lease seat (multi-node deployments). The protocol's SAFETY * never depends on it (§1.7 four-independence table) — the lease is COST suppression (one * distillation fleet-wide) plus the fencing token that shrinks a stale holder's overlap window to * a single applyPatches batch. `acquire` answers `undefined` when the lease is NOT held; a held * answer may carry a monotonic `token` whose CURRENT value is re-checked before every apply batch * (a changed token aborts the batch — the plan stays replayable under the new holder). */ export interface ConsolidationLeaseSeat { acquire(scope: string): Promise<{ token?: string; } | undefined> | { token?: string; } | undefined; renew?(scope: string): Promise<{ token?: string; } | undefined> | { token?: string; } | undefined; release?(scope: string): Promise<void> | void; } /** design/339 §6.1 — the `MemoryEngineOptions.consolidation` seat (absent = OFF, the v3 default). */ export interface MemoryConsolidationOptions { /** Fuse ratio (0,1]: one plan's supersession targets ≤ min(N-1, max(floor, ⌊ratio×N⌋)). */ supersedeRatioCap?: number; /** Small-library floor (positive integer), capped by the N-1 ceiling. */ supersedeAbsoluteFloor?: number; maxProductsPerRun?: number; maxInputsPerProduct?: number; maxDirectedPatchesPerPlan?: number; minRunIntervalMs?: number; minSessionsBetweenRuns?: number; /** design/376 C-2② — the scan-refusal fuse's ratio leg, in (0, 1]: scan findings refuse the * affected PRODUCT (disclosed beside eligibility drift), and the whole plan refuses only when * more than max(1, ⌊ratio × submitted products⌋) of them fail the scan (the floor keeps the * per-product tolerance alive on 1-4 product plans — the drive lane's common size) — see * {@link scanRefusalCeiling}. Default 0.25. */ scanRefusalRatioCap?: number; /** design/376 C-2② — the fuse's absolute leg (positive integer): the large-batch upper bound on * tolerated scan refusals per plan, so the ratio cannot scale the tolerance without limit. * Default 8 (the point where the 0.25 ratio hands over at 32 submitted products — the family's * "小 N" quantification; below that the ratio leg alone governs). */ scanRefusalMaxPerPlan?: number; /** design/376 C-3 — the EXPLICIT escape hatch for the unconditional instruction-form gate: * instruction-form consolidation products (`type: "feedback"` / v2 hint carriers) are refused * by default REGARDLESS of the visible set's markers — a clean-set instruction product is the * model-reachable privilege route (N ordinary notes in, one instruction entry out). `true` is * the only accepted value (a posture declaration, like {@link multiNode}); it opens the * CLEAN-set arm only — a marked visible set refuses instruction forms under the laundering * hard gate no matter what (privilege and hard gate enter and exit through one door). */ allowInstructionProducts?: true; /** D-13a — the multi-node POSTURE declaration: present ⇒ `lease` must be injected (constructor * refusal otherwise). Multi-node without declaring is OUT OF CONTRACT (structurally * undetectable — the declaration turns a silent omission into an explicit choice). */ multiNode?: true; /** The global lease (§1.7). Absent ⇒ the single-machine plan-seat CAS is the only mutex. */ lease?: ConsolidationLeaseSeat; } /** * D-10 — factory defaults. CC-product-default transplants, NOT sema calibrations (178 ①-3): the * 24h/5-session cadence mirrors the background-consolidation cadence CC-family products ship; * 0.25/floor-4 mirror the mass-deletion fuse family's calibration posture (MASS_DELETION_FUSE_RATIO * 0.5 halved — a LOGICAL delete rides on top of the physical fuse, so its单窗 ceiling is stricter); * 64/32/32 are the "小 N" ruling's quantification. Hosts can configure every one. */ export declare const CONSOLIDATION_DEFAULTS: { readonly supersedeRatioCap: 0.25; readonly supersedeAbsoluteFloor: 4; readonly maxProductsPerRun: 64; readonly maxInputsPerProduct: 32; readonly maxDirectedPatchesPerPlan: 32; readonly minRunIntervalMs: number; readonly minSessionsBetweenRuns: 5; readonly scanRefusalRatioCap: 0.25; readonly scanRefusalMaxPerPlan: 8; }; /** * §2.2 — the HARD throttle floor: even a `force` run cannot start a second same-scope run within * this window of the previous one (cost floor + double-run suppression; the mutex covers overlap, * this covers thrash). The window is `min(minRunIntervalMs, THIS)` — the knob is reused as the * cap, never exceeded — because a full-`minRunIntervalMs` hard throttle would make `force` * structurally dead (the doc's force-bypassable time gate and its unbypassable throttle share the * knob; this floor form is the one reading that keeps both meaningful, disclosed). */ export declare const CONSOLIDATION_FORCE_THROTTLE_FLOOR_MS = 60000; /** The bounded distinct-session ring (§2.3 sessionsSince dedup). */ export declare const CONSOLIDATION_SESSION_RING_MAX = 64; /** * the seat's FREEZE GRACE: reconcile's orphan-release arm treats a seat whose plan file * is absent as crashed ONLY once the claim is at least this old. Calibration: the freeze between * the seat CAS and the plan file's durable write is committed READS only (headers + a bounded * getByIds) — seconds on any healthy store; ten minutes is orders of magnitude past it, while a * genuinely crashed claim still self-heals within one recommendation cadence. Inside the window * reconcile reports the claim as in-flight instead of releasing it. */ export declare const CONSOLIDATION_SEAT_FREEZE_GRACE_MS: number; export interface ScreenedConsolidationOptions { supersedeRatioCap: number; supersedeAbsoluteFloor: number; maxProductsPerRun: number; maxInputsPerProduct: number; maxDirectedPatchesPerPlan: number; minRunIntervalMs: number; minSessionsBetweenRuns: number; scanRefusalRatioCap: number; scanRefusalMaxPerPlan: number; allowInstructionProducts: boolean; multiNode: boolean; lease?: ConsolidationLeaseSeat; } /** * Screen the consolidation options at engine construction (#123 bad-value loudness: every bad * value refuses with a code, never folds to a default). Two structural refusals beyond values: * - D-12a: `provenance: "off"` × consolidation present is a CONSTRUCTION refusal — off means "no * origin is ever minted", while consolidation's fold law must be able to mint (a marked input's * product would otherwise commit unmarked = the laundering window). Enable carry or disable * consolidation. * - D-13a: `multiNode: true` without an injected lease refuses — the declaration exists exactly * to make that omission explicit. */ export declare function screenConsolidationOptions(raw: MemoryConsolidationOptions, provenance: "off" | "carry"): ScreenedConsolidationOptions; /** §1.2-2 — the supersession fuse ceiling: min(N-1, max(floor, ⌊ratio×N⌋)); N ≤ 1 ⇒ 0 (one plan * may NEVER empty the active set — the N-1 ceiling is unconditional, G21). */ export declare function supersessionFuseCeiling(activeSetSize: number, opts: Pick<ScreenedConsolidationOptions, "supersedeRatioCap" | "supersedeAbsoluteFloor">): number; /** * design/376 C-2② — the scan-refusal fuse ceiling: how many scan-refused products ONE plan may * carry before the whole plan refuses instead of committing around them. * `min(max(1, ⌊ratio × submitted⌋), absolute max)`; the ratio leg governs small plans, the * absolute leg is the large-batch upper bound, and the FLOOR of one keeps C-2①'s per-product * tolerance alive at the drive lane's common plan sizes — under the default 0.25 ratio a bare * floor answers 0 for every ≤3-product plan, so one stray refused product re-killed the whole * plan there, exactly the shape C-2① was adopted to remove (an adopted review finding). The * floor is NOT a hole for the fully-refused tiny plan: a plan whose every product is refused * (and which carries no directed work) still dies at the freeze-emptied C-2③ backstop, never a * quiet mostly-no-op — what the floor tolerates is exactly one refused product beside at least * one survivor. Submitted count = the proposal's product count (per THIS plan, never a * cross-run tally). */ export declare function scanRefusalCeiling(submittedProducts: number, opts: Pick<ScreenedConsolidationOptions, "scanRefusalRatioCap" | "scanRefusalMaxPerPlan">): number; /** §3.1 — the type axis of the eligibility predicate: episodic entries are read-only evidence * (never candidates, never supersession/directed targets, A-7); procedural promotion is a v3 * follow-on ticket. Unknown/absent types are ordinary semantic notes — eligible. */ export declare function consolidationTypeEligible(type: string | undefined): boolean; export declare const CONSOLIDATION_GATE_FILE = "consolidation-gate.json"; export declare const CONSOLIDATION_PLANS_DIR = "consolidation-plans"; export declare const CONSOLIDATION_INTENTS_FILE = "consolidation-intents.json"; export interface ConsolidationGateRow { /** Last COMPLETED run's settle time (ms epoch). */ lastRunAt?: number; /** Distinct terminal-harvest sessions since the last run (count + bounded dedup ring). */ sessions: { count: number; ring: string[]; }; /** The plan-seat mutex: the one non-terminal (or conflict-parked) plan of this scope. */ openPlanId?: string; /** When the seat was claimed (ms epoch) — the reconcile orphan-release arm's staleness anchor * (a freeze legitimately computes for a while between the seat CAS and the plan * file's durable write; an absent file alone must never read as a crash). */ seatClaimedAt?: number; /** §1.4.5 collapse-fuse baseline (last observed active-set size). */ activeSetBaseline?: number; /** Once-per-crossing edge for the recommendation notice. */ lastRecommendedAt?: number; /** The last force requestId whose FORCED announcement was enqueued (additive; design/376 §1.1-4): * a RUN-scoped force carries ONE requestId across every cycle of a multi-cycle run, and the * audit announcement is deduplicated on it — one disclosure per run, never one per cycle (a * cold start would otherwise flood the next session's injection face with a dozen identical * lines). A NEW requestId re-arms the disclosure. The dedup is the ONLY record: this row keeps * just the LAST id (no history structure exists — the gate row is mutated in place, the * announcement queue is drained destructively, and plan files do not mark forced-ness), so * per-force facts beyond the one announcement are not persisted, and a caller that reuses a * fixed requestId across runs keeps the disclosure silent for as long as it does. */ lastForcedRequestId?: string; /** Run watermark: increments on every completed/discarded settlement (the stale-proposal gate). */ epoch: number; /** §1.2-1 — the last COMPLETED run's full-eligible-set fingerprint (id → committed rev): * change-driven increments diff against THIS — a candidate is an eligible entry that is new * to the map or whose rev moved (eligibility flips INTO the set read as "new": the map only * ever holds eligible entries). Absent/lost ⇒ full candidates (conservative: 多看不少看). */ fingerprint?: Record<string, { rev: string; }>; /** The last snapshot taken for this scope (the engine's visible-set account, §1.2-2 r3-1: the * fold/instruction authority is what the engine actually SERVED — recorded here, keyed by the * cycle token; a newer snapshot replaces it, and commit refuses a token that no longer * matches). `candidates`: the served (incremental) id → rev set — the edge-target legality * domain; `eligible`: the FULL eligible id → rev map at snapshot time (the next fingerprint on * completion); `marked`: any served candidate carried a committed external-origin marker; * `markedIds` (additive, design/376-C1 D-1b): WHICH served candidates carried it — the * attested freeze's world-movement baseline (a candidate marked NOW but absent here turned * marked after the snapshot ⇒ the mint's clean-arm premise is stale). Absent on rows written * before the field existed: an attested freeze over such a row takes the conservative arm * (ANY currently-marked served candidate refuses stale — no baseline means no proof the mark * predates the mint, and refusing to re-mint beats guessing). */ snapshot?: { token: string; at: number; epoch: number; candidates: Record<string, string>; eligible: Record<string, string>; marked: boolean; markedIds?: string[]; }; } interface ConsolidationGateFile { v: 1; scopes: Record<string, ConsolidationGateRow>; } /** Locked, journaled gate update (the strict family's exact discipline). */ export declare function updateConsolidationGate<T>(controlDir: string, fn: (file: ConsolidationGateFile) => { next?: ConsolidationGateFile; result: T; }): T; /** Lock-less journal-aware gate read (corrupt ⇒ throws — the verbs' fail-closed arm). */ export declare function readConsolidationGateFile(controlDir: string): ConsolidationGateFile; /** True ⇔ the gate store file exists at all (G1's zero-file pin reads this negatively). */ export declare function consolidationGateFileExists(controlDir: string): boolean; /** * design/376 slice ③ (#437②) — the gate read face's DISCRIMINATED answer * ({@link import("./engine.js").MemoryEngine.readConsolidationGateState}): `present` carries the * row; `absent` means the store is readable and simply has no row for the scope (a never-run * scope, or no gate file at all); `corrupt` means the store cannot be trusted and carries the * refusal detail. The undiscriminated `readConsolidationGate` face folds corrupt into `undefined` * — an operator (or a driver's scheduling read) could not tell a healthy never-run scope from a * store every verb will refuse fail-closed. */ export type ConsolidationGateRead = { state: "present"; row: ConsolidationGateRow; } | { state: "absent"; } | { state: "corrupt"; detail: string; }; /** * §2.2 — does the row hold a recommendation no COMPLETED run has consumed? `true` ⇔ a crossing was recorded * (`lastRecommendedAt`) and no run has settled since. The recommendation EDGE in {@link recordConsolidationSession} * is exactly this fact's negation, and the settle-tail auto-run trigger asks this same function of the gate row: * ONE source for "is a run owed", so the edge can never sit disarmed while the trigger believes nothing is owed. */ export declare function recommendationUnconsumed(row: Pick<ConsolidationGateRow, "lastRecommendedAt" | "lastRunAt">): boolean; /** §2.2 — the ONE reading of "how many distinct sessions has this scope banked since its last completed run", for every gate comparing it against `minSessionsBetweenRuns` and every sentence reporting it. The stored `count` includes the most recent harvest's session, and that one is never material to a decision taken on its own behalf: this bookkeeping also runs on the suspend path, so it may still be running, and at a terminal harvest its notes are the ones just written. Banked is one less than stored; an absent row banks nothing. * Never re-derived — two gates comparing two numbers is how a run is admitted for a crossing that was never minted, and how a refusal reports a figure the recommendation notice contradicts. */ export declare function sessionsSinceExcludingCurrent(row: Pick<ConsolidationGateRow, "sessions"> | undefined): number; /** * §2.2 — the engine-minted session count (harvest-tail bookkeeping; ≤1 row write per terminal * harvest; the OFF mode never calls this — D-9a zero-write). Returns the recommendation edge: * `recommended` is true exactly ONCE per threshold crossing (once-per-越线沿; a completed run * resets the edge). * * The ring records the calling session (dedup), so `count` INCLUDES the caller; the gate here and the number beside it are {@link sessionsSinceExcludingCurrent}, which takes it back out. */ export declare function recordConsolidationSession(controlDir: string, scope: string, sessionId: string, opts: ScreenedConsolidationOptions, now: () => number): { recommended: boolean; sessionsSince: number; }; /** * §1.4.5 — the active-set COLLAPSE fuse: record the freshly derived active-set size against the * stored baseline; a single-window shrink beyond the ratio cap answers the collapse fact (the * caller discloses via incident + announcement — the read face NEVER refuses service over it). * Baseline loss ⇒ re-baseline silently (conservative: one extra disclosure at worst next window). */ export declare function recordActiveSetBaseline(controlDir: string, scope: string, size: number, ratioCap: number, _now: () => number): { collapsed?: { from: number; to: number; }; }; /** §1.5 — a directed-change intent (TERMINAL-STATE form: the intent names the target end state, * never an increment — the precondition for replay/re-judgment converging at any interleaving). */ export type ConsolidationIntent = { requestId: string; kind: "forget"; entryId: string; } | { requestId: string; kind: "rewrite"; entryId: string; body: string; name?: string; description?: string; type?: string; }; /** §1.1 — one proposed product (driver output = DATA, zero carriage authority: origin/distilled * in a proposal do not exist as fields — the engine computes both at freeze). The design/376-C1 * attestation does not breach this invariant: `mintExposure` on the PROPOSAL declares how the * prompts were composed, it carries no origin value and can never set one — the engine still * computes every origin itself (per product under attestation, run-level otherwise). */ export interface ConsolidationProductProposal { name?: string; description?: string; type?: string; body: string; /** The product's COMPLETE input attribution; `supersede: true` rows are the proposed edges. */ inputs: Array<{ id: string; supersede?: boolean; }>; } export interface ConsolidationProposal { scope: string; products: ConsolidationProductProposal[]; intents?: ConsolidationIntent[]; /** * design/376-C1 D-2 — the HOST-CODE attestation about prompt composition: `"partitioned"` * declares that the proposal was minted under exposure-partitioned calls, i.e. every all-clean * declared product's authoring chain was served NO byte of any committed-origin (marked) entry * (the clean arm's grouping/writing calls saw the clean subset only — mechanical isolation, * auditable from the archived transcript). The freeze folds origin PER PRODUCT only under this * attestation; absent (or any other spelling — refused structurally, closed set), the freeze * keeps the run-level single-value fold: any marked candidate in the served set marks EVERY * product (today's safe default, and the permanent default for foreign minters). * * Trust plane, stated in full: this is a host-code attestation, NOT driver/model output — the * drive re-assembles the proposal it commits (`{ scope, products, mintExposure }`), so model * output can never populate it. Attesting falsely is a HOST defect outside the threat model, * the same plane as a mis-declared `ToolSpec.contentOrigin` — and unlike most host mistakes it * stays auditable after the fact: the archived clean-arm grouping prompt must contain zero * marked-entry bytes, a mechanical check any reader can re-run against any plan that claims * this attestation. */ mintExposure?: "partitioned"; } export type ConsolidationDirectedState = "pending" | "applied" | "satisfied" | "conflict"; export interface ConsolidationDirectedPatch { intentRequestId: string; op: "update" | "delete"; id: string; baseRev: string; /** update: computeEntryRev of the frozen terminal entry (the satisfied-judgment anchor, r2-4); * absent for delete (the delete terminal is ABSENCE — probed, not hashed). */ plannedPostRev?: string; entry?: MemoryEntry; state: ConsolidationDirectedState; /** delete only (§1.5-2): who reached the terminal — "self" when THIS plan's apply landed it, * "unattributed" when a replay found the terminal already standing (the F9 crash micro-window: * audit degrades, the judgment never does). */ attribution?: "self" | "unattributed"; } export type ConsolidationPlanState = "open" | "applying" | "completed" | "conflict" | "discarded"; export interface ConsolidationPlanFile { /** Plan schema version. `2` ⇔ the plan carries {@link freezeRefusedInputIds} — that member is a * LOAD-BEARING settle input (C-2④ keeps the named ids out of the completion fingerprint), and * a reader that predates it would accept the file as v1, ignore the member, and settle a * fingerprint that stamps the refused members (silently blinding the incremental face — the * exact hole the field closes). Version-gating the member makes such a reader answer its * "unrecognized version" corrupt arm instead: the plan parks loudly for the valve, never * settles wrong (mixed-fleet crash window between the durable plan write and gate settlement). * Plans without the member stay v1 — zero noise on the common path. */ v: 1 | 2; planId: string; scope: string; requestId: string; createdAt: number; cycleToken?: string; epoch: number; /** r3-1 — the ENGINE-VISIBLE-SET fold fact this run froze under (authority = what the engine * served, never the proposal's declaration). */ visibleMarked: boolean; /** The RUN-LEVEL single-value fold, when it applied (design/376-C1 D-6): present ⇔ this plan * froze UN-attested over a marked served set, and this one value rides every product. An * attested (per-product) plan omits it — each product's origin lives in its own frozen bytes * below, and a single plan-level value would misdescribe a mixed plan. */ foldedOrigin?: MemoryEntryOrigin; /** design/376-C1 D-6 — the proposal's accepted attestation, echoed for audit (informational: * apply is byte-faithful to `products` and never re-reads this; a reader that predates the * field ignores it and replays correctly, which is why the plan version does NOT bump). */ mintExposure?: "partitioned"; /** Frozen products — FULL entry bytes (id/frontmatter incl. distilled + origin/body/rev). */ products: MemoryEntry[]; productStates: Record<string, "pending" | "applied" | "conflict">; /** design/376 C-2④ — the declared input ids of every product the FREEZE refused (scan findings * and eligibility drift alike). Settlement keeps these ids OUT of the completion fingerprint: * a scan-refused input's rev never moved, so stamping it would make every later incremental * snapshot read it as unchanged and never offer it again — the refused member must stay a * candidate. Absent on plans frozen before the field existed, and on plans with nothing * refused (either way the settle filter simply has nothing extra to exclude). */ freezeRefusedInputIds?: string[]; directed: ConsolidationDirectedPatch[]; intents: Array<{ requestId: string; state: "pending" | "settled" | "abandoned"; }>; state: ConsolidationPlanState; audit: Array<{ at: number; event: string; requestId?: string; detail?: string; }>; } /** * Plan ids are ENGINE-MINTED uuids, but the host-facing verbs (`resolveConsolidationPlan`, * `reconcileConsolidation` via the gate row) echo them back as ARGUMENTS — and the id doubles as a * control-plane FILE NAME, so a crafted spelling (`../…`, an absolute path) would escape the plans * directory on the read AND on the corrupt-custody RENAME (an outside pass's security finding, * adopted). One grammar (the entry-id family's shape), judged before any path join — the 幽灵行 * guard's refusal posture: an invalid spelling reads as absent/refused, never as a path. */ export declare function isValidConsolidationPlanId(value: string): boolean; export declare function writeConsolidationPlan(controlDir: string, plan: ConsolidationPlanFile): void; export type ConsolidationPlanRead = { state: "ok"; plan: ConsolidationPlanFile; } | { state: "absent"; } | { state: "corrupt"; detail: string; }; export declare function readConsolidationPlan(controlDir: string, planId: string): ConsolidationPlanRead; export declare function listConsolidationPlanIds(controlDir: string): string[]; /** F5 custody — a corrupt plan file the valve DISCARDS is moved aside (never deleted: the bytes * are the only record of what the plan was). Returns the custody file name. */ export declare function quarantineCorruptPlan(controlDir: string, planId: string): string | undefined; export interface ConsolidationIntentCredentialRow { requestId: string; state: "settled" | "abandoned"; planId: string; at: number; patches: Array<{ op: "update" | "delete"; id: string; attribution?: "self" | "unattributed"; }>; } /** * Record intent credentials — IDEMPOTENT on requestId (F10: two plans racing one intent cannot * double-settle; the first credential row stands, a later contradictory write is dropped with the * losing verdict returned to the caller for audit). Returns the requestIds actually written. */ export declare function recordIntentCredentials(controlDir: string, rows: readonly ConsolidationIntentCredentialRow[]): string[]; export declare function readIntentCredentials(controlDir: string): Record<string, ConsolidationIntentCredentialRow>; /** Derive a product's slug from its proposed name (memory filename charset; collisions are the * backend's deterministic -n suffix). */ export declare function deriveProductSlug(name: string | undefined, fallback: string): string; /** The refusal error every consolidation verb throws (structured: stable `code` + the per-item * disclosure list — G5/G7/G14's "结构化拒+披露" shape). */ export declare class ConsolidationRefusedError extends Error { readonly code: string; readonly reasons: string[]; /** * #495 — THIS refusal's own occurrence identity, minted where the fact is created (one throw, one * id) rather than at whatever face later describes it. The notice dialect * ({@link import("./engine.js").memoryConsolidationRefusedNotice}) reads it off the error, so the * throw and its announcement name the same event however many times a host renders it: a repeated * gate refusal (same `code`, same `scope`, byte-identical `message`) is otherwise indistinguishable * from one refusal logged twice. * * Per-occurrence by construction, and correctly so: a refusal is a pure in-process event with no * durable seat — nothing persists it, so nothing replays it, and two refusals ARE two facts. (The * fail-open shape #476 rejected is a per-DELIVERY mint for families whose occurrences do return * from a durable store and must answer the same id each time; this is the opposite geometry.) */ readonly occurrenceId: string; constructor(code: string, message: string, reasons?: string[]); } /** A NotePatch list for one frozen product (the §1.3 add-only arm's one spelling). */ export declare function productAddPatch(product: MemoryEntry): NotePatch; /** * Why a driver run stopped — ONE closed set shared verbatim by the run receipt and the * `memory.consolidation_incomplete` notice (two vocabularies for the same fact would let an * operator-facing channel and the host-facing receipt drift). Non-`converged` values mark a * library in a PARTIAL fold — a transitional state the readings show is worse than unfolded, so * it must never pass silently: * - `converged` — nothing left to fold (the only completed-run value; includes the * empty-plan no-op: a model that proposes nothing commits nothing); * - `fuse_residue` — the supersession fuse admits no remaining product; the residue is named * and the next run's full snapshot re-distills the shrunken library; * - `max_cycles` — the cycle bound elapsed before a fixpoint; * - `parked` — a plan settled conflict (or a corrupt plan holds the seat); the host * valve `resolveConsolidationPlan` is the only way forward; * - `refused` — an engine verb refused and the drive could not continue this run; * - `driver_failed` — the DRIVER half could not complete. Two grains, one verdict: a failed * MINT (model unreachable/unreadable/over the repair budget) settles with * zero writes, zero seat and an unburned time gate; a run whose surviving * plan reached its fixpoint while the driver left groups unfolded (writing * failures, or products dropped stale against inputs edited mid-run) settles * here TOO — committed cycles stand (add-only), the residue is named, and a * fresh run re-mints over current bytes. `converged` never covers a residue; * - `mint_invalidated` — design/376-C1 D-1b: the mint's served-world ROSTER failed the * cross-cycle re-check — a served candidate moved rev, flipped its marked * flag, or left the candidate set without a fold this run's own plans * attest. The cached plan is discarded (its prompts were composed over a * world that no longer stands, and replaying it could publish clean products * whose authoring chain absorbed the moved row's influence); committed * cycles stand, and the host re-runs to re-mint over the current bytes. * - `purity_violation` — design/383 §1.3 (S-1): the minted plan carries a product that will * commit UNMARKED while containing bytes only the MARKED arm was shown. The * exposure partition's isolation premise is falsified, so the whole mint is * discarded with ZERO writes and the transcript is archived (a security * event's only evidence). DELIBERATELY DISTINCT from `driver_failed`: that * one says the model half could not finish, this one says the safety * topology did not hold — a host that folds them loses the ability to tell * "the model was flaky" from "content crossed a boundary it must not cross", * and only the second is an incident. * Extending this set is a consumer-visible closed-set expansion (release-note duty: every * downstream that pinned the enumeration becomes a named party — `mint_invalidated` was the * design/376-C1 add, `purity_violation` the design/383 片1 add, each named on its release). * * The type DERIVES from the array (never `satisfies` beside it): the array exists precisely for * downstream enumeration, and two independent declarations would let a new union arm skip the * array with no compile error — the derived form makes an edit to either side a type error at * the other. */ export declare const CONSOLIDATION_RUN_STOP_REASONS: readonly ["converged", "fuse_residue", "max_cycles", "parked", "refused", "driver_failed", "mint_invalidated", "purity_violation"]; /** See the doc on {@link CONSOLIDATION_RUN_STOP_REASONS} — the single source this derives from. */ export type ConsolidationRunStopReason = (typeof CONSOLIDATION_RUN_STOP_REASONS)[number]; export {};