@sema-agent/core
Version:
Stateless, task-oriented AI agent core
590 lines (589 loc) • 38.4 kB
TypeScript
/**
* 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;
/** The token of the applier that holds this plan's application (the LAST claimer holds it; an earlier one is stale and
* stops). Written by the engine at the start of each apply; absent on a plan nobody has applied yet. */
applier?: string;
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 code of a run (or one cycle's commit) a write judge stopped. */
export declare const CONSOLIDATION_WRITE_REFUSED_CODE = "memory.consolidation_write_refused";
/**
* The write gate a plan's application may carry, asked per product with the file it lands in. It resolves iff that write is
* permitted and rejects with a {@link ConsolidationRefusedError} (code {@link CONSOLIDATION_WRITE_REFUSED_CODE}) otherwise.
* It is asked where the plan is APPLIED, whichever entry applies it (commit, `reconcileConsolidation`'s replay, the drive's
* recovery), at the backend's FINAL landing spelling of every product before any is written; a refusal there writes nothing and
* DISCARDS the plan, so a suspended plan is no grant that outlives the rule that now forbids it. Products an earlier pass of the
* plan landed (a crash cut it short between two products) stay; the refusal the apply throws names them.
*/
export type ConsolidationWriteGate = (target: {
scope: string;
path: string;
}) => Promise<void>;
/** 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 {};