Deterministic continuity runtime

CSM

A deterministic Postgres continuity runtime for coding agents that records per-item injection provenance and re-reads files to check which of its edits survived.

LicenceMIT
Size57,379 lines of TypeScript in 414 files under src/, owning 46 tables
Activity312 commits on master by eight author names, two of them bots, 25 June – 17 September 2026
Tests1,782 test() and it() call sites in 212 files under test/; the committed full-test-output.txt of 8 July 2026 records 808 passing in 172 suites; not run

Carries 2 of 7 rubric mechanisms. Most systems here carry none or one (43%), and a dash means the mechanism was not found at this commit — not that the system needed it. Each mark is one LLM reviewer's reading of the code at this commit rather than a run of it — known limits.

  • Tombstone
  • Trust state
  • Bi-temporal
  • Scope enforced
  • Mutation audit
  • Human review
  • Negative evals

1. Executive Summary

CSM — Cross-Session Memory — is continuity infrastructure for a coding agent, an OpenCode plugin with a Codex MCP bridge beside it. It captures without a language model and can say, per item, why something did not reach the context window. Its weakness is the Living State tier on top: beliefs, candidate review and capability confidence each compute carefully and terminate in a state no code path writes, a table nothing fills, or an exit code read as evidence.

It is large, and the size is not padding: memories, experience packets, an append-only operational ledger, a work ledger of file edits, a self-model, belief candidates, checkpoints, goals, a context cache, injection telemetry and recall telemetry are all distinct stores with distinct lifecycles.

The design commitment that sets it apart: nothing on the write path calls a language model. The only outbound request in the entire runtime is an embedding call to Ollama or OpenAI (src/embedding-provider-client.ts:9). Extraction is a deterministic distiller that groups tool calls and stamps extractionMethod: 'deterministic' (src/memory-extractor.ts:161); classification is a set of regexes; belief promotion is five numeric threshold checks. A system this large that never asks a model to summarize anything is rare here, and it buys real properties — no extraction cost, no extraction hallucination, and a store you can reason about by reading SQL.

Two mechanisms stand out. The first is per-item injection provenance: context_injection_items (src/schema/context-injection-telemetry-schema.ts:26) records every candidate considered for the re-entry block with a position, a selection_score, a disposition of injected / trimmed / omitted, and a selection_reason_code that includes budget_trim, layer_budget_exhausted and filter_rejection. Most systems in this atlas can tell you what they injected. This one can tell you what it nearly injected and which budget line killed it. The second is the work ledger: every file edit is stored with before/after content hashes and a line-hash multiset, and evaluateSurvival (src/work-ledger-lineage.ts:37) later re-reads the file on disk to classify the change active, partially_superseded, superseded, or reverted. It is a memory of work that gets checked against the artifact it claims to have produced.

The weakest parts are not in the mechanisms but in what connects them. Merge and archive maintain superseded_by, superseded_at, archived_at and an archive_reason, and the shared WHERE builder all three search lanes call opens with superseded_by IS NULL and archived_at IS NULL (src/hybrid-search-sources.ts:13-37), as do the fallbacks, list, cascade and the evidence block. One reader was left out. The lesson-trigger cache selects up to 50 lessons with neither those predicates nor a project filter and injects all of them on every turn (src/lesson-trigger-cache.ts:74-80), beside a lessons query in the same stage that carries both. That reader is why scope_enforced is withheld; audit_log and negative_eval are the two marks.

The more expensive disconnect is in the belief tier. buildBeliefsLayer (src/reentry-layers-secondary.ts:42) — the layer that puts the Living State's consolidated beliefs into the injected block — filters belief.status === 'promoted', and no code path in the repository ever writes 'promoted'. The belief consolidator runs every 120 seconds, maintains confidence and uncertainty per claim, counts contradictions, and has a committed schema migration to double precision so that subnormal decay values survive. Everything it produces is candidate or stale, so the layer renders "No consolidated beliefs yet." on every turn, permanently. The same shape appears in memory_candidates: the table is created, indexed, selected from, updated, cleaned up, and has a one-off dedup script committed against it — and no INSERT exists anywhere in src/, scripts/ or test/.

And the AgentBook — the operational ledger whose whole job is to answer "what is happening in this project right now" — declares 25 event types (src/agentbook-types.ts:1) of which its one and only writer (src/hooks/tool-execute.ts:154) can produce seven. decision, verification_evidence, blocker_identified, goal_achieved, user_correction and milestone are never emitted by any code path. The result is visible in the repository's own committed AGENTBOOK_STATE.md: after 5,111 events across 49 sessions, the turn-1 front page reports no active goal, no phase, "No active blockers or known failures", and a Recent Work section in which eight of ten entries are truncated dumps of CSM's own tool output. The agent's autobiography is mostly a record of it reading its own memory.

2. Mental Model

CSM does not have one notion of memory; it has four, layered, with different epistemic standing. Reading them as one pipeline is the fastest way to misunderstand the system.

Durable memory (memories) is the semantic tier: eleven types plus self_continuity, each row carrying importance, confidence, emotion, source, tags, extracted concepts, and a provenance block in metadata. Experience packets (experience_packets) are the episodic substrate — one row per tool execution, error, milestone, decision or checkpoint, holding signals and internal_state JSON. AgentBook events are the operational journal: append-only, project-scoped, summarized in ranges and projected into a single current-state row. Work-ledger changes are the physical record: one row per file edit, hashed.

How a thing becomes a belief

There are two independent promotion routes and they meet in the same table.

The distiller route is fast and effectively unguarded. ToolDistiller groups a session's tool calls; autoDistill (src/hooks/tool-execute-memory.ts:62) writes a distilled_summaries row and hands the groups to MemoryExtractor.extractFromDistilledSummaries. Each group becomes a candidate with a hardcoded confidence of 0.92 (src/memory-extractor.ts:152), which clears the default autoApproveThreshold of 0.9 (src/config-defaults-base.ts:56) and is written straight into memories. determineInitialStatus declares a 'rejected' return in its signature that the function body cannot produce, and the pending state it can produce goes nowhere: saveCandidateAsMemory writes to memories or does nothing, and memory_candidates — the table holding the reviewable queue — has no INSERT anywhere in the repository. The one function that would have populated it, extractFromTurns (src/memory-extractor.ts:51), has no callers, and stamps extractionMethod: 'llm' on output produced by a keyword if-chain.

The belief route is the careful one. CandidateGenerator derives candidate_belief, candidate_preference, candidate_worldview, candidate_capability and friends into memory_candidate_queue, keyed by a dedup_key with a partial unique index over pending rows. BeliefPromotionEngine.evaluateCandidate (src/belief-promotion.ts:207) then applies five gates in order: confidence, reinforcement count, zero contradictions, evidence-reference count, and distinct source sessions — the last computed by joining source_packet_ids back to experience_packets.session_id (src/belief-promotion.ts:297). A contradicted candidate does not fail; it returns needs_review, which is a different answer from "skip". Every decision carries a thresholdChecks object recording actual-versus-required for each gate, so a promotion report explains itself.

