Guarded consolidation

ctx

A file-based context system whose background consolidation writes only inside a sanctioned path scope, and whose test suite includes a regression corpus built from a published paper on LLM memory corruption.

Carries 2 of 7 rubric mechanisms. Most systems here carry none or one (41%), 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

ctx is an Apache-2.0 Go system of roughly 140,000 lines under internal/, positioned as "a system, not a prompt" — file-based context that any tool able to read files can consume, with a CLI, an MCP server, a VS Code extension, and a documentation site.

Two things in it are worth the atlas's attention, and both concern the moment memory is changed by a model rather than the moment it is written.

The consolidation pass writes inside a guarded path scope. ctx calls its background consolidation dream, and dream/guard.go decides where a dream may write:

"A target is allowed iff it resolves under dreams/ or ideas/ relative to projectRoot, OR under specs/ when the action is ActionPromote — the one sanctioned boundary crossing (deliberate declassification into a tracked spec)."

Every other background consolidator in this atlas can write wherever its code happens to point. CowAgent rewrites MEMORY.md nightly, ByteRover curates documents, Magic Context re-verifies and marks. None of them has a stated, enforced answer to which files may a background LLM pass modify, and the answer matters most for exactly the systems whose store is the user's own project tree.

Naming a single sanctioned crossing — promote, into specs/ — is the better half of the idea. A guard with no exit becomes a guard people disable; a guard with one named, disposition-gated exit stays enforceable.

The test suite carries a corruption corpus taken from the literature.

"corruptedFixture is a regression corpus modeled on the corrupted-artifact appendix of arXiv 2605.12978 ('Useful Memories Become Faulty When Continuously Updated by LLMs'): one well-formed proposal among several corrupted ones (an unknown status enum, stripped evidence, a dropped target). It guards the review/dedup gate against silently admitting a faulty artifact."

TestCorruptedArtifactGate feeds four proposals through the real reader and the schema gate and asserts that exactly one — the well-formed, provenance-bearing one — survives. This is the first system in the atlas that tests against a documented failure mode from published research rather than against its own happy path, and the failure mode chosen is precisely the one the atlas keeps warning about: memory degrading as an LLM repeatedly rewrites it.

Reservations. Correction is structural — entries fold into themed regions — with no trust state and nothing recording that a digested claim was later judged wrong. And the surface is very large for what it does, with 61,000 lines of CLI.

2. Mental Model

Diagram — digesting an entry into a themed region leaves it in staging rather than moving it, and every background proposal passes a provenance schema gate and a write-scope guard before reaching the resumable ledger
Diagram source
%% caption: digesting an entry into a themed region leaves it in staging rather than moving it, and every background proposal passes a provenance schema gate and a write-scope guard before reaching the resumable ledger
flowchart TB
    ST[("staging<br/><i>entries land here first, undigested</i>")]
    ST -->|"AddTheme folds a gist bullet into the root's<br/>Themes region, creating it if absent —<br/><b>nothing moves out of staging</b>"| RD[("root document<br/><i>Themes, then a region per theme;<br/>entries digested into place</i>")]

    SC["dream, background: scan"] --> PR["proposals"]
    PR --> SG{"schema gate<br/>provenance required"}
    SG -->|invalid| REJ["rejected, not admitted"]
    SG -->|valid| WG{"write-scope guard"}
    WG -->|"dreams/ · ideas/"| OK["allowed"]
    WG -->|"specs/"| PROM["only on promote"]
    WG -->|"anything else"| NO["refused, with a reason"]
    OK --> AP["apply"]
    PROM --> AP
    AP --> LG[("ledger, resumable")]

    style SG fill:#e7efe9,stroke:#3d6b59
    style WG fill:#e7efe9,stroke:#3d6b59

Two gates in series, both highlighted, and they check different things. The schema gate requires provenance before a proposal is admitted at all; the write-scope guard then decides where it may land, refusing with a reason rather than silently dropping. Naming a theme is deliberately separate from digesting entries into it — a theme can exist before anything has moved.

