@noir-ai/memory 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +21 -0
- package/dist/index.d.ts +803 -0
- package/dist/index.js +762 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/capture.ts","../src/config.ts","../src/consolidate.ts","../src/store.ts","../src/types.ts","../src/engine.ts","../src/recall.ts"],"sourcesContent":["// Host-neutral capture schema for @noir-ai/memory (slice S7, task t7).\n//\n// The bridge between a HOST's auto-capture surface and Noir's `memory_save`. A\n// host hook (Claude Code PreToolUse / PostToolUse / UserPromptSubmit / Stop, or\n// any future host's equivalent) emits a host-neutral {@link CaptureEvent}; this\n// module's pure {@link toSaveInput} mapper projects it into a {@link SaveInput}\n// that flows through the SAME `MemoryEngine.save` path as a deliberate save.\n//\n// Capture is ALWAYS local + free (blueprint D6 / DS-10): {@link toSaveInput} is\n// a PURE function — no I/O, no network, no LLM, no store. It builds the input;\n// the engine's `save` performs the write. There is deliberately NO source field\n// on {@link SaveInput} (it is set at save time), so an event routed through\n// `memory_save` lands with `source:'explicit'`; {@link captureSource} is exported\n// for a dedicated `noir memory capture` command (S9) that tags provenance as\n// `'auto:<hook>'` without going through the MCP `memory_save` envelope.\n//\n// This module does NOT install anything. Auto-capture is OPT-IN: the user wires\n// a host hooks block deliberately (see templates/claude-hooks.md). `noir init` /\n// `noir sync` NEVER install hooks (DS-4 — no surprise captures, no privacy\n// surface the user did not ask for).\n//\n// Canonical ProjectId (D6): {@link CaptureEvent.project} is the canonical id the\n// capture command resolved from cwd — NEVER a filesystem path. The mapper copies\n// it through unchanged (it is recorded on the observation at save time).\n\nimport type { MemorySource, MemoryType, ProjectId, SaveInput } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Hook event names (open enum — forward-compat with new host hook names)\n// ---------------------------------------------------------------------------\n\n/**\n * Known host hook events Noir can capture (the four Claude Code hooks the v1\n * template targets, DS-4 / spec §5). The list is intentionally NOT closed:\n * {@link CaptureEventType} also accepts any unknown string so a future host's\n * hook names round-trip through the mapper without a code change (mirrors the\n * open-enum taxonomy in types.ts).\n */\nexport const CAPTURE_HOOKS = ['PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'Stop'] as const;\n\n/** One of {@link CAPTURE_HOOKS} OR any host-defined hook name (open enum). */\nexport type CaptureEventType = (typeof CAPTURE_HOOKS)[number] | (string & {});\n\n// ---------------------------------------------------------------------------\n// Defaults — the opinionated capture policy (spec §5)\n// ---------------------------------------------------------------------------\n\n/**\n * Hook events captured by DEFAULT (DS-4). Persist session-end summaries + the\n * prompts a user submits; SKIP the noisy per-tool events (`PreToolUse` /\n * `PostToolUse` fire on every tool call and would flood memory + leak tool\n * inputs the user did not ask to remember). A user opts INTO tool-event capture\n * by passing a custom {@link CapturePolicy.hooks}.\n */\nexport const DEFAULT_CAPTURE_HOOKS: readonly CaptureEventType[] = ['Stop', 'UserPromptSubmit'];\n\n/**\n * The default capture policy: capture {@link DEFAULT_CAPTURE_HOOKS} only. Used\n * when {@link toSaveInput} is called with no `policy` argument. Host-neutral +\n * conservative — the user owns the policy.\n */\nexport const DEFAULT_CAPTURE_POLICY: CapturePolicy = { hooks: DEFAULT_CAPTURE_HOOKS };\n\n// ---------------------------------------------------------------------------\n// CaptureEvent (the host-neutral schema)\n// ---------------------------------------------------------------------------\n\n/**\n * The structured payload a host hook forwards. All fields optional — a hook\n * populates whichever it has (a `Stop` hook carries a `summary`; a\n * `UserPromptSubmit` hook carries the `prompt`; a tool hook carries `toolName`\n * + `toolInput`). Unknown extra fields are tolerated (the mapper reads only the\n * fields below), so a host can forward its raw stdin without shape-massaging.\n */\nexport interface CapturePayload {\n /** Tool name for tool hooks (e.g. `'Bash'`, `'Edit'`, `'Write'`). */\n toolName?: string;\n /** The tool's input object (e.g. `{ command: 'npm test' }`, `{ file_path }`). */\n toolInput?: unknown;\n /** The submitted prompt text (`UserPromptSubmit`). */\n prompt?: string;\n /** A session-end summary (`Stop`) — free text the host derived or was given. */\n summary?: string;\n}\n\n/**\n * A host-neutral capture event. Built by a host hook (or a `noir memory capture`\n * CLI) from whatever the host emitted on stdin, then handed to\n * {@link toSaveInput}. The shape is deliberately minimal + portable so it is not\n * tied to Claude Code's stdin schema (a future host adapter translates its own\n * payload into this).\n *\n * `project` is the CANONICAL project id (NEVER a filesystem path — blueprint\n * D6); the capture command resolves it from `cwd` before constructing the event.\n */\nexport interface CaptureEvent {\n /** Hook event name (open enum — {@link CaptureEventType}). */\n event_type: CaptureEventType;\n /** Epoch millis of the event (host-supplied or `Date.now()` at capture). */\n ts: number;\n /** Host session id if known, else null (recorded on the observation). */\n sessionId: string | null;\n /** Canonical project identifier (NEVER a filesystem path — D6). */\n project: ProjectId;\n /** The event-specific fields (see {@link CapturePayload}). */\n payload: CapturePayload;\n}\n\n// ---------------------------------------------------------------------------\n// Capture policy\n// ---------------------------------------------------------------------------\n\n/**\n * An opinionated capture policy. Controls WHICH hook events are persisted when\n * {@link toSaveInput} is called. Defaults to {@link DEFAULT_CAPTURE_POLICY}\n * (session summaries + submitted prompts only). The user owns this — it is the\n * one knob that keeps auto-capture from flooding memory (R3).\n */\nexport interface CapturePolicy {\n /**\n * Hook events to capture. An event whose `event_type` is NOT in this list is\n * skipped (`toSaveInput` returns `null`). Defaults to\n * {@link DEFAULT_CAPTURE_HOOKS}.\n */\n hooks?: ReadonlyArray<CaptureEventType>;\n}\n\n// ---------------------------------------------------------------------------\n// toSaveInput — the pure host-event → SaveInput mapper\n// ---------------------------------------------------------------------------\n\n/**\n * Project a host-neutral {@link CaptureEvent} into a {@link SaveInput} ready for\n * `MemoryEngine.save` / the `memory_save` MCP tool, OR return `null` when the\n * policy says to skip this event (a noisy hook the user did not opt into, or an\n * event with no usable content).\n *\n * The mapper is PURE: it reads no environment, holds no secrets, performs NO I/O\n * and NO LLM call (capture is always local + free, DS-10). It applies the\n * opinionated defaults from spec §5:\n * • only the policy's `hooks` are captured (default: Stop + UserPromptSubmit);\n * • content is built from the event's payload fields (summary / prompt / a\n * compact tool-call description);\n * • `type` is inferred lightly (a decision-shaped prompt → `'decision'`; a\n * tool hook → `'workflow'`; otherwise `'fact'`) — a heuristic the user can\n * override by editing the saved row;\n * • `files` is pulled best-effort from a tool input's `file_path`;\n * • `sessionId` is forwarded when present.\n *\n * Returning `null` is the documented \"skip\" signal — the caller (a capture\n * command) MUST NOT treat it as an error; it is the policy working as intended.\n *\n * @param event The host-neutral capture event.\n * @param policy Capture policy (defaults to {@link DEFAULT_CAPTURE_POLICY}).\n * @returns The {@link SaveInput}, or `null` to skip.\n */\nexport function toSaveInput(\n event: CaptureEvent,\n policy: CapturePolicy = DEFAULT_CAPTURE_POLICY,\n): SaveInput | null {\n const allowed = policy.hooks ?? DEFAULT_CAPTURE_HOOKS;\n // Open-enum match: an unknown hook name is captured only when the policy\n // explicitly lists it (so a stray host event never sneaks past the policy).\n if (!allowed.includes(event.event_type)) return null;\n\n const content = buildContent(event);\n // No usable text ⇒ nothing worth persisting. Trim so a whitespace-only\n // summary/prompt is treated as empty (R3: keep memory signal-rich).\n if (content === null || content.trim().length === 0) return null;\n\n const input: SaveInput = {\n content,\n type: inferType(event),\n sessionId: event.sessionId ?? undefined,\n };\n const files = extractFiles(event.payload);\n if (files.length > 0) input.files = files;\n return input;\n}\n\n// ---------------------------------------------------------------------------\n// Pure content / type / file helpers (exported for direct unit testing)\n// ---------------------------------------------------------------------------\n\n/**\n * Build the observation `content` for an event from its payload, or `null` when\n * the payload carries nothing persistable. Per hook:\n * • `Stop` → `payload.summary` (a session-end summary); null when absent.\n * • `UserPromptSubmit` → `payload.prompt`; null when absent.\n * • tool hooks (`PreToolUse` / `PostToolUse`, or any `toolName`-bearing\n * event) → a compact {@link describeToolCall} line.\n * Pure.\n */\nexport function buildContent(event: CaptureEvent): string | null {\n const { event_type, payload } = event;\n // Named hooks have a primary field (Stop → summary, UserPromptSubmit → prompt).\n if (event_type === 'Stop') {\n return payload.summary ?? null;\n }\n if (event_type === 'UserPromptSubmit') {\n return payload.prompt ?? null;\n }\n // Tool hooks (PreToolUse / PostToolUse) + any host-defined hook: prefer a\n // derived `summary` or a `prompt` when the host shipped one (so a hook like\n // SessionEnd that carries a summary is captured without special-casing),\n // then fall back to a compact {@link describeToolCall} for tool-bearing\n // events. describeToolCall returns null when there is nothing to describe.\n if (typeof payload.summary === 'string') return payload.summary;\n if (typeof payload.prompt === 'string') return payload.prompt;\n return describeToolCall(payload);\n}\n\n/**\n * Render a compact one-line description of a tool call for memory content, e.g.\n * `\"Ran Bash: npm test\"` or `\"Edited src/foo.ts\"`. Returns `null` when the\n * payload carries neither a toolName nor a toolInput. Pure + best-effort — it\n * never throws on an unexpected toolInput shape (it renders what it can).\n */\nexport function describeToolCall(payload: CapturePayload): string | null {\n const { toolName, toolInput } = payload;\n // Try to surface the load-bearing field of common tools.\n const cmd = stringField(toolInput, 'command');\n const filePath = stringField(toolInput, 'file_path');\n const pattern = stringField(toolInput, 'pattern');\n\n if (toolName !== undefined) {\n if (cmd !== null) return `Ran ${toolName}: ${cmd}`;\n if (filePath !== null) {\n // Write tools mutate the file; everything else is a read-style op on it.\n // `${toolName}: ${filePath}` generalizes cleanly (Read/Grep/Glob …).\n return isWriteTool(toolName) ? `Edited ${filePath}` : `${toolName}: ${filePath}`;\n }\n if (pattern !== null) return `${toolName} /${pattern}/`;\n return `Used ${toolName}`;\n }\n // No toolName — describe the input object best-effort, else nothing.\n if (cmd !== null) return `Ran command: ${cmd}`;\n if (filePath !== null) return `Touched ${filePath}`;\n return null;\n}\n\n/**\n * Light type inference for a captured event (a heuristic — the user can override\n * by editing the saved row; no LLM is involved, DS-10). Decision-shaped prompts\n * → `'decision'`; tool hooks → `'workflow'`; otherwise `'fact'` (the engine's\n * default type). Pure.\n */\nexport function inferType(event: CaptureEvent): MemoryType {\n if (event.event_type === 'UserPromptSubmit' && event.payload.prompt !== undefined) {\n return looksDecisionShaped(event.payload.prompt) ? 'decision' : 'fact';\n }\n if (event.event_type === 'PreToolUse' || event.event_type === 'PostToolUse') {\n return 'workflow';\n }\n if (event.event_type === 'Stop') {\n return 'workflow';\n }\n return 'fact';\n}\n\n/**\n * Extract repo-relative file paths from a tool input best-effort: the\n * `file_path` field (Edit / Write / Read) when present. Returns `[]` when none.\n * Pure; never throws on a non-object toolInput.\n */\nexport function extractFiles(payload: CapturePayload): string[] {\n const filePath = stringField(payload.toolInput, 'file_path');\n return filePath !== null ? [filePath] : [];\n}\n\n/**\n * The memory `source` provenance for a captured event: `'auto:<hook>'` (e.g.\n * `'auto:stop'`, `'auto:posttooluse'`), lowercased so the bucket name is a\n * stable identifier. Exported for a dedicated `noir memory capture` CLI (S9)\n * that persists an event OUTSIDE the `memory_save` envelope and wants to tag\n * provenance — the MCP `memory_save` path itself records `source:'explicit'`\n * (SaveInput carries no source field by design).\n */\nexport function captureSource(eventType: CaptureEventType): MemorySource {\n return `auto:${String(eventType).toLowerCase()}`;\n}\n\n// ---------------------------------------------------------------------------\n// Module-local helpers\n// ---------------------------------------------------------------------------\n\n/** Words that mark a prompt as decision-shaped (heuristic for `type:'decision'`). */\nconst DECISION_CUES = [\n 'decided',\n 'decision',\n 'should',\n 'let’s',\n \"let's\",\n 'going with',\n 'use ',\n 'adopt',\n 'convention',\n 'rule:',\n 'always ',\n 'never ',\n] as const;\n\n/** True when a prompt reads like a decision / convention statement. Pure. */\nfunction looksDecisionShaped(prompt: string): boolean {\n const lower = prompt.toLowerCase();\n return DECISION_CUES.some((cue) => lower.includes(cue));\n}\n\n/** Tool names that mutate files (→ \"Edited\"); others read (→ \"Read … on …\"). */\nfunction isWriteTool(toolName: string): boolean {\n const t = toolName.toLowerCase();\n return t === 'edit' || t === 'write' || t === 'multiedit' || t === 'notebookedit';\n}\n\n/**\n * Read a string field off an unknown `obj` (a tool input), safely. Returns\n * `null` when `obj` is not an object, the field is absent, or the field is not a\n * string. The pattern that keeps `noUncheckedIndexedAccess` + `unknown` happy\n * without a value-level `zod` parse (capture is best-effort, not validated).\n */\nfunction stringField(obj: unknown, field: string): string | null {\n if (typeof obj !== 'object' || obj === null) return null;\n const rec = obj as Record<string, unknown>;\n const val = rec[field];\n return typeof val === 'string' ? val : null;\n}\n","// Memory config resolver for @noir-ai/memory (slice S7, task t6).\n//\n// The single bridge from @noir-ai/core's user-facing `memory` zod schema to the\n// runtime {@link MemoryConfig} the memory engine (and `runConsolidation`) consume.\n// Lives HERE, in memory, so @noir-ai/core never imports @noir-ai/memory\n// (no core→memory cycle): core owns the user-facing schema, memory owns the\n// engine type + this mapper (mirrors @noir-ai/context's `resolveEmbedderConfig`\n// and @noir-ai/model's `resolveModelConfig` — blueprint / hard rule). The fully-\n// resolved zod output is structurally assignable to the permissive\n// {@link MemoryUserConfig} mirror below, so the mapper accepts a\n// `NoirConfig['memory']` directly — callers pass `resolveMemoryConfig(cfg.memory)`.\n//\n// Provider-EXPLICIT, never silent paid (blueprint D5/D6, DS-6): this mapper is a\n// PURE projection of what the user wrote — it NEVER infers a provider from\n// env-var presence. No explicit `consolidation.provider` ⇒ a disabled runtime\n// config ⇒ `runConsolidation` refuses with `'no-provider'` + writes a miss audit\n// and makes NO S8 `complete()` call. This is the line between free (store/embed)\n// and paid (LLM) — NEVER a silent paid call (the Agent-Memory anti-pattern, §9).\n// The mapper reads NO environment, holds NO secrets, and never throws — an\n// unusable config stays a clean disabled default.\n\nimport type { MemoryConfig } from './types.js';\n\n/**\n * User-facing memory config shape — mirrors `NoirConfig['memory']` (the zod block\n * @noir-ai/core ships, slice S7 / task t6). Declared LOCALLY with every field\n * optional so this module type-checks WITHOUT a forward dependency on a core\n * type (core never imports memory — no cycle; @noir-ai/core is not even\n * consulted here), AND so a config with no `memory:` block (or a partial one)\n * maps cleanly to a fully-disabled runtime config. The fully-resolved zod output\n * is structurally assignable to this permissive shape, so the mapper accepts a\n * `NoirConfig['memory']` directly.\n */\nexport interface MemoryUserConfig {\n /** Consolidation gate (provider-explicit — never silent paid, DS-6). */\n consolidation?: {\n /** Master switch (default false). When false, `consolidate` refuses + logs. */\n enabled?: boolean;\n /** Provider key, e.g. 'anthropic' | 'openai' | 'ollama'. Required to run. */\n provider?: string;\n /** Provider-specific model id. */\n model?: string;\n /** Restrict candidates to these types (default: every non-`lesson` type). */\n types?: string[];\n };\n}\n\n/**\n * Resolve a user-facing {@link MemoryUserConfig} into the runtime\n * {@link MemoryConfig} the memory engine (and `runConsolidation`) consume.\n *\n * - `undefined` / missing block ⇒ `{ consolidation: { enabled: false } }`\n * (consolidation disabled — the safe default; capture/store/retrieve stay\n * local + free. `runConsolidation` then refuses with `'no-provider'` + logs,\n * making NO paid call, blueprint D6).\n * - A block whose `consolidation.enabled` is absent or `false` ⇒ the same\n * disabled default, regardless of any `provider`/`model` written alongside it\n * (the master switch is the first gate `runConsolidation` checks).\n * - A block with `consolidation.enabled === true` + a `provider` ⇒ the provider\n * + optional `model`/`types` pass straight through; `runConsolidation` then\n * proceeds to its model-availability + candidate gates.\n *\n * `consolidation` is ALWAYS populated (with `enabled` defaulted) so consumers\n * read `config.consolidation.enabled` without a separate undefined check —\n * mirrors how `resolveModelConfig` normalizes `tiers`/`providers` to\n * always-present objects.\n *\n * This mapper is a PURE projection — it copies fields through unchanged, NEVER\n * infers a provider from env-var presence (DS-6), reads NO environment, holds NO\n * secrets, and never throws. Whether a configured provider is actually USABLE is\n * decided at call time inside `complete()` (S8, which re-reads env idempotently),\n * NOT here.\n */\nexport function resolveMemoryConfig(raw?: MemoryUserConfig): MemoryConfig {\n const cons = raw?.consolidation;\n // Destructure once into locals so every narrowing below is unambiguous (the\n // model + context bridges follow the same `const x = raw?.field` shape). TS\n // then narrows each local cleanly under strictNullChecks without re-checking\n // the optional chain on assignment.\n // `enabled` is defaulted to false: an absent OR explicitly-false block both\n // disable consolidation. Strict `=== true` so a stray truthy non-boolean\n // (impossible post-zod, possible from hand-built test input) never enables a\n // paid call by accident — provider-EXPLICIT means opt-in is deliberate.\n const enabled = cons?.enabled === true;\n const provider = cons?.provider;\n const model = cons?.model;\n const types = cons?.types;\n\n // Always populate `consolidation` with `enabled` defaulted, so consumers read\n // `config.consolidation.enabled` without a separate undefined check (mirrors\n // how resolveModelConfig normalizes `tiers`/`providers` to always-present\n // objects). An absent block ⇒ disabled — NEVER a silent paid call.\n const consolidation: NonNullable<MemoryConfig['consolidation']> = { enabled };\n // Pure passthrough — no normalization, no env inference, no defaults beyond\n // `enabled`. Optional fields are copied only when present so `undefined` never\n // overwrites a meaningful absent-with-a-default state downstream.\n if (provider !== undefined) consolidation.provider = provider;\n if (model !== undefined) consolidation.model = model;\n if (types !== undefined) consolidation.types = types;\n return { consolidation };\n}\n","// Consolidation for @noir-ai/memory (slice S7, task t5).\n//\n// The explicit, append-only, provider-gated consolidation job (DS-6). Extracted\n// from the engine into its own module so the algorithm — gate → gather\n// candidates → synthesize ONE lesson via the S8 bounded model → append the\n// derived row — is unit-testable in isolation (no sqlite-vec, no embedder: the\n// engine passes an `indexDerived` callback that performs the actual write). The\n// engine delegates here; the engine still owns the store handle, the embedder,\n// and the FTS5+vec+KV write path (`indexObservation`, shared with `save`).\n//\n// Blueprint D6 / §9 hard rules enforced here (non-negotiable):\n// • Provider-EXPLICIT — the provider is resolved ONLY from\n// `config.consolidation.provider`; it is NEVER inferred from env-var\n// presence. No explicit, enabled provider ⇒ refuse + log (`no-provider`)\n// and NO S8 call is made. This is the line between free (store) and paid\n// (LLM) — NEVER a silent paid call (the Agent-Memory anti-pattern, §9).\n// • Append-only — on success a DERIVED `type:'lesson'` row is appended with\n// `provenance:[candidate ids]`; the original observations are NEVER mutated\n// or deleted (reversible + auditable, DS-6).\n// • Single-shot (D5) — one `complete()` call, free-text → lesson body. There\n// is no `tools` / `stream` parameter on the request and no loop here.\n// • Canonical ProjectId — the lesson's `project` is the engine's canonical\n// id (NEVER a filesystem path — D6).\n//\n// A refusal is NEVER a crash and NEVER silent: every miss is recorded in the\n// `memory:consolidation:miss` KV audit log (`logged:true`) so the user can see\n// exactly why no lesson was written — and that NO paid call was made.\n\nimport { randomUUID } from 'node:crypto';\nimport type { Store } from '@noir-ai/store';\n// TYPE-ONLY import: the model injection lives in engine.ts (it is also referenced\n// by MemoryEngineOptions.model). Erased at runtime, so there is NO module cycle —\n// engine.ts imports `runConsolidation` (value) from here; this file imports only\n// the `MemoryModel` TYPE from engine.ts.\nimport type { MemoryModel } from './engine.js';\nimport { appendConsolidationMiss, getObservation, getObservationIds } from './store.js';\nimport {\n type ConsolidateOptions,\n type ConsolidationResult,\n DEFAULT_IMPORTANCE,\n type MemoryConfig,\n type Observation,\n type ProjectId,\n} from './types.js';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Default cap on consolidation candidates (spec §7.4 — deterministic selection). */\nexport const DEFAULT_CONSOLIDATE_LIMIT = 50;\n\n/**\n * Single-shot consolidation instruction (D5: no tools, no loop). The lesson is\n * free-text PROSE (not JSON): a lesson is a synthesized insight, and free-text\n * avoids a JSON-parse/repair round on what is naturally unstructured output.\n */\nexport const CONSOLIDATION_SYSTEM_PROMPT = [\n \"You are consolidating a developer's cross-session memory observations.\",\n 'Synthesize the given observations into ONE concise derived lesson.',\n 'State the general insight; do not merely list the inputs.',\n 'Output only the lesson text (no preamble, no JSON, no formatting).',\n].join(' ');\n\n// ---------------------------------------------------------------------------\n// Deps (the engine supplies these; tests fake them)\n// ---------------------------------------------------------------------------\n\n/**\n * Capabilities `runConsolidation` needs from the engine. The engine owns the\n * store handle + the embedder; `indexDerived` is the callback that writes a\n * derived observation to every index (embed → FTS5 + vec + KV + id index),\n * reusing the engine's shared `indexObservation` path so a lesson lands exactly\n * like a user-saved observation. Consolidation itself never touches the\n * embedder or the write path directly — it builds the lesson row + hands it off.\n */\nexport interface ConsolidationDeps {\n /** The daemon's single-writer store handle (possibly read-only — D6). */\n store: Store;\n /** Optional S8 model injection; absent ⇒ `'model-unavailable'` refusal. */\n model: MemoryModel | undefined;\n /** Runtime memory config (the provider-explicit consolidation gate). */\n config: MemoryConfig;\n /** Canonical project identifier (NEVER a filesystem path — D6). */\n projectId: ProjectId;\n /**\n * Write a derived lesson observation to all indexes. The engine implements\n * this as `embedBestEffort(content)` then the shared `indexObservation(obs,\n * vec)` — so the lesson is searchable (BM25 + vec) + hydrated from KV just\n * like a user save.\n */\n indexDerived: (observation: Observation) => Promise<void>;\n}\n\n// ---------------------------------------------------------------------------\n// runConsolidation — the explicit, provider-gated job (DS-6)\n// ---------------------------------------------------------------------------\n\n/**\n * Run one explicit consolidation pass.\n *\n * Gate order (each refusal logs to `memory:consolidation:miss` and returns\n * `logged:true`; NONE makes a paid call before its gate passes):\n * 1. `no-provider` — consolidation not enabled OR no explicit `provider`\n * configured. The provider is NEVER inferred from env-var presence (D5/D6).\n * 2. `model-unavailable` — enabled + provider set, but the S8 model injection\n * is absent OR no explicit `model` id is configured (the documented S7 stub,\n * OQ-3/OQ-8). Also the refusal when `complete()` returns `null` (provider\n * not resolvable at call time — e.g. key missing) OR `{ok:false}` (an\n * attempted call failed): both are wrapped as `'model-unavailable'`.\n * 3. `no-candidates` — nothing matches the (optional) type filter / lookback,\n * OR the model returned empty text.\n *\n * On success: appends ONE derived `type:'lesson'` observation with\n * `provenance:[candidate ids]` via `indexDerived`; originals are NEVER mutated\n * (append-only — reversible + auditable, DS-6).\n */\nexport async function runConsolidation(\n deps: ConsolidationDeps,\n opts?: ConsolidateOptions,\n): Promise<ConsolidationResult> {\n const { store, model, config, projectId, indexDerived } = deps;\n const cons = config.consolidation;\n const provider = cons?.provider;\n\n // GATE 1 — provider-explicit (D5/D6/DS-6). The provider is NEVER inferred\n // from env-var presence; no explicit, enabled provider ⇒ refuse + log, and\n // NO S8 call is made. This is the line between free (store) and paid (LLM).\n if (!cons?.enabled || !provider) {\n appendConsolidationMiss(store, { ts: Date.now(), reason: 'no-provider' });\n return { ok: false, reason: 'no-provider', logged: true };\n }\n\n // GATE 2 — the S8 bounded model layer must be injected AND an explicit model\n // id configured. Its absence is the documented S7 stub (OQ-3/OQ-8): refuse +\n // log, never crash, never a call.\n if (model === undefined || !cons.model) {\n appendConsolidationMiss(store, {\n ts: Date.now(),\n reason: 'model-unavailable',\n provider,\n });\n return { ok: false, reason: 'model-unavailable', logged: true };\n }\n const modelId = cons.model;\n\n // Candidates: deterministic selection (no clustering LLM). Recent first;\n // never consolidate an existing lesson; honor the optional type filter.\n // `opts.types` overrides the configured filter; `opts.limit` caps the set.\n const candidates = gatherCandidates(\n store,\n opts?.types ?? cons.types,\n opts?.limit ?? DEFAULT_CONSOLIDATE_LIMIT,\n );\n if (candidates.length === 0) {\n appendConsolidationMiss(store, {\n ts: Date.now(),\n reason: 'no-candidates',\n provider,\n });\n return { ok: false, reason: 'no-candidates', logged: true };\n }\n\n // Single-shot synthesis (D5: no `tools`, no loop). Free-text → lesson body.\n const result = await model.complete({\n system: CONSOLIDATION_SYSTEM_PROMPT,\n prompt: serializeCandidates(candidates),\n provider,\n model: modelId,\n tier: 'consolidate',\n });\n if (result === null || !result.ok) {\n // null = provider not resolvable at call time (key missing / block absent);\n // ok:false = an attempted call failed. Either way: no lesson written; the\n // task approach wraps BOTH as a failure reason. Log + refuse (never crash).\n appendConsolidationMiss(store, {\n ts: Date.now(),\n reason: 'model-unavailable',\n provider,\n });\n return { ok: false, reason: 'model-unavailable', logged: true };\n }\n\n const text = result.text.trim();\n if (text.length === 0) {\n // Model returned empty — treat as nothing to synthesize (no-candidates).\n appendConsolidationMiss(store, {\n ts: Date.now(),\n reason: 'no-candidates',\n provider,\n });\n return { ok: false, reason: 'no-candidates', logged: true };\n }\n\n // Append the derived lesson (originals UNTOUCHED — append-only, DS-6). The\n // lesson inherits the baseline salience + the de-duplicated concept tags from\n // its sources; `files` is empty (a lesson is project-level, not file-scoped).\n const provenance = candidates.map((c) => c.id);\n const ts = Date.now();\n const lesson: Observation = {\n id: randomUUID(),\n type: 'lesson',\n content: text,\n project: projectId,\n sessionId: null,\n ts,\n lastAccessTs: ts,\n importance: DEFAULT_IMPORTANCE,\n concepts: dedupeConcepts(candidates),\n files: [],\n source: 'explicit',\n provenance,\n };\n await indexDerived(lesson);\n return { ok: true, lessons: [lesson], from: provenance };\n}\n\n// ---------------------------------------------------------------------------\n// Pure helpers (exported for direct unit testing — mirrors @noir-ai/context\n// exporting its pure RRF / snippet helpers)\n// ---------------------------------------------------------------------------\n\n/**\n * Gather consolidation candidates: observations with `type != 'lesson'`,\n * newest-first, optionally restricted to `types`, capped at `limit`. Pure read\n * off the id index + KV — no LLM, no clustering (spec §7.4). Exported so the\n * deterministic selection is testable without a model.\n */\nexport function gatherCandidates(\n store: Store,\n types: ReadonlyArray<string> | undefined,\n limit: number,\n): Observation[] {\n const ids = getObservationIds(store);\n const out: Observation[] = [];\n // Iterate newest-first (the id index is append/oldest-first).\n for (let i = ids.length - 1; i >= 0 && out.length < limit; i--) {\n const id = ids[i];\n if (id === undefined) break;\n const obs = getObservation(store, id);\n if (obs === null) continue;\n if (obs.type === 'lesson') continue; // never re-consolidate a lesson\n if (types !== undefined && !types.includes(obs.type)) continue;\n out.push(obs);\n }\n return out;\n}\n\n/**\n * Serialize candidates into the consolidation prompt body (deterministic order,\n * 1-indexed, with type + ts for the model's context). Pure.\n */\nexport function serializeCandidates(candidates: ReadonlyArray<Observation>): string {\n return candidates\n .map((c, i) => `[${i + 1}] (type: ${c.type}, ts: ${c.ts}) ${c.content}`)\n .join('\\n\\n');\n}\n\n/**\n * De-duplicate + collect concept tags across the candidate set (becomes the\n * derived lesson's `concepts`). Order is first-seen; pure.\n */\nexport function dedupeConcepts(candidates: ReadonlyArray<Observation>): string[] {\n const set = new Set<string>();\n for (const c of candidates) {\n for (const concept of c.concepts) set.add(concept);\n }\n return [...set];\n}\n","// Memory store layer for @noir-ai/memory (slice S7, task t2).\n//\n// A thin KV + index facade over the existing @noir-ai/store. Observations are\n// realized ON TOP of the store (DS-2 — NO schema migration): the authoritative\n// full row lives in KV `memory:obs:<id>`, with `indexDoc({source:'memory'})`\n// (FTS5) + `upsertVec({source:'memory'})` (sqlite-vec) as search indexes. This\n// module owns ONLY the KV layout + single-key accessors (+ the two\n// read-modify-write helpers `bumpSession` / `decrementSession`); the\n// read-modify-write orchestration across keys + single-flight serialization\n// lives in engine.ts.\n//\n// KV layout (namespaced `memory:` — disjoint from `ctx:` / `workflow:*`):\n// memory:obs:<id> → Observation (authoritative full row, DS-9)\n// memory:sessions → SessionInfo[] (per-project rollup, A3)\n// memory:index → string[] (all obs ids; status count +\n// consolidate candidate source)\n// memory:consolidation:miss → ConsolidationMiss[] (refusal audit log, DS-6)\n//\n// Authoritative-source discipline (R4 mitigation): the KV row\n// `memory:obs:<id>` is the source of truth; `docs.meta` is a denormalized\n// search payload written alongside it. Recall hydrates the FULL `Observation`\n// from KV (never the truncated FTS snippet — DS-9). `forget` clears the KV row\n// (tombstone, mirroring the context indexer) AND purges the doc/vec indexes\n// best-effort (A2's acceptable v1 behavior).\n//\n// These helpers use ONLY the Store interface (no second connection, no schema\n// change — blueprint D6: in-process only, canonical ProjectId). The RMW\n// helpers (`bumpSession`, `decrementSession`, `appendConsolidationMiss`) MUST\n// be called from within the engine's single-flight / synchronous KV block so\n// two concurrent writers cannot clobber the list — see engine.ts.\n\nimport type { Store } from '@noir-ai/store';\nimport type { Observation, SessionInfo } from './types.js';\n\n// ---------------------------------------------------------------------------\n// KV keys (namespaced `memory:`)\n// ---------------------------------------------------------------------------\n\n/** Per-observation authoritative-row key prefix; value is the full {@link Observation}. */\nexport const OBS_PREFIX = 'memory:obs:';\n/** KV key holding the per-project {@link SessionInfo} rollup list. */\nexport const SESSIONS_KEY = 'memory:sessions';\n/** KV key holding the sorted list of all observation ids (status + candidates). */\nexport const INDEX_KEY = 'memory:index';\n/** KV key holding the consolidation refusal audit log (DS-6: refuse + LOG). */\nexport const CONSOLIDATION_MISS_KEY = 'memory:consolidation:miss';\n\n/** Build a `memory:obs:<id>` KV key. */\nexport function obsKey(id: string): string {\n return `${OBS_PREFIX}${id}`;\n}\n\n// ---------------------------------------------------------------------------\n// Authoritative observation row (KV `memory:obs:<id>`)\n// ---------------------------------------------------------------------------\n\n/**\n * Hydrate the FULL {@link Observation} for `id` from the authoritative KV row.\n * Returns `null` when the id was never saved (or has been forgotten — the\n * tombstone reads back as `null`). This is the ONLY correct way to read an\n * observation's complete `content` (DS-9: never the truncated FTS snippet).\n */\nexport function getObservation(store: Store, id: string): Observation | null {\n return store.getState<Observation>(obsKey(id));\n}\n\n/** Write the authoritative full row (the source of truth — DS-2/R4). */\nexport function setObservation(store: Store, obs: Observation): void {\n store.setState(obsKey(obs.id), obs);\n}\n\n/**\n * Clear the authoritative KV row for `id` (tombstone — mirrors the context\n * indexer's `persist(..., tombstones)` which writes `null`). The id MUST also\n * be dropped from {@link INDEX_KEY} and the session rollup by the caller so the\n * index stays consistent (see engine.ts `forget`).\n */\nexport function clearObservation(store: Store, id: string): void {\n store.setState(obsKey(id), null);\n}\n\n// ---------------------------------------------------------------------------\n// Observation id index (KV `memory:index`) — status count + consolidate source\n// ---------------------------------------------------------------------------\n\n/** All observation ids currently tracked (insertion order). Empty array if none. */\nexport function getObservationIds(store: Store): string[] {\n return store.getState<string[]>(INDEX_KEY) ?? [];\n}\n\n/** Overwrite the full id list (used by `save`/`forget` after an atomic RMW). */\nexport function setObservationIds(store: Store, ids: string[]): void {\n store.setState(INDEX_KEY, ids);\n}\n\n// ---------------------------------------------------------------------------\n// Sessions rollup (KV `memory:sessions`)\n// ---------------------------------------------------------------------------\n\n/** The per-project {@link SessionInfo} rollup list (empty array if none). */\nexport function getSessions(store: Store): SessionInfo[] {\n return store.getState<SessionInfo[]>(SESSIONS_KEY) ?? [];\n}\n\n/** Overwrite the sessions rollup (used by the RMW helpers below). */\nexport function setSessions(store: Store, sessions: SessionInfo[]): void {\n store.setState(SESSIONS_KEY, sessions);\n}\n\n/**\n * Increment the rollup for `sessionId` (inserting a new entry when first seen),\n * bumping `lastTs` to the max. RMW — call within the engine's serialized/sync KV\n * block so two concurrent saves cannot lose an increment.\n */\nexport function bumpSession(\n store: Store,\n sessionId: string,\n project: SessionInfo['project'],\n ts: number,\n): void {\n const sessions = getSessions(store);\n const existing = sessions.find((s) => s.id === sessionId);\n if (existing) {\n existing.count += 1;\n if (ts > existing.lastTs) existing.lastTs = ts;\n } else {\n sessions.push({ id: sessionId, project, count: 1, lastTs: ts });\n }\n setSessions(store, sessions);\n}\n\n/**\n * Decrement the rollup for `sessionId`, dropping the entry when it reaches zero\n * so a forgotten observation cleans up its own empty session. RMW — call within\n * the engine's serialized/sync KV block. No-op for an unknown session.\n */\nexport function decrementSession(store: Store, sessionId: string): void {\n const sessions = getSessions(store);\n const existing = sessions.find((s) => s.id === sessionId);\n if (existing === undefined) return;\n existing.count -= 1;\n if (existing.count <= 0) {\n setSessions(\n store,\n sessions.filter((s) => s.id !== sessionId),\n );\n } else {\n setSessions(store, sessions);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Consolidation refusal audit (KV `memory:consolidation:miss`) — DS-6\n// ---------------------------------------------------------------------------\n\n/**\n * One recorded consolidation refusal. `reason` is the documented\n * {@link ConsolidationResult} refusal cause; `provider` is recorded when a\n * provider WAS configured but the run still refused (e.g. the S8 layer was\n * unavailable), so the user can see exactly why nothing happened.\n */\nexport interface ConsolidationMiss {\n /** Epoch millis of the refusal. */\n ts: number;\n /** Documented refusal reason (`'no-provider' | 'model-unavailable' | 'no-candidates'`). */\n reason: string;\n /** Provider key, when one was configured (absent for the no-provider case). */\n provider?: string;\n}\n\n/** The full refusal audit log (newest appended last). Empty array if none. */\nexport function getConsolidationMisses(store: Store): ConsolidationMiss[] {\n return store.getState<ConsolidationMiss[]>(CONSOLIDATION_MISS_KEY) ?? [];\n}\n\n/**\n * Append a refusal record to the audit log. RMW — called ONLY from the engine's\n * serialized `consolidate`, so the append cannot race itself. DS-6: a refusal is\n * never silent — the miss is recorded so the user can see why no lesson was\n * written (and that NO paid call was made).\n */\nexport function appendConsolidationMiss(store: Store, miss: ConsolidationMiss): void {\n const misses = getConsolidationMisses(store);\n misses.push(miss);\n store.setState(CONSOLIDATION_MISS_KEY, misses);\n}\n","// @noir-ai/memory types (slice S7, task t1).\n//\n// The observation data model + the MemoryEngine contract. These are the\n// package's OWN interfaces — the storage surface (`Store`, `ProjectId`) is\n// re-exported from @noir-ai/core / @noir-ai/store, and the embedder seam\n// (`EmbedFn`, `EmbedderConfig`) is re-exported from @noir-ai/context (S6). The\n// engine is built once per serve lifecycle from the daemon's already-open Store\n// handle (the single writer) + the SAME `EmbedFn` the daemon already resolved\n// for S6 — memory takes `{store, embed, ...}`, no embedder duplication.\n//\n// There is deliberately NO zod here: core owns the user-facing schema\n// (`NoirConfigSchema.memory`, task t6) and memory owns this engine/type\n// surface, which keeps the dependency graph acyclic (core never imports memory\n// — mirrors @noir-ai/context types.ts).\n//\n// Blueprint D6 hard rules enforced by this model:\n// • canonical ProjectId (NEVER a filesystem path) — `Observation.project`;\n// • capture/store/retrieve always local + free — no field implies a network\n// or LLM call;\n// • ANY LLM touch (consolidation) is opt-in + provider-explicit — the ONLY\n// LLM entry point is the optional `MemoryEngine.consolidate`, gated on\n// `MemoryConfig.consolidation.provider` (task t5). It refuses + logs when\n// no provider is configured — NEVER a silent paid call;\n// • never truncate — `Observation.content` and `MemoryHit.content` carry the\n// FULL text; the FTS snippet is only a preview window (DS-9).\n//\n// Conventions mirror @noir-ai/store / @noir-ai/context types: JSDoc on every\n// interface/field, `as const` source tables → derived unions, `.js` extensions\n// on relative ESM imports.\n\nimport type { ProjectId } from '@noir-ai/core';\n\n// ---------------------------------------------------------------------------\n// Taxonomy (DS-3: dev-flavored OPEN enum; unknown values accepted + stored)\n// ---------------------------------------------------------------------------\n\n/**\n * Known observation types (dev-flavored, DS-3). `lesson` is reserved for\n * consolidation output (task t5); the others are user-supplied on save. This\n * list is intentionally NOT a closed set: {@link MemoryType} also accepts any\n * unknown string so forward-compatible / user-defined types round-trip through\n * the store without a migration.\n */\nexport const MEMORY_TYPES = [\n 'pattern',\n 'preference',\n 'architecture',\n 'bug',\n 'workflow',\n 'fact',\n 'decision',\n 'lesson',\n] as const;\n\n/**\n * Open enum: one of {@link MEMORY_TYPES} OR any string. The `(string & {})`\n * intersection preserves literal autocompletion for the known values while\n * permitting arbitrary user-defined types (DS-3: unknown values accepted +\n * stored). `lesson` is reserved for consolidation output.\n */\nexport type MemoryType = (typeof MEMORY_TYPES)[number] | (string & {});\n\n/**\n * Provenance of a capture. `'explicit'` = a deliberate `memory_save` (the v1\n * default, DS-4). `'auto:<hook>'` = an opt-in Claude Code hooks template\n * capture (e.g. `'auto:stop'`, `'auto:posttooluse'`); the template ships as\n * files the user installs deliberately — never auto-wired by `noir init`/`sync`.\n */\nexport type MemorySource = 'explicit' | `auto:${string}`;\n\n// ---------------------------------------------------------------------------\n// Defaults (applied at save time / on derived consolidation lessons)\n// ---------------------------------------------------------------------------\n\n/**\n * Default observation salience (spec §4.1). Applied by `MemoryEngine.save` when\n * {@link SaveInput.importance} is omitted, and by consolidation when appending a\n * derived `type:'lesson'` row. Shared via types.ts so save + consolidation agree\n * on the baseline without a cross-module value import.\n */\nexport const DEFAULT_IMPORTANCE = 0.5;\n\n// ---------------------------------------------------------------------------\n// Observation (the canonical row, DS-2 / DS-9)\n// ---------------------------------------------------------------------------\n\n/**\n * The canonical memory row. Realized ON TOP of the store (DS-2): the full row\n * lives in KV `memory:obs:<id>` (the authoritative source of truth — the line\n * between it and the indexes is R4's mitigation), with\n * `indexDoc({source:'memory'})` (FTS5) + `upsertVec({source:'memory'})`\n * (sqlite-vec) as search indexes. `content` is NEVER truncated (DS-9) — the\n * FTS snippet is only a preview window hydrated-around on recall.\n */\nexport interface Observation {\n /** Unique id (crypto.randomUUID()). Key into KV `memory:obs:<id>`. */\n id: string;\n /** Open enum (DS-3). `lesson` is reserved for consolidation output. */\n type: MemoryType;\n /** Full text — never truncated (DS-9). Indexed into FTS5 + embedded into vec0. */\n content: string;\n /** Canonical project id (NEVER a filesystem path — blueprint D6). */\n project: ProjectId;\n /** Host session id if known, else null. */\n sessionId: string | null;\n /** Created-at epoch millis. */\n ts: number;\n /** Bumped on recall hit (best-effort, single writer). */\n lastAccessTs: number;\n /** Salience 0..1 (default 0.5, applied at save time). */\n importance: number;\n /** User tags (no auto-LLM tagging in v1 — explicit only). */\n concepts: string[];\n /** Repo-relative paths mentioned. */\n files: string[];\n /** Capture provenance (DS-4). */\n source: MemorySource;\n /**\n * Source observation ids, set ONLY on derived `type:'lesson'` rows produced\n * by consolidation (task t5). Absent on user-saved observations. Originals\n * are never mutated or deleted (append-only — reversible + auditable, DS-6).\n */\n provenance?: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Save input (the `memory_save` payload)\n// ---------------------------------------------------------------------------\n\n/**\n * Input to {@link MemoryEngine.save} / the `memory_save` MCP tool. Only\n * `content` is required; the engine applies defaults (`type`, `importance`,\n * `source:'explicit'`, `ts`, `id`) at save time. No field here triggers a\n * network or LLM call — capture is always local + free (DS-10).\n */\nexport interface SaveInput {\n /** Full text to remember. Required. */\n content: string;\n /** Open enum (DS-3); a default is applied at save time when omitted. */\n type?: MemoryType;\n /** User tags. */\n concepts?: string[];\n /** Repo-relative paths mentioned. */\n files?: string[];\n /** Salience 0..1 (defaults to 0.5 at save time). */\n importance?: number;\n /** Host session id (recorded when known). */\n sessionId?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Recall / search hits (DS-9: full content, never truncated)\n// ---------------------------------------------------------------------------\n\n/**\n * Options for {@link MemoryEngine.recall} (the hybrid path). Mirrors the S6\n * retriever's source-scoped search plus memory-side filters.\n */\nexport interface RecallOptions {\n /** Max results (default 10). */\n limit?: number;\n /** Filter to a single {@link MemoryType}. */\n type?: MemoryType;\n /** Filter to a single host session. */\n sessionId?: string;\n}\n\n/** Options for {@link MemoryEngine.search} (the BM25-only instant path). */\nexport interface SearchOptions {\n /** Max results (default 10). */\n limit?: number;\n}\n\n/**\n * A ranked recall/search hit. `content` is the FULL observation text hydrated\n * from the authoritative KV row — never the truncated FTS snippet (DS-9).\n * `score` is the RRF-fused rank score (recall, k=60) or the BM25 score\n * (search); it is rank-based, not a normalized similarity.\n */\nexport interface MemoryHit {\n id: string;\n type: MemoryType;\n /** Full text — never truncated (DS-9). */\n content: string;\n /** RRF-fused rank score (recall) or BM25 score (search). */\n score: number;\n concepts: string[];\n files: string[];\n /** Created-at epoch millis (from the observation). */\n ts: number;\n importance: number;\n source: MemorySource;\n}\n\n// ---------------------------------------------------------------------------\n// Sessions rollup (KV `memory:sessions`)\n// ---------------------------------------------------------------------------\n\n/**\n * Per-session rollup, listed by {@link MemoryEngine.sessions}. Stored in KV\n * `memory:sessions` as a per-project list (solo user v1 — A3).\n */\nexport interface SessionInfo {\n /** Host session id. */\n id: string;\n /** Canonical project id (NEVER a filesystem path — D6). */\n project: ProjectId;\n /** Number of observations in this session. */\n count: number;\n /** Epoch millis of the most recent observation in this session. */\n lastTs: number;\n}\n\n// ---------------------------------------------------------------------------\n// Config (the runtime consolidation gate)\n// ---------------------------------------------------------------------------\n\n/**\n * Consolidation config block. Provider-EXPLICIT (blueprint D5/D6, DS-6): the\n * provider is NEVER inferred from env-var presence — no explicit `provider`\n * ⇒ {@link MemoryEngine.consolidate} refuses + logs\n * (`{ok:false, reason:'no-provider'}`) and writes a `memory:consolidation:miss`\n * KV audit entry. NEVER a silent paid call.\n */\nexport interface ConsolidationConfig {\n /** Master switch (default false). When false, `consolidate` is unregistered. */\n enabled?: boolean;\n /** Provider key, e.g. 'anthropic' | 'openai' | 'ollama'. Required to run. */\n provider?: string;\n /** Provider-specific model id. */\n model?: string;\n /** Restrict candidates to these types (default: every non-`lesson` type). */\n types?: string[];\n}\n\n/**\n * Runtime memory config consumed by the engine (the subset of the core\n * `memory:` block that affects engine behavior). The full user-facing schema\n * (capture / hooksTemplate / recall / consolidation) lives in\n * `NoirConfigSchema.memory` (@noir-ai/core, task t6); this is its runtime\n * projection. Defaults to `{}` ⇒ consolidation disabled (offline, free).\n */\nexport interface MemoryConfig {\n consolidation?: ConsolidationConfig;\n}\n\n// ---------------------------------------------------------------------------\n// Op results\n// ---------------------------------------------------------------------------\n\n/**\n * Result of {@link MemoryEngine.forget}: the KV row removed + best-effort\n * doc/vec purge (deleteDoc + deleteVec — A2's acceptable v1 behavior).\n */\nexport interface ForgetResult {\n /** Number of observations actually removed. */\n deleted: number;\n /** Ids passed to forget (echoed for the MCP envelope). */\n ids: string[];\n}\n\n/** Options for {@link MemoryEngine.consolidate}. */\nexport interface ConsolidateOptions {\n /** Restrict candidates to these types (overrides config). */\n types?: string[];\n /** Cap on candidate observations. */\n limit?: number;\n}\n\n/**\n * Result of {@link MemoryEngine.consolidate} (DS-6). Either a success appending\n * one or more derived `type:'lesson'` rows (originals never mutated), or a\n * documented refusal. A refusal is NEVER a crash and NEVER a silent paid call:\n * `logged:true` records the miss so the user can see why nothing happened.\n */\nexport type ConsolidationResult =\n | { ok: true; lessons: Observation[]; from: string[] }\n | {\n ok: false;\n /** Why consolidation did not run. */\n reason: 'no-provider' | 'model-unavailable' | 'no-candidates';\n /** True once the miss has been written to the KV audit log. */\n logged: boolean;\n };\n\n/**\n * Snapshot returned by {@link MemoryEngine.status} (mirrors `ContextStatus` /\n * `StoreStatus`). `observations` is a live read off the single writer handle.\n */\nexport interface MemoryStatus {\n ok: boolean;\n /** Canonical project id (never a filesystem path — D6). */\n projectId: string;\n /** Number of observations (rows indexed with `source:'memory'`). */\n observations: number;\n /** True when the store handle is read-only (saves/forgets will refuse). */\n degraded: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Engine contract (the `ctx.memory` service)\n// ---------------------------------------------------------------------------\n\n/**\n * The memory engine — the `ctx.memory` service (the contract the daemon seam\n * in task t4 types against, and tests mock). Constructed once per serve\n * lifecycle from the daemon's store handle + the shared S6 `EmbedFn` (mirrors\n * `ContextEngine` / `WorkflowEngine`). The daemon owns the single embedder and\n * passes the SAME `EmbedFn` to both the context and memory engines — no\n * embedder duplication (plan §Architecture).\n *\n * `consolidate` is OPTIONAL: registered only when consolidation is enabled AND\n * a provider is configured (task t5, OQ-5). Its absence is the static signal\n * that no LLM surface is wired (DS-6/D5).\n *\n * Single-writer discipline: the engine — like the context indexer — is the\n * ONLY thing that writes `source:'memory'` rows through the injected handle; it\n * never opens a second store connection (blueprint D6: in-process only, no\n * sidecar).\n */\nexport interface MemoryEngine {\n /** Persist an observation (FTS5 + vec0 + KV `memory:obs:<id>`); returns the row. */\n save(input: SaveInput): Promise<Observation>;\n /**\n * Hybrid recall: BM25 ∪ kNN fused by RRF (k=60), scoped to `source:'memory'`,\n * + cheap regex entity-boost, hydrated from KV (DS-5/DS-9). Degrades to\n * BM25-only when the embedder is unavailable.\n */\n recall(query: string, opts?: RecallOptions): Promise<MemoryHit[]>;\n /** Instant BM25-only lookup scoped to `source:'memory'` (no embed cost). */\n search(query: string, opts?: SearchOptions): Promise<MemoryHit[]>;\n /** Per-session rollups from KV `memory:sessions`. */\n sessions(): SessionInfo[];\n /** Remove observations: KV row + best-effort doc/vec purge. */\n forget(ids: string[]): ForgetResult;\n /**\n * Explicit consolidation job (DS-6). Provider-gated: refuses + logs if no\n * provider is configured — NEVER a silent paid call. Appends derived\n * `type:'lesson'` rows; originals are never mutated.\n */\n consolidate?(opts?: ConsolidateOptions): Promise<ConsolidationResult>;\n /** State snapshot (mirrors `ContextStatus` / `StoreStatus`). */\n status(): MemoryStatus;\n}\n\n// ---------------------------------------------------------------------------\n// Re-exports (single import surface — mirrors @noir-ai/context types.ts)\n// ---------------------------------------------------------------------------\n\n// The embedder seam S7 reuses: recall embeds the query via the SAME `EmbedFn`\n// the daemon already resolved for S6. Re-exported from @noir-ai/context so the\n// memory package has a single import surface for both the seam + its config.\nexport type { EmbedderConfig, EmbedFn } from '@noir-ai/context';\n// Canonical project identifier (NEVER a filesystem path — blueprint D6).\n// Re-exported so memory modules import it from `../types.js` rather than\n// reaching into @noir-ai/core directly.\nexport type { ProjectId } from '@noir-ai/core';\n","// MemoryEngine for @noir-ai/memory (slice S7, task t2).\n//\n// The single object that ties the store layer + the shared S6 embedder + the\n// optional S8 model layer together, and that the daemon injects as `ctx.memory`\n// — the new optional ServerContext service, mirroring `ctx.store` /\n// `ctx.engine` / `ctx.context`. It is constructed ONCE per serve lifecycle from\n// the daemon's already-open Store handle (the single writer — blueprint D6:\n// in-process, no sidecar, canonical ProjectId) + the SAME `EmbedFn` the daemon\n// already resolved for S6 (the daemon owns one embedder; memory takes\n// `{store, embed, ...}`, no embedder duplication — plan §Architecture).\n//\n// Public surface (the {@link MemoryEngine} contract in types.ts):\n// • save(input) → indexDoc + upsertVec + KV(memory:obs:*) + sessions rollup\n// • recall(query) → BM25 ∪ kNN fused by RRF (k=60) scoped to source:'memory',\n// + cheap regex entity-boost, hydrated from KV (DS-5/DS-9).\n// Implemented in recall.ts (t3); degrades to BM25-only when\n// the embedder is unavailable (F8).\n// • search(query) → store.searchFt BM25-only (the instant path, DS-7)\n// • sessions() → KV(memory:sessions) rollup\n// • forget(ids) → delete KV + best-effort doc/vec purge (A2)\n// • consolidate() → explicit, provider-gated job (DS-6); appends type:'lesson'\n// with provenance; originals never mutated\n// • status() → snapshot mirroring ContextStatus / StoreStatus\n// • get(id) → hydrate the full row from KV (engine-internal + tests)\n//\n// Single-writer discipline: the engine — like the context indexer — is the ONLY\n// thing that writes `source:'memory'` rows through the injected handle; it\n// never opens a second connection. Mutating ops are serialized:\n// • `save` and `consolidate` are ASYNC (they await the embedder / the model)\n// and run strictly one at a time over a promise chain (mirrors the context\n// indexer's `serialized`). Without it, two concurrent saves would each load\n// the same `memory:index` / `memory:sessions` snapshot, mutate their own\n// copy, and persist last-write-wins — orphaning the loser's row.\n// • `forget` is SYNCHRONOUS (the Store interface's deletes/sets are sync, and\n// the interface declares `forget(): ForgetResult` not a Promise). Its entire\n// read-modify-write runs in ONE synchronous block with no `await`, so it is\n// atomic w.r.t. the single-threaded event loop and cannot interleave with\n// itself; it therefore needs no chain. (A `save` suspended at `await embed`\n// and a `forget` called concurrently still commute: `save` re-reads the KV\n// inside its own post-await sync block, so it observes `forget`'s effects,\n// and `save` always mints a fresh unique id that cannot collide with a\n// forgotten one.)\n//\n// Blueprint D6 hard rules enforced here:\n// • in-process only — NO sidecar / external server;\n// • canonical ProjectId — NEVER a filesystem path;\n// • capture / store / retrieve ALWAYS local + free — no field here triggers a\n// network or LLM call;\n// • ANY LLM touch (consolidation) is OPT-IN + provider-explicit — `consolidate`\n// refuses + logs (`memory:consolidation:miss`) when no provider is\n// configured, and NEVER makes a paid call without one (the Agent-Memory\n// anti-pattern, §9);\n// • never truncate — `recall`/`search` hydrate the FULL `content` from the KV\n// row; the FTS snippet is only a preview window (DS-9).\n\nimport { randomUUID } from 'node:crypto';\nimport type { FtsHit, Store } from '@noir-ai/store';\nimport { runConsolidation } from './consolidate.js';\nimport { recallMemory } from './recall.js';\nimport {\n bumpSession,\n clearObservation,\n decrementSession,\n getObservation,\n getObservationIds,\n getSessions,\n setObservation,\n setObservationIds,\n} from './store.js';\nimport {\n type ConsolidateOptions,\n type ConsolidationResult,\n DEFAULT_IMPORTANCE,\n type EmbedFn,\n type ForgetResult,\n type MemoryConfig,\n type MemoryEngine,\n type MemoryHit,\n type MemoryStatus,\n type Observation,\n type ProjectId,\n type RecallOptions,\n type SaveInput,\n type SearchOptions,\n type SessionInfo,\n} from './types.js';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Source bucket for every memory row (keeps context + memory disjoint, DS-2). */\nconst MEMORY_SOURCE = 'memory';\n\n/** Default recall/search hit cap (mirrors Store.searchFt's default). */\nconst DEFAULT_SEARCH_LIMIT = 10;\n\n/** Default observation type when {@link SaveInput.type} is omitted. */\nconst DEFAULT_TYPE: Observation['type'] = 'fact';\n\n// ---------------------------------------------------------------------------\n// S8 model injection (the ONLY LLM entry point — provider-gated, D5/DS-6)\n// ---------------------------------------------------------------------------\n\n/**\n * Per-call request shape passed to {@link MemoryModel.complete}. A structural\n * subset of S8's `CompleteRequest` (provider-EXPLICIT; no `tools`/`stream` —\n * single-shot only, blueprint D5). Defined locally so the memory package has no\n * value-level dependency on `@noir-ai/model`; the daemon seam (t6) binds\n * `complete(req, cfg)` into this shape, and tests inject a fake.\n */\nexport interface MemoryCompleteRequest {\n system?: string;\n prompt: string;\n /** Provider block name — explicit, NEVER env-inferred (D5/DS-6). */\n provider: string;\n /** Model id for this call. */\n model: string;\n maxTokens?: number;\n /** Bounded task tier (DS-9). Selects a per-tier output cap; never a provider. */\n tier?: 'draft' | 'title' | 'summarize' | 'consolidate';\n}\n\n/**\n * The subset of S8's `CompleteResult` that consolidation consumes. `null` is\n * first-class degradation (no provider resolvable at call time — the always-\n * available offline path, D5); `{ok:false}` is an attempted-call failure.\n */\nexport type MemoryCompleteResult =\n | { ok: true; text: string }\n | { ok: false; reason: string }\n | null;\n\n/**\n * The optional S8 model injection. Its absence is the runtime signal that the\n * bounded model layer is not wired (consolidation then refuses\n * `'model-unavailable'` — the documented S7 stub, OQ-3/OQ-8). When present,\n * `complete` is the SOLE LLM entry point and is reached ONLY after the provider\n * gate passes (DS-6: never a silent paid call).\n */\nexport interface MemoryModel {\n complete(req: MemoryCompleteRequest): Promise<MemoryCompleteResult>;\n}\n\n// ---------------------------------------------------------------------------\n// Construction options\n// ---------------------------------------------------------------------------\n\n/** Construction options for {@link MemoryEngineImpl} / {@link createMemoryEngine}. */\nexport interface MemoryEngineOptions {\n /** The daemon's store handle — the ONLY storage surface used (single writer). */\n store: Store;\n /**\n * Project root. Stored for API symmetry with {@link ContextEngine} and future\n * path normalization; v1 stores {@link Observation.files} repo-relative as-is.\n */\n root: string;\n /** Canonical project identifier (NEVER a filesystem path — blueprint D6). */\n projectId: ProjectId;\n /**\n * The shared S6 embedder (the daemon resolves it once and passes the SAME\n * `EmbedFn` to context + memory). A throw on `embed()` ⇒ the vec index is\n * skipped for that observation (F8-style degradation — the row is still\n * BM25-searchable via FTS5 + the authoritative KV row).\n */\n embed: EmbedFn;\n /** Optional S8 model injection for consolidation (absent ⇒ consolidation refuses). */\n model?: MemoryModel;\n /** Runtime memory config (the consolidation gate). Defaults to `{}` (offline). */\n config?: MemoryConfig;\n /** True when `store` was opened read-only (the daemon-down fallback). */\n storeDegraded?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Engine\n// ---------------------------------------------------------------------------\n\n/**\n * Noir's cross-session memory engine — the `ctx.memory` service. Constructed\n * once per serve lifecycle (mirror `ContextEngine` / `WorkflowEngine`) from the\n * daemon's store handle + the shared S6 `EmbedFn` + an optional S8 model.\n * Implements the {@link MemoryEngine} contract; {@link get} is an extra public\n * method (beyond the interface) for recall hydration + tests.\n *\n * `consolidate` is ALWAYS present on the class and self-gates on the configured\n * provider; the daemon decides whether to register the `memory_consolidate` MCP\n * tool from `config.memory.consolidation.enabled` (OQ-5). Its refusal paths are\n * the documented stub behavior (spec §7) — never a crash, never a silent call.\n */\nexport class MemoryEngineImpl implements MemoryEngine {\n /** The daemon's single-writer store handle (possibly read-only). */\n readonly store: Store;\n /** Project root (stored for symmetry / future path normalization). */\n readonly root: string;\n /** Canonical project identifier (NEVER a filesystem path — D6). */\n readonly projectId: ProjectId;\n /**\n * Persistent degradation flag (read-only store). Mutating ops throw a clear\n * error upfront instead of letting the first write fail mid-run; reads\n * (recall/search/sessions/status) keep working.\n */\n readonly degraded: boolean;\n\n private readonly embed: EmbedFn;\n private readonly model: MemoryModel | undefined;\n private readonly config: MemoryConfig;\n\n // Single-flight serialization of ASYNC mutating ops (save / consolidate).\n // `forget` is sync + atomic (see file header) and bypasses this chain.\n private chain: Promise<unknown> = Promise.resolve();\n\n constructor(opts: MemoryEngineOptions) {\n this.store = opts.store;\n this.root = opts.root;\n this.projectId = opts.projectId;\n this.embed = opts.embed;\n this.model = opts.model;\n this.config = opts.config ?? {};\n this.degraded = opts.storeDegraded === true;\n }\n\n // -------------------------------------------------------------------------\n // save\n // -------------------------------------------------------------------------\n\n /** @inheritDoc MemoryEngine.save */\n save(input: SaveInput): Promise<Observation> {\n return this.serialized(() => this.saveInternal(input));\n }\n\n private async saveInternal(input: SaveInput): Promise<Observation> {\n this.assertNotDegraded('save');\n const ts = Date.now();\n const observation: Observation = {\n id: randomUUID(),\n type: input.type ?? DEFAULT_TYPE,\n content: input.content,\n project: this.projectId,\n sessionId: input.sessionId ?? null,\n ts,\n lastAccessTs: ts,\n importance: input.importance ?? DEFAULT_IMPORTANCE,\n concepts: input.concepts ?? [],\n files: input.files ?? [],\n source: 'explicit',\n };\n // Embed BEFORE the synchronous KV block so the read-modify-write of\n // `memory:index` / `memory:sessions` runs without an `await` in between\n // (atomic w.r.t. the event loop — see file header).\n const vec = await this.embedBestEffort(observation.content);\n this.indexObservation(observation, vec);\n return observation;\n }\n\n /**\n * Write one observation to ALL three indexes in a single synchronous block:\n * FTS5 (`indexDoc`, content searchable) + sqlite-vec (`upsertVec`, best-effort\n * — skipped if the embedder or vec0 is unavailable, F8-style) + the\n * authoritative KV row (`memory:obs:<id>`) + the id index + the sessions\n * rollup. The `docs.meta` payload is the denormalized search projection\n * (Observation minus content); the KV row is the source of truth (R4).\n */\n private indexObservation(observation: Observation, vec: Float32Array | null): void {\n this.store.indexDoc({\n id: observation.id,\n source: MEMORY_SOURCE,\n content: observation.content,\n meta: obsMeta(observation),\n });\n if (vec !== null) {\n try {\n this.store.upsertVec(observation.id, vec, { source: MEMORY_SOURCE });\n } catch {\n // vec0 unavailable (native binary missing / read-only vec) — the row is\n // still BM25-searchable + hydrated from KV. Never crash a save on vec.\n }\n }\n setObservation(this.store, observation);\n // Append to the id index (idempotent on id — provenance lessons re-use save).\n const ids = getObservationIds(this.store);\n if (!ids.includes(observation.id)) {\n ids.push(observation.id);\n setObservationIds(this.store, ids);\n }\n if (observation.sessionId !== null) {\n bumpSession(this.store, observation.sessionId, observation.project, observation.ts);\n }\n }\n\n // -------------------------------------------------------------------------\n // get (extra public method — hydrate the FULL row from KV, DS-9)\n // -------------------------------------------------------------------------\n\n /**\n * Hydrate the full {@link Observation} for `id` from the authoritative KV row.\n * Returns `null` for an unknown / forgotten id. This is the only correct way\n * to read an observation's complete `content` (DS-9).\n */\n get(id: string): Observation | null {\n return getObservation(this.store, id);\n }\n\n // -------------------------------------------------------------------------\n // recall (t3: hybrid BM25 ∪ kNN + RRF + entity-boost — see recall.ts)\n // -------------------------------------------------------------------------\n\n /** @inheritDoc MemoryEngine.recall */\n async recall(query: string, opts?: RecallOptions): Promise<MemoryHit[]> {\n // Hybrid (t3): BM25 ∪ kNN fused by RRF (k=60, weights [0.5,0.5]) scoped to\n // source:'memory', + cheap regex entity-boost, hydrated to FULL content from\n // the authoritative KV row (DS-5/DS-9). Degrades to BM25-only when the\n // embedder is unavailable (F8) — `recallMemory` carries the `degraded` /\n // `mode` signal; the public {@link MemoryEngine.recall} contract returns the\n // hits. Read-only against the injected store; not serialized (concurrent\n // recalls are safe — they never mutate state).\n const { hits } = await recallMemory({ store: this.store, embed: this.embed }, query, opts);\n return hits;\n }\n\n // -------------------------------------------------------------------------\n // search (BM25-only instant path — final design, DS-7)\n // -------------------------------------------------------------------------\n\n /** @inheritDoc MemoryEngine.search */\n async search(query: string, opts?: SearchOptions): Promise<MemoryHit[]> {\n const limit = opts?.limit ?? DEFAULT_SEARCH_LIMIT;\n let ftsHits: FtsHit[] = [];\n try {\n ftsHits = this.store.searchFt(query, { source: MEMORY_SOURCE, limit });\n } catch {\n return [];\n }\n return this.hydrateHits(\n ftsHits.map((h) => ({ id: h.id, score: h.score })),\n undefined,\n );\n }\n\n /**\n * Hydrate a ranked id list into {@link MemoryHit}s from the authoritative KV\n * row, applying the optional `type` / `sessionId` filters. Each hit carries\n * the FULL `content` (DS-9). A ranked id with no KV row (a stale vec-only hit\n * whose row was forgotten) is dropped — never emitted with partial data.\n */\n private hydrateHits(\n scored: ReadonlyArray<{ id: string; score: number }>,\n opts: RecallOptions | undefined,\n ): MemoryHit[] {\n const out: MemoryHit[] = [];\n for (const row of scored) {\n const obs = getObservation(this.store, row.id);\n if (obs === null) continue;\n if (opts?.type !== undefined && obs.type !== opts.type) continue;\n if (opts?.sessionId !== undefined && obs.sessionId !== opts.sessionId) continue;\n out.push(toMemoryHit(obs, row.score));\n }\n return out;\n }\n\n // -------------------------------------------------------------------------\n // sessions\n // -------------------------------------------------------------------------\n\n /** @inheritDoc MemoryEngine.sessions */\n sessions(): SessionInfo[] {\n return getSessions(this.store);\n }\n\n // -------------------------------------------------------------------------\n // forget (synchronous + atomic — see file header)\n // -------------------------------------------------------------------------\n\n /** @inheritDoc MemoryEngine.forget */\n forget(ids: string[]): ForgetResult {\n this.assertNotDegraded('forget');\n let deleted = 0;\n const idSet = new Set(ids);\n for (const id of ids) {\n const obs = getObservation(this.store, id);\n if (obs === null) continue;\n // Best-effort doc/vec purge (A2) + authoritative KV row clear. deleteDoc /\n // deleteVec are idempotent in the store, so a missing row is harmless.\n this.store.deleteDoc(id);\n try {\n this.store.deleteVec(id);\n } catch {\n // vec0 unavailable — the KV + FTS cleanup is the load-bearing part.\n }\n clearObservation(this.store, id);\n if (obs.sessionId !== null) {\n decrementSession(this.store, obs.sessionId);\n }\n deleted += 1;\n }\n // Drop the forgotten ids from the index (RMW, sync ⇒ atomic) so status()\n // and consolidate see a consistent count even if some ids were unknown.\n if (deleted > 0) {\n const remaining = getObservationIds(this.store).filter((i) => !idSet.has(i));\n setObservationIds(this.store, remaining);\n }\n return { deleted, ids };\n }\n\n // -------------------------------------------------------------------------\n // consolidate (explicit, provider-gated — DS-6)\n // -------------------------------------------------------------------------\n\n /** @inheritDoc MemoryEngine.consolidate */\n consolidate(opts?: ConsolidateOptions): Promise<ConsolidationResult> {\n return this.serialized(() => this.consolidateInternal(opts));\n }\n\n private async consolidateInternal(opts?: ConsolidateOptions): Promise<ConsolidationResult> {\n // Delegate to the standalone consolidation module (task t5). The engine\n // supplies the single-writer store handle, the optional S8 model injection,\n // the provider-explicit config, the canonical projectId, and an\n // `indexDerived` callback that embeds the lesson best-effort then writes it\n // through the SAME shared `indexObservation` path as a user `save` — so a\n // derived lesson lands identically (FTS5 + vec + KV + id index) and stays\n // searchable + hydratable. The gate logic (no-provider / model-unavailable /\n // no-candidates) + the append-only lesson construction live in consolidate.ts.\n return runConsolidation(\n {\n store: this.store,\n model: this.model,\n config: this.config,\n projectId: this.projectId,\n indexDerived: async (obs) => {\n const vec = await this.embedBestEffort(obs.content);\n this.indexObservation(obs, vec);\n },\n },\n opts,\n );\n }\n\n // -------------------------------------------------------------------------\n // status\n // -------------------------------------------------------------------------\n\n /** @inheritDoc MemoryEngine.status */\n status(): MemoryStatus {\n return {\n ok: true,\n projectId: this.store.projectId,\n observations: getObservationIds(this.store).length,\n degraded: this.degraded,\n };\n }\n\n // -------------------------------------------------------------------------\n // shared internals\n // -------------------------------------------------------------------------\n\n /**\n * Best-effort embedding: returns the vec, or `null` if the embedder is\n * unavailable (`kind:'none'`, native load failure, provider error). A `null`\n * vec skips `upsertVec` so the save still succeeds — the row is BM25-searchable\n * + hydrated from KV (F8-style degradation).\n */\n private async embedBestEffort(content: string): Promise<Float32Array | null> {\n try {\n return await this.embed(content);\n } catch {\n return null;\n }\n }\n\n /** Throw a clear, typed error when the store handle is read-only. */\n private assertNotDegraded(op: string): void {\n if (this.degraded) {\n throw new Error(`memory ${op} refused: store is read-only (daemon down)`);\n }\n }\n\n /**\n * Single-flight serialization of async mutating ops (mirrors the context\n * indexer's `serialized`). Forces `work` to run strictly after the previous\n * queued op completes. A failed op does NOT poison the queue — the chain\n * advances regardless, and the caller observes the real outcome via `result`.\n */\n private serialized<T>(work: () => Promise<T>): Promise<T> {\n const result = this.chain.then(work);\n this.chain = result.then(\n () => undefined,\n () => undefined,\n );\n return result;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory (the daemon seam / tests construct via this — mirrors buildContextEngine)\n// ---------------------------------------------------------------------------\n\n/**\n * Build a {@link MemoryEngineImpl} bound to a single store handle + the shared\n * S6 embedder + an optional S8 model. Constructed once per serve lifecycle\n * alongside the context + workflow engines. The daemon passes the SAME `embed`\n * it resolved for S6 (no embedder duplication) and resolves the model via S8's\n * `resolveModelConfig` only when `config.memory.consolidation.enabled` is set\n * (provider-explicit — never a silent paid call, D6).\n */\nexport function createMemoryEngine(opts: MemoryEngineOptions): MemoryEngineImpl {\n return new MemoryEngineImpl(opts);\n}\n\n// ---------------------------------------------------------------------------\n// Small pure helpers (module-local)\n// ---------------------------------------------------------------------------\n\n/**\n * Build the denormalized `docs.meta` search payload (Observation minus `content`\n * + `id`). Written alongside the authoritative KV row; the KV row is the source\n * of truth for hydration (R4).\n */\nfunction obsMeta(obs: Observation): Record<string, unknown> {\n const meta: Record<string, unknown> = {\n type: obs.type,\n project: obs.project,\n sessionId: obs.sessionId,\n ts: obs.ts,\n lastAccessTs: obs.lastAccessTs,\n importance: obs.importance,\n concepts: obs.concepts,\n files: obs.files,\n source: obs.source,\n };\n if (obs.provenance !== undefined) meta.provenance = obs.provenance;\n return meta;\n}\n\n/** Project an {@link Observation} into a {@link MemoryHit} at a given score. */\nfunction toMemoryHit(obs: Observation, score: number): MemoryHit {\n return {\n id: obs.id,\n type: obs.type,\n content: obs.content,\n score,\n concepts: obs.concepts,\n files: obs.files,\n ts: obs.ts,\n importance: obs.importance,\n source: obs.source,\n };\n}\n","// Hybrid recall pipeline for @noir-ai/memory (slice S7, task t3).\n//\n// Reuses the S6 hybrid retriever's recipe (DS-5) — BM25 (`Store.searchFt`) ∪\n// cosine kNN (`Store.knn`) fused by Reciprocal Rank Fusion (rank-based, k=60,\n// weights [0.5, 0.5] — raw BM25+cosine scores are NEVER summed) — scoped to\n// `source:'memory'` so context (S6) and memory never collide. Adds a cheap\n// regex **entity-boost** (identifiers / file-paths extracted from the query,\n// NO LLM — DS-5) that promotes hits whose `content` / `concepts` / `files`\n// mention a queried entity.\n//\n// Pipeline (spec §6):\n// recallMemory(query, opts)\n// ├─ store.searchFt(query, {limit, source:'memory'}) → FtsHit[] (BM25)\n// ├─ store.knn(await embed(query), {limit, source:'memory'}) → VecHit[] (kNN)\n// ├─ fuseRrf(bm25, knn, {k:60, weights:[0.5,0.5]}) → RrfResult[] (rank fusion)\n// ├─ entity-boost: extract query entities (regex), add a per-match delta to\n// │ each fused row's score (needs the obs → hydrated first), then re-sort\n// │ by boosted score desc (stable — RRF order preserved on ties).\n// └─ hydrate each fused id into a MemoryHit from the authoritative KV row\n// `memory:obs:<id>` (FULL content — never the truncated FTS snippet,\n// DS-9), applying the optional `type` / `sessionId` filters.\n//\n// Degradation (mirrors the S6 retriever's F8): if `embed()` throws (the\n// embedder is `kind:'none'`, a native load failure, a provider error) OR `knn()`\n// itself threw, the kNN leg is skipped and recall degrades to BM25-only — the\n// searchFt results are still returned, fused trivially (each with only its BM25\n// rank term), and the outcome carries `degraded:true, mode:'bm25-only'`. A BM25\n// throw (e.g. a foreign read-only DB missing `docs_fts`) is also caught: recall\n// keeps going on the kNN leg when it can, or returns an empty result set rather\n// than crashing.\n//\n// This module owns NO state and opens NO second store connection — it reads\n// through the INJECTED handle only (blueprint D6: in-process only, canonical\n// ProjectId, capture/store/retrieve always local + free). The engine passes its\n// single-writer handle + the SAME `EmbedFn` the daemon already resolved for S6\n// (no embedder duplication). `lastAccessTs` is intentionally NOT bumped here:\n// recall is a read pipeline, kept side-effect-free + deterministic for tests;\n// the field is set at save time and a best-effort bump is a tracked v1.x extra\n// (the task marks the bump optional).\n\nimport { fuseRrf } from '@noir-ai/context';\nimport type { FtsHit, Store, VecHit } from '@noir-ai/store';\nimport { getObservation } from './store.js';\nimport type { EmbedFn, MemoryHit, Observation, RecallOptions } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/** Source bucket for every memory row (keeps context + memory disjoint, DS-2). */\nconst MEMORY_SOURCE = 'memory';\n\n/** Default recall hit cap (mirrors Store.searchFt + the S6 retriever default). */\nconst DEFAULT_RECALL_LIMIT = 10;\n\n/**\n * Minimum token length to count as an entity. Filters noise from tiny tokens\n * (`ts`, `a`, `an`, `is`) that would otherwise substring-match half the corpus.\n */\nconst MIN_ENTITY_LEN = 3;\n\n/**\n * Score delta added per DISTINCT query entity an observation matches. RRF\n * scores are tiny (rank-based: max ≈ 2·0.5/(60+1) ≈ 0.016), so a single entity\n * match (0.1) deliberately OUTWEIGHS any rank gap — an explicit identifier /\n * path in the query is a strong intent signal, and a hit that mentions it\n * outranks one that does not. Multiple distinct matches stack. Tunable.\n */\nconst ENTITY_BOOST_PER_MATCH = 0.1;\n\n/**\n * Common dev-query noise words dropped before matching so they do not inflate\n * the boost (every hit mentioning \"the\"/\"for\"/\"use\" would otherwise tie). Kept\n * small + conservative — only words that carry no retrieval signal.\n */\nconst STOPWORDS: ReadonlySet<string> = new Set([\n 'the',\n 'and',\n 'for',\n 'with',\n 'that',\n 'this',\n 'from',\n 'have',\n 'your',\n 'was',\n 'are',\n 'can',\n 'how',\n 'when',\n 'what',\n 'where',\n 'why',\n 'who',\n 'use',\n 'using',\n 'used',\n 'into',\n 'but',\n 'not',\n 'you',\n 'all',\n 'any',\n 'get',\n 'set',\n 'put',\n 'new',\n 'one',\n 'two',\n 'via',\n 'our',\n 'its',\n 'etc',\n]);\n\n// ---------------------------------------------------------------------------\n// Dependencies + outcome\n// ---------------------------------------------------------------------------\n\n/** The injected store handle + the shared S6 embedder (read-only pipeline). */\nexport interface RecallDeps {\n /** The daemon's single-writer store handle (may be read-only — reads keep working). */\n store: Store;\n /** Query embedder. A throw on `embed(query)` ⇒ BM25-only degradation. */\n embed: EmbedFn;\n}\n\n/** Internal outcome of {@link recallMemory} (the engine projects this to `MemoryHit[]`). */\nexport interface RecallMemoryResult {\n /** Ranked, hydrated hits (FULL content — DS-9), truncated to `limit`. */\n hits: MemoryHit[];\n /** True when the kNN OR BM25 leg failed this call (honest per-query signal). */\n degraded: boolean;\n /** `'hybrid'` when the kNN leg ran, `'bm25-only'` when it was skipped. */\n mode: 'hybrid' | 'bm25-only';\n}\n\n// ---------------------------------------------------------------------------\n// Entity extraction (cheap regex, NO LLM — DS-5)\n// ---------------------------------------------------------------------------\n\n/**\n * Cheap regex extraction of identifiers + file/path tokens from a query (NO LLM\n * — DS-5). Two kinds of entity are collected, de-duplicated:\n *\n * 1. **Qualified tokens** — whitespace-delimited tokens that contain a `/`,\n * `.`, or `:` (e.g. `packages/memory/src/recall.ts`, `memory:obs`,\n * `MemoryEngine.save`). Kept whole + lowercased so they match an\n * observation's `files` / `concepts` exactly when it mentions the same path\n * or qualified name.\n * 2. **Identifier subwords** — every token is also split at camelCase /\n * PascalCase / snake_case / kebab-case boundaries into lowercase subwords\n * (mirrors the index-side `explodeIdentifiers` convention from\n * `@noir-ai/context`), so a query for `ContextEngine` yields `context` +\n * `engine` and a query for `recall_memory` yields `recall` + `memory`.\n *\n * Tokens shorter than {@link MIN_ENTITY_LEN} and {@link STOPWORDS} are dropped\n * to keep matching precise. Pure + deterministic.\n */\nexport function extractEntities(query: string): string[] {\n const set = new Set<string>();\n const words = query.match(/\\S+/g);\n if (words === null) return [];\n for (const token of words) {\n // Qualified (path / dotted / colon) token: keep whole, lowercased, so a\n // full path or qualified name can match an obs `files` entry verbatim.\n if (/[/.:]/.test(token)) {\n const lower = token.toLowerCase();\n if (lower.length >= MIN_ENTITY_LEN) set.add(lower);\n }\n // Explode every token into lowercase identifier subwords.\n for (const piece of splitIdentifier(token)) {\n if (piece.length < MIN_ENTITY_LEN) continue;\n if (STOPWORDS.has(piece)) continue;\n set.add(piece);\n }\n }\n return [...set];\n}\n\n/**\n * Split a token into lowercase subwords at camelCase / PascalCase / snake_case /\n * kebab-case boundaries. A focused re-implementation of `@noir-ai/context`'s\n * `explodeIdentifiers` (kept local so the recall path depends on context only\n * for the `fuseRrf` fusion primitive, not the chunker). Pure + deterministic.\n */\nfunction splitIdentifier(token: string): string[] {\n // Alphanumeric runs only; '-' and '_' are left as explicit separators so\n // kebab / snake identifiers split at their boundaries for free.\n const words = token.match(/[A-Za-z0-9]+/g);\n if (words === null) return [];\n const out: string[] = [];\n for (const word of words) {\n const spaced = word\n // lowercase|digit → uppercase: myVar → my Var | HttpContext → Http Context\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n // uppercase-run → uppercase+lowercase: XMLHttp → XML Http | HTTPSConn → HTTPS Conn\n .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');\n for (const piece of spaced.split(/\\s+/)) {\n if (piece.length > 0) out.push(piece.toLowerCase());\n }\n }\n return out;\n}\n\n/**\n * Boost for one observation given the extracted query entities: +1 delta per\n * DISTINCT entity that appears in the obs's `content` (case-insensitive\n * substring), `concepts` (exact, case-insensitive), or `files` (case-insensitive\n * substring, so a bare `recall.ts` matches `packages/memory/src/recall.ts`).\n * Each entity contributes at most once (no double-count across fields). Cheap\n * + LLM-free (DS-5).\n */\nfunction entityBoostForObs(obs: Observation, entities: ReadonlyArray<string>): number {\n if (entities.length === 0) return 0;\n const contentLower = obs.content.toLowerCase();\n let matched = 0;\n for (const entity of entities) {\n if (contentLower.includes(entity)) {\n matched += 1;\n continue;\n }\n if (obs.concepts.some((c) => c.toLowerCase() === entity)) {\n matched += 1;\n continue;\n }\n if (obs.files.some((f) => f.toLowerCase().includes(entity))) {\n matched += 1;\n }\n }\n return matched * ENTITY_BOOST_PER_MATCH;\n}\n\n// ---------------------------------------------------------------------------\n// Hybrid recall\n// ---------------------------------------------------------------------------\n\n/**\n * Run hybrid recall (BM25 ∪ kNN → RRF → entity-boost → KV hydration) scoped to\n * `source:'memory'`. Read-only against the injected store; no second connection,\n * no network, no LLM (blueprint D6).\n *\n * The returned hits carry the FULL `content` hydrated from the authoritative KV\n * row (DS-9 — never the truncated FTS snippet). `degraded`/`mode` describe the\n * actual outcome of THIS call: `mode:'bm25-only'` + `degraded:true` when the\n * embedder was unavailable and recall fell back to BM25.\n */\nexport async function recallMemory(\n deps: RecallDeps,\n query: string,\n opts?: RecallOptions,\n): Promise<RecallMemoryResult> {\n const store = deps.store;\n const limit = opts?.limit ?? DEFAULT_RECALL_LIMIT;\n\n // --- BM25 leg (always attempted; the cheap, always-available signal) ---\n let ftsHits: FtsHit[] = [];\n let ftsFailed = false;\n try {\n ftsHits = store.searchFt(query, { limit, source: MEMORY_SOURCE });\n } catch {\n // e.g. a foreign read-only DB with no `docs_fts` table. Keep going — the\n // vec leg may still return something; flag degraded.\n ftsFailed = true;\n }\n\n // --- kNN leg (attempted; any failure ⇒ BM25-only degradation, F8-style) ---\n let knnHits: VecHit[] = [];\n let knnFailed = false;\n try {\n const qvec = await deps.embed(query); // throws on kind:'none' / load / provider error\n try {\n knnHits = store.knn(qvec, { limit, source: MEMORY_SOURCE });\n } catch {\n // vec0 table missing or query malformed — degrade to BM25-only.\n knnFailed = true;\n }\n } catch {\n // embed() threw: the embedder is unavailable. BM25-only (F8).\n knnFailed = true;\n }\n\n const mode: 'hybrid' | 'bm25-only' = knnFailed ? 'bm25-only' : 'hybrid';\n const degraded = knnFailed || ftsFailed;\n\n // --- RRF fusion (rank-based; raw BM25+cosine scores NEVER summed) ---\n const fused = fuseRrf(ftsHits, knnHits);\n\n // --- Hydrate + filter + entity-boost, then stable re-sort by boosted score ---\n const entities = extractEntities(query);\n const hits: MemoryHit[] = [];\n for (const row of fused) {\n const obs = getObservation(store, row.id);\n // A fused id with no KV row is a stale vec-only hit whose row was forgotten\n // — never emit it with partial data (mirrors the engine's hydrateHits).\n if (obs === null) continue;\n if (opts?.type !== undefined && obs.type !== opts.type) continue;\n if (opts?.sessionId !== undefined && obs.sessionId !== opts.sessionId) continue;\n hits.push(toMemoryHit(obs, row.score + entityBoostForObs(obs, entities)));\n }\n\n // Stable sort by boosted score desc. Node's Array.prototype.sort is stable\n // (V8 ≥ 7.0), so equal scores preserve insertion order — which is fuseRrf's\n // own deterministic order (score desc → best rank → first-seen). Truncate to\n // `limit` AFTER the sort so a boosted hit can leapfrog into the top window.\n hits.sort((a, b) => b.score - a.score);\n return { hits: hits.slice(0, limit), degraded, mode };\n}\n\n// ---------------------------------------------------------------------------\n// Small pure helpers (module-local)\n// ---------------------------------------------------------------------------\n\n/**\n * Project an {@link Observation} into a {@link MemoryHit} at a given (already\n * boosted) score. Carries the FULL `content` (DS-9). Mirrors the engine's\n * private `toMemoryHit` shape exactly so recall + search hits read the same.\n */\nfunction toMemoryHit(obs: Observation, score: number): MemoryHit {\n return {\n id: obs.id,\n type: obs.type,\n content: obs.content,\n score,\n concepts: obs.concepts,\n files: obs.files,\n ts: obs.ts,\n importance: obs.importance,\n source: obs.source,\n };\n}\n"],"mappings":"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","names":["randomUUID","MEMORY_SOURCE","randomUUID","toMemoryHit"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@noir-ai/memory",
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
|
+
"description": "Noir memory — cross-session observations over the store, with hybrid recall and provider-gated consolidation.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "agaaaptr",
|
|
7
|
+
"homepage": "https://github.com/agaaaptr/noir#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/agaaaptr/noir.git",
|
|
11
|
+
"directory": "packages/memory"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/agaaaptr/noir/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"noir",
|
|
18
|
+
"memory",
|
|
19
|
+
"cross-session",
|
|
20
|
+
"recall",
|
|
21
|
+
"consolidation",
|
|
22
|
+
"agent"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public",
|
|
29
|
+
"provenance": true
|
|
30
|
+
},
|
|
31
|
+
"type": "module",
|
|
32
|
+
"main": "./dist/index.js",
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"import": "./dist/index.js"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"README.md"
|
|
43
|
+
],
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@noir-ai/core": "1.0.0-beta.1",
|
|
46
|
+
"@noir-ai/context": "1.0.0-beta.1",
|
|
47
|
+
"@noir-ai/store": "1.0.0-beta.1"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^26.1.1"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsup",
|
|
54
|
+
"typecheck": "tsc --noEmit"
|
|
55
|
+
}
|
|
56
|
+
}
|