Three caveats matter. The engine is disabled by default (CSM_BELIEF_PROMOTION_ENABLED=false) and dry-run by default. And minSessions defaults to 1 (src/config-defaults-continuity.ts:112), so the session-diversity gate — the most interesting of the five, and the one that distinguishes a pattern from a coincidence — is a no-op unless an operator raises it.

A parallel belief_knowledge_store holds preference/opinion/worldview claims with confidence, uncertainty, contradicted_count and a status of candidate | promoted | rejected | stale. Reinforcement nudges confidence up by 10% of the remaining gap; contradiction raises uncertainty by 15% of its gap; crossing uncertainty ≥ 0.7 with more than two contradictions marks the belief stale (src/belief-knowledge-store.ts:195). Of the four declared states, only candidate and stale are ever written — no code path sets promoted or rejected. That is not a cosmetic gap, because the one consumer that reads the field, buildBeliefsLayer, admits only promoted. The belief tier is therefore a closed loop: it computes, it decays, it contradicts, and nothing it produces can reach the agent.

How a belief stops being one

Four exits, and they are unequal.

Supersede. MemoryMerger groups by LOWER(TRIM(content)) — exact duplicates only, no fuzzy matching — sets superseded_by on the losers, and appends a row to memory_merges (src/merge-tool.ts:189-202). Non-destructive and audited.

Archive. archive-superseded-duplicates.ts and archive-tiny-junk.ts stamp archived_at, archive_reason, archive_batch_id, archive_source and archive_note, with an explicit un-archive path that sets them back to NULL. Batch-reversible, which is better than most.

Expire. cleanupExpiredMemories (src/memory-manager-base.ts:963) is a real hard delete: per-project, dry-run unless apply: true, capped at 1,000 rows by default and 10,000 absolute, transactional, and it emits a memory.retention_cleanup event.

Promote. lesson-auto-promotion.ts is the best-gated exit. A memory candidate becomes a durable lesson only if it passes four conditions stated in the module docstring — at least minRecall recall events, recall in at least minSessions distinct sessions, a content-quality heuristic, and no existing lesson with the same content hash. A regex list refuses the noise classes by shape before any of that: ^\[modified\], ^Tool used:, ^Command executed:, ^\[git\] and the rest. What survives is tagged auto-promoted and stamped metadata.auto_promoted = true "so they can be audited and reverted", and the source candidate is marked applied. Cross-session recall as the promotion signal is the good idea here: a lesson earns durability from having been useful more than once, in more than one session, rather than from a score assigned when it was written. It is wired from lifecycle-orchestrator.ts:171.

Reject. On paper, a caller of the Codex bridge's reject tool sets status = 'rejected' with reviewed_by, and cleanupExpiredCandidates (src/memory-extractor.ts:591) deletes rejected candidates after seven days. Two things spoil it. Nothing is keyed on the rejected value, so re-observing the same behaviour in week two regenerates the identical candidate with no record that anyone said no — this is the closest CSM comes to a tombstone and it is deliberately temporary storage rather than a durable veto. And the queue it operates on is never filled, so in practice this exit is unreachable.

Supersede and archive take effect on the read path. buildWhereClause (src/hybrid-search-sources.ts:13-37), the single WHERE builder FTS, vector and entity boost share, starts from superseded_by IS NULL and archived_at IS NULL before any scope clause, so the predicates hold in global and legacy mode too. The vector-only fallback (src/memory-manager-base.ts:591-592), the text fallback (:813-814), the evidence block (src/hooks/system-transform-live-evidence.ts:44-102) and the onboarding brief (src/agent-onboarding.ts:364, :717) carry the same pair. The whole set landed on 17 September 2026 in 09e42f98…, closing issue #99, with governance vetoes and evidence injection following the same morning.

The reader that missed the sweep is the one with its own cache. LessonTriggerCache.refresh (src/lesson-trigger-cache.ts:68-102) selects every lesson at importance 0.6 or above, 50 at most, with no lifecycle predicate and no project_id, and injectLessonTriggers pushes the whole set into the system prompt as <active_lessons> on every turn (src/hooks/system-transform.ts:40). The stage before it, injectDirectMemoryEvidence, selects lessons scoped to the project and filtered on both columns. So a lesson superseded or archived in one repository is withdrawn from one block and still instructs the agent in the next, in every repository. The deep-continuity block, which runs only when enabled and a trigger keyword appears, has the same gap in the causal-thread hydrator's temporal-neighbour and decision queries (src/self-continuity-causal-thread.ts:246-276).

Diagram — corrections reach every read path but the lesson-trigger cache, and the promoted status the beliefs layer requires has no code path that writes it
Diagram source
%% caption: corrections reach every read path but the lesson-trigger cache, and the promoted status the beliefs layer requires has no code path that writes it
stateDiagram-v2
    [*] --> Packet: tool.execute.after hook
    Packet --> Distilled: deterministic distiller, confidence fixed at 0.92
    Distilled --> Memory: auto-approved, no reviewable queue exists
    Packet --> Candidate: candidate generator
    Candidate --> Memory: five promotion gates, engine off by default
    Candidate --> NeedsReview: contradicted at least once
    Candidate --> Belief: belief consolidator, every 120 seconds
    Belief --> Stale: uncertainty over 0.7 and three contradictions
    Belief --> Promoted: unreachable, no code path writes this status
    Promoted --> Injected: the beliefs layer admits only promoted
    Memory --> Superseded: exact-content merge
    Memory --> Archived: archive pass sets a flag
    Memory --> Deleted: per-project TTL cleanup
    Memory --> Retrieved: hybrid search, both lifecycle predicates
    Retrieved --> Injected: re-entry block, 2100 characters
    Memory --> LessonCache: lessons at importance 0.6 or more
    Superseded --> LessonCache: no lifecycle predicate in the cache
    Archived --> LessonCache: no lifecycle predicate in the cache
    LessonCache --> Injected: active_lessons, every turn, every project
    note right of Injected
        Superseded and archived rows are
        hidden from search. Lessons return
        through the trigger cache. Beliefs
        never reach the agent at all.
    end note

3. Architecture

CSM is a library that runs inside the agent host. There is no daemon, no queue and no worker process. Everything — hooks, tools, background timers, schema initialization — executes in the OpenCode plugin process or in the csm-mcp stdio process.

Persistence is PostgreSQL 14/16 with pgvector, or SQLite. The Postgres path owns 46 tables. Vectors are dual-written: an embedding column on memories for the vector-only fallback, and a memory_chunks row carrying the same vector under an HNSW index with m = 16, ef_construction = 64 (src/schema/memory-support-schema.ts:14). Full-text is a generated tsvector column weighting content A, tags B and metadata C (src/schema/memory-support-schema.ts:44).

