claude_memory 0.12.1 → 0.13.0

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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/memory.sqlite3 +0 -0
  3. data/.claude/rules/claude_memory.generated.md +6 -1
  4. data/.claude/settings.local.json +2 -1
  5. data/.claude-plugin/marketplace.json +2 -2
  6. data/.claude-plugin/plugin.json +2 -2
  7. data/CHANGELOG.md +28 -0
  8. data/CLAUDE.md +11 -6
  9. data/README.md +35 -0
  10. data/db/migrations/019_add_observations.rb +43 -0
  11. data/db/migrations/020_add_observation_promotion.rb +33 -0
  12. data/docs/GETTING_STARTED.md +38 -0
  13. data/docs/api_stability.md +16 -5
  14. data/docs/architecture.md +18 -6
  15. data/docs/audit_runbook.md +67 -0
  16. data/docs/dashboard.md +28 -0
  17. data/docs/improvements.md +94 -1
  18. data/docs/influence/mastra-observational-memory.md +198 -0
  19. data/docs/influence/strands-agent-sops.md +163 -0
  20. data/docs/quality_review.md +45 -0
  21. data/lib/claude_memory/audit/checks.rb +149 -0
  22. data/lib/claude_memory/audit/runner.rb +4 -0
  23. data/lib/claude_memory/commands/census_command.rb +1 -1
  24. data/lib/claude_memory/commands/hook_command.rb +16 -3
  25. data/lib/claude_memory/commands/initializers/hooks_configurator.rb +3 -1
  26. data/lib/claude_memory/commands/install_skill_command.rb +4 -0
  27. data/lib/claude_memory/commands/observations_command.rb +367 -0
  28. data/lib/claude_memory/commands/registry.rb +1 -0
  29. data/lib/claude_memory/commands/skills/reflect.md +68 -0
  30. data/lib/claude_memory/commands/stats_command.rb +60 -1
  31. data/lib/claude_memory/dashboard/api.rb +4 -0
  32. data/lib/claude_memory/dashboard/index.html +154 -2
  33. data/lib/claude_memory/dashboard/observations.rb +115 -0
  34. data/lib/claude_memory/dashboard/server.rb +1 -0
  35. data/lib/claude_memory/distill/extraction.rb +6 -4
  36. data/lib/claude_memory/distill/null_distiller.rb +86 -3
  37. data/lib/claude_memory/distill/reference_material_detector.rb +4 -1
  38. data/lib/claude_memory/domain/observation.rb +118 -0
  39. data/lib/claude_memory/embeddings/generator.rb +1 -1
  40. data/lib/claude_memory/hook/context_injector.rb +100 -2
  41. data/lib/claude_memory/mcp/handlers/management_handlers.rb +113 -2
  42. data/lib/claude_memory/mcp/handlers/query_handlers.rb +48 -1
  43. data/lib/claude_memory/mcp/instructions_builder.rb +1 -0
  44. data/lib/claude_memory/mcp/query_guide.rb +28 -0
  45. data/lib/claude_memory/mcp/tool_definitions.rb +58 -0
  46. data/lib/claude_memory/mcp/tools.rb +3 -0
  47. data/lib/claude_memory/observe/observations_renderer.rb +49 -0
  48. data/lib/claude_memory/observe/reflector.rb +91 -0
  49. data/lib/claude_memory/publish.rb +53 -1
  50. data/lib/claude_memory/resolve/resolver.rb +45 -8
  51. data/lib/claude_memory/store/schema_manager.rb +1 -1
  52. data/lib/claude_memory/store/sqlite_store.rb +181 -0
  53. data/lib/claude_memory/sweep/maintenance.rb +15 -1
  54. data/lib/claude_memory/sweep/sweeper.rb +7 -1
  55. data/lib/claude_memory/version.rb +1 -1
  56. data/lib/claude_memory.rb +5 -0
  57. metadata +11 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cec498c9aec388d1decdcb7ae05616fb414d7f026401248b6a051d051d238e39
4
- data.tar.gz: 9a8b44aa97db10ead04a074f2619f08cdb74976dd22ce7843a5968bbbbda410e
3
+ metadata.gz: 93e553fc87bd36ebe27b8709a28dea9bd206a669491327fc8b6340e6078a8f67
4
+ data.tar.gz: c19722c5028ed5746f58792bb34e57939600deaa0a59b6ff00a421b3ed829bdc
5
5
  SHA512:
6
- metadata.gz: 22c84d179c6af89f06909604674794741739237edc5cdb11c18bd9ba06ae4f9334bf20a4d8e8057181d25a206419cf50b35fec392ec3cd7d5fb093ea0046ef0c
7
- data.tar.gz: a138ae2049416a7ec21ecef3545115a8415a2d64618d8f3334a50e2b6d552b61a7056824f5ef968b78907cfbc6515e38f60a1feed75a4db8b94793736bf0826d
6
+ metadata.gz: 7bd3a98c636c525e2bfc579e9e265ecef3a07311d4ba543d692abc092ddc0822df86832b9e5b53d802597a6dbe7495aa192210d6d4e5961c21383daac22cef09
7
+ data.tar.gz: b587239c11c342551f35937ecdbb108254269306fa98fbaac6680d23f8e427a32f26cbb1524c8f19a08da1773e292b6d75df92e101cb724d59fd65017f275832
Binary file
@@ -1,13 +1,18 @@
1
1
  <!--
2
2
  This file is auto-generated by claude-memory.
3
3
  Do not edit manually - changes will be overwritten.
4
- Generated: 2026-06-03T13:21:02Z
4
+ Generated: 2026-06-16T18:31:16Z
5
5
  -->
6
6
 
7
7
  # Project Memory
8
8
 
9
9
  ## Current Decisions
10
10
 
