Status: Required by META v2.0 R10 (Reversibility-Weighted Verification) before implementation begins. This document imagines the memory layer already exists, has shipped, and has failed — then works backward to the specific failure mode, the cause, and the design mitigation that would have prevented it. Mitigations marked [gating] must hold before Phase 2.1 ships; [ratchet] mitigations are improvements we'll layer in later.
Unlike the rest of ai-consensus-mcp, which is stateless, the memory
layer:
- Holds long-lived data on disk. Corruption, unintended retention, and migration breakage are durable failure modes.
- Touches user privacy. A
ConsensusResultcontains the question the user asked plus the panel's reasoning. Questions can contain source code, credentials, PII, business secrets. Storing them unencrypted on disk is a deliberate trade — surfaced explicitly here. - Cross-cuts the protocol. Recall surfaces past results into new consensus runs. A bad recall poisons the next decision.
- Crosses project boundaries. A naïve implementation will mix
results from
/repos/billingwith/repos/cmsbecause the working directory changes between sessions.
Reversibility cost: recovering from a corrupt or poisoned memory store requires either manual JSON repair or a full wipe. Wiping forfeits months of accumulated context.
Imagined outcome: Six months in, consensus_recall returns results
from the wrong panel for the wrong question. The user has been
quietly making decisions informed by mismatched context.
Root cause hypothesis: The index (index.jsonl) and result files
(results/<id>.json) drifted because the index was updated before
the result file was fully written, then the process was killed mid-
operation. Subsequent recalls hit the index but find missing or
mismatched files.
Mitigation [gating]:
- Write the result file to a
<id>.tmpsibling, thenfs.rename(2)into place atomically. - Append to the index only after the result file rename returns.
- On every recall, validate that each indexed id has a corresponding readable file. If not, skip the entry and log a one-line warning; never crash.
- Provide a
consensus_memory_repairadmin function (not an MCP tool; CLI only) that rebuilds the index by scanningresults/.
Imagined outcome: A user pastes a private key into a debate
("here's the token we use, why is it leaking"). The full key is
durably stored on disk; later it shows up in a consensus_recall
answer surfaced to a colleague who pair-codes via Claude Desktop with
a different account.
Root cause hypothesis: No secret-scanning on intake; no scoping of recall results to the storing identity.
Mitigation [gating]:
- Memory is off by default. Opt-in via explicit config flag
memory.enabled: trueand a per-callconsensus_store: trueflag on the consensus tool. No silent retention. - Project-scope by default. Recall is filtered to the current
project (resolved from
cwd). Cross-project recall is a separate, explicit operation. - Storage path is user-owned. Default
~/.consensus/memory/<sha256(projectAbsPath)[0:12]>/— under the user's home dir, file permissions0o600, no network sync. - Document the threat model explicitly.
docs/memory-layer.mdnames what is durably stored and recommends the user.gitignorethe storage dir if their home is in a synced location (Dropbox, iCloud Drive). No surprises.
Mitigation [ratchet]:
- Add an
--exclude-patternsconfig that runs cheap regex-based scrubbing on thequestionbefore storing (well-known token prefixes:sk-,xoxb-,ghp_, AWS access key shape, etc.). Imperfect; explicitly not a security boundary.
Imagined outcome: After a year of use, consensus_recall "what did we decide about auth" takes 4 seconds. The user disables memory.
Root cause hypothesis: Naïve full-scan of result JSON files for every recall, no index over question text, no cap on stored results.
Mitigation [gating]:
- Two-tier index.
index.jsonl(one line per result, ~200 bytes) carries summary fields — id, storedAt, panelId, question, tags. Recall scans the index, not the full results, for the first-pass filter. - Cap by default.
memory.retention.maxResults: 1000andmemory.retention.maxAgeDays: 365. Older entries pruned on the next store. User-configurable; the CLI tool surfaces what's about to be pruned before doing it. - Lazy result loading. Only load full
ConsensusResultJSON for entries the first-pass filter returned.
Mitigation [ratchet]:
- Optional embedding index (Phase 2.2+) — when the user has an embedding provider configured, store a vector per question and use cosine similarity in the first pass. Off by default; needs explicit opt-in to avoid the dependency.
Imagined outcome: Two users on the same machine working in
~/work/project-a and ~/work/project-a (different home dirs but
same suffix) get each other's results.
Root cause hypothesis: Project key was derived from basename(cwd),
not the absolute canonical path.
Mitigation [gating]:
- Project key =
sha256(realpath(cwd))[0:12]. The full absolute resolved path goes into the key derivation. Users with different home dirs cannot collide. - Store the canonical project path in each result's metadata, not just the key. Recall validates the stored path equals the current realpath before returning — defense in depth.
- Symlinks resolved at store-time, not at recall-time, so the key is stable across symlink topology changes.
Imagined outcome: v0.13 adds a new field to RoundResult. Stored
results from v0.12 fail to deserialize. The user's accumulated
history is silently lost.
Root cause hypothesis: No schema version on stored entries; no forward-compat reader.
Mitigation [gating]:
- Versioned storage envelope. Every stored entry wraps the
ConsensusResultin aStoredEntryshape withschemaVersion: 1. - Reader tolerates unknown fields. Use zod's
.passthrough()instead of.strict()on stored shapes — adding fields is non-breaking. - Migrations are explicit. A
migrateStoredEntry(entry, fromVersion)function exists and is exercised in tests. Refusing to migrate is a user-visible warning, not a silent drop.
Imagined outcome: User runs Claude Code in two windows on the
same project; both servers append to index.jsonl simultaneously;
one line ends up half-written.
Root cause hypothesis: Append-only file with no locking; assumed single-writer.
Mitigation [gating]:
- Advisory lock on writes. Acquire an exclusive
flockonindex.jsonl.lockfor the brief window of the append. Use a POSIX file lock (proper-lockfileor stdlib equivalent — needs research; if it adds dependency weight, prefer stdlibopen()withO_EXLOCKwhere available, otherwise a sentinel file). - Each append writes a complete line atomically. The kernel guarantees writes ≤ PIPE_BUF (typically 4 KiB on Linux, 512 on POSIX minimum) are atomic. Index lines are designed to be small enough that the atomicity guarantee holds even without the lock — but we still take the lock for the corner-cases.
Mitigation [ratchet]:
- Self-healing reader. When parsing
index.jsonl, a malformed line is logged and skipped rather than aborting the recall.
Imagined outcome: The user changed their architecture decision
in March. In May, consensus_recall surfaces the March decision
into a new debate, and the panel anchors on it. Stale guidance
quietly propagates.
Root cause hypothesis: Recall returns raw content without freshness signal; new debate's prompt template doesn't communicate that the recalled material is historical context, not current truth.
Mitigation [gating]:
- Recall returns metadata, not raw context. The MCP tool
surfaces a structured list —
{ id, storedAt, panelId, question, summary, finalScore }— and the caller (the model in the MCP host) decides whether to use it. Recall does not inject into prompts automatically. - Every recalled item is tagged with age. "Stored 76 days ago," not just a timestamp — surfaces relative recency at a glance.
consensus_what_we_decidedreturns the decision + the date of the decision + the conditions named as tripwires (which v2 panels emit). A caller using this tool sees both the conclusion and the conditions that would invalidate it.
F8. The memory layer becomes a hidden coupling that breaks tests
Imagined outcome: Test suites that exercise the consensus engine
start reading from ~/.consensus/memory/ and pulling in real prior
runs. CI is non-deterministic.
Root cause hypothesis: Memory module reads from a globally- configured path with no override hook for tests.
Mitigation [gating]:
- Memory module takes the storage root as a constructor argument.
No reads from
$HOMEinside the module — the CLI / server is responsible for resolving the path and passing it in. - Tests get a per-test
tmpdir-based root. Vitest helper:withTempMemoryRoot(callback). - No global singletons. Each
createMemoryStore(args)call produces an independent store; calling it in tests with/tmp/Xnever touches the user's real memory.
Imagined outcome: A keyword-based recall matches "auth" in "author" and surfaces an unrelated debate about authoring tooling as if it were an authentication decision.
Root cause hypothesis: Naïve substring or stemming with no explainability — recall returns matches without showing why.
Mitigation [gating]:
- Recall shows the matched fragment. The result envelope includes the snippet of the stored question/answer that matched, so the caller can sanity-check the relevance.
- Score every match. Even with a simple keyword index, return a score; let the caller filter low-score matches.
- No fuzzy matching across word boundaries by default. Match
whole tokens. Add a
--fuzzyflag for explicit opt-in later.
Imagined outcome: A new user installs v0.12.5, runs a panel, and gets a recall response that's empty/confusing. They think the panel is broken.
Root cause hypothesis: Recall is exposed as a tool by default, new users see it in autocomplete but it has nothing to return.
Mitigation [gating]:
memory.enabled: trueis required in config for any memory tool (consensus_recall,consensus_project_summary,consensus_what_we_decided) to appear inListTools. Off by default.- Tool descriptions are honest. When enabled but empty, the
tool description says "no stored results yet — runs are stored
when invoked with
consensus_store: true."
The following risks are not mitigated in Phase 2.1 and are documented here so reviewers know they were considered:
- Cross-machine sync. Users with multiple machines won't share
memory unless they sync their
~/.consensus/memory/themselves. Adding sync introduces conflict-resolution, encryption-in-transit, and identity questions that are a separate project. Defer. - True semantic search via embeddings. Adding an embedding provider crosses the dependency-light line. Phase 2.2 can layer it in as an opt-in feature when a user has a provider configured; Phase 2.1 ships keyword + metadata + LLM-on-demand ranking.
- Encryption at rest. Files are
chmod 0600and live under the user's home dir. Adding cipher (with key management — passphrase prompts, key files, OS keychains) is a major surface. Out of scope; documented as a known limitation. - Audit log of who read what. Single-user assumption. A multi-user scenario would need it; today the calling MCP host is the same identity as the storing process.
Before any code lands in Phase 2.1:
- This premortem document exists and was reviewed against the charter (R10 satisfied).
- Storage envelope shape locked:
{ schemaVersion, id, storedAt, projectKey, projectPath, panelId, question, tags, result }. - CLI command added for inspection (
ai-consensus-mcp memory list/memory show <id>/memory wipe— admin-only, never an MCP tool). -
memory.enabled: falseis the default in the config schema. - Recall tool description includes the freshness disclaimer.
- Test plan covers F1–F10 contracts (each failure-mode has at least one named test).
Mitigations marked [gating] above are the acceptance criteria for Phase 2.1; [ratchet] items can land in Phase 2.2 or later without blocking the initial ship.
The memory layer is shippable in Phase 2.1 with the gating mitigations in place. The dominant residual risk is unencrypted storage of potentially-sensitive prompts, mitigated by opt-in defaults and explicit documentation. The dominant deferred risk is recall ranking quality — keyword + LLM-on- demand is a known-imperfect first cut, but it ships value immediately and the embedding upgrade path is open.
If the gating mitigations slip — particularly atomic writes (F1) and opt-in defaults (F2, F10) — the layer is not ready.