Schema management is unusually disciplined for a project this size. A csm_schema_migrations ledger with per-migration checksums (src/schema/migration-ledger.ts) fails fast on an unknown future migration or on a ledger belonging to the other provider. docs/SCHEMA_SUPPORT_MATRIX.md names each supported upgrade path and the test that proves it, and states plainly that rollback restores a backup rather than reversing DDL.

Background work is in-process timers, not jobs: a 2-second debounced documentation flush (src/hooks/tool-execute-memory.ts:11), a 120-second belief consolidation interval, self-model replay, and the distiller flush. Nothing survives a host crash except what already reached the database. There is no periodic pass that re-reads or rewrites the whole store, which is a real operational virtue — the background token bill is zero because there is no background model call at all.

External dependencies are one embedding provider and a database. A third is optional and is a separate project by the same author: CSM_LIVING_MIND_URL (src/hooks/system-transform-live-end.ts:41) names a Living Mind Cortex server, and when it is set every system-prompt transform issues GET /api/agent/context with a 500 ms timeout and skips silently on any failure. Unset, nothing is fetched. Section 8 has what the block carries.

Deployment and ergonomics

The full path needs PostgreSQL with pgvector plus a running Ollama (or an OpenAI key). First run is npx csm-init then npx csm-doctor --online, which checks package, Node version, strict config, security baseline, database, migration history and embedding model, and is documented not to include credentials or memory content in its output.

The SQLite "core mode" is narrower than the README's feature table suggests, and the difference is worth stating in retrieval terms rather than feature terms: searchMemories skips vector search entirely on SQLite (src/memory-manager-base.ts:528) and checkFtsAvailable returns false unconditionally for that dialect (src/hybrid-search-sources.ts:171). What remains is content LIKE '%query%' ordered by importance and creation date. That is a usable local mode; it is not the hybrid retrieval the same document describes two sections earlier.

Nothing is human-readable except AGENTBOOK_STATE.md and .csm/continuity-snapshot.json. Repair means SQL. Against that, the backup/restore drill (npm run drill:backup-restore) is committed and wired into the release gate.

4. Essential Implementation Paths

Capture. tool.execute.after (src/hooks/tool-execute.ts) → logToolUsage (src/hooks/tool-execute-memory.ts:111) → an AgentBook event via buildAgentBookToolEventInput (src/agentbook-tool-event.ts:72) and an experience packet. Tool output is classified by classifyToolEvent, and the event summary is ${tool}: ${output.slice(0, 200)}.

Write. MemoryManager.saveMemory (src/memory-manager-base.ts:185) in strict order: default provenance metadata → project-ownership verification against sessions → transcript dedup on metadata.messageId → redaction of content, metadata and tags → per-type token quota → concept extraction → embedding generation → INSERT → dual-write to memory_chunks → memory.created event → graph link building.

Extraction. autoDistill (src/hooks/tool-execute-memory.ts:62) → MemoryExtractor.extractFromDistilledSummaries → saveCandidateAsMemory. The lesson text is shaped by makeActionableLesson (src/memory-extractor.ts:198), which prefixes Avoid this — when no imperative verb is detected.

Retrieval. MemoryManager.searchMemories (src/memory-manager-base.ts:510) → hybridSearch (src/hybrid-search.ts:26), which runs vectorSearch, ftsSearch and entityMatchBoost concurrently, fuses with reciprocalRankFusion at RRF_K = 60, normalizes each channel to its own max, applies weights, and drops near-duplicates at Jaccard ≥ 0.85 (src/hybrid-search-ranking.ts:83). Three fallback tiers follow on failure, each recorded in memory_recall_events with a distinct source value.

Injection. experimental.chat.system.transform → runSystemTransform (src/hooks/system-transform.ts:32-56), twelve sequential stages ending in finalizeSystemTransform; the third of them injects the lesson-trigger cache (src/lesson-trigger-cache.ts:155-190). The re-entry block itself is built by ReentryLayerBuilder (src/reentry-layer-builder.ts) across eight layers and logged item-by-item through context-injection-logger.ts.

Correction. MemoryMerger.applyGroup (src/merge-tool.ts:189), archive-superseded-duplicates.ts, archive-tiny-junk.ts, MemoryManager.deleteMemory (src/memory-manager-base.ts:491), cleanupExpiredMemories (src/memory-manager-base.ts:963).

Work verification. verifyWorkLedgerChanges (src/work-ledger-verification.ts:11) takes a per-file capture lease, re-reads the file, and calls evaluateSurvival (src/work-ledger-lineage.ts:37).

Integration. registerHooks (src/hooks-registration.ts:16) and createRegisteredToolList (src/hooks/tool-registry.ts:52) for OpenCode; src/codex-mcp-server.ts with src/codex-bridge-extra-ops.ts for Codex.

5. Memory Data Model

memories (src/schema/memory-table-schema.ts) carries a twelve-value memory_type CHECK, importance and confidence as bounded floats, an emotion enum, a tags array, linked_memory_ids, a JSONB metadata, four timestamps, access_count, five archive columns and a self-referencing superseded_by.

Scoping is single-axis, and strict on the search path. Everything hangs off project_id, which is the workspace directory. There is no user, tenant, agent or team axis — CSM assumes one operator. What it does with that axis is careful: saveMemory refuses a write whose declared project disagrees with the session's recorded project (src/memory-manager-base.ts:215-218), and every public tool is constructed with its projectId bound at registration so the agent cannot pass one (src/tools.ts:71).

Provenance is defaulted onto every row that does not supply it: source_kind, evidence_strength, source_session_id, source_agent_id, source_model_id, source_surface (src/memory-manager-base.ts:190-199). Promoted memories supply their own instead — source_kind: 'belief_promotion' and evidence_strength: 'derived_pattern', alongside the source packet ids and the session count that justified the promotion — so a derived belief is distinguishable from a directly observed one at query time. This is the one place CSM does encode epistemic difference; it just encodes it in JSONB rather than in a column anything filters on.

Temporal modelling is record-time only. created_at, updated_at, accessed_at, superseded_at, archived_at, last_reinforced_at all describe when CSM learned or changed something. Nothing records when a fact was true. A preference that held for three months and then changed is representable only as two rows, one of them superseded — with no interval on either.

The other stores are cleanly separated rather than overloaded: experience_packets for episodes, agentbook_events/summaries/current_state for operations, work_ledger_changes for physical edits, self_model_capabilities for capability confidence, belief_knowledge_store for stances, checkpoints and goals for task state, memory_quality_scores for a scored band per memory. That separation is the main reason 46 tables is legible rather than sprawling.

Two readers break the scoping model. The lesson-trigger cache reads memories itself with no project predicate and injects what it finds (section 2). And self_model_capabilities has no project_id, and SelfModelUpdater.loadPackets (src/self-model-updater.ts:136) reads experience_packets with no project filter and no limit. Capability confidence earned in one repository is derived from, and displayed in, every other.