11
+ - From Mastra Observational Memory study: add an episodic observation layer that augments (not replaces) the dynamic-recall semantic fact store — because facts answer 'what is true' and observations answer 'what happened', and claude_memory currently lacks the episodic half; recall stays for targeted lookups.
12
+ - From Mastra Observational Memory study: make observation reflection automatic via the PreCompact and SessionEnd hooks rather than a manual-only skill — because Claude Code exposes no timer/cron hook, but PreCompact fires on context pressure (the analog of Mastra's token threshold) and rides the existing session at no extra API cost.
13
+ - From Mastra Observational Memory study: run the Reflector's deterministic GC shell-side in Ruby and its semantic consolidation via PreCompact additionalContext (Claude-as-reflector inline) — to keep automatic reflection within the no-extra-API-cost convention, explicitly rejecting Claude Code Routines and subagents because each incurs a separate token budget.
14
+ - From Mastra Observational Memory study: tombstone superseded observations via a consolidated_into link rather than hard-deleting them (unlike Mastra's lossy drop) — to preserve claude_memory's provenance guarantee while still bounding context size.
15
+ - From Mastra Observational Memory study: promote an observation to a structured fact only after corroboration across multiple observations — because requiring repeated sightings before commitment doubles as an anti-hallucination gate against reject-churn from one-off doc/example text.
11
16
  - MCP tool-call telemetry stores minimal columns (tool_name, duration_ms, result_count, scope, error_class) — deliberately no query_text or query_hash. YAGNI: hashes are write-only without the raw text, and raw text adds privacy concerns without clear value beyond existing shortcut tools (memory.decisions, memory.conventions). — to avoid storing query hashes that are write-only without the raw text.
12
17
  - From QMD 2026-02-02 restudy: adopt Claude Code plugin format, MCP structured content pattern, MCP query guide prompt, inline status checks. Carry forward sqlite-vec, RRF, docids, smart expansion from 2026-01-26. Reject custom fine-tuned models, LLM reranking, YAML collections. — to align with our pure-Ruby/SQLite constraints (no GGUF, no LLM-side fine-tuning).
13
18
  - From claude-supermemory study: adopt SessionStart hook context injection (hookSpecificOutput.additionalContext), tool-specific observation compression, and relative time formatting. Reject cloud storage dependency and no-test approach. — to avoid cloud-storage lock-in and no-test fragility identified in the study.
@@ -53,6 +53,7 @@
53
53
  "mcp__memory__memory_conflicts"
54
54
  ]
55
55
  },
56
- "enableAllProjectMcpServers": true,
56
+ "enableAllProjectMcpServers": false,
57
+ "disabledMcpjsonServers": ["memory"],
57
58
  "outputStyle": "memory-aware"
58
59
  }
