This is the abridged developer documentation for memhtml # memhtml > An agent's long-term memory as a git repository of semantic HTML files, with a rebuildable SQLite index, retrieval that fuses four ranking arms, and a seventeen-phase curation pass. Memory for Agents, in HTML MEANING · MEMORY · MARKUP ## What memhtml is [Section titled “What memhtml is”](#what-memhtml-is) memhtml stores an agent’s long-term memory as a git repository of small HTML files, one fact per file. A SQLite index over that repository makes the files searchable, and an agent reads and writes the store either through the `memhtml` command line or through an MCP server that serves the same store over stdio. What the agent remembers is a directory on disk that you can open in a browser, diff, and edit with ordinary tools. ## The problem it solves [Section titled “The problem it solves”](#the-problem-it-solves) An agent that writes down what it learns needs three answers later: what do I know, where did it come from, and what has changed since. Keeping those facts as rows in a vector database answers the first one well and drops the other two, because a row carries no history and a re-embedding pass rewrites the store with nothing for a reviewer to read. memhtml answers all three out of git. Each fact is a file, so you read it without a query. Each change to the corpus is exactly one commit, so `git log` in the store is the history of what the agent learned, and a night of automated curation is a diff a person can review. Retiring a memory moves the file into `archive//` with the original path mirrored beneath it, so `git log --follow` reads through a memory’s whole life and no correction destroys the fact it corrected. The index holds no facts of its own: delete `.memhtml/index.db` and `memhtml index rebuild` reconstructs it from the tree. ## What it is made of [Section titled “What it is made of”](#what-it-is-made-of) Each memory is standard HTML5 that a browser renders with no build step. The `` elements in the head carry the typed metadata, one `` span in the body carries the claim the file exists to state, and `` elements carry the relationships an author asserted between memories. One fact per file is what keeps a correction reviewable: `memhtml correct` writes the replacement and archives the original in one commit, so an interrupted run leaves no pair of live contradicting memories behind. Two SQLite databases sit under `.memhtml/`, opened through Node’s built-in `node:sqlite` driver, and they differ in what a rebuild can recover. `index.db` holds the projected rows, the full-text index, the embedding vectors, and the mined edges, all of which the indexer regenerates from the tree. `state.db` holds the facts the tree cannot reproduce: access counts, reinforcement counts, and outcome scores. Its durable copy is a committed file, `.memhtml/state/access.jsonl`. Retrieval runs four independent ranking arms over one query and then combines their orderings. Full-text search ranks on title, claim, and body text. Vector similarity ranks on the embeddings. Recency ranks an episodic memory by when the fact happened rather than by when someone wrote it down. Salience ranks on how often someone has chosen to open the memory. One SQL statement fuses the four orderings by reciprocal rank fusion, where each arm contributes `1/(rank + 60)` under its own weight, and a pass in TypeScript then drops near-duplicate hits. That fusion is what this site means by four-arm RRF. When an arm’s precondition is missing, memhtml drops that arm and sets `degraded: true` on the response, so the search gets narrower and still answers. A store with no embedder bound is the common case, and it still returns ranked hits from the other three arms. Three doors write into the same tree: `memhtml write` and `memhtml apply` on the command line, the write tools on the MCP server, and your own file tools against the checkout. The store owns git staging, so one call becomes exactly one commit and a caller cannot bundle two unrelated writes into it. `memhtml sleep run` is the curation pass you schedule overnight. It walks seventeen ordered phases on a `sleep/` branch and commits each phase’s work on its own, so a reviewer reads the night one phase-shaped diff at a time. It detects contradictions and leaves the choice of a winner to a writer or a human, because that choice is a one-way door. `memhtml sleep merge` then refuses to move `main` when the night made retrieval worse. Every command writes one JSON envelope to stdout and nothing else, sends its logs to stderr, and exits 0 for success, 2 for a usage error, and 1 for a runtime failure. `memhtml manifest` prints every command, flag, response type, error code, and environment variable the binary accepts, and it answers on a machine with no store, no database, and no credentials. ## Where to start [Section titled “Where to start”](#where-to-start) Install the binary and work through the four tutorials in order. One published package, [`memhtml`](https://www.npmjs.com/package/memhtml), carries the whole system, and a clone-and-build path exists for working on it. If you are an AI agent rather than a person, read [For agents](/agents/) first: it states the assumptions to drop and the shortest path to a correct first call. * **1. [Learn](/learn/)**: from a clone to a working store, then task-shaped how-tos * 1.1. [Install memhtml and initialize a store](/learn/tutorial/install/) * 1.2. [Write your first memory](/learn/tutorial/first-memory/) * 1.3. [Retrieve it](/learn/tutorial/first-retrieval/) * 1.4. [Wire up the MCP server](/learn/tutorial/mcp-server/) * 1.5. [Run the store day to day](/learn/operations/run-the-store-day-to-day/) * **2. [Reference](/reference/)**: generated from the registries the binary parses its own arguments with * 2.1. [Global flags](/reference/global-flags/) and [configuration](/reference/config/) * 2.2. [Response types](/reference/response-types/) and [error codes](/reference/error-codes/) * 2.3. [MCP tools](/reference/mcp-tools/) and [resources](/reference/mcp-resources/) * 2.4. [Vocabularies](/reference/vocabulary/), [sleep phases](/reference/sleep-phases/), [RRF arms](/reference/rrf-arms/) * 2.5. [Index schema](/reference/schema/) and the [requirements ledger](/reference/requirements/) * 2.6. [Contracts](/reference/contracts/): every exported symbol of `@memhtml/contracts`, with its doc comment * **3. [Internals](/internals/)**: why the system is shaped this way, and what each decision refuses * 3.1. [Packages and dependency direction](/internals/packages-and-dependency-direction/) * 3.2. [The memory file format](/internals/the-memory-file-format/) * 3.3. [The write path](/internals/the-write-path/) * 3.4. [Four-arm retrieval](/internals/four-arm-retrieval/) * 3.5. [The sleep pipeline](/internals/the-sleep-pipeline/) * 3.6. [Testing posture](/internals/testing-posture/) * **Appendix A. [Glossary](/glossary/)**: the domain vocabulary, each term linked to the chapter that develops it ## How to read this site [Section titled “How to read this site”](#how-to-read-this-site) The Reference tier is generated from the same registries the binary parses its own arguments with, so a flag, error code, or response type on those pages is the one the binary ships. Where this site and the binary disagree, the binary is right and a test is missing. Architectural claims cite the code in repo-relative `path:line` form. Those line numbers point into the commit this site was built from, so read them as a pointer into the source rather than as a permanent address. Every measurement on the site carries the date someone took it. Section numbers live in the heading text, as in `## 3.2. Edge encoding`, so the page, the contents, the search index, and the Markdown this site serves to an agent all agree on which section §3.2 is. Anchors stay unnumbered, so inserting a section does not break an inbound link. Every page also serves its own Markdown source at the same path with `.md` appended, and links that twin from its ``. [`llms.txt`](/llms.txt) indexes the whole corpus for an agent that would rather read the document than the page. # For agents > What to unlearn, which surface you are on, and the shortest path from nothing to a working memhtml integration. This page is addressed to an AI agent working with memhtml, and it is written so the person evaluating memhtml over your shoulder can read it too. It is short, and every claim on it points at behavior a test checks. ## 1. The manifest outranks this page [Section titled “1. The manifest outranks this page”](#1-the-manifest-outranks-this-page) `memhtml manifest` prints the binary’s whole contract: every command, argument, flag, response type, error code, and environment variable. A bare `memhtml` returns the same thing, and so do `memhtml help` and `memhtml --help` when stdout is a pipe. Each of them answers on a machine with no store, no database, and no credentials, which makes the manifest your liveness check as well as your contract. For one command, `memhtml help ` (or `memhtml --help`, or `-h`) returns that command’s entry alone: a usage line, its arguments and flags, the response type it emits, the usage-error codes, examples, and sibling commands. Piped, it is a `cli.help` envelope whose `data` is the manifest’s entry for the command plus those derived fields, so nothing it says can contradict the manifest. On a terminal it is Markdown, for the person behind you; `--json` forces the envelope there too. Every usage error on a command that exists ends its `suggestions` with the `memhtml help ` call that shows the table you needed; an unknown command gets nearest-name candidates instead. **For an agent.** Prefer the manifest to this page wherever the two could disagree. The manifest is generated from the source of truth, so it is right and this page is stale. This page adds what a schema cannot state: what a silence means, and which surface you are on. ## 2. Assumptions to drop [Section titled “2. Assumptions to drop”](#2-assumptions-to-drop) Each item below is a place where memhtml behaves unlike the system you are pattern-matching it to. Branch on `code` and never on `error`. A failure envelope carries four fields: `{apiVersion, error, code, suggestions}`. The `code` values are append-only, so a shipped code keeps its meaning for good and is never removed. The `error` string is prose for a human, and whoever improves the wording rewrites it freely, so a matcher over that string gives you a test that passes until someone edits a sentence. See [error codes](/reference/error-codes/) and [the envelope](/reference/envelope/). An absent path was archived. memhtml removes nothing from a store. Eviction is a `git mv` into `archive//` that mirrors the original path, so `git log --follow` reads through a memory’s whole life. When a path you were holding stops appearing in results, retry with `--include-archived` before you conclude the fact is gone. See [store layout and path algebra](/internals/store-layout-and-path-algebra/). The git tree is the system of record and the index is disposable. `.memhtml/index.db` is a projection, and `memhtml index rebuild` reconstructs it from the tree, so anything that has to survive lives in a file: authored links are `` elements and metadata is `` elements. Read a query result as a pointer to a file, and the file as the fact. See [the index plane and the state plane](/internals/index-plane-and-state-plane/). One memory holds one fact. A memory is a single claim. Writing a five-paragraph summary as one memory defeats retrieval, because the arm that should rank one of those claims scores the whole blob instead. Split the summary into claims and let `memhtml link` carry the relationships between them. See [the memory file format](/internals/the-memory-file-format/). Read the flags instead of guessing them. Every flag the binary accepts is in the manifest, and an unknown flag returns a usage error with a suggestion attached rather than a silent default. A guess costs a round trip, where the manifest answers the same question in one call. See [global flags](/reference/global-flags/). ## 3. Which surface you are on [Section titled “3. Which surface you are on”](#3-which-surface-you-are-on) Two surfaces reach the same store, and the test for which one you have is free. | Signal | Surface | Entry point | | ------------------------------------ | --------- | ---------------------------------------------------------------- | | `memory_search` is in your tool list | MCP | Call the tools directly. Nothing to install. | | You can run a shell command | CLI | `memhtml manifest`, then the command you need. | | Neither | Read-only | You are reading documentation. Fetch the Markdown, not the HTML. | The MCP server is the same binary. `memhtml serve mcp` speaks stdio and composes the same services the command line does, so both surfaces agree on which store, which database, and which vector space a call reaches. A tool’s arguments mirror its command’s flags. See [MCP tools](/reference/mcp-tools/) and the [reference overview](/reference/), which links one page per command. **For an agent.** Read your tool list to find your surface, rather than running a command and inspecting the failure. When `memory_search` is absent from that list, you are on the CLI. ## 4. The shortest path to a working integration [Section titled “4. The shortest path to a working integration”](#4-the-shortest-path-to-a-working-integration) On the MCP surface there is no setup: call `memory_search` with prose, then `memory_read` on a hit’s path. On the command line: ```bash 1 memhtml manifest # the contract, and the liveness check 2 memhtml init # scaffold a store at --repo or $MEMHTML_ROOT 3 memhtml write --title … --claim … # one memory, one fact, one commit 4 memhtml search "prose, not a query language" --dense ``` Make `--dense` a habit on every call. It emits minified JSON with null fields dropped, which is what you want when the output goes into a prompt. `memhtml recall` runs the same retrieval under a character budget, for when you want a context pack rather than a hit list. Every command writes exactly one JSON envelope to stdout and nothing else, and sends its logs to stderr. The one exception is `memhtml help` on a terminal, which writes Markdown; piped, or with `--json`, it is an envelope like every other command. Exit 0 is success. Exit 2 is a usage error you fix by changing the call. Exit 1 is a runtime failure you fix by changing the store or the environment. When you are the operator wiring a host agent rather than the agent being wired, one command is the whole path: it writes the host’s MCP entry, its hooks, an instruction block, and a skill, and records them in a receipt so uninstall removes exactly that. ```bash 1 memhtml integrations install # claude, codex, cursor, opencode; omitted, it detects them ``` ## 5. What to avoid [Section titled “5. What to avoid”](#5-what-to-avoid) * Scraping these pages. Every one of them is served as Markdown; section 7 has the URLs. * Parsing the `error` prose. Section 2 explains what that costs you. * Writing a query language into `memhtml search`. The argument is prose, and retrieval fuses four ranking arms, so an operator syntax has nothing to bind to. * Bundling unrelated writes. The store owns staging and every corpus change is exactly one commit, so a caller cannot make one commit mean two things. Use `memhtml apply` when the writes are one change. * Spending a tool call per hop. A question that needs several traversals of the corpus is one `memhtml exec` script against a read-only mount. See [the code-mode guide](/reference/guide/code-mode/). * Editing `.memhtml/index.db`, or hand-writing a memory file’s markup from memory. Write through the commands, which own the commit and the validation. ## 6. Read next [Section titled “6. Read next”](#6-read-next) Each row carries the page and its raw Markdown, so you can fetch rather than render. The [reference tier](/reference/) is generated from the source registries, and it is where every count and every enumerated value lives. | Read this | Raw Markdown | | ----------------------------------------------------------------------------------- | --------------------------------------------------------- | | [First call](/reference/guide/first-call/): the envelope, the exit codes, `--dense` | [`first-call.md`](/reference/guide/first-call.md) | | [Write surfaces](/reference/guide/write-surfaces/): the three ways in | [`write-surfaces.md`](/reference/guide/write-surfaces.md) | | [When to batch](/reference/guide/when-to-batch/): one write or many | [`when-to-batch.md`](/reference/guide/when-to-batch.md) | | [Conflicts](/reference/guide/conflicts/): what a contradiction does | [`conflicts.md`](/reference/guide/conflicts.md) | | [Authoring](/reference/guide/authoring/): the markup a memory is | [`authoring.md`](/reference/guide/authoring.md) | | [Code mode](/reference/guide/code-mode/): many hops in one call | [`code-mode.md`](/reference/guide/code-mode.md) | | [Error codes](/reference/error-codes/): the values you branch on | [`error-codes.md`](/reference/error-codes.md) | | [Response types](/reference/response-types/): the `type` discriminators | [`response-types.md`](/reference/response-types.md) | | [MCP tools](/reference/mcp-tools/): the tools and their arguments | [`mcp-tools.md`](/reference/mcp-tools.md) | | [Commands](/reference/): every command, one page each | [`reference.md`](/reference.md) | ## 7. The machine surfaces [Section titled “7. The machine surfaces”](#7-the-machine-surfaces) Any page is available as Markdown: append `.md` to its path. `/reference/mcp-tools/` is served at `/reference/mcp-tools.md` as `text/markdown`. This holds for every page on the site, including the generated ones, and each page links its own twin from `` with `rel="alternate" type="text/markdown"`. The whole site comes three ways. [`llms.txt`](/llms.txt) is the index, and it lists this page first. [`llms-full.txt`](/llms-full.txt) is every page in one file. [`llms-small.txt`](/llms-small.txt) is the same corpus with non-essential content removed, for a tighter context. **For an agent.** Fetch `llms-small.txt` before `llms-full.txt`. When you already know which page you want, fetch that page’s `.md` twin instead of either bundle: it is a fraction of the tokens and the same text. # API > The generated reference for the workspace packages, one directory per package and one page per module, built from the TSDoc in the source on every site build. Every page below this one is generated from the TSDoc on a package’s exported surface; this index is the tier’s one authored page. The generated pages are not committed: `starlight-typedoc` writes them into `api//` on every `astro check` and `astro build`, those directories are gitignored, and so a page always describes the source at the commit the site was built from. When a signature on a page and a signature in the source disagree, the source is newer and the site is behind. ## What is covered [Section titled “What is covered”](#what-is-covered) | Package | Pages | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `@memhtml/contracts` | [Overview](/api/contracts/) and one page per import path: [edges](/api/contracts/edges/), [errors](/api/contracts/errors/), [paths](/api/contracts/paths/), [slug](/api/contracts/slug/), [types](/api/contracts/types/). | | `@memhtml/traces` | [One page](/api/traces/), because the package publishes a single import path. | | `@memhtml/eval` | [One page](/api/eval/): the corpus generator and the discrimination gate, one import path. | | `@memhtml/domain` | [One page](/api/domain/): the pure arithmetic of retention, decay, fusion, diversification, merge guards and graph scores, one import path. | | `@memhtml/llm` | [One page](/api/llm/): the Bedrock and proxy model clients, embeddings, the model table and the wire helpers, one import path. | Each package’s overview page is its `README.md` from the repository, with the module list appended by the generator. The remaining workspace packages are added one per maintenance run, smallest exported surface first, once their generated pages pass the same gates as the authored ones. ## How to read a page [Section titled “How to read a page”](#how-to-read-a-page) Every declaration is rendered as a fenced code block in the shape TypeScript would print it, followed by the doc comment from the source. Parameters are listed one per line rather than in a table, so an object-literal type keeps its braces inside a code span. “Defined in” lines are omitted on purpose: the module a symbol belongs to is the page it sits on, and the [Reference](/reference/contracts/) tier names each symbol’s file. The generator excludes external symbols, so a type that comes from `effect` is named but not expanded. Follow the import path shown at the top of the page to read it in its own package. ## Adding a package [Section titled “Adding a package”](#adding-a-package) Each package is its own `starlightTypeDoc` plugin instance in `apps/docs/astro.config.ts`, writing to its own directory under `api/`. The plugin refuses two instances whose output directories nest or overlap, which is why the tier is one directory per package rather than one flat directory. The comment block above the first instance records every option and the reason it is set. # @memhtml/contracts `@memhtml/contracts`: schemas, enums, errors, and path algebra. Zero I/O. This is the innermost workspace package: it imports only `effect`, and every other package imports it. It holds the words the rest of the system agrees on. The closed vocabularies say what a memory can be. The path algebra says where one lives in the tree. The edge model says how one memory points at another. The slug rules say how a title becomes a filename. The tagged errors say how an operation can fail. ## Modules [Section titled “Modules”](#modules) Each module is its own import path, and the package root re-exports all five. The pages beside this one are generated from the TSDoc in the source, one page per module. | Import path | What it holds | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `@memhtml/contracts/edges` | The edge vocabulary and the `Edge` schema. Four edge classes that never mix, so a person or task edge stays out of PageRank, MMR, and the retention bridge count. | | `@memhtml/contracts/errors` | The tagged errors every operation can fail with, each carrying only what a caller needs to recover and nothing that leaks corpus content. | | `@memhtml/contracts/paths` | Path algebra. Every function is pure and total, and a path is always the repo-root-relative git-tree form: no leading slash, forward slashes only. | | `@memhtml/contracts/slug` | Slugs and filenames: how a title becomes `[a-z0-9-]`, how an episodic entry carries its date, and how a collision gets a suffix. | | `@memhtml/contracts/types` | The closed vocabularies (memory type, status, PARA bucket, task status), the `type:name` entity reference, and the scalar schemas for importance and confidence. | ## Where else this is documented [Section titled “Where else this is documented”](#where-else-this-is-documented) The Reference tier’s [contracts page](https://memhtml.github.io/reference/contracts/) lists every exported symbol with its doc comment, derived from the same source at build time. The [closed vocabularies page](https://memhtml.github.io/reference/vocabulary/) lists each vocabulary’s values. ## Modules [Section titled “Modules”](#modules-1) * [edges](/api/contracts/edges/) * [errors](/api/contracts/errors/) * [paths](/api/contracts/paths/) * [slug](/api/contracts/slug/) * [types](/api/contracts/types/) # edges ## Type Aliases [Section titled “Type Aliases”](#type-aliases) ### Edge [Section titled “Edge”](#edge) ```ts 1 type Edge = typeof Edge.Type; ``` One edge. `derived` separates a sleep-mined suspicion from an authored assertion: the retention `contested_status` signal counts only `derived: false` contradictions, so an uncorroborated machine guess can never evict a memory. `strength` is unitless in `[0, 1]`; an authored edge is 1.0 and a mined one carries its cosine. `srcPath`/`dstPath` are repo-root-relative with no leading slash. *** ### EdgeClass [Section titled “EdgeClass”](#edgeclass) ```ts 1 type EdgeClass = typeof EdgeClass.Type; ``` *** ### EdgeProvenance [Section titled “EdgeProvenance”](#edgeprovenance) ```ts 1 type EdgeProvenance = typeof EdgeProvenance.Type; ``` *** ### EdgeRel [Section titled “EdgeRel”](#edgerel) ```ts 1 type EdgeRel = typeof EdgeRel.Type; ``` *** ### MemoryRel [Section titled “MemoryRel”](#memoryrel) ```ts 1 type MemoryRel = typeof MemoryRel.Type; ``` *** ### PersonRel [Section titled “PersonRel”](#personrel) ```ts 1 type PersonRel = typeof PersonRel.Type; ``` *** ### ProvenanceRel [Section titled “ProvenanceRel”](#provenancerel) ```ts 1 type ProvenanceRel = typeof ProvenanceRel.Type; ``` *** ### TaskRel [Section titled “TaskRel”](#taskrel) ```ts 1 type TaskRel = typeof TaskRel.Type; ``` ## Variables [Section titled “Variables”](#variables) ### ALL\_RELS [Section titled “ALL\_RELS”](#all_rels) ```ts 1 const ALL_RELS: readonly ["supersedes", "contradicts", "caused_by", "leads_to", "part_of", "relates_to", "example_of", "supports", "laterally_related", "about_person", "authored_by", "from_session", "blocks", "subtask_of"]; ``` Every rel across all four classes. The `edges.rel` column’s full vocabulary. *** ### Edge [Section titled “Edge”](#edge-1) ```ts 1 const Edge: Struct<{ 2 derived: Boolean; 3 dstPath: String; 4 edgeClass: Literals; 5 provenance: Literals; 6 rel: Literals; 7 srcPath: String; 8 strength: Number; 9 }>; ``` One edge. `derived` separates a sleep-mined suspicion from an authored assertion: the retention `contested_status` signal counts only `derived: false` contradictions, so an uncorroborated machine guess can never evict a memory. `strength` is unitless in `[0, 1]`; an authored edge is 1.0 and a mined one carries its cosine. `srcPath`/`dstPath` are repo-root-relative with no leading slash. *** ### EDGE\_CLASSES [Section titled “EDGE\_CLASSES”](#edge_classes) ```ts 1 const EDGE_CLASSES: readonly ["memory", "person", "provenance", "task"]; ``` The four non-mixing edge classes. The class is what keeps a person or task edge out of PageRank, MMR, and the retention bridge count. Every memory-graph query filters `edge_class = 'memory'`, and the SQL CHECK constraint refuses a rel that belongs to another class. *** ### EDGE\_PROVENANCES [Section titled “EDGE\_PROVENANCES”](#edge_provenances) ```ts 1 const EDGE_PROVENANCES: readonly ["authored", "sleep", "import"]; ``` Where an edge came from. `derived` edges are only ever `sleep`-provenanced. *** ### EdgeClass [Section titled “EdgeClass”](#edgeclass-1) ```ts 1 const EdgeClass: Literals; ``` *** ### EdgeProvenance [Section titled “EdgeProvenance”](#edgeprovenance-1) ```ts 1 const EdgeProvenance: Literals; ``` *** ### EdgeRel [Section titled “EdgeRel”](#edgerel-1) ```ts 1 const EdgeRel: Literals; ``` *** ### MEMORY\_RELS [Section titled “MEMORY\_RELS”](#memory_rels) ```ts 1 const MEMORY_RELS: readonly ["supersedes", "contradicts", "caused_by", "leads_to", "part_of", "relates_to", "example_of", "supports", "laterally_related"]; ``` The nine memory rels. `supersedes` and `contradicts` are penalty-bearing: they gate the retention `contested_status` signal, so sleep promotes a corroborated one into both files rather than leaving it in the rebuildable index. *** ### MemoryRel [Section titled “MemoryRel”](#memoryrel-1) ```ts 1 const MemoryRel: Literals; ``` *** ### PERSON\_RELS [Section titled “PERSON\_RELS”](#person_rels) ```ts 1 const PERSON_RELS: readonly ["about_person", "authored_by"]; ``` The two person rels, pointing at `resources/people/*`. *** ### PersonRel [Section titled “PersonRel”](#personrel-1) ```ts 1 const PersonRel: Literals; ``` *** ### PROVENANCE\_RELS [Section titled “PROVENANCE\_RELS”](#provenance_rels) ```ts 1 const PROVENANCE_RELS: readonly ["from_session"]; ``` The one provenance rel, linking a memory to the session that produced it. *** ### ProvenanceRel [Section titled “ProvenanceRel”](#provenancerel-1) ```ts 1 const ProvenanceRel: Literals; ``` *** ### REL\_TOKEN\_PREFIX [Section titled “REL\_TOKEN\_PREFIX”](#rel_token_prefix) ```ts 1 const REL_TOKEN_PREFIX: "memhtml-" = "memhtml-"; ``` A `` token, which is the rel prefixed for the HTML plane. `rel` tokens cannot hold a colon, so the prefix is hyphenated and the rel’s own underscores become hyphens: `laterally_related` ⇒ `memhtml-laterally-related`. *** ### TASK\_RELS [Section titled “TASK\_RELS”](#task_rels) ```ts 1 const TASK_RELS: readonly ["blocks", "subtask_of"]; ``` The two task rels, both between two `task` files. Their own class for the same reason the person rels have one: task topology is working state, and a `blocks` edge entering PageRank would let an agent’s to-do list reweight the retention of its knowledge. `@memhtml/store`’s `linkMemories` refuses a task rel unless BOTH endpoints are tasks, and the `edges` CHECK refuses the rel under any other class. *** ### TaskRel [Section titled “TaskRel”](#taskrel-1) ```ts 1 const TaskRel: Literals; ``` ## Functions [Section titled “Functions”](#functions) ### isEdgeRel() [Section titled “isEdgeRel()”](#isedgerel) ```ts 1 function isEdgeRel(rel): rel is "supersedes" | "contradicts" | "caused_by" | "leads_to" | "part_of" | "relates_to" | "example_of" | "supports" | "laterally_related" | "about_person" | "authored_by" | "from_session" | "blocks" | "subtask_of"; ``` True when `rel` is in the closed vocabulary. Narrows an untrusted string. #### Parameters [Section titled “Parameters”](#parameters) ##### rel [Section titled “rel”](#rel) `string` #### Returns [Section titled “Returns”](#returns) rel is “supersedes” | “contradicts” | “caused\_by” | “leads\_to” | “part\_of” | “relates\_to” | “example\_of” | “supports” | “laterally\_related” | “about\_person” | “authored\_by” | “from\_session” | “blocks” | “subtask\_of” *** ### isWellFormedEdge() [Section titled “isWellFormedEdge()”](#iswellformededge) ```ts 1 function isWellFormedEdge(edge): boolean; ``` True when an edge’s declared class matches its rel and it is not a self-loop, the two conditions the `edges` table’s CHECK constraints enforce, stated once here so a caller can refuse a bad edge before the driver does. #### Parameters [Section titled “Parameters”](#parameters-1) ##### edge [Section titled “edge”](#edge-2) ###### derived [Section titled “derived”](#derived) `boolean` = `Schema.Boolean` ###### dstPath [Section titled “dstPath”](#dstpath) `string` = `Schema.String` ###### edgeClass [Section titled “edgeClass”](#edgeclass-2) `"memory"` | `"person"` | `"provenance"` | `"task"` = `EdgeClass` ###### provenance [Section titled “provenance”](#provenance) `"authored"` | `"sleep"` | `"import"` = `EdgeProvenance` ###### rel [Section titled “rel”](#rel-1) \| `"supersedes"` | `"contradicts"` | `"caused_by"` | `"leads_to"` | `"part_of"` | `"relates_to"` | `"example_of"` | `"supports"` | `"laterally_related"` | `"about_person"` | `"authored_by"` | `"from_session"` | `"blocks"` | `"subtask_of"` = `EdgeRel` ###### srcPath [Section titled “srcPath”](#srcpath) `string` = `Schema.String` ###### strength [Section titled “strength”](#strength) `number` = `...` #### Returns [Section titled “Returns”](#returns-1) `boolean` *** ### relClassFor() [Section titled “relClassFor()”](#relclassfor) ```ts 1 function relClassFor(rel): "memory" | "person" | "provenance" | "task"; ``` The class a rel belongs to. Total over [ALL\_RELS](/api/contracts/edges/#all_rels) and injective per class: a rel name appears in exactly one class, which is what lets the class be derived rather than carried alongside the rel and risk disagreeing with it. #### Parameters [Section titled “Parameters”](#parameters-2) ##### rel [Section titled “rel”](#rel-2) \| `"supersedes"` | `"contradicts"` | `"caused_by"` | `"leads_to"` | `"part_of"` | `"relates_to"` | `"example_of"` | `"supports"` | `"laterally_related"` | `"about_person"` | `"authored_by"` | `"from_session"` | `"blocks"` | `"subtask_of"` #### Returns [Section titled “Returns”](#returns-2) `"memory"` | `"person"` | `"provenance"` | `"task"` *** ### relForToken() [Section titled “relForToken()”](#relfortoken) ```ts 1 function relForToken(token): 2 | "supersedes" 3 | "contradicts" 4 | "caused_by" 5 | "leads_to" 6 | "part_of" 7 | "relates_to" 8 | "example_of" 9 | "supports" 10 | "laterally_related" 11 | "about_person" 12 | "authored_by" 13 | "from_session" 14 | "blocks" 15 | "subtask_of" 16 | undefined; ``` The rel behind a `` token, or `undefined` when the token is outside the closed vocabulary. Inverse of [relTokenFor](/api/contracts/edges/#reltokenfor) on its image. #### Parameters [Section titled “Parameters”](#parameters-3) ##### token [Section titled “token”](#token) `string` #### Returns [Section titled “Returns”](#returns-3) \| `"supersedes"` | `"contradicts"` | `"caused_by"` | `"leads_to"` | `"part_of"` | `"relates_to"` | `"example_of"` | `"supports"` | `"laterally_related"` | `"about_person"` | `"authored_by"` | `"from_session"` | `"blocks"` | `"subtask_of"` | `undefined` *** ### relsForClass() [Section titled “relsForClass()”](#relsforclass) ```ts 1 function relsForClass(edgeClass): readonly ( 2 | "supersedes" 3 | "contradicts" 4 | "caused_by" 5 | "leads_to" 6 | "part_of" 7 | "relates_to" 8 | "example_of" 9 | "supports" 10 | "laterally_related" 11 | "about_person" 12 | "authored_by" 13 | "from_session" 14 | "blocks" 15 | "subtask_of")[]; ``` The rels of one class. The inverse of [relClassFor](/api/contracts/edges/#relclassfor), as a set. #### Parameters [Section titled “Parameters”](#parameters-4) ##### edgeClass [Section titled “edgeClass”](#edgeclass-3) `"memory"` | `"person"` | `"provenance"` | `"task"` #### Returns [Section titled “Returns”](#returns-4) readonly ( | `"supersedes"` | `"contradicts"` | `"caused_by"` | `"leads_to"` | `"part_of"` | `"relates_to"` | `"example_of"` | `"supports"` | `"laterally_related"` | `"about_person"` | `"authored_by"` | `"from_session"` | `"blocks"` | `"subtask_of"`)\[] *** ### relTokenFor() [Section titled “relTokenFor()”](#reltokenfor) ```ts 1 function relTokenFor(rel): string; ``` The HTML `` token for a rel. #### Parameters [Section titled “Parameters”](#parameters-5) ##### rel [Section titled “rel”](#rel-3) \| `"supersedes"` | `"contradicts"` | `"caused_by"` | `"leads_to"` | `"part_of"` | `"relates_to"` | `"example_of"` | `"supports"` | `"laterally_related"` | `"about_person"` | `"authored_by"` | `"from_session"` | `"blocks"` | `"subtask_of"` #### Returns [Section titled “Returns”](#returns-5) `string` # errors ## Classes [Section titled “Classes”](#classes) ### DirtyTree [Section titled “DirtyTree”](#dirtytree) An operation that requires a clean tree found uncommitted changes. #### Extends [Section titled “Extends”](#extends) * `object` & `YieldableError`<`this`> #### Constructors [Section titled “Constructors”](#constructors) ##### Constructor [Section titled “Constructor”](#constructor) ```ts 1 new DirtyTree(...args): DirtyTree; ``` ###### Parameters [Section titled “Parameters”](#parameters) ###### args [Section titled “args”](#args) …\[`object`, `MakeOptions`] ###### Returns [Section titled “Returns”](#returns) [`DirtyTree`](/api/contracts/errors/#dirtytree) ###### Inherited from [Section titled “Inherited from”](#inherited-from) ```ts 1 Schema.TaggedError()("DirtyTree", { 2 paths: Schema.Array(Schema.String) 3 }).constructor ``` #### Properties [Section titled “Properties”](#properties) ##### paths [Section titled “paths”](#paths) ```ts 1 readonly paths: readonly string[]; ``` ###### Inherited from [Section titled “Inherited from”](#inherited-from-1) ```ts 1 Schema.TaggedError()("DirtyTree", { 2 paths: Schema.Array(Schema.String) 3 }).paths ``` *** ### DuplicateContent [Section titled “DuplicateContent”](#duplicatecontent) The content hash already belongs to an active file. `existingPath` is what the caller wanted to create, so a deduped write is answerable without a second query. #### Extends [Section titled “Extends”](#extends-1) * `object` & `YieldableError`<`this`> #### Constructors [Section titled “Constructors”](#constructors-1) ##### Constructor [Section titled “Constructor”](#constructor-1) ```ts 1 new DuplicateContent(...args): DuplicateContent; ``` ###### Parameters [Section titled “Parameters”](#parameters-1) ###### args [Section titled “args”](#args-1) …\[`object`, `MakeOptions`] ###### Returns [Section titled “Returns”](#returns-1) [`DuplicateContent`](/api/contracts/errors/#duplicatecontent) ###### Inherited from [Section titled “Inherited from”](#inherited-from-2) ```ts 1 Schema.TaggedError()("DuplicateContent", { 2 contentHash: Schema.String, 3 existingPath: Schema.String 4 }).constructor ``` #### Properties [Section titled “Properties”](#properties-1) ##### contentHash [Section titled “contentHash”](#contenthash) ```ts 1 readonly contentHash: string = Schema.String; ``` ###### Inherited from [Section titled “Inherited from”](#inherited-from-3) ```ts 1 Schema.TaggedError()("DuplicateContent", { 2 contentHash: Schema.String, 3 existingPath: Schema.String 4 }).contentHash ``` ##### existingPath [Section titled “existingPath”](#existingpath) ```ts 1 readonly existingPath: string = Schema.String; ``` ###### Inherited from [Section titled “Inherited from”](#inherited-from-4) ```ts 1 Schema.TaggedError()("DuplicateContent", { 2 contentHash: Schema.String, 3 existingPath: Schema.String 4 }).existingPath ``` *** ### InvalidMemory [Section titled “InvalidMemory”](#invalidmemory) A memory that violates the file format or the type/placement vocabulary. #### Extends [Section titled “Extends”](#extends-2) * `object` & `YieldableError`<`this`> #### Constructors [Section titled “Constructors”](#constructors-2) ##### Constructor [Section titled “Constructor”](#constructor-2) ```ts 1 new InvalidMemory(...args): InvalidMemory; ``` ###### Parameters [Section titled “Parameters”](#parameters-2) ###### args [Section titled “args”](#args-2) …\[`object`, `MakeOptions`] ###### Returns [Section titled “Returns”](#returns-2) [`InvalidMemory`](/api/contracts/errors/#invalidmemory) ###### Inherited from [Section titled “Inherited from”](#inherited-from-5) ```ts 1 Schema.TaggedError()("InvalidMemory", { 2 reason: Schema.String 3 }).constructor ``` #### Properties [Section titled “Properties”](#properties-2) ##### reason [Section titled “reason”](#reason) ```ts 1 readonly reason: string = Schema.String; ``` ###### Inherited from [Section titled “Inherited from”](#inherited-from-6) ```ts 1 Schema.TaggedError()("InvalidMemory", { 2 reason: Schema.String 3 }).reason ``` *** ### LlmContractViolation [Section titled “LlmContractViolation”](#llmcontractviolation) The model broke its structured-output contract: an undecodable tool payload, a `max_tokens` stop, or a refusal. The item is reported with no result, and a violation does not become a value. #### Extends [Section titled “Extends”](#extends-3) * `object` & `YieldableError`<`this`> #### Constructors [Section titled “Constructors”](#constructors-3) ##### Constructor [Section titled “Constructor”](#constructor-3) ```ts 1 new LlmContractViolation(...args): LlmContractViolation; ``` ###### Parameters [Section titled “Parameters”](#parameters-3) ###### args [Section titled “args”](#args-3) …\[`object`, `MakeOptions`] ###### Returns [Section titled “Returns”](#returns-3) [`LlmContractViolation`](/api/contracts/errors/#llmcontractviolation) ###### Inherited from [Section titled “Inherited from”](#inherited-from-7) ```ts 1 Schema.TaggedError()( 2 "LlmContractViolation", 3 { 4 reason: Schema.String 5 } 6 ).constructor ``` #### Properties [Section titled “Properties”](#properties-3) ##### reason [Section titled “reason”](#reason-1) ```ts 1 readonly reason: string = Schema.String; ``` ###### Inherited from [Section titled “Inherited from”](#inherited-from-8) ```ts 1 Schema.TaggedError()( 2 "LlmContractViolation", 3 { 4 reason: Schema.String 5 } 6 ).reason ``` *** ### ModelUnavailable [Section titled “ModelUnavailable”](#modelunavailable) Bedrock refused the call: throttling, an unavailable model, or a denied region. #### Extends [Section titled “Extends”](#extends-4) * `object` & `YieldableError`<`this`> #### Constructors [Section titled “Constructors”](#constructors-4) ##### Constructor [Section titled “Constructor”](#constructor-4) ```ts 1 new ModelUnavailable(...args): ModelUnavailable; ``` ###### Parameters [Section titled “Parameters”](#parameters-4) ###### args [Section titled “args”](#args-4) …\[`object`, `MakeOptions`] ###### Returns [Section titled “Returns”](#returns-4) [`ModelUnavailable`](/api/contracts/errors/#modelunavailable) ###### Inherited from [Section titled “Inherited from”](#inherited-from-9) ```ts 1 Schema.TaggedError()("ModelUnavailable", { 2 modelId: Schema.String, 3 reason: Schema.String 4 }).constructor ``` #### Properties [Section titled “Properties”](#properties-4) ##### modelId [Section titled “modelId”](#modelid) ```ts 1 readonly modelId: string = Schema.String; ``` ###### Inherited from [Section titled “Inherited from”](#inherited-from-10) ```ts 1 Schema.TaggedError()("ModelUnavailable", { 2 modelId: Schema.String, 3 reason: Schema.String 4 }).modelId ``` ##### reason [Section titled “reason”](#reason-2) ```ts 1 readonly reason: string = Schema.String; ``` ###### Inherited from [Section titled “Inherited from”](#inherited-from-11) ```ts 1 Schema.TaggedError()("ModelUnavailable", { 2 modelId: Schema.String, 3 reason: Schema.String 4 }).reason ``` *** ### PathNotFound [Section titled “PathNotFound”](#pathnotfound) A repo-root-relative path with no file behind it. #### Extends [Section titled “Extends”](#extends-5) * `object` & `YieldableError`<`this`> #### Constructors [Section titled “Constructors”](#constructors-5) ##### Constructor [Section titled “Constructor”](#constructor-5) ```ts 1 new PathNotFound(...args): PathNotFound; ``` ###### Parameters [Section titled “Parameters”](#parameters-5) ###### args [Section titled “args”](#args-5) …\[`object`, `MakeOptions`] ###### Returns [Section titled “Returns”](#returns-5) [`PathNotFound`](/api/contracts/errors/#pathnotfound) ###### Inherited from [Section titled “Inherited from”](#inherited-from-12) ```ts 1 Schema.TaggedError()("PathNotFound", { 2 path: Schema.String 3 }).constructor ``` #### Properties [Section titled “Properties”](#properties-5) ##### path [Section titled “path”](#path) ```ts 1 readonly path: string = Schema.String; ``` ###### Inherited from [Section titled “Inherited from”](#inherited-from-13) ```ts 1 Schema.TaggedError()("PathNotFound", { 2 path: Schema.String 3 }).path ``` *** ### StorageFailure [Section titled “StorageFailure”](#storagefailure) A driver or filesystem rejection, reduced to the operation that failed. The payload deliberately excludes SQL text, parameters, and row contents so a storage error can be returned to an agent without leaking corpus content; the driver’s own message goes to `Effect.logError` at the adapter edge instead. #### Extends [Section titled “Extends”](#extends-6) * `object` & `YieldableError`<`this`> #### Constructors [Section titled “Constructors”](#constructors-6) ##### Constructor [Section titled “Constructor”](#constructor-6) ```ts 1 new StorageFailure(...args): StorageFailure; ``` ###### Parameters [Section titled “Parameters”](#parameters-6) ###### args [Section titled “args”](#args-6) …\[`object`, `MakeOptions`] ###### Returns [Section titled “Returns”](#returns-6) [`StorageFailure`](/api/contracts/errors/#storagefailure) ###### Inherited from [Section titled “Inherited from”](#inherited-from-14) ```ts 1 Schema.TaggedError()("StorageFailure", { 2 operation: Schema.String 3 }).constructor ``` #### Properties [Section titled “Properties”](#properties-6) ##### operation [Section titled “operation”](#operation) ```ts 1 readonly operation: string = Schema.String; ``` ###### Inherited from [Section titled “Inherited from”](#inherited-from-15) ```ts 1 Schema.TaggedError()("StorageFailure", { 2 operation: Schema.String 3 }).operation ``` *** ### WriteConflict [Section titled “WriteConflict”](#writeconflict) Two writers touched the same file. `ourSha` is the blob sha this process wrote from, `theirSha` the blob sha now in the tree. Recovery belongs to the caller: re-read the current content and reapply. #### Extends [Section titled “Extends”](#extends-7) * `object` & `YieldableError`<`this`> #### Constructors [Section titled “Constructors”](#constructors-7) ##### Constructor [Section titled “Constructor”](#constructor-7) ```ts 1 new WriteConflict(...args): WriteConflict; ``` ###### Parameters [Section titled “Parameters”](#parameters-7) ###### args [Section titled “args”](#args-7) …\[`object`, `MakeOptions`] ###### Returns [Section titled “Returns”](#returns-7) [`WriteConflict`](/api/contracts/errors/#writeconflict) ###### Inherited from [Section titled “Inherited from”](#inherited-from-16) ```ts 1 Schema.TaggedError()("WriteConflict", { 2 path: Schema.String, 3 ourSha: Schema.String, 4 theirSha: Schema.String 5 }).constructor ``` #### Properties [Section titled “Properties”](#properties-7) ##### ourSha [Section titled “ourSha”](#oursha) ```ts 1 readonly ourSha: string = Schema.String; ``` ###### Inherited from [Section titled “Inherited from”](#inherited-from-17) ```ts 1 Schema.TaggedError()("WriteConflict", { 2 path: Schema.String, 3 ourSha: Schema.String, 4 theirSha: Schema.String 5 }).ourSha ``` ##### path [Section titled “path”](#path-1) ```ts 1 readonly path: string = Schema.String; ``` ###### Inherited from [Section titled “Inherited from”](#inherited-from-18) ```ts 1 Schema.TaggedError()("WriteConflict", { 2 path: Schema.String, 3 ourSha: Schema.String, 4 theirSha: Schema.String 5 }).path ``` ##### theirSha [Section titled “theirSha”](#theirsha) ```ts 1 readonly theirSha: string = Schema.String; ``` ###### Inherited from [Section titled “Inherited from”](#inherited-from-19) ```ts 1 Schema.TaggedError()("WriteConflict", { 2 path: Schema.String, 3 ourSha: Schema.String, 4 theirSha: Schema.String 5 }).theirSha ``` # paths ## Interfaces [Section titled “Interfaces”](#interfaces) ### PlacementInput [Section titled “PlacementInput”](#placementinput) What [placementFor](/api/contracts/paths/#placementfor) decides from. `path` is the caller’s explicit override. Every optional field also admits `undefined` explicitly. Under `exactOptionalPropertyTypes` a bare `path?: string` would refuse `placementFor({ path: params.path, … })` for a `string | undefined` param, forcing every tool adapter to strip absent fields by hand. The contract therefore accepts the shape the MCP and CLI layers actually hold. #### Properties [Section titled “Properties”](#properties) ##### entities? [Section titled “entities?”](#entities) ```ts 1 readonly optional entities?: readonly string[]; ``` ##### memoryType [Section titled “memoryType”](#memorytype) ```ts 1 readonly memoryType: string; ``` ##### path? [Section titled “path?”](#path) ```ts 1 readonly optional path?: string; ``` ##### tags? [Section titled “tags?”](#tags) ```ts 1 readonly optional tags?: readonly string[]; ``` ##### workspace? [Section titled “workspace?”](#workspace) ```ts 1 readonly optional workspace?: string; ``` ## Variables [Section titled “Variables”](#variables) ### ARCHIVE\_BUCKET [Section titled “ARCHIVE\_BUCKET”](#archive_bucket) ```ts 1 const ARCHIVE_BUCKET: "archive" = "archive"; ``` The bucket eviction moves into, partitioned by year. *** ### ARCS\_DIR [Section titled “ARCS\_DIR”](#arcs_dir) ```ts 1 const ARCS_DIR: "areas/arcs" = "areas/arcs"; ``` Behavioral arcs. Under `areas/` because PARA is fixed at four buckets. *** ### INBOX\_DIR [Section titled “INBOX\_DIR”](#inbox_dir) ```ts 1 const INBOX_DIR: "areas/inbox" = "areas/inbox"; ``` Where a memory lands when no rule claims it. `memhtml doctor` reports inbox depth as a health signal, so an unplaceable memory is visible rather than lost. *** ### MEMORY\_EXTENSION [Section titled “MEMORY\_EXTENSION”](#memory_extension) ```ts 1 const MEMORY_EXTENSION: ".html" = ".html"; ``` The file extension every memory carries. *** ### PEOPLE\_DIR [Section titled “PEOPLE\_DIR”](#people_dir) ```ts 1 const PEOPLE_DIR: "resources/people" = "resources/people"; ``` The person plane, folded into `resources/` rather than given its own bucket. *** ### TASKS\_SUBDIR [Section titled “TASKS\_SUBDIR”](#tasks_subdir) ```ts 1 const TASKS_SUBDIR: "tasks" = "tasks"; ``` The directory segment every task file sits under, appended to its workspace’s project directory or to the inbox. A subdirectory rather than a fifth bucket: PARA is fixed at four, and a task belongs to whatever the memory beside it belongs to. Keeping tasks in one named segment is what makes `ls projects//tasks` the list operation, which is the design’s CRUDL-without-retrieval contract. The segment name is therefore part of the contract. ## Functions [Section titled “Functions”](#functions) ### archivePathFor() [Section titled “archivePathFor()”](#archivepathfor) ```ts 1 function archivePathFor(path, year): string; ``` The archive path a memory moves to on eviction: `archive//`, with the original path mirrored exactly beneath the year. `year` is a calendar year (a label, not an offset). Mirroring the whole original path is what makes the mapping injective and invertible, so `git log --follow` reads through the move and `diff -M` reports it as `R100` rather than a delete plus an add. #### Parameters [Section titled “Parameters”](#parameters) ##### path [Section titled “path”](#path-1) `string` ##### year [Section titled “year”](#year) `number` #### Returns [Section titled “Returns”](#returns) `string` *** ### archiveYearOf() [Section titled “archiveYearOf()”](#archiveyearof) ```ts 1 function archiveYearOf(path): number | undefined; ``` The archive year of an archive path, or `undefined` when the path is not archived. #### Parameters [Section titled “Parameters”](#parameters-1) ##### path [Section titled “path”](#path-2) `string` #### Returns [Section titled “Returns”](#returns-1) `number` | `undefined` *** ### isArchivePath() [Section titled “isArchivePath()”](#isarchivepath) ```ts 1 function isArchivePath(path): boolean; ``` True when a path sits under the archive bucket with a year partition. #### Parameters [Section titled “Parameters”](#parameters-2) ##### path [Section titled “path”](#path-3) `string` #### Returns [Section titled “Returns”](#returns-2) `boolean` *** ### isValidMemoryPath() [Section titled “isValidMemoryPath()”](#isvalidmemorypath) ```ts 1 function isValidMemoryPath(path): boolean; ``` True when a path is a usable memory path: rooted in a PARA bucket, ending in `.html`, carrying no `.` or `..` segment. [memoryPathViolation](/api/contracts/paths/#memorypathviolation) asked as a boolean, so the predicate and the refusal’s reason cannot disagree about which paths are usable. #### Parameters [Section titled “Parameters”](#parameters-3) ##### path [Section titled “path”](#path-4) `string` #### Returns [Section titled “Returns”](#returns-3) `boolean` *** ### memoryPathFor() [Section titled “memoryPathFor()”](#memorypathfor) ```ts 1 function memoryPathFor(input): string; ``` The full path for a new memory. An explicit valid `path` is authoritative and returned verbatim in canonical form; otherwise the directory comes from [placementFor](/api/contracts/paths/#placementfor) and the filename from [filenameFor](/api/contracts/slug/#filenamefor), which date-prefixes an episodic entry. #### Parameters [Section titled “Parameters”](#parameters-4) ##### input [Section titled “input”](#input) [`PlacementInput`](/api/contracts/paths/#placementinput) & `object` #### Returns [Section titled “Returns”](#returns-4) `string` *** ### memoryPathViolation() [Section titled “memoryPathViolation()”](#memorypathviolation) ```ts 1 function memoryPathViolation(path): string | undefined; ``` Why a path is not a usable memory path, or `undefined` when it is one. The RULE and its explanation in one function, because a refusal that restated the rule in its own words would be a second copy of it, free to name a clause this function does not check. A caller that refuses an unusable path quotes this string; [isValidMemoryPath](/api/contracts/paths/#isvalidmemorypath) is the same question asked as a boolean. Each clause names the input it saw rather than the rule in the abstract, so a caller holding the message can act without re-reading the format doc. The traversal clause is the security one: it is what keeps a caller-supplied path from escaping the memory repo. #### Parameters [Section titled “Parameters”](#parameters-5) ##### path [Section titled “path”](#path-5) `string` #### Returns [Section titled “Returns”](#returns-5) `string` | `undefined` *** ### normalizePath() [Section titled “normalizePath()”](#normalizepath) ```ts 1 function normalizePath(path): string; ``` Reduce a caller-supplied path to the canonical git-tree form: leading slashes dropped (callers may pass the `` document-reference form), repeated slashes collapsed, trailing slash dropped. The trailing slash is removed by `endsWith`/`slice` rather than by `//+$/`, which is not a style choice: an unanchored-left `/+$` is quadratic on a long run of slashes followed by a non-slash, measured 2026-08-18 at 4 ms / 57 ms / 769 ms / 3049 ms for n = 2k / 8k / 32k / 64k. The collapse above happens to defuse it — after it, no run of two slashes survives, so the pattern can only ever match one character — but that made the cost of this function depend on the ORDER of three chained calls, with nothing stating it and nothing checking it. Reordering them, or dropping the collapse, would reintroduce a multi-second stall on 64 KB of input across this function’s callers, all of which sit on the write path that accepts agent-supplied paths. A single-character slice cannot be reordered into a hazard. `packages/contracts/tests/paths.test.ts` asserts the cost curve at adversarial sizes. #### Parameters [Section titled “Parameters”](#parameters-6) ##### path [Section titled “path”](#path-6) `string` #### Returns [Section titled “Returns”](#returns-6) `string` *** ### originalPathFor() [Section titled “originalPathFor()”](#originalpathfor) ```ts 1 function originalPathFor(archivePath): string | undefined; ``` The pre-eviction path behind an archive path, or `undefined` when the path is not an archive path. Strips exactly one `archive//` prefix, so it is the left inverse of [archivePathFor](/api/contracts/paths/#archivepathfor) even for a memory archived twice. #### Parameters [Section titled “Parameters”](#parameters-7) ##### archivePath [Section titled “archivePath”](#archivepath) `string` #### Returns [Section titled “Returns”](#returns-7) `string` | `undefined` *** ### paraBucketOf() [Section titled “paraBucketOf()”](#parabucketof) ```ts 1 function paraBucketOf(path): "projects" | "areas" | "resources" | "archive" | undefined; ``` The PARA bucket a path sits in, or `undefined` when it sits outside all four. #### Parameters [Section titled “Parameters”](#parameters-8) ##### path [Section titled “path”](#path-7) `string` #### Returns [Section titled “Returns”](#returns-8) `"projects"` | `"areas"` | `"resources"` | `"archive"` | `undefined` *** ### placementFor() [Section titled “placementFor()”](#placementfor) ```ts 1 function placementFor(input): string; ``` The directory a memory belongs in, following design §2.1’s six rules in order, with the `task` rule sitting between the arc rule and the person rule. Total: it always returns a directory rooted in a PARA bucket, so the write path never guesses twice and never fails. Returns the *directory*, not the full path, because the filename needs a title this input does not carry. [memoryPathFor](/api/contracts/paths/#memorypathfor) composes the two. An explicit `path` contributes its directory; when that path is unusable it is ignored rather than propagated, so the return stays a valid bucket and this function stays total. Totality is why the refusal cannot live here. A caller that wants an unusable path refused rather than re-derived asks the store for it (`strictPath` on a `WriteInput`), which gates on [memoryPathViolation](/api/contracts/paths/#memorypathviolation) before any of this runs and quotes its reason. Refusing here instead would make placement fallible for every caller, including the sleep phases that place a synthesized arc and have no caller path to be wrong about. #### Parameters [Section titled “Parameters”](#parameters-9) ##### input [Section titled “input”](#input-1) [`PlacementInput`](/api/contracts/paths/#placementinput) #### Returns [Section titled “Returns”](#returns-9) `string` # slug ## Variables [Section titled “Variables”](#variables) ### EPISODIC\_PREFIX\_LENGTH [Section titled “EPISODIC\_PREFIX\_LENGTH”](#episodic_prefix_length) ```ts 1 const EPISODIC_PREFIX_LENGTH: 9 = 9; ``` The `YYYYMMDD-` prefix an episodic filename carries. Time is part of an episodic entry’s identity and it never receives a correction in place, so the date belongs in the name; every other type is timeless and correctable, so it gets a bare slug. *** ### SLUG\_FALLBACK [Section titled “SLUG\_FALLBACK”](#slug_fallback) ```ts 1 const SLUG_FALLBACK: "untitled" = "untitled"; ``` The stem used when a title reduces to nothing sluggable, such as an all-punctuation or non-Latin title. A placeholder beats an empty filename, and `memhtml doctor` can find these by name. *** ### SLUG\_MAX\_LENGTH [Section titled “SLUG\_MAX\_LENGTH”](#slug_max_length) ```ts 1 const SLUG_MAX_LENGTH: 80 = 80; ``` Maximum slug length in characters, before any collision suffix. ## Functions [Section titled “Functions”](#functions) ### datePrefix() [Section titled “datePrefix()”](#dateprefix) ```ts 1 function datePrefix(at): string; ``` Format an instant as the `YYYYMMDD` stamp of an episodic filename, in UTC. #### Parameters [Section titled “Parameters”](#parameters) ##### at [Section titled “at”](#at) `Date` #### Returns [Section titled “Returns”](#returns) `string` *** ### filenameFor() [Section titled “filenameFor()”](#filenamefor) ```ts 1 function filenameFor(input): string; ``` The filename for a memory: `20260802-slug.html` for episodic, `slug.html` otherwise. The date prefix sits outside the slug’s length budget, because it is identity, not title. #### Parameters [Section titled “Parameters”](#parameters-1) ##### input [Section titled “input”](#input) ###### at [Section titled “at”](#at-1) `Date` ###### episodic [Section titled “episodic”](#episodic) `boolean` ###### slug [Section titled “slug”](#slug) `string` #### Returns [Section titled “Returns”](#returns-1) `string` *** ### hasDatePrefix() [Section titled “hasDatePrefix()”](#hasdateprefix) ```ts 1 function hasDatePrefix(filename): boolean; ``` True when a filename carries the `YYYYMMDD-` episodic prefix. #### Parameters [Section titled “Parameters”](#parameters-2) ##### filename [Section titled “filename”](#filename) `string` #### Returns [Section titled “Returns”](#returns-2) `boolean` *** ### isSlug() [Section titled “isSlug()”](#isslug) ```ts 1 function isSlug(value): boolean; ``` True when a string is already a valid slug, the fixed point of [slugify](/api/contracts/slug/#slugify). #### Parameters [Section titled “Parameters”](#parameters-3) ##### value [Section titled “value”](#value) `string` #### Returns [Section titled “Returns”](#returns-3) `boolean` *** ### slugify() [Section titled “slugify()”](#slugify) ```ts 1 function slugify(title): string; ``` Kebab-case a title into `[a-z0-9-]`, at most [SLUG\_MAX\_LENGTH](/api/contracts/slug/#slug_max_length) characters. Diacritics are folded to their base letters (`déployé` ⇒ `deploye`) rather than dropped, so a title stays recognizable. Runs of separators collapse to one hyphen and the result carries no leading or trailing hyphen, which makes the function idempotent: a slug fed back in comes out unchanged. Truncation cuts at [SLUG\_MAX\_LENGTH](/api/contracts/slug/#slug_max_length) and then trims any hyphen the cut exposed, so a truncated slug is still a valid slug rather than one ending mid-separator. #### Parameters [Section titled “Parameters”](#parameters-4) ##### title [Section titled “title”](#title) `string` #### Returns [Section titled “Returns”](#returns-4) `string` *** ### withCollisionOrdinal() [Section titled “withCollisionOrdinal()”](#withcollisionordinal) ```ts 1 function withCollisionOrdinal(slug, ordinal): string; ``` Append a collision suffix. `ordinal` is a 1-based ordinal for display in the filename; ordinal 1 is the unsuffixed slug, 2 becomes `-2`, and so on, matching the `-2`/`-3` convention. The suffix is added inside the length budget, so a maximum-length slug is shortened rather than overflowed. **The result never equals the input, at any slug length.** That is what makes the store’s collision loop (`packages/store/src/store.ts`, `pathFor`) terminate rather than re-propose the name that collided. It is not free near the length cap: for a slug whose own tail IS the suffix, cutting to make room and appending the suffix can rebuild the slug — and because the cut also trims any hyphen it exposes, the rebuild can recur at MORE than one cut width. The stem is therefore re-cut from its own post-trim length until appending the suffix no longer reproduces the input; each re-cut strictly shortens the stem, so the loop terminates and the result stays inside the budget. #### Parameters [Section titled “Parameters”](#parameters-5) ##### slug [Section titled “slug”](#slug-1) `string` ##### ordinal [Section titled “ordinal”](#ordinal) `number` #### Returns [Section titled “Returns”](#returns-5) `string` # types ## Type Aliases [Section titled “Type Aliases”](#type-aliases) ### MemoryStatus [Section titled “MemoryStatus”](#memorystatus) ```ts 1 type MemoryStatus = typeof MemoryStatus.Type; ``` The status a memory file carries in `memhtml-status`. *** ### MemoryType [Section titled “MemoryType”](#memorytype) ```ts 1 type MemoryType = typeof MemoryType.Type; ``` *** ### ParaBucket [Section titled “ParaBucket”](#parabucket) ```ts 1 type ParaBucket = typeof ParaBucket.Type; ``` *** ### TaskStatus [Section titled “TaskStatus”](#taskstatus) ```ts 1 type TaskStatus = typeof TaskStatus.Type; ``` *** ### WritableMemoryType [Section titled “WritableMemoryType”](#writablememorytype) ```ts 1 type WritableMemoryType = typeof WritableMemoryType.Type; ``` ## Variables [Section titled “Variables”](#variables) ### Confidence [Section titled “Confidence”](#confidence) ```ts 1 const Confidence: Number; ``` Confidence, unitless in `[0, 1]`. 1.0 is an unqualified assertion. *** ### ENTITY\_SEPARATOR [Section titled “ENTITY\_SEPARATOR”](#entity_separator) ```ts 1 const ENTITY_SEPARATOR: ":" = ":"; ``` A `type:name` entity reference, e.g. `service:checkout-api`, `person:sanju`. The prefix before the first colon is the entity type; everything after is the name, which may itself contain colons. *** ### Importance [Section titled “Importance”](#importance) ```ts 1 const Importance: Int; ``` Importance, 1-10 inclusive, 1-based ordinal on a display scale, never an arithmetic input on its own. The retention scorer divides it by 10 to reach `[0, 1]` before it meets any other signal. *** ### MEMORY\_TYPES [Section titled “MEMORY\_TYPES”](#memory_types) ```ts 1 const MEMORY_TYPES: readonly ["episodic", "semantic", "procedural", "agent_insight", "user_preference", "error_pattern", "verdict", "precedent", "arc", "task"]; ``` The memory type vocabulary, closed. Ten values, restated by the `files.memory_type` CHECK constraint in SQL. `arc` is in the vocabulary but absent from [WRITABLE\_MEMORY\_TYPES](/api/contracts/types/#writable_memory_types): an arc is synthesized by the sleep cycle from many memories, so an agent naming one directly would be asserting a conclusion the corpus has not yet earned. `task` is ONE axis with the other nine rather than a parallel `kind` column, because three overlapping type vocabularies is what made the predecessor memory system’s classification unanswerable. A task is a memory type whose retrieval, dedup, and curation treatment a filter states, not a second axis. Tasks are default-excluded from search and skipped by sleep. See `@memhtml/index`‘s `assembleScope` and the sleep phases’ `excludeTypes`. *** ### MemoryPath [Section titled “MemoryPath”](#memorypath) ```ts 1 const MemoryPath: String; ``` A repo-root-relative path to a memory file, e.g. `areas/oncall/rollback-order.html`. No leading slash: this is the git-tree form, the `files.path` primary key, and the id of a memory. `` values carry the same path with a leading `/`. That is a document-reference form, converted at the HTML boundary, never stored here. *** ### MemoryStatus [Section titled “MemoryStatus”](#memorystatus-1) ```ts 1 const MemoryStatus: Literals; ``` The status a memory file carries in `memhtml-status`. *** ### MemoryType [Section titled “MemoryType”](#memorytype-1) ```ts 1 const MemoryType: Literals; ``` *** ### PARA\_BUCKETS [Section titled “PARA\_BUCKETS”](#para_buckets) ```ts 1 const PARA_BUCKETS: readonly ["projects", "areas", "resources", "archive"]; ``` PARA’s four buckets, closed and ordered. `archive` is a bucket rather than a status because eviction is a `git mv`. The path itself records the state, so `git log --follow` reads through it and `diff -M` reports the move as `R100`. *** ### ParaBucket [Section titled “ParaBucket”](#parabucket-1) ```ts 1 const ParaBucket: Literals; ``` *** ### PERSON\_ENTITY\_PREFIX [Section titled “PERSON\_ENTITY\_PREFIX”](#person_entity_prefix) ```ts 1 const PERSON_ENTITY_PREFIX: "person:"; ``` The `person:` entity prefix, which routes a semantic memory to `resources/people/`. *** ### TASK\_STATUSES [Section titled “TASK\_STATUSES”](#task_statuses) ```ts 1 const TASK_STATUSES: readonly ["todo", "doing", "blocked", "done"]; ``` A task’s own status, carried in `memhtml-task-status`, a SEPARATE axis from [MemoryStatus](/api/contracts/types/#memorystatus-1), which stays `active | archived` for every type including `task`. Two axes rather than four `memhtml-status` values because `active`/`archived` is what every archive, correction, and publish path switches on, and a fifth value there would silently change the meaning of each of them. Finishing a task stamps `done` AND archives the file through the same `archiveMemory` machinery, so `done` is not a resting state on its own and “what did I finish” is answered by the archive tree plus `git log`. *** ### TaskStatus [Section titled “TaskStatus”](#taskstatus-1) ```ts 1 const TaskStatus: Literals; ``` *** ### WRITABLE\_MEMORY\_TYPES [Section titled “WRITABLE\_MEMORY\_TYPES”](#writable_memory_types) ```ts 1 const WRITABLE_MEMORY_TYPES: ( 2 | "task" 3 | "episodic" 4 | "semantic" 5 | "procedural" 6 | "agent_insight" 7 | "user_preference" 8 | "error_pattern" 9 | "verdict" 10 | "precedent")[]; ``` The nine types `memory_write` exposes. `arc` is system-written only, so the tool parameter enum is narrower than the storage vocabulary by exactly that one value. *** ### WritableMemoryType [Section titled “WritableMemoryType”](#writablememorytype-1) ```ts 1 const WritableMemoryType: Literals; ``` ## Functions [Section titled “Functions”](#functions) ### isPersonEntity() [Section titled “isPersonEntity()”](#ispersonentity) ```ts 1 function isPersonEntity(entity): boolean; ``` True when an entity reference names a person: the `person:` prefix plus a name that survives `trim()`. The trim is what makes this predicate agree with the rest of the person plane. `placementFor` routes on it, and the sleep phase that mints the person file and its `memhtml-about-person` links keys on `entity_name.trim() !== ""`. A whitespace-only name accepted here would land a memory in `resources/people/` that no phase will ever give a person file to link at — and `slugify` maps that name to `untitled`, which names nobody. #### Parameters [Section titled “Parameters”](#parameters) ##### entity [Section titled “entity”](#entity) `string` #### Returns [Section titled “Returns”](#returns) `boolean` *** ### isTaskStatus() [Section titled “isTaskStatus()”](#istaskstatus) ```ts 1 function isTaskStatus(value): value is "todo" | "doing" | "blocked" | "done"; ``` True when a string is in the closed task-status vocabulary. Narrows an untrusted value. #### Parameters [Section titled “Parameters”](#parameters-1) ##### value [Section titled “value”](#value) `string` #### Returns [Section titled “Returns”](#returns-1) value is “todo” | “doing” | “blocked” | “done” *** ### isWritableMemoryType() [Section titled “isWritableMemoryType()”](#iswritablememorytype) ```ts 1 function isWritableMemoryType(type): type is "task" | "episodic" | "semantic" | "procedural" | "agent_insight" | "user_preference" | "error_pattern" | "verdict" | "precedent"; ``` True for the nine types an agent may write. Narrows, so a caller can branch on it. #### Parameters [Section titled “Parameters”](#parameters-2) ##### type [Section titled “type”](#type) \| `"task"` | `"episodic"` | `"semantic"` | `"procedural"` | `"agent_insight"` | `"user_preference"` | `"error_pattern"` | `"verdict"` | `"precedent"` | `"arc"` #### Returns [Section titled “Returns”](#returns-2) type is “task” | “episodic” | “semantic” | “procedural” | “agent\_insight” | “user\_preference” | “error\_pattern” | “verdict” | “precedent” *** ### normalizeEntityName() [Section titled “normalizeEntityName()”](#normalizeentityname) ```ts 1 function normalizeEntityName(name): string; ``` Lowercase, NFC-normalize, collapse internal whitespace, trim. What it means for two entity names to be the SAME name. It lives in contracts rather than beside a caller because it is a vocabulary rule, and a second copy of a vocabulary rule fails silently: `entity-resolution` decides which `memhtml-entity` metas to rewrite by comparing a name against this form, so a divergent copy would have the phase canonicalize files toward a spelling nothing else recognizes, reporting merges no query can reach. `file_entities` stores names AS AUTHORED, not in this form, and that is deliberate — the phase finds its work by reading those rows back, so a projection that pre-normalized would hide every unnormalized meta from the one pass whose job is to fix it. The two SQL doors fold with `lower()` on both sides instead: a narrower fold (SQLite’s `lower()` is ASCII-only) that cannot disagree with itself across the JS/SQL seam. Full canonicalization is durable in the TREE, applied by the phase. #### Parameters [Section titled “Parameters”](#parameters-3) ##### name [Section titled “name”](#name) `string` #### Returns [Section titled “Returns”](#returns-3) `string` *** ### normalizeEntityRef() [Section titled “normalizeEntityRef()”](#normalizeentityref) ```ts 1 function normalizeEntityRef(entity): string; ``` A whole reference in that form: both halves normalized, rejoined, padding around the separator gone. For callers comparing references in TypeScript — the write path deciding whether a candidate entity is one the corpus already names. The SQL doors do NOT use this; see the seam note above. Total over unparseable input. A string with no separator normalizes as a whole and is returned without one, so the caller still decides what an untyped reference means rather than receiving a silently invented type. #### Parameters [Section titled “Parameters”](#parameters-4) ##### entity [Section titled “entity”](#entity-1) `string` #### Returns [Section titled “Returns”](#returns-4) `string` *** ### parseEntity() [Section titled “parseEntity()”](#parseentity) ```ts 1 function parseEntity(entity): 2 | { 3 entityName: string; 4 entityType: string; 5 } 6 | undefined; ``` Split an entity reference into its type and name at the first separator. `undefined` when the reference has no separator, or when the type or the name on either side of it is empty. #### Parameters [Section titled “Parameters”](#parameters-5) ##### entity [Section titled “entity”](#entity-2) `string` #### Returns [Section titled “Returns”](#returns-5) ##### Type Literal [Section titled “Type Literal”](#type-literal) ```ts 1 { 2 entityName: string; 3 entityType: string; 4 } ``` ###### entityName [Section titled “entityName”](#entityname) ```ts 1 readonly entityName: string; ``` Everything after the first separator, colons included. ###### entityType [Section titled “entityType”](#entitytype) ```ts 1 readonly entityType: string; ``` The prefix before the first separator. *** `undefined` # @memhtml/domain `@memhtml/domain`: the pure math under memhtml’s retrieval and sleep phases: cosine similarity and pairwise neighbor selection, reciprocal-rank fusion and maximal marginal relevance, PageRank and communities over the memory graph, the frame key, the retention scorer with its triage bands and reprieve gate, confidence decay on a fixed-point grid, the reinforcement cooldown, and the near-duplicate merge guards. Every export is synchronous, deterministic and free of I/O, so a full index rebuild or a sleep re-run reproduces the same numbers, and the package’s `dist` imports nothing but `effect`; `tests/layering.test.ts` is the standing proof. The similarity space starts at `cosine`, which is total and clamped to `[-1, 1]`: a zero-magnitude vector, or one carrying a `NaN` or infinite component, reads `0` rather than `NaN`, because MMR folds a `max` over similarities and `Math.max(penalty, NaN)` disagrees with `NaN > penalty`. The clamp exists because subnormal magnitudes push the unclamped ratio to `1.000000106821595`, mismatched lengths walk the shorter vector, and the argument type is `ArrayLike` so a `Float32Array` decoded off a stored blob is accepted as is, since this function is the body of the `vector_distance_cos` SQL function and runs once per candidate row. `cosineDistance` is `1 - similarity` in `[0, 2]`, the space the vector arm’s SQL works in, and `compensatedSum` is Neumaier summation, needed because naive addition of the retention weight profiles returns `0.9999999999999999` for two of the six. `neighbors.ts` is the n-by-n arm of the same space: the SQL boundary is priced per call and re-copies the corpus n times over, so consumers decode each vector once into a `KeyedVector` and `topNeighborPairs` computes every unordered pair’s similarity once, offers it to both endpoints’ neighborhoods (so `(a, b)` and `(b, a)` can both appear), keeps the per-source top `perSourceK` above `floor`, and orders the result `sim` descending, `src` ascending, `dst` ascending before the `limit`, the three knobs of `NeighborOptions`. Pair similarities are bit-identical to `cosine` because the square roots are hoisted but the operations run in the same order. `rankCandidatePairs` applies the same floor, per-source top-k and ordering to an enumerated `CandidatePair` set, the shape `sharedEntityPairs` in `packages/sleep/src/sql.ts` feeds it with for edge typing’s shared-entity candidates, and `float32View` views a row’s bytes as a `Float32Array` in place, copying only when the offset is misaligned and returning `undefined` for an empty or ragged blob rather than throwing. Each returned `NeighborPair` carries `src`, `dst` and `sim`. Fusion is split between the production constants and a reference fold. `ranking.ts` holds `RRF_K` (60, the published default and the literal the retrieval SQL inlines), `MMR_LAMBDA` (0.5) and `REINFORCE_COOLDOWN_S` (900 seconds), plus `rrfContribution`, which returns an `Option` that is `None` for a zero-weight arm or an out-of-range rank so a disabled arm is absent from the fold rather than a neutral term. `rrf.ts` is the reference implementation, not the production path: `packages/index/src/retrieval-sql.ts` inlines the fusion arithmetic in SQL sharing only `RRF_K`, and this fold is what that SQL is checked against. An `ArmHit` carries a 1-based rank within its own arm’s list, `rrfScore` is `weight / (rank + k)`, `fuseArms` sums contributions into a `path -> score` map (order-insensitive across arms, and a duplicate path inside one arm accumulates, matching the SQL’s `SUM` over a `UNION ALL`), `rankFused` orders best first with ties broken on path ascending, and `fuseLateral` folds a graph walk’s best-first order in as one more arm, returning the scores unchanged for an empty order or non-positive weight so the lateral arm is inert when dark; it is defined and tested here and has no production caller yet. `applyMmr` then reorders `MmrCandidate` rows greedily by `lambda * relevance - (1 - lambda) * maxSimilarityToSelected` with no embedding round-trip, since candidates carry their own vectors; at `lambda >= 1` it returns the input order truncated, a vectorless candidate takes penalty 0 because it cannot be shown to duplicate anything, and each round folds only the newest selection into a cached per-candidate max, so the cost is O(k times n) dot products rather than O(k squared times n). `graph.ts` replaces two networkx calls with deterministic equivalents, because these scores feed the retention `pagerank` and `bridgeImportance` signals and a run-to-run reordering would change which memories are evicted on an unchanged corpus: nodes are sorted before iteration, parallel edges fold to their maximum strength, and label propagation uses sorted order with lexicographic tie-breaking instead of a random seed. `pagerank` is weighted power iteration over `GraphEdge` rows (strength in `[0, 1]`) with `PAGERANK_DAMPING` 0.85 (the networkx default), `PAGERANK_MAX_ITERATIONS` 100 and `PAGERANK_TOLERANCE` 1e-6; passing `seeds` makes it personalized, the form a query-conditioned graph walk would use (the one production caller, the sleep retention pass in `packages/sleep/src/retention.ts`, passes none), seeds naming absent nodes are dropped, and dangling mass is redistributed over the teleport distribution so the scores sum to 1. `labelPropagation` reads the edges as undirected, canonicalizes each community to its smallest member path, and collapses communities below `MIN_COMMUNITY_SIZE` (3) to `undefined` within `LABEL_PROPAGATION_MAX_SWEEPS` (100), since a pair passed off as a community would make every cross-pair edge look like a bridge; `bridgeCounts` then counts each node’s edges that cross a community boundary, with a node in no community counting 0 rather than its full degree. `frameKeyOf` is the other structural key: it reads a claim as a frame (subject plus relation up to the last linking token among `of`, `is`, `in`, `to`, `by`, `as`) and a value, so “The capital of India is New Delhi” keys on `the capital of india is` and a later claim writing a different city into the same slot is recognizable without a model or an embedding. It fails closed to `null` unless the frame is at least three tokens and the value is one to six tokens, is case- and whitespace-insensitive, and is a verbatim port from the evaluation harness’s consolidate adapter, where the rule was measured against the benchmark’s conflict-resolution rows before it was believed; the tokens, thresholds and greedy match are fixed by that measurement, and `tests/frame.test.ts` carries the reference’s own cases. `retention.ts` is the eight-signal scorer ported from the predecessor’s `domain/retention.py`. `scoreRetention` takes a `RetentionInput` of raw per-memory quantities (age in days, access count, confidence, PageRank and the run’s maximum, bridge count, inbound reinforcement count, word count, and authored contradiction count), normalizes them with `computeSignals` into the `Signals` named by `SIGNAL_NAMES` (`recency`, `accessFrequency`, `confidence`, `pagerank`, `bridgeImportance`, `reinforcementCount`, `contentDensity`, `contestedStatus`), weights them with `compositeScore` under the type’s `WeightProfile` from `weightsFor`, and bands the result with `bandFor` into a `RetentionScore`. `WEIGHT_PROFILES` has five typed profiles (`episodic`, `semantic`, `procedural`, `arc`, `error_pattern`) and every other type takes `DEFAULT_WEIGHTS`; each profile’s eight weights sum to exactly 1.0 under `profileWeightSum`, which is what keeps the composite in `[0, 1]`, and `SCORE_PRECISION` rounds it to 4 places. `HALF_LIVES_DAYS` gives the recency half-life per type (`episodic` 10, `semantic` 90, `arc` 30, `error_pattern` 14, `procedural` and `task` `null` for no decay) with `DEFAULT_HALF_LIFE_DAYS` 30 for an unlisted type, and `LN2` is `Math.LN2` so the curve is exactly 0.5 at the half-life. The `TRIAGE_ACTIONS` are `keep`, `compress` and `evict`, with `KEEP_THRESHOLD` 0.7 and `EVICT_THRESHOLD` 0.3 and each boundary owned by the lower band, so exactly 0.7 compresses and exactly 0.3 evicts. The reprieve gate sits beside the scorer: `reprieveScore` sums four terms over a `ReprieveInput` with `REPRIEVE_W_IMPORTANCE` 0.4, `REPRIEVE_W_ACCESS` 0.3, `REPRIEVE_W_OUTCOME` 0.2 and `REPRIEVE_W_RECENCY` 0.1, decaying recency at `SALIENCE_DECAY_RATE` 0.01 per hour; the coefficients sum to 1.0 but the score is not convex because the `log1p(accessCount)` term is unbounded, and a negative outcome score contributes exactly 0 so a memory is not punished twice for one bad outcome. `shouldReprieve` is the boolean gate, with no side effect of its own: it is true when the score clears `REPRIEVE_FLOOR` (0.5) and the memory has been reprieved fewer than `MAX_REPRIEVES` (3) times, where 0 is the kill switch that restores pure-age TTL, and the sleep reprieve phase (`packages/sleep/src/phases/reprieve.ts`) is what then extends the memory’s `memhtml-valid-until` by `REPRIEVE_DAYS` (14). `decay.ts` puts confidence decay and the outcome EWMA on a fixed-point grid of `SCALE` 10^4 (`POS_ONE_FP` and `NEG_ONE_FP` are +1.0 and -1.0 on it, `toFp` and `fromFp` convert) so the boundary claims hold exactly rather than within an ulp: `ewmaStepFp` is one step with floor division, `applyOutcomesFp` folds signals with one rounding per step so an N-signal batch equals N single calls, and `applyNegativeHitsFp` decays toward -1.0 over a count of negative corrections and is a no-op for `hits <= 0`. The confidence half is the production path, called by `@memhtml/sleep`‘s confidence-decay phase: `decayConfidenceFp` is an EWMA toward the floor followed by a `min` with the previous value, so decay is unconditionally non-increasing and never rehabilitates a claim an operator pushed below the floor, `decayConfidenceNFp` folds it over cycles, and `decayConfidence` and `decayConfidenceN` are the `[0, 1]` float wrappers defaulting to `DEFAULT_CONFIDENCE_DECAY_ALPHA` (0.1, a tenth of the remaining distance per sleep cycle) and `DEFAULT_CONFIDENCE_FLOOR` (0.2, because a decayed-to-zero memory would be indistinguishable from a retracted one). The outcome half is the reference implementation: `packages/index/src/reinforce.ts` computes the EWMA in float SQL with its own `OUTCOME_EWMA_ALPHA` twin of `DEFAULT_EWMA_ALPHA` (0.3), and both packages’ tests pin the pair. `CONFIDENCE_COMMIT_DELTA` (0.005) and `isCommittableConfidenceChange` drop a sub-threshold change so the widest commit of a sleep run stays reviewable. `reinforce.ts` is the cooldown predicate whose twin is the salience arm’s SQL guard: `shouldBumpAccess` is true when no stamp exists or the elapsed time is at least the cooldown (`>=`, matching the SQL, so a stamp exactly 900 seconds old is bumpable), `partitionByCooldown` splits paths into `bumped` and `cooledDown` in one order-preserving pass, and `REINFORCE_SIGNALS` (`positive`, `negative`, `neutral`) map through `signalValue` to 1, -1 and 0, where neutral bumps the access count without moving the outcome score because being read is evidence of relevance, not correctness. `merge.ts` guards the near-duplicate merge, which keeps the older file and so would fold a newer correction into an older wrong memory if it trusted cosine alone: `negationDivergent`, `numericTokenDivergent` and `variantQualifierDivergent` catch “safe” against “NOT safe”, “retry 3” against “retry 13” and “M1” against “M1 Pro”. Each memory is a `MergeText` of `gist` (the one-line claim) and `body` (the article), and the predicates read different ones: `mergeVetoReasons` runs the numeric predicate over the gist alone, because a body’s citations (a date, a ticket id, an exit code) differ between two honest tellings of one fact, and runs negation and variant over `joinMergeText` of both, because a polarity flip stated only in the body still contradicts. `mergeVetoed` is true when any reason fires. `mergeOutcomes` classifies oriented `MergePair` rows into a `MergeStage` each (`cap`, `threshold`, `vetoed`, `self`, `role-guard`, `committed`) by applying in order the `MAX_MERGE_PAIRS` cap (100 per sleep cycle), the strict `NEAR_DUPLICATE_THRESHOLD` (0.92), the veto when both texts are present, a self-merge check, and an in-batch role guard under which a dropped path is never dropped again or kept, and a keeper is never dropped, so a transitive chain cannot supersede content into a file the same batch archives; a keeper may keep again, so one page absorbs several duplicates in one batch. `mergeCandidates` is the `committed` subset as `MergeDecision` rows. The guards are a post-filter over every proposal including a model’s, so binding a model to dedup-merge cannot widen the committable set. `connectedComponents` is the order-invariant union-find whose components are dedup’s units of work (the smaller root always wins, so the same edge set yields the same partition in any arrival order), and `excludeSelfSupersede` removes the canonical from a compress batch’s member list so the file just folded into is not archived. ## Modules [Section titled “Modules”](#modules) The package publishes one import path, `@memhtml/domain`, which re-exports the eleven modules below plus a type-only re-export of `InvalidMemory` from `@memhtml/contracts/errors`, so the reference on this page is the whole exported surface. | Module | What it holds | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `cosine.ts` | `cosine` (total, clamped similarity), `cosineDistance` (`1 - similarity`) and `compensatedSum` (Neumaier summation). | | `decay.ts` | The grid (`SCALE`, `POS_ONE_FP`, `NEG_ONE_FP`, `toFp`, `fromFp`), the defaults (`DEFAULT_EWMA_ALPHA`, `DEFAULT_CONFIDENCE_FLOOR`, `DEFAULT_CONFIDENCE_DECAY_ALPHA`), the EWMA steps (`ewmaStepFp`, `applyOutcomesFp`, `applyNegativeHitsFp`), confidence decay (`decayConfidenceFp`, `decayConfidenceNFp`, `decayConfidence`, `decayConfidenceN`), and the commit gate (`CONFIDENCE_COMMIT_DELTA`, `isCommittableConfidenceChange`). | | `frame.ts` | `frameKeyOf`, the claim’s slot key or `null`. | | `graph.ts` | `GraphEdge`, the constants (`PAGERANK_DAMPING`, `PAGERANK_MAX_ITERATIONS`, `PAGERANK_TOLERANCE`, `MIN_COMMUNITY_SIZE`, `LABEL_PROPAGATION_MAX_SWEEPS`), `pagerank`, `labelPropagation` and `bridgeCounts`. | | `merge.ts` | `NEAR_DUPLICATE_THRESHOLD`, `MAX_MERGE_PAIRS`, the shapes `MergeText`, `MergePair`, `MergeDecision`, `MergeOutcome` and `MergeStage`, `joinMergeText`, the guards (`negationDivergent`, `numericTokenDivergent`, `variantQualifierDivergent`, `mergeVetoReasons`, `mergeVetoed`), `mergeOutcomes`, `mergeCandidates`, `connectedComponents` and `excludeSelfSupersede`. | | `mmr.ts` | `MmrCandidate` and `applyMmr`. | | `neighbors.ts` | The shapes `KeyedVector`, `NeighborPair`, `NeighborOptions` and `CandidatePair`, `float32View`, `topNeighborPairs` and `rankCandidatePairs`. | | `ranking.ts` | `RRF_K`, `MMR_LAMBDA`, `REINFORCE_COOLDOWN_S` and `rrfContribution`. | | `reinforce.ts` | `shouldBumpAccess`, `REINFORCE_SIGNALS`, `ReinforceSignal`, `signalValue` and `partitionByCooldown`. | | `retention.ts` | The signal and profile types (`SIGNAL_NAMES`, `SignalName`, `Signals`, `WeightProfile`, `TRIAGE_ACTIONS`, `TriageAction`), the tables (`WEIGHT_PROFILES`, `DEFAULT_WEIGHTS`, `HALF_LIVES_DAYS`, `DEFAULT_HALF_LIFE_DAYS`, `LN2`, `KEEP_THRESHOLD`, `EVICT_THRESHOLD`, `SCORE_PRECISION`), the scorer (`RetentionInput`, `RetentionScore`, `weightsFor`, `halfLifeFor`, `profileWeightSum`, `computeSignals`, `compositeScore`, `bandFor`, `scoreRetention`), and the reprieve gate (`REPRIEVE_FLOOR`, `REPRIEVE_DAYS`, `MAX_REPRIEVES`, `SALIENCE_DECAY_RATE`, `REPRIEVE_W_IMPORTANCE`, `REPRIEVE_W_ACCESS`, `REPRIEVE_W_OUTCOME`, `REPRIEVE_W_RECENCY`, `ReprieveInput`, `reprieveScore`, `shouldReprieve`). | | `rrf.ts` | `ArmHit`, `rrfScore`, `fuseArms`, `rankFused` and `fuseLateral`. | | `index.ts` | The eleven re-exports above plus the type-only re-export of `InvalidMemory` from `@memhtml/contracts/errors`, erased by `verbatimModuleSyntax` so `dist` still imports nothing but `effect`. | ## Interfaces [Section titled “Interfaces”](#interfaces) ### ArmHit [Section titled “ArmHit”](#armhit) One arm’s hit. `rank` is a **1-based position within that one arm’s own candidate list**, not a global rank and not a score. That is what lets RRF work across arms whose scores are incomparable. #### Properties [Section titled “Properties”](#properties) ##### path [Section titled “path”](#path) ```ts 1 readonly path: string; ``` ##### rank [Section titled “rank”](#rank) ```ts 1 readonly rank: number; ``` *** ### CandidatePair [Section titled “CandidatePair”](#candidatepair) One enumerated candidate pair, oriented by the caller. Pairs must be distinct. #### Properties [Section titled “Properties”](#properties-1) ##### dst [Section titled “dst”](#dst) ```ts 1 readonly dst: string; ``` ##### src [Section titled “src”](#src) ```ts 1 readonly src: string; ``` *** ### GraphEdge [Section titled “GraphEdge”](#graphedge) One directed weighted edge. `strength` is unitless in `[0, 1]`. #### Properties [Section titled “Properties”](#properties-2) ##### dst [Section titled “dst”](#dst-1) ```ts 1 readonly dst: string; ``` ##### src [Section titled “src”](#src-1) ```ts 1 readonly src: string; ``` ##### strength [Section titled “strength”](#strength) ```ts 1 readonly strength: number; ``` *** ### InvalidMemory [Section titled “InvalidMemory”](#invalidmemory) A memory that violates the file format or the type/placement vocabulary. #### Extends [Section titled “Extends”](#extends) * `InvalidMemory_base` #### Properties [Section titled “Properties”](#properties-3) ##### reason [Section titled “reason”](#reason) ```ts 1 readonly reason: string; ``` ###### Inherited from [Section titled “Inherited from”](#inherited-from) ```ts 1 InvalidMemory_base.reason ``` *** ### KeyedVector [Section titled “KeyedVector”](#keyedvector) One decoded vector under the key the ranking reports. Keys must be unique. #### Properties [Section titled “Properties”](#properties-4) ##### key [Section titled “key”](#key) ```ts 1 readonly key: string; ``` ##### vec [Section titled “vec”](#vec) ```ts 1 readonly vec: Float32Array; ``` *** ### MergeDecision [Section titled “MergeDecision”](#mergedecision) A committed merge: fold `dropPath` into `keepPath`. #### Properties [Section titled “Properties”](#properties-5) ##### dropPath [Section titled “dropPath”](#droppath) ```ts 1 readonly dropPath: string; ``` ##### keepPath [Section titled “keepPath”](#keeppath) ```ts 1 readonly keepPath: string; ``` ##### similarity [Section titled “similarity”](#similarity) ```ts 1 readonly similarity: number; ``` *** ### MergeOutcome [Section titled “MergeOutcome”](#mergeoutcome) One candidate pair’s fate. `committed` is the only stage that produces a [MergeDecision](/api/domain/#mergedecision). #### Properties [Section titled “Properties”](#properties-6) ##### pair [Section titled “pair”](#pair) ```ts 1 readonly pair: MergePair; ``` ##### stage [Section titled “stage”](#stage) ```ts 1 readonly stage: MergeStage; ``` *** ### MergePair [Section titled “MergePair”](#mergepair) One oriented candidate pair. `keepPath` is already the canonical (the older file). The orientation is the caller’s, because it depends on commit dates this module does not read. The texts are optional; when either is absent the divergence guards are skipped. #### Properties [Section titled “Properties”](#properties-7) ##### dropPath [Section titled “dropPath”](#droppath-1) ```ts 1 readonly dropPath: string; ``` ##### dropText? [Section titled “dropText?”](#droptext) ```ts 1 readonly optional dropText?: MergeText; ``` ##### keepPath [Section titled “keepPath”](#keeppath-1) ```ts 1 readonly keepPath: string; ``` ##### keepText? [Section titled “keepText?”](#keeptext) ```ts 1 readonly optional keepText?: MergeText; ``` ##### similarity [Section titled “similarity”](#similarity-1) ```ts 1 readonly similarity: number; ``` *** ### MergeText [Section titled “MergeText”](#mergetext) The two texts the veto reads for one memory: its one-line claim (the `` gist) and the whole article’s text. The predicates do not all read the same one, and that is the point of the split. `numericTokenDivergent` reads the GIST alone. A body is where a memory cites its evidence — a date, a pull-request or ticket id, an exit code, a cosine — and two honest tellings of one fact almost never cite the same numbers, so a numeric set-equality over the whole article vetoed nearly every true duplicate the store proposed (349 of 349 candidates over two consecutive runs, every review task naming this predicate). A number in the CLAIM is the fact itself: “retry 3” against “retry 13” is two claims, and the gist is where that difference lives. `negationDivergent` and `variantQualifierDivergent` read the JOINED text, gist and body together. Their markers are rare, load-bearing words (“not”, “never”, “pro”, “deprecated”) that a body does not carry incidentally the way it carries a citation, and a polarity flip stated only in the body — the claim “run the cutover during business hours” over a body that says draining does NOT complete — is a contradiction the veto has to see. The two all-veto runs named neither predicate once. #### Properties [Section titled “Properties”](#properties-8) ##### body [Section titled “body”](#body) ```ts 1 readonly body: string; ``` ##### gist [Section titled “gist”](#gist) ```ts 1 readonly gist: string; ``` *** ### MmrCandidate [Section titled “MmrCandidate”](#mmrcandidate) A candidate for diversification. `score` is its fused relevance (unitless, higher is better); `vector` is its embedding, or `undefined` when the chunk has no vector yet, which happens for a lexical-floor hit or a deferred embed. #### Properties [Section titled “Properties”](#properties-9) ##### path [Section titled “path”](#path-1) ```ts 1 readonly path: string; ``` ##### score [Section titled “score”](#score) ```ts 1 readonly score: number; ``` ##### vector? [Section titled “vector?”](#vector) ```ts 1 readonly optional vector?: readonly number[]; ``` *** ### NeighborOptions [Section titled “NeighborOptions”](#neighboroptions) The selection knobs every pair consumer states. #### Properties [Section titled “Properties”](#properties-10) ##### floor [Section titled “floor”](#floor) ```ts 1 readonly floor: number; ``` Pairs below this similarity never enter ranking. ##### limit [Section titled “limit”](#limit) ```ts 1 readonly limit: number; ``` Global cap, applied after the final `sim` DESC, `src` ASC, `dst` ASC ordering. ##### perSourceK [Section titled “perSourceK”](#persourcek) ```ts 1 readonly perSourceK: number; ``` Nearest neighbors kept per `src`, ordered `sim` DESC then `dst` ASC. *** ### NeighborPair [Section titled “NeighborPair”](#neighborpair) One selected pair. `sim` is cosine similarity, unitless in `[-1, 1]`. #### Properties [Section titled “Properties”](#properties-11) ##### dst [Section titled “dst”](#dst-2) ```ts 1 readonly dst: string; ``` ##### sim [Section titled “sim”](#sim) ```ts 1 readonly sim: number; ``` ##### src [Section titled “src”](#src-2) ```ts 1 readonly src: string; ``` *** ### ReprieveInput [Section titled “ReprieveInput”](#reprieveinput) What the reprieve score reads. Units and scopes: * `importance`: 1-based ordinal in `[1, 10]`, clamped; divided by 10 before use. * `accessCount`: a quantity, per-memory lifetime, 0-based. * `outcomeScore`: unitless in `[-1, 1]`. A negative value contributes 0. * `hoursSinceAccess`: fractional hours, non-negative. #### Properties [Section titled “Properties”](#properties-12) ##### accessCount [Section titled “accessCount”](#accesscount) ```ts 1 readonly accessCount: number; ``` ##### decayRate? [Section titled “decayRate?”](#decayrate) ```ts 1 readonly optional decayRate?: number; ``` ##### hoursSinceAccess [Section titled “hoursSinceAccess”](#hourssinceaccess) ```ts 1 readonly hoursSinceAccess: number; ``` ##### importance [Section titled “importance”](#importance) ```ts 1 readonly importance: number; ``` ##### outcomeScore [Section titled “outcomeScore”](#outcomescore) ```ts 1 readonly outcomeScore: number; ``` *** ### RetentionInput [Section titled “RetentionInput”](#retentioninput) The raw per-memory inputs the SQL phase gathers. Kept raw rather than pre-normalized so the normalization curves live in exactly one place, [computeSignals](/api/domain/#computesignals). Units and scopes, per field: * `memoryType` selects the weight profile and the half-life. * `ageDays`: fractional days since the memory’s own last update. Non-negative. * `accessCount`: a quantity, per-memory lifetime, 0-based. * `confidence`: unitless in `[0, 1]`; clamped rather than rejected. * `graphRank` / `maxGraphRank`: PageRank scores in the same run’s scale; `maxGraphRank` is that run’s maximum, so the ratio is corpus-relative, not absolute. * `bridgeCount`: a quantity of cross-community memory edges for this memory. * `reinforcementCount`: a quantity of inbound `supports`/`reinforces` edges. * `wordCount`: a quantity of words in the memory’s body. * `contradictionCount`: a quantity of **authored** (`derived: false`) contradictions; a sleep-mined suspicion is excluded upstream, so a machine guess cannot evict. #### Properties [Section titled “Properties”](#properties-13) ##### accessCount [Section titled “accessCount”](#accesscount-1) ```ts 1 readonly accessCount: number; ``` ##### ageDays [Section titled “ageDays”](#agedays) ```ts 1 readonly ageDays: number; ``` ##### bridgeCount [Section titled “bridgeCount”](#bridgecount) ```ts 1 readonly bridgeCount: number; ``` ##### confidence [Section titled “confidence”](#confidence) ```ts 1 readonly confidence: number; ``` ##### contradictionCount [Section titled “contradictionCount”](#contradictioncount) ```ts 1 readonly contradictionCount: number; ``` ##### graphRank [Section titled “graphRank”](#graphrank) ```ts 1 readonly graphRank: number; ``` ##### maxGraphRank [Section titled “maxGraphRank”](#maxgraphrank) ```ts 1 readonly maxGraphRank: number; ``` ##### memoryType [Section titled “memoryType”](#memorytype) ```ts 1 readonly memoryType: string; ``` ##### reinforcementCount [Section titled “reinforcementCount”](#reinforcementcount) ```ts 1 readonly reinforcementCount: number; ``` ##### wordCount [Section titled “wordCount”](#wordcount) ```ts 1 readonly wordCount: number; ``` *** ### RetentionScore [Section titled “RetentionScore”](#retentionscore) The scoring result: the composite, its band, and the normalized signals behind it. #### Properties [Section titled “Properties”](#properties-14) ##### action [Section titled “action”](#action) ```ts 1 readonly action: "keep" | "compress" | "evict"; ``` ##### score [Section titled “score”](#score-1) ```ts 1 readonly score: number; ``` ##### signals [Section titled “signals”](#signals) ```ts 1 readonly signals: Signals; ``` ## Type Aliases [Section titled “Type Aliases”](#type-aliases) ### MergeStage [Section titled “MergeStage”](#mergestage) ```ts 1 type MergeStage = "cap" | "threshold" | "vetoed" | "self" | "role-guard" | "committed"; ``` Where a candidate pair stopped in [mergeOutcomes](/api/domain/#mergeoutcomes), in the filter’s own order. *** ### MergeVetoReason [Section titled “MergeVetoReason”](#mergevetoreason) ```ts 1 type MergeVetoReason = "negation" | "numeric" | "variant"; ``` The three divergence families, in the veto’s own disjunction order. *** ### ReinforceSignal [Section titled “ReinforceSignal”](#reinforcesignal) ```ts 1 type ReinforceSignal = typeof REINFORCE_SIGNALS[number]; ``` *** ### SignalName [Section titled “SignalName”](#signalname) ```ts 1 type SignalName = typeof SIGNAL_NAMES[number]; ``` *** ### Signals [Section titled “Signals”](#signals-1) ```ts 1 type Signals = { readonly [K in SignalName]: number }; ``` A full set of normalized signal values, each unitless in `[0, 1]`. *** ### TriageAction [Section titled “TriageAction”](#triageaction) ```ts 1 type TriageAction = typeof TRIAGE_ACTIONS[number]; ``` *** ### WeightProfile [Section titled “WeightProfile”](#weightprofile) ```ts 1 type WeightProfile = { readonly [K in SignalName]: number }; ``` A weight profile: one coefficient per signal, the eight summing to exactly 1.0. ## Variables [Section titled “Variables”](#variables) ### CONFIDENCE\_COMMIT\_DELTA [Section titled “CONFIDENCE\_COMMIT\_DELTA”](#confidence_commit_delta) ```ts 1 const CONFIDENCE_COMMIT_DELTA: 0.005 = 0.005; ``` The smallest confidence change worth committing. Confidence decay is the widest commit in a sleep run, one meta line across many files, so a sub-threshold delta is dropped rather than committed, keeping the night’s diff reviewable. *** ### DEFAULT\_CONFIDENCE\_DECAY\_ALPHA [Section titled “DEFAULT\_CONFIDENCE\_DECAY\_ALPHA”](#default_confidence_decay_alpha) ```ts 1 const DEFAULT_CONFIDENCE_DECAY_ALPHA: 0.1 = 0.1; ``` The per-sleep-cycle confidence decay weight. Gentler than [DEFAULT\_EWMA\_ALPHA](/api/domain/#default_ewma_alpha) on purpose: confidence should erode over many unreinforced nights rather than collapse in one, so a single missed reinforcement is forgiving. At 0.1 a claim closes a tenth of its distance to the floor per cycle. *** ### DEFAULT\_CONFIDENCE\_FLOOR [Section titled “DEFAULT\_CONFIDENCE\_FLOOR”](#default_confidence_floor) ```ts 1 const DEFAULT_CONFIDENCE_FLOOR: 0.2 = 0.2; ``` The floor confidence erodes toward but never past. A claim that stops being reinforced loses weight without vanishing: an old uncorroborated claim is weak evidence, not absent evidence, and a memory decayed to 0 would be indistinguishable from a retracted one. *** ### DEFAULT\_EWMA\_ALPHA [Section titled “DEFAULT\_EWMA\_ALPHA”](#default_ewma_alpha) ```ts 1 const DEFAULT_EWMA_ALPHA: 0.3 = 0.3; ``` The outcome EWMA’s weight on an incoming signal. Its production twin is `OUTCOME_EWMA_ALPHA` in `packages/index/src/reinforce.ts`, the value the reinforce SQL binds; both packages’ tests pin their constant to 0.3 so a fork fails loudly. *** ### DEFAULT\_HALF\_LIFE\_DAYS [Section titled “DEFAULT\_HALF\_LIFE\_DAYS”](#default_half_life_days) ```ts 1 const DEFAULT_HALF_LIFE_DAYS: 30 = 30; ``` Half-life in days for a type with no listed one. *** ### DEFAULT\_WEIGHTS [Section titled “DEFAULT\_WEIGHTS”](#default_weights) ```ts 1 const DEFAULT_WEIGHTS: WeightProfile; ``` The profile for a type with no dedicated one. Also sums to exactly 1.0. *** ### EVICT\_THRESHOLD [Section titled “EVICT\_THRESHOLD”](#evict_threshold) ```ts 1 const EVICT_THRESHOLD: 0.3 = 0.3; ``` *** ### HALF\_LIVES\_DAYS [Section titled “HALF\_LIVES\_DAYS”](#half_lives_days) ```ts 1 const HALF_LIVES_DAYS: Readonly>; ``` Recency half-lives in days. `null` means no time decay. An unlisted type takes [DEFAULT\_HALF\_LIFE\_DAYS](/api/domain/#default_half_life_days). *** ### KEEP\_THRESHOLD [Section titled “KEEP\_THRESHOLD”](#keep_threshold) ```ts 1 const KEEP_THRESHOLD: 0.7 = 0.7; ``` Band edges. KEEP is `> 0.7`, EVICT is `<= 0.3`, and COMPRESS is the open interval between. **Each boundary is owned by the lower band**: exactly 0.7 compresses, exactly 0.3 evicts. The three bands partition `[0, 1]` with no gap and no overlap. *** ### LABEL\_PROPAGATION\_MAX\_SWEEPS [Section titled “LABEL\_PROPAGATION\_MAX\_SWEEPS”](#label_propagation_max_sweeps) ```ts 1 const LABEL_PROPAGATION_MAX_SWEEPS: 100 = 100; ``` Label-propagation sweep cap. Sorted-order propagation settles well inside it. *** ### LN2 [Section titled “LN2”](#ln2) ```ts 1 const LN2: number = Math.LN2; ``` The recency decay constant. `Math.LN2` rather than a `0.693` literal, so `exp(-LN2 * age / halfLife)` is **exactly** 0.5 at `age == halfLife`. The half-life is then the definition of the curve rather than an approximation of it, and a property test can assert the equality instead of a tolerance. *** ### MAX\_MERGE\_PAIRS [Section titled “MAX\_MERGE\_PAIRS”](#max_merge_pairs) ```ts 1 const MAX_MERGE_PAIRS: 100 = 100; ``` Merge decisions applied per sleep cycle, so a flood cannot fan the phase out unboundedly. *** ### MAX\_REPRIEVES [Section titled “MAX\_REPRIEVES”](#max_reprieves) ```ts 1 const MAX_REPRIEVES: 3 = 3; ``` Reprieves a memory may earn before it is forced to expire. `0` is the kill switch that restores pure-age TTL: the floor alone can never force expiry, because the reprieve score is a sum of non-negative terms and is therefore always above 0. *** ### MIN\_COMMUNITY\_SIZE [Section titled “MIN\_COMMUNITY\_SIZE”](#min_community_size) ```ts 1 const MIN_COMMUNITY_SIZE: 3 = 3; ``` Communities smaller than this are not communities; compress batching ignores them. *** ### MMR\_LAMBDA [Section titled “MMR\_LAMBDA”](#mmr_lambda) ```ts 1 const MMR_LAMBDA: 0.5 = 0.5; ``` Maximal-marginal-relevance’s relevance/diversity split. *** ### NEAR\_DUPLICATE\_THRESHOLD [Section titled “NEAR\_DUPLICATE\_THRESHOLD”](#near_duplicate_threshold) ```ts 1 const NEAR_DUPLICATE_THRESHOLD: 0.92 = 0.92; ``` Cosine similarity above which two bodies are the same content. Strict. *** ### NEG\_ONE\_FP [Section titled “NEG\_ONE\_FP”](#neg_one_fp) ```ts 1 const NEG_ONE_FP: number = -SCALE; ``` *** ### PAGERANK\_DAMPING [Section titled “PAGERANK\_DAMPING”](#pagerank_damping) ```ts 1 const PAGERANK_DAMPING: 0.85 = 0.85; ``` PageRank teleport factor. The networkx default, kept from the predecessor memory system. *** ### PAGERANK\_MAX\_ITERATIONS [Section titled “PAGERANK\_MAX\_ITERATIONS”](#pagerank_max_iterations) ```ts 1 const PAGERANK_MAX_ITERATIONS: 100 = 100; ``` Power-iteration cap. Convergence at this graph’s scale is well inside it. *** ### PAGERANK\_TOLERANCE [Section titled “PAGERANK\_TOLERANCE”](#pagerank_tolerance) ```ts 1 const PAGERANK_TOLERANCE: 0.000001 = 1e-6; ``` L1 convergence tolerance per node. *** ### POS\_ONE\_FP [Section titled “POS\_ONE\_FP”](#pos_one_fp) ```ts 1 const POS_ONE_FP: 10000 = SCALE; ``` `+1.0` and `-1.0` on the grid. The outcome EWMA’s domain is `[NEG_ONE_FP, POS_ONE_FP]`. *** ### REINFORCE\_COOLDOWN\_S [Section titled “REINFORCE\_COOLDOWN\_S”](#reinforce_cooldown_s) ```ts 1 const REINFORCE_COOLDOWN_S: 900 = 900; ``` Seconds an access bump waits before it counts again. Stated once here and once in the salience arm’s SQL; a property test pins the two to agree at the boundary. *** ### REINFORCE\_SIGNALS [Section titled “REINFORCE\_SIGNALS”](#reinforce_signals) ```ts 1 const REINFORCE_SIGNALS: readonly ["positive", "negative", "neutral"]; ``` The signal a reinforcement carries. `negative` is what drives the outcome EWMA down. *** ### REPRIEVE\_DAYS [Section titled “REPRIEVE\_DAYS”](#reprieve_days) ```ts 1 const REPRIEVE_DAYS: 14 = 14; ``` Days a reprieve extends `memhtml-valid-until` by. *** ### REPRIEVE\_FLOOR [Section titled “REPRIEVE\_FLOOR”](#reprieve_floor) ```ts 1 const REPRIEVE_FLOOR: 0.5 = 0.5; ``` The reprieve gate’s floor. A TTL-passed memory scoring at least this, under the reprieve cap, has its `memhtml-valid-until` extended instead of being archived. *** ### REPRIEVE\_W\_ACCESS [Section titled “REPRIEVE\_W\_ACCESS”](#reprieve_w_access) ```ts 1 const REPRIEVE_W_ACCESS: 0.3 = 0.3; ``` *** ### REPRIEVE\_W\_IMPORTANCE [Section titled “REPRIEVE\_W\_IMPORTANCE”](#reprieve_w_importance) ```ts 1 const REPRIEVE_W_IMPORTANCE: 0.4 = 0.4; ``` The four reprieve coefficients. They sum to 1.0 but the score is NOT convex. *** ### REPRIEVE\_W\_OUTCOME [Section titled “REPRIEVE\_W\_OUTCOME”](#reprieve_w_outcome) ```ts 1 const REPRIEVE_W_OUTCOME: 0.2 = 0.2; ``` *** ### REPRIEVE\_W\_RECENCY [Section titled “REPRIEVE\_W\_RECENCY”](#reprieve_w_recency) ```ts 1 const REPRIEVE_W_RECENCY: 0.1 = 0.1; ``` *** ### RRF\_K [Section titled “RRF\_K”](#rrf_k) ```ts 1 const RRF_K: 60 = 60; ``` Reciprocal-rank-fusion’s rank offset. 60 is the published default and the value the retrieval SQL inlines as a literal, so it lives here once and the assembler reads it rather than restating it. *** ### SALIENCE\_DECAY\_RATE [Section titled “SALIENCE\_DECAY\_RATE”](#salience_decay_rate) ```ts 1 const SALIENCE_DECAY_RATE: 0.01 = 0.01; ``` Salience decay rate per hour for the reprieve score’s recency term. *** ### SCALE [Section titled “SCALE”](#scale) ```ts 1 const SCALE: 10000 = 10_000; ``` The fixed-point scale: 10^4, so the grid step is the 4th decimal place. *** ### SCORE\_PRECISION [Section titled “SCORE\_PRECISION”](#score_precision) ```ts 1 const SCORE_PRECISION: 4 = 4; ``` Decimal places the composite is rounded to, matching the predecessor’s grain. *** ### SIGNAL\_NAMES [Section titled “SIGNAL\_NAMES”](#signal_names) ```ts 1 const SIGNAL_NAMES: readonly ["recency", "accessFrequency", "confidence", "pagerank", "bridgeImportance", "reinforcementCount", "contentDensity", "contestedStatus"]; ``` The eight signals, in the fixed order every weight profile keys on. *** ### TRIAGE\_ACTIONS [Section titled “TRIAGE\_ACTIONS”](#triage_actions) ```ts 1 const TRIAGE_ACTIONS: readonly ["keep", "compress", "evict"]; ``` The triage verdict for one memory. *** ### WEIGHT\_PROFILES [Section titled “WEIGHT\_PROFILES”](#weight_profiles) ```ts 1 const WEIGHT_PROFILES: Readonly>; ``` Per-type weight profiles. Every profile’s eight weights sum to exactly 1.0 under compensated summation, which is what makes the composite a convex combination of eight `[0, 1]` signals and therefore itself in `[0, 1]`. The five profiles below are the typed ones; every other memory type falls back to [DEFAULT\_WEIGHTS](/api/domain/#default_weights). Recency carries the most weight for `episodic` (time is that type’s identity) and zero for `procedural` (a working procedure does not stale). ## Functions [Section titled “Functions”](#functions) ### applyMmr() [Section titled “applyMmr()”](#applymmr) ```ts 1 function applyMmr( 2 candidates, 3 limit, 4 lambda? 5 ): readonly MmrCandidate[]; ``` Greedily reorder candidates by `lambda * relevance - (1 - lambda) * maxSimilarityToSelected`. `lambda` is unitless in `[0, 1]`: 1 is pure relevance, 0 is pure diversity. At `lambda >= 1` the function short-circuits and returns the input order truncated, because the penalty term is multiplied by zero and the greedy pass would otherwise burn O(n^2) cosines to reproduce the order it was given. A candidate with no vector takes penalty 0, which is how “unknown similarity” reads here. A vectorless candidate cannot be shown to duplicate anything, so it is not penalized for it. Vectorless candidates therefore keep their relative fusion order among themselves rather than being shuffled by a fabricated distance. The output is always a duplicate-free subsequence of the input by membership: each candidate is selected at most once and nothing is invented. #### Parameters [Section titled “Parameters”](#parameters) ##### candidates [Section titled “candidates”](#candidates) readonly [`MmrCandidate`](/api/domain/#mmrcandidate)\[] ##### limit [Section titled “limit”](#limit-1) `number` ##### lambda? [Section titled “lambda?”](#lambda) `number` = `MMR_LAMBDA` #### Returns [Section titled “Returns”](#returns) readonly [`MmrCandidate`](/api/domain/#mmrcandidate)\[] *** ### applyNegativeHitsFp() [Section titled “applyNegativeHitsFp()”](#applynegativehitsfp) ```ts 1 function applyNegativeHitsFp( 2 alphaFp, 3 prevFp, 4 hits 5 ): number; ``` Decay a score toward -1.0 over `hits` negative corrections. `hits <= 0` is a no-op, so re-running a phase over an already-drained watermark writes the same value. #### Parameters [Section titled “Parameters”](#parameters-1) ##### alphaFp [Section titled “alphaFp”](#alphafp) `number` ##### prevFp [Section titled “prevFp”](#prevfp) `number` ##### hits [Section titled “hits”](#hits) `number` #### Returns [Section titled “Returns”](#returns-1) `number` *** ### applyOutcomesFp() [Section titled “applyOutcomesFp()”](#applyoutcomesfp) ```ts 1 function applyOutcomesFp( 2 alphaFp, 3 prevFp, 4 signalsFp 5 ): number; ``` Fold signals through [ewmaStepFp](/api/domain/#ewmastepfp), one rounding per step. Rounding inside the fold rather than once at the end is what makes an N-signal batch agree with N single-signal calls. Sleep may process a memory’s corrections in one batch or across several nights, and both paths must reach the same score. #### Parameters [Section titled “Parameters”](#parameters-2) ##### alphaFp [Section titled “alphaFp”](#alphafp-1) `number` ##### prevFp [Section titled “prevFp”](#prevfp-1) `number` ##### signalsFp [Section titled “signalsFp”](#signalsfp) readonly `number`\[] #### Returns [Section titled “Returns”](#returns-2) `number` *** ### bandFor() [Section titled “bandFor()”](#bandfor) ```ts 1 function bandFor(score): "keep" | "compress" | "evict"; ``` The band a composite falls in. Boundaries belong to the lower band: 0.7 compresses and 0.3 evicts, so the bands partition `[0, 1]` and no score is ever unbanded. #### Parameters [Section titled “Parameters”](#parameters-3) ##### score [Section titled “score”](#score-2) `number` #### Returns [Section titled “Returns”](#returns-3) `"keep"` | `"compress"` | `"evict"` *** ### bridgeCounts() [Section titled “bridgeCounts()”](#bridgecounts) ```ts 1 function bridgeCounts( 2 nodes, 3 edges, 4 communities 5 ): ReadonlyMap; ``` Per-node count of incident memory edges that cross a community boundary. This is the `bridgeImportance` signal’s raw input. A node in no community (below the size floor) has no boundary to cross, so its bridge count is 0 rather than its full degree; counting those would make every isolated pair look maximally structural. #### Parameters [Section titled “Parameters”](#parameters-4) ##### nodes [Section titled “nodes”](#nodes) readonly `string`\[] ##### edges [Section titled “edges”](#edges) readonly [`GraphEdge`](/api/domain/#graphedge)\[] ##### communities [Section titled “communities”](#communities) `ReadonlyMap`<`string`, `string` | `undefined`> #### Returns [Section titled “Returns”](#returns-4) `ReadonlyMap`<`string`, `number`> *** ### compensatedSum() [Section titled “compensatedSum()”](#compensatedsum) ```ts 1 function compensatedSum(values): number; ``` Neumaier compensated summation. Naive left-to-right addition of the retention weight profiles gives `0.9999999999999999` for two of the six (verified in node), which would make a convexity assertion fail against a profile that is correct by construction. This is the `math.fsum` the ported scorer relies on. #### Parameters [Section titled “Parameters”](#parameters-5) ##### values [Section titled “values”](#values) `Iterable`<`number`> #### Returns [Section titled “Returns”](#returns-5) `number` *** ### compositeScore() [Section titled “compositeScore()”](#compositescore) ```ts 1 function compositeScore(signals, profile): number; ``` The weighted composite, unitless in `[0, 1]`, rounded to [SCORE\_PRECISION](/api/domain/#score_precision) places. Compensated summation, so the fold does not accumulate the drift that makes a by-construction-convex profile score marginally above 1. #### Parameters [Section titled “Parameters”](#parameters-6) ##### signals [Section titled “signals”](#signals-2) [`Signals`](/api/domain/#signals-1) ##### profile [Section titled “profile”](#profile) [`WeightProfile`](/api/domain/#weightprofile) #### Returns [Section titled “Returns”](#returns-6) `number` *** ### computeSignals() [Section titled “computeSignals()”](#computesignals) ```ts 1 function computeSignals(input): Signals; ``` Normalize the raw inputs to their `[0, 1]` signal values. #### Parameters [Section titled “Parameters”](#parameters-7) ##### input [Section titled “input”](#input) [`RetentionInput`](/api/domain/#retentioninput) #### Returns [Section titled “Returns”](#returns-7) [`Signals`](/api/domain/#signals-1) *** ### connectedComponents() [Section titled “connectedComponents()”](#connectedcomponents) ```ts 1 function connectedComponents(edges): readonly readonly string[][]; ``` Connected components over an undirected edge list, as sorted member lists. The near-duplicate graph’s components are dedup’s units of work. A component is what “these memories might all be one memory” looks like before anything has judged them, and it is the right unit because near-duplication is transitive in practice: three rewordings of one fact produce three edges, and folding them one pair at a time would ask the same question three times and could answer it three different ways. **The partition is order-INVARIANT, not merely order-stable.** A union always keeps the lexicographically smaller root, so every set’s root is the smallest key it holds no matter which order the edges arrive in. Members come back sorted, and components come back ordered by root, which is each component’s own smallest member. So the same edge SET produces the same output whether it arrives mined-first, frame-first, mirrored, or shuffled. That is stronger than sorting the input would be, and it is why no sort happens here: a caller cannot make this disagree with itself by changing how it enumerates. A “larger root wins” or “first root seen wins” rule would break exactly that, because both make the surviving root a fact about arrival order rather than about the set. Each pair is normalized before it is unioned, so `(a, b)` and `(b, a)` are one edge. A self-edge introduces its key and joins nothing. Cost is near-linear in the edge count, and no step here ever enumerates a pair the caller did not hand over. #### Parameters [Section titled “Parameters”](#parameters-8) ##### edges [Section titled “edges”](#edges-1) readonly readonly \[`string`, `string`]\[] #### Returns [Section titled “Returns”](#returns-8) readonly readonly `string`\[]\[] *** ### cosine() [Section titled “cosine()”](#cosine) ```ts 1 function cosine(a, b): number; ``` Cosine similarity of two vectors, unitless and clamped to `[-1, 1]`. Total: every input maps to a number in range, and `NaN` is never returned. A zero-magnitude input yields `0`, and so does a vector carrying a `NaN` or infinite component — the two conditions the arithmetic below cannot answer. `0` is the reading a vectorless candidate already gets in MMR: no usable direction, so no demonstrable duplication, so no penalty. Returning `NaN` would be worse than returning a wrong number. MMR folds a `max` over the similarities to the already-selected set, and the two equivalent ways of writing that fold disagree on `NaN` — `Math.max(penalty, NaN)` is `NaN` while `NaN > penalty` is `false` — so a single degenerate embedding would make the result depend on which fold runs, not on the corpus. The result is clamped because the unclamped ratio does not stay in range. Verified in node 2026-08-02: two vectors whose squared magnitudes fall into the subnormal range return `1.000000106821595`. The squares underflow, so `sqrt` divides by a magnitude smaller than the true one. Similarity is also the input to `1 - similarity` distance and to the MMR penalty, both of which state a range, so the clamp belongs here rather than at each reader. Length mismatch is handled by walking the shorter vector rather than failing. The only way two stored vectors differ in length is a half-migrated embedding model, a condition the index refuses at the `embed_model` watermark, so it does not reach here. `ArrayLike` rather than `ReadonlyArray` so a `Float32Array` decoded straight off a stored blob is an argument: this is the `vector_distance_cos` SQL function’s own body, called once per candidate row, and materialising two 1024-element arrays per call to satisfy a narrower type would allocate more than the arithmetic costs. Every array caller still fits. #### Parameters [Section titled “Parameters”](#parameters-9) ##### a [Section titled “a”](#a) `ArrayLike`<`number`> ##### b [Section titled “b”](#b) `ArrayLike`<`number`> #### Returns [Section titled “Returns”](#returns-9) `number` *** ### cosineDistance() [Section titled “cosineDistance()”](#cosinedistance) ```ts 1 function cosineDistance(a, b): number; ``` Cosine *distance*, `1 - similarity`, unitless in `[0, 2]`. This is the space the vector arm’s SQL works in (`vector_distance_cos`), so a threshold stated as a similarity is converted once here rather than inverted at each call site. #### Parameters [Section titled “Parameters”](#parameters-10) ##### a [Section titled “a”](#a-1) `ArrayLike`<`number`> ##### b [Section titled “b”](#b-1) `ArrayLike`<`number`> #### Returns [Section titled “Returns”](#returns-10) `number` *** ### decayConfidence() [Section titled “decayConfidence()”](#decayconfidence) ```ts 1 function decayConfidence( 2 confidence, 3 alpha?, 4 floor? 5 ): number; ``` One confidence-decay step in the `[0, 1]` float space the HTML `memhtml-confidence` meta uses. `alpha` is the fraction of the remaining distance to `floor` closed per cycle, both unitless in `[0, 1]`. #### Parameters [Section titled “Parameters”](#parameters-11) ##### confidence [Section titled “confidence”](#confidence-1) `number` ##### alpha? [Section titled “alpha?”](#alpha) `number` = `DEFAULT_CONFIDENCE_DECAY_ALPHA` ##### floor? [Section titled “floor?”](#floor-1) `number` = `DEFAULT_CONFIDENCE_FLOOR` #### Returns [Section titled “Returns”](#returns-11) `number` *** ### decayConfidenceFp() [Section titled “decayConfidenceFp()”](#decayconfidencefp) ```ts 1 function decayConfidenceFp( 2 alphaFp, 3 confFp, 4 floorFp 5 ): number; ``` One confidence-decay step on the grid: an EWMA toward the floor, then a `min` with the previous value. The `min` is what makes the step *unconditionally* non-increasing. Without it, a claim already below the floor (one an operator correction pushed down, say) would be pulled back *up* toward the floor by the same convex combination that erodes a healthy claim, so decay would rehabilitate a discredited memory. The floor is a resting place for a claim that stops being reinforced. A refuted claim is not pulled back up to it. #### Parameters [Section titled “Parameters”](#parameters-12) ##### alphaFp [Section titled “alphaFp”](#alphafp-2) `number` ##### confFp [Section titled “confFp”](#conffp) `number` ##### floorFp [Section titled “floorFp”](#floorfp) `number` #### Returns [Section titled “Returns”](#returns-12) `number` *** ### decayConfidenceN() [Section titled “decayConfidenceN()”](#decayconfidencen) ```ts 1 function decayConfidenceN( 2 confidence, 3 cycles, 4 alpha?, 5 floor? 6 ): number; ``` [decayConfidence](/api/domain/#decayconfidence) folded over `cycles` unreinforced sleep cycles. #### Parameters [Section titled “Parameters”](#parameters-13) ##### confidence [Section titled “confidence”](#confidence-2) `number` ##### cycles [Section titled “cycles”](#cycles) `number` ##### alpha? [Section titled “alpha?”](#alpha-1) `number` = `DEFAULT_CONFIDENCE_DECAY_ALPHA` ##### floor? [Section titled “floor?”](#floor-2) `number` = `DEFAULT_CONFIDENCE_FLOOR` #### Returns [Section titled “Returns”](#returns-13) `number` *** ### decayConfidenceNFp() [Section titled “decayConfidenceNFp()”](#decayconfidencenfp) ```ts 1 function decayConfidenceNFp( 2 alphaFp, 3 confFp, 4 floorFp, 5 cycles 6 ): number; ``` Fold `cycles` decay steps. `cycles <= 0` is a no-op, so a phase re-run within one night writes the same value. #### Parameters [Section titled “Parameters”](#parameters-14) ##### alphaFp [Section titled “alphaFp”](#alphafp-3) `number` ##### confFp [Section titled “confFp”](#conffp-1) `number` ##### floorFp [Section titled “floorFp”](#floorfp-1) `number` ##### cycles [Section titled “cycles”](#cycles-1) `number` #### Returns [Section titled “Returns”](#returns-14) `number` *** ### ewmaStepFp() [Section titled “ewmaStepFp()”](#ewmastepfp) ```ts 1 function ewmaStepFp( 2 alphaFp, 3 prevFp, 4 signalFp 5 ): number; ``` One EWMA step on the grid: `alpha * signal + (1 - alpha) * prev`, divided back down with **floor** division. Floor rather than round-half-away because it is the one rounding mode whose N-fold composition is reproducible without a rounding-mode argument, and the residual drift against an unrounded fold is bounded by one grid step. For `alphaFp` in `[0, SCALE]` and both values in `[NEG_ONE_FP, POS_ONE_FP]` the result stays in that domain and lies between `prevFp` and `signalFp`, so a negative signal can never raise the score. #### Parameters [Section titled “Parameters”](#parameters-15) ##### alphaFp [Section titled “alphaFp”](#alphafp-4) `number` ##### prevFp [Section titled “prevFp”](#prevfp-2) `number` ##### signalFp [Section titled “signalFp”](#signalfp) `number` #### Returns [Section titled “Returns”](#returns-15) `number` *** ### excludeSelfSupersede() [Section titled “excludeSelfSupersede()”](#excludeselfsupersede) ```ts 1 function excludeSelfSupersede(canonicalPath, memberPaths): readonly string[]; ``` The compress-path exclusion: the members of a batch to supersede and archive, with the canonical removed and order preserved. When a batch folds into a pre-existing canonical, a member can *be* that canonical, and archiving it would destroy the file just folded into. #### Parameters [Section titled “Parameters”](#parameters-16) ##### canonicalPath [Section titled “canonicalPath”](#canonicalpath) `string` ##### memberPaths [Section titled “memberPaths”](#memberpaths) readonly `string`\[] #### Returns [Section titled “Returns”](#returns-16) readonly `string`\[] *** ### float32View() [Section titled “float32View()”](#float32view) ```ts 1 function float32View(bytes): Float32Array | undefined; ``` A `Float32Array` over stored bytes, copying only when it must. `Float32Array` requires a 4-byte-aligned `byteOffset`, and a driver row’s `Uint8Array` may be a view into a pooled buffer at any offset. Viewing in place is the common case and costs nothing. A misaligned or ragged blob is copied rather than rejected, because the vector arm’s job is to rank and a throw here would fail a whole search over one row. #### Parameters [Section titled “Parameters”](#parameters-17) ##### bytes [Section titled “bytes”](#bytes) `Uint8Array` #### Returns [Section titled “Returns”](#returns-17) `Float32Array`<`ArrayBufferLike`> | `undefined` *** ### frameKeyOf() [Section titled “frameKeyOf()”](#framekeyof) ```ts 1 function frameKeyOf(gist): string | null; ``` The frame key for one claim, or `null` when the claim states no frame+value shape this rule trusts. Pure and synchronous, with no clock, no randomness, no model, and no I/O, so a full index rebuild reproduces byte-identical keys by construction. Case- and whitespace-insensitive, because a restated fact varies in both: `"The Capital of India is X"` and `"the capital of India is Y"` collide. #### Parameters [Section titled “Parameters”](#parameters-18) ##### gist [Section titled “gist”](#gist-1) `string` The claim text. In memhtml, this is a memory’s `` claim. #### Returns [Section titled “Returns”](#returns-18) `string` | `null` The lowercased frame, or `null` for no-frame-shape (stored as SQL NULL). *** ### fromFp() [Section titled “fromFp()”](#fromfp) ```ts 1 function fromFp(valueFp): number; ``` A grid value back to a float. Exact. #### Parameters [Section titled “Parameters”](#parameters-19) ##### valueFp [Section titled “valueFp”](#valuefp) `number` #### Returns [Section titled “Returns”](#returns-19) `number` *** ### fuseArms() [Section titled “fuseArms()”](#fusearms) ```ts 1 function fuseArms( 2 armResults, 3 weights, 4 k? 5 ): ReadonlyMap; ``` Fuse per-arm ranked lists into one `path -> score` map by summing contributions. `armResults[i]` pairs with `weights[i]`; an arm with no weight scores 0 throughout. Addition commutes, so the result is order-insensitive across arms and the arm registry’s order is a presentation detail rather than a ranking input. A duplicate path inside one arm’s list accumulates, matching the SQL’s `SUM` over a `UNION ALL`. #### Parameters [Section titled “Parameters”](#parameters-20) ##### armResults [Section titled “armResults”](#armresults) readonly readonly [`ArmHit`](/api/domain/#armhit)\[]\[] ##### weights [Section titled “weights”](#weights) readonly `number`\[] ##### k? [Section titled “k?”](#k) `number` = `RRF_K` #### Returns [Section titled “Returns”](#returns-20) `ReadonlyMap`<`string`, `number`> *** ### fuseLateral() [Section titled “fuseLateral()”](#fuselateral) ```ts 1 function fuseLateral( 2 fused, 3 order, 4 weight, 5 k? 6 ): ReadonlyMap; ``` Fold a graph-walk result into existing fused scores, treating the walk’s best-first order as an arm’s ranked list. A node absent from `fused` enters at its lateral-only contribution, which is how a memory the lexical and vector arms both missed can still surface. An empty `order` or a non-positive `weight` returns the scores unchanged, so the lateral arm is inert when dark and cannot fail on a cold graph. #### Parameters [Section titled “Parameters”](#parameters-21) ##### fused [Section titled “fused”](#fused) `ReadonlyMap`<`string`, `number`> ##### order [Section titled “order”](#order) readonly `string`\[] ##### weight [Section titled “weight”](#weight) `number` ##### k? [Section titled “k?”](#k-1) `number` = `RRF_K` #### Returns [Section titled “Returns”](#returns-21) `ReadonlyMap`<`string`, `number`> *** ### halfLifeFor() [Section titled “halfLifeFor()”](#halflifefor) ```ts 1 function halfLifeFor(memoryType): number | null; ``` The recency half-life in days for a memory type; `null` means no time decay. #### Parameters [Section titled “Parameters”](#parameters-22) ##### memoryType [Section titled “memoryType”](#memorytype-1) `string` #### Returns [Section titled “Returns”](#returns-22) `number` | `null` *** ### isCommittableConfidenceChange() [Section titled “isCommittableConfidenceChange()”](#iscommittableconfidencechange) ```ts 1 function isCommittableConfidenceChange(before, after): boolean; ``` True when a decayed confidence differs enough from the stored one to be worth a commit. #### Parameters [Section titled “Parameters”](#parameters-23) ##### before [Section titled “before”](#before) `number` ##### after [Section titled “after”](#after) `number` #### Returns [Section titled “Returns”](#returns-23) `boolean` *** ### joinMergeText() [Section titled “joinMergeText()”](#joinmergetext) ```ts 1 function joinMergeText(text): string; ``` The joined text the negation and variant predicates read: the claim, a newline, the article. #### Parameters [Section titled “Parameters”](#parameters-24) ##### text [Section titled “text”](#text) [`MergeText`](/api/domain/#mergetext) #### Returns [Section titled “Returns”](#returns-24) `string` *** ### labelPropagation() [Section titled “labelPropagation()”](#labelpropagation) ```ts 1 function labelPropagation( 2 nodes, 3 edges, 4 options? 5 ): ReadonlyMap; ``` Communities by label propagation over the edge list read as undirected. Deterministic in place of a seed: nodes start labelled by themselves, sweeps visit them in sorted order, and a node adopts the highest-weighted neighboring label with the lexicographically smallest label breaking a tie. The returned labels are canonicalized to the smallest member path of each community, so the partition itself is reproducible and not only the grouping. Communities below `minCommunitySize` collapse to `undefined`: they only feed compress batching and the bridge count, and a pair passed off as a community would make every cross-pair edge look like a bridge. #### Parameters [Section titled “Parameters”](#parameters-25) ##### nodes [Section titled “nodes”](#nodes-1) readonly `string`\[] ##### edges [Section titled “edges”](#edges-2) readonly [`GraphEdge`](/api/domain/#graphedge)\[] ##### options? [Section titled “options?”](#options) ###### maxSweeps? [Section titled “maxSweeps?”](#maxsweeps) `number` ###### minCommunitySize? [Section titled “minCommunitySize?”](#mincommunitysize) `number` #### Returns [Section titled “Returns”](#returns-25) `ReadonlyMap`<`string`, `string` | `undefined`> *** ### mergeCandidates() [Section titled “mergeCandidates()”](#mergecandidates) ```ts 1 function mergeCandidates(pairs, options?): readonly MergeDecision[]; ``` Filter oriented candidate pairs into an in-batch-consistent decision list: the `committed` outcomes of [mergeOutcomes](/api/domain/#mergeoutcomes), in input order, as decisions. #### Parameters [Section titled “Parameters”](#parameters-26) ##### pairs [Section titled “pairs”](#pairs) readonly [`MergePair`](/api/domain/#mergepair)\[] ##### options? [Section titled “options?”](#options-1) ###### maxPairs? [Section titled “maxPairs?”](#maxpairs) `number` ###### threshold? [Section titled “threshold?”](#threshold) `number` #### Returns [Section titled “Returns”](#returns-26) readonly [`MergeDecision`](/api/domain/#mergedecision)\[] *** ### mergeOutcomes() [Section titled “mergeOutcomes()”](#mergeoutcomes) ```ts 1 function mergeOutcomes(pairs, options?): readonly MergeOutcome[]; ``` Classify every oriented candidate pair, applying in order: the per-cycle cap, the strict similarity threshold, the divergence veto (only when both texts are present), a self-merge check, and the in-batch role guard. [mergeCandidates](/api/domain/#mergecandidates) is the committed subset; this form exists so a caller can COUNT the refusals by cause. A phase that reported `candidates - decisions` as “vetoed” told an operator that a predicate fired on pairs no predicate had seen, which is how two all-refusing runs read as a divergence problem when half of them were role-guard skips. The **in-batch role guard** needs the most explanation of the five. Every committed decision fixes its drop path as DROPPED and its keep path as KEPT for the rest of the batch, and three later uses are refused, each ruling out a distinct corruption on a transitive chain: * A path already **dropped** cannot be dropped again (two keepers would each believe they absorbed it) nor become a keeper (content folded into a file this same batch archives). * A path already a **keeper** cannot later be dropped. Given `(gf, a)` then `(b, gf)`, both would commit, `gf` absorbs `a` and is then archived into `b`, so `a`’s content is superseded into a file that no longer exists. That is the loss the guard exists to prevent. Verified against the input `[(gf → a), (b → gf)]`. A path already a **keeper** MAY keep again. `(k, a)` then `(k, b)` is one page absorbing two duplicates in one batch, and nothing on that chain is archived twice or archived after being folded into: `k` survives with two `supersedes` links and both drops point at it. The earlier rule claimed BOTH roles of a committed pair, which made a model group of N members fold at most ONE of them per run — every pair a group implies names the same keeper — and reported the other N-2 as vetoes. The guard’s job is the two corruptions above, not a fold budget per keeper; the per-cycle cap is that. Fixing roles rather than memberships keeps the output a function of input order alone. The first decision claiming a path in a role wins, matching the SQL result-set iteration upstream, and a later pair contradicting it is skipped rather than reordering anything. #### Parameters [Section titled “Parameters”](#parameters-27) ##### pairs [Section titled “pairs”](#pairs-1) readonly [`MergePair`](/api/domain/#mergepair)\[] ##### options? [Section titled “options?”](#options-2) ###### maxPairs? [Section titled “maxPairs?”](#maxpairs-1) `number` ###### threshold? [Section titled “threshold?”](#threshold-1) `number` #### Returns [Section titled “Returns”](#returns-27) readonly [`MergeOutcome`](/api/domain/#mergeoutcome)\[] *** ### mergeVetoed() [Section titled “mergeVetoed()”](#mergevetoed) ```ts 1 function mergeVetoed(textA, textB): boolean; ``` The veto: the disjunction of the three divergence predicates, each over its own text. Symmetric, pure, total. A vetoed pair is never a duplicate no matter its cosine. #### Parameters [Section titled “Parameters”](#parameters-28) ##### textA [Section titled “textA”](#texta) [`MergeText`](/api/domain/#mergetext) ##### textB [Section titled “textB”](#textb) [`MergeText`](/api/domain/#mergetext) #### Returns [Section titled “Returns”](#returns-28) `boolean` *** ### mergeVetoReasons() [Section titled “mergeVetoReasons()”](#mergevetoreasons) ```ts 1 function mergeVetoReasons(textA, textB): readonly MergeVetoReason[]; ``` Which divergence predicates fire on a pair, each over the text it reads (see [MergeText](/api/domain/#mergetext)): negation and variant over the joined text, numeric over the gist alone. Symmetric, pure, total. Empty exactly when [mergeVetoed](/api/domain/#mergevetoed) is false. Exported so a caller that must NAME the refusal (the review task a vetoed pair becomes, the acceptance arm’s per-pair stage) reads the same scoping the filter applied, instead of re-deriving which text each predicate saw. #### Parameters [Section titled “Parameters”](#parameters-29) ##### textA [Section titled “textA”](#texta-1) [`MergeText`](/api/domain/#mergetext) ##### textB [Section titled “textB”](#textb-1) [`MergeText`](/api/domain/#mergetext) #### Returns [Section titled “Returns”](#returns-29) readonly [`MergeVetoReason`](/api/domain/#mergevetoreason)\[] *** ### negationDivergent() [Section titled “negationDivergent()”](#negationdivergent) ```ts 1 function negationDivergent(textA, textB): boolean; ``` True when exactly one side carries a negation marker: “X is safe” against “X is NOT safe”. Symmetric. Both sides negating, or neither, is not divergent. #### Parameters [Section titled “Parameters”](#parameters-30) ##### textA [Section titled “textA”](#texta-2) `string` ##### textB [Section titled “textB”](#textb-2) `string` #### Returns [Section titled “Returns”](#returns-30) `boolean` *** ### numericTokenDivergent() [Section titled “numericTokenDivergent()”](#numerictokendivergent) ```ts 1 function numericTokenDivergent(textA, textB): boolean; ``` True when the two texts carry different numeric tokens: “retry 3 times” against “retry 13 times”. Two texts with no numbers at all, or with identical numbers, do not trip it. Symmetric. [mergeVetoReasons](/api/domain/#mergevetoreasons) hands it the GIST of each memory, not the article — see [MergeText](/api/domain/#mergetext) for why. #### Parameters [Section titled “Parameters”](#parameters-31) ##### textA [Section titled “textA”](#texta-3) `string` ##### textB [Section titled “textB”](#textb-3) `string` #### Returns [Section titled “Returns”](#returns-31) `boolean` *** ### pagerank() [Section titled “pagerank()”](#pagerank) ```ts 1 function pagerank( 2 nodes, 3 edges, 4 options? 5 ): ReadonlyMap; ``` PageRank by power iteration, weighted, with a uniform or personalized teleport prior. `seeds` present makes this personalized PageRank: the teleport distribution is the seed weights rather than uniform, so the scores describe reachability *from the query’s own hits*. That is what keeps the lateral retrieval arm query-conditioned. A uniform prior would inject the same hub memories into every query’s results regardless of what was asked. Seeds naming absent nodes are dropped, and an all-absent seed set falls back to the uniform prior, which the caller reads as “no lateral result”. Dangling nodes (no outbound edges) have their mass redistributed over the teleport distribution, so the scores sum to 1 rather than leaking. #### Parameters [Section titled “Parameters”](#parameters-32) ##### nodes [Section titled “nodes”](#nodes-2) readonly `string`\[] ##### edges [Section titled “edges”](#edges-3) readonly [`GraphEdge`](/api/domain/#graphedge)\[] ##### options? [Section titled “options?”](#options-3) ###### damping? [Section titled “damping?”](#damping) `number` ###### maxIterations? [Section titled “maxIterations?”](#maxiterations) `number` ###### seeds? [Section titled “seeds?”](#seeds) `ReadonlyMap`<`string`, `number`> ###### tolerance? [Section titled “tolerance?”](#tolerance) `number` #### Returns [Section titled “Returns”](#returns-32) `ReadonlyMap`<`string`, `number`> *** ### partitionByCooldown() [Section titled “partitionByCooldown()”](#partitionbycooldown) ```ts 1 function partitionByCooldown( 2 entries, 3 now, 4 cooldownSeconds? 5 ): object; ``` Split paths into those whose access stamp may be bumped now and those still cooling down. One pass, order-preserving, so `memory_reinforce` can answer both lists from one call. #### Parameters [Section titled “Parameters”](#parameters-33) ##### entries [Section titled “entries”](#entries) readonly `object`\[] ##### now [Section titled “now”](#now) `Date` ##### cooldownSeconds? [Section titled “cooldownSeconds?”](#cooldownseconds) `number` = `REINFORCE_COOLDOWN_S` #### Returns [Section titled “Returns”](#returns-33) `object` ##### bumped [Section titled “bumped”](#bumped) ```ts 1 readonly bumped: readonly string[]; ``` ##### cooledDown [Section titled “cooledDown”](#cooleddown) ```ts 1 readonly cooledDown: readonly string[]; ``` *** ### profileWeightSum() [Section titled “profileWeightSum()”](#profileweightsum) ```ts 1 function profileWeightSum(profile): number; ``` A profile’s weight sum under compensated summation. Every shipped profile returns exactly `1`; this is the convexity fact the composite’s `[0, 1]` range rests on. #### Parameters [Section titled “Parameters”](#parameters-34) ##### profile [Section titled “profile”](#profile-1) [`WeightProfile`](/api/domain/#weightprofile) #### Returns [Section titled “Returns”](#returns-34) `number` *** ### rankCandidatePairs() [Section titled “rankCandidatePairs()”](#rankcandidatepairs) ```ts 1 function rankCandidatePairs( 2 pairs, 3 vectors, 4 options 5 ): readonly NeighborPair[]; ``` Rank an ENUMERATED pair set: similarity, floor, per-source top-`k`, final ordering, cap. This is the shape for a consumer whose candidate pairs come from a selective predicate — the conflict scan’s shared-entity join — rather than from the whole pair space. The predicate runs BEFORE ranking, exactly as a `WHERE` inside the ranking CTE would, so per-source top-`k` is computed over passing pairs only. A pair naming a key with no vector contributes nothing, the same outcome the SQL join’s missing-embedding row produces. #### Parameters [Section titled “Parameters”](#parameters-35) ##### pairs [Section titled “pairs”](#pairs-2) readonly [`CandidatePair`](/api/domain/#candidatepair)\[] ##### vectors [Section titled “vectors”](#vectors) readonly [`KeyedVector`](/api/domain/#keyedvector)\[] ##### options [Section titled “options”](#options-4) [`NeighborOptions`](/api/domain/#neighboroptions) #### Returns [Section titled “Returns”](#returns-35) readonly [`NeighborPair`](/api/domain/#neighborpair)\[] *** ### rankFused() [Section titled “rankFused()”](#rankfused) ```ts 1 function rankFused(fused): readonly object[]; ``` Fused scores as a ranked list, best first. Ties break on path ascending, so the ordering is total and reproducible. Two runs over the same corpus produce the same list, which is what the discrimination gate compares against. #### Parameters [Section titled “Parameters”](#parameters-36) ##### fused [Section titled “fused”](#fused-1) `ReadonlyMap`<`string`, `number`> #### Returns [Section titled “Returns”](#returns-36) readonly `object`\[] *** ### reprieveScore() [Section titled “reprieveScore()”](#reprievescore) ```ts 1 function reprieveScore(input): number; ``` The four-term reprieve score. Deliberately **not** convex: the `log1p(accessCount)` term is unbounded, so the score can exceed 1. It is proven only monotone and sign-clamped. A negative `outcomeScore` contributes exactly 0 and does not subtract, mirroring the salience arm’s `max(coalesce(outcome_score, 0.0), 0.0)`. Without that clamp a memory that once produced a bad outcome would be punished twice, once by the outcome EWMA that already lowered its salience, and again here by having its reprieve pushed below the floor. #### Parameters [Section titled “Parameters”](#parameters-37) ##### input [Section titled “input”](#input-1) [`ReprieveInput`](/api/domain/#reprieveinput) #### Returns [Section titled “Returns”](#returns-37) `number` *** ### rrfContribution() [Section titled “rrfContribution()”](#rrfcontribution) ```ts 1 function rrfContribution(rank, weight): Option; ``` One arm’s contribution to a fused score. `rank` is 1-based within that arm’s candidate list; `weight` is the arm’s configured multiplier. A zero-weight arm and an out-of-range rank both yield `None` rather than 0, so a disabled arm is structurally absent from the fold instead of silently adding a neutral term that later arithmetic could mistake for a real score. #### Parameters [Section titled “Parameters”](#parameters-38) ##### rank [Section titled “rank”](#rank-1) `number` ##### weight [Section titled “weight”](#weight-1) `number` #### Returns [Section titled “Returns”](#returns-38) `Option`<`number`> *** ### rrfScore() [Section titled “rrfScore()”](#rrfscore) ```ts 1 function rrfScore( 2 rank, 3 weight, 4 k? 5 ): number; ``` One arm’s contribution: `weight / (rank + k)`, unitless. Strictly decreasing in `rank` and linear in `weight`, so a weight-0 arm contributes exactly 0 and is inert in the fold. `k` damps the difference between the top ranks, which is what stops one arm’s confident first place from dominating four other arms’ consensus. #### Parameters [Section titled “Parameters”](#parameters-39) ##### rank [Section titled “rank”](#rank-2) `number` ##### weight [Section titled “weight”](#weight-2) `number` ##### k? [Section titled “k?”](#k-2) `number` = `RRF_K` #### Returns [Section titled “Returns”](#returns-39) `number` *** ### scoreRetention() [Section titled “scoreRetention()”](#scoreretention) ```ts 1 function scoreRetention(input): RetentionScore; ``` Score one memory: normalize, weight by its type’s profile, band the composite. #### Parameters [Section titled “Parameters”](#parameters-40) ##### input [Section titled “input”](#input-2) [`RetentionInput`](/api/domain/#retentioninput) #### Returns [Section titled “Returns”](#returns-40) [`RetentionScore`](/api/domain/#retentionscore) *** ### shouldBumpAccess() [Section titled “shouldBumpAccess()”](#shouldbumpaccess) ```ts 1 function shouldBumpAccess( 2 lastAccessedAt, 3 now, 4 cooldownSeconds? 5 ): boolean; ``` The reinforcement cooldown predicate. Its twin is the salience arm’s SQL guard: ```sql 1 WHERE last_accessed_at IS NULL 2 OR unixepoch('now') - unixepoch(last_accessed_at) >= 900 ``` SQL cannot call this function, so the shared source of truth is the window constant [REINFORCE\_COOLDOWN\_S](/api/domain/#reinforce_cooldown_s) and the boundary behavior is pinned by a property test on both sides. The `>=` here matches the SQL’s `>=`: a stamp exactly `cooldownSeconds` old **is** bumpable. The cooldown exists because `access_count` feeds the salience RRF arm. Without it, replaying one query ten times would inflate that memory’s salience tenfold and let a loop in an agent rewrite the corpus’s ranking. #### Parameters [Section titled “Parameters”](#parameters-41) ##### lastAccessedAt [Section titled “lastAccessedAt”](#lastaccessedat) `Date` | `undefined` ##### now [Section titled “now”](#now-1) `Date` ##### cooldownSeconds? [Section titled “cooldownSeconds?”](#cooldownseconds-1) `number` = `REINFORCE_COOLDOWN_S` #### Returns [Section titled “Returns”](#returns-41) `boolean` *** ### shouldReprieve() [Section titled “shouldReprieve()”](#shouldreprieve) ```ts 1 function shouldReprieve(input): boolean; ``` The bounded reprieve gate. A TTL-passed memory is reprieved iff its score clears `floor` AND it has been reprieved fewer than `maxReprieves` times. `maxReprieves: 0` forces every TTL-passed memory to expire regardless of score. #### Parameters [Section titled “Parameters”](#parameters-42) ##### input [Section titled “input”](#input-3) ###### floor? [Section titled “floor?”](#floor-3) `number` ###### maxReprieves? [Section titled “maxReprieves?”](#maxreprieves) `number` ###### reprieveCount [Section titled “reprieveCount”](#reprievecount) `number` ###### score [Section titled “score”](#score-3) `number` #### Returns [Section titled “Returns”](#returns-42) `boolean` *** ### signalValue() [Section titled “signalValue()”](#signalvalue) ```ts 1 function signalValue(signal): number; ``` The outcome-EWMA signal value for a reinforcement, unitless in `[-1, 1]`. `neutral` is 0, so a neutral reinforcement bumps the access count without moving the outcome score. A memory being read is evidence of relevance, not of correctness. #### Parameters [Section titled “Parameters”](#parameters-43) ##### signal [Section titled “signal”](#signal) `"positive"` | `"negative"` | `"neutral"` #### Returns [Section titled “Returns”](#returns-43) `number` *** ### toFp() [Section titled “toFp()”](#tofp) ```ts 1 function toFp(value): number; ``` A float in `[-1, 1]` onto the grid, rounded to the nearest grid point. #### Parameters [Section titled “Parameters”](#parameters-44) ##### value [Section titled “value”](#value) `number` #### Returns [Section titled “Returns”](#returns-44) `number` *** ### topNeighborPairs() [Section titled “topNeighborPairs()”](#topneighborpairs) ```ts 1 function topNeighborPairs(vectors, options): readonly NeighborPair[]; ``` Per-source top-`k` nearest neighbors above a similarity floor, over every unordered pair. Each pair’s similarity is computed ONCE and offered to BOTH endpoints’ neighborhoods, so the output can hold `(a, b)` and `(b, a)` — each is a fact about a different source’s neighborhood, and a consumer folding pairs must dedup the mirror itself (dedup-merge does, with its `seen` set). A floor comparison a NaN similarity cannot pass keeps a vector carrying NaN bytes out of every neighborhood rather than poisoning an ordering. #### Parameters [Section titled “Parameters”](#parameters-45) ##### vectors [Section titled “vectors”](#vectors-1) readonly [`KeyedVector`](/api/domain/#keyedvector)\[] ##### options [Section titled “options”](#options-5) [`NeighborOptions`](/api/domain/#neighboroptions) #### Returns [Section titled “Returns”](#returns-45) readonly [`NeighborPair`](/api/domain/#neighborpair)\[] *** ### variantQualifierDivergent() [Section titled “variantQualifierDivergent()”](#variantqualifierdivergent) ```ts 1 function variantQualifierDivergent(textA, textB): boolean; ``` True when the two bodies carry different variant qualifiers: “M1” against “M1 Pro”. Symmetric. #### Parameters [Section titled “Parameters”](#parameters-46) ##### textA [Section titled “textA”](#texta-4) `string` ##### textB [Section titled “textB”](#textb-4) `string` #### Returns [Section titled “Returns”](#returns-46) `boolean` *** ### weightsFor() [Section titled “weightsFor()”](#weightsfor) ```ts 1 function weightsFor(memoryType): WeightProfile; ``` The weight profile for a memory type. #### Parameters [Section titled “Parameters”](#parameters-47) ##### memoryType [Section titled “memoryType”](#memorytype-2) `string` #### Returns [Section titled “Returns”](#returns-47) [`WeightProfile`](/api/domain/#weightprofile) # @memhtml/eval `@memhtml/eval`: the fixture corpus generator, the refusable retrieval discrimination gate, and the write-path discrimination arm. The retrieval corpus is never committed; each run regenerates it from a seed. The write-path corpus is committed, because its labels are the oracle. The gate asks one question: can the retrieval stack rank a memory above its own high-similarity WRONG twin? `controls` turns a true claim into a plausible impostor along three divergence families (negation, numeric, variant), validates each control against its own family’s predicate from `@memhtml/domain`, and discards a control that failed to diverge. `corpus` is a pure function of `(seed, now)`: it yields the same memory specs and probes on any machine, with every timestamp anchored as an offset behind the run so that salience decay cannot move the ranks from one calendar day to the next. `fixture` is the one module that writes, rendering a spec into a real git repository under a temp directory. `harness` builds the stack the gate measures (that fixture, a real database with the shipped migrations, the real indexer and the real four-arm retrieval) and substitutes only the embedder: a deterministic one whose cosine relations are a pure function of the text, so the numbers reproduce without credentials. `discriminate` runs the probes and reports two numbers, of which the strict one is the gate: `mrr` against a floor of 0.85, and a per-probe inversion count where one inversion fails the run regardless of MRR. `run` picks the mode and shapes the outcome so that a skipped gate never looks like a passing one: `fake` mode runs everywhere, and `live` mode without credentials reports `skipped: true` and `passed: false`. The write-path arm asks the other question the 2026-08-27 eval plan posed: does the WRITE path tell a good candidate memory from a bad one? `write-path-corpus` is a labeled corpus of 62 candidate memories (31 the gate should write, 31 it should refuse), each with the provenance of its label (the contract clause or design rule that decides it) and two fixture transcripts every good quote is a verbatim span of. `write-path-gate` composes the production gate from its own functions in the order the fleet runs them: the consolidator door’s schema decode, readable-session check and verbatim-quote check, then the sleep phase’s per-candidate refusal (`candidateRefusalFor`). Nothing is re-implemented. `write-path-metrics` scores the decisions with refusal as the positive class (precision, recall, F1, the four-cell confusion matrix, and a per-class breakdown naming the items that disagreed with their label). `write-path-run` runs the arm, derives the floor (the plan named none, so it is the measured baseline F1 0.9333 minus five points, 0.88, stated as such), and freezes every decision into `fixtures/write-path-discrimination.manifest.json`, which the `test:eval` tier replays decision for decision. `pnpm freeze:write-path` is the one way that manifest changes. Three failure classes are labeled bad and accepted by the gate today (a duplicated evidence quote, twice; a claim that is a verbatim transcript span), and one good item is refused for a whitespace-padded session id; `KNOWN_GAP_CLASSES` states exactly that set and the tier fails if the set moves in either direction. The write-path ACCEPTANCE arm asks the question the discrimination arm cannot: does `dedup-merge` FOLD the pairs it should? A phase that refuses everything scores perfectly on refusal, which is what production did on 2026-09-10 and 2026-09-11 (`vetoed: 171/171`, `vetoed: 178/178`, `merged: 0`). `write-path-acceptance-corpus` is 19 labeled groups (12 `merge`, 7 `keep`), each the answer a model would have given, with the provenance of its label. `write-path-acceptance` runs them through the phase’s own fold path — `dedupMergeTextFor`, `groupPairsFor`, and `mergeOutcomes` under `DEDUP_ADMIT_FLOOR`, all imported — with a fold as the positive class, reports acceptance (folds over `merge` pairs), precision, and where every refused pair stopped (read off the filter’s own `MergeStage`, a veto refined to its predicate by `mergeVetoReasons`), and gates on an acceptance floor (measured baseline 1.0 minus five points, 0.95) plus zero folds of a `keep` pair. `ACCEPTANCE_KNOWN_GAP_CLASSES` is empty: the two mechanisms behind the all-veto runs — `numericTokenDivergent` over the whole article vetoing the same claim for an incidental number in the body (`incidental-number`), and a role guard that claimed both roles folding at most one member of a model group (`group-fanout`) — measured 8 of 12 under the old rules and 14 of 14 under the current ones, which read numbers off the gist and admit a repeated keeper. The `keep` side holds the counterexamples the new rules must still refuse (a numeric conflict in the claim, a negation or variant qualifier only in the body, a templated series whose claims quote different ids), and `BODY_NUMBER_ONLY_GROUP` states the shape the gist rule no longer refuses, measured outside the gated corpus. This package sits above `@memhtml/contracts`, `@memhtml/domain`, `@memhtml/html`, `@memhtml/index`, `@memhtml/llm`, `@memhtml/store`, `@memhtml/sleep` and `@memhtml/consolidator` (the last two only for the write-path arm, which imports the gate it measures), and `apps/cli` depends on it: `memhtml eval discriminate` is `runDiscrimination`, and `@memhtml/sleep`’s merge takes `discriminationGate` as its pre-merge gate from the CLI’s wiring, because the sleep package supplies no default. ## Modules [Section titled “Modules”](#modules) The package publishes one import path, `@memhtml/eval`, which re-exports the twelve modules below, so the reference on this page is the whole exported surface. `gen-fixture.ts` is the body of `pnpm gen:fixture` and is not exported; `scripts/freeze-write-path.mjs` is the body of `pnpm freeze:write-path` and lives outside `src/` because it resolves a repo fixture the published package does not ship. | Module | What it holds | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `controls.ts` | `DIVERGENCE_FAMILIES`, the three flips (`negationFlip`, `numericFlip`, `variantFlip`), `familyPredicate` and `deriveControl`. | | `corpus.ts` | `MemorySpec`, `Probe`, `CorpusSpec`, the defaults (`DEFAULT_SEED`, `DEFAULT_CORPUS_SIZE`, `DEFAULT_PROBE_COUNT`), `quantizeNow`, `articleFor`, `queryFor` and `buildCorpus`. | | `discriminate.ts` | `MRR_FLOOR`, `PROBE_LIMIT`, `runProbes`, `summarize`, `discriminate`, `runFloor`, `describeFailure` and the report shapes. | | `fixture.ts` | `FixtureCorpus`, `FixtureOptions`, `memoryFileFor`, `writeCorpus` and `makeFixtureCorpus`. | | `harness.ts` | `EvalStack`, `StackOptions`, the embedders (`fakeEmbedder`, `failingEmbedder`, `liveEmbedder`, `fakeVector`, `FAKE_DIM`), `buildStack` and `withStack`. | | `run.ts` | `EvalOptions`, `EvalOutcome`, `BEDROCK_TOKEN_VAR`, `hasBedrockCredentials`, `runDiscrimination`, `DiscriminationFailed` and `discriminationGate`. | | `write-path-corpus.ts` | `WRITE_PATH_CORPUS` (62 labeled candidates), `TRANSCRIPTS`, `KNOWN_GAP_CLASSES`, `CEILINGS`, `MANY_SPANS`, `padTo` and the fixture session ids. | | `write-path-gate.ts` | `admitCandidate`, `GATE_STAGES`, `GateDecision`, `GateBatch`: the production gate composed stage by stage. | | `write-path-metrics.ts` | `summarizeWritePath`, `confusionOf`, `breakdownByClass`, `agrees`, `describeWritePath` and the report shapes. | | `write-path-acceptance-corpus.ts` | `ACCEPTANCE_CORPUS` (16 labeled groups), `ACCEPTANCE_KNOWN_GAP_CLASSES` and the group shapes. | | `write-path-acceptance.ts` | `runWritePathAcceptance`, `ACCEPTANCE_FLOOR`, `ACCEPTANCE_BASELINE`, `impliedPairs`, `decideAcceptance`, `summarizeAcceptance`, `acceptanceByClass`, `describeAcceptance` and the report shapes. | | `write-path-run.ts` | `runWritePathDiscrimination`, `WRITE_PATH_F1_FLOOR`, `WRITE_PATH_BASELINE_F1`, `GATE_SOURCES`, `MANIFEST_FILENAME`, `manifestFor`, `replayDrift`, `readManifest`, `writeManifest`, `withBatch`, `decideAll`. | ## Classes [Section titled “Classes”](#classes) ### DiscriminationFailed [Section titled “DiscriminationFailed”](#discriminationfailed) A failed gate, as a tagged error. Tagged so it is a failure like every other in the system rather than a bare value. The CLI’s `codeFor` switches on `_tag`, and `ERR_DISCRIMINATION_FAILED`, the error code design §8 named for exactly this refusal, would otherwise have no producer at all and degrade to `ERR_UNKNOWN`. The whole outcome rides along, because a refusal an operator cannot reproduce is a refusal they will override. `seed` regenerates the corpus that failed, and `inversions` says which probes. #### Constructors [Section titled “Constructors”](#constructors) ##### Constructor [Section titled “Constructor”](#constructor) ```ts 1 new DiscriminationFailed(outcome): DiscriminationFailed; ``` ###### Parameters [Section titled “Parameters”](#parameters) ###### outcome [Section titled “outcome”](#outcome) [`EvalOutcome`](/api/eval/#evaloutcome) ###### Returns [Section titled “Returns”](#returns) [`DiscriminationFailed`](/api/eval/#discriminationfailed) #### Properties [Section titled “Properties”](#properties) ##### \_tag [Section titled “\_tag”](#_tag) ```ts 1 readonly _tag: "DiscriminationFailed" = "DiscriminationFailed"; ``` ##### outcome [Section titled “outcome”](#outcome-1) ```ts 1 readonly outcome: EvalOutcome; ``` #### Accessors [Section titled “Accessors”](#accessors) ##### reason [Section titled “reason”](#reason) ###### Get Signature [Section titled “Get Signature”](#get-signature) ```ts 1 get reason(): string; ``` The one-line summary, used as the envelope’s human message. ###### Returns [Section titled “Returns”](#returns-1) `string` ## Interfaces [Section titled “Interfaces”](#interfaces) ### AcceptanceClassBreakdown [Section titled “AcceptanceClassBreakdown”](#acceptanceclassbreakdown) One class’s agreement with its labels. #### Properties [Section titled “Properties”](#properties-1) ##### agreed [Section titled “agreed”](#agreed) ```ts 1 readonly agreed: number; ``` Decisions that matched the label: folded for `merge`, refused for `keep`. ##### class [Section titled “class”](#class) ```ts 1 readonly class: string; ``` ##### disagreed [Section titled “disagreed”](#disagreed) ```ts 1 readonly disagreed: readonly string[]; ``` ##### label [Section titled “label”](#label) ```ts 1 readonly label: AcceptanceLabel; ``` ##### pairs [Section titled “pairs”](#pairs) ```ts 1 readonly pairs: number; ``` *** ### AcceptanceMember [Section titled “AcceptanceMember”](#acceptancemember) One member of a group, as the phase’s `CorpusRow` would carry it. Order in the array is corpus order. #### Properties [Section titled “Properties”](#properties-2) ##### body [Section titled “body”](#body) ```ts 1 readonly body: string; ``` The `
`’s text content, `files.body_text`. ##### gist [Section titled “gist”](#gist) ```ts 1 readonly gist: string; ``` The `` claim, `files.gist`. ##### path [Section titled “path”](#path) ```ts 1 readonly path: string; ``` *** ### AcceptanceOptions [Section titled “AcceptanceOptions”](#acceptanceoptions) What [runWritePathAcceptance](/api/eval/#runwritepathacceptance) takes. #### Properties [Section titled “Properties”](#properties-3) ##### acceptanceFloor? [Section titled “acceptanceFloor?”](#acceptancefloor) ```ts 1 readonly optional acceptanceFloor?: number; ``` ##### corpus? [Section titled “corpus?”](#corpus) ```ts 1 readonly optional corpus?: readonly LabeledGroup[]; ``` *** ### AcceptanceReport [Section titled “AcceptanceReport”](#acceptancereport) The report the gate reads. #### Properties [Section titled “Properties”](#properties-4) ##### acceptance [Section titled “acceptance”](#acceptance) ```ts 1 readonly acceptance: number; ``` `accepted / eligible`, four decimals; `0` when nothing was eligible. ##### acceptanceFloor [Section titled “acceptanceFloor”](#acceptancefloor-1) ```ts 1 readonly acceptanceFloor: number; ``` ##### accepted [Section titled “accepted”](#accepted) ```ts 1 readonly accepted: number; ``` Folds among the eligible. ##### decisions [Section titled “decisions”](#decisions) ```ts 1 readonly decisions: readonly PairDecision[]; ``` ##### eligible [Section titled “eligible”](#eligible) ```ts 1 readonly eligible: number; ``` Pairs from `merge` groups: the ones that should fold. ##### f1 [Section titled “f1”](#f1) ```ts 1 readonly f1: number; ``` ##### foldedKeep [Section titled “foldedKeep”](#foldedkeep) ```ts 1 readonly foldedKeep: number; ``` Folds among the guarded. Any value above zero fails the tier. ##### groups [Section titled “groups”](#groups) ```ts 1 readonly groups: number; ``` ##### guarded [Section titled “guarded”](#guarded) ```ts 1 readonly guarded: number; ``` Pairs from `keep` groups. ##### pairs [Section titled “pairs”](#pairs-1) ```ts 1 readonly pairs: number; ``` ##### passed [Section titled “passed”](#passed) ```ts 1 readonly passed: boolean; ``` True when both labels are present AND no `keep` pair folded AND `acceptance >= acceptanceFloor`. One label alone is a failure, not a vacuous pass, for the reason the discrimination arm gives. ##### perClass [Section titled “perClass”](#perclass) ```ts 1 readonly perClass: readonly AcceptanceClassBreakdown[]; ``` ##### precision [Section titled “precision”](#precision) ```ts 1 readonly precision: number; ``` `accepted / (accepted + foldedKeep)`, four decimals; `0` when nothing folded. ##### recall [Section titled “recall”](#recall) ```ts 1 readonly recall: number; ``` Same as `acceptance`, stated under the name the discrimination arm uses. *** ### ClaimText [Section titled “ClaimText”](#claimtext) The text of a memory as the flips see it, meaning the claim then each body paragraph. #### Extended by [Section titled “Extended by”](#extended-by) * [`DerivedControl`](/api/eval/#derivedcontrol) #### Properties [Section titled “Properties”](#properties-5) ##### body [Section titled “body”](#body-1) ```ts 1 readonly body: readonly string[]; ``` ##### claim [Section titled “claim”](#claim) ```ts 1 readonly claim: string; ``` *** ### ClassBreakdown [Section titled “ClassBreakdown”](#classbreakdown) One class’s agreement with its labels. #### Properties [Section titled “Properties”](#properties-6) ##### agreed [Section titled “agreed”](#agreed-1) ```ts 1 readonly agreed: number; ``` Decisions that matched the label: rejected for `bad`, accepted for `good`. ##### class [Section titled “class”](#class-1) ```ts 1 readonly class: string; ``` ##### disagreed [Section titled “disagreed”](#disagreed-1) ```ts 1 readonly disagreed: readonly string[]; ``` Ids of the items whose decision contradicted the label. ##### items [Section titled “items”](#items) ```ts 1 readonly items: number; ``` ##### label [Section titled “label”](#label-1) ```ts 1 readonly label: WritePathLabel; ``` *** ### ConfusionMatrix [Section titled “ConfusionMatrix”](#confusionmatrix) The four cells, named by what happened rather than by TP/FP letters a reader has to re-derive. `badRejected` is the true positive, `goodRejected` the false positive, `badAccepted` the false negative, `goodAccepted` the true negative. #### Properties [Section titled “Properties”](#properties-7) ##### badAccepted [Section titled “badAccepted”](#badaccepted) ```ts 1 readonly badAccepted: number; ``` ##### badRejected [Section titled “badRejected”](#badrejected) ```ts 1 readonly badRejected: number; ``` ##### goodAccepted [Section titled “goodAccepted”](#goodaccepted) ```ts 1 readonly goodAccepted: number; ``` ##### goodRejected [Section titled “goodRejected”](#goodrejected) ```ts 1 readonly goodRejected: number; ``` *** ### CorpusSpec [Section titled “CorpusSpec”](#corpusspec) The whole fixture: the memories to write, their access history, and the probes. #### Properties [Section titled “Properties”](#properties-8) ##### access [Section titled “access”](#access) ```ts 1 readonly access: readonly AccessSpec[]; ``` `state.access` rows the harness seeds before running the probes. ##### memories [Section titled “memories”](#memories) ```ts 1 readonly memories: readonly MemorySpec[]; ``` ##### now [Section titled “now”](#now) ```ts 1 readonly now: number; ``` The quantized run instant every stamp is anchored behind, UTC millis. See [quantizeNow](/api/eval/#quantizenow). ##### probes [Section titled “probes”](#probes) ```ts 1 readonly probes: readonly Probe[]; ``` ##### seed [Section titled “seed”](#seed) ```ts 1 readonly seed: number; ``` *** ### DerivedControl [Section titled “DerivedControl”](#derivedcontrol) A derived control, holding the flipped text plus the family that produced it. #### Extends [Section titled “Extends”](#extends) * [`ClaimText`](/api/eval/#claimtext) #### Properties [Section titled “Properties”](#properties-9) ##### body [Section titled “body”](#body-2) ```ts 1 readonly body: readonly string[]; ``` ###### Inherited from [Section titled “Inherited from”](#inherited-from) [`ClaimText`](/api/eval/#claimtext).[`body`](/api/eval/#claimtext) ##### claim [Section titled “claim”](#claim-1) ```ts 1 readonly claim: string; ``` ###### Inherited from [Section titled “Inherited from”](#inherited-from-1) [`ClaimText`](/api/eval/#claimtext).[`claim`](/api/eval/#claimtext) ##### family [Section titled “family”](#family) ```ts 1 readonly family: DivergenceFamily; ``` ##### note [Section titled “note”](#note) ```ts 1 readonly note: string; ``` What changed, for the probe’s own record. *** ### DiscriminationReport [Section titled “DiscriminationReport”](#discriminationreport) The whole run’s outcome. This is the `eval.discrimination` envelope payload. #### Extended by [Section titled “Extended by”](#extended-by-1) * [`EvalOutcome`](/api/eval/#evaloutcome) #### Properties [Section titled “Properties”](#properties-10) ##### corpusMrr [Section titled “corpusMrr”](#corpusmrr) ```ts 1 readonly corpusMrr: number; ``` Mean reciprocal rank over the WHOLE hit list, unitless in `[0, 1]`, four decimals. Reported rather than gated, because corpus size dominates it more than ranking quality does. `DEFAULT_ARM_LIMIT` is 40, which is 13% of a 300-file fixture and under 1% of a real corpus, so the two query-blind arms cover a far larger share of a fixture than of production. The value is seed- and parameter-dependent — it moves with the corpus size, the probe count, and the seed — but it rises with corpus size on this generator while the inversion count stays flat, which is exactly why a gate on this number would be a gate on how big the fixture is. ##### degradedProbes [Section titled “degradedProbes”](#degradedprobes) ```ts 1 readonly degradedProbes: number; ``` Probes ranked by a degraded (vector-arm-free) search. ##### discriminated [Section titled “discriminated”](#discriminated) ```ts 1 readonly discriminated: number; ``` Probes whose target strictly outranked all of its controls. ##### inversions [Section titled “inversions”](#inversions) ```ts 1 readonly inversions: readonly ProbeResult[]; ``` Probes with at least one inversion. The gate fails when this is non-zero. ##### mode [Section titled “mode”](#mode) ```ts 1 readonly mode: EvalMode; ``` ##### mrr [Section titled “mrr”](#mrr) ```ts 1 readonly mrr: number; ``` Mean reciprocal rank over `{target} ∪ controls`, unitless in `[0, 1]`, four decimals. **This is the gated number, and its coordinate space is the discrimination set, not the corpus.** Design §5 states the floor in the same clause as `rank(target) < min(rank(control))`, so the space the sentence is about is the target against its own impostors. `1.0` means every target beat every control outright. See [corpusMrr](/api/eval/#discriminationreport) for the other reading, which is reported and not gated. ##### mrrFloor [Section titled “mrrFloor”](#mrrfloor) ```ts 1 readonly mrrFloor: number; ``` The floor [mrr](/api/eval/#discriminationreport) was measured against. ##### passed [Section titled “passed”](#passed-1) ```ts 1 readonly passed: boolean; ``` True when there are zero inversions AND `mrr >= mrrFloor`. ##### probes [Section titled “probes”](#probes-1) ```ts 1 readonly probes: number; ``` ##### results [Section titled “results”](#results) ```ts 1 readonly results: readonly ProbeResult[]; ``` *** ### EvalEmbedder [Section titled “EvalEmbedder”](#evalembedder) Both embed ports plus a call counter. #### Extends [Section titled “Extends”](#extends-1) * `EmbedPort`.`QueryEmbedPort` #### Properties [Section titled “Properties”](#properties-11) ##### calls [Section titled “calls”](#calls) ```ts 1 readonly calls: () => number; ``` ###### Returns [Section titled “Returns”](#returns-2) `number` ##### embed [Section titled “embed”](#embed) ```ts 1 readonly embed: (texts) => Effect[], ModelUnavailable>; ``` ###### Parameters [Section titled “Parameters”](#parameters-1) ###### texts [Section titled “texts”](#texts) readonly `string`\[] ###### Returns [Section titled “Returns”](#returns-3) `Effect`\\[], `ModelUnavailable`> ###### Inherited from [Section titled “Inherited from”](#inherited-from-2) ```ts 1 EmbedPort.embed ``` ##### embedQuery [Section titled “embedQuery”](#embedquery) ```ts 1 readonly embedQuery: (text) => Effect, ModelUnavailable>; ``` ###### Parameters [Section titled “Parameters”](#parameters-2) ###### text [Section titled “text”](#text) `string` ###### Returns [Section titled “Returns”](#returns-4) `Effect`<`Float32Array`<`ArrayBufferLike`>, `ModelUnavailable`> ###### Inherited from [Section titled “Inherited from”](#inherited-from-3) ```ts 1 QueryEmbedPort.embedQuery ``` *** ### EvalOptions [Section titled “EvalOptions”](#evaloptions) What [runDiscrimination](/api/eval/#rundiscrimination) takes. #### Properties [Section titled “Properties”](#properties-12) ##### env? [Section titled “env?”](#env) ```ts 1 readonly optional env?: Readonly>; ``` Injected for tests, so the credential branch is drivable without touching the environment. ##### mode? [Section titled “mode?”](#mode-1) ```ts 1 readonly optional mode?: EvalMode; ``` `fake` by default, the mode that works in CI and whose numbers are reproducible. ##### mrrFloor? [Section titled “mrrFloor?”](#mrrfloor-1) ```ts 1 readonly optional mrrFloor?: number; ``` ##### now? [Section titled “now?”](#now-1) ```ts 1 readonly optional now?: number; ``` The run instant to anchor the corpus’s stamps behind, UTC millis. The clock when absent. ##### probes? [Section titled “probes?”](#probes-2) ```ts 1 readonly optional probes?: number; ``` ##### seed? [Section titled “seed?”](#seed-1) ```ts 1 readonly optional seed?: number; ``` ##### size? [Section titled “size?”](#size) ```ts 1 readonly optional size?: number; ``` *** ### EvalOutcome [Section titled “EvalOutcome”](#evaloutcome) What `memhtml eval discriminate` emits, the report plus which mode was asked for. #### Extends [Section titled “Extends”](#extends-2) * [`DiscriminationReport`](/api/eval/#discriminationreport) #### Properties [Section titled “Properties”](#properties-13) ##### corpusMrr [Section titled “corpusMrr”](#corpusmrr-1) ```ts 1 readonly corpusMrr: number; ``` Mean reciprocal rank over the WHOLE hit list, unitless in `[0, 1]`, four decimals. Reported rather than gated, because corpus size dominates it more than ranking quality does. `DEFAULT_ARM_LIMIT` is 40, which is 13% of a 300-file fixture and under 1% of a real corpus, so the two query-blind arms cover a far larger share of a fixture than of production. The value is seed- and parameter-dependent — it moves with the corpus size, the probe count, and the seed — but it rises with corpus size on this generator while the inversion count stays flat, which is exactly why a gate on this number would be a gate on how big the fixture is. ###### Inherited from [Section titled “Inherited from”](#inherited-from-4) [`DiscriminationReport`](/api/eval/#discriminationreport).[`corpusMrr`](/api/eval/#discriminationreport) ##### corpusSize [Section titled “corpusSize”](#corpussize) ```ts 1 readonly corpusSize: number; ``` ##### degradedProbes [Section titled “degradedProbes”](#degradedprobes-1) ```ts 1 readonly degradedProbes: number; ``` Probes ranked by a degraded (vector-arm-free) search. ###### Inherited from [Section titled “Inherited from”](#inherited-from-5) [`DiscriminationReport`](/api/eval/#discriminationreport).[`degradedProbes`](/api/eval/#discriminationreport) ##### discriminated [Section titled “discriminated”](#discriminated-1) ```ts 1 readonly discriminated: number; ``` Probes whose target strictly outranked all of its controls. ###### Inherited from [Section titled “Inherited from”](#inherited-from-6) [`DiscriminationReport`](/api/eval/#discriminationreport).[`discriminated`](/api/eval/#discriminationreport) ##### inversions [Section titled “inversions”](#inversions-1) ```ts 1 readonly inversions: readonly ProbeResult[]; ``` Probes with at least one inversion. The gate fails when this is non-zero. ###### Inherited from [Section titled “Inherited from”](#inherited-from-7) [`DiscriminationReport`](/api/eval/#discriminationreport).[`inversions`](/api/eval/#discriminationreport) ##### mode [Section titled “mode”](#mode-2) ```ts 1 readonly mode: EvalMode; ``` ###### Inherited from [Section titled “Inherited from”](#inherited-from-8) [`DiscriminationReport`](/api/eval/#discriminationreport).[`mode`](/api/eval/#discriminationreport) ##### mrr [Section titled “mrr”](#mrr-1) ```ts 1 readonly mrr: number; ``` Mean reciprocal rank over `{target} ∪ controls`, unitless in `[0, 1]`, four decimals. **This is the gated number, and its coordinate space is the discrimination set, not the corpus.** Design §5 states the floor in the same clause as `rank(target) < min(rank(control))`, so the space the sentence is about is the target against its own impostors. `1.0` means every target beat every control outright. See [corpusMrr](/api/eval/#discriminationreport) for the other reading, which is reported and not gated. ###### Inherited from [Section titled “Inherited from”](#inherited-from-9) [`DiscriminationReport`](/api/eval/#discriminationreport).[`mrr`](/api/eval/#discriminationreport) ##### mrrFloor [Section titled “mrrFloor”](#mrrfloor-2) ```ts 1 readonly mrrFloor: number; ``` The floor [mrr](/api/eval/#discriminationreport) was measured against. ###### Inherited from [Section titled “Inherited from”](#inherited-from-10) [`DiscriminationReport`](/api/eval/#discriminationreport).[`mrrFloor`](/api/eval/#discriminationreport) ##### now [Section titled “now”](#now-2) ```ts 1 readonly now: number; ``` The quantized run instant the corpus’s stamps were anchored behind, UTC millis. The other half of reproducing a failing run, since the corpus is a function of `(seed, now)`. `0` on a skipped run, where no corpus was generated. ##### passed [Section titled “passed”](#passed-2) ```ts 1 readonly passed: boolean; ``` True when there are zero inversions AND `mrr >= mrrFloor`. ###### Inherited from [Section titled “Inherited from”](#inherited-from-11) [`DiscriminationReport`](/api/eval/#discriminationreport).[`passed`](/api/eval/#discriminationreport) ##### probes [Section titled “probes”](#probes-3) ```ts 1 readonly probes: number; ``` ###### Inherited from [Section titled “Inherited from”](#inherited-from-12) [`DiscriminationReport`](/api/eval/#discriminationreport).[`probes`](/api/eval/#discriminationreport) ##### requested [Section titled “requested”](#requested) ```ts 1 readonly requested: EvalMode; ``` The mode the caller asked for, which differs from `mode` only on a refused live run. ##### results [Section titled “results”](#results-1) ```ts 1 readonly results: readonly ProbeResult[]; ``` ###### Inherited from [Section titled “Inherited from”](#inherited-from-13) [`DiscriminationReport`](/api/eval/#discriminationreport).[`results`](/api/eval/#discriminationreport) ##### seed [Section titled “seed”](#seed-2) ```ts 1 readonly seed: number; ``` The seed the corpus was generated at, so a failing run is reproducible. ##### skipped [Section titled “skipped”](#skipped) ```ts 1 readonly skipped: boolean; ``` True when live mode was requested with no credentials present. A skipped run is `passed: false` and carries zero probes, so it is a refusal rather than a result. ##### skipReason? [Section titled “skipReason?”](#skipreason) ```ts 1 readonly optional skipReason?: string; ``` Why it was skipped, absent otherwise. *** ### EvalStack [Section titled “EvalStack”](#evalstack) A built stack, holding the fixture, the database, and retrieval over both. #### Properties [Section titled “Properties”](#properties-14) ##### db [Section titled “db”](#db) ```ts 1 readonly db: DatabaseShape; ``` ##### embedCalls [Section titled “embedCalls”](#embedcalls) ```ts 1 readonly embedCalls: () => number; ``` ###### Returns [Section titled “Returns”](#returns-5) `number` ##### fixture [Section titled “fixture”](#fixture) ```ts 1 readonly fixture: FixtureCorpus; ``` ##### indexed [Section titled “indexed”](#indexed) ```ts 1 readonly indexed: number; ``` How many files the indexer projected, so a caller can assert the corpus really landed. ##### retrieval [Section titled “retrieval”](#retrieval) ```ts 1 readonly retrieval: RetrievalShape; ``` *** ### FixtureCorpus [Section titled “FixtureCorpus”](#fixturecorpus) A generated fixture repo, with the root, the git service over it, and the spec behind it. #### Properties [Section titled “Properties”](#properties-15) ##### cleanup [Section titled “cleanup”](#cleanup) ```ts 1 readonly cleanup: () => Promise; ``` ###### Returns [Section titled “Returns”](#returns-6) `Promise`<`void`> ##### git [Section titled “git”](#git) ```ts 1 readonly git: GitShape; ``` ##### root [Section titled “root”](#root) ```ts 1 readonly root: string; ``` ##### spec [Section titled “spec”](#spec) ```ts 1 readonly spec: CorpusSpec; ``` ##### written [Section titled “written”](#written) ```ts 1 readonly written: number; ``` How many files were written, generated artifacts excluded. *** ### FixtureOptions [Section titled “FixtureOptions”](#fixtureoptions) What [makeFixtureCorpus](/api/eval/#makefixturecorpus) takes. Every field has a default, so a caller can pass nothing. #### Extended by [Section titled “Extended by”](#extended-by-2) * [`StackOptions`](/api/eval/#stackoptions) #### Properties [Section titled “Properties”](#properties-16) ##### now? [Section titled “now?”](#now-3) ```ts 1 readonly optional now?: number; ``` The run instant the corpus’s stamps are anchored behind, UTC millis. Effect’s `Clock` when absent, which is the one place the fixture pipeline consults time at all — `buildCorpus` requires it, so a test pinning this value pins every stamp in the tree. ##### probes? [Section titled “probes?”](#probes-4) ```ts 1 readonly optional probes?: number; ``` ##### root? [Section titled “root?”](#root-1) ```ts 1 readonly optional root?: string; ``` Where to generate. A fresh temp directory when absent. ##### seed? [Section titled “seed?”](#seed-3) ```ts 1 readonly optional seed?: number; ``` ##### size? [Section titled “size?”](#size-1) ```ts 1 readonly optional size?: number; ``` *** ### FixtureTranscript [Section titled “FixtureTranscript”](#fixturetranscript) One fixture transcript, as the batch would offer it. #### Properties [Section titled “Properties”](#properties-17) ##### jsonl [Section titled “jsonl”](#jsonl) ```ts 1 readonly jsonl: string; ``` JSONL, one object per line, in the shape Claude Code writes under `~/.claude/projects`. ##### sessionId [Section titled “sessionId”](#sessionid) ```ts 1 readonly sessionId: string; ``` *** ### FloorReport [Section titled “FloorReport”](#floorreport) The lexical-floor scenario runs the same suite with no vector arm. Design §5 requires two things of it: no error, and the lexical arm still beating the controls on the probes that carry lexical signal. Only the SECOND half is a subset claim, and it has to be. A numeric-family control differs from its target by one token, which the FTS arm cannot order, so demanding the full gate without vectors would be demanding the vector arm be unnecessary. `lexicallyDiscriminated` is therefore the number reported and asserted, not `passed`. #### Properties [Section titled “Properties”](#properties-18) ##### allDegraded [Section titled “allDegraded”](#alldegraded) ```ts 1 readonly allDegraded: boolean; ``` True when every probe was ranked degraded, which asserts that the arm really is absent. ##### lexicallyDiscriminated [Section titled “lexicallyDiscriminated”](#lexicallydiscriminated) ```ts 1 readonly lexicallyDiscriminated: number; ``` Probes the lexical floor still discriminated. ##### probes [Section titled “probes”](#probes-5) ```ts 1 readonly probes: number; ``` ##### results [Section titled “results”](#results-2) ```ts 1 readonly results: readonly ProbeResult[]; ``` Every result, so a caller can see which families survive without vectors. *** ### FrozenDecision [Section titled “FrozenDecision”](#frozendecision) One frozen decision: enough to detect drift, nothing that needs a transcript to read. #### Properties [Section titled “Properties”](#properties-19) ##### accepted [Section titled “accepted”](#accepted-1) ```ts 1 readonly accepted: boolean; ``` ##### class [Section titled “class”](#class-2) ```ts 1 readonly class: string; ``` ##### id [Section titled “id”](#id) ```ts 1 readonly id: string; ``` ##### label [Section titled “label”](#label-2) ```ts 1 readonly label: "good" | "bad"; ``` ##### stage [Section titled “stage”](#stage) ```ts 1 readonly stage: GateStage; ``` *** ### GateBatch [Section titled “GateBatch”](#gatebatch) What one batch made readable: the transcripts on disk, keyed the way the door keys them. #### Properties [Section titled “Properties”](#properties-20) ##### reachable [Section titled “reachable”](#reachable) ```ts 1 readonly reachable: readonly ReachableTranscript[]; ``` *** ### GateDecision [Section titled “GateDecision”](#gatedecision) One candidate’s outcome. #### Properties [Section titled “Properties”](#properties-21) ##### accepted [Section titled “accepted”](#accepted-2) ```ts 1 readonly accepted: boolean; ``` ##### reason [Section titled “reason”](#reason-1) ```ts 1 readonly reason: string | null; ``` The production function’s own reason text, `null` when accepted. Truncated to one line. ##### stage [Section titled “stage”](#stage-1) ```ts 1 readonly stage: GateStage; ``` *** ### LabeledCandidate [Section titled “LabeledCandidate”](#labeledcandidate) One labeled candidate. `candidate` is `unknown` on purpose: a `bad` item may violate the type the door decodes (a bare string where an entity object belongs, a missing field), and a typed field would make those shapes unrepresentable. The gate’s first stage IS the decode, so the corpus has to be able to hand it something the type refuses. #### Properties [Section titled “Properties”](#properties-22) ##### candidate [Section titled “candidate”](#candidate) ```ts 1 readonly candidate: unknown; ``` ##### class [Section titled “class”](#class-3) ```ts 1 readonly class: string; ``` For a `bad` item, its failure class (what is wrong with it). For a `good` item, its shape (what the gate has to get RIGHT to accept it). The per-class breakdown groups on this. ##### id [Section titled “id”](#id-1) ```ts 1 readonly id: string; ``` Stable id, `g01`…`g31` for good, `b01`…`b31` for bad. The manifest keys on it. ##### label [Section titled “label”](#label-3) ```ts 1 readonly label: WritePathLabel; ``` ##### provenance [Section titled “provenance”](#provenance) ```ts 1 readonly provenance: string; ``` The rule that decides the label, with the file that holds it. *** ### LabeledDecision [Section titled “LabeledDecision”](#labeleddecision) One candidate’s decision beside its label. #### Properties [Section titled “Properties”](#properties-23) ##### accepted [Section titled “accepted”](#accepted-3) ```ts 1 readonly accepted: boolean; ``` ##### class [Section titled “class”](#class-4) ```ts 1 readonly class: string; ``` ##### id [Section titled “id”](#id-2) ```ts 1 readonly id: string; ``` ##### label [Section titled “label”](#label-4) ```ts 1 readonly label: WritePathLabel; ``` ##### reason [Section titled “reason”](#reason-2) ```ts 1 readonly reason: string | null; ``` ##### stage [Section titled “stage”](#stage-2) ```ts 1 readonly stage: GateStage; ``` *** ### LabeledGroup [Section titled “LabeledGroup”](#labeledgroup) One labeled group. #### Properties [Section titled “Properties”](#properties-24) ##### class [Section titled “class”](#class-5) ```ts 1 readonly class: string; ``` For a `merge` group, its shape (what the fold has to get RIGHT to accept it). For a `keep` group, its divergence (why the veto must refuse it). The per-class breakdown groups on this. ##### id [Section titled “id”](#id-3) ```ts 1 readonly id: string; ``` Stable id, `m01`… for merge, `k01`… for keep. Reports and gap sets key on it. ##### label [Section titled “label”](#label-5) ```ts 1 readonly label: AcceptanceLabel; ``` ##### members [Section titled “members”](#members) ```ts 1 readonly members: readonly AcceptanceMember[]; ``` Two or more members, oldest first. ##### provenance [Section titled “provenance”](#provenance-1) ```ts 1 readonly provenance: string; ``` The rule that decides the label, with the file that holds it. *** ### MemorySpec [Section titled “MemorySpec”](#memoryspec) One memory to write, before it becomes HTML. #### Properties [Section titled “Properties”](#properties-25) ##### archivedAt? [Section titled “archivedAt?”](#archivedat) ```ts 1 readonly optional archivedAt?: string; ``` Set on the archived members, which carry `memhtml-status: archived` and a `memhtml-archived` stamp. ##### body [Section titled “body”](#body-3) ```ts 1 readonly body: readonly string[]; ``` ##### claim [Section titled “claim”](#claim-2) ```ts 1 readonly claim: string; ``` The sentence the memory turns on. It becomes the `` span and therefore `files.gist`. ##### confidence [Section titled “confidence”](#confidence) ```ts 1 readonly confidence: number; ``` ##### createdAt [Section titled “createdAt”](#createdat) ```ts 1 readonly createdAt: string; ``` ##### entities [Section titled “entities”](#entities) ```ts 1 readonly entities: readonly string[]; ``` ##### extras [Section titled “extras”](#extras) ```ts 1 readonly extras: readonly string[]; ``` Extra article markup after the claim paragraph, the element kit this memory exercises. ##### importance [Section titled “importance”](#importance) ```ts 1 readonly importance: number; ``` ##### links [Section titled “links”](#links) ```ts 1 readonly links: readonly object[]; ``` ##### memoryType [Section titled “memoryType”](#memorytype) ```ts 1 readonly memoryType: string; ``` ##### path [Section titled “path”](#path-1) ```ts 1 readonly path: string; ``` ##### sessionId? [Section titled “sessionId?”](#sessionid-1) ```ts 1 readonly optional sessionId?: string; ``` ##### tags [Section titled “tags”](#tags) ```ts 1 readonly tags: readonly string[]; ``` ##### title [Section titled “title”](#title) ```ts 1 readonly title: string; ``` ##### updatedAt [Section titled “updatedAt”](#updatedat) ```ts 1 readonly updatedAt: string; ``` ##### validUntil? [Section titled “validUntil?”](#validuntil) ```ts 1 readonly optional validUntil?: string; ``` *** ### PairDecision [Section titled “PairDecision”](#pairdecision) One oriented pair’s decision beside its group’s label. #### Properties [Section titled “Properties”](#properties-26) ##### class [Section titled “class”](#class-6) ```ts 1 readonly class: string; ``` ##### dropPath [Section titled “dropPath”](#droppath) ```ts 1 readonly dropPath: string; ``` ##### folded [Section titled “folded”](#folded) ```ts 1 readonly folded: boolean; ``` ##### group [Section titled “group”](#group) ```ts 1 readonly group: string; ``` ##### id [Section titled “id”](#id-4) ```ts 1 readonly id: string; ``` `/`, stable across runs. ##### keepPath [Section titled “keepPath”](#keeppath) ```ts 1 readonly keepPath: string; ``` ##### label [Section titled “label”](#label-6) ```ts 1 readonly label: AcceptanceLabel; ``` ##### stage [Section titled “stage”](#stage-3) ```ts 1 readonly stage: AcceptanceStage; ``` *** ### Probe [Section titled “Probe”](#probe) One discrimination probe: a query, the memory that answers it, and the near-twins that do not. `controlPaths` are ordered by family so a failure names the axis. Each control is validated against its OWN family’s divergence predicate at generation time. See `controls.ts`. #### Properties [Section titled “Properties”](#properties-27) ##### controlPaths [Section titled “controlPaths”](#controlpaths) ```ts 1 readonly controlPaths: readonly string[]; ``` ##### families [Section titled “families”](#families) ```ts 1 readonly families: readonly DivergenceFamily[]; ``` ##### query [Section titled “query”](#query) ```ts 1 readonly query: string; ``` ##### targetPath [Section titled “targetPath”](#targetpath) ```ts 1 readonly targetPath: string; ``` *** ### ProbeResult [Section titled “ProbeResult”](#proberesult) One probe’s outcome. #### Properties [Section titled “Properties”](#properties-28) ##### controlRanks [Section titled “controlRanks”](#controlranks) ```ts 1 readonly controlRanks: readonly object[]; ``` Each control’s 1-based rank in the whole hit list, `null` when absent from the hits. ##### corpusReciprocalRank [Section titled “corpusReciprocalRank”](#corpusreciprocalrank) ```ts 1 readonly corpusReciprocalRank: number; ``` `1 / targetRank`, or `0` when the target was absent. The term of `corpusMrr`. ##### degraded [Section titled “degraded”](#degraded) ```ts 1 readonly degraded: boolean; ``` True when the vector arm did not fire for this probe. ##### discriminated [Section titled “discriminated”](#discriminated-2) ```ts 1 readonly discriminated: boolean; ``` True when the target strictly outranks EVERY control. A control the search never returned counts as outranked, since being absent is worse than being last. ##### discriminationRank [Section titled “discriminationRank”](#discriminationrank) ```ts 1 readonly discriminationRank: number; ``` 1-based rank of the target within `{target} ∪ controls` ALONE, the space the gate is stated in. `1` means the target beat every one of its own impostors. Never `null`, because the target is always a member of this set, so an absent target ranks last rather than nowhere. ##### query [Section titled “query”](#query-1) ```ts 1 readonly query: string; ``` ##### reciprocalRank [Section titled “reciprocalRank”](#reciprocalrank) ```ts 1 readonly reciprocalRank: number; ``` `1 / discriminationRank`. The term of [DiscriminationReport.mrr](/api/eval/#discriminationreport). ##### targetPath [Section titled “targetPath”](#targetpath-1) ```ts 1 readonly targetPath: string; ``` ##### targetRank [Section titled “targetRank”](#targetrank) ```ts 1 readonly targetRank: number | null; ``` 1-based rank of the target within the WHOLE returned hit list, or `null` when it was not returned. The scope is every active memory in the corpus. Compare against [discriminationRank](/api/eval/#proberesult), which measures the same position in a different space. *** ### ReplayDrift [Section titled “ReplayDrift”](#replaydrift) One decision that replayed differently from the frozen one. #### Properties [Section titled “Properties”](#properties-29) ##### fresh [Section titled “fresh”](#fresh) ```ts 1 readonly fresh: 2 | { 3 accepted: boolean; 4 stage: GateStage; 5 } 6 | null; ``` ##### frozen [Section titled “frozen”](#frozen) ```ts 1 readonly frozen: 2 | { 3 accepted: boolean; 4 stage: GateStage; 5 } 6 | null; ``` ##### id [Section titled “id”](#id-5) ```ts 1 readonly id: string; ``` *** ### StackOptions [Section titled “StackOptions”](#stackoptions) What [buildStack](/api/eval/#buildstack) takes beyond the fixture options. #### Extends [Section titled “Extends”](#extends-3) * [`FixtureOptions`](/api/eval/#fixtureoptions) #### Properties [Section titled “Properties”](#properties-30) ##### embedder? [Section titled “embedder?”](#embedder) ```ts 1 readonly optional embedder?: EvalEmbedder; ``` The embedder to measure through. `fakeEmbedder()` when absent. ##### now? [Section titled “now?”](#now-4) ```ts 1 readonly optional now?: number; ``` The run instant the corpus’s stamps are anchored behind, UTC millis. Effect’s `Clock` when absent, which is the one place the fixture pipeline consults time at all — `buildCorpus` requires it, so a test pinning this value pins every stamp in the tree. ###### Inherited from [Section titled “Inherited from”](#inherited-from-14) [`FixtureOptions`](/api/eval/#fixtureoptions).[`now`](/api/eval/#fixtureoptions) ##### probes? [Section titled “probes?”](#probes-6) ```ts 1 readonly optional probes?: number; ``` ###### Inherited from [Section titled “Inherited from”](#inherited-from-15) [`FixtureOptions`](/api/eval/#fixtureoptions).[`probes`](/api/eval/#fixtureoptions) ##### root? [Section titled “root?”](#root-2) ```ts 1 readonly optional root?: string; ``` Where to generate. A fresh temp directory when absent. ###### Inherited from [Section titled “Inherited from”](#inherited-from-16) [`FixtureOptions`](/api/eval/#fixtureoptions).[`root`](/api/eval/#fixtureoptions) ##### seed? [Section titled “seed?”](#seed-4) ```ts 1 readonly optional seed?: number; ``` ###### Inherited from [Section titled “Inherited from”](#inherited-from-17) [`FixtureOptions`](/api/eval/#fixtureoptions).[`seed`](/api/eval/#fixtureoptions) ##### size? [Section titled “size?”](#size-2) ```ts 1 readonly optional size?: number; ``` ###### Inherited from [Section titled “Inherited from”](#inherited-from-18) [`FixtureOptions`](/api/eval/#fixtureoptions).[`size`](/api/eval/#fixtureoptions) *** ### VariantOptions [Section titled “VariantOptions”](#variantoptions) What a variant-family derivation needs beyond the target’s own text. #### Properties [Section titled “Properties”](#properties-31) ##### anchor [Section titled “anchor”](#anchor) ```ts 1 readonly anchor: string; ``` The token the qualifier is inserted after, a product or host name in the claim. ##### qualifier [Section titled “qualifier”](#qualifier) ```ts 1 readonly qualifier: string; ``` A `VARIANT_QUALIFIERS` member, such as `pro`, `beta`, or `legacy`. *** ### WritePathManifest [Section titled “WritePathManifest”](#writepathmanifest) The freeze/replay manifest. #### Properties [Section titled “Properties”](#properties-32) ##### corpusSha256 [Section titled “corpusSha256”](#corpussha256) ```ts 1 readonly corpusSha256: string; ``` sha256 of the corpus as JSON, so a changed candidate is attributable. ##### decisions [Section titled “decisions”](#decisions-1) ```ts 1 readonly decisions: readonly FrozenDecision[]; ``` ##### f1Floor [Section titled “f1Floor”](#f1floor) ```ts 1 readonly f1Floor: number; ``` ##### frozenAt [Section titled “frozenAt”](#frozenat) ```ts 1 readonly frozenAt: string; ``` When the freeze ran, ISO-8601. Informational; not compared on replay. ##### gate [Section titled “gate”](#gate) ```ts 1 readonly gate: readonly object[]; ``` The stages, in order, and the production functions each one calls. ##### memhtmlVersion [Section titled “memhtmlVersion”](#memhtmlversion) ```ts 1 readonly memhtmlVersion: string; ``` The memhtml workspace version the decisions were frozen at. ##### metrics [Section titled “metrics”](#metrics) ```ts 1 readonly metrics: object; ``` ###### accuracy [Section titled “accuracy”](#accuracy) ```ts 1 readonly accuracy: number; ``` ###### confusion [Section titled “confusion”](#confusion) ```ts 1 readonly confusion: ConfusionMatrix; ``` ###### f1 [Section titled “f1”](#f1-1) ```ts 1 readonly f1: number; ``` ###### items [Section titled “items”](#items-1) ```ts 1 readonly items: number; ``` ###### precision [Section titled “precision”](#precision-1) ```ts 1 readonly precision: number; ``` ###### recall [Section titled “recall”](#recall-1) ```ts 1 readonly recall: number; ``` ##### schemaVersion [Section titled “schemaVersion”](#schemaversion) ```ts 1 readonly schemaVersion: number; ``` ##### transcriptsSha256 [Section titled “transcriptsSha256”](#transcriptssha256) ```ts 1 readonly transcriptsSha256: string; ``` sha256 of the two transcripts’ JSONL, so a changed fixture line is attributable. *** ### WritePathOptions [Section titled “WritePathOptions”](#writepathoptions) What [runWritePathDiscrimination](/api/eval/#runwritepathdiscrimination) takes. #### Properties [Section titled “Properties”](#properties-33) ##### corpus? [Section titled “corpus?”](#corpus-1) ```ts 1 readonly optional corpus?: readonly LabeledCandidate[]; ``` ##### f1Floor? [Section titled “f1Floor?”](#f1floor-1) ```ts 1 readonly optional f1Floor?: number; ``` ##### transcripts? [Section titled “transcripts?”](#transcripts) ```ts 1 readonly optional transcripts?: readonly FixtureTranscript[]; ``` *** ### WritePathReport [Section titled “WritePathReport”](#writepathreport) The report the gate reads. This is the `eval.write-path` payload shape. #### Properties [Section titled “Properties”](#properties-34) ##### accuracy [Section titled “accuracy”](#accuracy-1) ```ts 1 readonly accuracy: number; ``` `(badRejected + goodAccepted) / items`, four decimals. ##### bad [Section titled “bad”](#bad) ```ts 1 readonly bad: number; ``` ##### confusion [Section titled “confusion”](#confusion-1) ```ts 1 readonly confusion: ConfusionMatrix; ``` ##### decisions [Section titled “decisions”](#decisions-2) ```ts 1 readonly decisions: readonly LabeledDecision[]; ``` ##### f1 [Section titled “f1”](#f1-2) ```ts 1 readonly f1: number; ``` Harmonic mean of the two, four decimals; `0` when either is `0`. ##### f1Floor [Section titled “f1Floor”](#f1floor-2) ```ts 1 readonly f1Floor: number; ``` ##### good [Section titled “good”](#good) ```ts 1 readonly good: number; ``` ##### items [Section titled “items”](#items-2) ```ts 1 readonly items: number; ``` ##### passed [Section titled “passed”](#passed-3) ```ts 1 readonly passed: boolean; ``` True when the corpus is non-empty AND carries both labels AND `f1 >= f1Floor`. An empty corpus, or one with a single label, is a FAILURE rather than a vacuous pass: a mean over nothing is not a measurement, and a gate scored only on good items (or only on bad) cannot have discriminated anything. ##### perClass [Section titled “perClass”](#perclass-1) ```ts 1 readonly perClass: readonly ClassBreakdown[]; ``` ##### precision [Section titled “precision”](#precision-2) ```ts 1 readonly precision: number; ``` `badRejected / (badRejected + goodRejected)`, four decimals; `0` when nothing was rejected. ##### recall [Section titled “recall”](#recall-2) ```ts 1 readonly recall: number; ``` `badRejected / (badRejected + badAccepted)`, four decimals; `0` when nothing was bad. ## Type Aliases [Section titled “Type Aliases”](#type-aliases) ### AcceptanceLabel [Section titled “AcceptanceLabel”](#acceptancelabel) ```ts 1 type AcceptanceLabel = "merge" | "keep"; ``` Which way the label points. `merge`: every member folds into the oldest. `keep`: none does. *** ### AcceptanceStage [Section titled “AcceptanceStage”](#acceptancestage) ```ts 1 type AcceptanceStage = 2 | "threshold" 3 | "veto:negation" 4 | "veto:numeric" 5 | "veto:variant" 6 | "self" 7 | "role-guard" 8 | "cap" 9 | "folded"; ``` Where a pair’s fate was decided. `folded` is the outcome the corpus’s `merge` label asks for. *** ### DivergenceFamily [Section titled “DivergenceFamily”](#divergencefamily) ```ts 1 type DivergenceFamily = "negation" | "numeric" | "variant"; ``` Which divergence a control embodies. Named on the probe, so a failure says which axis broke. *** ### EvalMode [Section titled “EvalMode”](#evalmode) ```ts 1 type EvalMode = "live" | "fake"; ``` Which embedder produced the numbers. Recorded on the report so a pass is never ambiguous. *** ### GateStage [Section titled “GateStage”](#gatestage) ```ts 1 type GateStage = 2 | "door:schema" 3 | "door:grounding" 4 | "door:quote" 5 | "phase:refusal" 6 | "accepted"; ``` Where a candidate’s fate was decided. `accepted` is the fifth outcome: it cleared all four. *** ### WritePathLabel [Section titled “WritePathLabel”](#writepathlabel) ```ts 1 type WritePathLabel = "good" | "bad"; ``` Which way the label points. ## Variables [Section titled “Variables”](#variables) ### ACCEPTANCE\_BASELINE [Section titled “ACCEPTANCE\_BASELINE”](#acceptance_baseline) ```ts 1 const ACCEPTANCE_BASELINE: 1 = 1; ``` The measured baseline this floor was derived from, so the derivation is checkable. Measured 2026-09-11 at memhtml 0.14.0 on corpus v2 (12 merge groups implying 14 pairs, 7 keep groups implying 7 pairs): acceptance 1.0 (14 of 14), precision 1.0 (no keep pair folded). Corpus v1 under the rules before this one measured 0.6667 (8 of 12): three `incidental-number` pairs stopped at `veto:numeric` because the numeric predicate read the whole article, and one `group-fanout` pair stopped at `role-guard` because the guard claimed the keeper after the first fold. Both classes now fold, and the known-gap set is empty. *** ### ACCEPTANCE\_CORPUS [Section titled “ACCEPTANCE\_CORPUS”](#acceptance_corpus) ```ts 1 const ACCEPTANCE_CORPUS: ReadonlyArray; ``` *** ### ACCEPTANCE\_FLOOR [Section titled “ACCEPTANCE\_FLOOR”](#acceptance_floor) ```ts 1 const ACCEPTANCE_FLOOR: number; ``` Acceptance floor: the baseline minus five points, floored to two decimals — the discrimination arm’s derivation, for the same reason. The floor is the alarm for a class-sized regression (a predicate that starts firing on a class it did not), and the known-gap set is the alarm for a one-pair one. A floor at the baseline would fire on every deliberate corpus edit. *** ### ACCEPTANCE\_KNOWN\_GAP\_CLASSES [Section titled “ACCEPTANCE\_KNOWN\_GAP\_CLASSES”](#acceptance_known_gap_classes) ```ts 1 const ACCEPTANCE_KNOWN_GAP_CLASSES: ReadonlyArray = []; ``` The classes whose label the fold path is KNOWN to disagree with, stated so the tier fails if the set moves in either direction. Empty as of the rules measured 2026-09-11: every `merge` pair folds and every `keep` pair is refused. Two classes used to be here. `incidental-number` (the same claim, one side carrying a date, a ticket id, or an exit code the other lacks) stopped at `veto:numeric` while `numericTokenDivergent` read the whole `gist\nbody_text`; it reads the gist now. `group-fanout` (a group of three implying two pairs with one keeper) stopped its second pair at `role-guard` while the guard claimed both roles of a committed pair; it admits a repeated keeper now. *** ### BATCH\_SESSION\_IDS [Section titled “BATCH\_SESSION\_IDS”](#batch_session_ids) ```ts 1 const BATCH_SESSION_IDS: ReadonlyArray; ``` The batch’s session ids: what the door treats as READABLE. *** ### BEDROCK\_TOKEN\_VAR [Section titled “BEDROCK\_TOKEN\_VAR”](#bedrock_token_var) ```ts 1 const BEDROCK_TOKEN_VAR: "AWS_BEARER_TOKEN_BEDROCK" = "AWS_BEARER_TOKEN_BEDROCK"; ``` The variable whose presence decides whether live mode can run. *** ### CEILINGS [Section titled “CEILINGS”](#ceilings) ```ts 1 const CEILINGS: object; ``` The ceilings, restated from `apps/consolidator/src/contract.ts` so the corpus reads on its own. #### Type Declaration [Section titled “Type Declaration”](#type-declaration) ##### claim [Section titled “claim”](#claim-3) ```ts 1 readonly claim: 300 = 300; ``` ##### entities [Section titled “entities”](#entities-1) ```ts 1 readonly entities: 64 = 64; ``` ##### evidence [Section titled “evidence”](#evidence) ```ts 1 readonly evidence: 32 = 32; ``` ##### gist [Section titled “gist”](#gist-1) ```ts 1 readonly gist: 1500 = 1500; ``` ##### quote [Section titled “quote”](#quote) ```ts 1 readonly quote: 600 = 600; ``` *** ### DEFAULT\_CORPUS\_SIZE [Section titled “DEFAULT\_CORPUS\_SIZE”](#default_corpus_size) ```ts 1 const DEFAULT_CORPUS_SIZE: 200 = 200; ``` How many base memories a default corpus carries, before controls and the archive tier. *** ### DEFAULT\_PROBE\_COUNT [Section titled “DEFAULT\_PROBE\_COUNT”](#default_probe_count) ```ts 1 const DEFAULT_PROBE_COUNT: 36 = 36; ``` How many probes a default corpus carries. Design §5 requires at least 30. *** ### DEFAULT\_SEED [Section titled “DEFAULT\_SEED”](#default_seed) ```ts 1 const DEFAULT_SEED: 20260802 = 20_260_802; ``` The default seed. Named so a caller changing it is making a visible choice. *** ### DIVERGENCE\_FAMILIES [Section titled “DIVERGENCE\_FAMILIES”](#divergence_families) ```ts 1 const DIVERGENCE_FAMILIES: ReadonlyArray; ``` *** ### FAKE\_DIM [Section titled “FAKE\_DIM”](#fake_dim) ```ts 1 const FAKE_DIM: 1024 = EMBED_DIM; ``` The vector width the fake produces. It is the real one, so a width check cannot pass by luck. *** ### GATE\_SOURCES [Section titled “GATE\_SOURCES”](#gate_sources) ```ts 1 const GATE_SOURCES: ReadonlyArray<{ 2 source: string; 3 stage: GateStage; 4 }>; ``` The gate’s stage table as the manifest records it. One place, so the manifest cannot drift. *** ### GATE\_STAGES [Section titled “GATE\_STAGES”](#gate_stages) ```ts 1 const GATE_STAGES: ReadonlyArray; ``` *** ### KNOWN\_GAP\_CLASSES [Section titled “KNOWN\_GAP\_CLASSES”](#known_gap_classes) ```ts 1 const KNOWN_GAP_CLASSES: ReadonlyArray; ``` The failure classes the gate is KNOWN not to cover, as of the frozen manifest. Stated in code so the eval can assert the gap is exactly this and no wider: a class that starts failing here is a regression, and a class that starts passing is a change the manifest should record. Three classes: * `restatement/*`: the TRACE-2 bar is checked as a COUNT of quotes, and nothing dedupes them, so the same line twice clears it. * `content-leak/*`: nothing compares a claim against its own evidence, so a claim that is a verbatim transcript span is written into the corpus as prose. * `shape/padded-session-id`: the door’s `ungroundedEvidenceReason` compares ids untrimmed while the phase trims them, so a real quote from a real session is refused for its whitespace. A GOOD item the gate rejects, which is the one false rejection on this corpus. *** ### MANIFEST\_FILENAME [Section titled “MANIFEST\_FILENAME”](#manifest_filename) ```ts 1 const MANIFEST_FILENAME: "write-path-discrimination.manifest.json" = "write-path-discrimination.manifest.json"; ``` The frozen manifest’s file name, under `packages/eval/fixtures/`. The PATH is the caller’s to resolve: the eval tier resolves it from its own location and `scripts/freeze-write-path.mjs` from its own. This module deliberately holds no module-relative URL resolution, because `@memhtml/eval` is bundled into the published binary and the packaging census (`tests-integration/tests/packaging.test.ts`) requires every run-time asset resolution in shipped source to be a declared, shipped asset. The manifest is a repo fixture, not a shipped one: no command reads it, so declaring it would ship a file nothing in the package uses. *** ### MANIFEST\_SCHEMA\_VERSION [Section titled “MANIFEST\_SCHEMA\_VERSION”](#manifest_schema_version) ```ts 1 const MANIFEST_SCHEMA_VERSION: 1 = 1; ``` *** ### MANY\_SPANS [Section titled “MANY\_SPANS”](#many_spans) ```ts 1 const MANY_SPANS: ReadonlyArray<{ 2 quote: string; 3 sessionId: string; 4 }>; ``` Distinct verbatim spans, enough for the evidence-count ceiling cases. Twenty-six named spans plus windows cut from the long line. Every entry is a substring of its transcript, and the corpus test asserts that for each one. *** ### MRR\_FLOOR [Section titled “MRR\_FLOOR”](#mrr_floor) ```ts 1 const MRR_FLOOR: 0.85 = 0.85; ``` The MRR floor, from design §5. A gate below this admits a target that loses to one of its own negation-flipped twins on one probe in seven. An agent cannot trust that retrieval layer to answer with the right fact. *** ### PROBE\_LIMIT [Section titled “PROBE\_LIMIT”](#probe_limit) ```ts 1 const PROBE_LIMIT: 40 = 40; ``` How many hits each probe requests. Wide enough that a control’s rank is observable rather than truncated into `null`. A window of 10 would report an inversion and a merely-narrow miss identically, and the two need different fixes. The gate itself compares ranks, so widening the window can only make it stricter. *** ### SESSION\_A [Section titled “SESSION\_A”](#session_a) ```ts 1 const SESSION_A: "session-a" = "session-a"; ``` *** ### SESSION\_B [Section titled “SESSION\_B”](#session_b) ```ts 1 const SESSION_B: "session-b" = "session-b"; ``` *** ### SESSION\_OUTSIDE\_BATCH [Section titled “SESSION\_OUTSIDE\_BATCH”](#session_outside_batch) ```ts 1 const SESSION_OUTSIDE_BATCH: "session-z" = "session-z"; ``` A session id no transcript in the batch carries. *** ### TRANSCRIPT\_A [Section titled “TRANSCRIPT\_A”](#transcript_a) ```ts 1 const TRANSCRIPT_A: FixtureTranscript; ``` *** ### TRANSCRIPT\_B [Section titled “TRANSCRIPT\_B”](#transcript_b) ```ts 1 const TRANSCRIPT_B: FixtureTranscript; ``` *** ### TRANSCRIPTS [Section titled “TRANSCRIPTS”](#transcripts-1) ```ts 1 const TRANSCRIPTS: ReadonlyArray; ``` *** ### WRITE\_PATH\_BASELINE\_F1 [Section titled “WRITE\_PATH\_BASELINE\_F1”](#write_path_baseline_f1) ```ts 1 const WRITE_PATH_BASELINE_F1: 0.9333 = 0.9333; ``` The measured baseline this floor was derived from. Recorded so the derivation is checkable. *** ### WRITE\_PATH\_CORPUS [Section titled “WRITE\_PATH\_CORPUS”](#write_path_corpus) ```ts 1 const WRITE_PATH_CORPUS: ReadonlyArray; ``` The whole labeled corpus: 31 good, 31 bad, in id order. *** ### WRITE\_PATH\_F1\_FLOOR [Section titled “WRITE\_PATH\_F1\_FLOOR”](#write_path_f1_floor) ```ts 1 const WRITE_PATH_F1_FLOOR: number; ``` F1 floor: the baseline minus five points, rounded down to two decimals. Five points and not zero, because the floor is the alarm for a class-sized regression and the manifest is the alarm for a one-item one; a floor pinned at the baseline would fire on every deliberate corpus edit and train the reader to refreeze without looking. ## Functions [Section titled “Functions”](#functions) ### acceptanceAgrees() [Section titled “acceptanceAgrees()”](#acceptanceagrees) ```ts 1 function acceptanceAgrees(decision): boolean; ``` True when the decision matches the label. #### Parameters [Section titled “Parameters”](#parameters-3) ##### decision [Section titled “decision”](#decision) [`PairDecision`](/api/eval/#pairdecision) #### Returns [Section titled “Returns”](#returns-7) `boolean` *** ### acceptanceByClass() [Section titled “acceptanceByClass()”](#acceptancebyclass) ```ts 1 function acceptanceByClass(decisions): readonly AcceptanceClassBreakdown[]; ``` Group by class, in first-seen order. #### Parameters [Section titled “Parameters”](#parameters-4) ##### decisions [Section titled “decisions”](#decisions-3) readonly [`PairDecision`](/api/eval/#pairdecision)\[] #### Returns [Section titled “Returns”](#returns-8) readonly [`AcceptanceClassBreakdown`](/api/eval/#acceptanceclassbreakdown)\[] *** ### admitCandidate() [Section titled “admitCandidate()”](#admitcandidate) ```ts 1 function admitCandidate(raw, batch): Effect; ``` Run one candidate through the gate. Never fails: a decision is the answer, and the only I/O (`fabricatedQuoteReason` re-reading a transcript) already folds an unreadable file into a refusal rather than an error. #### Parameters [Section titled “Parameters”](#parameters-5) ##### raw [Section titled “raw”](#raw) `unknown` ##### batch [Section titled “batch”](#batch) [`GateBatch`](/api/eval/#gatebatch) #### Returns [Section titled “Returns”](#returns-9) `Effect`<[`GateDecision`](/api/eval/#gatedecision)> *** ### agrees() [Section titled “agrees()”](#agrees) ```ts 1 function agrees(decision): boolean; ``` True when the decision matches the label. #### Parameters [Section titled “Parameters”](#parameters-6) ##### decision [Section titled “decision”](#decision-1) [`LabeledDecision`](/api/eval/#labeleddecision) #### Returns [Section titled “Returns”](#returns-10) `boolean` *** ### articleFor() [Section titled “articleFor()”](#articlefor) ```ts 1 function articleFor(spec): string; ``` The article markup for a spec: the claim paragraph, the first body paragraph joined onto it, then the remaining paragraphs and the element kit. Written here rather than left to `renderTemplate`’s claim/body path because the kits carry real markup, and `renderTemplate` escapes its `body` strings as text. That is correct for a tool parameter and wrong for a `
`. #### Parameters [Section titled “Parameters”](#parameters-7) ##### spec [Section titled “spec”](#spec-1) [`MemorySpec`](/api/eval/#memoryspec) #### Returns [Section titled “Returns”](#returns-11) `string` *** ### breakdownByClass() [Section titled “breakdownByClass()”](#breakdownbyclass) ```ts 1 function breakdownByClass(decisions): readonly ClassBreakdown[]; ``` Group by class, in first-seen order, so the breakdown reads in corpus order. #### Parameters [Section titled “Parameters”](#parameters-8) ##### decisions [Section titled “decisions”](#decisions-4) readonly [`LabeledDecision`](/api/eval/#labeleddecision)\[] #### Returns [Section titled “Returns”](#returns-12) readonly [`ClassBreakdown`](/api/eval/#classbreakdown)\[] *** ### buildCorpus() [Section titled “buildCorpus()”](#buildcorpus) ```ts 1 function buildCorpus(options): CorpusSpec; ``` The whole fixture specification. The order is fixed as people, base, archive tier, then controls, so the generated tree is written in a stable order and two runs at one `(seed, now)` produce byte-identical files. `now` is REQUIRED rather than defaulted from a clock, because this module’s contract is purity: every caller threads its own run instant (the harness reads Effect’s `Clock` once), it is quantized to the UTC day, and it is recorded on the spec, so a failing run is reproducible by passing the reported `now` back in. #### Parameters [Section titled “Parameters”](#parameters-9) ##### options [Section titled “options”](#options) ###### now [Section titled “now”](#now-5) `number` The run instant to anchor stamps behind, UTC millis. ###### probes? [Section titled “probes?”](#probes-7) `number` ###### seed? [Section titled “seed?”](#seed-5) `number` ###### size? [Section titled “size?”](#size-3) `number` #### Returns [Section titled “Returns”](#returns-13) [`CorpusSpec`](/api/eval/#corpusspec) *** ### buildStack() [Section titled “buildStack()”](#buildstack) ```ts 1 function buildStack(options?): Effect; ``` Build the whole stack inside a scope: generate the corpus, index it, and return retrieval over it. `Effect.acquireRelease` owns the database, so the caller’s `Effect.scoped` closes the connection, and the fixture’s own `cleanup` removes the temp tree. Both matter in a CLI, because a command that leaked a database handle would keep a WAL file alive under `/tmp` for the life of the process. #### Parameters [Section titled “Parameters”](#parameters-10) ##### options? [Section titled “options?”](#options-1) [`StackOptions`](/api/eval/#stackoptions) = `{}` #### Returns [Section titled “Returns”](#returns-14) `Effect`<[`EvalStack`](/api/eval/#evalstack), `never`, `Scope`> *** ### confusionOf() [Section titled “confusionOf()”](#confusionof) ```ts 1 function confusionOf(decisions): ConfusionMatrix; ``` The four cells from the decisions. #### Parameters [Section titled “Parameters”](#parameters-11) ##### decisions [Section titled “decisions”](#decisions-5) readonly [`LabeledDecision`](/api/eval/#labeleddecision)\[] #### Returns [Section titled “Returns”](#returns-15) [`ConfusionMatrix`](/api/eval/#confusionmatrix) *** ### corpusSha256() [Section titled “corpusSha256()”](#corpussha256-1) ```ts 1 function corpusSha256(corpus?): string; ``` The corpus hash. Plain `JSON.stringify` over static data with a fixed key order. #### Parameters [Section titled “Parameters”](#parameters-12) ##### corpus? [Section titled “corpus?”](#corpus-2) readonly [`LabeledCandidate`](/api/eval/#labeledcandidate)\[] = `WRITE_PATH_CORPUS` #### Returns [Section titled “Returns”](#returns-16) `string` *** ### decideAcceptance() [Section titled “decideAcceptance()”](#decideacceptance) ```ts 1 function decideAcceptance(corpus?): readonly PairDecision[]; ``` Run the corpus through the composed fold path. Pure. #### Parameters [Section titled “Parameters”](#parameters-13) ##### corpus? [Section titled “corpus?”](#corpus-3) readonly [`LabeledGroup`](/api/eval/#labeledgroup)\[] = `ACCEPTANCE_CORPUS` #### Returns [Section titled “Returns”](#returns-17) readonly [`PairDecision`](/api/eval/#pairdecision)\[] *** ### decideAll() [Section titled “decideAll()”](#decideall) ```ts 1 function decideAll(batch, corpus?): Effect; ``` Run every labeled candidate through the gate, in corpus order. #### Parameters [Section titled “Parameters”](#parameters-14) ##### batch [Section titled “batch”](#batch-1) [`GateBatch`](/api/eval/#gatebatch) ##### corpus? [Section titled “corpus?”](#corpus-4) readonly [`LabeledCandidate`](/api/eval/#labeledcandidate)\[] = `WRITE_PATH_CORPUS` #### Returns [Section titled “Returns”](#returns-18) `Effect`\ *** ### deriveControl() [Section titled “deriveControl()”](#derivecontrol) ```ts 1 function deriveControl( 2 target, 3 family, 4 options? 5 ): DerivedControl | undefined; ``` Derive one control from a target, or refuse. Refusal, returned as `undefined`, is the behavior callers depend on. It has two causes, and each one would otherwise produce a control that LOOKS adversarial and is not: 1. The family does not apply (no number to flip). 2. The pair fails the family’s own predicate. For `negation` that means the target body already carried a marker, so the flip is invisible to the guard that defines the family. A caller that ignores the refusal and ships the pair anyway gets a probe whose control is a paraphrase, and a gate built on paraphrases passes no matter how badly retrieval discriminates. #### Parameters [Section titled “Parameters”](#parameters-15) ##### target [Section titled “target”](#target) [`ClaimText`](/api/eval/#claimtext) ##### family [Section titled “family”](#family-1) [`DivergenceFamily`](/api/eval/#divergencefamily) ##### options? [Section titled “options?”](#options-2) [`VariantOptions`](/api/eval/#variantoptions) #### Returns [Section titled “Returns”](#returns-19) [`DerivedControl`](/api/eval/#derivedcontrol) | `undefined` *** ### describeAcceptance() [Section titled “describeAcceptance()”](#describeacceptance) ```ts 1 function describeAcceptance(report): string; ``` A one-line summary: the numbers, then the first disagreement. #### Parameters [Section titled “Parameters”](#parameters-16) ##### report [Section titled “report”](#report) [`AcceptanceReport`](/api/eval/#acceptancereport) #### Returns [Section titled “Returns”](#returns-20) `string` *** ### describeFailure() [Section titled “describeFailure()”](#describefailure) ```ts 1 function describeFailure(report): string; ``` A one-line summary of a failure, for stderr and for the sleep merge’s refusal log. Names the first inversion rather than every one, because an operator needs a probe to reproduce and a thirty-line dump of a failing gate is a thirty-line dump nobody reads. The full list is on the report. #### Parameters [Section titled “Parameters”](#parameters-17) ##### report [Section titled “report”](#report-1) [`DiscriminationReport`](/api/eval/#discriminationreport) #### Returns [Section titled “Returns”](#returns-21) `string` *** ### describeWritePath() [Section titled “describeWritePath()”](#describewritepath) ```ts 1 function describeWritePath(report): string; ``` A one-line summary for stderr and a report subject: the four numbers, then the first disagreement. #### Parameters [Section titled “Parameters”](#parameters-18) ##### report [Section titled “report”](#report-2) [`WritePathReport`](/api/eval/#writepathreport) #### Returns [Section titled “Returns”](#returns-22) `string` *** ### discriminate() [Section titled “discriminate()”](#discriminate) ```ts 1 function discriminate( 2 retrieval, 3 probes, 4 options 5 ): Effect; ``` Run the suite and summarize it in one call. #### Parameters [Section titled “Parameters”](#parameters-19) ##### retrieval [Section titled “retrieval”](#retrieval-1) `RetrievalShape` ##### probes [Section titled “probes”](#probes-8) readonly [`Probe`](/api/eval/#probe)\[] ##### options [Section titled “options”](#options-3) ###### limit? [Section titled “limit?”](#limit) `number` ###### mode [Section titled “mode”](#mode-3) [`EvalMode`](/api/eval/#evalmode) ###### mrrFloor? [Section titled “mrrFloor?”](#mrrfloor-3) `number` #### Returns [Section titled “Returns”](#returns-23) `Effect`<[`DiscriminationReport`](/api/eval/#discriminationreport), `StorageFailure`> *** ### discriminationGate() [Section titled “discriminationGate()”](#discriminationgate) ```ts 1 function discriminationGate(options?): Effect; ``` The gate, for `MergeOptions.preMergeGate`. A failing gate FAILS this effect, which is what `@memhtml/sleep`’s `merge` reads to refuse. It wraps the gate in `Effect.result` and turns a failure into `refusal: "gate-failed"` with `main` never moving. The shape matters, because a version returning a boolean would let a caller forget to check it, and a refusable gate must not be optional at its call site. #### Parameters [Section titled “Parameters”](#parameters-20) ##### options? [Section titled “options?”](#options-4) [`EvalOptions`](/api/eval/#evaloptions) = `{}` #### Returns [Section titled “Returns”](#returns-24) `Effect`<[`EvalOutcome`](/api/eval/#evaloutcome), [`DiscriminationFailed`](/api/eval/#discriminationfailed), `never`> *** ### failingEmbedder() [Section titled “failingEmbedder()”](#failingembedder) ```ts 1 function failingEmbedder(): EvalEmbedder; ``` An embedder that always fails, for the lexical-floor scenario. The failure travels through the ERROR channel as a typed `ModelUnavailable` rather than a throw. The floor only holds if retrieval can catch it, and a defect would kill the fiber instead of narrowing the search. #### Returns [Section titled “Returns”](#returns-25) [`EvalEmbedder`](/api/eval/#evalembedder) *** ### fakeEmbedder() [Section titled “fakeEmbedder()”](#fakeembedder) ```ts 1 function fakeEmbedder(): EvalEmbedder; ``` #### Returns [Section titled “Returns”](#returns-26) [`EvalEmbedder`](/api/eval/#evalembedder) *** ### fakeVector() [Section titled “fakeVector()”](#fakevector) ```ts 1 function fakeVector(text): Float32Array; ``` The deterministic embedder, built as a hash-seeded bag of words, L2-normalized. The same construction `@memhtml/index` and `@memhtml/sleep` use in their own harnesses. Two texts sharing vocabulary have a genuinely high cosine and two disjoint texts a low one, which makes a negation-flipped control a real adversary here instead of a random vector the arm trivially separates. A random fake would make the gate meaningless in the easy direction and a constant fake in the hard one. #### Parameters [Section titled “Parameters”](#parameters-21) ##### text [Section titled “text”](#text-1) `string` #### Returns [Section titled “Returns”](#returns-27) `Float32Array` *** ### familyPredicate() [Section titled “familyPredicate()”](#familypredicate) ```ts 1 function familyPredicate(family): (textA, textB) => boolean; ``` The predicate a family’s control must satisfy against its target. #### Parameters [Section titled “Parameters”](#parameters-22) ##### family [Section titled “family”](#family-2) [`DivergenceFamily`](/api/eval/#divergencefamily) #### Returns [Section titled “Returns”](#returns-28) (`textA`, `textB`) => `boolean` *** ### hasBedrockCredentials() [Section titled “hasBedrockCredentials()”](#hasbedrockcredentials) ```ts 1 function hasBedrockCredentials(env?): boolean; ``` True when a Bedrock bearer token is present in the environment. #### Parameters [Section titled “Parameters”](#parameters-23) ##### env? [Section titled “env?”](#env-1) `Readonly`<`Record`<`string`, `string` | `undefined`>> = `process.env` #### Returns [Section titled “Returns”](#returns-29) `boolean` *** ### impliedPairs() [Section titled “impliedPairs()”](#impliedpairs) ```ts 1 function impliedPairs(corpus?): readonly object[]; ``` The oriented pairs the corpus implies, in group order, exactly as the phase would hand them to `mergeCandidates`: `groupPairsFor` over each group, with corpus order as the age order and `dedupMergeTextFor` as the veto’s texts. `simFor` is empty because no pair here was mined, so every similarity is the recall floor — the phase’s own value for a group pair it never scored. A path named by two groups takes its age from its FIRST appearance, the way one corpus row has one offset however many groups a model puts it in. The labeled corpus never repeats a path; the role-guard cases the tests build do, and a last-wins map would re-age a keeper under them. #### Parameters [Section titled “Parameters”](#parameters-24) ##### corpus? [Section titled “corpus?”](#corpus-5) readonly [`LabeledGroup`](/api/eval/#labeledgroup)\[] = `ACCEPTANCE_CORPUS` #### Returns [Section titled “Returns”](#returns-30) readonly `object`\[] *** ### liveEmbedder() [Section titled “liveEmbedder()”](#liveembedder) ```ts 1 function liveEmbedder(): Effect; ``` The real Bedrock embedder, for `live` mode. Built only when a caller asks for it. #### Returns [Section titled “Returns”](#returns-31) `Effect`<[`EvalEmbedder`](/api/eval/#evalembedder)> *** ### makeFixtureCorpus() [Section titled “makeFixtureCorpus()”](#makefixturecorpus) ```ts 1 function makeFixtureCorpus(options?): Effect; ``` A scaffolded memory repo carrying a generated corpus. `initRepo` is the real one, so the fixture carries the real `.gitignore`, the real `.gitattributes`, and the real `merge.ours.driver` config. That config is per-clone, and the `merge=ours` attribute does nothing without it. #### Parameters [Section titled “Parameters”](#parameters-25) ##### options? [Section titled “options?”](#options-5) [`FixtureOptions`](/api/eval/#fixtureoptions) = `{}` #### Returns [Section titled “Returns”](#returns-32) `Effect`<[`FixtureCorpus`](/api/eval/#fixturecorpus)> *** ### manifestFor() [Section titled “manifestFor()”](#manifestfor) ```ts 1 function manifestFor(report, context): WritePathManifest; ``` Build the manifest a run would freeze. #### Parameters [Section titled “Parameters”](#parameters-26) ##### report [Section titled “report”](#report-3) [`WritePathReport`](/api/eval/#writepathreport) ##### context [Section titled “context”](#context) ###### frozenAt [Section titled “frozenAt”](#frozenat-1) `string` ###### memhtmlVersion [Section titled “memhtmlVersion”](#memhtmlversion-1) `string` #### Returns [Section titled “Returns”](#returns-33) [`WritePathManifest`](/api/eval/#writepathmanifest) *** ### memoryFileFor() [Section titled “memoryFileFor()”](#memoryfilefor) ```ts 1 function memoryFileFor(spec): string; ``` One spec as a memory file’s bytes. Hand-assembled rather than routed through `@memhtml/html`’s `renderTemplate`, because of the element kits. `renderTemplate` escapes each `body` string as TEXT, which is right for an agent’s tool parameter and wrong for a `
` the fixture means as markup. The head is written in `META_ORDER` so a generated file is byte-identical to what the serializer would emit for the same metadata. The rest of the system then treats the fixture as an ordinary document. #### Parameters [Section titled “Parameters”](#parameters-27) ##### spec [Section titled “spec”](#spec-2) [`MemorySpec`](/api/eval/#memoryspec) #### Returns [Section titled “Returns”](#returns-34) `string` *** ### negationFlip() [Section titled “negationFlip()”](#negationflip) ```ts 1 function negationFlip(claim): string; ``` The affirmative claim as its negation. Total. When no verb anchor is present the whole sentence is wrapped rather than left unchanged. A transform that returned its input would produce a “control” identical to its target, and an identical control is a duplicate the content-hash index refuses at index time, one layer too late to explain itself. #### Parameters [Section titled “Parameters”](#parameters-28) ##### claim [Section titled “claim”](#claim-4) `string` #### Returns [Section titled “Returns”](#returns-35) `string` *** ### numericFlip() [Section titled “numericFlip()”](#numericflip) ```ts 1 function numericFlip(claim): string | undefined; ``` The claim with its first numeric token replaced by a different one. `undefined` when the claim carries no number, because the family does not apply. Inventing a number to flip would produce a control that differs from its target by an ADDED fact rather than by a contradicted one. The probe builder reads the `undefined` and skips the family instead of emitting a weaker control under the same name. The replacement is `value + 10` for a small integer and `value * 2` otherwise, so the wrong number stays in the plausible range for whatever the sentence counts. A retry budget of 13 is a believable misremembering of 3, and 3000 is not. #### Parameters [Section titled “Parameters”](#parameters-29) ##### claim [Section titled “claim”](#claim-5) `string` #### Returns [Section titled “Returns”](#returns-36) `string` | `undefined` *** ### padTo() [Section titled “padTo()”](#padto) ```ts 1 function padTo(text, length): string; ``` Pad a sentence to EXACTLY `length` characters with prose filler, for the at-the-ceiling cases. The filler is words, so the result still slugs to a title and still reads as a sentence, and the last character is forced to a letter so a boundary case cannot end in a space the door might trim. #### Parameters [Section titled “Parameters”](#parameters-30) ##### text [Section titled “text”](#text-2) `string` ##### length [Section titled “length”](#length) `number` #### Returns [Section titled “Returns”](#returns-37) `string` *** ### quantizeNow() [Section titled “quantizeNow()”](#quantizenow) ```ts 1 function quantizeNow(nowMillis): number; ``` Quantize a run instant to its UTC day. The anchor is the day rather than the millisecond so that every run on one calendar date builds a byte-identical corpus at a given seed. The gate’s determinism checks compare two full runs, and a millisecond-precision anchor would make “same seed, same numbers” true only for runs started in the same millisecond. `Math.floor` rather than a `%` remainder, because JS `%` keeps the DIVIDEND’s sign: at `nowMillis = -1`, `nowMillis % DAY_MILLIS` is `-1` and subtracting it lands on 0 — an anchor AHEAD of the instant asked for, which is the one thing the anchoring exists to rule out. A non-finite instant is refused here rather than carried: `NaN` passes every arithmetic step and surfaces hundreds of lines later as `RangeError: Invalid time value` out of a stamp formatter, which names neither the input nor the caller that supplied it. #### Parameters [Section titled “Parameters”](#parameters-31) ##### nowMillis [Section titled “nowMillis”](#nowmillis) `number` #### Returns [Section titled “Returns”](#returns-38) `number` *** ### queryFor() [Section titled “queryFor()”](#queryfor) ```ts 1 function queryFor(spec, limit?): string; ``` A probe query holds the target’s own content words, in order, capped. Content words rather than the claim verbatim, and the query design decides what the probe can measure. A NUMERIC-family control differs from its target in exactly one numeric token, so a query that dropped the number would have identical overlap with both and the vector arm could not order them at all. The probe would measure a tie-break instead. Keeping the digits is what makes the numeric family a probe rather than a coin toss. The same reasoning keeps a variant qualifier’s ANCHOR in the query while the qualifier itself, which only the control carries, stays out. Stopwords go so that `not` cannot hide behind them. A query carrying the target’s function words would raise the negation control’s lexical overlap for a reason unrelated to the fact either states. #### Parameters [Section titled “Parameters”](#parameters-32) ##### spec [Section titled “spec”](#spec-3) [`MemorySpec`](/api/eval/#memoryspec) ##### limit? [Section titled “limit?”](#limit-1) `number` = `12` #### Returns [Section titled “Returns”](#returns-39) `string` *** ### readManifest() [Section titled “readManifest()”](#readmanifest) ```ts 1 function readManifest(path): Effect; ``` Read a frozen manifest from `path`. #### Parameters [Section titled “Parameters”](#parameters-33) ##### path [Section titled “path”](#path-2) `string` | `URL` #### Returns [Section titled “Returns”](#returns-40) `Effect`<[`WritePathManifest`](/api/eval/#writepathmanifest)> *** ### replayDrift() [Section titled “replayDrift()”](#replaydrift-1) ```ts 1 function replayDrift(frozen, fresh): readonly ReplayDrift[]; ``` Every id whose fresh decision differs from the frozen one, plus ids present on one side only. Per item rather than a hash of the whole list, because a replay that fails has to say WHICH candidate moved and where it now stops; a boolean would send the reader back to run it by hand. #### Parameters [Section titled “Parameters”](#parameters-34) ##### frozen [Section titled “frozen”](#frozen-1) [`WritePathManifest`](/api/eval/#writepathmanifest) ##### fresh [Section titled “fresh”](#fresh-1) [`WritePathReport`](/api/eval/#writepathreport) #### Returns [Section titled “Returns”](#returns-41) readonly [`ReplayDrift`](/api/eval/#replaydrift)\[] *** ### runDiscrimination() [Section titled “runDiscrimination()”](#rundiscrimination) ```ts 1 function runDiscrimination(options?): Effect; ``` Run the gate. Never fails, because the report IS the answer and a caller maps `passed` to an exit code. An eval whose error channel could fire would hand a caller a run that both happened and errored, with no way to say whether the corpus, the index, or the ranking was at fault. #### Parameters [Section titled “Parameters”](#parameters-35) ##### options? [Section titled “options?”](#options-6) [`EvalOptions`](/api/eval/#evaloptions) = `{}` #### Returns [Section titled “Returns”](#returns-42) `Effect`<[`EvalOutcome`](/api/eval/#evaloutcome), `never`, `never`> *** ### runFloor() [Section titled “runFloor()”](#runfloor) ```ts 1 function runFloor( 2 retrieval, 3 probes, 4 options? 5 ): Effect; ``` #### Parameters [Section titled “Parameters”](#parameters-36) ##### retrieval [Section titled “retrieval”](#retrieval-2) `RetrievalShape` ##### probes [Section titled “probes”](#probes-9) readonly [`Probe`](/api/eval/#probe)\[] ##### options? [Section titled “options?”](#options-7) ###### limit? [Section titled “limit?”](#limit-2) `number` #### Returns [Section titled “Returns”](#returns-43) `Effect`<[`FloorReport`](/api/eval/#floorreport), `StorageFailure`> *** ### runProbes() [Section titled “runProbes()”](#runprobes) ```ts 1 function runProbes( 2 retrieval, 3 probes, 4 options? 5 ): Effect; ``` Run every probe through the real retrieval service. `includeArchived` stays false. The corpus carries an archived tier and an archived memory must not be a candidate, so a probe that ranked one would be reporting a scope leak rather than a ranking failure. The controls are ACTIVE files, which is what makes them adversaries. #### Parameters [Section titled “Parameters”](#parameters-37) ##### retrieval [Section titled “retrieval”](#retrieval-3) `RetrievalShape` ##### probes [Section titled “probes”](#probes-10) readonly [`Probe`](/api/eval/#probe)\[] ##### options? [Section titled “options?”](#options-8) ###### limit? [Section titled “limit?”](#limit-3) `number` #### Returns [Section titled “Returns”](#returns-44) `Effect`\ *** ### runWritePathAcceptance() [Section titled “runWritePathAcceptance()”](#runwritepathacceptance) ```ts 1 function runWritePathAcceptance(options?): AcceptanceReport; ``` Run the arm end to end. Pure and total: the report is the answer. #### Parameters [Section titled “Parameters”](#parameters-38) ##### options? [Section titled “options?”](#options-9) [`AcceptanceOptions`](/api/eval/#acceptanceoptions) = `{}` #### Returns [Section titled “Returns”](#returns-45) [`AcceptanceReport`](/api/eval/#acceptancereport) *** ### runWritePathDiscrimination() [Section titled “runWritePathDiscrimination()”](#runwritepathdiscrimination) ```ts 1 function runWritePathDiscrimination(options?): Effect; ``` Run the arm end to end. Never fails, for the reason `run.ts` gives for the retrieval arm: the report IS the answer and a caller maps `passed` to an exit code or an assertion. A failing report logs one line at error level. #### Parameters [Section titled “Parameters”](#parameters-39) ##### options? [Section titled “options?”](#options-10) [`WritePathOptions`](/api/eval/#writepathoptions) = `{}` #### Returns [Section titled “Returns”](#returns-46) `Effect`<[`WritePathReport`](/api/eval/#writepathreport)> *** ### summarize() [Section titled “summarize()”](#summarize) ```ts 1 function summarize( 2 mode, 3 results, 4 mrrFloor? 5 ): DiscriminationReport; ``` Aggregate probe results into the report the gate reads. **An empty suite is a FAILURE, not a vacuous pass.** Zero probes yields `mrr: 0`, which is below any floor, so a corpus whose probe generation produced nothing refuses instead of reporting a green gate over no measurement. A skipped quality gate must not look like a passing one, and “no probes ran” is the purest form of skipped. #### Parameters [Section titled “Parameters”](#parameters-40) ##### mode [Section titled “mode”](#mode-4) [`EvalMode`](/api/eval/#evalmode) ##### results [Section titled “results”](#results-3) readonly [`ProbeResult`](/api/eval/#proberesult)\[] ##### mrrFloor? [Section titled “mrrFloor?”](#mrrfloor-4) `number` = `MRR_FLOOR` #### Returns [Section titled “Returns”](#returns-47) [`DiscriminationReport`](/api/eval/#discriminationreport) *** ### summarizeAcceptance() [Section titled “summarizeAcceptance()”](#summarizeacceptance) ```ts 1 function summarizeAcceptance( 2 decisions, 3 acceptanceFloor, 4 groups 5 ): AcceptanceReport; ``` Aggregate the decisions into the report the gate reads. #### Parameters [Section titled “Parameters”](#parameters-41) ##### decisions [Section titled “decisions”](#decisions-6) readonly [`PairDecision`](/api/eval/#pairdecision)\[] ##### acceptanceFloor [Section titled “acceptanceFloor”](#acceptancefloor-2) `number` ##### groups [Section titled “groups”](#groups-1) `number` #### Returns [Section titled “Returns”](#returns-48) [`AcceptanceReport`](/api/eval/#acceptancereport) *** ### summarizeWritePath() [Section titled “summarizeWritePath()”](#summarizewritepath) ```ts 1 function summarizeWritePath(decisions, f1Floor): WritePathReport; ``` Aggregate the decisions into the report the gate reads. #### Parameters [Section titled “Parameters”](#parameters-42) ##### decisions [Section titled “decisions”](#decisions-7) readonly [`LabeledDecision`](/api/eval/#labeleddecision)\[] ##### f1Floor [Section titled “f1Floor”](#f1floor-3) `number` #### Returns [Section titled “Returns”](#returns-49) [`WritePathReport`](/api/eval/#writepathreport) *** ### transcriptsSha256() [Section titled “transcriptsSha256()”](#transcriptssha256-1) ```ts 1 function transcriptsSha256(transcripts?): string; ``` #### Parameters [Section titled “Parameters”](#parameters-43) ##### transcripts? [Section titled “transcripts?”](#transcripts-2) readonly [`FixtureTranscript`](/api/eval/#fixturetranscript)\[] = `TRANSCRIPTS` #### Returns [Section titled “Returns”](#returns-50) `string` *** ### variantFlip() [Section titled “variantFlip()”](#variantflip) ```ts 1 function variantFlip( 2 claim, 3 anchor, 4 qualifier 5 ): string; ``` The claim with a variant qualifier inserted after `anchor`. `qualifier` must be a token `@memhtml/domain`’s `VARIANT_QUALIFIERS` knows, or the pair is not variant-divergent and [deriveControl](/api/eval/#derivecontrol) refuses it. Total, so an absent anchor appends a scope sentence naming the qualifier. That states a different fact about a different variant instead of a paraphrase. #### Parameters [Section titled “Parameters”](#parameters-44) ##### claim [Section titled “claim”](#claim-6) `string` ##### anchor [Section titled “anchor”](#anchor-1) `string` ##### qualifier [Section titled “qualifier”](#qualifier-1) `string` #### Returns [Section titled “Returns”](#returns-51) `string` *** ### wholeText() [Section titled “wholeText()”](#wholetext) ```ts 1 function wholeText(text): string; ``` The whole text one memory contributes, claim first. What the veto predicates compare. #### Parameters [Section titled “Parameters”](#parameters-45) ##### text [Section titled “text”](#text-3) [`ClaimText`](/api/eval/#claimtext) #### Returns [Section titled “Returns”](#returns-52) `string` *** ### withBatch() [Section titled “withBatch()”](#withbatch) ```ts 1 function withBatch(body, transcripts?): Effect; ``` Write the transcripts into a fresh temp directory and hand back the batch the door reads. `fabricatedQuoteReason` re-reads a cited transcript from its host path, which is the production shape (the phase never holds transcript bytes), so the fixture has to be on disk. Scoped: the directory is removed when the caller’s scope closes, on the failure path too. #### Type Parameters [Section titled “Type Parameters”](#type-parameters) ##### A [Section titled “A”](#a) `A` ##### E [Section titled “E”](#e) `E` ##### R [Section titled “R”](#r) `R` #### Parameters [Section titled “Parameters”](#parameters-46) ##### body [Section titled “body”](#body-4) (`batch`) => `Effect`<`A`, `E`, `R`> ##### transcripts? [Section titled “transcripts?”](#transcripts-3) readonly [`FixtureTranscript`](/api/eval/#fixturetranscript)\[] = `TRANSCRIPTS` #### Returns [Section titled “Returns”](#returns-53) `Effect`<`A`, `E`, `R`> *** ### withStack() [Section titled “withStack()”](#withstack) ```ts 1 function withStack(body, options?): Effect; ``` Build the stack, run `body`, then tear both down. A scoped helper rather than a fixture the caller assembles, so the temp tree is removed on the failure path too. An eval that exits 1 on an inversion must not leave a 200-file corpus under `/tmp` every time the gate refuses. #### Type Parameters [Section titled “Type Parameters”](#type-parameters-1) ##### A [Section titled “A”](#a-1) `A` ##### E [Section titled “E”](#e-1) `E` ##### R [Section titled “R”](#r-1) `R` #### Parameters [Section titled “Parameters”](#parameters-47) ##### body [Section titled “body”](#body-5) (`stack`) => `Effect`<`A`, `E`, `R`> ##### options? [Section titled “options?”](#options-11) [`StackOptions`](/api/eval/#stackoptions) = `{}` #### Returns [Section titled “Returns”](#returns-54) `Effect`<`A`, `E`, `R`> *** ### writeCorpus() [Section titled “writeCorpus()”](#writecorpus) ```ts 1 function writeCorpus( 2 root, 3 git, 4 spec 5 ): Effect; ``` Generate the corpus into `root`, committing it. ONE commit for the whole corpus. A commit per memory would make the fixture’s git history the dominant cost of every eval run. The gate is about ranking, and `git log` over a generated corpus tells a reader nothing. #### Parameters [Section titled “Parameters”](#parameters-48) ##### root [Section titled “root”](#root-3) `string` ##### git [Section titled “git”](#git-1) `GitShape` ##### spec [Section titled “spec”](#spec-4) [`CorpusSpec`](/api/eval/#corpusspec) #### Returns [Section titled “Returns”](#returns-55) `Effect`<`number`> *** ### writeManifest() [Section titled “writeManifest()”](#writemanifest) ```ts 1 function writeManifest(manifest, path): Effect; ``` Write a frozen manifest to `path`. Only `scripts/freeze-write-path.mjs` calls this. #### Parameters [Section titled “Parameters”](#parameters-49) ##### manifest [Section titled “manifest”](#manifest) [`WritePathManifest`](/api/eval/#writepathmanifest) ##### path [Section titled “path”](#path-3) `string` | `URL` #### Returns [Section titled “Returns”](#returns-56) `Effect`<`void`> # @memhtml/llm `@memhtml/llm`: the Bedrock embeddings and structured-output lanes every memhtml model call goes through, over one client type that is satisfied by Bedrock directly or by an HTTP LLM proxy. The embeddings lane is Cohere Embed v4 on InvokeModel. `Embeddings` is the service and `EmbeddingsShape` has two entry points: `embed` sends documents with `input_type` `search_document`, and `embedQuery` sends one retrieval query with `input_type` `search_query`, because Cohere embeds the two into different regions of the same space and a corpus indexed one way must be queried the other way. `chunkTexts` slices a document list into batches of `EMBED_BATCH_LIMIT` (96) texts, `makeEmbeddings` runs up to `EMBED_CONCURRENCY` (6) batches at once and flattens the vectors back in input order, and `buildEmbedBody` names `output_dimension` as `EMBED_DIM` (1024) because the model returns 1536 floats when the field is absent. `readEmbeddings` refuses a response whose vector count or vector width disagrees with the request, since a short answer would shift every later vector onto the wrong chunk. `EMBED_WATERMARK` joins the model id and the dimension into `cohere.embed-v4:0@1024`, the value `index_state.embed_model` stores, and `@memhtml/index`, `@memhtml/eval` and `apps/cli` import it from here rather than concatenating their own. The generation lane is `ModelClient`, whose `ModelClientShape` has `generate` for prose (returning a `Generation` with text, token counts and latency) and `generateObject` for one schema-shaped answer described by a `StructuredRequest`. `MODELS` is the table of five `ModelKey` values, three Anthropic (`sonnet-5`, `opus-5`, `fable-5`) and two OpenAI (`gpt-5.6-sol`, `gpt-5.6-terra`), each carrying a `global.` inference-profile id and a provider, and `modelByKey` resolves a key to its row. `Effort` is the four-value reasoning dial (`low`, `medium`, `high`, `xhigh`); the Anthropic body sends it as `output_config.effort` and the OpenAI body as `reasoning_effort`. `thinkingFor` returns `{type: "adaptive"}` for `opus-5` and `fable-5` and `null` for `sonnet-5`, which rejects a thinking key instead of ignoring it. `buildInvokeBody` writes the body in the model’s dialect: the Anthropic Messages body with `anthropic_version` set to `ANTHROPIC_VERSION` and, for a structured call, a forced tool named `STRUCTURED_TOOL_NAME` (`emit`); or the OpenAI chat-completions body with `response_format` set to a strict `json_schema` under the same name. `clampTokens` bounds the output budget between 1 and `MAX_TOKENS_CEILING` (128,000), defaulting to `MAX_TOKENS_DEFAULT` (64,000), because Bedrock rejects rather than clamps. On the read side both dialects are folded into one `InvokeResponseBody`, `incompleteReason` fails a response whose `stop_reason` is in `INCOMPLETE_STOP_REASONS` (`max_tokens` or `refusal`) before any content is read, `readText` joins the text blocks and drops thinking blocks, and `readToolInput` finds the `emit` block by name rather than by position. `toInputSchema` derives the tool’s JSON Schema from an effect `Schema`, folding hoisted definitions back under `$defs`, and `decodeToolInput` decodes the tool payload with excess properties rejected, allowing one repair for a top-level container field that arrived double-encoded as a JSON string. `wrapAsData` wraps memory or rollout text in a labeled data block and neutralizes any closing tag inside it, so instruction-shaped corpus prose cannot end the block early. Both lanes are written against `InvokeClient`, a structural type with one `send` method resolving to an `InvokeResult` that a `BedrockRuntimeClient` and a recording fake both satisfy, and `invokeJson` is the one round trip: send the body, decode the JSON payload. `LlmConfig` reads `MEMHTML_AWS_REGION` (default `us-east-1`) and the four proxy variables; `makeBedrockClient` builds the direct client with `maxAttempts: 10` adaptive retry and `REQUEST_HANDLER_OPTIONS`, whose `requestTimeout` of 300,000 ms turns a hung socket into a retryable failure. `proxyConfigFromEnv` is the plain-function reader of the same variables: `MEMHTML_LLM_BASE_URL` names the proxy origin and is `null` when absent or blank, which means Bedrock directly; `MEMHTML_LLM_API_KEY` is an optional bearer token; `MEMHTML_LLM_MODEL_PREFIX` is the prefix put in front of every Bedrock id, `DEFAULT_PROXY_MODEL_PREFIX` (`bedrock/`) when blank and no prefix when it is the word `PROXY_MODEL_PREFIX_NONE` (`none`); and `MEMHTML_LLM_MODEL_MAP` is comma-separated `from=to` pairs that `parseProxyModelMap` reads and `proxyModelId` applies ahead of the prefix. A set-but-malformed origin or map entry throws at construction with the variable named, so a typo cannot silently route traffic back to Bedrock. `invokeClientFor` is the one construction site: `makeProxyClient` when the configuration names a proxy, `makeBedrockClient` otherwise, and both `ModelClientLive` and `EmbeddingsLive` call it so the two lanes cannot disagree about where a run’s traffic goes. `makeProxyClient` satisfies `InvokeClient` over `fetch`, typed as `ProxyFetch` returning a `ProxyFetchResponse` so a test can hand it a recorder: `proxyRouteFor` picks the route by model id from `PROXY_ROUTE_PATHS` (`/v1/messages` for Anthropic models, `/v1/chat/completions` for OpenAI models, `/v1/embeddings` for the embedder), `toProxyRequest` drops `anthropic_version` and adds `model` on the Messages route, adds `model` on the completions route, and rewrites Cohere’s `texts` and `output_dimension` into OpenAI’s `input` and `dimensions` on the embeddings route, and `fromProxyResponse` folds an OpenAI embeddings answer back into the Cohere shape `readEmbeddings` reads. A non-2xx answer becomes a `ProxyHttpError` carrying the status and the body’s first 200 characters, and `isRetryableProxyFailure` retries 408, 429 and 5xx under exponential backoff with jitter bounded to about two minutes. `apps/consolidator` keeps its own dependency-free copy of the `proxy-config.ts` parsers because its agent file cannot import a workspace package. Two error types from `@memhtml/contracts` divide the failures. `ModelUnavailable` means no usable answer arrived: a transport rejection or a payload that fails to parse (reduced by `modelFailure` to the model id and `name: message`), an incomplete `stop_reason`, an embedding count or width mismatch, or a prose call that returned no text. `LlmContractViolation` means an answer arrived but broke the structured contract: no `emit` tool call, or a payload that fails the strict decode, with the raw payload capped at `MAX_RAW` (800) characters in the reason. ## Modules [Section titled “Modules”](#modules) The package publishes one import path, `@memhtml/llm`, which re-exports the nine modules below, so the reference on this page is the whole exported surface. One module-level export is not re-exported: `wire.ts`’s `normalizeOpenAiResponse`, which `model-client.ts` uses internally to fold an OpenAI payload into `InvokeResponseBody`. | Module | What it holds | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `client.ts` | `InvokeClient`, `InvokeResult`, `invokeJson`, `modelFailure`, `REQUEST_HANDLER_OPTIONS`, `makeBedrockClient`, and the `LlmConfig` reader with its `LlmConfigShape`. | | `constants.ts` | The embed wire constants (`EMBED_MODEL_ID`, `EMBED_DIM`, `EMBED_BATCH_LIMIT`, `EMBED_CONCURRENCY`, `EMBED_WATERMARK`) and the generation ones (`STRUCTURED_TOOL_NAME`, `MAX_TOKENS_DEFAULT`, `MAX_TOKENS_CEILING`, `ANTHROPIC_VERSION`). | | `embeddings.ts` | `Embeddings`, `EmbeddingsShape`, `EmbeddingsLive`, `makeEmbeddings`, and the pure pieces `chunkTexts`, `buildEmbedBody` and `readEmbeddings`. | | `model-client.ts` | `ModelClient`, `ModelClientShape`, `ModelClientLive`, `makeModelClient`, the `Generation` and `StructuredRequest` shapes, and `wrapAsData`. | | `models.ts` | `MODELS`, `ModelInfo`, `ModelKey`, `Provider`, `modelByKey`, `Effort` and `thinkingFor`. | | `proxy-config.ts` | The variable names (`PROXY_BASE_URL_VAR`, `PROXY_API_KEY_VAR`, `PROXY_MODEL_PREFIX_VAR`, `PROXY_MODEL_MAP_VAR`), `DEFAULT_PROXY_MODEL_PREFIX`, `PROXY_MODEL_PREFIX_NONE`, `ProxyConfig`, and the parsers `normalizeProxyBaseUrl`, `parseProxyModelMap`, `proxyModelPrefix`, `proxyConfigFromEnv` and `proxyModelId`. | | `proxy.ts` | `ProxyRoute`, `PROXY_ROUTE_PATHS`, `proxyRouteFor`, `ProxyRequest`, `toProxyRequest`, `fromProxyResponse`, `ProxyFetch`, `ProxyFetchResponse`, `ProxyHttpError`, `isRetryableProxyFailure`, `ProxyClientOptions`, `makeProxyClient` and `invokeClientFor`. | | `structured.ts` | `toInputSchema`, `decodeToolInput` and `MAX_RAW`. | | `wire.ts` | `GenerateOptions`, `clampTokens`, `JsonSchemaObject`, `StructuredTool`, `buildInvokeBody`, `INCOMPLETE_STOP_REASONS`, `ContentBlock`, `InvokeResponseBody`, `asResponseBody`, `incompleteReason`, `readText` and `readToolInput`. | ## Classes [Section titled “Classes”](#classes) ### ProxyHttpError [Section titled “ProxyHttpError”](#proxyhttperror) A non-2xx answer, carrying the status and the body’s first 200 characters. The body is kept because a proxy reports routing and quota failures as structured JSON an operator needs verbatim — `{"error":{"code":"model_not_found"}}` names the fix, where a bare status does not. #### Extends [Section titled “Extends”](#extends) * `Error` #### Constructors [Section titled “Constructors”](#constructors) ##### Constructor [Section titled “Constructor”](#constructor) ```ts 1 new ProxyHttpError(status, body): ProxyHttpError; ``` ###### Parameters [Section titled “Parameters”](#parameters) ###### status [Section titled “status”](#status) `number` ###### body [Section titled “body”](#body) `string` ###### Returns [Section titled “Returns”](#returns) [`ProxyHttpError`](/api/llm/#proxyhttperror) ###### Overrides [Section titled “Overrides”](#overrides) ```ts 1 Error.constructor ``` #### Properties [Section titled “Properties”](#properties) ##### name [Section titled “name”](#name) ```ts 1 readonly name: "ProxyHttpError" = "ProxyHttpError"; ``` ###### Overrides [Section titled “Overrides”](#overrides-1) ```ts 1 Error.name ``` ##### status [Section titled “status”](#status-1) ```ts 1 readonly status: number; ``` ## Interfaces [Section titled “Interfaces”](#interfaces) ### ContentBlock [Section titled “ContentBlock”](#contentblock) #### Properties [Section titled “Properties”](#properties-1) ##### input? [Section titled “input?”](#input) ```ts 1 readonly optional input?: unknown; ``` ##### name? [Section titled “name?”](#name-1) ```ts 1 readonly optional name?: string; ``` ##### text? [Section titled “text?”](#text) ```ts 1 readonly optional text?: string; ``` ##### type? [Section titled “type?”](#type) ```ts 1 readonly optional type?: string; ``` *** ### EmbeddingsShape [Section titled “EmbeddingsShape”](#embeddingsshape) Cohere Embed v4 on `bedrock-runtime` InvokeModel. Two entry points, and the difference changes the result. Cohere embeds documents and queries into deliberately different regions of the same space, so a corpus indexed with `search_document` must be queried with `search_query` for the cosine to mean what the retrieval arm assumes. Reusing one `input_type` for both degrades every vector hit without failing anything. #### Properties [Section titled “Properties”](#properties-2) ##### embed [Section titled “embed”](#embed) ```ts 1 readonly embed: (texts) => Effect[], ModelUnavailable>; ``` Embed documents for storage, in order, chunking requests at Cohere’s ceiling. ###### Parameters [Section titled “Parameters”](#parameters-1) ###### texts [Section titled “texts”](#texts) readonly `string`\[] ###### Returns [Section titled “Returns”](#returns-1) `Effect`\\[], `ModelUnavailable`> ##### embedQuery [Section titled “embedQuery”](#embedquery) ```ts 1 readonly embedQuery: (text) => Effect, ModelUnavailable>; ``` Embed one retrieval query. Same space as [embed](/api/llm/#embeddingsshape), different `input_type`. ###### Parameters [Section titled “Parameters”](#parameters-2) ###### text [Section titled “text”](#text-1) `string` ###### Returns [Section titled “Returns”](#returns-2) `Effect`<`Float32Array`<`ArrayBufferLike`>, `ModelUnavailable`> *** ### GenerateOptions [Section titled “GenerateOptions”](#generateoptions) The `invoke_model` bodies, one dialect per provider, and the read-side that folds both dialects into ONE response shape. The Anthropic lane is the native Messages body. The effort and thinking rules are per-model and exact, and Converse has no field for either, so Converse is not used. The OpenAI lane is the chat-completions body, and its structured mechanism is `response_format: {type: "json_schema", strict: true}` — constrained decoding, so the bytes cannot leave the schema. Its response is normalized into [InvokeResponseBody](/api/llm/#invokeresponsebody) right here at the wire, with the schema-constrained answer presented as a `tool_use` block named `emit`: every consumer from `readToolInput` through `decodeToolInput` then has exactly one shape to read, and the decode stays the single gate for both providers. #### Properties [Section titled “Properties”](#properties-3) ##### cacheSystem? [Section titled “cacheSystem?”](#cachesystem) ```ts 1 readonly optional cacheSystem?: boolean; ``` Mark the system prompt as a cache breakpoint. The batched phases send one system prompt and one tool schema across every batch of a night, with only the member list changing per call, so the prefix is the same bytes tens of times in a row. With this set, `system` goes out as a content-block array carrying `cache_control: {type: "ephemeral"}` instead of a plain string, which is how the Messages API names a prefix to cache. A plain string carries no place to put the marker, so the shape has to change and not only gain a field. ##### effort [Section titled “effort”](#effort) ```ts 1 readonly effort: "low" | "medium" | "high" | "xhigh"; ``` ##### maxTokens? [Section titled “maxTokens?”](#maxtokens) ```ts 1 readonly optional maxTokens?: number; ``` ##### openaiPromptCache? [Section titled “openaiPromptCache?”](#openaipromptcache) ```ts 1 readonly optional openaiPromptCache?: OpenAiPromptCache; ``` How the OpenAI dialect asks Bedrock to treat prompt caching. See [OpenAiPromptCache](/api/llm/#openaipromptcache-3). Absent means [DEFAULT\_OPENAI\_PROMPT\_CACHE](/api/llm/#default_openai_prompt_cache). The Anthropic dialect ignores it: that lane names its cache prefix with `cacheSystem` and has no automatic breakpoint to turn off. ##### system? [Section titled “system?”](#system) ```ts 1 readonly optional system?: string; ``` *** ### Generation [Section titled “Generation”](#generation) The two ways a sleep phase reaches a model: prose, and one forced-tool object. Per-item failure isolation is NOT here. A phase that iterates candidates decides for itself whether one bad response skips an item or fails the phase, and it does that with `Effect.result` over each call. This service’s contract is narrower and total. One call goes in, and either a value that honors its type or a typed failure comes out. #### Properties [Section titled “Properties”](#properties-4) ##### inputTokens [Section titled “inputTokens”](#inputtokens) ```ts 1 readonly inputTokens: number | null; ``` ##### latencyMs [Section titled “latencyMs”](#latencyms) ```ts 1 readonly latencyMs: number; ``` ##### outputTokens [Section titled “outputTokens”](#outputtokens) ```ts 1 readonly outputTokens: number | null; ``` ##### text [Section titled “text”](#text-2) ```ts 1 readonly text: string; ``` *** ### InvokeClient [Section titled “InvokeClient”](#invokeclient) The one Bedrock call this package makes, named as a structural type instead of the SDK class. `BedrockRuntimeClient` satisfies it, and so does a fake that records the request body and returns a canned payload. That fake lets every wire assertion (batch boundaries, `output_dimension`, the `thinking` key, a truncated `stop_reason`) run with no network and no credential. The response is narrowed to `body` because that is the only field either lane reads. #### Properties [Section titled “Properties”](#properties-5) ##### send [Section titled “send”](#send) ```ts 1 readonly send: (command, options) => Promise; ``` ###### Parameters [Section titled “Parameters”](#parameters-3) ###### command [Section titled “command”](#command) `InvokeModelCommand` ###### options [Section titled “options”](#options) ###### abortSignal [Section titled “abortSignal”](#abortsignal) `AbortSignal` ###### Returns [Section titled “Returns”](#returns-3) `Promise`<[`InvokeResult`](/api/llm/#invokeresult)> *** ### InvokeResponseBody [Section titled “InvokeResponseBody”](#invokeresponsebody) #### Properties [Section titled “Properties”](#properties-6) ##### content? [Section titled “content?”](#content) ```ts 1 readonly optional content?: readonly ContentBlock[]; ``` ##### stop\_reason? [Section titled “stop\_reason?”](#stop_reason) ```ts 1 readonly optional stop_reason?: string | null; ``` ##### usage? [Section titled “usage?”](#usage) ```ts 1 readonly optional usage?: object; ``` ###### input\_tokens? [Section titled “input\_tokens?”](#input_tokens) ```ts 1 readonly optional input_tokens?: number; ``` ###### output\_tokens? [Section titled “output\_tokens?”](#output_tokens) ```ts 1 readonly optional output_tokens?: number; ``` *** ### InvokeResult [Section titled “InvokeResult”](#invokeresult) What `InvokeClient.send` resolves to: the SDK’s InvokeModel output narrowed to the one field either lane reads. `BedrockRuntimeClient`’s own output type satisfies it. #### Properties [Section titled “Properties”](#properties-7) ##### body? [Section titled “body?”](#body-1) ```ts 1 readonly optional body?: Uint8Array; ``` The raw response payload, absent when the SDK returned no body. *** ### JsonSchemaObject [Section titled “JsonSchemaObject”](#jsonschemaobject) A JSON Schema object for the forced tool’s `input_schema`. Open-keyed because JSON Schema is, and because the derivation in `structured.ts` emits keys this module has no reason to enumerate. #### Indexable [Section titled “Indexable”](#indexable) ```ts 1 [key: string]: unknown ``` *** ### LlmConfigShape [Section titled “LlmConfigShape”](#llmconfigshape) #### Properties [Section titled “Properties”](#properties-8) ##### openaiPromptCache [Section titled “openaiPromptCache”](#openaipromptcache-1) ```ts 1 readonly openaiPromptCache: OpenAiPromptCache; ``` ##### proxy [Section titled “proxy”](#proxy) ```ts 1 readonly proxy: ProxyConfig | null; ``` ##### region [Section titled “region”](#region) ```ts 1 readonly region: string; ``` *** ### ModelClientDefaults [Section titled “ModelClientDefaults”](#modelclientdefaults) Per-deployment wire defaults a caller’s `GenerateOptions` does not name. The sleep phases say what a call IS (system, effort, budget); the environment says how the transport should carry it, and this is where the two meet. A field set on the call wins over the default. #### Properties [Section titled “Properties”](#properties-9) ##### openaiPromptCache? [Section titled “openaiPromptCache?”](#openaipromptcache-2) ```ts 1 readonly optional openaiPromptCache?: OpenAiPromptCache; ``` See [OpenAiPromptCache](/api/llm/#openaipromptcache-3); `LlmConfig` reads it from `MEMHTML_OPENAI_PROMPT_CACHE`. *** ### ModelClientShape [Section titled “ModelClientShape”](#modelclientshape) #### Properties [Section titled “Properties”](#properties-10) ##### generate [Section titled “generate”](#generate) ```ts 1 readonly generate: (modelKey, prompt, options) => Effect; ``` ###### Parameters [Section titled “Parameters”](#parameters-4) ###### modelKey [Section titled “modelKey”](#modelkey) `"sonnet-5"` | `"opus-5"` | `"fable-5"` | `"gpt-5.6-sol"` | `"gpt-5.6-terra"` ###### prompt [Section titled “prompt”](#prompt) `string` ###### options [Section titled “options”](#options-1) [`GenerateOptions`](/api/llm/#generateoptions) ###### Returns [Section titled “Returns”](#returns-4) `Effect`<[`Generation`](/api/llm/#generation), `ModelUnavailable`> ##### generateObject [Section titled “generateObject”](#generateobject) ```ts 1 readonly generateObject: (request) => Effect; ``` ###### Type Parameters [Section titled “Type Parameters”](#type-parameters) ###### A [Section titled “A”](#a) `A` ###### I [Section titled “I”](#i) `I` ###### Parameters [Section titled “Parameters”](#parameters-5) ###### request [Section titled “request”](#request) [`StructuredRequest`](/api/llm/#structuredrequest)<`A`, `I`> ###### Returns [Section titled “Returns”](#returns-5) `Effect`<`A`, `ModelUnavailable` | `LlmContractViolation`> *** ### ModelInfo [Section titled “ModelInfo”](#modelinfo) #### Properties [Section titled “Properties”](#properties-11) ##### key [Section titled “key”](#key) ```ts 1 readonly key: "sonnet-5" | "opus-5" | "fable-5" | "gpt-5.6-sol" | "gpt-5.6-terra"; ``` ##### label [Section titled “label”](#label) ```ts 1 readonly label: string; ``` ##### modelId [Section titled “modelId”](#modelid) ```ts 1 readonly modelId: string; ``` ##### provider [Section titled “provider”](#provider) ```ts 1 readonly provider: Provider; ``` *** ### ProxyClientOptions [Section titled “ProxyClientOptions”](#proxyclientoptions) #### Properties [Section titled “Properties”](#properties-12) ##### fetch? [Section titled “fetch?”](#fetch) ```ts 1 readonly optional fetch?: ProxyFetch; ``` ##### schedule? [Section titled “schedule?”](#schedule) ```ts 1 readonly optional schedule?: Schedule; ``` The retry schedule; a test substitutes one with no delay. *** ### ProxyConfig [Section titled “ProxyConfig”](#proxyconfig) #### Properties [Section titled “Properties”](#properties-13) ##### apiKey [Section titled “apiKey”](#apikey) ```ts 1 readonly apiKey: string | null; ``` `null` when the proxy takes no credential. ##### baseUrl [Section titled “baseUrl”](#baseurl) ```ts 1 readonly baseUrl: string; ``` The origin with no trailing slash; routes are appended to it. ##### modelMap [Section titled “modelMap”](#modelmap) ```ts 1 readonly modelMap: ReadonlyMap; ``` memhtml’s model id → the exact id the proxy wants, sent without the prefix. ##### modelPrefix [Section titled “modelPrefix”](#modelprefix) ```ts 1 readonly modelPrefix: string; ``` Prepended to every Bedrock id the map does not name. May be empty. *** ### ProxyFetchResponse [Section titled “ProxyFetchResponse”](#proxyfetchresponse) The part of a `fetch` `Response` the proxy client reads; the platform `Response` satisfies it. #### Properties [Section titled “Properties”](#properties-14) ##### ok [Section titled “ok”](#ok) ```ts 1 readonly ok: boolean; ``` `true` for a 2xx status, as the platform `fetch` reports it. ##### status [Section titled “status”](#status-2) ```ts 1 readonly status: number; ``` The HTTP status code the proxy answered with. ##### text [Section titled “text”](#text-3) ```ts 1 readonly text: () => Promise; ``` Reads the whole response body as text, once. ###### Returns [Section titled “Returns”](#returns-6) `Promise`<`string`> *** ### ProxyRequest [Section titled “ProxyRequest”](#proxyrequest) #### Properties [Section titled “Properties”](#properties-15) ##### body [Section titled “body”](#body-2) ```ts 1 readonly body: Record; ``` ##### path [Section titled “path”](#path) ```ts 1 readonly path: string; ``` ##### route [Section titled “route”](#route) ```ts 1 readonly route: ProxyRoute; ``` *** ### StructuredRequest [Section titled “StructuredRequest”](#structuredrequest) #### Type Parameters [Section titled “Type Parameters”](#type-parameters-1) ##### A [Section titled “A”](#a-1) `A` ##### I [Section titled “I”](#i-1) `I` #### Properties [Section titled “Properties”](#properties-16) ##### cacheSystem? [Section titled “cacheSystem?”](#cachesystem-1) ```ts 1 readonly optional cacheSystem?: boolean; ``` Cache the system prompt as a prefix across calls. See [GenerateOptions.cacheSystem](/api/llm/#generateoptions). Set by callers that repeat one system prompt over many calls, which is every batched sleep phase. The value only reshapes the request body; the response and the decode are unchanged, so setting it can change what a call costs and cannot change what it returns. ##### effort [Section titled “effort”](#effort-1) ```ts 1 readonly effort: "low" | "medium" | "high" | "xhigh"; ``` ##### inputSchema? [Section titled “inputSchema?”](#inputschema) ```ts 1 readonly optional inputSchema?: JsonSchemaObject; ``` A hand-written `input_schema`, when the derived one is wrong for the case. The schema still decodes the response, so an override widens what the model is asked for without widening what is accepted. ##### maxTokens? [Section titled “maxTokens?”](#maxtokens-1) ```ts 1 readonly optional maxTokens?: number; ``` ##### modelKey [Section titled “modelKey”](#modelkey-1) ```ts 1 readonly modelKey: "sonnet-5" | "opus-5" | "fable-5" | "gpt-5.6-sol" | "gpt-5.6-terra"; ``` ##### prompt [Section titled “prompt”](#prompt-1) ```ts 1 readonly prompt: string; ``` ##### schema [Section titled “schema”](#schema) ```ts 1 readonly schema: Codec; ``` ##### system? [Section titled “system?”](#system-1) ```ts 1 readonly optional system?: string; ``` ##### toolDescription? [Section titled “toolDescription?”](#tooldescription) ```ts 1 readonly optional toolDescription?: string; ``` The tool description. Set it, since it is the model’s only prose about the shape. *** ### StructuredTool [Section titled “StructuredTool”](#structuredtool) #### Properties [Section titled “Properties”](#properties-17) ##### description? [Section titled “description?”](#description) ```ts 1 readonly optional description?: string; ``` ##### inputSchema [Section titled “inputSchema”](#inputschema-1) ```ts 1 readonly inputSchema: JsonSchemaObject; ``` ## Type Aliases [Section titled “Type Aliases”](#type-aliases) ### Effort [Section titled “Effort”](#effort-2) ```ts 1 type Effort = typeof Effort.Type; ``` Reasoning effort. The Anthropic lane passes it as `output_config.effort`, the OpenAI lane as `reasoning_effort`; both accept all four values (sol probed live 2026-08-22). *** ### ModelKey [Section titled “ModelKey”](#modelkey-2) ```ts 1 type ModelKey = typeof ModelKey.Type; ``` *** ### OpenAiPromptCache [Section titled “OpenAiPromptCache”](#openaipromptcache-3) ```ts 1 type OpenAiPromptCache = "off" | "implicit"; ``` The two things the OpenAI dialect can say about prompt caching. * `off` sends `prompt_cache_options: {mode: "explicit"}` with no breakpoints, which Bedrock documents as “the request does not use prompt caching or incur cache-write charges” (, “GPT-5.6 models”). * `implicit` sends no caching field at all, leaving the endpoint’s default in place. `off` is the default because of what the sleep prompts are. Bedrock’s implicit mode for GPT-5.6 places an automatic breakpoint on the LATEST message, so with a 1,024-token minimum every call whose whole prompt clears the minimum writes that whole prompt to the cache at 1.25x the input rate; and the eight sleep system prompts are 189 to 496 tokens, so the only prefix that could be reused never reaches the minimum and the only prefix that does reach it — system plus a unique batch — is never seen twice. Measured 2026-09-11 on the access log: every OpenAI-lane call reported `cache_write_tokens` about equal to its input and `cached_tokens: 0`, a pure premium with no read to pay it back. `implicit` is for a request whose prefix WILL repeat and clear the minimum, and for an OpenAI-compatible endpoint that rejects the Bedrock-only field. Probed live 2026-09-11 against `bedrock-runtime.us-east-1.amazonaws.com/openai/v1/chat/completions` with `global.openai.gpt-5.6-sol`: the field is accepted on Chat Completions (200) and honored (`prompt_tokens: 3424, cache_write_tokens: 0` on a prompt that wrote 2,641 tokens without it). *** ### Provider [Section titled “Provider”](#provider-1) ```ts 1 type Provider = "anthropic" | "openai"; ``` Which wire dialect a model speaks. Selected per model, never per call. *** ### ProxyFetch [Section titled “ProxyFetch”](#proxyfetch) ```ts 1 type ProxyFetch = (url, init) => Promise; ``` The minimal `fetch` this client needs, so a test can hand it a recorder. #### Parameters [Section titled “Parameters”](#parameters-6) ##### url [Section titled “url”](#url) `string` ##### init [Section titled “init”](#init) ###### body [Section titled “body”](#body-3) `string` ###### headers [Section titled “headers”](#headers) `Record`<`string`, `string`> ###### method [Section titled “method”](#method) `"POST"` ###### signal [Section titled “signal”](#signal) `AbortSignal` #### Returns [Section titled “Returns”](#returns-7) `Promise`<[`ProxyFetchResponse`](/api/llm/#proxyfetchresponse)> *** ### ProxyRoute [Section titled “ProxyRoute”](#proxyroute) ```ts 1 type ProxyRoute = "messages" | "completions" | "embeddings"; ``` Which of the proxy’s routes a model id is served on. ## Variables [Section titled “Variables”](#variables) ### ANTHROPIC\_VERSION [Section titled “ANTHROPIC\_VERSION”](#anthropic_version) ```ts 1 const ANTHROPIC_VERSION: "bedrock-2023-05-31" = "bedrock-2023-05-31"; ``` The only valid `anthropic_version`. Not a model date. *** ### DEFAULT\_OPENAI\_PROMPT\_CACHE [Section titled “DEFAULT\_OPENAI\_PROMPT\_CACHE”](#default_openai_prompt_cache) ```ts 1 const DEFAULT_OPENAI_PROMPT_CACHE: OpenAiPromptCache = "off"; ``` *** ### DEFAULT\_PROXY\_MODEL\_PREFIX [Section titled “DEFAULT\_PROXY\_MODEL\_PREFIX”](#default_proxy_model_prefix) ```ts 1 const DEFAULT_PROXY_MODEL_PREFIX: "bedrock/" = "bedrock/"; ``` LiteLLM’s provider prefix for Bedrock: `bedrock/global.anthropic.claude-opus-5`. *** ### Effort [Section titled “Effort”](#effort-3) ```ts 1 const Effort: Literals; ``` Reasoning effort. The Anthropic lane passes it as `output_config.effort`, the OpenAI lane as `reasoning_effort`; both accept all four values (sol probed live 2026-08-22). *** ### EMBED\_BATCH\_LIMIT [Section titled “EMBED\_BATCH\_LIMIT”](#embed_batch_limit) ```ts 1 const EMBED_BATCH_LIMIT: 96 = 96; ``` Cohere’s per-request text ceiling. Batches larger than this are rejected. *** ### EMBED\_CONCURRENCY [Section titled “EMBED\_CONCURRENCY”](#embed_concurrency) ```ts 1 const EMBED_CONCURRENCY: 6 = 6; ``` How many embed batches are in flight at once. A whole-store pass is `ceil(chunks / EMBED_BATCH_LIMIT)` requests, about 105 for a 10k-chunk corpus. Issuing them one after another makes `index rebuild --embed` a serial chain of network round trips, which is the slowest thing this system does. The concurrency is bounded, and the bound protects a shared quota rather than local resources. Bedrock rate-limits per account, so every caller in the account draws on one tokens-per-minute budget, and an unbounded fan-out would spend that budget in one burst and throttle every other consumer. Throttles that do occur are absorbed below Effect by the SDK’s adaptive retry (`maxAttempts: 10`), which backs off per request. A slightly-too-high bound therefore costs latency instead of failing the run. *** ### EMBED\_DIM [Section titled “EMBED\_DIM”](#embed_dim) ```ts 1 const EMBED_DIM: 1024 = 1024; ``` *** ### EMBED\_MODEL\_ID [Section titled “EMBED\_MODEL\_ID”](#embed_model_id) ```ts 1 const EMBED_MODEL_ID: "cohere.embed-v4:0" = "cohere.embed-v4:0"; ``` Bedrock wire constants. Cohere Embed v4 returns 1536 floats when `output_dimension` is absent and exactly 1024 when it is named (probed live 2026-08-02), so the InvokeModel body names it. If the default changed without notice, every stored vector would be invalid against a schema that says 1024. *** ### EMBED\_WATERMARK [Section titled “EMBED\_WATERMARK”](#embed_watermark) ```ts 1 const EMBED_WATERMARK: "cohere.embed-v4:0@1024"; ``` The watermark value `index_state.embed_model` stores. Both axes in one string, because a model id alone does not identify a vector space: the same id at another `output_dimension` produces vectors that are silently incomparable with the stored ones. *** ### Embeddings [Section titled “Embeddings”](#embeddings) ```ts 1 const Embeddings: Service; ``` *** ### EmbeddingsLive [Section titled “EmbeddingsLive”](#embeddingslive) ```ts 1 const EmbeddingsLive: Layer; ``` The transport is whichever `invokeClientFor` selects: Bedrock directly (`maxAttempts: 10` with adaptive retry) or the LLM proxy `MEMHTML_LLM_BASE_URL` names (jittered exponential backoff on throttles and upstream failures). The embed lane is why the retries matter: it issues hundreds of calls per index run, so a throttle that failed the run instead of backing off would make a full rebuild unreliable at exactly the corpus size where the rebuild matters. Both transports bound a hung socket (`REQUEST_HANDLER_OPTIONS`), so one dead connection inside a batch fan-out fails that batch instead of stalling the whole rebuild. Through the proxy, `output_dimension` travels as the OpenAI `dimensions` field. A proxy that drops it returns Cohere’s 1536-wide default, which `readEmbeddings` refuses by width — a typed failure and a degraded search, never a vector of the wrong shape in the index. *** ### INCOMPLETE\_STOP\_REASONS [Section titled “INCOMPLETE\_STOP\_REASONS”](#incomplete_stop_reasons) ```ts 1 const INCOMPLETE_STOP_REASONS: ReadonlySet; ``` `stop_reason` values that mean the content is not a complete answer. Both become typed failures. A response cut off at `max_tokens` may never have reached the point that made it a judgment, and a refusal carries no judgment at all. Reading either as a finished result would be a silent data-quality bug, so neither is coerced into one. *** ### LlmConfig [Section titled “LlmConfig”](#llmconfig) ```ts 1 const LlmConfig: Config<{ 2 openaiPromptCache: OpenAiPromptCache; 3 proxy: ProxyConfig | null; 4 region: string; 5 }>; ``` Where every lane sends its calls. `region` is what the direct path resolves against. `us-east-1` is the default because it is where both `cohere.embed-v4:0` and the `global.anthropic.*` inference profiles are reachable. Auth for the direct path is deliberately absent: the SDK’s default chain picks up `AWS_BEARER_TOKEN_BEDROCK` from the environment when it is set, and falls back to the standard credential chain (instance role, profile, environment keys) otherwise. Naming a profile or a key here would break both paths. `proxy` is the LLM proxy every lane goes through instead, when `MEMHTML_LLM_BASE_URL` names one (`proxy-config.ts` has the three variables and the routes). `null` is the default and means Bedrock directly; the region is then unused but still read, so flipping a deployment between the two paths is one variable and not a reshuffle. A set-but-malformed value fails here, at construction, with the variable named — see `normalizeProxyBaseUrl` for why it does not fall back to the direct path. *** ### MAX\_RAW [Section titled “MAX\_RAW”](#max_raw) ```ts 1 const MAX_RAW: 800 = 800; ``` Cap on the raw payload carried on a violation, so a runaway response cannot bloat it. *** ### MAX\_TOKENS\_CEILING [Section titled “MAX\_TOKENS\_CEILING”](#max_tokens_ceiling) ```ts 1 const MAX_TOKENS_CEILING: 128000 = 128_000; ``` Every Claude 5 generation tops out here. Above the ceiling Bedrock raises a `ValidationException` rather than clamping, so the clamp lives on this side. *** ### MAX\_TOKENS\_DEFAULT [Section titled “MAX\_TOKENS\_DEFAULT”](#max_tokens_default) ```ts 1 const MAX_TOKENS_DEFAULT: 64000 = 64_000; ``` The per-call output budget when a caller names none. A budget of 8192 has been observed truncating structured responses mid-object, and a truncated structured response is a contract violation rather than a partial result. `max_tokens` bounds thinking and answer together, so a tight budget is consumed sooner than the answer’s length alone suggests, and every sleep phase here runs with reasoning on. 64,000 rather than the 16,384 that stood here: the same number the consolidator settled on for its own per-call ceiling (`apps/consolidator/src/output-budget.ts`, issue #113), half of the 128,000 ceiling below, and generous against the largest structured answer any phase asks for. A budget is a ceiling and not a spend — the model stops when the answer is done — so the cost of a high default is only paid by the answers that needed it. *** ### ModelClient [Section titled “ModelClient”](#modelclient) ```ts 1 const ModelClient: Service; ``` *** ### ModelClientLive [Section titled “ModelClientLive”](#modelclientlive) ```ts 1 const ModelClientLive: Layer; ``` The transport is whichever `invokeClientFor` selects from the configuration: Bedrock directly, or the LLM proxy `MEMHTML_LLM_BASE_URL` names. Both bound a hung socket (`REQUEST_HANDLER_OPTIONS`) as well as retrying throttles. The sleep phases run their model calls sequentially, so without the bound one dead connection would stall a whole night’s phase rather than failing it. *** ### ModelKey [Section titled “ModelKey”](#modelkey-3) ```ts 1 const ModelKey: Literals; ``` *** ### MODELS [Section titled “MODELS”](#models) ```ts 1 const MODELS: ReadonlyArray; ``` Bedrock ids use the `global.` inference profiles, which makes them reachable from a single region without provisioning per-region throughput. The OpenAI models REQUIRE the profile: the bare `openai.gpt-5.6-*` ids reject on-demand invocation outright (probed live 2026-08-22). *** ### OPENAI\_PROMPT\_CACHE\_OFF\_FIELD [Section titled “OPENAI\_PROMPT\_CACHE\_OFF\_FIELD”](#openai_prompt_cache_off_field) ```ts 1 const OPENAI_PROMPT_CACHE_OFF_FIELD: object; ``` The wire field `off` emits. One constant, so the body and the tests spell it once. #### Type Declaration [Section titled “Type Declaration”](#type-declaration) ##### mode [Section titled “mode”](#mode) ```ts 1 readonly mode: "explicit" = "explicit"; ``` *** ### OPENAI\_PROMPT\_CACHE\_VALUES [Section titled “OPENAI\_PROMPT\_CACHE\_VALUES”](#openai_prompt_cache_values) ```ts 1 const OPENAI_PROMPT_CACHE_VALUES: ReadonlyArray; ``` *** ### OPENAI\_PROMPT\_CACHE\_VAR [Section titled “OPENAI\_PROMPT\_CACHE\_VAR”](#openai_prompt_cache_var) ```ts 1 const OPENAI_PROMPT_CACHE_VAR: "MEMHTML_OPENAI_PROMPT_CACHE" = "MEMHTML_OPENAI_PROMPT_CACHE"; ``` The environment variable [openAiPromptCacheFrom](/api/llm/#openaipromptcachefrom) reads. *** ### PROXY\_API\_KEY\_VAR [Section titled “PROXY\_API\_KEY\_VAR”](#proxy_api_key_var) ```ts 1 const PROXY_API_KEY_VAR: "MEMHTML_LLM_API_KEY" = "MEMHTML_LLM_API_KEY"; ``` A bearer token the proxy requires, sent as `Authorization: Bearer `. Optional. *** ### PROXY\_BASE\_URL\_VAR [Section titled “PROXY\_BASE\_URL\_VAR”](#proxy_base_url_var) ```ts 1 const PROXY_BASE_URL_VAR: "MEMHTML_LLM_BASE_URL" = "MEMHTML_LLM_BASE_URL"; ``` The proxy’s origin, e.g. `http://127.0.0.1:4000`. Absent or blank means Bedrock directly. *** ### PROXY\_MODEL\_MAP\_VAR [Section titled “PROXY\_MODEL\_MAP\_VAR”](#proxy_model_map_var) ```ts 1 const PROXY_MODEL_MAP_VAR: "MEMHTML_LLM_MODEL_MAP" = "MEMHTML_LLM_MODEL_MAP"; ``` `from=to` pairs, comma-separated, naming single models to the proxy by exact id: `cohere.embed-v4:0=cohere-embed-v4`. A mapped id is sent verbatim, with no prefix. *** ### PROXY\_MODEL\_PREFIX\_NONE [Section titled “PROXY\_MODEL\_PREFIX\_NONE”](#proxy_model_prefix_none) ```ts 1 const PROXY_MODEL_PREFIX_NONE: "none" = "none"; ``` The value of [PROXY\_MODEL\_PREFIX\_VAR](/api/llm/#proxy_model_prefix_var) that means “no prefix”. A WORD rather than the empty string, because `effect/Config` reads an empty environment value as absent (probed against the pinned release: `Config.String` fails on `""` exactly as on a missing key, so `withDefault` fires for both). The sleep lanes read this variable through `Config` and the consolidator reads it from `process.env` directly, and “” would have meant the default on one path and no prefix on the other. Compared case-insensitively. *** ### PROXY\_MODEL\_PREFIX\_VAR [Section titled “PROXY\_MODEL\_PREFIX\_VAR”](#proxy_model_prefix_var) ```ts 1 const PROXY_MODEL_PREFIX_VAR: "MEMHTML_LLM_MODEL_PREFIX" = "MEMHTML_LLM_MODEL_PREFIX"; ``` The prefix put in front of every unmapped Bedrock id. Unset or blank means [DEFAULT\_PROXY\_MODEL\_PREFIX](/api/llm/#default_proxy_model_prefix); the literal [PROXY\_MODEL\_PREFIX\_NONE](/api/llm/#proxy_model_prefix_none) sends bare ids. *** ### PROXY\_ROUTE\_PATHS [Section titled “PROXY\_ROUTE\_PATHS”](#proxy_route_paths) ```ts 1 const PROXY_ROUTE_PATHS: Readonly>; ``` *** ### REQUEST\_HANDLER\_OPTIONS [Section titled “REQUEST\_HANDLER\_OPTIONS”](#request_handler_options) ```ts 1 const REQUEST_HANDLER_OPTIONS: object; ``` Request-handler options for every Bedrock client this package constructs. The SDK’s default request timeout is 0 — no bound at all — so a socket that hangs after the request is written never errors, and the call holding it stalls its caller forever. A sleep phase runs its calls sequentially, so one hung socket stalls the whole night. This client’s default handler is `NodeHttp2Handler` (pinned SDK 3.1111.0, resolved at `dist-es/runtimeConfig.js`), and a plain options object here is passed to that handler’s constructor, so every key must be one that handler reads: `requestTimeout`, `sessionTimeout`, `disableConcurrentStreams`, `maxConcurrentStreams`, and `nodeHttp2ConnectOptions`. There is no connect timeout among them — a `connectionTimeout` is accepted by the type and never read. `requestTimeout` is the one bound, and it is per-STREAM: the handler arms it with `clientHttp2Stream.setTimeout`, so it fires after that long with no activity ON THE CALL and rejects with a `TimeoutError`. That name is what @smithy/core’s service-error-classification matches, so the rejection is retryable and `maxAttempts: 10` re-attempts it. 300s of inactivity is far above any legitimate single-token gap — a high-effort structured call streams nothing until it answers, and the slowest observed answers are minutes, not five — so the bound turns only genuinely dead sockets into typed failures. `sessionTimeout` is deliberately absent, because it does not bound only an idle session. Probed 2026-08-25 against a loopback h2 server: `NodeHttp2Handler` passes `sessionTimeout` as the connection manager’s `connectionConfiguration.requestTimeout`, which arms `session.setTimeout(value, ensureDestroyed)`, and `ClientHttp2SessionRef.destroy()` tears the session down with no in-flight check. Waiting for a slow answer IS “no activity” on the session, so the timer fires mid-request: against a server answering at 400 ms, a 50 ms `sessionTimeout` rejected the call at 71 ms with a bare `Error: Unexpected error: http2 request did not get a response` carrying no `code` and no `$metadata` — a shape no retry predicate matches, so `$metadata.attempts` was 1. With the key absent the same call answered at 414 ms. Any session bound below the slowest legitimate answer therefore converts a working call into an unretryable failure, and one above it bounds nothing `requestTimeout` does not already bound at the stream. `disableConcurrentStreams: true` is restated because naming a `requestHandler` REPLACES bedrock-runtime’s own default provider, which resolves to exactly `{disableConcurrentStreams: true}` (the defaults mode is `legacy`, so the mode’s config is empty). Without it the client silently changes connection model, from one isolated session per request to a pooled multiplexed one. #### Type Declaration [Section titled “Type Declaration”](#type-declaration-1) ##### disableConcurrentStreams [Section titled “disableConcurrentStreams”](#disableconcurrentstreams) ```ts 1 readonly disableConcurrentStreams: true = true; ``` ##### requestTimeout [Section titled “requestTimeout”](#requesttimeout) ```ts 1 readonly requestTimeout: 300000 = 300_000; ``` *** ### STRUCTURED\_TOOL\_NAME [Section titled “STRUCTURED\_TOOL\_NAME”](#structured_tool_name) ```ts 1 const STRUCTURED_TOOL_NAME: "emit" = "emit"; ``` The forced-tool name for structured output. One name across every phase, so a decoder can assert on it rather than on positional order in `content`. ## Functions [Section titled “Functions”](#functions) ### asResponseBody() [Section titled “asResponseBody()”](#asresponsebody) ```ts 1 function asResponseBody(payload): InvokeResponseBody; ``` The parsed payload, read defensively, since every field on the wire is optional. #### Parameters [Section titled “Parameters”](#parameters-7) ##### payload [Section titled “payload”](#payload) `unknown` #### Returns [Section titled “Returns”](#returns-8) [`InvokeResponseBody`](/api/llm/#invokeresponsebody) *** ### buildEmbedBody() [Section titled “buildEmbedBody()”](#buildembedbody) ```ts 1 function buildEmbedBody(texts, inputType): string; ``` The InvokeModel body for one embed request. `output_dimension` is named rather than defaulted. Probed live 2026-08-02: the model returns 1536 floats when the field is absent and exactly 1024 when it is present, while the `embeddings` table stores a fixed-width F32 blob. A default that changed under us would produce vectors of the wrong width against a schema that cannot hold them, and the failure would surface as a distance function returning nonsense rather than an error. `embedding_types: ["float"]` is what puts the vectors under `embeddings.float`; without it the response nests them elsewhere and the reader below finds nothing. #### Parameters [Section titled “Parameters”](#parameters-8) ##### texts [Section titled “texts”](#texts-1) readonly `string`\[] ##### inputType [Section titled “inputType”](#inputtype) `"search_document"` | `"search_query"` #### Returns [Section titled “Returns”](#returns-9) `string` *** ### buildInvokeBody() [Section titled “buildInvokeBody()”](#buildinvokebody) ```ts 1 function buildInvokeBody( 2 key, 3 prompt, 4 options, 5 tool? 6 ): string; ``` Build the request body in the model’s own dialect. The `tool` argument selects the lane. When it is absent the model answers in prose. When it is present the request constrains the model to exactly one schema-shaped answer: a forced `emit` tool call on the Anthropic dialect, a strict `json_schema` response format on the OpenAI one. `system` is omitted rather than sent empty, because an empty system block is a distinct (and rejected) input from no system block at all. An omitted system also has nothing to cache, so `cacheSystem` over an absent or empty system emits no `system` key at all instead of an empty cached block. #### Parameters [Section titled “Parameters”](#parameters-9) ##### key [Section titled “key”](#key-1) `"sonnet-5"` | `"opus-5"` | `"fable-5"` | `"gpt-5.6-sol"` | `"gpt-5.6-terra"` ##### prompt [Section titled “prompt”](#prompt-2) `string` ##### options [Section titled “options”](#options-2) [`GenerateOptions`](/api/llm/#generateoptions) ##### tool? [Section titled “tool?”](#tool) [`StructuredTool`](/api/llm/#structuredtool) #### Returns [Section titled “Returns”](#returns-10) `string` *** ### chunkTexts() [Section titled “chunkTexts()”](#chunktexts) ```ts 1 function chunkTexts(texts, size?): readonly readonly string[][]; ``` Slice `texts` into request-sized chunks, order-preserving. Exported so a test can pin the boundary arithmetic without a client. An off-by-one here drops or duplicates a vector, which lands in the index as a chunk pointing at the wrong body. #### Parameters [Section titled “Parameters”](#parameters-10) ##### texts [Section titled “texts”](#texts-2) readonly `string`\[] ##### size? [Section titled “size?”](#size) `number` = `EMBED_BATCH_LIMIT` #### Returns [Section titled “Returns”](#returns-11) readonly readonly `string`\[]\[] *** ### clampTokens() [Section titled “clampTokens()”](#clamptokens) ```ts 1 function clampTokens(requested): number; ``` Bound a requested budget to what Bedrock accepts. Above the ceiling the service raises a `ValidationException` rather than clamping, so an unbounded caller value would fail the call instead of shortening the answer. The floor is 1 for the same reason one axis down: `max_tokens` must be a positive integer, so a zero or negative request would also fail the call at the service — the floor turns it into the smallest budget the wire accepts, where the truncation gate then reports the response as incomplete rather than the request as malformed. “Positive integer” is the whole contract, so the clamp also has to answer the two values a min/max pair passes through unchanged. `NaN` loses every comparison, so it survives both bounds and `JSON.stringify` writes it as `max_tokens: null` — the malformed request this function exists to prevent — and it is not a budget at all, so it resolves to the default rather than to a bound. A fractional request IS a budget, so it truncates toward zero and then meets the floor; `Infinity` truncates to itself and meets the ceiling. #### Parameters [Section titled “Parameters”](#parameters-11) ##### requested [Section titled “requested”](#requested) `number` | `undefined` #### Returns [Section titled “Returns”](#returns-12) `number` *** ### decodeToolInput() [Section titled “decodeToolInput()”](#decodetoolinput) ```ts 1 function decodeToolInput(schema, input): Effect; ``` Decode a forced-tool payload against its schema. `onExcessProperty: "error"` is the option this decode depends on. The default, `"ignore"`, strips an undeclared key and SUCCEEDS (verified against effect 4.0.0-beta.102), which would let a model answer a schema next to the one it was given and have the extra field vanish. One failure shape is repaired before the violation is constructed: a top-level container field double-encoded as a JSON string (`unwrapDoubleEncoded`). The repaired payload goes through the SAME strict decode, and a repair that still does not satisfy the schema reports the original payload’s violation, so the repair cannot mask a genuinely off-schema answer. `undefined` input means the model produced no `emit` call at all. That is the same class of failure as a malformed one, and the reason text names it so a caller can tell the two apart in a log without a second error type. #### Type Parameters [Section titled “Type Parameters”](#type-parameters-2) ##### A [Section titled “A”](#a-2) `A` ##### I [Section titled “I”](#i-2) `I` #### Parameters [Section titled “Parameters”](#parameters-12) ##### schema [Section titled “schema”](#schema-1) `Codec`<`A`, `I`> ##### input [Section titled “input”](#input-1) `unknown` #### Returns [Section titled “Returns”](#returns-13) `Effect`<`A`, `LlmContractViolation`> *** ### fromProxyResponse() [Section titled “fromProxyResponse()”](#fromproxyresponse) ```ts 1 function fromProxyResponse(route, payload): unknown; ``` Fold a proxy response into the InvokeModel payload the lane expects. Messages and chat-completions responses are already the payloads `asResponseBody` and `normalizeOpenAiResponse` read, so they pass through untouched. An OpenAI embeddings response (`data: [{index, embedding}]`) becomes Cohere’s `embeddings.float`, ordered by `index` because the OpenAI shape permits any order and `readEmbeddings` pairs vectors with texts positionally. An off-shape embeddings payload folds to an empty `float`, which `readEmbeddings` then reports as a count mismatch — the same typed failure a short Cohere answer produces. #### Parameters [Section titled “Parameters”](#parameters-13) ##### route [Section titled “route”](#route-1) [`ProxyRoute`](/api/llm/#proxyroute) ##### payload [Section titled “payload”](#payload-1) `unknown` #### Returns [Section titled “Returns”](#returns-14) `unknown` *** ### incompleteReason() [Section titled “incompleteReason()”](#incompletereason) ```ts 1 function incompleteReason(parsed): string | null; ``` The incomplete `stop_reason`, or null when the response ran to a natural end. #### Parameters [Section titled “Parameters”](#parameters-14) ##### parsed [Section titled “parsed”](#parsed) [`InvokeResponseBody`](/api/llm/#invokeresponsebody) #### Returns [Section titled “Returns”](#returns-15) `string` | `null` *** ### invokeClientFor() [Section titled “invokeClientFor()”](#invokeclientfor) ```ts 1 function invokeClientFor(config): InvokeClient; ``` The one construction site for a lane’s transport: the proxy when the configuration names one, Bedrock directly otherwise. Both `ModelClientLive` and `EmbeddingsLive` call this, so the two lanes cannot disagree about where a night’s traffic goes. #### Parameters [Section titled “Parameters”](#parameters-15) ##### config [Section titled “config”](#config) [`LlmConfigShape`](/api/llm/#llmconfigshape) #### Returns [Section titled “Returns”](#returns-16) [`InvokeClient`](/api/llm/#invokeclient) *** ### invokeJson() [Section titled “invokeJson()”](#invokejson) ```ts 1 function invokeJson( 2 client, 3 modelId, 4 body 5 ): Effect; ``` One InvokeModel round trip: send the body, decode the JSON payload. Both the transport rejection and an unparseable payload land on `ModelUnavailable`, because neither one says anything about the model’s answer. They say only that no answer arrived. Reading the answer, and judging whether it honors its contract, is the caller’s job. #### Parameters [Section titled “Parameters”](#parameters-16) ##### client [Section titled “client”](#client) [`InvokeClient`](/api/llm/#invokeclient) ##### modelId [Section titled “modelId”](#modelid-1) `string` ##### body [Section titled “body”](#body-4) `string` #### Returns [Section titled “Returns”](#returns-17) `Effect`<`unknown`, `ModelUnavailable`> *** ### isRetryableProxyFailure() [Section titled “isRetryableProxyFailure()”](#isretryableproxyfailure) ```ts 1 function isRetryableProxyFailure(cause): boolean; ``` Which failures are worth a second attempt: a throttle, a timeout at the proxy, an upstream that fell over, and a socket that never answered. A 4xx other than those is the request’s fault and repeats identically; an abort is the caller’s decision. #### Parameters [Section titled “Parameters”](#parameters-17) ##### cause [Section titled “cause”](#cause) `unknown` #### Returns [Section titled “Returns”](#returns-18) `boolean` *** ### makeBedrockClient() [Section titled “makeBedrockClient()”](#makebedrockclient) ```ts 1 function makeBedrockClient(region): BedrockRuntimeClient; ``` One construction for both lanes: `maxAttempts: 10` with adaptive retry absorbs throttles below Effect, and [REQUEST\_HANDLER\_OPTIONS](/api/llm/#request_handler_options) bounds a hung socket. #### Parameters [Section titled “Parameters”](#parameters-18) ##### region [Section titled “region”](#region-1) `string` #### Returns [Section titled “Returns”](#returns-19) `BedrockRuntimeClient` *** ### makeEmbeddings() [Section titled “makeEmbeddings()”](#makeembeddings) ```ts 1 function makeEmbeddings(client): EmbeddingsShape; ``` The service over an already-built client. Exported as the seam every test uses: a fake `InvokeClient` records each request body, so the batch boundaries and the wire fields are asserted against the bytes that would go to Bedrock rather than against a mock’s recollection of them. #### Parameters [Section titled “Parameters”](#parameters-19) ##### client [Section titled “client”](#client-1) [`InvokeClient`](/api/llm/#invokeclient) #### Returns [Section titled “Returns”](#returns-20) [`EmbeddingsShape`](/api/llm/#embeddingsshape) *** ### makeModelClient() [Section titled “makeModelClient()”](#makemodelclient) ```ts 1 function makeModelClient(client, defaults?): ModelClientShape; ``` #### Parameters [Section titled “Parameters”](#parameters-20) ##### client [Section titled “client”](#client-2) [`InvokeClient`](/api/llm/#invokeclient) ##### defaults? [Section titled “defaults?”](#defaults) [`ModelClientDefaults`](/api/llm/#modelclientdefaults) = `{}` #### Returns [Section titled “Returns”](#returns-21) [`ModelClientShape`](/api/llm/#modelclientshape) *** ### makeProxyClient() [Section titled “makeProxyClient()”](#makeproxyclient) ```ts 1 function makeProxyClient(config, options?): InvokeClient; ``` The client. `send` mirrors `BedrockRuntimeClient.send` closely enough that `invokeJson` cannot tell them apart: it resolves with the response bytes and rejects with an `Error` whose `name: message` `modelFailure` renders. Each attempt is bounded by the same per-request inactivity window the Bedrock client uses (`REQUEST_HANDLER_OPTIONS.requestTimeout`), composed with the caller’s own signal, and a rejected attempt is retried under `PROXY_BACKOFF` when [isRetryableProxyFailure](/api/llm/#isretryableproxyfailure) says so. The proxy’s response bytes are returned verbatim for the two chat lanes and re-encoded for the embedding lane after [fromProxyResponse](/api/llm/#fromproxyresponse). #### Parameters [Section titled “Parameters”](#parameters-21) ##### config [Section titled “config”](#config-1) [`ProxyConfig`](/api/llm/#proxyconfig) ##### options? [Section titled “options?”](#options-3) [`ProxyClientOptions`](/api/llm/#proxyclientoptions) = `{}` #### Returns [Section titled “Returns”](#returns-22) [`InvokeClient`](/api/llm/#invokeclient) *** ### modelByKey() [Section titled “modelByKey()”](#modelbykey) ```ts 1 function modelByKey(key): ModelInfo; ``` Resolve a key to its model. Total over `ModelKey`, so the type makes the throw unreachable. It is there to fail loudly if the table is ever edited out of agreement with the literal union, not to be caught. #### Parameters [Section titled “Parameters”](#parameters-22) ##### key [Section titled “key”](#key-2) `"sonnet-5"` | `"opus-5"` | `"fable-5"` | `"gpt-5.6-sol"` | `"gpt-5.6-terra"` #### Returns [Section titled “Returns”](#returns-23) [`ModelInfo`](/api/llm/#modelinfo) *** ### modelFailure() [Section titled “modelFailure()”](#modelfailure) ```ts 1 function modelFailure(modelId, cause): ModelUnavailable; ``` A Bedrock rejection reduced to the model and the driver’s own summary. The reason carries no prompt and no memory body, because a `ModelUnavailable` goes back to an agent through a tool response, and the corpus content that produced it does not need to be repeated there. #### Parameters [Section titled “Parameters”](#parameters-23) ##### modelId [Section titled “modelId”](#modelid-2) `string` ##### cause [Section titled “cause”](#cause-1) `unknown` #### Returns [Section titled “Returns”](#returns-24) `ModelUnavailable` *** ### normalizeProxyBaseUrl() [Section titled “normalizeProxyBaseUrl()”](#normalizeproxybaseurl) ```ts 1 function normalizeProxyBaseUrl(raw): string; ``` The origin memhtml will append `/v1/...` to, or a thrown `Error` naming what is wrong with it. Trailing slashes are dropped so `http://host:4000/` and `http://host:4000` are one value, and the scheme is required because a bare `host:4000` is what a shell profile most easily gets wrong and `fetch` would reject it with a message that does not name the variable. Throwing rather than returning `null` is deliberate: a set-but-unusable value dying at construction, with the variable named, is the outcome `MEMHTML_CONSOLIDATOR_TURN_TIMEOUT_MS` chose for the same reason — a typo that silently fell back to Bedrock direct would route production traffic somewhere the operator did not point it. #### Parameters [Section titled “Parameters”](#parameters-24) ##### raw [Section titled “raw”](#raw) `string` #### Returns [Section titled “Returns”](#returns-25) `string` *** ### openAiPromptCacheFrom() [Section titled “openAiPromptCacheFrom()”](#openaipromptcachefrom) ```ts 1 function openAiPromptCacheFrom(raw): OpenAiPromptCache; ``` Parse a raw [OPENAI\_PROMPT\_CACHE\_VAR](/api/llm/#openai_prompt_cache_var) value. Unset or blank is the default, for the reason every proxy variable treats blank so — a blank export is how a variable goes missing. Compared case-insensitively after trimming. Anything else throws with the variable named, matching `normalizeProxyBaseUrl`: a typo that silently fell back to the default would put the write premium back on every call while the environment claimed to have turned it off. #### Parameters [Section titled “Parameters”](#parameters-25) ##### raw [Section titled “raw”](#raw-1) `string` | `undefined` #### Returns [Section titled “Returns”](#returns-26) [`OpenAiPromptCache`](/api/llm/#openaipromptcache-3) *** ### parseProxyModelMap() [Section titled “parseProxyModelMap()”](#parseproxymodelmap) ```ts 1 function parseProxyModelMap(raw): ReadonlyMap; ``` Parse [PROXY\_MODEL\_MAP\_VAR](/api/llm/#proxy_model_map_var). Empty entries (a trailing comma) are ignored; an entry with no `=`, an empty side, or a key mapped twice throws with the offending entry quoted, for the reason [normalizeProxyBaseUrl](/api/llm/#normalizeproxybaseurl) records. #### Parameters [Section titled “Parameters”](#parameters-26) ##### raw [Section titled “raw”](#raw-2) `string` #### Returns [Section titled “Returns”](#returns-27) `ReadonlyMap`<`string`, `string`> *** ### proxyConfigFromEnv() [Section titled “proxyConfigFromEnv()”](#proxyconfigfromenv) ```ts 1 function proxyConfigFromEnv(env?): ProxyConfig | null; ``` The proxy configuration an environment describes, or `null` when [PROXY\_BASE\_URL\_VAR](/api/llm/#proxy_base_url_var) is absent or blank — which is the default, Bedrock direct. A blank value reads as absent for the reason the credential preflight treats `""` as unset: a blank export is how a variable goes missing in practice, and an empty origin is not a place to send traffic. #### Parameters [Section titled “Parameters”](#parameters-27) ##### env? [Section titled “env?”](#env) `Record`<`string`, `string` | `undefined`> = `process.env` #### Returns [Section titled “Returns”](#returns-28) [`ProxyConfig`](/api/llm/#proxyconfig) | `null` *** ### proxyModelId() [Section titled “proxyModelId()”](#proxymodelid) ```ts 1 function proxyModelId(config, modelId): string; ``` The id the proxy is asked for: the map’s exact name when it has one, else the prefix and the Bedrock id. The map wins so one odd model can be named by hand while the rest follow the rule. #### Parameters [Section titled “Parameters”](#parameters-28) ##### config [Section titled “config”](#config-2) [`ProxyConfig`](/api/llm/#proxyconfig) ##### modelId [Section titled “modelId”](#modelid-3) `string` #### Returns [Section titled “Returns”](#returns-29) `string` *** ### proxyModelPrefix() [Section titled “proxyModelPrefix()”](#proxymodelprefix) ```ts 1 function proxyModelPrefix(raw): string; ``` The prefix a raw [PROXY\_MODEL\_PREFIX\_VAR](/api/llm/#proxy_model_prefix_var) value means: unset or blank is the LiteLLM default, [PROXY\_MODEL\_PREFIX\_NONE](/api/llm/#proxy_model_prefix_none) is no prefix at all, anything else is taken as written after trimming. Blank reads as unset for the reason every other variable here treats it so — a blank export is how a variable goes missing — and because `Config` cannot see the difference. #### Parameters [Section titled “Parameters”](#parameters-29) ##### raw [Section titled “raw”](#raw-3) `string` | `undefined` #### Returns [Section titled “Returns”](#returns-30) `string` *** ### proxyRouteFor() [Section titled “proxyRouteFor()”](#proxyroutefor) ```ts 1 function proxyRouteFor(modelId): ProxyRoute | null; ``` The route for a model id, or `null` for an id this package does not call. Anthropic models speak Messages, the OpenAI model speaks chat completions, and the one embedding model is its own lane. #### Parameters [Section titled “Parameters”](#parameters-30) ##### modelId [Section titled “modelId”](#modelid-4) `string` #### Returns [Section titled “Returns”](#returns-31) [`ProxyRoute`](/api/llm/#proxyroute) | `null` *** ### readEmbeddings() [Section titled “readEmbeddings()”](#readembeddings) ```ts 1 function readEmbeddings(payload, expected): ModelUnavailable | readonly Float32Array[]; ``` Read the vectors out of a decoded payload, or say why they are unusable. A count mismatch is a typed failure rather than a short array, because the caller pairs vectors with chunk ids positionally. A response one vector short would shift every subsequent pairing and store each embedding against the wrong body. A width mismatch fails the same way one axis over. The `embed_model` watermark records `id@dim`, so a vector of another width cannot be compared against the stored ones. #### Parameters [Section titled “Parameters”](#parameters-31) ##### payload [Section titled “payload”](#payload-2) `unknown` ##### expected [Section titled “expected”](#expected) `number` #### Returns [Section titled “Returns”](#returns-32) `ModelUnavailable` | readonly `Float32Array`<`ArrayBufferLike`>\[] *** ### readText() [Section titled “readText()”](#readtext) ```ts 1 function readText(parsed): string; ``` Join the text blocks. Thinking blocks are discarded on purpose, because a caller reads the answer rather than the deliberation. Concatenating the two would put reasoning the model did not commit to into the value a phase acts on. #### Parameters [Section titled “Parameters”](#parameters-32) ##### parsed [Section titled “parsed”](#parsed-1) [`InvokeResponseBody`](/api/llm/#invokeresponsebody) #### Returns [Section titled “Returns”](#returns-33) `string` *** ### readToolInput() [Section titled “readToolInput()”](#readtoolinput) ```ts 1 function readToolInput(parsed): unknown; ``` The forced tool’s `input`, or undefined when the model answered without calling it. Matched on the block’s `name` instead of its position, because a thinking block precedes the tool call on the two adaptive models and an index-based read would find that block. #### Parameters [Section titled “Parameters”](#parameters-33) ##### parsed [Section titled “parsed”](#parsed-2) [`InvokeResponseBody`](/api/llm/#invokeresponsebody) #### Returns [Section titled “Returns”](#returns-34) `unknown` *** ### thinkingFor() [Section titled “thinkingFor()”](#thinkingfor) ```ts 1 function thinkingFor(key): 2 | { 3 type: "adaptive"; 4 } 5 | null; ``` The `thinking` object per Anthropic model. Opus 5 and Fable 5 take `{type: "adaptive"}` (Fable is adaptive-only). Sonnet 5 reasons unconditionally and takes NO thinking key. Sending one to Sonnet 5 raises a validation error instead of being ignored. The OpenAI lane never consults this: its reasoning dial is `reasoning_effort` alone. Verified live 2026-08-02: all three Claude models accept this shape alongside a forced `tool_choice`, so structured output and adaptive thinking compose. #### Parameters [Section titled “Parameters”](#parameters-34) ##### key [Section titled “key”](#key-3) `"sonnet-5"` | `"opus-5"` | `"fable-5"` | `"gpt-5.6-sol"` | `"gpt-5.6-terra"` #### Returns [Section titled “Returns”](#returns-35) ##### Type Literal [Section titled “Type Literal”](#type-literal) ```ts 1 { 2 type: "adaptive"; 3 } ``` ###### type [Section titled “type”](#type-1) ```ts 1 readonly type: "adaptive"; ``` The only thinking mode the Claude 5 models accept here. *** `null` *** ### toInputSchema() [Section titled “toInputSchema()”](#toinputschema) ```ts 1 function toInputSchema(schema): JsonSchemaObject; ``` Derive the tool’s `input_schema` from an effect schema. `Schema.toJsonSchemaDocument` returns its `definitions` map separately from the schema while emitting `$ref: "#/$defs/"` into the schema, so the map is folded back under the root as `$defs` — the pointer those refs already name. Verified live 2026-08-02 that Bedrock resolves a `$ref` into a root-level `$defs` inside `input_schema`. A RECURSIVE schema is what makes this load-bearing, and it is the only thing that does: probed 2026-08-25 on effect 4.0.0-rc.111, a struct reused from two places is emitted inline at each, while a self-reference is hoisted because it has no finite expansion. So the fold is a narrow repair for recursion rather than the general case, and `definitions` is usually empty. A numeric field should be declared `Schema.Finite`, not `Schema.Number`: the latter emits an `anyOf` with a string branch for `Infinity`/`NaN`, which invites the model to answer a number field with the string `"NaN"`. #### Parameters [Section titled “Parameters”](#parameters-35) ##### schema [Section titled “schema”](#schema-2) `Top` #### Returns [Section titled “Returns”](#returns-36) [`JsonSchemaObject`](/api/llm/#jsonschemaobject) *** ### toProxyRequest() [Section titled “toProxyRequest()”](#toproxyrequest) ```ts 1 function toProxyRequest( 2 config, 3 modelId, 4 body 5 ): ProxyRequest | null; ``` Translate one InvokeModel request into the proxy’s request for it. Pure, so the wire test pins every field against the bytes that would go out. `model` is the proxy’s name for the Bedrock id (`proxyModelId`: `bedrock/` by default, or the map’s exact name). * Messages: `anthropic_version` is Bedrock’s field and the Messages API has no such key, so it is dropped; `model` is added. Everything else — `max_tokens`, `system` (with any cache marker), `thinking`, `output_config`, `tools`, `tool_choice` — is already the Messages dialect. * Completions: the body is already chat completions; only `model` is added. * Embeddings: Cohere’s `texts` becomes `input`, `output_dimension` becomes `dimensions`, and `input_type` rides through under its own name — the proxy forwards it to Cohere, and it is the field the retrieval asymmetry depends on (`embeddings.ts`). `encoding_format: "float"` asks for the plain arrays the fold below reads. #### Parameters [Section titled “Parameters”](#parameters-36) ##### config [Section titled “config”](#config-3) [`ProxyConfig`](/api/llm/#proxyconfig) ##### modelId [Section titled “modelId”](#modelid-5) `string` ##### body [Section titled “body”](#body-5) `Record`<`string`, `unknown`> #### Returns [Section titled “Returns”](#returns-37) [`ProxyRequest`](/api/llm/#proxyrequest) | `null` *** ### wrapAsData() [Section titled “wrapAsData()”](#wrapasdata) ```ts 1 function wrapAsData(label, text): string; ``` Wrap rollout or memory text for a user turn. The delimiters keep the content’s own prose from being read as a directive to the model. That prose is often instruction-shaped in this corpus, because the memories record instructions. The delimiters only hold if the content cannot produce them: a body carrying the literal closing tag would end the data block early and place its own remainder OUTSIDE the boundary, where it reads as the caller’s instructions. So every end tag `closingTagsFor` names is neutralized in the content before wrapping — the slash gains a backslash, which keeps the text legible, and the attribute text if any is kept, so the neutralizer rewrites nothing but the one character that made the text a delimiter. Case-insensitive, because the boundary is prose to the model rather than parsed markup, and a cased variant reads as the same tag. #### Parameters [Section titled “Parameters”](#parameters-37) ##### label [Section titled “label”](#label-1) `string` ##### text [Section titled “text”](#text-4) `string` #### Returns [Section titled “Returns”](#returns-38) `string` # @memhtml/traces `@memhtml/traces`: a streaming JSONL parser and trace indexer over agent session transcripts. Read-only, and it stores no session content. The package turns a directory of session transcripts into rows the index can hold: pointers, counters, and capped text heads, never the transcript itself. `discover` names the layout of that directory (a `projects/` tree of per-cwd slug directories, each session a `.jsonl`, each subagent an `agent-.jsonl` sidecar under `subagents/`). `watermark` decides, from a file’s size, modification time and a stored byte offset, whether a scan can skip the file, read only its tail, or must rescan it from byte 0. `parse` streams one file line by line, splitting on the raw buffer so an unterminated trailing line is neither folded nor counted as consumed. `extract` folds those lines into a `SessionExtract` without ever throwing, because another process may be appending to the file mid-line. `scan` composes the four into the unit the indexer persists and owns `mergeTailExtract`, the rule for folding a tail’s extract into a stored row. Persistence is not here. `@memhtml/index`’s trace persister owns the `traces` and `trace_watermarks` tables and binds to these shapes structurally; the CLI’s trace operations call `scanTraceRoot` and `mergeTailExtract` from this package and hand the results to that persister. ## Modules [Section titled “Modules”](#modules) The package publishes one import path, `@memhtml/traces`, which re-exports the five modules below, so the reference on this page is the whole exported surface. | Module | What it holds | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `discover.ts` | The transcript-tree layout: `PROJECTS_DIR`, `SUBAGENTS_DIR`, and the walk that yields each session file with its sidecars. | | `watermark.ts` | `Watermark`, `WatermarkAction`, `watermarkPlan` and `advanceWatermark`: the pure skip, tail or rescan arithmetic over a file’s stat. | | `parse.ts` | The streaming reader that splits a transcript into lines and reports what one scan consumed. | | `extract.ts` | `READ_RECORD_TYPES`, the per-line fold (`emptyAccumulator`, `foldLine`, `finalizeExtract`) and the `SessionExtract` it produces. | | `scan.ts` | `scanTraceRoot`, `WatermarkReader`, the per-file outcome, and `mergeTailExtract`. | ## Interfaces [Section titled “Interfaces”](#interfaces) ### Accumulator [Section titled “Accumulator”](#accumulator) Mutable fold state. Private, so callers see [SessionExtract](/api/traces/#sessionextract) only. #### Properties [Section titled “Properties”](#properties) ##### agentIds [Section titled “agentIds”](#agentids) ```ts 1 readonly agentIds: Set; ``` ##### aiTitle [Section titled “aiTitle”](#aititle) ```ts 1 aiTitle: string | null; ``` ##### cwd [Section titled “cwd”](#cwd) ```ts 1 cwd: string | null; ``` ##### droppedLines [Section titled “droppedLines”](#droppedlines) ```ts 1 droppedLines: number; ``` ##### droppedNoSession [Section titled “droppedNoSession”](#droppednosession) ```ts 1 droppedNoSession: number; ``` ##### entrypoint [Section titled “entrypoint”](#entrypoint) ```ts 1 entrypoint: string | null; ``` ##### firstPrompt [Section titled “firstPrompt”](#firstprompt) ```ts 1 firstPrompt: string; ``` ##### gitBranch [Section titled “gitBranch”](#gitbranch) ```ts 1 gitBranch: string | null; ``` ##### maxEpochMs [Section titled “maxEpochMs”](#maxepochms) ```ts 1 maxEpochMs: number | null; ``` ##### maxIso [Section titled “maxIso”](#maxiso) ```ts 1 maxIso: string | null; ``` ##### minEpochMs [Section titled “minEpochMs”](#minepochms) ```ts 1 minEpochMs: number | null; ``` ##### minIso [Section titled “minIso”](#miniso) ```ts 1 minIso: string | null; ``` ##### modelCounts [Section titled “modelCounts”](#modelcounts) ```ts 1 readonly modelCounts: Map; ``` ##### parsedLines [Section titled “parsedLines”](#parsedlines) ```ts 1 parsedLines: number; ``` ##### prompts [Section titled “prompts”](#prompts) ```ts 1 readonly prompts: Map; ``` ##### sessionId [Section titled “sessionId”](#sessionid) ```ts 1 sessionId: string | null; ``` ##### skippedTypeLines [Section titled “skippedTypeLines”](#skippedtypelines) ```ts 1 skippedTypeLines: number; ``` ##### turnCount [Section titled “turnCount”](#turncount) ```ts 1 turnCount: number; ``` ##### unknownTypeLines [Section titled “unknownTypeLines”](#unknowntypelines) ```ts 1 unknownTypeLines: number; ``` ##### version [Section titled “version”](#version) ```ts 1 version: string | null; ``` *** ### FileIdentity [Section titled “FileIdentity”](#fileidentity) How the reader learns a file’s `slug` when the caller has not already discovered it. #### Properties [Section titled “Properties”](#properties-1) ##### slug [Section titled “slug”](#slug) ```ts 1 readonly slug: string; ``` *** ### FileStat [Section titled “FileStat”](#filestat) A file’s current stat, the half of [Watermark](/api/traces/#watermark-1) the filesystem supplies. #### Properties [Section titled “Properties”](#properties-2) ##### mtimeMs [Section titled “mtimeMs”](#mtimems) ```ts 1 readonly mtimeMs: number; ``` ##### size [Section titled “size”](#size) ```ts 1 readonly size: number; ``` *** ### ParseCounters [Section titled “ParseCounters”](#parsecounters) How many lines each disposition claimed. Every line lands in exactly one counter, so `parsedLines + droppedLines` is the number of non-empty lines the scan consumed. * `parsedLines`: decoded to a JSON object, whatever happened afterwards. * `droppedLines`: undecodable JSON, or decoded to something that is not an object with a string `type`. Counted rather than fatal, so one bad line does not abandon the file. * `droppedNoSession`: a read-type record with no string `sessionId`. Subset of `parsedLines`. * `skippedTypeLines`: a [SKIP\_RECORD\_TYPES](/api/traces/#skip_record_types) type. Subset of `parsedLines`. * `unknownTypeLines`: a type in neither list, meaning a record type the runtime added after this allowlist was written. Subset of `parsedLines`, and the counter to watch when a new Claude Code release lands. #### Properties [Section titled “Properties”](#properties-3) ##### droppedLines [Section titled “droppedLines”](#droppedlines-1) ```ts 1 readonly droppedLines: number; ``` ##### droppedNoSession [Section titled “droppedNoSession”](#droppednosession-1) ```ts 1 readonly droppedNoSession: number; ``` ##### parsedLines [Section titled “parsedLines”](#parsedlines-1) ```ts 1 readonly parsedLines: number; ``` ##### skippedTypeLines [Section titled “skippedTypeLines”](#skippedtypelines-1) ```ts 1 readonly skippedTypeLines: number; ``` ##### unknownTypeLines [Section titled “unknownTypeLines”](#unknowntypelines-1) ```ts 1 readonly unknownTypeLines: number; ``` *** ### ParseResult [Section titled “ParseResult”](#parseresult) What one file’s scan consumed, alongside the extract. #### Properties [Section titled “Properties”](#properties-4) ##### bytesRead [Section titled “bytesRead”](#bytesread) ```ts 1 readonly bytesRead: number; ``` Bytes occupied by *newline-terminated* lines, terminators included. The next read starts at `startByte + bytesRead`, so this is what keeps a tail landing on a record boundary. ##### extract [Section titled “extract”](#extract) ```ts 1 readonly extract: SessionExtract; ``` ##### readFailed [Section titled “readFailed”](#readfailed) ```ts 1 readonly readFailed: boolean; ``` True when the stream itself could not be read: an absent file, a permission rejection, a transient IO error. The extract is empty and `bytesRead` is 0 however far the read got before breaking, because a partial fold ends on no boundary a watermark could stand on. A caller holding a watermark must leave it untouched on a failed read. Stamping the file’s current size and mtime over a read that consumed nothing makes the next run’s comparison see an unchanged file and skip it, which turns one transient error into a transcript that is never indexed. ##### startByte [Section titled “startByte”](#startbyte) ```ts 1 readonly startByte: number; ``` Byte offset the read began at, 0 for a full scan. *** ### PromptRow [Section titled “PromptRow”](#promptrow) One `trace_prompts` row (design §3.3), minus the `session_id` the enclosing [SessionExtract](/api/traces/#sessionextract) carries. #### Properties [Section titled “Properties”](#properties-5) ##### agentId [Section titled “agentId”](#agentid) ```ts 1 readonly agentId: string | null; ``` Set only on a subagent sidecar’s records. ##### at [Section titled “at”](#at) ```ts 1 readonly at: string; ``` ISO-8601 UTC instant of the prompt’s first record. ##### ordinal [Section titled “ordinal”](#ordinal) ```ts 1 readonly ordinal: number; ``` 0-based position of this prompt among the distinct prompts of **this session**, in first-appearance order. Per-session scope, so it is comparable only within one `session_id`. ##### promptId [Section titled “promptId”](#promptid) ```ts 1 readonly promptId: string; ``` ##### textHead [Section titled “textHead”](#texthead) ```ts 1 readonly textHead: string; ``` First [TEXT\_HEAD\_LIMIT](/api/traces/#text_head_limit) characters of the prompt’s text, whitespace-collapsed. ##### turnUuid [Section titled “turnUuid”](#turnuuid) ```ts 1 readonly turnUuid: string; ``` The `uuid` of the first user record carrying this `promptId`, the `(sessionId, uuid)` cite. *** ### ScannedFile [Section titled “ScannedFile”](#scannedfile) One file’s outcome, whether or not it was read. #### Properties [Section titled “Properties”](#properties-6) ##### action [Section titled “action”](#action) ```ts 1 readonly action: WatermarkAction; ``` ##### agentCount [Section titled “agentCount”](#agentcount) ```ts 1 readonly agentCount: number; ``` `agent_count` for the `traces` row: this file’s distinct `agentId`s unioned with the sidecar filenames of its session. `0` for a skipped or unreadable file. ##### extract [Section titled “extract”](#extract-1) ```ts 1 readonly extract: SessionExtract | null; ``` Absent for a `skip`, because a skipped file is not opened and yields no extract. Also absent when the read failed, so a persister writes nothing for the file and the next run retries it. ##### file [Section titled “file”](#file) ```ts 1 readonly file: SessionFile; ``` ##### watermark [Section titled “watermark”](#watermark) ```ts 1 readonly watermark: Watermark; ``` The watermark to store. Unchanged from the previous one for a `skip` and for a failed read, because a failed read consumed nothing and advancing past the stored offset would make the next run’s size+mtime comparison skip a transcript nobody has read. *** ### ScanReport [Section titled “ScanReport”](#scanreport) A whole scan: per-file outcomes plus the totals an operator reads. The four action counters PARTITION `files`: `skipped + tailed + rescanned + failed` is exactly `files.length`. That is why a failed read counts here and nowhere else — counting it as `tailed` would report a transcript as read when its rows were never extracted, and a scan whose numbers add up is the only way an operator can tell a quiet night from a broken one. #### Properties [Section titled “Properties”](#properties-7) ##### bytesRead [Section titled “bytesRead”](#bytesread-1) ```ts 1 readonly bytesRead: number; ``` Bytes actually read. The number the incremental design exists to keep small. ##### failed [Section titled “failed”](#failed) ```ts 1 readonly failed: number; ``` Files the scan planned to read and could not: an absent file, a permission rejection, a transient IO error. Each holds its stored watermark, so the next run retries it. ##### files [Section titled “files”](#files) ```ts 1 readonly files: readonly ScannedFile[]; ``` ##### rescanned [Section titled “rescanned”](#rescanned) ```ts 1 readonly rescanned: number; ``` Files read from byte zero, and read successfully. ##### skipped [Section titled “skipped”](#skipped) ```ts 1 readonly skipped: number; ``` ##### tailed [Section titled “tailed”](#tailed) ```ts 1 readonly tailed: number; ``` Files read from their recorded offset forward, and read successfully. *** ### SessionExtract [Section titled “SessionExtract”](#sessionextract) Everything one session file yields: the `traces` row’s content fields, its prompt rows, and the scan’s counters. `file_size`/`file_mtime`/`indexed_at` are absent. They belong to the stat the watermark already took, and duplicating them would give the same fact two sources. #### Properties [Section titled “Properties”](#properties-8) ##### agentIds [Section titled “agentIds”](#agentids-1) ```ts 1 readonly agentIds: readonly string[]; ``` Distinct `agentId` seen in this file, first-appearance order. Empty for a main session. ##### aiTitle [Section titled “aiTitle”](#aititle-1) ```ts 1 readonly aiTitle: string | null; ``` ##### counters [Section titled “counters”](#counters) ```ts 1 readonly counters: ParseCounters; ``` ##### cwd [Section titled “cwd”](#cwd-1) ```ts 1 readonly cwd: string | null; ``` ##### endedAt [Section titled “endedAt”](#endedat) ```ts 1 readonly endedAt: string | null; ``` Latest record instant, ISO-8601 UTC. ##### entrypoint [Section titled “entrypoint”](#entrypoint-1) ```ts 1 readonly entrypoint: string | null; ``` ##### filePath [Section titled “filePath”](#filepath) ```ts 1 readonly filePath: string; ``` Absolute path of the file scanned. ##### firstPrompt [Section titled “firstPrompt”](#firstprompt-1) ```ts 1 readonly firstPrompt: string; ``` ##### gitBranch [Section titled “gitBranch”](#gitbranch-1) ```ts 1 readonly gitBranch: string | null; ``` ##### model [Section titled “model”](#model) ```ts 1 readonly model: string | null; ``` Most frequent `message.model` on assistant records, excluding [SYNTHETIC\_MODEL](/api/traces/#synthetic_model). ##### promptCount [Section titled “promptCount”](#promptcount) ```ts 1 readonly promptCount: number; ``` Distinct `promptId` count on user records. Equals `prompts.length`. ##### prompts [Section titled “prompts”](#prompts-1) ```ts 1 readonly prompts: readonly PromptRow[]; ``` ##### sessionId [Section titled “sessionId”](#sessionid-1) ```ts 1 readonly sessionId: string | null; ``` From the first record carrying one, bare types included. `null` when the file has none. ##### slug [Section titled “slug”](#slug-1) ```ts 1 readonly slug: string; ``` The `~/.claude/projects/` directory name, which is a *path* slug. The `slug` field on a subagent record is a title slug for the agent’s task, and is a different fact entirely. ##### startedAt [Section titled “startedAt”](#startedat) ```ts 1 readonly startedAt: string | null; ``` Earliest record instant, ISO-8601 UTC. ##### turnCount [Section titled “turnCount”](#turncount-1) ```ts 1 readonly turnCount: number; ``` Enveloped records, meaning those carrying a `uuid`. A `pr-link` has a session but no turn. ##### version [Section titled “version”](#version-1) ```ts 1 readonly version: string | null; ``` *** ### SessionFile [Section titled “SessionFile”](#sessionfile) One discovered transcript file with the stat the watermark decision needs. #### Properties [Section titled “Properties”](#properties-9) ##### agentId [Section titled “agentId”](#agentid-1) ```ts 1 readonly agentId: string | null; ``` Set only on a sidecar, from its `agent-.jsonl` filename. ##### filePath [Section titled “filePath”](#filepath-1) ```ts 1 readonly filePath: string; ``` Absolute path, built from the caller’s `traceRoot`. ##### kind [Section titled “kind”](#kind) ```ts 1 readonly kind: SessionFileKind; ``` ##### mtimeMs [Section titled “mtimeMs”](#mtimems-1) ```ts 1 readonly mtimeMs: number; ``` ##### sessionId [Section titled “sessionId”](#sessionid-2) ```ts 1 readonly sessionId: string; ``` The session this file belongs to, read from the path. That is the filename stem for a main session and the owning directory name for a sidecar. Deriving it from the path costs one `readdir` per directory and opens no file. ##### size [Section titled “size”](#size-1) ```ts 1 readonly size: number; ``` ##### slug [Section titled “slug”](#slug-2) ```ts 1 readonly slug: string; ``` The `projects/` directory name, which is the cwd slug, `traces.slug`. *** ### Watermark [Section titled “Watermark”](#watermark-1) What a previous scan recorded about a file, mirroring the `trace_watermarks` row (design §3.3). `@memhtml/index`’s trace persister owns the table; this module owns the arithmetic. * `size`: file length in bytes at scan time. * `mtimeMs`: modification time in **milliseconds since the Unix epoch**, the unit `node:fs`’s `Stats.mtimeMs` reports. The SQL column stores an ISO-8601 string, so the adapter converts at the boundary and this type carries one unit only. * `byteOff`: a **0-based byte offset** into the file, one past the last byte consumed, and therefore the `start` of the next read. Equal to `size` after a complete scan. #### Properties [Section titled “Properties”](#properties-10) ##### byteOff [Section titled “byteOff”](#byteoff) ```ts 1 readonly byteOff: number; ``` ##### mtimeMs [Section titled “mtimeMs”](#mtimems-2) ```ts 1 readonly mtimeMs: number; ``` ##### size [Section titled “size”](#size-2) ```ts 1 readonly size: number; ``` *** ### WatermarkPlan [Section titled “WatermarkPlan”](#watermarkplan) An action plus the byte offset to open the read stream at. #### Properties [Section titled “Properties”](#properties-11) ##### action [Section titled “action”](#action-1) ```ts 1 readonly action: WatermarkAction; ``` ##### startByte [Section titled “startByte”](#startbyte-1) ```ts 1 readonly startByte: number; ``` 0-based byte offset to pass as `createReadStream({ start })`. Always 0 for a rescan. ## Type Aliases [Section titled “Type Aliases”](#type-aliases) ### ReadRecordType [Section titled “ReadRecordType”](#readrecordtype) ```ts 1 type ReadRecordType = typeof READ_RECORD_TYPES[number]; ``` *** ### SessionFileKind [Section titled “SessionFileKind”](#sessionfilekind) ```ts 1 type SessionFileKind = "session" | "subagent"; ``` * `session`: `/projects//.jsonl`, the main transcript. * `subagent`: `/projects///subagents/agent-.jsonl`. Its records carry the parent `sessionId` too, so it indexes into the same session row. *** ### SkipRecordType [Section titled “SkipRecordType”](#skiprecordtype) ```ts 1 type SkipRecordType = typeof SKIP_RECORD_TYPES[number]; ``` *** ### WatermarkAction [Section titled “WatermarkAction”](#watermarkaction) ```ts 1 type WatermarkAction = "skip" | "tail" | "rescan"; ``` * `skip`: nothing changed, so do not open the file. * `tail`: the file grew by append, so read from [WatermarkPlan.startByte](/api/traces/#watermarkplan). * `rescan`: the file was rewritten, compacted, or is unknown, so read from byte 0. *** ### WatermarkReader [Section titled “WatermarkReader”](#watermarkreader) ```ts 1 type WatermarkReader = (filePath) => Effect.Effect; ``` Reads a file’s stored watermark. `null` for a file never scanned. #### Parameters [Section titled “Parameters”](#parameters) ##### filePath [Section titled “filePath”](#filepath-2) `string` #### Returns [Section titled “Returns”](#returns) `Effect.Effect`<[`Watermark`](/api/traces/#watermark-1) | `null`, `StorageFailure`> ## Variables [Section titled “Variables”](#variables) ### FIRST\_PROMPT\_LIMIT [Section titled “FIRST\_PROMPT\_LIMIT”](#first_prompt_limit) ```ts 1 const FIRST_PROMPT_LIMIT: 500 = 500; ``` `traces.first_prompt` is an index entry, not a copy of the prompt. *** ### PROJECTS\_DIR [Section titled “PROJECTS\_DIR”](#projects_dir) ```ts 1 const PROJECTS_DIR: "projects" = "projects"; ``` The `projects/` directory the per-cwd slug directories live under. *** ### READ\_RECORD\_TYPES [Section titled “READ\_RECORD\_TYPES”](#read_record_types) ```ts 1 const READ_RECORD_TYPES: readonly ["user", "assistant", "system", "attachment", "agent-name", "ai-title", "pr-link"]; ``` Record types the parser reads. Applied as an allowlist *before* any field access, because the bare types carry no envelope. Reaching for `record.cwd` on a `file-history-snapshot` would read a field that does not exist, on a record that is not about a session at all. *** ### SKIP\_RECORD\_TYPES [Section titled “SKIP\_RECORD\_TYPES”](#skip_record_types) ```ts 1 const SKIP_RECORD_TYPES: readonly ["last-prompt", "mode", "permission-mode", "queue-operation", "file-history-snapshot", "file-history-delta"]; ``` Counted and skipped, never an error. `file-history-snapshot` and `file-history-delta` carry no `sessionId` and no envelope at all (probed on 5,387 real files, 2026-08-01: their keys are `{isSnapshotUpdate, messageId, snapshot, type}` and `{backup, messageId, snapshotMessageId, trackingPath, timestamp, type}`), so a record with no session key is dropped rather than treated as malformed input. *** ### SUBAGENTS\_DIR [Section titled “SUBAGENTS\_DIR”](#subagents_dir) ```ts 1 const SUBAGENTS_DIR: "subagents" = "subagents"; ``` The subdirectory holding a session’s subagent sidecars. *** ### SYNTHETIC\_MODEL [Section titled “SYNTHETIC\_MODEL”](#synthetic_model) ```ts 1 const SYNTHETIC_MODEL: "" = ""; ``` The placeholder model id the runtime emits for a non-model turn; never a session’s model. *** ### TEXT\_HEAD\_LIMIT [Section titled “TEXT\_HEAD\_LIMIT”](#text_head_limit) ```ts 1 const TEXT_HEAD_LIMIT: 200 = 200; ``` `trace_prompts.text_head` is an index entry, not a copy of the prompt. ## Functions [Section titled “Functions”](#functions) ### advanceWatermark() [Section titled “advanceWatermark()”](#advancewatermark) ```ts 1 function advanceWatermark( 2 stat, 3 startByte, 4 bytesRead 5 ): Watermark; ``` The watermark to store after a scan consumed `bytesRead` bytes starting at `startByte`. `size`/`mtimeMs` come from the stat taken *before* the read, so a file appended to during the scan compares unequal next time and gets tailed rather than skipped. #### Parameters [Section titled “Parameters”](#parameters-1) ##### stat [Section titled “stat”](#stat) [`FileStat`](/api/traces/#filestat) ##### startByte [Section titled “startByte”](#startbyte-2) `number` ##### bytesRead [Section titled “bytesRead”](#bytesread-2) `number` #### Returns [Section titled “Returns”](#returns-1) [`Watermark`](/api/traces/#watermark-1) *** ### agentCountFor() [Section titled “agentCountFor()”](#agentcountfor) ```ts 1 function agentCountFor(extract, sidecarAgentIds): number; ``` The `agent_count` for a `traces` row: distinct agents named by the session’s records unioned with those named by its sidecar filenames (design §7). The union covers both sides, because an agent’s sidecar exists before its first record lands, and a resumed session’s records can name an agent whose sidecar has been pruned. #### Parameters [Section titled “Parameters”](#parameters-2) ##### extract [Section titled “extract”](#extract-2) [`SessionExtract`](/api/traces/#sessionextract) ##### sidecarAgentIds [Section titled “sidecarAgentIds”](#sidecaragentids) readonly `string`\[] #### Returns [Section titled “Returns”](#returns-2) `number` *** ### discoverSessions() [Section titled “discoverSessions()”](#discoversessions) ```ts 1 function discoverSessions(traceRoot): Effect; ``` Every transcript file under `traceRoot`, main sessions and subagent sidecars alike. `traceRoot` is a parameter with `~/.claude` as the caller’s default, and it is not hardcoded here, so a test drives a fixture tree and an operator can point the indexer at an archive. A file that vanishes between `readdir` and `stat` is dropped rather than failed. Transcripts are written by a live process that may compact or delete one mid-scan, and the next run rediscovers whatever is there. #### Parameters [Section titled “Parameters”](#parameters-3) ##### traceRoot [Section titled “traceRoot”](#traceroot) `string` #### Returns [Section titled “Returns”](#returns-3) `Effect`\ *** ### emptyAccumulator() [Section titled “emptyAccumulator()”](#emptyaccumulator) ```ts 1 function emptyAccumulator(): Accumulator; ``` A fold state with nothing seen yet. #### Returns [Section titled “Returns”](#returns-4) [`Accumulator`](/api/traces/#accumulator) *** ### extractFromText() [Section titled “extractFromText()”](#extractfromtext) ```ts 1 function extractFromText(text, file): SessionExtract; ``` Fold a whole JSONL text. The streaming reader in `parse.ts` is the production path; this is the same fold for a string in hand. #### Parameters [Section titled “Parameters”](#parameters-4) ##### text [Section titled “text”](#text) `string` ##### file [Section titled “file”](#file-1) ###### filePath [Section titled “filePath”](#filepath-3) `string` ###### slug [Section titled “slug”](#slug-3) `string` #### Returns [Section titled “Returns”](#returns-5) [`SessionExtract`](/api/traces/#sessionextract) *** ### finalizeExtract() [Section titled “finalizeExtract()”](#finalizeextract) ```ts 1 function finalizeExtract(accumulator, file): SessionExtract; ``` Close the fold into the immutable extract. Pure with respect to the accumulator. #### Parameters [Section titled “Parameters”](#parameters-5) ##### accumulator [Section titled “accumulator”](#accumulator-1) [`Accumulator`](/api/traces/#accumulator) ##### file [Section titled “file”](#file-2) ###### filePath [Section titled “filePath”](#filepath-4) `string` ###### slug [Section titled “slug”](#slug-4) `string` #### Returns [Section titled “Returns”](#returns-6) [`SessionExtract`](/api/traces/#sessionextract) *** ### foldLine() [Section titled “foldLine()”](#foldline) ```ts 1 function foldLine(accumulator, line): Accumulator; ``` Fold one raw line into the accumulator. Total, so every input either updates state or a counter, and no input throws. A blank line is not a record and is not counted. Mutates and returns `accumulator`. A fresh object per line would allocate once per line of a 3.67 GB corpus. #### Parameters [Section titled “Parameters”](#parameters-6) ##### accumulator [Section titled “accumulator”](#accumulator-2) [`Accumulator`](/api/traces/#accumulator) ##### line [Section titled “line”](#line) `string` #### Returns [Section titled “Returns”](#returns-7) [`Accumulator`](/api/traces/#accumulator) *** ### mergePrompts() [Section titled “mergePrompts()”](#mergeprompts) ```ts 1 function mergePrompts(stored, tail): readonly PromptRow[]; ``` Concatenate two prompt lists into one per-session first-appearance ordering. A tail’s ordinals are 0-based over the appended slice, so they are renumbered from the end of the stored list. Without that, every tail would collide with ordinal 0 and `trace_prompts.ordinal` would stop being an order at all. A `promptId` present in both sides keeps its stored ordinal, uuid, and instant, since it began before the tail. It takes the tail’s `textHead` only when the stored one is empty, which is how a prompt whose text arrived after the boundary gets indexed. #### Parameters [Section titled “Parameters”](#parameters-7) ##### stored [Section titled “stored”](#stored) readonly [`PromptRow`](/api/traces/#promptrow)\[] ##### tail [Section titled “tail”](#tail) readonly [`PromptRow`](/api/traces/#promptrow)\[] #### Returns [Section titled “Returns”](#returns-8) readonly [`PromptRow`](/api/traces/#promptrow)\[] *** ### mergeTailExtract() [Section titled “mergeTailExtract()”](#mergetailextract) ```ts 1 function mergeTailExtract(stored, tail): SessionExtract; ``` Merge a tail’s extract into the session’s stored one. **Tails only**, because a rescan’s extract already describes the whole file and replaces the stored row outright. The merge exists because a tail’s extract describes the *appended slice* and not the session. Its `first_prompt` is a prompt from the middle of the conversation, its `started_at` is an hour after the session began, its `turn_count` counts only new turns, and its prompt ordinals restart at 0. Every field below states which side owns it and why. The producer owns these reading semantics, so an indexer that merged the fields itself would have to rediscover all of it. #### Parameters [Section titled “Parameters”](#parameters-8) ##### stored [Section titled “stored”](#stored-1) [`SessionExtract`](/api/traces/#sessionextract) ##### tail [Section titled “tail”](#tail-1) [`SessionExtract`](/api/traces/#sessionextract) #### Returns [Section titled “Returns”](#returns-9) [`SessionExtract`](/api/traces/#sessionextract) *** ### parseSessionFile() [Section titled “parseSessionFile()”](#parsesessionfile) ```ts 1 function parseSessionFile( 2 filePath, 3 startByte?, 4 identity? 5 ): Effect; ``` Stream a transcript from `startByte` and fold it into a [SessionExtract](/api/traces/#sessionextract). Cannot fail. A truncated line and a line of binary garbage degrade to counters on the extract, while a read the filesystem refuses outright (absent file, permission rejection, transient IO error) degrades to an empty result flagged `readFailed`. This runs over thousands of files written by a live process, so one unreadable transcript costs that transcript’s rows and not the whole run. The counters and the flag are what an operator reads in place of an error, and the flag is what lets a caller hold its watermark still so the next run retries. `startByte` is a 0-based byte offset and must be one [watermarkPlan](/api/traces/#watermarkplan-1) produced. A caller-invented offset can land mid-line, and that first partial line is then counted as malformed rather than recovered. #### Parameters [Section titled “Parameters”](#parameters-9) ##### filePath [Section titled “filePath”](#filepath-5) `string` ##### startByte? [Section titled “startByte?”](#startbyte-3) `number` = `0` ##### identity? [Section titled “identity?”](#identity) [`FileIdentity`](/api/traces/#fileidentity) #### Returns [Section titled “Returns”](#returns-10) `Effect`<[`ParseResult`](/api/traces/#parseresult), `never`> *** ### scanTraceRoot() [Section titled “scanTraceRoot()”](#scantraceroot) ```ts 1 function scanTraceRoot(traceRoot, readWatermark): Effect; ``` Scan every transcript under `traceRoot`, reading only what the watermarks say changed. `traceRoot` is a parameter. `~/.claude` is the caller’s default rather than this module’s constant, so the whole scan is drivable against a fixture tree. Files are processed sequentially. Concurrency here would trade a bounded, predictable IO profile for contention with the live process that is *writing* these transcripts, and the incremental watermark has already reduced a daily run to the handful of files that changed. #### Parameters [Section titled “Parameters”](#parameters-10) ##### traceRoot [Section titled “traceRoot”](#traceroot-1) `string` ##### readWatermark [Section titled “readWatermark”](#readwatermark) [`WatermarkReader`](/api/traces/#watermarkreader) #### Returns [Section titled “Returns”](#returns-11) `Effect`<[`ScanReport`](/api/traces/#scanreport), `StorageFailure`> *** ### sessionIdFromPath() [Section titled “sessionIdFromPath()”](#sessionidfrompath) ```ts 1 function sessionIdFromPath(filePath, traceRoot): string | null; ``` The session id a transcript path names, or `null` when the path is not one. Pure. #### Parameters [Section titled “Parameters”](#parameters-11) ##### filePath [Section titled “filePath”](#filepath-6) `string` ##### traceRoot [Section titled “traceRoot”](#traceroot-2) `string` #### Returns [Section titled “Returns”](#returns-12) `string` | `null` *** ### sidecarAgentIds() [Section titled “sidecarAgentIds()”](#sidecaragentids-1) ```ts 1 function sidecarAgentIds(files, sessionId): readonly string[]; ``` The `agentId`s of a session’s sidecars, from filenames alone. Feeds `agent_count` without opening a sidecar, because the count is a property of the tree. The sidecars’ own records are indexed on their own pass. #### Parameters [Section titled “Parameters”](#parameters-12) ##### files [Section titled “files”](#files-1) readonly [`SessionFile`](/api/traces/#sessionfile)\[] ##### sessionId [Section titled “sessionId”](#sessionid-3) `string` #### Returns [Section titled “Returns”](#returns-13) readonly `string`\[] *** ### slugFromPath() [Section titled “slugFromPath()”](#slugfrompath) ```ts 1 function slugFromPath(filePath): string; ``` The `projects/` directory a transcript sits under, derived from its path. A sidecar sits two levels deeper (`//subagents/`), so the slug is the grandparent’s parent there. Returns `""` for a path outside the tree. The extract still carries a real `session_id` from the records, so an unslugged file indexes rather than being lost. #### Parameters [Section titled “Parameters”](#parameters-13) ##### filePath [Section titled “filePath”](#filepath-7) `string` #### Returns [Section titled “Returns”](#returns-14) `string` *** ### userText() [Section titled “userText()”](#usertext) ```ts 1 function userText(message): string; ``` The text of a user record’s `message.content`, or `""` when it carries none. Content arrives in two shapes and both hold real prompt text: a bare string, and a block list whose `text` blocks are joined. Probed 2026-08-02: of the distinct prompts in six large sessions, the block-list form was the first appearance for 24 of 30 in one file and 34 of 38 in another, so a string-only rule would leave `first_prompt` empty for most sessions. A `tool_result`-only list yields `""`, because a tool’s output is not something the user said. #### Parameters [Section titled “Parameters”](#parameters-14) ##### message [Section titled “message”](#message) `unknown` #### Returns [Section titled “Returns”](#returns-15) `string` *** ### watermarkAction() [Section titled “watermarkAction()”](#watermarkaction-1) ```ts 1 function watermarkAction(prev, curr): WatermarkAction; ``` Decide how to read a file given what the last scan recorded. Both size *and* mtime must match to skip. Size alone would miss an in-place rewrite that happens to preserve the length; mtime alone would miss a write inside the same clock tick. A grown file is tailed only when mtime also advanced or held steady. A file that grew while its mtime moved *backward* was restored or rewritten instead of appended to, so it is rescanned. Shrinking is unambiguous, because bytes the watermark counted are gone and any offset into the file is now meaningless. A `byteOff` past the current size is treated as a rescan even when size grew, because that offset could only come from a larger earlier file. The growth is a rewrite that has not yet reached the old length. #### Parameters [Section titled “Parameters”](#parameters-15) ##### prev [Section titled “prev”](#prev) [`Watermark`](/api/traces/#watermark-1) | `null` ##### curr [Section titled “curr”](#curr) [`FileStat`](/api/traces/#filestat) #### Returns [Section titled “Returns”](#returns-16) [`WatermarkAction`](/api/traces/#watermarkaction) *** ### watermarkPlan() [Section titled “watermarkPlan()”](#watermarkplan-1) ```ts 1 function watermarkPlan(prev, curr): WatermarkPlan; ``` [watermarkAction](/api/traces/#watermarkaction-1) with the read offset the caller needs. #### Parameters [Section titled “Parameters”](#parameters-16) ##### prev [Section titled “prev”](#prev-1) [`Watermark`](/api/traces/#watermark-1) | `null` ##### curr [Section titled “curr”](#curr-1) [`FileStat`](/api/traces/#filestat) #### Returns [Section titled “Returns”](#returns-17) [`WatermarkPlan`](/api/traces/#watermarkplan) # Glossary > The project's domain vocabulary, one entry each, with the page that develops the term. ## 1. How to read this page [Section titled “1. How to read this page”](#1-how-to-read-this-page) The terms are in alphabetical order, ignoring a leading “The”. Each entry defines one term and links to the page that develops it. Several of these words mean something looser elsewhere. Each entry gives the narrow sense this project uses, and says so when a nearby term is easy to confuse with it. ## 2. Terms [Section titled “2. Terms”](#2-terms) ### Arc [Section titled “Arc”](#arc) A memory of type `arc`: a summary of a pattern that runs across many other memories, usually about how the agent itself works. Sleep’s `arc-synthesis` phase writes arcs into `areas/arcs/` when a model is bound, one commit per arc. No MCP write tool lets an agent name the type, because an agent naming an arc would assert a conclusion the corpus has not earned. On the CLI, `memhtml write` and `memhtml apply` accept it, for curated imports. Retention triage and reprieve never archive an arc, and `search` and `recall` refuse `--type arc`. `recall` gives arcs their own 9,000-character budget, apart from the budget other memories share. See [The sleep pipeline](/internals/the-sleep-pipeline/). ### Archive path [Section titled “Archive path”](#archive-path) `archive//`, where an evicted memory moves. The year is the year of the eviction, and the original path is kept whole beneath it. So `originalPathFor` gets the original path back by stripping exactly one `archive//` prefix, and sleep’s `integrity` phase can compute where a linked file went from its old path, without comparing file contents. Two cases bend the rule. If a file already sits at the archive path, because the same path was archived earlier in the same year, `memhtml archive`, `memhtml correct`, and `memhtml task status done` fail with `ERR_GIT`, and the file stays where it was. A sleep phase instead adds `-2`, `-3`, and so on to the file name, and that suffixed path no longer maps back to the original. Archiving a file that is already archived nests a second prefix. See [Store layout and path algebra](/internals/store-layout-and-path-algebra/). ### Authored edge and derived edge [Section titled “Authored edge and derived edge”](#authored-edge-and-derived-edge) The two kinds of link between memories. An authored edge is a `` element in a memory file’s head, written by a caller or by a sleep phase. The index stores it as an `edges` row with `derived = 0`, and a rebuild restores it from the file. A derived edge exists only as an `edges` row with `derived = 1`. Sleep’s `relationship-mining` phase writes these from embedding similarity: `relates_to`, plus `laterally_related` under `--deep`. A rebuild drops them, and the next run mines them again. The retention score counts only authored contradictions. Every edge also has one of four classes, `memory`, `person`, `provenance`, or `task`, decided by its rel, and the memory-graph queries read only the `memory` class. See [Edge encoding](/internals/edge-encoding/). ### Claim [Section titled “Claim”](#claim) The one `` span in a memory’s `
`: the sentence that states the memory’s fact. The format requires exactly one. It must contain text other than code, sit inside the article’s first `

` or `

  • `, and never sit inside an `