6. Retrieval Mechanics

Four signals, fused. Vector similarity over pgvector cosine distance; Postgres websearch_to_tsquery full-text with ts_rank_cd; an entity boost that scores 2.0 for a content match, 1.8 for a match inside metadata.extracted_concepts, and 1.5 for a tag match (src/hybrid-search-sources.ts:146-148); and recency with a 168-hour half-life. Default weights are vector 0.35, text 0.25, entity 0.35, recency 0.05 — an unusually high entity weight, and a defensible one for a coding agent where the query is often a literal symbol or path.

The fusion is a two-stage normalize: RRF within each channel, then min-zero max-one normalization per channel before weighting (src/hybrid-search-ranking.ts:39-43). Recency is clamped rather than normalized, so its contribution is absolute rather than relative to the candidate set — which is the right choice, since normalizing recency would make the newest result in every set score 1.0 regardless of age.

Retrieval is tool-mediated for search and automatic for injection. The agent calls csm_memory_search; the re-entry block arrives without being asked. The search tool returns 150-character previews with score, type, importance and access count — enough to decide, not enough to use, which pushes the agent toward a second fetch.

Budgeting is tight where it matters. The whole re-entry block is capped at maxChars: 2100 (src/reentry-contract.ts:95) — roughly 525 tokens — allocated across eight layers with per-layer budgets and priorities, identity and constraints marked neverTrim. Compared with systems in this atlas that inject several thousand tokens of "relevant memories" unbounded, a 2,100-character ceiling is a deliberate and defensible constraint.

Failure modes, in order of how much they would bother me:

  • Retired lessons return through the trigger cache. Covered in section 2: search excludes superseded and archived rows, and <active_lessons> does not, across projects. It is the highest-value fix in the codebase and it is three predicates in one query.
  • Jaccard-0.85 dedup is unigram set overlap, so two memories that state opposite conclusions in similar vocabulary ("always use X" / "never use X") are near-identical under it and one is silently dropped.
  • SQLite degrades to substring matching without saying so at the call site.
  • Entity boost uses ILIKE '%query%' on the whole query string, so multi-word queries rarely trigger the channel that carries 0.35 of the weight.

7. Write Mechanics

Writes are agent-triggered or hook-triggered, never model-mediated. There is no extraction prompt in this repository because there is no extraction model.

Redaction runs before persistence, embedding, concept extraction and chunking — the ordering is explicit in the file header (src/redactor.ts:1, "Redact before storage, never after") and enforced in saveMemory before any other processing. Secrets, emails, phones, IPs and URL credentials are redacted by default; workspace paths are normalized to [WORKSPACE]/src/foo.ts rather than destroyed, which preserves the one kind of PII a coding agent actually needs. Metadata and tags are redacted structurally too, and the audit records counts only, never values.

The per-type quota is the one lossy step and it is honest about it. applyTypeQuota (src/memory-type-quota.ts:26) compresses over-budget content to signal lines — lesson is exempt unconditionally, error-marked content is exempt, episodic gets 200 tokens — and saveMemory logs a warning stating that "The original text is NOT recoverable" and naming the type to use instead. Destructive, documented, and pointed at the workaround.

Deduplication is layered: an in-flight check on metadata.messageId for transcript rows, a unique partial index as the backstop with a unique-violation handler that returns the existing row rather than failing the capture (src/memory-manager-base.ts:336), an md5(compressed) unique index on distilled summaries, a partial unique index on pending candidates by (candidate_type, dedup_key), and the exact-content merge pass. This is what a system built by someone who has actually run two plugin instances at once looks like.

Conflict handling exists only at the belief tier: contradiction increments a counter, raises uncertainty, and eventually marks a belief stale. Two contradictory memories rows simply coexist — and since the belief tier's output never reaches the agent, in practice conflict handling does not exist on any path the model can see.

Operational cost

The write path is synchronous and blocks on the embedding call. saveMemory awaits this.embeddings.generate(contentToProcess) before the INSERT (src/memory-manager-base.ts:303), so every capture costs one round trip to Ollama or OpenAI on the agent's critical path. An embedding failure is caught and logged, and the row is stored with a NULL embedding — recoverable later via csm_memory_backfill_embeddings, but invisible to vector search until then. There is no LLM call, so the cost is milliseconds rather than seconds; the lag before a memory is retrievable is effectively zero.

No background pass re-reads or rewrites the store. Merge, archive and quality scoring are all operator-invoked tools, not schedules.

On the read path, the per-turn cost is the concerning number. Twelve injection stages run inside experimental.chat.system.transform on every request (src/hooks/system-transform.ts:37-55), appending re-entry, advisories, lesson triggers, governance notes, session context, self-continuity and a compiler status line to the system prompt. A system prompt that changes every turn invalidates the provider's prompt-prefix cache every turn. The re-entry block is small; the cache miss it forces is not, and nothing in the repository measures it.

8. Agent Integration

Eleven OpenCode hook keys (src/hooks-registration.ts:37-47): event, chat.message, permission.ask, tool.execute.before, tool.execute.after, tool, dispose, and four under experimental.* — chat.system.transform, chat.messages.transform, session.compacting, compaction.autocontinue. The deepest and most valuable integration points are all experimental, which is a standing coupling risk: the injection architecture depends on OpenCode APIs its own namespace marks as unstable.

Roughly fifty tools register in the Postgres path across memory, governance, Living State, AgentBook, continuity, checkpoints/goals and context cache. The agent has substantial agency — it can save, search, delete, merge, generate candidates, promote beliefs, run governance reports and export a wiki — but not over scope: projectId is bound at registration and absent from every tool's argument schema, verified by test/public-memory-tool-isolation.test.ts.

The Codex MCP bridge exposes a comparable surface over stdio, and is the only surface that reaches the candidate review tools — memory_candidate_list, memory_candidate_approve, memory_candidate_reject (src/codex-mcp-extra-tools.ts:32). The equivalent OpenCode tools are written and exported in src/tools.ts:489 and never registered, so on the plugin path they exist in the binary and cannot be called.

That asymmetry turns out not to matter, which is the more interesting fact. memory_candidates has no writer: every reference in src/, scripts/ and test/ is a SELECT, an UPDATE, a DELETE, a CREATE TABLE or an index, and the only function that constructs a pending candidate is the uncalled extractFromTurns. So the Codex bridge's approve/reject tools are live code over a table nothing fills.

The human_review mark is withheld on that basis and on a second one that would hold even if the table filled: the approve verb is registered on the Codex MCP server, the same tool surface the agent writes memories through, so the producing agent can clear its own queue. No commit on any branch, from the first on 25 June 2026, contains an INSERT into the table. Yet the one-off scripts/dedup-candidates.mjs, committed on 29 June 2026, counts and de-duplicates rows in it, so the maintainer's database was filled by code that predates this repository's history.

