UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

756 lines (755 loc) • 38.9 kB
/** * design/376 片① — the LLM distillation DRIVER's single source (the "driver half" of design/339 * §1.1: the driver proposes, the engine adjudicates). Everything here is the product code the * memory benchmark measures — the bench adapters import THESE exports, so every compare run is an * end-to-end regression of the shipped driver (the true-load-path law: the bytes the rig scores * are the bytes the npm package ships). * * Structure, and why each piece lives here: * - the PROMPT CONTRACT (frozen, versioned): the prompt is a behavior surface. v1 of the product * contract is the dl-3 contract BYTE-IDENTICAL — the version whose readings the benchmark * archived. Any change to these strings is a NEW contract version and must produce bench * readings before it ships (no-readings prompt edits are unreviewed behavior changes). * - `parseJsonAnswer` / `sanitizeLlmGroups` / `planParseRepairs`: the structural adapter between * a model answer and a protocol-shaped proposal, every repair COUNTED (a silent repair lets a * sloppy plan read as a clean one). * - `scheduleUnderFuse`: the driver-side plan selection under the engine's supersession fuse * (next-fit decreasing, no backfill). The ceiling itself is NEVER re-derived here — callers * compute it with the engine's own exported `supersessionFuseCeiling` (a second implementation * would be a drift seam). * - `mintLlmConsolidationPlan`: the live half (grouping + writing passes), archiving everything * the model was told and answered. * - `llmPlanDistiller`: a minted plan replayed as a per-cycle distiller (the run-cache form: mint * once, filter per cycle — never re-mint inside one run). * - `driveConsolidationToFixpoint`: the host loop over the four engine verbs (snapshot → commit → * reconcile, repeated), counting exclusively STORE deltas. * * The engine holds no distillation power and no model seat (design/339 §1.1 boundary): this module * calls the engine's HOST verbs and nothing else writes. */ import type { MemoryEntry } from "./types.js"; import type { ConsolidationProposal, ConsolidationWriteGate } from "./consolidation.js"; export declare const DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT = 32; export interface LlmDistillerContract { /** The contract's registered version id (a NEW id for any byte change, with bench readings). */ version: string; grouping: { system: string; instructionLines: readonly string[]; instructions: string; }; writing: { system: string; instructions: string; }; sampling: { temperature: number; maxTokens: { grouping: number; writing: number; }; }; /** Format-failure retries per call. Frozen INTO the contract: the archived readings were taken * under this value, so changing it moves the thing under measurement. */ retries: number; } export declare const LLM_DISTILLER_CONTRACT: LlmDistillerContract; /** dl-2: the SAME contract with the retrievability clause disambiguated, nothing else touched. */ export declare const LLM_DISTILLER_CONTRACT_DL2: LlmDistillerContract; /** dl-3: dl-2 with the SIZE clause substituted, nothing else touched. */ export declare const LLM_DISTILLER_CONTRACT_DL3: LlmDistillerContract; export declare const LLM_DISTILLER_CONTRACTS: Record<string, LlmDistillerContract>; /** * The PRODUCT contract, v1 = dl-3 byte-identical (the version whose benchmark readings are * archived). The shipping rule this alias exists to make auditable: a change to the product prompt * is a NEW `version` (pd-2, …) and must produce rig readings BEFORE it ships — the contract-hash * pin in the test suite reds on any silent byte drift under an unchanged version id. * * STANDING OBLIGATION on pd-2 (design/376-C1, ruled — mandatory, not optional): the next contract * version MUST carry, as its FIRST A/B item, the root-elimination arm for the shared-grouping * influence channel — the clean arm's writing pass without the grouping-authored `intent` (the * template's own "(none stated)" arm) and without the `key` name fallback, which empties the one * byte path an unattributed candidate has into a clean product and retires most of the D-1b guard * machinery. The A/B must publish an EXPLICIT continuation verdict, one of three states — switch * (root elimination ships) / hold (guard form stays, with readings) / redirect — it may not be * left hanging. The exposure-partitioned topology itself deliberately did NOT bump this version * (the candidate-set composition is topology, not wording; the wording axis stays single-variable * for exactly this A/B). * * WHERE THAT OBLIGATION NOW LIVES IN CODE: the root-elimination arm is the writing-channel option pair on * {@link mintLlmConsolidationPlan} (`writingIntent: "none-stated"` + `productKeyFallback: false`, recorded as * `minting.writingChannel`); the A/B readings, verdict and ruling are tracked in `docs/memory-distillation-ga.json`. */ export declare const MEMORY_DISTILLER_CONTRACT_V1: LlmDistillerContract; /** * The A/B witness: which grouping lines differ between two contracts, by index — published beside * readings so "only the wording moved" is a checkable statement about the prompts actually sent. */ export declare function contractGroupingDiff(a: LlmDistillerContract | undefined, b: LlmDistillerContract | undefined): { sameLineCount: boolean; changedClauseIndexes: number[]; before: Array<string | null>; after: Array<string | null>; otherClausesIdentical: boolean; writingPromptIdentical: boolean; systemPromptsIdentical: boolean; samplingIdentical: boolean; }; /** The snapshot row shape the engine's `snapshotConsolidationCandidates` answers. */ export interface DistillerCandidate { entry: MemoryEntry; marked: boolean; } /** One JSON-only completion request (no streaming, no tools — the contract's whole call shape). */ export interface DistillerChatRequest { system: string; user: string; maxTokens: number; temperature: number; } export interface DistillerChatAnswer { content: string; /** The provider's finish reason; `"length"` marks a token-ceiling cut-off (retried, disclosed). */ finishReason?: string | null; usage?: { promptTokens?: number; completionTokens?: number; } | null; /** The model id the provider REPORTS having served (audit anchor beside the requested id). */ model?: string | null; } export type DistillerChatFn = (req: DistillerChatRequest) => Promise<DistillerChatAnswer>; /** True when the id is an ALIAS spelling (re-pointable upstream), refused wherever a model id is * taken. A conservative SCREEN over the common alias grammars, not a full catalog — the honest * direction is one-way: `true` ⇒ alias, while a `false` only means "not a spelling this screen * knows". Screened: any id whose FINAL dash segment is `latest`/`default` regardless of how many * segments the family name has (`chatgpt-4o-latest`, `claude-3-5-sonnet-latest`, * `gemini-1.5-pro-latest`), plus the single-family `-chat` form (`deepseek-chat`, `qwen-chat` — * the alias family upstream re-points have actually ridden). Multi-segment `-chat` ids are NOT * screened on purpose: that suffix also names pinned chat variants of fixed weights * (`llama-2-7b-chat`), and refusing those would reject explicit ids. */ export declare function isAliasModelId(model: string): boolean; /** * An OpenAI-compatible chat seat over an explicit endpoint. Screened at CONSTRUCTION (the * bad-value loudness law): a missing key, an alias model id or an unservable request root refuses * with a code before any call is made — an alias can be re-pointed upstream and would move the * driven model with no trace in the archived plan. * * The root is judged by {@link assertServableRequestRoot} — the SAME law the brains apply, because * emptiness alone let userinfo, a non-http(s) scheme and a non-URL spelling reach `fetch` as a * TRANSPORT error. Only the ENVELOPE is this seat's; the driver routes config throws apart. */ export declare function openAiCompatChatSeat(seat: { baseUrl: string; apiKey: string; model: string; }): DistillerChatFn; export type ParsedJsonAnswer = { ok: true; value: unknown; repaired: boolean; } | { ok: false; error: string; sample: string; }; /** * Parse ONE JSON object out of a model answer. Fenced and prefixed answers are tolerated (a * formatting failure, not a distillation one) and the tolerance is COUNTED. A truncated answer is * never salvaged — half a plan scored (or committed) as a whole plan is the failure mode this * function exists to refuse. */ export declare function parseJsonAnswer(text: unknown): ParsedJsonAnswer; export interface PlanParseRepairs { grouping: number; writing: number; groupingCalls: number; writingCalls: number; /** Calls whose answer was cut off at the token ceiling — a HARNESS parameter biting, named per * call and never folded into a repair count it is not. */ lengthCapped: string[]; retries: number; } /** * Format repairs per PHASE, re-derived from a plan's own archived transcript — one derivation * produces the number (the grouping stage cannot be counted while the writing stage silently is * not), and any reader can re-run it against the archive and get the same answer. */ export declare function planParseRepairs(plan: { minting?: { transcript?: Array<{ label?: unknown; answer?: unknown; finishReason?: unknown; }>; }; } | undefined | null): PlanParseRepairs; export interface SanitizedLlmGroups { groups: Array<{ key: string; intent: string; members: Array<{ n: number; id: string; }>; }>; repairs: { hallucinatedMembers: number; duplicateMembers: number; emptyGroups: number; oversizeGroups: Array<{ key: string; members: number; cap: number; }>; nonIntegerMembers: number; }; claimedIds: string[]; } /** * Turn the model's groups into a protocol-shaped product list, repairing only STRUCTURE and * counting every repair: * * hallucinatedMembers — a number that is not in the candidate list at all; * duplicateMembers — a number claimed by a second group (first claim, in the model's own * group order, wins; the later claim is dropped); * emptyGroups — a group left with no members after the two above; * oversizeGroups — a group over `maxInputsPerProduct`. NOT split: chunking a group is a * distillation decision, and making it on the model's behalf would put * this module in the driver's seat. They ride on and the schedule reports * them as structurally infeasible; * nonIntegerMembers — a member reference that is not a strict integer. STRICT because * `parseInt` is a silent truncator and the thing truncated is a ROW * reference: "2foo"→2 and "1.5"→1 would each fold a DIFFERENT entry than * the model named while every repair counter stayed at zero. */ export declare function sanitizeLlmGroups(groups: unknown, { candidateIds, maxInputsPerProduct }: { candidateIds: readonly string[]; maxInputsPerProduct?: number; }): SanitizedLlmGroups; export interface LlmConsolidationPlanProduct { key: string; intent: string; name: string; description: string; type: "project" | "reference"; body: string; inputIds: string[]; /** The committed rev of each input AT MINT TIME (id → rev). The replay filter refuses to fold an * input whose committed rev has moved since the model read it — the product's body was written * over the OLD bytes, and superseding the edited entry under it would suppress a newer value * behind stale prose. Absent on plans minted before this field existed: those replay by id * alone, exactly as they always did (the archived-readings compatibility arm). */ inputRevs?: Record<string, string>; } /** design/376-C1 §2.2 — one arm's mint accounting inside an exposure-partitioned plan (usage * "两臂合计+分列": the merged plan's top-level `minting` carries the totals, these rows carry the * per-arm split PLUS each arm's verbatim grouping prompt — the clean arm's prompt bytes are the * C1-G5 audit face: any reader can mechanically verify zero marked-entry bytes reached it). */ export interface LlmConsolidationPlanArm { arm: "clean" | "marked"; candidateCount: number; calls: number; promptTokens: number; completionTokens: number; retries: number; truncations: number; groupingPrompt: string; } export interface LlmConsolidationPlan { kind: "llm-consolidation-plan"; contractVersion: string; model: string; /** The endpoint the mint ran against, when the caller supplied one (audit locator only). */ baseUrl?: string; mintedAt: string; maxInputsPerProduct: number; candidateCount: number; products: LlmConsolidationPlanProduct[]; /** design/376-C1 D-2 — present ⇔ this plan was minted by {@link mintExposurePartitionedPlan} * (clean/marked candidates partitioned into physically separate call chains). The DRIVE threads * this onto the committed proposal, where it is the engine's per-product-fold attestation. * Absent on every plan minted before the field existed — those replay UN-attested (run-level * fold, the safe default), which is exactly right for prompts composed over the whole library. */ mintExposure?: "partitioned"; /** design/376-C1 D-1b — the mint's served-world roster: every candidate actually RENDERED into * some arm's grouping call, with the rev and marked flag it was served at. The driver's * cross-cycle invalidation predicate compares each row against the fresh snapshot * (offered ∧ same rev ∧ same marked, or departed-with-own-fold-evidence) — any other state * means the world the prompts were composed over no longer stands. A single withheld marked * row (markedCount = 1, no marked arm) is deliberately NOT in the roster: no call ever saw it. */ servedCandidates?: Array<{ id: string; rev: string; marked: boolean; }>; /** design/376-C1 A-5 disclosure — how many marked candidates were withheld from the CLEAN arm's * prompts (= the marked candidate count when the partition engaged). Present ⇔ partitioned * mint over a library with at least one marked row. */ withheldFromCleanArm?: number; minting: { calls: number; groupingAttempts: number; groupingParseRepairs: number; parseRepairs: PlanParseRepairs; repairs: SanitizedLlmGroups["repairs"]; writeFailures: Array<{ key: string; members: number; reason: string | null; }>; usage: { promptTokens: number; completionTokens: number; }; prompts: { grouping: string; writingTemplate: string; systems: { grouping: string; writing: string; }; }; transcript: Array<{ label: string; ms: number; usage: DistillerChatAnswer["usage"]; finishReason: string | null; model: string | null; answer: string; }>; /** Per-arm split of a partitioned mint (see {@link LlmConsolidationPlanArm}); absent on * single-call-chain plans, INCLUDING the zero-marked degenerate of the partitioned mint * (whose bytes must stay identical to the plain mint's, modulo the attestation echo). */ arms?: LlmConsolidationPlanArm[]; /** pd-2 root-elimination A/B — the WRITING CHANNEL this plan was minted under, PRESENT ONLY when non-default, * so a default plan's bytes are unchanged and a reading can attribute its arm from the archive alone. */ writingChannel?: { intent: "grouping" | "none-stated"; keyFallback: boolean; }; }; } export type MintLlmConsolidationPlanResult = { ok: true; plan: LlmConsolidationPlan; } | { ok: false; reason: string; calls: LlmConsolidationPlan["minting"]["transcript"]; }; /** * Mint a fold plan from a live model over one candidate snapshot. `candidates` is the snapshot * shape the engine hands the driver (`{ entry, marked }`). Everything the model saw and everything * it answered is returned inside the plan so the run can be re-read, re-scored and disputed from * the archive alone. */ export declare function mintLlmConsolidationPlan({ candidates, chat, model, baseUrl, maxInputsPerProduct, contract, onProgress, armLabel, writingIntent, productKeyFallback, }: { candidates: readonly DistillerCandidate[]; chat: DistillerChatFn; /** The requested model id, recorded into the plan (audit anchor). Alias spellings refuse. */ model: string; baseUrl?: string; maxInputsPerProduct?: number; contract?: LlmDistillerContract; onProgress?: ((p: { phase: "grouping"; groups: number; claimed: number; of: number; } | { phase: "writing"; done: number; of: number; }) => void) | null; /** design/376-C1 — the partitioned mint's TRANSCRIPT arm tag (`grouping.clean#1` / * `writing.marked#1:key#1` label forms). Prompt bytes are untouched — this names calls in the * archive only, and stays compatible with `planParseRepairs`' `startsWith("grouping")` phase * split and its `#N` retry grammar. Absent ⇒ the historical label forms, byte-identical. */ armLabel?: "clean" | "marked"; /** * pd-2 root-elimination A/B (the standing obligation on {@link MEMORY_DISTILLER_CONTRACT_V1}) — the WRITING * CHANNEL: which grouping-authored bytes reach the writing pass. `"grouping"` (absent ≡ this, the shipped form) * fills `{{INTENT}}` with the grouping pass's own `intent`; `"none-stated"` fills it with `(none stated)` for * EVERY group, so the clean arm's authoring prompt carries no grouping-authored intent bytes. Mint TOPOLOGY, not * wording: the contract's frozen text is untouched and no version moves. Recorded on the plan * (`minting.writingChannel`) whenever non-default. */ writingIntent?: "grouping" | "none-stated"; /** * The channel's second byte path: `true` (absent ≡ this) names a product after the grouping-authored `key` when * the writing answer carries no `name`; `false` treats a missing or empty `name` as a FORMAT failure (retried, * then a counted write failure — R3), so nothing grouping-authored ever becomes a product's name on this arm. */ productKeyFallback?: boolean; }): Promise<MintLlmConsolidationPlanResult>; /** ONE rev sentinel for every face that RECORDS or RE-CHECKS a candidate's rev (the mint roster * below, the driver's cross-cycle roster re-check, the replay's offered map). The snapshot * interface is structural — a host-supplied engine can hand entries whose rev is not a string at * runtime — and the recording and re-checking sides MUST agree on the fallback, or a rev-less * candidate compares unequal to its own roster row and self-invalidates the mint it was just * served into (a paid mint discarded on its own cycle, re-minted and re-discarded forever). */ export declare const normalizedCandidateRev: (rev: unknown) => string; /** * design/376-C1 §2.2 — mint an EXPOSURE-PARTITIONED fold plan: candidates are bisected on the * snapshot's per-candidate `marked` flag and each subset is minted through its own physically * separate call chain, so no clean product's authoring chain (grouping intent included) is ever * served a marked entry's bytes. This is what makes the engine's per-product fold SOUND — the * attestation the plan carries (`mintExposure: "partitioned"`) is the freeze's gate, and this * function is the single source the product driver and every bench adapter share. * * - CLEAN arm: `mintLlmConsolidationPlan` over the clean subset — contract, templates, retries, * sampling all byte-identical to the plain mint (dl-3 恒等; only the candidate-set composition * moves, which is topology, not wording — no prompt-contract version bump, D-10/A-10). * - MARKED arm: the same contract over the marked subset, minted only at `markedCount ≥ 2` * (design C1-B): a single marked row has no group to fold and simply stays where it is — an * index handle row loses no discoverability by standing still. Withheld rows are disclosed * (`withheldFromCleanArm`) and kept OUT of the served roster (no call saw them). * - ZERO-marked degenerate: exactly ONE arm over exactly the full set with the HISTORICAL * transcript labels — byte-identical to the plain mint modulo the attestation echo and the * roster (C1-G1's identity claim). * - Either arm failing fails the WHOLE mint (one mint, one verdict; both arms' calls ride the * failure transcript). This widens nothing: under the run-level mint an adversarial marked row * already sat inside the single grouping call and could fail it whole. * - Cross-arm group structure is impossible by construction: each arm's `sanitizeLlmGroups` * valid map is built over that arm's own 1..N numbering. */ export declare function mintExposurePartitionedPlan(args: Parameters<typeof mintLlmConsolidationPlan>[0]): Promise<MintLlmConsolidationPlanResult>; /** * The detector's frozen algorithm parameters — a CONTRACT SIBLING, published beside the readings * so a run's verdict can be re-derived from the archive alone, and deliberately NOT a knob: this * is a safety gate, and a safety gate with an off switch is a safety gate that gets switched off * under deadline pressure. A new parameter set is a NEW `version`, exactly like the prompt * contract's own rule. */ export interface MemoryDistillerPurityContract { version: string; /** Unicode normalization applied to BOTH sides before comparison (so a decomposed copy of a * composed original still compares equal). */ normalization: "NFC"; /** Case is PRESERVED. Case-folding would widen the detector toward paraphrase matching and start * minting false positives on ordinary prose — this gate answers "were these bytes copied", and * a case change is already outside "verbatim". */ caseFold: false; /** Shingle width, in whitespace-separated words: the comparison unit on every side. */ shingleWords: number; /** A shingle shorter than this many characters is dropped before comparison (the floor that * keeps short, high-frequency phrases out of the evidence set). */ minShingleChars: number; } export declare const MEMORY_DISTILLER_PURITY_CONTRACT_V1: MemoryDistillerPurityContract; /** One witness: a marked-side-unique shingle found verbatim in a clean product's own bytes. */ export interface CleanArmLeakFinding { /** The product's INDEX in `plan.products` — the locator safe to put in a message. Unlike * {@link productKey} it is harness-assigned, so it cannot itself carry a payload. */ productIndex: number; /** The product's key AS THE MODEL WROTE IT (`sanitizeLlmGroups` passes `String(g.key)` through * unbounded). Useful when reading the archive; NEVER safe to interpolate into a notice, an * announcement or a log line that reaches a model context — the key is model-authored text and * can carry the same payload the shingle does. Use {@link productIndex} there. */ productKey: string; field: "name" | "description" | "body"; /** The offending shingle VERBATIM — the evidence, not a description of it. Same rule as * {@link productKey}: this is for the archive and an operator's own eyes, never for a message * that lands in front of a model. */ shingle: string; } export interface CleanArmLeakVerdict { /** False ⇔ the gate had nothing to check (see {@link notEngagedReason}); never a pass claim. */ engaged: boolean; notEngagedReason?: string; /** Size of the marked-side-unique evidence set (clean-arm bytes already subtracted). */ markedUniqueShingles: number; /** How many products were scanned (the all-clean-input products of a partitioned plan). */ scannedProducts: number; findings: CleanArmLeakFinding[]; } /** * design/383 §1.3 (S-1) — a VERBATIM-LEAK DETECTOR over a minted exposure-partitioned plan. Pure * bytes, zero model calls, run in the driver between the mint and the plan cache. * * WHAT IT ASKS: did any byte sequence that ONLY the marked arm was shown come back out inside a * product that will commit UNMARKED? The prompt-side audit (C1-G5) proves the clean arm's prompt * carried no marked bytes; this proves the partition machine itself was not bypassed, mis-wired or * side-channelled — a different failure surface, which is why both gates ship. * * HOW: * 1. evidence source = the MARKED arm's verbatim grouping prompt (`minting.arms[marked]`), which * is the exact rendered form the model saw — `name — description\n body`. Reading only entry * bodies would miss a payload parked in a `description`; * 2. minus the CLEAN arm's rendered bytes ⇒ the MARKED-SIDE-UNIQUE set. Text both sides legitimately * carry (the shared instruction template, a description a clean sibling also holds) proves * nothing and is subtracted here — this step is what keeps the gate off false positives; * 3. scanned surface = the all-clean-input products' `name` / `description` / `body`. "Clean" is * read off the plan's own served roster (no marked input id), which is the same judgment that * decides the product commits without an origin marker. A plan that claims the attestation but * carries NO roster (no plan this driver mints, but a foreign minter could stamp one) has every * product read as clean — including the marked arm's own, which legitimately holds marked * bytes — so it refuses. That is the intended direction: an attestation with no evidence behind * it should not buy a plan the per-product fold; * 4. any intersection ⇒ the caller fails the mint WHOLE. A leak falsifies the clean arm's * isolation premise, and the other products of the same mint rest on that same premise. * * HONEST BOUNDARY (do not let this JSDoc be read as more than it is): this catches VERBATIM * copying. It does not catch paraphrase, translation, or a summary of marked content — a complete * detector would need a semantic judge, i.e. a model judging a model, which this design forbids. * A leak shorter than one shingle (see the contract's `shingleWords`/`minShingleChars`) is below * the floor and will not be seen. It is a leak DETECTOR, not a purity PROOF; `engaged: false` is * "nothing to check here", never "checked and clean". */ export declare function detectCleanArmVerbatimLeak(plan: LlmConsolidationPlan, contract?: MemoryDistillerPurityContract): CleanArmLeakVerdict; /** The distiller signature the drive consumes: `(candidates, snapshot, scope) -> proposal`. * At runtime only `entry.id` is load-bearing for the replay filter — test/bench stubs that hand * partial entry shapes are tolerated by the reads, the type states the engine's real snapshot. * The drive consumes the proposal's FOLD face only (`products`; `scope` is the drive's own): a * proposal carrying directed `intents` is REFUSED by the drive rather than silently narrowed — * directed update/delete intents go through `commitConsolidationPlan` directly (see the drive's * own doc for why a per-cycle forward is structurally impossible). */ export type ConsolidationDistillFn = (candidates: readonly DistillerCandidate[], snapshot: unknown, scope: string) => ConsolidationProposal | Promise<ConsolidationProposal>; /** * A minted plan replayed as a per-cycle distiller. Per cycle it filters each product's inputs down * to the ids the snapshot is still offering — a product whose inputs have all been folded is empty * and is the drive's termination signal. `supersede: true` rides on every input: an input * attributed without the edge is provenance only, the row would stay active, and the run would * read as "consolidation did nothing" for a reason on this side of the seam. * * This is the RUN-CACHE form the cost model depends on: one mint per run, this filter per cycle — * re-minting per cycle would multiply the model bill by the cycle count for the same answer. */ export declare function llmPlanDistiller(plan: { products?: ReadonlyArray<Pick<LlmConsolidationPlanProduct, "name" | "description" | "type" | "body" | "inputIds"> & { inputRevs?: Record<string, string>; }>; mintExposure?: "partitioned"; } | undefined | null): ConsolidationDistillFn; export interface FuseSchedule<P> { schedule: "next-fit-decreasing"; picked: P[]; targets: number; ceiling: number; deferred: number; oversize: Array<{ name: string; inputs: number; cap: number; }>; } /** * One cycle's plan selection under the engine's supersession fuse. * * NEXT-FIT DECREASING: products in descending target count, stop at the first that does not fit. * The missing backfill is the point, not an oversight: the ceiling is a FRACTION of the active set * and every fold shrinks the active set, so a cycle that spends leftover budget on a small fold — * one that would still fit later, at a lower ceiling — permanently lowers the ceiling every * remaining large fold has to clear. Measured with the shipped 0.25 cap: a backfilling schedule * dead-ends with two large digests permanently unfoldable while the same products converge when * the large folds simply go first. * * `maxProductsPerRun` is honoured here too; a product whose input count exceeds * `maxInputsPerProduct` can never be frozen at all and is reported structurally infeasible rather * than retried forever. */ export declare function scheduleUnderFuse<P extends { name?: string; inputs?: Array<{ id: string; supersede?: boolean; }>; }>(products: readonly P[] | undefined, ceiling: number, { maxProductsPerRun, maxInputsPerProduct }?: { maxProductsPerRun?: number; maxInputsPerProduct?: number; }): FuseSchedule<P>; /** The engine face the drive consumes (MemoryEngine satisfies it structurally; tests may stub). */ export interface ConsolidationDriveEngine { snapshotConsolidationCandidates(scope: string, opts?: { force?: { requestId: string; }; full?: boolean; }): Promise<{ cycleToken: string; watermark?: unknown; candidates: Array<{ entry: MemoryEntry; marked: boolean; }>; }>; commitConsolidationPlan(cycleToken: string, proposal: ConsolidationProposal, opts: { requestId: string; writeGate: ConsolidationWriteGate; }): Promise<ConsolidationDriveReceipt>; reconcileConsolidation(scope: string, opts: { readonly writeGate: ConsolidationWriteGate; }): Promise<{ replayed?: { planId: string; state: string; } | null; } | undefined | void>; } /** The commit receipt fields the drive reads (the engine's receipt carries more). */ export interface ConsolidationDriveReceipt { planId?: string; state?: string; products?: { frozen?: number; applied?: number | null; conflicts?: number | null; refused?: Array<{ index: number; reason: string; }>; }; } export interface ConsolidationFoldState { active: number; superseded: number; products: number; } export interface ConsolidationDriveCycleRow { cycle: number; activeSetSize: number; ceiling: number; candidates?: number; /** How many of the cycle's served candidates carried a committed external-origin marker — the * engine-served fact the fold law and the instruction hard gate key on (design/376 §2.1: a * reading about the marked treatment must show the snapshot actually saw marked content, and a * zero-marked run must show the zero). Absent only on the row of a snapshot that refused. */ markedCandidates?: number; deferred?: number; planId?: string; state?: string; applied?: number | null; conflicts?: number | null; refusedProducts?: Array<{ index: number; reason: string; }>; reconciled?: { planId: string; state: string; } | null; note?: string; refused?: string; recovery?: { attempted: boolean; ok: boolean; replayed: { planId: string; state: string; } | null; summary: string; report?: unknown; }; attempted: { products: number; targets: number; }; committed: { products: number; superseded: number; }; /** `committed`'s two flat mirrors — the drive-report projections read the row under these names * as well as under `committed.*`; all three spellings are one store-delta fact. */ products: number; targets: number; } export interface ConsolidationDriveResult { /** A STORE fact: did anything actually fold. A refused first cycle answers false. */ ran: boolean; schedule: "next-fit-decreasing"; cycles: ConsolidationDriveCycleRow[]; cycleCount: number; productsCommitted: number; entriesSuperseded: number; productsAttempted: number; engineRefusedProducts: Array<{ index: number; reason: string; }>; converged: boolean; foldState: { before: ConsolidationFoldState; after: ConsolidationFoldState; }; stop?: { reason: string; refused?: boolean; stalled?: boolean; parked?: boolean; exhausted?: boolean; planId?: string; fuseBlocked?: Array<{ name: string; targets: number; }>; oversize?: Array<{ name: string; inputs: number; cap: number; }>; recovery?: unknown; }; } /** The engine's refusals carry a `code` and a `reasons` array; a bare `.message` throws half away. */ export declare function codedReason(err: unknown): string; export interface DriveConsolidationOptions { distill: ConsolidationDistillFn; /** The fuse denominator AND every published count are store facts, never driver assumptions. */ readFoldState: () => Promise<ConsolidationFoldState> | ConsolidationFoldState; options: { supersedeRatioCap: number; supersedeAbsoluteFloor: number; maxProductsPerRun: number; maxInputsPerProduct: number; }; /** The ceiling arithmetic — pass the engine's `supersessionFuseCeiling` (single source). */ fuseCeilingOf: (activeSetSize: number, opts: { supersedeRatioCap: number; supersedeAbsoluteFloor: number; }) => number; requestIdPrefix?: string; maxCycles?: number; /** * The snapshot step, injectable so a HOST scheduling policy (throttle waits, run-scoped force, * busy recovery) wraps the verb without forking the loop. Default = the plain full snapshot. * Anything it throws takes the loop's refusal arm exactly as the plain call would. */ takeSnapshot?: (cycle: number) => ReturnType<ConsolidationDriveEngine["snapshotConsolidationCandidates"]>; /** Observation hook after each cycle row closes against the store (run persistence seat). */ onCycle?: (row: ConsolidationDriveCycleRow) => void | Promise<void>; /** The write gate every cycle's commit and recovery carries (the host's judgment of where this run may write) — required, as at every * verb that applies a plan; a host that holds no rules passes one that resolves. */ writeGate: ConsolidationWriteGate; } /** * Drive consolidation to its FIXPOINT: repeat snapshot → commit → reconcile until nothing is left * to fold, the fuse can no longer admit the smallest remaining product, or the engine refuses. * * Why a loop is the honest shape: the supersession fuse caps ONE plan at a fraction of the active * set (0.25 shipped), so a library that accumulated for many sessions cannot be folded by a single * plan — the engine refuses that by design, and a driver that fired one plan and stopped would be * converting a safety property into a failure report. * * Every snapshot asks `full: true`: snapshots are CHANGE-DRIVEN by default, so cycle 2 of a stable * library would answer an EMPTY candidate set and strand most of the library unfolded. `full` is * the protocol's own name for "re-distill what is already there", which is exactly what every * cycle of a first consolidation is. * * WHAT WAS PROPOSED IS NEVER WHAT IS COUNTED: every per-cycle figure is a STORE delta — * `readFoldState()` before and after each cycle, `committed` is the difference. Receipts are not * enough (a `completed` receipt can carry per-product freeze refusals; a `conflict` receipt says * how many applied but not which); the store is the only witness that cannot be off by a refusal. */ export declare function driveConsolidationToFixpoint(engine: ConsolidationDriveEngine, scope: string, opts: DriveConsolidationOptions): Promise<ConsolidationDriveResult>; /** A host snapshot policy (the injectable `takeSnapshot`) may classify its own stop as PARKED by * throwing an error carrying this marker — e.g. a busy seat whose reconcile answered a conflict * plan awaiting the host valve. The drive maps it onto the parked stop family. */ export declare const CONSOLIDATION_SNAPSHOT_PARKED: unique symbol; /** * A host ABORT marker: an error the injectable snapshot policy tags with this symbol is NOT an * engine refusal — it is the host tearing the run down (a cancelled wait, a shutdown hook). The * drive RETHROWS it instead of composing a refusal stop, so the caller's run account stays * PENDING and the paid mint survives to the resume — folding a host abort into a terminal * `refused` would silently discard a whole mint over a shutdown. */ export declare const CONSOLIDATION_DRIVE_ABORT: unique symbol;