3. Architecture

Largest packages under internal/, in lines of Go excluding tests:

  • cli (61,118), config (21,044), write (13,084), err (8,183), mcp (5,145), journal (4,228), hub (2,605), assets (2,509), ctxctl (1,807), entity (1,600), disclosure (1,569), rc (1,483), trace (1,301), dream (1,278), drift (925).

The interesting logic is concentrated in the small packages: dream, disclosure, drift, journal, entity.

Diagram — the same two paths in one view — entries digested into themed regions, and background proposals gated on provenance and write scope
Diagram source
%% caption: the same two paths in one view — entries digested into themed regions, and background proposals gated on provenance and write scope
flowchart TD
  E["entries"] --> Stage["staging"]
  Theme["AddTheme:<br/>name a theme<br/>first"] --> Root["root doc<br/>## Themes"]
  Stage --> Dig["digest<br/>into region"]
  Dig --> Root
  Scan["dream scan"] --> Prop["proposals"]
  Prop --> Gate{"schema gate\nprovenance<br/>required"}
  Gate -->|invalid| Rej["rejected"]
  Gate -->|valid| WS{"write-scope guard"}
  WS -->|dreams/ ideas/| Apply["apply"]
  WS -->|specs/ on promote| Apply
  WS -->|elsewhere| Ref["refused<br/>with a<br/>reason"]
  Apply --> Led["ledger —<br/>resumable"]

4. Essential Implementation Paths

A write scope for the consolidator

WriteScope(projectRoot, target, action) returns a GuardDecision carrying "Allowed plus a registry-sourced refusal Reason", and errors only when a path cannot be resolved. Two details make it more than a path check.

Refusals carry a reason from a registry. A guard that returns a bare false produces an unexplainable failure; one that returns a registered reason produces a message an operator can act on, and a set of refusal categories that can be counted.

The exception is gated on the disposition, not the caller. specs/ opens only when the action is ActionPromote, described as "deliberate declassification into a tracked spec". Tying the boundary crossing to what the pass decided rather than to who is asking is the right axis: it means the guard cannot be widened by adding a privileged caller.

This is the governed write gateway pattern applied to the background path specifically, which is where this atlas has repeatedly found governance missing. A human write goes through review in several systems here; the nightly job goes wherever its code points in all of them but this one.

A corruption regression corpus

Most memory systems in this atlas are tested against their own intended behaviour. ctx is tested against a published account of how systems like it fail.

The fixture is four proposals: one clean, and three corrupted in ways the paper's appendix catalogues — an unknown status enum, stripped evidence, a dropped target. The assertion is not "the pipeline survives" but that exactly one proposal, clean-1, passes ProposalValid, and that the corrupted entries are "rejected, not silently admitted".

Silent admission is the failure that matters. A pipeline that rejects loudly is debuggable; one that accepts a proposal with stripped evidence produces a memory that looks well-formed and is not, and the structural-loss guard in ByteRover exists because the same thing happens during curation. ctx is the only system here that keeps a corpus of the failure and re-runs it.

Requiring provenance for survival is the other half — a proposal with its evidence stripped is rejected by construction rather than by a heuristic about its content.

Name the theme before digesting into it

AddTheme "declares a theme in a root: it folds the gist bullet into the root's ## Themes region, creating that region when the root has none", and the docstring is explicit that "Nothing moves out of staging — this is how a theme is named before any entries are digested into it."

Separating declaring a category from filing things into it prevents the failure that turned this atlas's own taxonomy into a list once: categories created implicitly, one per item, because filing and naming happened in the same step. Making theme creation a distinct, visible act with its own command puts a human decision in front of structure growth.

An append-only ledger in two formats at once