Turn 1 is handled outside the plugin entirely: AGENTBOOK_STATE.md is listed in opencode.json instructions, so the front page is read by the host before any hook runs. Solving cold start with a file the host already loads is a good, cheap idea — it works even when the plugin fails to initialize.

One integration behaviour deserves naming. When a user's message matches /only\s+<agent_reentry_context>/i and similar patterns, CSM latches a "source-only" mode for three minutes, disables every csm_* tool plus bash, read, grep, edit, write and nine others, and injects an instruction block (src/hooks/reentry-source-only.ts:32-47). Restricting tools so an answer is provably grounded in the injected block is a legitimate and clever guarantee. But the same block instructs the model: "Do not mention blocked tools, failed tools, guards, permissions, shell attempts, or hidden implementation details" — the runtime tells the agent to conceal from the user that its tools were taken away. And the first line prescribes a verbatim opening sentence about git history that is asserted in test/phase-7b-system-transform.test.ts:124. A product's system prompt containing the exact expected answer to one evaluation question is a smell worth flagging regardless of intent.

The Living Mind block is the one input CSM injects from outside its own store. With CSM_LIVING_MIND_URL set, injectLivingMind (src/hooks/system-transform-live-end.ts:38-52) fetches /api/agent/context in the last stage of every transform and renders a <living_mind_context> block of cognitive stance, urgency, creative pressure, circadian phase, energy, pain, load and any phase-blocked task types (:17-36). The server is NovasPlace/living-mind-cortex, matched on the route rather than the name. At f8bdb805… its FastAPI router mounts /api/agent (api/agent_gateway.py:36), serves GET /context (:326-388) on port 8008 (api/main.py:702), and returns every field the block reads but one under the same names. CSM's first commit, 672a78a6… of 25 June 2026, fetched http://localhost:8008/api/agent/context with no timeout under the comment "Living Mind Cortex: inject cognitive state"; 3b8bfa39… moved it behind the variable on 14 July 2026.

Three consequences follow from the match. The block carries no memory: the route also returns relevant_memories, and CSM renders none of it. One line never renders, because CSM reads hormones.dominant_emotion and the route's hormones object holds five numbers, with the dominant emotion returned as recall_bias instead (api/agent_gateway.py:345, :378-384). And the phase gate is wall-clock: from 22:00 to 05:00 the server reports night, and CSM tells a coding agent Phase blocked: execution, heavy_refactor, research (api/agent_gateway.py:87-92, state/circadian.py:150-157). The server's ARCHIVE.md marks it a fossil as of 5 August 2026 and names CSM as the memory organ its ideas moved to; the hook is unchanged on every CSM branch.

9. Reliability, Safety, and Trust

Provenance is the strongest column. Every memory carries source kind, evidence strength, session, agent, model and surface. classifyValueClaim (src/value-source-guard.ts:45) marks a claim known only when a stored memory backs it and its provenance is one of transcript | tool_trace | file_diff | user_supplied at direct_original strength and confidence ≥ 0.7 — otherwise inferred. detectUnlabeledInferences scans generated text for hedges like "seems to prefer" and "based on patterns" and flags them as unlabelled inference. That is a memory layer that can represent uncertainty and says so.

What it cannot represent is epistemic status on a memory. The memories table has a confidence float and no status column at all — no candidate, no verified, no rejected — so a promoted belief and a directly observed fact differ only in their provenance metadata, not in anything the read path can filter on. The two places a discrete status does exist both fail to close, and both are worth stating exactly, because each looks closed from one end.

The belief status has two writers and neither writes the value four readers want. belief_knowledge_store.status is CHECK (status IN ('candidate', 'promoted', 'rejected', 'stale')). Three statements in src/ touch the table: one INSERT that writes 'candidate', and two UPDATEs that write 'stale'. Nothing anywhere writes 'promoted' or 'rejected'. Four read paths filter on 'promoted' — reentry-layers-secondary.ts:42 and reentry-layers-state.ts:51 building the re-entry brief, agent-onboarding.ts:430, and continuity-resilience-report.ts:438 counting them for the health report — so the belief section of a re-entry brief is empty by construction and the health report reports zero as a fact about the system rather than about the data.

The trap for a reader is the filename. src/belief-promotion.ts exists, is 400 lines, is wired, and logs Belief promotion: N promoted — and what it promotes is rows of memory_candidate_queue into memories. It never touches belief_knowledge_store. A grep for "promotion" finds a live module; a grep for what writes the column finds nothing.

And memory_candidates is a table with no INSERT. Its pending | approved | rejected | auto-approved | archived machine is declared in schema/core-schema.ts:52, given four indexes, and then selected from (memory-extractor.ts:499, :574), updated (:531, :599) and swept (:593, memory-manager-base.ts:1015) — five statements and a TTL delete over rows nothing creates. It is a different table from the live memory_candidate_queue, which candidate-generator.ts:145 and belief-promotion-scanner.ts:468 do fill; the two names differ by one word and only one of them is connected.

trust_state is withheld, and the reason is the useful sentence: CSM has more trust machinery than most systems that carry the mark, and none of it terminates in a status a query can act on. The nearest miss is on the lifecycle axis. Every search lane excludes superseded_by and archived_at, but those are a pointer and a timestamp rather than a status, and every writer of either is exact-content deduplication or junk archival (src/merge-tool.ts:194, src/archive-superseded-duplicates.ts:147, src/archive-tiny-junk.ts:239). A superseded row says the same thing as its survivor, so the filter withholds a duplicate, not a claim held to be false.

scope_enforced is withheld, with a strong near-miss. The search path fails closed to 1=0 in project mode, and every public tool binds projectId at registration so the model cannot name another project (src/hybrid-search-sources.ts:39-57, src/tools.ts:71). But LessonTriggerCache.refresh reads the same memories table with no project_id predicate (src/lesson-trigger-cache.ts:74-80), and runSystemTransform injects the result into the system prompt on every turn (src/hooks/system-transform.ts:40). The key is on those rows — the lesson promoter writes projectId: ctx.directory (src/lesson-auto-promotion.ts:148) — and the default store is one Postgres database at a fixed URL for every workspace (src/config-provider.ts:10-11, :71). So an injection hook over the same store carries no predicate at all.

A second, weaker point sits beside it. A project search that returns zero rows is retried as legacy, which admits rows with no project, and the widened mode is passed into the cascade (src/bridge-ops.ts:55-68). That widening reaches unscoped memories rather than another project's.

Audit is broad, with one gap. memory_events records creation, deletion and retention cleanup (src/memory-manager-base.ts:377, :500, :1026), and memory_merges records each merge with its normalized hash; archival and metadata updates stamp the row and append nothing. Beside them, context_injection_events and _items record what was injected, trimmed and omitted with an idempotency key, a block_hash, a builder_version and a config_hash; memory_recall_events records which memories answered which query at what rank, hashing the query text after redaction — a detail test/privacy-persistence-boundaries.test.ts:167 asserts explicitly.