@@ -7,9 +7,9 @@
7
7
  "plugins": [
8
8
  {
9
9
  "name": "claude-memory",
10
- "version": "0.12.1",
10
+ "version": "0.13.0",
11
11
  "source": "./",
12
- "description": "Long-term memory for Claude Code. Recalls architecture, conventions, and decisions across sessions — so Claude explains your codebase without file traversal, follows your patterns, and never re-asks what it already learned.",
12
+ "description": "Long-term memory for Claude Code. Recalls architecture, conventions, and decisions across sessions, plus an episodic observation log of what happened — so Claude explains your codebase without file traversal, follows your patterns, learns from corrections, and never re-asks what it already learned.",
13
13
  "repository": "https://github.com/codenamev/claude_memory"
14
14
  }
15
15
  ]
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "claude-memory",
3
- "version": "0.12.1",
4
- "description": "Long-term memory for Claude Code. Recalls architecture, conventions, and decisions across sessions — so Claude explains your codebase without file traversal, follows your patterns, and never re-asks what it already learned.",
3
+ "version": "0.13.0",
4
+ "description": "Long-term memory for Claude Code. Recalls architecture, conventions, and decisions across sessions, plus an episodic observation log of what happened — so Claude explains your codebase without file traversal, follows your patterns, learns from corrections, and never re-asks what it already learned.",
5
5
  "author": {
6
6
  "name": "Valentino Stoll",
7
7
  "email": "v@codenamev.com"
data/CHANGELOG.md CHANGED
@@ -4,6 +4,34 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.13.0] - 2026-06-18
8
+
9
+ Theme: **Episodic memory — a second kind of memory.** ClaudeMemory gains an append-only *observation* layer ("what happened") that complements the semantic fact store ("what is true"), modeled on [Mastra's Observational Memory](docs/influence/mastra-observational-memory.md). Observations accrue automatically, are deduplicated/consolidated by reflection, and are promoted to facts only after corroboration — making repeated sighting an anti-hallucination gate built into the memory model. Schema advances to v20 (additive; no breaking changes to existing facts/queries).
10
+
11
+ ### Added
12
+
13
+ **Episodic Observation Layer**
14
+
15
+ - **`observations` table** (schema v19–v20) — append-only episodic rows complementing facts. Columns include `body`, `kind`, `priority` (1=🔴/2=🟡/3=🟢), `scope`, `source_content_item_id` (provenance), `consolidated_into` (tombstone lineage — superseded observations are preserved, never deleted), `corroboration_count`, `promoted_at`/`promoted_fact_id`, `status`.
16
+ - **Observer** — Layer-1 `NullDistiller` (regex) plus Layer-2 Claude-as-observer (SessionStart context hook, zero extra API cost) emit observations alongside facts; persisted by the `Resolver` inside the existing extraction transaction.
17
+ - **Reflector** (`Observe::Reflector`) — deterministic dedup + TTL-expiry of info-level observations runs shell-side on `PreCompact`/`SessionEnd`; semantic consolidation (Claude-as-reflector) rides the next turn's `PreCompact` `additionalContext`. No separate API spend.
18
+ - **Two-block SessionStart injection** — Block 1 is the stable observation log (🔴-marked); Block 2 is the undistilled "pending knowledge" tail. Wrapped in `<claude-memory-context>` so the injected log isn't re-ingested as new observations.
19
+ - **Observation→fact promotion bridge** — facts are created from observations only after corroboration (≥2 sightings, `Domain::Observation::PROMOTION_THRESHOLD`); refuses uncorroborated or already-promoted rows.
20
+ - **MCP tools** (28 total): `memory.observations` (read the episodic log), `memory.promote_observation` (corroboration-gated promotion), `memory.consolidate_observations` (semantic merge; corroboration combines, sources tombstoned).
21
+ - **`/reflect` skill** — guided survey → consolidate → promote → report pass over the observation log.
22
+ - **`claude-memory observations`** command — list/inspect the log (counts by status/kind/priority, promotion readiness, compression), plus `promote` and `consolidate` subcommands; `claude-memory stats --observations` summarizes counts.
23
+ - **Dashboard Observations panel** (first-class, main sidebar + Advanced tab) — counts by kind/priority, corroboration + promotion readiness, source→observation compression ratio, recent timeline.
24
+ - **`claude-memory audit` observation health checks** — orphaned observations, promotion consistency, and tombstone-chain validity, documented in `docs/audit_runbook.md`.
25
+
26
+ ### Changed
27
+
28
+ - **Dependencies updated** to current within constraints — `sequel` 5.105, `standard` 1.55, `rubocop` 1.87, `json`, `psych`, `parallel` 2.x, among others. No runtime-gem major bumps required.
29
+
30
+ ### Fixed
31
+
32
+ - **`consolidate_observations` read-modify-write race** — the source read and corroboration sum now run inside the transaction, with `status='active'` re-asserted on the tombstone update, so concurrent `PreCompact`/`SessionEnd` reflectors can't double-count or double-tombstone.
33
+ - **`go` language false positive** — the distiller no longer extracts the English word "go" as the Go language (case-sensitive match + `golang` normalization).
34
+
7
35
  ## [0.12.1] - 2026-06-05
8
36
 
9
37
  Theme: **Upgrade-experience patches surfaced by the 0.12.0 soak.** Four small but high-impact fixes — all uncovered by one user upgrading a single project — closing visibility gaps in the doctor and the plugin manifest. No schema changes, no breaking changes.
data/CLAUDE.md CHANGED
@@ -113,7 +113,7 @@ bin/run-evals --comparative --setup-competitors # Install + run in one step
113
113
 
114
114
  NullDistiller (regex, Layer 1):
115
115
  - Concept Recall: 0.952 (regex-detectable entities/facts)
116
- - Fact Precision: 1.000, Fact Recall: 1.000 (on 31 test cases)
116
+ - Fact Precision: 0.935, Fact Recall: 1.000 (on 31 test cases) — deliberately high-recall ("extract every mention, filter downstream"); see improvements.md #70
117
117
  - Pipeline latency: P95 < 5ms (medium text)
118
118
 
119
119
  Claude Code (LLM, Layers 2+3):
@@ -150,7 +150,7 @@ Transcripts → Ingest → Index (FTS5)
150
150
  The distillation pipeline operates at three levels of depth:
151
151
 
152
152
  - **Layer 1: NullDistiller** (automatic, regex, free) — Runs in the ingest pipeline on every hook event. Extracts entities, facts, and scope hints using pattern matching. P95 latency < 5ms.
153
- - **Layer 2: Context Hook Injection** (automatic, LLM, zero extra cost) — At SessionStart, undistilled content is injected into the session via `hookSpecificOutput.additionalContext` with extraction instructions. Claude Code itself acts as the distiller, extracting structured facts at no additional API cost.
153
+ - **Layer 2: Context Hook Injection** (automatic, LLM, zero extra cost) — At SessionStart, undistilled content is injected into the session via `hookSpecificOutput.additionalContext` with extraction instructions. Claude Code itself acts as the distiller, extracting structured facts at no additional API cost. The same prompt also asks Claude to emit episodic **observations** (the Layer-2 Claude-as-observer) in the `observations` field of its `memory.store_extraction` call — coerced/validated at the handler border and persisted via the resolver alongside facts.
154
154
  - **Layer 3: `/distill-transcripts` Skill** (manual, on-demand) — Deep extraction triggered by the user. Processes undistilled content with depth-aware prompts (initial extraction, consolidation, contradiction resolution).
155
155
 
156
156
  New MCP tools `memory.undistilled` and `memory.mark_distilled` support the pipeline by tracking which content items have been deeply distilled.
@@ -233,7 +233,7 @@ New MCP tools `memory.undistilled` and `memory.mark_distilled` support the pipel
233
233
  - Modes: shared (repo), local (uncommitted), home (user directory)
234
234
 
235
235
  - **`MCP`**: Model Context Protocol server and tools (`mcp/`)
236
- - Exposes memory tools to Claude Code (23 tools total)
236
+ - Exposes memory tools to Claude Code (28 tools total)
237
237
  - `Telemetry`: Records tool invocations to `mcp_tool_calls` table for usage stats
238
238
  - Dual content/structuredContent responses with compact mode
239
239
 
@@ -256,6 +256,7 @@ Key tables (defined in `sqlite_store.rb`):
256
256
  - `mcp_tool_calls`: MCP server tool invocation telemetry (schema v13)
257
257
  - `activity_events`: Hook/recall/context/sweep/nudge telemetry (schema v15) — powers the dashboard timeline, moments feed, efficacy reports. Event types: `hook_ingest`, `hook_context` (carries `context_tokens` since 0.11.0), `hook_sweep`, `hook_publish`, `recall`, `store_extraction`, `roi_nudge` (since 0.11.0).
258
258
  - `moment_feedback`: Per-moment 👍/👎 verdicts with optional notes (schema v16) — unique on event_id, repeat clicks upsert
259
+ - `observations`: Episodic "what happened" layer (schema v19–v20) — append-only narrative rows complementing facts ("what is true"). Columns: `body`, `kind` (decision/preference/event/…), `priority` (1=🔴/2=🟡/3=info), `scope`, `source_content_item_id` (provenance), `consolidated_into` (Reflector tombstone lineage — never hard-deleted), `token_count`, `status`, `corroboration_count` (folded by dedup; the promotion-gate signal), `promoted_at`/`promoted_fact_id` (set when promoted to a fact). Written by the Resolver from `Extraction#observations` (NullDistiller is the Layer-1 Observer). The full observational layer (Observer → injection → deterministic Reflector → promotion bridge) is in `lib/claude_memory/observe/`; see [docs/influence/mastra-observational-memory.md](docs/influence/mastra-observational-memory.md).
259
260
 
260
261
  Facts include:
261
262
  - `scope`: "global" or "project" (determines applicability)
@@ -354,7 +355,7 @@ Also update `SECTION_MAP` if the predicate should appear in a specific snapshot
354
355
 
355
356
  The gem includes an MCP server (`claude-memory serve-mcp`) that exposes memory operations as tools. Configuration should be in `.mcp.json` at project root.
356
357
 
357
- Available MCP tools (23 total):
358
+ Available MCP tools (28 total):
358
359
  - **Query & Recall**: `memory.recall`, `memory.recall_index`, `memory.recall_details`, `memory.recall_semantic`, `memory.search_concepts`
359
360
  - **Provenance**: `memory.explain`, `memory.fact_graph`
360
361
  - **Shortcuts**: `memory.decisions`, `memory.conventions`, `memory.architecture`
@@ -362,6 +363,7 @@ Available MCP tools (23 total):
362
363
  - **Management**: `memory.promote`, `memory.reject_fact`, `memory.store_extraction`
363
364
  - **Distillation**: `memory.undistilled`, `memory.mark_distilled`
364
365
  - **Monitoring**: `memory.status`, `memory.stats`, `memory.changes`, `memory.conflicts`, `memory.activity`
366
+ - **Observational layer** (experimental): `memory.observations` (read-only episodic log), `memory.promote_observation` (corroboration-gated observation→fact promotion), `memory.consolidate_observations` (semantic reflection: merge related observations, combine corroboration)
365
367
  - **Maintenance**: `memory.sweep_now`
366
368
  - **Discovery**: `memory.check_setup`, `memory.list_projects`
367
369
 
@@ -373,13 +375,16 @@ ClaudeMemory integrates with Claude Code via hooks in `.claude/settings.json`:
373
375
  - Calls `claude-memory hook ingest` with stdin JSON
374
376
  - Reads transcript delta and updates both global and project databases
375
377
 
376
- - **Context hook**: Triggers on SessionStart
378
+ - **Context hook**: Triggers on SessionStart (and PreCompact — see below)
377
379
  - Calls `claude-memory hook context`
378
380
  - Injects recent facts via `hookSpecificOutput.additionalContext`
381
+ - Two-block layout (observational layer): Block 1 = the episodic observation log (`Observe::ObservationsRenderer`, 🔴-marked), Block 2 = the undistilled "Pending Knowledge Extraction" tail. `ContextInjector#emitted_observation_count` feeds the `hook_context` telemetry.
382
+ - On **PreCompact** the same `claude-memory hook context` injects only the reflection nudge (`ContextInjector#reflection_context` — the promote/consolidate instructions for corroborated/related observations), not the full snapshot, since PreCompact is context-pressure (Mastra's token-threshold analog). `HooksConfigurator` wires it into the PreCompact hook set alongside ingest + sweep.
379
383
 
380
384
  - **Sweep hook**: Triggers on PreCompact/SessionEnd events
381
385
  - Runs time-bounded maintenance on both databases
382
386
  - Cleans up vec0 entries for superseded/expired facts
387
+ - Runs the deterministic observation Reflector (`Observe::Reflector` via `Maintenance#reflect_observations`): dedupes near-identical observations + expires stale 🟢 info-level ones (TTL `observation_info_ttl_days`). Free/no-LLM, provenance-preserving (tombstone). Context-pressure-triggered — the analog of Mastra's token-threshold reflection.
383
388
 
384
389
  - **Nudge hook** (0.11.0+): Triggers on SessionEnd, fires after ingest+sweep
385
390
  - Calls `claude-memory hook nudge`
@@ -394,7 +399,7 @@ Hook commands read JSON payloads from stdin for robustness. Supports `--async` f
394
399
 
395
400
  Local web UI for inspecting memory state. Started via `claude-memory dashboard` (default port 3377). Reads from both global and project databases; no write side effects from page loads.
396
401
 
397
- The dashboard is a thin web layer over the same `Recall`/`Conflicts`/`Trust`/`Moments`/`Knowledge`/`Reuse`/`Health`/`Timeline` classes the MCP server uses. Each panel is backed by a dedicated module under `lib/claude_memory/dashboard/`; `Dashboard::API` holds HTTP-shape glue and per-endpoint formatting (delegating non-trivial logic to the panel classes).
402
+ The dashboard is a thin web layer over the same `Recall`/`Conflicts`/`Trust`/`Moments`/`Knowledge`/`Reuse`/`Health`/`Timeline`/`Observations` classes the MCP server uses. Each panel is backed by a dedicated module under `lib/claude_memory/dashboard/`; `Dashboard::API` holds HTTP-shape glue and per-endpoint formatting (delegating non-trivial logic to the panel classes). The `Observations` panel (`/api/observations`, Advanced → Observations tab) surfaces the episodic layer: counts by status/kind/priority, corroboration + promotion readiness, a compression ratio (source content tokens ÷ observation tokens), and a recent timeline.
398
403
 
399
404
  Connections are released after each request — never holds a WAL writer lock open across page loads.
400
405
 
data/README.md CHANGED
@@ -12,9 +12,13 @@ It automatically:
12
12
  - ✅ Remembers project-specific and global knowledge
13
13
  - ✅ Provides instant recall without manual prompting
14
14
  - ✅ Maintains truth (handles conflicts, supersession)
15
+ - ✅ Tracks *what happened*, not just *what's true* — an episodic observation log alongside the facts (0.13.0+)
16
+ - ✅ Promotes an observation to a fact only after it recurs — a corroboration gate against one-off noise
15
17
 
16
18
  **No API keys. No configuration. Just works.**
17
19
 
20
+ ClaudeMemory now has **two complementary halves**: a *semantic* fact store ("what is true" — your stack, conventions, decisions) and an *episodic* observation layer ("what happened" — the narrative of your sessions). Observations are deduplicated and consolidated automatically, and only graduate to facts once corroborated — so fleeting mentions never harden into false memory. See [Episodic Memory](#episodic-memory-observations).
21
+
18
22
  ## Quick Start
19
23
 
20
24
  ### 1. Install the Gem
@@ -137,6 +141,7 @@ File-searchable questions ("what version is this?") and one-shot code generation
137
141
  - **Progressive Disclosure**: Lightweight queries before full details
138
142
  - **Semantic Shortcuts**: Quick access to decisions, conventions, architecture
139
143
  - **Truth Maintenance**: Automatic conflict resolution
144
+ - **Episodic Memory** (0.13.0+): An append-only observation log of *what happened* alongside the semantic fact store. Auto-consolidated via deterministic + LLM reflection on `PreCompact`/`SessionEnd`; corroborated observations are promoted to facts (anti-hallucination gate). See **[Episodic Memory →](#episodic-memory-observations)**.
140
145
  - **Claude-Powered**: Uses Claude's intelligence to extract facts (no API key needed)
141
146
  - **Token Efficient**: 10x reduction in memory queries with progressive disclosure
142
147
  - **Database Maintenance**: Compact, export, and backup commands
@@ -152,6 +157,36 @@ File-searchable questions ("what version is this?") and one-shot code generation
152
157
 
153
158
  Only metrics and event names are captured by default — verbatim prompts and bodies stay off until you explicitly opt in via `claude-memory otel --capture-prompts`. The receiver binds to `127.0.0.1` only.
154
159
 
160
+ ## Episodic Memory (Observations)
161
+
162
+ Facts answer **"what is true"** (your stack, conventions, decisions). Observations answer **"what happened"** — a narrative log of the moments in your sessions. ClaudeMemory now keeps both, modeled on [Mastra's Observational Memory](docs/influence/mastra-observational-memory.md).
163
+
164
+ | | Facts (semantic) | Observations (episodic) |
165
+ |---|---|---|
166
+ | Capture | Durable truths — `uses_database: sqlite` | Narrative events — "decided to add a corroboration gate to avoid reject-churn" |
167
+ | Change | Explicitly, via supersession/rejection | Automatically — deduped, consolidated, low-priority ones expire |
168
+ | Promotion | — | Promoted to a fact only after corroboration (≥2 sightings) |
169
+
170
+ **Why it's a leap forward:** the distiller used to commit a fact the first time it saw a claim — so a database mentioned once in a comparison could harden into a false `uses_database`. The observation layer makes repeated sighting the gate: an observation becomes a fact only after it recurs. That's an **anti-hallucination defense built into the memory model**, not a cleanup afterthought.
171
+
172
+ **How it runs (no extra API cost):**
173
+ - **Observer** — a regex Layer-1 pass plus Claude-as-observer in the SessionStart context hook emit observations as sessions happen.
174
+ - **Reflector** — deterministic dedup + TTL-expiry runs on `PreCompact`/`SessionEnd`; semantic consolidation rides the next turn's context hook (Claude-as-reflector). Superseded observations are *tombstoned*, never deleted, preserving provenance.
175
+ - **Promotion bridge** — corroborated observations graduate to facts on the corroboration gate.
176
+
177
+ **See it / use it:** the dashboard's **Observations** panel (counts by kind/priority, corroboration + promotion readiness, source→observation compression ratio, recent timeline); the `claude-memory observations` CLI; the `memory.observations` / `memory.promote_observation` / `memory.consolidate_observations` MCP tools; and the `/reflect` skill for a guided survey→consolidate→promote pass.
178
+
179
+ ## What's New in 0.13.0
180
+
181
+ **Episodic Observation Layer** — ClaudeMemory gains a second kind of memory (see [Episodic Memory](#episodic-memory-observations) above):
182
+
183
+ - New `observations` table (schema v19–v20), append-only with `consolidated_into` tombstone lineage and `corroboration_count` / `promoted_at` / `promoted_fact_id` promotion tracking.
184
+ - Two-block SessionStart injection: a stable observation log (🔴-marked) + the undistilled "pending knowledge" tail.
185
+ - Automatic reflection on `PreCompact` (context-pressure, Mastra's token-threshold analog) and `SessionEnd` — deterministic GC shell-side in Ruby, semantic consolidation via the context hook (no extra API spend).
186
+ - Corroboration-gated observation→fact promotion — repeated sightings required before commitment, an anti-hallucination gate against reject-churn from one-off doc/example text.
187
+ - New surfaces: dashboard **Observations** panel, `claude-memory observations` command (+ `claude-memory stats --observations`), `claude-memory audit` observation health checks, three `memory.*observation*` MCP tools, and the `/reflect` skill.
188
+ - Dependencies refreshed to current (sequel, standard, rubocop, and others).
189
+
155
190
  ## What's New in 0.11.0
156
191
 
157
192
  Five user-visible signals so you can answer "is memory still worth it?" with
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Migration v19: Add observations table — the episodic memory layer.
4
+ #
5
+ # Facts answer "what is true" (semantic memory); observations answer "what
6
+ # happened" (episodic memory). This is the storage half of Phase 1 of the
7
+ # observational layer (see docs/influence/mastra-observational-memory.md).
8
+ #
9
+ # Observations are append-only: the Reflector consolidates by writing a new
10
+ # observation and pointing superseded ones at it via consolidated_into,
11
+ # rather than hard-deleting — preserving provenance (unlike Mastra's lossy
12
+ # drop). source_content_item_id links each observation back to the raw
13
+ # transcript chunk it was distilled from.
14
+ Sequel.migration do
15
+ up do
16
+ create_table?(:observations) do
17
+ primary_key :id
18
+ String :body, text: true, null: false # dense narrative text — the observation itself
19
+ String :kind, null: false, default: "event" # user_statement | agent_action | tool_result | preference | decision | event
20
+ Integer :priority, null: false, default: 3 # 1=important (🔴), 2=maybe (🟡), 3=info (🟢)
21
+ String :scope, null: false, default: "project" # "project" or "global"
22
+ String :project_path # set for project-scoped observations
23
+ Integer :source_content_item_id # provenance: raw transcript chunk
24
+ Integer :consolidated_into # Reflector lineage: id of the observation this was merged into
25
+ Integer :token_count # for budget / compression math (Phase 2)
26
+ String :status, null: false, default: "active" # "active" or "consolidated"
27
+ String :session_id # session that produced the observation
28
+ String :observed_at, null: false # ISO 8601 event time
29
+ String :created_at, null: false # ISO 8601 row creation time
30
+ String :reflected_at # ISO 8601 — set when the Reflector last touched it
31
+ end
32
+
33
+ run "CREATE INDEX IF NOT EXISTS idx_observations_status ON observations(status)"
34
+ run "CREATE INDEX IF NOT EXISTS idx_observations_scope ON observations(scope)"
35
+ run "CREATE INDEX IF NOT EXISTS idx_observations_observed_at ON observations(observed_at)"
36
+ run "CREATE INDEX IF NOT EXISTS idx_observations_source ON observations(source_content_item_id)"
37
+ run "CREATE INDEX IF NOT EXISTS idx_observations_consolidated_into ON observations(consolidated_into)"
38
+ end
39
+
40
+ down do
41
+ drop_table?(:observations)
42
+ end
43
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Migration v20: observation→fact promotion bridge (Phase 4 of the
4
+ # observational layer).
5
+ #
6
+ # - corroboration_count: how many times this observation has been sighted.
7
+ # Starts at 1; the deterministic Reflector's dedup pass folds duplicates'
8
+ # counts into the keeper instead of just dropping them. This count is the
9
+ # "repeated sightings" signal the promotion gate requires — an observation
10
+ # is only eligible to become a structured fact once corroborated, which
11
+ # doubles as an anti-hallucination gate against one-off doc/example text.
12
+ # - promoted_at / promoted_fact_id: set when an observation has been promoted
13
+ # to a fact, so it is not re-suggested. The observation row is preserved
14
+ # (provenance), it just stops appearing as a promotion candidate.
15
+ Sequel.migration do
16
+ up do
17
+ alter_table(:observations) do
18
+ add_column :corroboration_count, Integer, null: false, default: 1
19
+ add_column :promoted_at, String # ISO 8601, set on promotion
20
+ add_column :promoted_fact_id, Integer # the fact this was promoted into
21
+ end
22
+
23
+ run "CREATE INDEX IF NOT EXISTS idx_observations_promoted_at ON observations(promoted_at)"
24
+ end
25
+
26
+ down do
27
+ alter_table(:observations) do
28
+ drop_column :corroboration_count
29
+ drop_column :promoted_at
30
+ drop_column :promoted_fact_id
31
+ end
32
+ end
33
+ end
@@ -119,6 +119,44 @@ claude-memory promote <fact_id>
119
119
  "Remember that I prefer descriptive commit messages - make that a global preference"
120
120
  ```
121
121
 
122
+ ## Two Kinds of Memory: Facts and Observations
123
+
124
+ ClaudeMemory remembers two complementary things:
125
+
126
+ - **Facts** answer *"what is true"* — durable, structured truths about your
127
+ project (`uses_database: sqlite`, conventions, decisions). This is the
128
+ semantic layer the sections above describe.
129
+ - **Observations** answer *"what happened"* — an append-only narrative log of
130
+ the moments in your sessions ("decided to add a corroboration gate so
131
+ fleeting mentions don't harden into facts"). This is the **episodic** layer
132
+ (0.13.0+).
133
+
134
+ | | Facts | Observations |
135
+ |---|---|---|
136
+ | Captures | Durable truths | Events / narrative |
137
+ | Changes | Explicitly (supersession, rejection) | Automatically (dedup, consolidation, expiry) |
138
+ | Promotion | — | Promoted to a fact after corroboration (≥2 sightings) |
139
+
140
+ **Why this matters:** the distiller used to commit a fact the first time it
141
+ saw a claim — so a database named once in a comparison could become a false
142
+ `uses_database`. Observations make repeated sighting the gate: an observation
143
+ graduates to a fact only after it recurs. That's an anti-hallucination defense
144
+ built into the memory model.
145
+
146
+ Observations are managed for you — deduplicated and consolidated automatically
147
+ on `PreCompact`/`SessionEnd` at no extra API cost. To see or curate them:
148
+
149
+ ```bash
150
+ # Inspect the episodic log (counts, promotion readiness, compression, recent)
151
+ claude-memory observations
152
+
153
+ # Promote a corroborated observation to a fact
154
+ claude-memory observations promote <id> --predicate uses_database --object sqlite
155
+ ```
156
+
157
+ The dashboard's **Observations** panel shows the same at a glance, and the
158
+ `/reflect` skill runs a guided survey → consolidate → promote pass.
159
+
122
160
  ## Setting Up Your First Project
123
161
 
124
162
  ### Scenario 1: Fresh Install (New Project)
@@ -114,7 +114,7 @@ Renaming or repurposing a code is a major-version change.
114
114
 
115
115
  ## 3. Public MCP tool surface
116
116
 
117
- All 23 tools registered via `MCP::ToolDefinitions.all`. Argument schemas, return shapes (both `content` and `structuredContent`), and tool-annotation hints (`readOnlyHint`, `idempotentHint`, `destructiveHint`) are **stable** for the listed tools.
117
+ All 28 tools registered via `MCP::ToolDefinitions.all` — 25 stable + 3 experimental (the observational-layer tools below). Argument schemas, return shapes (both `content` and `structuredContent`), and tool-annotation hints (`readOnlyHint`, `idempotentHint`, `destructiveHint`) are **stable** for the listed stable tools.
118
118
 
119
119
  ### Stable MCP tools
120
120
 
@@ -134,7 +134,7 @@ All 23 tools registered via `MCP::ToolDefinitions.all`. Argument schemas, return
134
134
  | `memory.facts_by_context` | Context | Stable. |
135
135
  | `memory.promote` | Management | Stable. |
136
136
  | `memory.reject_fact` | Management | Stable since 0.10.0. |
137
- | `memory.store_extraction` | Management | Argument schema (`facts`, `entities`, `decisions`) stable. |
137
+ | `memory.store_extraction` | Management | Argument schema (`facts`, `entities`, `decisions`) stable. The `observations` field (Layer-2 observer) is **experimental** while the observational layer is built out. |
138
138
  | `memory.undistilled` | Distillation | Stable since 0.10.0. |
139
139
  | `memory.mark_distilled` | Distillation | Stable since 0.10.0. |
140
140
  | `memory.status` | Monitoring | Stable. |
@@ -146,6 +146,16 @@ All 23 tools registered via `MCP::ToolDefinitions.all`. Argument schemas, return
146
146
  | `memory.check_setup` | Discovery | Stable. |
147
147
  | `memory.list_projects` | Discovery | Stable since 0.10.0. |
148
148
 
149
+ ### Experimental MCP tools
150
+
151
+ These are registered in `MCP::ToolDefinitions.all` but **not yet covered by the stability guarantees above** — argument schema and return shape may change while the feature is built out.
152
+
153
+ | Tool | Group | Status |
154
+ |---|---|---|
155
+ | `memory.observations` | Observational layer | Experimental (0.13.0+). Read-only listing of episodic observations by status/kind/priority with corroboration counts. Return shape may change. |
156
+ | `memory.promote_observation` | Observational layer | Experimental (0.13.0+). Promotes a corroborated observation (≥2 sightings) into a fact; refuses uncorroborated or already-promoted ones (anti-hallucination gate). Args/shape may change. |
157
+ | `memory.consolidate_observations` | Observational layer | Experimental (0.13.0+). Merges related observations into one synthesized row (corroboration combines, sources tombstoned via `consolidated_into`). Args/shape may change. |
158
+
149
159
  ### Stability of tool responses
150
160
 
151
161
  Both response shapes are stable:
@@ -201,7 +211,7 @@ Adding a new field to `detail_json` is a stable-surface addition (non-breaking).
201
211
 
202
212
  Current covered events (0.11.0):
203
213
 
204
- - `hook_context`: `context_length`, `context_tokens` (since 0.11.0), `top_fact_ids`, `fact_count`.
214
+ - `hook_context`: `context_length`, `context_tokens` (since 0.11.0), `top_fact_ids`, `fact_count`. (`observation_count`, added with the observational layer, is additive and **experimental** — not yet on the smoke-gate manifest.)
205
215
  - `roi_nudge`: `n`, `used`, `pct`, `prior_count` (all since 0.11.0).
206
216
 
207
217
  `hook_ingest`, `hook_sweep`, `hook_publish` event detail fields are currently **internal** (not on the smoke-gate manifest). Promoting them to stable is a 0.12.x or later task.
@@ -255,7 +265,7 @@ If you need a feature from one of the internal classes, **open an issue** so we
255
265
 
256
266
  ### Schema migrations
257
267
 
258
- Schema is at v18 as of 0.12.0 with 18 migrations under `db/migrations/`. Migrations remain forward-compatible per the round-trip-spec convention (`feedback_round_trip_migration_specs.md`): each release's specs verify that DBs from the prior 3 schema boundaries can be migrated into the current schema without data loss.
268
+ Schema is at v20 (v18 shipped in 0.12.0; v19–v20 add the observational `observations` table in 0.13.0) with 20 migrations under `db/migrations/`. Migrations remain forward-compatible per the round-trip-spec convention (`feedback_round_trip_migration_specs.md`): each release's specs verify that DBs from the prior 3 schema boundaries can be migrated into the current schema without data loss.
259
269
 
260
270
  **What's stable:**
261
271
 
@@ -268,6 +278,7 @@ Schema is at v18 as of 0.12.0 with 18 migrations under `db/migrations/`. Migrati
268
278
 
269
279
  - The `vec0` virtual-table internals — sqlite-vec evolution may shift representation.
270
280
  - `mcp_tool_calls` retention behavior (currently 90 days, configurable); the column set is stable, the retention default is not.
281
+ - The `observations` table (v19–v20, incl. `corroboration_count`/`promoted_at`/`promoted_fact_id`) — episodic layer. Column set may still change while the layer is experimental.
271
282
 
272
283
  **What's internal:**
273
284
 
@@ -324,7 +335,7 @@ Listed here for honesty — these surfaces look public but are not.
324
335
 
325
336
  - **Dashboard JSON HTTP API.** The `claude-memory dashboard` server's endpoints are an internal interface for the bundled UI. Don't build scripts against `GET /api/trust` etc. — endpoints, response shapes, and even URL paths may change without notice.
326
337
  - **`activity_events.detail_json` fields not in `spec/smoke/expected_fields.yml`.** Inspecting a missing field during debugging is fine; relying on it in scripts is not.
327
- - **The exact text of `additionalContext`.** The Markdown sections (`## Decisions`, `## Conventions`, `## Architecture`, `## Pending Knowledge Extraction`, `## Auto-Memory Mirror`) and their order are stable; the per-fact rendering format inside each section is tuned for prompt quality and may change.
338
+ - **The exact text of `additionalContext`.** The Markdown sections (`## Decisions`, `## Conventions`, `## Architecture`, `## Pending Knowledge Extraction`, `## Auto-Memory Mirror`) and their order are stable; the per-fact rendering format inside each section is tuned for prompt quality and may change. The `## Observations` and `## Observation Reflection` sections (observational layer) and the published `.claude/rules/claude_memory.observations.md` snapshot are **experimental** while the layer is built out.
328
339
  - **Internal env vars** (anything not listed in `Configuration` instance methods or in this doc). Examples that exist but are internal: `CLAUDE_MEMORY_LOG_LEVEL`, debug flags surfaced during development.
329
340
  - **Test/spec/fixture infrastructure.** `spec/benchmarks/`, `spec/evals/`, `spec/support/` are not public APIs.
330
341
  - **Plugin-format paths.** `.claude-plugin/`, `scripts/serve-mcp.sh`, etc. are part of the Claude Code plugin format integration; treat them as opaque.
data/docs/architecture.md CHANGED
@@ -14,20 +14,22 @@ ClaudeMemory is architected using Domain-Driven Design (DDD) principles with cle
14
14
 
15
15
  ┌──────────────────────▼──────────────────────────────────────┐
16
16
  │ Core Domain Layer │
17
- │ Domain Models: Fact, Entity, Provenance, Conflict
17
+ │ Domain Models: Fact, Entity, Provenance, Conflict,
18
+ │ Observation (episodic) │
18
19
  │ Value Objects: SessionId, TranscriptPath, FactId │
19
20
  │ Null Objects: NullFact, NullExplanation │
20
21
  └──────────────────────┬──────────────────────────────────────┘
21
22
 
22
23
  ┌──────────────────────▼──────────────────────────────────────┐
23
24
  │ Business Logic Layer │
24
- │ Recall → Resolve → Distill → Ingest → Publish
25
- SweepEmbeddingsMCPHook
25
+ │ Recall → Resolve (semantic) → Distill → Ingest → Publish
26
+ ObserveReflect (episodic) SweepEmbeddings →
27
+ │ MCP → Hook │
26
28
  └──────────────────────┬──────────────────────────────────────┘
27
29
 
28
30
  ┌──────────────────────▼──────────────────────────────────────┐
29
31
  │ Infrastructure Layer │
30
- │ Store (SQLite v17 + WAL) → FileSystem → Index (FTS5+Vector)│
32
+ │ Store (SQLite v20 + WAL) → FileSystem → Index (FTS5+Vector)│
31
33
  │ Templates │
32
34
  └─────────────────────────────────────────────────────────────┘
33
35
  ```
@@ -129,10 +131,19 @@ end
129
131
  - `DualQueryTemplate`: Query template handling for dual-database queries
130
132
 
131
133
  #### Resolve (`resolve/`)
132
- - Truth maintenance and conflict resolution
134
+ - Truth maintenance and conflict resolution (the **semantic** "what is true" layer)
133
135
  - **Transaction safety**: Multi-step operations wrapped in DB transactions
134
136
  - PredicatePolicy: Controls single vs. multi-value predicates
135
137
  - Handles supersession and conflict detection
138
+ - Also persists observations from the extraction inside the same transaction (see Observe & Reflect)
139
+
140
+ #### Observe & Reflect (`observe/`)
141
+ - The **episodic** "what happened" layer, complementing the semantic fact store
142
+ - **Observer**: `NullDistiller` emits high-precision Layer-1 observations (regex); Claude-as-observer enriches them via the SessionStart context hook (Layer-2, no extra API cost)
143
+ - **Reflector** (`Observe::Reflector`): deterministic dedup + TTL-expiry of info-level observations runs on `PreCompact`/`SessionEnd`; semantic consolidation (Claude-as-reflector) rides the next turn's context hook
144
+ - **Append-only/tombstoning**: superseded observations are linked via `consolidated_into`, never deleted — preserving provenance
145
+ - **Promotion bridge**: observations are promoted to facts only after corroboration (≥2 sightings) — an anti-hallucination gate
146
+ - `ObservationsRenderer` formats the injected log; `Domain::Observation` is the immutable value object
136
147
 
137
148
  #### Distill (`distill/`)
138
149
  - Extracts facts and entities from transcripts
@@ -164,7 +175,7 @@ end
164
175
 
165
176
  #### MCP (`mcp/`)
166
177
  - Model Context Protocol server
167
- - Exposes 19 tools including: recall, explain, promote, status, decisions, conventions, architecture, semantic search, check_setup, and more
178
+ - Exposes 28 tools including: recall, explain, promote, status, decisions, conventions, architecture, semantic search, check_setup, the observational-layer tools (observations, promote_observation, consolidate_observations), and more
168
179
  - `ResponseFormatter`: Consistent MCP response formatting
169
180
  - `SetupStatusAnalyzer`: Initialization and version status analysis
170
181
 
@@ -212,6 +223,7 @@ end
212
223
  - `Reuse`: most-used facts within window
213
224
  - `Health`: db / hooks / vec checks with actionable fix strings
214
225
  - `Timeline`: 30-day daily rollup
226
+ - `Observations` (0.13.0+): episodic-layer panel — counts by status/kind/priority, corroboration + promotion readiness, source→observation compression ratio, recent timeline (first-class main-sidebar panel + Advanced tab)
215
227
  - `FactPresenter`, `ScopedFactResolver`: shared rendering / scope-aware ID resolution
216
228
  - Connections released after every request — no held WAL writer locks across page loads
217
229
  - See [docs/dashboard.md](dashboard.md) for the user-facing guide
@@ -178,6 +178,73 @@ Exit code is `0` when `ok: true`, `1` otherwise. `--no-exit` always returns `0`.
178
178
  3. Clean up: `claude-memory reject` the historical disputed/superseded rows (or accept them as historical record).
179
179
  4. Re-audit.
180
180
 
181
+ ### C011 — Orphaned observations
182
+
183
+ **Severity:** warn
184
+
185
+ **Scope:** both the project and global DBs (observations may be scoped either way).
186
+
187
+ **Triggered when:** an observation has `source_content_item_id` set but no `content_items` row with that id exists.
188
+
189
+ **Why it matters:** An observation's `source_content_item_id` is its provenance link back to the transcript chunk it was distilled from. A dangling pointer means the source row was pruned (or never existed), so the observation can no longer be explained — breaking the same provenance guarantee facts enjoy. Observations with a `nil` source (e.g. consolidated ones synthesized from several sources) are *not* flagged.
190
+
191
+ **Remediation:**
192
+ - Inspect with `memory.observations` (or the dashboard Observations tab).
193
+ - The table is append-only — do **not** delete. If the provenance is genuinely unrecoverable, let the Reflector consolidate or expire the row on the next PreCompact/SessionEnd pass.
194
+
195
+ ### C012 — Observation promotion consistency
196
+
197
+ **Severity:** error
198
+
199
+ **Scope:** both DBs.
200
+
201
+ **Triggered when:** any of the following promotion-state invariants is violated —
202
+ - `promoted_at` is set but `promoted_fact_id` is `NULL`;
203
+ - `promoted_fact_id` points at a fact that does not exist;
204
+ - `promoted_fact_id` points at a fact that is **not** active (rejected/superseded);
205
+ - `promoted_fact_id` is set but `promoted_at` is `NULL`.
206
+
207
+ **Why it matters:** Promotion is meant to be atomic — `mark_observation_promoted` sets both `promoted_at` and `promoted_fact_id` pointing at a freshly-created, active fact. Half-set state means the write ran partially, or the target fact was later rejected/superseded, leaving the observation pointing at nothing usable. The promotion bridge keys off these columns, so an inconsistent row either re-promotes (duplicate facts) or is silently stuck.
208
+
209
+ **Remediation:**
210
+ 1. `claude-memory explain <fact_id>` on the `promoted_fact_id` to see why the fact is missing/inactive.
211
+ 2. If the fact was intentionally rejected, re-open the observation for re-promotion via `memory.promote_observation`.
212
+ 3. If `mark_observation_promoted` half-ran, re-run promotion so both columns are set together.
213
+
214
+ ### C013 — Observation tombstone-chain validity
215
+
216
+ **Severity:** error
217
+
218
+ **Scope:** both DBs.
219
+
220
+ **Triggered when:** any of the following tombstone invariants is violated —
221
+ - `consolidated_into` points at a non-existent observation;
222
+ - `consolidated_into` is a self-link (`consolidated_into == id`);
223
+ - a row is `status='active'` yet carries a `consolidated_into` target;
224
+ - a row is `status='consolidated'` yet has no `consolidated_into` keeper.
225
+
226
+ **Why it matters:** Supersession is append-only: a merged-away observation gets `status='consolidated'` and `consolidated_into` pointing at the surviving keeper, preserving lineage instead of hard-deleting (unlike Mastra's lossy drop). A broken chain corrupts that lineage — recall could surface a tombstoned row, or a consolidated row could orphan its history. A self-link or active-but-tombstoned row is a Reflector bug, not user error.
227
+
228
+ **Remediation:**
229
+ - Inspect with `memory.observations`.
230
+ - Re-running the deterministic Reflector (fires on PreCompact/SessionEnd) re-derives consolidation for dangling links.
231
+ - A self-link or `active` + `consolidated_into` row signals a Reflector defect — file it rather than hand-editing the append-only table.
232
+
233
+ ### C014 — Observation status / corroboration sanity
234
+
235
+ **Severity:** warn
236
+
237
+ **Scope:** both DBs.
238
+
239
+ **Triggered when:** an observation has a `status` outside `active`/`consolidated`/`expired`, or a `corroboration_count` less than 1.
240
+
241
+ **Why it matters:** Every observation should carry a known lifecycle status and at least one sighting (a fresh insert counts as 1; the migration default is 1). An unknown status means a migration or an external writer bypassed `insert_observation`; a `corroboration_count < 1` means `increment_corroboration` math went negative. Both break downstream behavior — recall filters key off `status`, and the promotion gate keys off `corroboration_count`.
242
+
243
+ **Remediation:**
244
+ - Inspect with `memory.observations`.
245
+ - For a bad `corroboration_count`, re-derive sighting counts via the Reflector's dedup pass.
246
+ - For an unknown status, find the writer that bypassed `insert_observation` (the only sanctioned insert path).
247
+
181
248
  ## Adding a new check
182
249
 
183
250
  The audit is extensible by design.