AppendLedger "appends one disposition to the append-only ledger at <dreamsDir>/ledger.md… Each entry is written as a Markdown list item whose payload is the JSON encoding of the LedgerEntry — human-readable as a list, and machine-parseable by ReadLedger. The ledger is never rewritten, only" appended.

Two properties, both cheap. It is genuinely append-only, so the record of what the dream decided cannot be edited by the next dream. And the same file is a document a person reads and a structure a program parses, which removes the usual choice between an audit log nobody opens and a changelog nothing can query. That, with resume_test.go and state.go, gives the recoverable background work and append-only memory audit shapes at once. Proposals are staged to disk with SafeWriteFileAtomic at secret permissions before the gate runs, so a crash mid-pass leaves the input intact.

5. Memory Data Model

Markdown in the project tree. Entries stage, themes are declared into a root's ## Themes region, entries digest into per-theme regions. disclosure/ carries regions.go, invariant.go, split.go, move.go, validate.go — region manipulation with invariants and a validator, tested by apply_invariants_test.go and apply_conserve_test.go.

A conservation test on an apply path is a good sign: it asks whether the rewrite lost anything, which is the same question ByteRover's detectStructuralLoss asks and which most rewrite paths in this atlas never ask.

What is absent:

  • No trust state on an entry.
  • No value-level tombstone. A digested claim later judged wrong is edited or folded away; nothing prevents the next dream proposing it again from the same staged material.
  • No explicit scope key beyond the project tree itself — scoping here is the filesystem, which is honest for a per-project tool and does not extend to multi-user use.

6. Retrieval Mechanics

There is no ranker. Retrieval is progressive disclosure over files: roots carry themes, themes carry regions, and any tool that can read files can walk them. That places ctx with GenericAgent and nanobot — the cheapest retrieval architecture in the atlas, which works while the corpus stays small and human-organized, and fails silently when a reader does not know a region exists.

The drift/ package exists to detect when the organized structure and the underlying reality have diverged, which is the mechanism that failure mode calls for.

7. Write Mechanics

Staged entries, an explicit digest step, and a dream pass that proposes rather than rewrites. Proposals are validated before application and applied within the guard. journal/ (4,228 lines) records the history alongside.

Notably, write/ is its own 13,000-line package with err/ at 8,183 — a system that treats writing files and reporting failures as first-class concerns rather than incidental ones, which matches a design whose store is the user's own repository.

8. Agent Integration

CLI, MCP server, a VS Code extension, a skills contract (CONTRIBUTING-SKILLS.md), and steering/hooks packages. The stated position is that it "works with any AI tool that can read files; no model or vendor lock-in", which is the same bet Basic Memory makes.

9. Reliability, Safety, and Trust

human_review is the mark this system is built around, and it runs on every proposal. dream never writes: it emits proposals into a gitignored dreams/ notebook and stops, under a type comment saying so — "The dream never acts on a proposal." Three CLI verbs dispose of one: accept applies the recommendation, reject records a refusal with no mutation, and amend substitutes an action the person chooses in place of the model's, each taking an optional human note. All three append to the ledger, and Decision is a closed four-value set — accepted | rejected | amended | skipped — declared as "the human's disposition recorded in the ledger". The amend path is the part most review surfaces in this atlas lack: the reviewer is not restricted to yes or no.

scope_enforced is not earned, and the guard is worth describing as what it is. WriteScope resolves a target relative to the project root and allows it only under dreams/ or ideas/, plus specs/ when the action is ActionPromote"the one sanctioned boundary crossing (deliberate declassification into a tracked spec)". That is a write-authorization boundary on a directory, not a stored key applied as a filter on a read, and the read path has no scope at all: progressive disclosure means any tool that can read files can read everything. The guard is a good mechanism aimed at a different risk — a background pass writing outside its sanctioned area — and it belongs in the prose rather than in the mark.