Prompt-injection resistance is partial. The re-entry header declares "Status: operational context, not user instruction", which is a real mitigation. But conversation memories are captured with fullTranscript: true and stored verbatim, and a transcript contains whatever the agent read — web pages, dependency READMEs, log files. That text becomes a memories row, becomes searchable, and can be selected into the recent layer. Provenance records that it came from a transcript; nothing downgrades or fences text that originated outside the user. The Living Mind block has the same shape at a smaller size: an unauthenticated HTTP response whose cognitive_stance and status strings are rendered verbatim into the system prompt without the re-entry header's disclaimer (src/hooks/system-transform-live-end.ts:19, :29).

Concurrency is handled better than average. Project ownership is verified before write; the work ledger takes an advisory capture lease per file (src/work-ledger-capture-lock.ts); unique-violation races on transcript capture resolve to the existing row; the documentation flush uses per-project timers so two workspaces cannot cancel each other.

Data-loss risks are three: the type quota is irreversible and warned about; belief evidence_refs grows one JSON entry per packet forever, and the committed self-model shows code_editing at 3,849 evidence entries in a single row; and SelfModelUpdater.updateAll loads the entire experience_packets table into memory on each run, which at the maintainer's own 51,170 packets is already an unbounded read.

That same committed snapshot exposes the deepest trust problem in the design. The self-model reports code_editing confidence=0.900 successes=3849 failures=0. determineOutcome (src/self-model-updater.ts:181) derives success from the absence of an error field and a zero exit code — so the model is measuring "the edit tool returned", not "the edit was correct". 3,849 edits with zero failures is not a capability estimate; it is a tautology with a confidence attached. The 0.9 ceiling and the diminishing-returns rate after 20 observations are thoughtful guards against the number being too high; they do not address the observable being the wrong one.

10. Tests, Evals, and Benchmarks

