import type { MemoryNoteHeader, MemoryNoteRecord, MemoryStore } from "./memory.js"; import type { ModelRef, TaskSpec, TaskResult } from "./types.js"; /** * Selective memory recall (design/65 §2.2 — port of CC `findRelevantMemories`). For a LARGE memory scope, * instead of injecting the whole `` block (inject-all), we inject a compact **manifest** (one * header line per note) + a side-query that picks the ≤K relevant note ids + only those bodies. Small * scopes keep inject-all (the common case after consolidation) — this path is threshold-gated, so most * tasks pay nothing. * * The model side-query is an injected **seam** ({@link MemorySelector}); core provides a brain-backed * default ({@link createBrainMemorySelector}) and a deployment can override it. The pipeline here is pure + * deterministic given a selector: it validates the returned ids against the manifest (rejects hallucinated * ids, council #5), caps at K, applies a hard timeout (council #12), and on ANY selector failure/timeout * DEGRADES to inject-all (council #4 — never silently drop knowledge). Output goes in the variable tail * (manifest + selected bodies are per-user/volatile, like the current memory block). */ /** Default max notes the side-query may select (CC `findRelevantMemories` ≤5). */ export declare const DEFAULT_MAX_SELECTED = 5; /** Default max 1-hop link-followed notes appended after the selected ones (design/65 P3). */ export declare const DEFAULT_MAX_LINKED = 3; /** Default side-query hard timeout (ms) — council #12; on timeout we degrade to inject-all. */ export declare const DEFAULT_SELECT_TIMEOUT_MS = 5000; /** * Render a memory note's age as a natural-language phrase (CC-parity P0-1, design/65). CC's eval found * models reason about memory age much better from natural language ("3 days ago") than from a boolean * "older note" flag (verify-claims 0/2 → 3/3 once the age was spelled out), because the model can weigh * staleness proportionally instead of as a single cliff. This is the relative phrase only; the * point-in-time / verify-before-trust discipline still lives in {@link RECALL_CAVEAT} + the per-note * "verify it's still current" hint (the age is ADDITIONAL signal, not a replacement). * * Buckets (each chooses the coarsest unit that's ≥1): `today` (<1 day, incl. future mtime / clock skew → * clamped to today, never a negative age), `yesterday` (1–2 days), `N days ago` (2–7 days), * `N weeks ago` (1 week – ~1 month), `N months ago` (≥ ~1 month). N is floored to the whole unit. */ export declare function formatMemoryAge(ageMs: number): string; /** * Read-side recall caveat (design/65 §8#2 / §9#1 — ported from CC's MEMORY_DRIFT_CAVEAT + * TRUSTING_RECALL_SECTION). The council found CC's two-section form is **eval-validated** (verify-claims * 0/2→3/3) and that the structure carries the effect: the precise decision-point heading * "## Before recommending from memory" scored 3/3 while an abstract heading or a buried bullet scored 0/3. * So this is deliberately TWO sections — a drift bullet + a titled action section with the frame-shift * sentence and concrete verify actions — NOT one merged paragraph (same semantics, 3× worse eval). * MEMORY_SAFETY already carries the authority hierarchy; this adds the point-in-time / verify-before-trust * discipline selective recall specifically needs. */ export declare const RECALL_CAVEAT: string; export interface MemorySelectRequest { /** The task objective the recall is for. */ objective: string; /** The manifest the selector chooses from (id + description [+ type]). */ manifest: MemoryNoteHeader[]; /** Tools the task will use — for these, keep gotchas/warnings, skip pure usage notes (design/65 §2.2). */ recentTools?: string[]; } /** The side-query seam: pick the relevant note ids from the manifest. MUST be fail-fast (throw → degrade). */ export type MemorySelector = (req: MemorySelectRequest, signal?: AbortSignal) => Promise; export interface SelectiveRecallOptions { store: MemoryStore; scope: string; objective: string; selector: MemorySelector; recentTools?: string[]; /** Absolute time for the freshness caveat (default Date.now()). */ nowMs?: number; maxSelected?: number; /** Max 1-hop `[[name]]` link-followed notes appended after the selected ones (design/65 P3). * `0` disables link-following. Default {@link DEFAULT_MAX_LINKED}. */ maxLinked?: number; timeoutMs?: number; signal?: AbortSignal; /** * CC-parity P1 (durable cross-worker recall de-dup). Note ids ALREADY surfaced to this logical * conversation in earlier turns/runs (CC's in-process `alreadySurfaced`/readFileState filter, lifted * to a **durable, caller-owned** set so it survives task hops, resume, and multi-worker fan-out). When * provided, these ids are **filtered out of the manifest BEFORE the selector sees it** — the selector * never sees an already-shown note, so it cannot re-select it, so a long/resumed/multi-worker session * does not re-inject the same bodies. core does NOT store this set (like {@link CheckpointStore}, the * durable store is the caller's): the caller persists {@link surfacedIds} from one call and feeds it * back here as `priorSurfacedIds` on the next. Omitted ⇒ no filtering (the prior, non-de-duped behavior). */ priorSurfacedIds?: ReadonlySet; } /** * Result of {@link selectAndComposeMemory}. `surfacedIds` (CC-parity P1) is the set of note ids whose * BODIES this call injected into the prompt — the caller appends them to its durable surfaced-set and * feeds that back as {@link SelectiveRecallOptions.priorSurfacedIds} next time, so a long/resumed/ * multi-worker session does not re-inject the same notes. THREE shapes (all carry `surfacedIds`): * * - **block branch** (`{ block: string }`): a selective-recall block was rendered. `surfacedIds` = the * validated selection + the 1-hop linked notes (everything whose body was rendered). The caller injects * `block` verbatim. * * - **no-new branch** (`{ block: undefined }`): a SUCCESS, NOT a degrade. Every selectable candidate was * already surfaced earlier (filtered out by {@link SelectiveRecallOptions.priorSurfacedIds}), so there is * NOTHING NEW to inject. The caller injects NOTHING (no block) — it must NOT fall back to inject-all, * because inject-all would re-dump the WHOLE scope including the already-shown notes, defeating the * de-dup. `surfacedIds` is `[]` (this call added no new note to the durable set; the prior ids are * already in the caller's store). Distinguished from `degrade` precisely so the caller does not * re-inject (the bug this branch fixes). `"block" in result` is TRUE here (the key is present, value * `undefined`), so the caller's `"degrade" in result` narrow routes this to the non-degrade path. * * - **degrade branch** (`{ degrade: true }`): a true FALLBACK — the store can't do selective recall * (missing manifest/getByIds), the scope is empty, OR the selector failed/timed out. The caller falls * back to inject-all (never drops knowledge, council #4), a best-effort FULL DUMP that * `composeMemoryBlock` hard-truncates to a head slice (so it does NOT reliably render every body), and * the caller already holds the prior surfaced ids. Degrade is therefore not a precise per-id surface that * can be de-duped: `surfacedIds` is ALWAYS `[]` and degrade does **not** advance the durable surfaced-set. * (Reporting the full header id list here would over-claim the truncated tail as surfaced → permanent * silent under-recall on the next turn.) See {@link selectAndComposeMemory}. */ export type SelectiveRecallResult = { block: string; surfacedIds: string[]; } | { block: undefined; surfacedIds: string[]; } | { degrade: true; surfacedIds: string[]; }; /** * Resolve the `[[name]]` references in the selected notes' bodies to manifest headers — ONE hop * (design/65 P3: "recall one note → bring its [[links]]"; linked bodies are NOT re-scanned, so a link * chain can't pull in the whole graph). Pure-text convention: names resolve against the manifest's * `name` field; an unresolved name is silently skipped (a `[[link]]` to a not-yet-written note is legal * authoring, design/65 §2.1); a name collision resolves to the newest note (strict mtime `>`; on a * tie the first-encountered header wins). Already-selected targets are excluded (they're injected * anyway). Returns at most `max` ids, in first-mention order. */ export declare function resolveLinkedIds(headers: MemoryNoteHeader[], selected: MemoryNoteRecord[], max: number): string[]; /** Render the manifest as compact lines, double-capped (council #7). One line per note, guaranteed. */ export declare function buildManifestText(headers: MemoryNoteHeader[]): string; /** Keep only ids present in the manifest (reject hallucinated, council #5), de-duped, capped at `max`. */ export declare function validateSelectedIds(headers: MemoryNoteHeader[], ids: string[], max: number): string[]; /** * Render the BODY of a selective-recall block: the optional `recall` manifest index + the selected note * bodies (+ 1-hop linked notes), WITHOUT the outer `` wrapper and WITHOUT {@link RECALL_CAVEAT}. * The wrapper + caveat are the caller's job — `composeSelectiveMemoryBlock` (single-scope) places ONE caveat * inside ONE ``, and the layered renderer places ONE caveat inside ONE * `` ahead of N `` subsections (MAJOR fix: no nested fences, no per-scope * caveat duplication). * * `recallable` controls the "Memory index — call recall …" affordance (MAJOR fix): the on-demand `recall` tool * is bound to a SINGLE scope, so the "load in full via recall" instruction is only TRUE for that scope. Pass * `false` for non-recallable layers — the selected bodies are already rendered inline, so the index is just a * false affordance that wastes a tool call (model searches the wrong scope → "No memory matches"). * * Notes are tagged with their natural-language age (CC-parity P0-1, design/65; >1-day-old ones keep the * "verify it's still current" hint, council #8). `linked` (design/65 P3) are 1-hop `[[name]]`-followed notes, * rendered in their own clearly-labeled subsection AFTER the selected ones — expanded as DATA under the same * fence/sanitize/byte-cap discipline (design/65 §8#9: a followed link must not become markup or escalate authority). */ export declare function composeSelectiveBody(manifestText: string, selected: MemoryNoteRecord[], nowMs: number, linked?: MemoryNoteRecord[], recallable?: boolean): string; /** Render the single-scope selective block: ONE `` with ONE * {@link RECALL_CAVEAT} at the top and the {@link composeSelectiveBody} (manifest + selected bodies + links). * `recall` is bound to this single scope, so the manifest affordance is recallable. * * @deprecated design/138 S4 — served ONLY the retired legacy memoryStore injection path; the Runner no * longer calls this. Kept exported one major for direct/standalone store-level use; removal is the next major. */ export declare function composeSelectiveMemoryBlock(scope: string, manifestText: string, selected: MemoryNoteRecord[], nowMs: number, linked?: MemoryNoteRecord[]): string; /** * Run the selective-recall pipeline. Returns the composed block (+ {@link SelectiveRecallResult.surfacedIds}), * or `{ degrade: true, surfacedIds: [] }` when the selector fails/times out (caller falls back to inject-all — * never drops knowledge, council #4). Degrade always reports `surfacedIds: []`: inject-all is a best-effort * full dump (head-truncated at the cap), so it cannot honestly attribute a precise per-id surface, and the * caller already holds the prior ids — degrade does NOT advance the durable surfaced-set. * * CC-parity P1 (durable cross-worker de-dup): when {@link SelectiveRecallOptions.priorSurfacedIds} is given, * those ids are dropped from the candidate headers BEFORE the manifest is built and BEFORE the selector runs, * so an already-shown note is invisible to the selector (and therefore cannot be re-selected, re-link-followed, * or re-rendered). `surfacedIds` reports what THIS call rendered, for the caller's durable surfaced-set. * * @deprecated design/138 S4 — served ONLY the retired legacy memoryStore injection path; the Runner no * longer calls this. Kept exported one major for direct/standalone store-level use; removal is the next major. */ export declare function selectAndComposeMemory(opts: SelectiveRecallOptions): Promise; /** A `(scope, id)` composite key for the multi-scope durable de-dup (design/84 Seam A decision 5). Note ids * are only unique WITHIN a scope, so the cross-scope `priorSurfacedKeys`/`surfacedKeys` set MUST be keyed on * both. Core owns the FORMAT so a caller can't drift the wire format (decision 7) — the encoding is OPAQUE * and length-prefixes the scope so a scope/id containing the separator cannot forge a collision. * * ONE-WAY by design (codex MINOR (d)): the key is only ever PRODUCED here (`surfacedKeys`) and compared by * STRING EQUALITY against the caller's persisted set ({@link LayeredRecallOptions.priorSurfacedKeys}, applied * at memory-recall.ts via `priorSurfacedKeys.has(encodeSurfacedKey(h.scope, h.id))`). Nothing ever needs the * `(scope, id)` BACK out of a key, so there is intentionally NO `decodeSurfacedKey` — adding a decode would * widen the public surface (and invite callers to parse the opaque format) for zero consumer. Treat the key * as an opaque token: persist it, feed it back, compare for equality — never split it. */ export declare function encodeSurfacedKey(scope: string, id: string): string; /** A manifest header tagged with the scope it came from (design/84 Seam A decision 5). */ export interface ScopedNoteHeader extends MemoryNoteHeader { scope: string; } /** A full record tagged with its scope (for cross-scope getByIds). */ export interface ScopedNoteRecord extends MemoryNoteRecord { scope: string; } export interface LayeredRecallOptions { store: MemoryStore; /** Ordered scopes (design/84 Seam A): list order = priority; the last is highest. */ scopes: ReadonlyArray; objective: string; selector: MemorySelector; recentTools?: string[]; nowMs?: number; maxSelected?: number; maxLinked?: number; timeoutMs?: number; signal?: AbortSignal; /** CC-parity P1, multi-scope: `(scope,id)` composite keys ({@link encodeSurfacedKey}) already surfaced to * this logical conversation. Filtered out of the merged manifest BEFORE the selector sees it. */ priorSurfacedKeys?: ReadonlySet; } /** Result of {@link selectAndComposeLayeredMemory}. `surfacedKeys` are `(scope,id)` composite keys * ({@link encodeSurfacedKey}) — the caller appends them to its durable set and feeds them back as * {@link LayeredRecallOptions.priorSurfacedKeys}. Mirrors {@link SelectiveRecallResult}'s three shapes. */ export type LayeredRecallResult = { block: string; surfacedKeys: string[]; } | { block: undefined; surfacedKeys: string[]; } | { degrade: true; surfacedKeys: string[]; }; /** * Multi-scope (layered) selective recall (design/84 Seam A decision 5). Merges each scope's * {@link MemoryStore.listStructuredNotes} into ONE manifest (each header tagged with its scope), lets the * selector pick ids from the merged manifest, then fetches the selected bodies per-scope via * {@link MemoryStore.getByIds} and renders ONE `` block with the selected notes grouped under a * `` subsection IN LIST ORDER (stable layers first = cacheable prefix, volatile last = tail). The * `(scope,id)` composite de-dup ({@link LayeredRecallOptions.priorSurfacedKeys}) is applied at the manifest * layer so an already-shown note in ANY scope is invisible to the selector. * * v1 (decision 6): accepts the N-small (3–4 layer) fan-out of one `listStructuredNotes`/`getByIds` per scope * — no pre-built batch getByIds. Degrades to inject-all (the caller's job, like the single-scope path) on * store-incapability / empty / selector failure. Threshold-gated by the caller, so a small layering pays nothing. * * @deprecated design/138 S4 — served ONLY the retired legacy memoryStore injection path; the Runner no * longer calls this. Kept exported one major for direct/standalone store-level use; removal is the next major. */ export declare function selectAndComposeLayeredMemory(opts: LayeredRecallOptions): Promise; /** Minimal structural view of the Runner needed by {@link createBrainMemorySelector} (avoids a circular * import of the Runner class). */ export interface MemorySelectorRunner { runTask(spec: TaskSpec): Promise; } /** * Default brain-backed {@link MemorySelector} (design/65 §6.2 / council #1: the side-query uses the MAIN * brain — a cheap model is our unproven ABOVE assumption, not a CC port). Runs a tiny isolated sub-task * with an `{ids}` output schema, no tools, a short budget. Returns the selected ids (validated downstream). * * @deprecated design/138 S4 — served ONLY the retired legacy memoryStore injection path; the Runner no * longer calls this. Kept exported one major for direct/standalone store-level use; removal is the next major. */ export declare function createBrainMemorySelector(runner: MemorySelectorRunner, opts?: { model?: ModelRef; timeoutSec?: number; release?: (sessionId: string) => Promise | void; }): MemorySelector; //# sourceMappingURL=memory-recall.d.ts.map