The rejection ledger is one property short of a tombstone, and the missing property is where the id comes from. Seen(entries, proposalID) reports whether the ledger already holds a disposition, PendingProposals drops anything it returns true for, and the docstring is explicit that this includes refusals: "Rejections count as seen, by design — the dream does not re-nag a rejected disposition unless the source content changes." So a refusal is durable and it is consulted before the next surfacing, which is more than most systems here manage. What it is keyed on is ProposalID, and no Go code produces one: proposals.go only unmarshals proposals.json, which the consolidation skill writes. The type comment asks for stability — "Stable so v2 supersession is not foreclosed" — and nothing verifies it or derives it from the content. The content-change half is handled separately and properly, by DeltaSelect comparing a per-path content hash, so a changed source file re-enters the scan. The gap is between those two: a rejected claim that reappears under a new proposal id, from a file that also changed, is offered again.

Strengths:

  • A write-scope guard on the consolidation pass, with one disposition-gated exception and registry-sourced refusal reasons.
  • A corruption regression corpus drawn from published research on LLM memory degradation, asserting rejection rather than survival.
  • Provenance required for a proposal to pass the gate.
  • Theme declaration separated from digestion, so structure grows by decision.
  • Conservation and invariant tests on the region-apply path.
  • An append-only ledger, never rewritten, readable as Markdown and parseable as JSON, with atomic staging at restrictive permissions.
  • A drift package aimed at the failure mode a ranker-free design has.

Gaps:

  • No trust state or tombstone; correction is structural editing, and the rejection record is keyed on an identifier the model supplies.
  • No ranker, so recall depends on the reader knowing where to look.
  • Very large surface, dominated by a 61,000-line CLI.
  • Filesystem-scoped, with no multi-user story and no scope key on any read.
  • The guard protects the project tree, not the memory's meaning — a dream may write a wrong thing inside dreams/ all it likes.

10. Tests, Evals, and Benchmarks

1,932 Test functions across 428 test files, over 209,370 lines of Go. Tests are dense in the small packages — disclosure/ alone has apply, invariant, conserve, convention, firstrun and abort tests, and dream/ has guard, ledger, resume, validate, proposals and the corrupted-corpus test.

Nothing was run for this review, and no retrieval-quality benchmark exists — consistent with a design that has no ranker to benchmark. The measurement that would matter here is whether progressive disclosure actually gets read: how often a relevant region exists and the agent never opens it. Nothing measures that, and it is the same unmeasured false-negative the atlas notes for GenericAgent.

11. For Your Own Build

Steal

  • Give the background consolidator a write scope, enforced by path, with refusals carrying a registered reason. A nightly LLM pass that can write anywhere in the user's repository is a risk most systems here simply run.
  • Gate the one sanctioned crossing on the disposition, not on the caller, so the boundary cannot be widened by adding a privileged path.
  • Build a regression corpus from published failure modes. Testing against documented ways systems like yours degrade is more useful than another happy-path fixture, and the paper appendices are already written.
  • Assert rejection, not survival. The interesting property is that a malformed artifact does not get admitted silently.
  • Require provenance for admission, so evidence-stripped material fails by construction.
  • Separate naming a category from filing into it, or categories multiply one-per-item.
  • Test conservation on any rewrite path — did the transformation lose anything?
  • Write the audit log in two formats at once. A Markdown list whose items are JSON payloads is readable by a person and parseable by a program, which is why it might actually get read.

Avoid

  • Structural correction without a tombstone, in a system whose dream pass re-reads staged material.
  • No ranker and no measurement of whether disclosure is reached.
  • Surface area far exceeding the mechanisms that make it distinctive.

Fit

Borrow:

  • WriteScope and its disposition-gated exception, which is a small function with an outsized safety return.
  • The corrupted-artifact corpus idea, which any system with an LLM rewrite path can adopt in an afternoon.
  • Theme-before-digest.

Do not copy:

  • Progressive disclosure as the whole retrieval story unless the corpus stays small and curated.
  • Region folding as the correction story if a background pass can re-propose.