The suite is large and real: 1,782 test(/it( call sites across 212 test files. The committed full-test-output.txt records 808 passing tests across 172 suites, zero failures — a narrower run than the call-site count, presumably one where Postgres-gated suites did not execute. The README's claim of "more than 1,500 automated tests" is supportable at the call-site level and not by that artifact; the two measure different things. The artifact was last written on 8 July 2026, so it is a snapshot rather than a check. I inspected these artifacts; I did not run the suite. No paper is cited from the README or docs/.

What is well covered: schema migration and idempotent replay, redaction across every persistence boundary, project isolation for tools and candidates, the context-injection contract and telemetry, work-ledger lineage and survival, belief promotion thresholds, archive and merge dry-run behaviour (including assert.equal(updateSeen, false, 'dry-run must not issue UPDATE')), and compaction telemetry.

Negative evidence exists and is explicit. The strongest case is about corrected material: test/active-memory-recall.test.ts:43-113 saves three memories sharing a query phrase, archives one, supersedes another, and asserts that search, list and cascade each return exactly the live id — a populated set with the positive control inside the same assertion. It runs on SQLite; the Postgres vector lane is covered only by a mocked pool asserting the SQL text (:115-130). test/privacy-persistence-boundaries.test.ts:174 asserts assert.deepEqual(missingProjectScope, [], 'project mode without a project ID must fail closed') — a committed case asserting that material must not be retrieved. test/memory-candidate-isolation.test.ts asserts a reviewer bound to one session cannot act on another session's candidate.

test/benchmark-hybrid.ts is a genuine retrieval harness: eight seeded memories and five labelled queries — three exact-symbol, two semantic — comparing hybrid against vector-only weights with an expected content substring per query. Eight documents is a smoke test rather than a benchmark, but it is a real one with ground truth, and its seed data candidly includes a memory describing a past entity-recall bug in this very code.

What the suite does not cover is reachability, and that is where its worst defects live. No test asserts that the beliefs layer can ever render a belief, or that a pending candidate can exist; both would fail at this commit. And no test file references LessonTriggerCache, so the one reader the active-memory sweep missed is the one reader with no case. A suite this thorough carries no check that a status a reader requires is a status some writer produces.

Also missing: a retrieval-quality regression over more than eight documents; a test for SelfModelUpdater at packet counts where the full-table load matters; and any measurement of the per-turn injection cost against prompt caching.

11. For Your Own Build

Steal

Log the injection, item by item, with the reason it lost. A table like context_injection_items — position, score, disposition, and a reason code distinguishing budget_trim from layer_budget_exhausted from filter_rejection — turns "the agent didn't know X" from an argument into a query. Almost every memory system can tell you what it injected. Being able to say what came fourth when three fit is a different class of debuggability.

Verify a memory of work against the artifact it describes. Store the before/after hash and a line-hash multiset with the edit, then re-read the file later and classify the change surviving, partially surviving, superseded or reverted (src/work-ledger-lineage.ts). Most agent memory records intent; this records intent and then checks it against the world.

Require session diversity before promoting a pattern to a belief. Counting distinct sessions behind a candidate, not just occurrences, is what separates a habit from a loop — one runaway session can reinforce a candidate fifty times and still be one observation. Set the threshold above 1, which CSM's own default does not.

Redact before every downstream step, and normalize paths instead of destroying them. The ordering rule at the top of src/redactor.ts — redact before storage, embedding, chunking and concept extraction — is the correct invariant, and turning C:\Users\x\proj\src\a.ts into [WORKSPACE]/src/a.ts keeps the memory useful.

Return the existing row on a unique-violation race instead of failing the capture. Memory writes are best-effort side effects of someone else's work; they should never surface an error to the agent.

Avoid

Don't let correction bookkeeping and retrieval drift apart, and count the caches as read paths. A superseded_by column that a read path ignores is worse than no column: the governance report says the store is clean, and the agent keeps reading the duplicate. CSM added the predicate to search, the fallbacks, list, cascade, graph recall, governance and evidence injection in one morning, and missed the one component that queries the table on its own schedule and holds the result for a minute. Grep for every FROM memories, not for every search function.

Don't derive capability confidence from tool exit codes. "The tool returned 0" is not evidence the work was right, and a self-model that has recorded 3,849 successes and zero failures has proved only that it is measuring the wrong thing. Tie confidence to an outcome someone or something else verified.

Don't declare a vocabulary your writers cannot produce. Twenty-six event types with seven emitters means the schema documents an intention and the data records tool calls. Either write the emitters or shrink the enum — the gap is invisible until someone reads the projection and finds no decisions in it.

Never let a read path filter on a status the write path cannot produce. This is the same defect three times in one codebase — a beliefs layer admitting only promoted when nothing writes promoted, a review queue whose table has no INSERT, an extractFromTurns with no callers — and each one fails silently, rendering an empty section rather than an error. The cheap defence is a startup assertion or a test per enum value: for every state a reader depends on, prove some writer can reach it. A dead branch that returns nothing looks exactly like a feature with nothing to say yet.

Don't record your own memory tooling as project activity. If the operational ledger logs csm_memory_list and csm_continuity_report as events, the recent-work summary fills with the agent reading its own memory. Exclude the memory system's own surface from the memory system's own journal.

Don't instruct the model to hide an active guard from the user. Disabling tools to guarantee a grounded answer is defensible; telling the agent not to mention that tools were disabled turns a safety mechanism into a misrepresentation.

Fit

This is a single-operator system, and it is unusually strong at the parts a single operator actually touches: capture, retrieval, redaction, injection budgets, migrations, backup. One project_id axis, no auth, no tenancy, no user model, an embedding call on the write path, and a database you are expected to back up yourself. If you are one engineer running coding agents across a handful of repositories on your own machine and you want continuity that is inspectable in SQL and costs nothing per capture, that core is among the most complete things in this atlas, and the migration ledger and backup drill mean it will survive its own upgrades.

Set your expectations one layer down, though. The Living State tier — beliefs, capability confidence, candidate review, the whole apparatus the README leads with — either terminates in an unreachable state, reads a table nothing writes, or counts exit codes as evidence. None of that will hurt you; it will simply do nothing, quietly, while looking like the most sophisticated thing in the package. Adopt CSM for the continuity runtime and treat the epistemics as unfinished, which at this commit is what they are.

If you are building a product, walk away. Not because of the quality — the engineering is careful and the operational discipline is above this atlas's median — but because the shape is wrong for it. There is no tenancy boundary to extend, the cross-project self-model is a leak the moment two clients share a deployment, four of the eleven hooks are marked experimental by the host, and 57,000 lines with 46 tables is a maintenance surface that assumes its author's context. The two mechanisms to take from it — per-item injection provenance and the work-survival ledger — are each a few hundred lines and are better copied than adopted.

The middle case, a team of five wanting shared agent memory, is the one to refuse outright. CSM has no concept of another person, and the design does not have a seam where one would go.

12. Open Questions

  • What is the per-turn latency and prompt-cache cost of the twelve-stage system transform in practice? Nothing in the repository measures it, and it is the number most likely to decide whether this is usable on a large model.
  • Does SelfModelUpdater.updateAll remain viable at the maintainer's own scale? The committed state reports 51,170 experience packets and a full-table load per run; whether that has been felt is not visible from the code.
  • Why are the OpenCode candidate review tools written but never registered — a deliberate narrowing of the plugin surface, or an unfinished wiring?
  • Does the belief-promotion engine get enabled in real use? It is off and dry-run by default, and the most interesting epistemics in the codebase sit behind those two flags — beside a beliefs layer that would render empty even if they were on.
  • What filled memory_candidates before 25 June 2026? The rows the dedup script counts came from code outside this repository's history, and the maintainer's earlier projects are the obvious place to look.
  • Is the lesson-trigger cache's missing project and lifecycle filter known? The 17 September 2026 sweep covered search, list, graph, governance and the evidence block, which makes this one read as an omission.

Appendix: File Index

Storage and schema — src/schema/memory-table-schema.ts, src/schema/memory-support-schema.ts, src/schema/core-schema.ts, src/schema/agentbook-schema.ts, src/schema/context-injection-telemetry-schema.ts, src/schema/migration-ledger.ts, src/candidate-schema.ts, src/experience-packet-schema.ts, src/belief-knowledge-schema.ts, src/self-model-schema.ts, src/work-ledger-schema.ts.

Write path — src/memory-manager-base.ts (behind the facade src/memory-manager.ts), src/memory-extractor.ts, src/redactor.ts, src/memory-type-quota.ts, src/concept-extractor.ts, src/hooks/tool-execute-memory.ts, src/agentbook-tool-event.ts.

Retrieval — src/hybrid-search.ts, src/hybrid-search-sources.ts, src/hybrid-search-ranking.ts, src/hybrid-search-types.ts, src/priming-engine.ts, src/recall-telemetry.ts.

Context assembly — src/reentry-contract.ts, src/reentry-layer-builder.ts, src/reentry-layers-primary.ts, src/reentry-layers-secondary.ts, src/context-injection-contract.ts, src/context-injection-logger.ts, src/hooks/system-transform.ts, src/hooks/system-transform-live-evidence.ts, src/hooks/system-transform-live-end.ts, src/lesson-trigger-cache.ts, src/hooks/reentry-source-only.ts, src/agentbook-frontpage.ts.

Trust and governance — src/belief-promotion.ts, src/belief-knowledge-store.ts, src/self-model-updater.ts, src/value-source-guard.ts, src/merge-tool.ts, src/archive-superseded-duplicates.ts, src/archive-tiny-junk.ts, src/memory-governance-report.ts, src/quality-scoring.ts, src/work-ledger-lineage.ts, src/work-ledger-verification.ts.

Integration — src/hooks-registration.ts, src/hooks/tool-registry.ts, src/tools.ts, src/tool-names.ts, src/codex-mcp-server.ts, src/codex-mcp-extra-tools.ts, src/codex-bridge-extra-ops.ts.

Tests and benchmarks — test/active-memory-recall.test.ts, test/privacy-persistence-boundaries.test.ts, test/public-memory-tool-isolation.test.ts, test/memory-candidate-isolation.test.ts, test/belief-promotion.test.ts, test/work-ledger-lineage.test.ts, test/context-injection-audit.test.ts, test/archive-tiny-junk.test.ts, test/benchmark-hybrid.ts, full-test-output.txt.

Operator-facing — AGENTBOOK_STATE.md, docs/SCHEMA_SUPPORT_MATRIX.md, docs/FEATURES.md, SECURITY.md, scripts/backup-restore-drill.ts, src/doctor.ts.

Appendix: Recorded Searches

Run from the root of the checkout at the pinned commit.

Claim Command Result at this pin
Search lanes and fallbacks carry both lifecycle predicates git grep -n -E "superseded_by IS NULL|archived_at IS NULL" -- src/hybrid-search-sources.ts src/memory-manager-base.ts hybrid-search-sources.ts:23-24; memory-manager-base.ts:568-569, :591-592, :676-677, :813-814
The lesson-trigger cache filters on neither lifecycle nor project git grep -n -E "superseded_by|archived_at|project_id" -- src/lesson-trigger-cache.ts Nothing
The cache is read by the per-turn system transform, which injects it, and by the tool hook, which only logs git grep -n -E "injectLessonTriggers\(|lessonTriggers\.refresh\(" -- src hooks/system-transform.ts:40, hooks/system-transform-live-evidence.ts:169, hooks/tool-execute.ts:219, and the definition at system-transform-live-evidence.ts:164
No test exercises the lesson-trigger cache git grep -l -E "LessonTriggerCache|lesson-trigger-cache" -- test Nothing
Nothing writes a promoted or rejected belief git grep -n -E "INSERT INTO belief_knowledge_store|UPDATE belief_knowledge_store" -- src scripts; git grep -n -E "'(promoted|rejected)'" -- src/belief-knowledge-store.ts One INSERT writing the entry's own status, which is set to 'candidate' (belief-knowledge-store.ts:233) or 'stale' (:196), and two UPDATEs writing 'stale'; the second command returns nothing
Four read paths want one git grep -n -E "'promoted'" -- src reentry-layers-secondary.ts:42, reentry-layers-state.ts:51, agent-onboarding.ts:430, continuity-resilience-report.ts:438, plus two schema CHECKs and the BeliefStatus type
memory_candidates has no INSERT at the pin or in any commit git grep -n -E "INSERT INTO memory_candidates" -- src scripts test; git log --all --format="%h %s" -G"INTO memory_candidates[^_]" Nothing, and no commit on any branch
The approve verb sits on the agent's Codex tool surface; the OpenCode twins are unregistered git grep -n -E "memory_candidate_(approve|reject)'" -- src; git grep -n -E "memoryCandidate(List|Approve|Reject)Tool" -- src codex-mcp-extra-tools.ts:37, :46 and the bridge dispatch; the second returns only the three definitions in tools.ts
One AgentBook writer, 25 declared types git grep -n -E "agentBookEvents\.append\(" -- src; sed -n "/^export type AgentBookEventType/,/;$/p" src/agentbook-types.ts | grep -c "^ | '" hooks/tool-execute.ts:154; 25
memory_events covers three memory channels git grep -n -E "emitEvent\('memory\." -- src memory.created, memory.deleted, memory.retention_cleanup
No validity time separate from record time git grep -n -i -E "valid_from|valid_to|as_of|observed_at|effective_" -- "src/schema/*.ts" "src/*schema*.ts" Nothing
No paper is cited git grep -n -i -E "arxiv|bibtex|@article|@misc|doi\.org" -- README.md docs Nothing
The service is named only in the hook git grep -n -i -E "living[-_ ]?mind|localhost:8008" src/hooks/system-transform-live-end.ts, src/hooks/system-transform-live-types.ts and four index rows in docs/SYSTEM_MAP.md
The server's context route has no dominant_emotion in hormones (in the living-mind-cortex checkout) git grep -n -E "dominant_emotion" -- api/agent_gateway.py agent_gateway.py:256 in the /state route, :345 and :406 as local reads; none in the hormones object at :378-384
Tree and suite size git ls-tree -r --name-only HEAD -- src | grep -c "\.ts$"; the same list piped through xargs cat | wc -l; ls test/*.test.ts | wc -l; cat test/*.test.ts | grep -c -E "\b(test|it)\(" 414 files and 57,379 lines; 212 test files, 1,782 call sites

History

2026-10-01 — 9c7cfb22… — audited at the same commit; scope_enforced withdrawn. The mark rested on the search path, which fails closed. LessonTriggerCache.refresh reads the same memories table with no project_id predicate (src/lesson-trigger-cache.ts:74-80), and the system-transform hook injects the result on every turn (src/hooks/system-transform.ts:40), so an injection read over the same store cannot carry the predicate. The search path is the near-miss (section 9). The zero-row retry to legacy is recorded beside it as a weaker point. Two marks remain, audit_log and negative_eval. Nothing installed, built or run.

2026-09-28 — 9c7cfb22… — audit at an unchanged pin; master had not moved. The headline criticism was wrong at this commit: since 09e42f98… (17 September 2026) the shared WHERE builder and both fallbacks exclude superseded and archived rows, with a populated test that becomes the negative_eval record. The gap that remains is the lesson-trigger cache, unscoped and unfiltered every turn (section 2). AgentBook declares 25 types, not 26, and eight of ten Recent Work entries are CSM's own tools, not seven of nine. memory_events covers three channels, not every mutation. Body anchors into the pre-split memory-manager.ts and fourteen other drifted lines were corrected. Marks unchanged. CSM_LIVING_MIND_URL is answered by route and field match (section 8). Re-screened: three auto-run surfaces, eleven floating ranges, AGENTS.md as data; nothing installed, built or run.

2026-09-18 — 9c7cfb22… — re-pinned from 4361d38; src/ and test/ moved, migrations/, plugins/ and hooks/ did not, and the tree was re-screened at the new pin. The three marks hold and both surviving anchors were stale: memory-manager.ts was split, with the memory_events insert now at memory-manager-base.ts:1057 after a 1,292-line extraction, and appendProjectScope moved to hybrid-search-sources.ts:39-57. The scope_enforced record gains the qualifications a reader needs: 1=0 is the fail-closed behaviour of project mode, one of three — global appends no clause at all and legacy widens to project_id = $n OR project_id IS NULL — and searchMemoriesOp moves between them by itself, retrying a zero-row project search as legacy and passing the widened mode into the cascade. The widening reaches unscoped rows rather than another project's, which is why the mark stands; it was there at the previous pin too, and this report did not say so. New here: src/memory-provenance-audit.ts counts how many memories carry all six required provenance fields, per field and separately over active rows, and distinguishes the unknown-model sentinel from the default — a store auditing the gaps in its own audit trail, printed by scripts/csm-audit.ts.

2026-09-11 — 4361d38d… — re-read, 189 files, 10,344 insertions and 4,568 deletions past the previous pin, arriving in one commit. Both central criticisms hold, and both are sharper. buildWhereClause is the one WHERE builder all three search lanes share and its whole body is still project, type, tags and importance — so supersession and archival remain invisible to search while agent-onboarding.ts filters archived_at IS NULL at two places, which is why the same store answers differently depending on the door. The belief tier's producer gap was described as three read paths; it is four, and the trap has a name: src/belief-promotion.ts is 400 lines, is wired, logs "Belief promotion: N promoted", and promotes memory_candidate_queue rows into memories without touching belief_knowledge_store. The memory_candidates finding is restated precisely — no INSERT exists anywhere in src/ against that table, and it is a different table from the live memory_candidate_queue, which two modules do fill. One substantial addition: lesson-auto-promotion.ts, a promoter gated on recall count, distinct-session count, a content-quality heuristic and a content-hash dedup, refusing named noise shapes by regex, tagging what it promotes auto_promoted "so they can be audited and reverted" — cross-session usefulness as the durability signal, which is a better rule than a score assigned at write time. Marks unchanged at three, each re-verified, and capability_evidence records added where the report had none. classifyValueClaim moved from value-source-guard.ts:44 to :45; the search and fallback line numbers still resolve. Suite 1,686 call sites across 189 files to 1,758 across 202, with the committed full-test-output.txt unchanged at 808. Screened before reading: three auto-run surfaces (.mcp.json, hooks/, hooks/hooks.json), eleven floating ranges behind a lockfile 31 days old, and an AGENTS.md read as data; nothing was installed or run.

2026-07-30 — 21d00969… — first reading.