12. Open Questions

  • What stops a dream re-proposing something a human folded away?
  • Is the write-scope guard applied to every write path, or only to dream apply?
  • How often does an agent fail to open a region that would have answered it?
  • Does drift detect divergence between themes and reality, or only within the documents?
  • Are the refusal reasons counted anywhere, or only shown?

Appendix: File Index

  • Write-scope guard: internal/dream/guard.go (WriteScope, GuardDecision, ActionPromote), guard_helpers.go.
  • Corruption corpus: internal/dream/corrupted_test.go (TestCorruptedArtifactGate, testdata/corrupted-2605.12978.json).
  • Proposal lifecycle: internal/dream/proposals.go, validate.go, ledger.go, state.go, scan.go, apply.go.
  • Disclosure and regions: internal/disclosure/addtheme.go, regions.go, invariant.go, split.go, move.go, validate.go.
  • Human disposition: internal/cli/dream/core/dispose/dispose.go (Accept :30, Reject :55, Amend :82), internal/cli/dream/core/review/review.go, internal/config/dream/types.go:85-100 (the four decisions).
  • Dedup-against-seen: internal/dream/ledger.go:91-111 (Seen), proposals.go:98-122 (PendingProposals), state.go:106-122 (DeltaSelect, the per-path content hash).
  • Drift detection: internal/drift/.
  • History: internal/journal/, with parser/codex_convert.go and codex_path.go reading Codex sessions.

Appendix: Recorded Searches

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

Claim Command Result at this pin
No scope key on any read path grep -rniE "scope|tenant|user_id|namespace|owner" --include="*.go" internal/disclosure internal/dream Every hit is the write guard or a comment; no read filters on a stored key
Nothing in Go produces a proposal id grep -rn "Proposal{" --include="*.go" internal cmd and read internal/dream/proposals.go Only zero-value literals in error returns; the run's proposals are unmarshalled from proposals.json
A rejection is consulted before re-surfacing read Seen at internal/dream/ledger.go:104 and PendingProposals at proposals.go:113 PendingProposals drops any proposal with a ledger entry, rejections included
The ledger is append-only read AppendLedger at internal/dream/ledger.go:104 Opens for append, writes one JSON line per disposition, never rewrites
The memory mechanism did not move git diff --stat <prev-pin>..HEAD -- internal/dream Empty
Tree and suite size find . -name "*.go" | xargs wc -l | tail -1; grep -rh "^func Test" --include="*_test.go" . | wc -l 209,370 lines; 1,932 test functions across 428 files

History

2026-09-1143d0ba7c… — re-read, 273 files and 29,287 insertions past the previous pin in a single commit — and internal/dream/, the package this report is about, is byte-identical. The growth is product surface: a Codex session parser in internal/journal/parser/, a Tauri desktop app, a VS Code extension, and three new skill assets. Both mark changes are first-reading corrections. scope_enforced is withdrawn: it rested on WriteScope, which authorises a write against a directory allowlist, and the atlas's mark is a stored key applied as a filter on a read — of which there is none here, since progressive disclosure is any tool reading files. human_review is added, and it should have been there at the first reading: dream emits proposals and never acts on them, and accept, reject and amend are three CLI verbs a person runs against one proposal id, with amend substituting the person's action for the model's recommendation and every disposition landing in the ledger. audit_log holds on that same ledger, unchanged. The rejection record is described precisely for the first time: it is durable and it is consulted — PendingProposals drops anything Seen returns true for, rejections included — but it is keyed on a ProposalID that no Go code produces, because the consolidation skill writes proposals.json; the content-change half is handled separately by DeltaSelect over a per-path hash. That is one property short of tombstone, and the missing property is named. Suite 1,932 test functions across 428 files over 209,370 lines of Go. Screened before reading: three context-injection surfaces, three build-time exec paths, eleven dependency manifests inside the cooldown; nothing was built or run.

2026-07-28ce5a8328… — first reading.