Skip to content

agentm auto organization

github-actions[bot] edited this page Oct 3, 2026 · 8 revisions

Note

Launched 2026-07-19; mostly retired since. Still running: the lint engine and /memory lint (a report in the nightly cycle, its repair behind --apply), lint's graph-snapshot cross-check, the write-time fingerprint guard, the two meters, and the stepped curve in shadow mode. The Document History records every retirement; the sections below keep the design as built.

AgentM Auto-Organization

Context

Objective

The vault accumulates cruft as it grows: cold notes crowd the browse surface, most notes carry no links, and nothing cleans up on its own. This design makes the vault maintain itself — work artifacts tuck away at project close or after a year unused, memories fade on a stepped schedule and reach the archive only after five years of silence, new notes arrive linked to their neighbors, and a lint pass repairs the small rot. A shelved artifact stays in everyday search but leaves your browse eyeline; an archived memory answers only an explicit archive search; nothing is ever deleted, and none of it waits on your confirmation. We define the signals, the three actions, the machinery that runs them, and the meters that prove the vault is getting tidier.

Background

You asked for this directly: cut down the cruft, and archive older notes so they are there when needed but out of the way when you browse agentm's memory. Today the engine scores every volatile note's decay, but that score only lowers the note's rank in search — the browse surface never gets cleaner. The baselines that motivated this design said the rest: 13.8% of notes carried an organic link, and three whole kinds held 880 notes with no links at all. (Connectivity now reads 30.0% — see the meters section.)

Every mechanism this design needs already runs. The lifecycle layer already stamps every note's last genuine recall — the clock every schedule here reads. Dreaming walks the whole corpus weekly. It uses staged proposals, a revert log, and an auto-apply lane you ruled on for compression. The archive convention exists at every tier. Recall skips _archive/ by default. The --include-archive flag reopens it. The capture design is on the same arc. It hands newly ingested batches to the linker.

The new work adds three focused pieces. These are an archive stage for dreaming, a write-time linker, and a lint engine. Each piece rides the shipped machinery. Building a second corpus walker duplicates the staging, confirmation, and revert plumbing of dreaming. Everything scheduled here runs as a dreaming stage.

This design is the memory-system design's own lifecycle tail, built out. That design names decay-to-archive and self-healing lint as designed-for; this child design is where they become real. It stands alone because the work spans two parents — the lifecycle belongs to the memory system, the weekly stages ride dreaming in the experience design — and because three plan-sized builds deserve their own document rather than a heavier amendment log. At landing, both parent designs gain an amendment pointing here.

Design

Overview

Auto-organization runs three actions over the vault, and this design is organized the same way. First, linking at write time: a new note arrives already connected — the engine finds its nearest neighbors and adds the links before the note settles in. Second, tidying on age: work artifacts leave the eyeline at project close or after a year of sitting unused, and memories fade on a stepped schedule until, after five years of silence, they reach the archive — where only an explicit archive search finds them. Third, lint: a pass that finds orphans, collapses duplicate notes, repairs broken links, discovers missing links between new content and the older notes it relates to, flags contradictions, and scores note quality — fixing and adding the safe things itself and surfacing the rest. Links are only ever added; the single removal case is a link that is already broken.

The machinery is the existing weekly dreaming pass — which also absorbs the inbox triage that used to wait for your command — plus one small write-time hook. Nothing is ever deleted: shelf and archive both mean move, every move auto-applies with a revert-log entry and a digest line, and two meters — organic connectivity and the browse-surface counts — tell you whether the vault is actually getting tidier. A sampled audit, a global mutation budget, and an anomaly breaker watch the automation itself.

Infrastructure

Everything runs on the existing dreaming pipeline, the runner, and the save path. The new components are:

Component What it is
write-time linker a post-save step: finds related notes via the vector index, adds wikilinks to the new note
tidying stage a new dreaming stage: shelves artifacts unused for a year, archives memories silent for five
lint engine orphan, broken-link, contradiction, and quality checks; runs on demand and weekly
link graph snapshot a derived, device-local copy of the vault's typed-edge graph, rebuilt incrementally each cycle
dedup guard a pre-write check: an arriving note that matches an existing one reinforces it instead of duplicating it
automation guards the sampled higher-tier audit, the global mutation budget, and the anomaly breaker
backfill batches link additions for today's unlinked notes, capped per cycle
Trigger What runs
every save, and every ingest batch the write-time linker
the weekly dreaming cycle inbox triage, the tidying stage, dedup, lint, and the backfill batches
every write the dedup guard
/memory lint the lint engine, on demand

The system makes four guarantees. Every shelf, archive, and link move auto-applies, lands in the revert log, and shows in the digest — none of it waits on you. The linker's additions are additive and capped. Judgment calls — contradictions, repairs that are not clearly safe — are surfaced, never acted on. A crashed cycle resumes where it left off.

As of agentm-vault plan 04, the first two guarantees describe retired machinery: no shelf, archive or link move auto-applies from the dream cycle, which reports and proposes and changes no note. The third holds, and now covers every finding the cycle makes.

Detailed Design

The signals

Two clocks and two shipped signals drive every action. The lifecycle layer stamps a note's last genuine recall; this design retunes its rank curve from the original 30-day half-life to your stepped, years-long schedule — full strength for six months of silence, half to a year, an eighth to three years, a sixteenth to five. The retune ships shadow-first: for the first cycles both curves compute and the rank deltas log, and the new curve takes over only after the diff reads sane and the pinned retrieval eval holds. The same touch clock extends to artifacts, where any injection into a conversation counts as a touch. The fingerprint field every entry already carries supplies exact-duplicate detection, and the vector index supplies the near-duplicate signal. Durable notes, failure incidents, and decision paths are exempt, and always-load or pinned notes are never candidates for anything here. The vector index supplies nearest neighbors by similarity. The typed-edge graph supplies the link picture, persisted as a derived, device-local snapshot so cycle queries cost nothing. It shows who points at whom and who is an orphan.

Linking at write time

Retired 2026-08-10 with the vector stack; see the Document History. The notes below are the record of what was built.

Task 3 of 7 (the write-time linker itself) shipped 2026-07-18 — see the as-built note after the paragraph below for how its timing actually works. Tasks 4, 5, 6, and 7 (the weekly link-improvement sweep, the ingest-batch handoff, the backfill, and the connectivity meter) have since shipped too, each with its own as-built note. All 7 tasks of this part are now shipped — see the Document History entry below for the part-2 landing.

When a note is saved, the linker asks the index for its nearest neighbors. It keeps the ones above a similarity floor. It adds up to three wikilinks to the new note under a short "Related" line. It runs after the save returns. It works the same way the embedding queue does, so saving stays fast. It edits only the note that just arrived. A reciprocal link on an older note is batched into the weekly cycle, capped and revert-logged. Ingest batches from the capture design arrive internally linked already. The linker adds their outward connections. The similarity floor and the three-link cap are calibration defaults. They are named in code and tuned from the connectivity meter.

As built (task 3 — the write-time linker itself): the linker is real and shipped, but "it runs after the save returns" is truer, and reads differently, than it first sounds. It is not synchronous on the save call's own return path. A version that queried the index and embedded the new note inline in save.py was tried first and caused a confirmed regression: it made every single save trigger a real BGE model load, and — on this machine — a live HuggingFace Hub network round-trip even with the model already cached. A follow-up fix forcing HF_HUB_OFFLINE=1 was tried and also confirmed broken (offline mode fails to resolve an already-cached model from disk on this machine's sentence-transformers/huggingface_hub version, a known revision-resolution quirk). The shipped mechanism instead defers the actual linking to the next drain of the embedding queue: save.py's post-save hook is now just vec_index.enqueue(vault, rel_path, "upsert", text=embed_text, link=True) — one new link=True flag on the same record the embedding queue already writes. drain_queue() applies write_time_linker.apply() right after computing that record's embedding for the ordinary vec-index upsert, reusing the same embedding rather than a second, save-time-only embed call. So the sentence above — "it works the same way the embedding queue does, so saving stays fast" — is honored exactly, and more literally than it first reads: the save call itself never touches the network or loads a model, but a newly saved note's "Related" links appear on the next drain cycle (the idle-time hook, a manual python3 vec_index.py drain, or a future /memory reindex) rather than before save_entry() returns. Applying the link also nudges task 2's persisted graph snapshot for just that one note (graph_snapshot.rebuild(vault, paths=[rel_path])), so the graph doesn't go stale waiting on the weekly full rebuild. The reciprocal-link case shipped with task 4 (its own as-built note appears further below), and the ingest-batch handoff shipped with task 5 (as-built note just below this one). The backfill (described below) shipped with task 6 — see its own as-built note just after that paragraph. The connectivity meter (task 7) shipped too — see its own as-built note under "The meters" section below.

As built (task 5 — the ingest-batch handoff): shipped exactly as the sentence above already described — the only task in this part that landed with no divergence from its own design text. A new write_time_linker.same_ingest_batch(slug_a, slug_b) matches ingest.py's deterministic <doc-slug>-chunk-<i> slug formula: sibling chunks of the same document, and a chunk against its own parent document, both count as the same batch; everything else doesn't. Both linking surfaces call it — task 3's apply() at write time, and task 4's _stage_link_improvement() in the weekly sweep — skipping same-batch neighbors before they ever become a proposed link, since a batch already carries its reading-order nav and document backlink from ingest itself. One companion fix turned out to be necessary alongside the filter, not named in the design text: querying the vector index at exactly the three-link cap returned nothing but a chunk's own near-identical siblings, crowding every outward candidate out of the result window before the filter ever got a chance to run. Both surfaces now query with _NEIGHBOR_QUERY_HEADROOM (8) extra results beyond the cap — cheap on a sqlite-vec top-k query — so an outward candidate stays visible in the window the filter works over. Verified against a real ingest of capture part 2's own fixture (scripts/fixtures/ingest/sample-article.md, via ingest.ingest()) plus a planted older related note, asserting outward links land in both directions and zero intra-batch Related links appear (scripts/test_dream.py::IngestBatchHandoffTests).

The backfill clears the existing debt. Currently, 880 notes across three kinds carry no links at all. Each weekly cycle links a capped batch of them. The batch limit is 25 per cycle, matching compression's cap. This continues until the pool drains.

As built (task 6 — the backfill): shipped folded into the same _stage_link_improvement() rather than as a separate stage — the design's text above describes the outcome, not a mechanism, and a second stage would have duplicated the per-origin link flow task 4 already built. After the changed-notes loop runs, the stage pulls graph_snapshot.orphans(vault_path) (zero edges in either direction, freshly accurate because this same stage rebuilds the snapshot in full at its own start) and runs up to _LINK_BACKFILL_BATCH_CAP (25, matching dream_confirm.py's existing compression cap, as the plan specifies) of them through _process_origin() — the identical exemption-gate, embed, neighbor-query, both-direction-merge flow task 4's changed-notes loop already uses, now a shared closure both loops call. Backfill proposals carry the same stage="link_improvement" as any other candidate — so AUTO_APPLY_STAGES and the revert log treat them identically — but kind="backfill" instead of kind="link", a label that costs nothing structurally and exists purely so the digest can tell the two apart. The two batch caps are independent: a cycle can propose up to 25 changed-note links and up to 25 backfill links, not 25 shared between them. A persisted attempted-set (_meta/link-backfill-state.json, alongside task 4's own sweep cursor) rotates the pool across cycles: an orphan that found no qualifying neighbor this cycle is skipped on the next one rather than re-occupying one of the 25 batch slots every time, which would otherwise starve the rest of the pool from ever getting a turn. Once every orphan has had a turn, the pass resets and previously-unmatched notes get a fresh chance — some of them may qualify by then, since newer content keeps arriving. A note leaves the pool the ordinary way: once it picks up a link (as a backfill origin, or as some other note's neighbor), it's no longer an orphan and graph_snapshot.orphans() stops returning it. Verified with a 30-orphan fixture split into two mutually-similar-but-orthogonal groups of 25 and 5 (so the carry-over set is exact, not just approximately right): cycle 1 processes exactly the first 25, the remaining 5 carry to cycle 2, and re-running a cycle against notes already linked in a prior one doesn't reprocess them (scripts/test_dream.py::LinkBackfillTests).

Deduplication — reinforce, never repeat

Current state: the write-time fingerprint guard still runs. The cluster-aware weekly pass and the needs-your-eye surface retired on 2026-09-06, the suffix-backlog drain gave way to the dreaming binary's copies job on 2026-09-05, and since agentm-vault plan 04 the dream cycle's own dedup lists possible twins on the needs-review map for you to merge by hand.

The inbox shows the problem plainly: whole families of near-identical memories suffixed _1, _2, and up. They exist because the write path handles a name collision by suffixing instead of asking whether the content already exists, and because the two shipped cleanup mechanisms each have a hole — triage's merge works on pairs and only runs when you invoke it, and dreaming's dedup stage skips the inbox and still waits on confirmation. This design closes all three gaps by extending what exists rather than adding a third mechanism.

Prevention comes first. The dedup guard runs before any note is written: an exact fingerprint match or a strong near-match against the index means the arriving note reinforces the existing one — its occurrence count and updated stamp bump, and no new file appears. A suffix now only ever means genuinely different content sharing a title.

As built (task 2 — the write-time dedup guard): the guard shipped EXACT-fingerprint-match only; the "or a strong near-match against the index" half of the paragraph above was deliberately not built at write time. The plan's own Locked design call — fingerprint-exact collapses are deterministic, fuzzy merges need a model verdict, no exception either direction — rules out a synchronous near-match auto-reinforce at save time: it would discard the arriving note's real differences without the verdict the locked call requires, and computing the embedding a near-match needs would reintroduce the exact synchronous-model-load-per-save regression part 2's own as-built note already records fixing (see "Linking at write time" above). Near-duplicates write normally today; they are the weekly cluster pass's (task 3, upcoming) to own — this paragraph's "strong near-match" clause is designed-for at that layer, not this one.

The guard (harness/skills/memory/scripts/dedup_guard.py) hooks two write paths. save_entry() looks up entry_meta.fingerprint in the vec-index DB (task 1's column), then re-verifies the match against the candidate file's current live body before reinforcing — a stale index row, which lags the next drain by design, must never cause a reinforce against content that has since changed, so a match is never trusted from a stored value alone. capture() runs a direct frontmatter scan of personal/_inbox/ (small by design; the index never carries inbox candidates). Both hold vault_mutex across the find-then-reinforce pair, so two concurrent identical writes can't both slip past the guard.

An adversarial review caught two gaps in an early version's match-target filtering. First, only a note carrying status: active (vault) or status: inbox (inbox candidates) is a legitimate reinforce target — a triage-expired tombstone, a deleted/superseded note, or a curated personal/_always-load/ standing rule never absorbs a duplicate. The first version checked this only on the arriving note, so a re-capture of an already-expired thought would have reinforced its own tombstone and never re-entered triage, breaking capture()'s "a capture is never silently dropped" contract. Second, capture()'s reinforce path now refuses the match outright when the arriving capture carries source_url or instructions the matched candidate lacks — a link resend deduping into an earlier plain-text capture would otherwise silently drop the ingest-sweep trigger those fields carry.

reinforce() is the one mutation: it bumps the matched note's occurrences count (absent = 1, so a first reinforce sets 2) and stamps updated with today's date, patched in place — no new file, no body rewrite. occurrences joined save.FRONTMATTER_FIELD_ORDER as a new optional field, patch-only: an ordinary write never emits it, only a reinforce ever does. save_entry()'s existing same-slug-collision path is untouched — the guard sits upstream of it, and a genuine slug collision with different content still raises FileExistsError exactly as before.

The document-ingest path (ingest()) short-circuits on a whole-document dedup: a duplicate document returns the existing note with deduplicated=True and writes zero chunks, rather than writing a chunk family that would dangle-link to a document note that never materialized (the bug an earlier version had). Chunk-level dedup within a document threads the same dedup_info signal through, so a mid-sequence rollback never unlinks a note the guard had already reinforced.

Both MCP tools are truthful about it now — an earlier version was not, review-caught. memory_append reports the real deduplicated flag and, on a reinforce, the existing note's own slug rather than the phantom slug the request asked for; memory_capture surfaces CaptureResult.deduplicated the same way. See Memory MCP tools for the response shape.

The cure runs weekly, inside dreaming, as one pass. Inbox triage folds into the cycle as a stage: it stops waiting for your command, and the inbox joins the same walk as the rest of the corpus. The two already share their staging and merge machinery underneath — this fold closes a historical split, and /memory inbox stays as the on-demand door into the same engine, alongside /memory lint. Dedup itself becomes cluster-aware: a whole suffix family collapses into the canonical earliest note in one disposition. A collapse is free when fingerprints match exactly — a suffix family is one note repeated. A fuzzy merge, similar but not identical, needs a cheap-model verdict before it applies, and a verdict the judge is unsure of joins the needs-your-eye list. Every collapse keeps the surviving note's content, marks the copies superseded — they then follow the normal tidying lanes — and auto-applies, capped, revert-logged, and digest-reported. Dreaming's corpus dedup was confirm-gated by its own standing ruling; approving this design is the fresh ruling that extends auto-apply to it, under these bounds.

As built (task 3 — the cluster-aware weekly pass): shipped as a two-pass scan inside _build_merge_and_promote_clusters, which now returns a ClusterScan dataclass (collapse_proposals, merge_proposals, promote_clusters, needs_your_eye) rather than the old 2-tuple. One boundary matters here that this paragraph's own opening sentence doesn't yet have: "inside dreaming" is task 4's job, shipped separately — see its own as-built note below — this task only rebuilt inbox_triage.py's own engine, which at the time this task shipped still ran exactly where it always had, on demand via /memory inbox or the CLI, not yet folded into run_dream()'s weekly walk. Pass 1 buckets the inbox pool by dedup_guard.live_content_fingerprint — the same live-recomputed hash the write-time guard (task 2) matches on — and a bucket of two or more is a suffix family, content-identical modulo formatting. The whole family collapses into the canonical EARLIEST note (by created frontmatter, path-order tiebreak for the legacy backlog shape with no created at all) in one disposition, a new inbox_collapse stage (kind="collapse"): every copy is marked status: superseded + supersedes: <canonical> — never deleted, so part 1's tidying lanes still pick the copies up later — and the survivor's own content is left untouched, since a copy carries nothing new to absorb. inbox_collapse joined ALL_AUTO_APPLY_STAGES alongside inbox_promote/inbox_merge, on the plan's own Locked design call ("Fingerprint-exact collapses are deterministic; fuzzy merges need a model verdict — no exception either direction"): membership in an exact family is a content-hash fact, not a judgment call, so it needs no verdict to auto-apply.

Pass 2 runs dream._stage_dedup (the same difflib comparison and threshold as always) over whatever the inbox pool has left once exact families are claimed. A hub matched by two or more others still becomes a PROMOTE cluster, untouched by this task — promotion is triage's recurrence mechanic, not dedup. A hub matched by exactly one other is now a FUZZY merge candidate, and this is where this section's "needs a cheap-model verdict" clause actually lands: a new judge_fuzzy_merge(hub_body, other_body) seam returns "yes" / "no" / "unsure", gated behind dream.cheap_model_tier_available() — the same seam part 2's link-improvement sweep (task 4) introduced, and it still always returns False today, so judge_fuzzy_merge is unreachable in production and raises rather than guessing if it's ever called without the tier. "Yes" emits dream's own merge-into-earlier / mark-later-superseded mutations verbatim; "no" leaves both notes exactly as they are; "unsure" — or the tier being unavailable, which is every run today — routes the pair to the needs-your-eye list instead. The behavior this replaces is gone on purpose: before this task, a pairwise difflib match auto-applied as a merge with no verdict at all; that path no longer exists, per the plan's Locked design call ruling out any exception either direction.

Separately, the occurrence-promote threshold now compares against max(mining_occurrences, occurrences) rather than mining_occurrences alone, unifying reflect's own miner counter with task 2's write-time reinforce counter — a note reinforced enough times via the write-time guard now promotes exactly as reliably as one reflect mined repeatedly.

Questionable cases stay put. A pair or family in the ambiguous band was left in the inbox untouched and flagged instead: a needs-your-eye list on the console, a line in the digest, and a count in the morning brief. All four — the list and its three surfaces — retired with the inbox triage engine on 2026-09-06, when the directory they read stopped existing. Nothing detects the ambiguous band today; the copies job takes content-identical families and the dedup stage proposes a merge rather than parking a pair for you.

As built (task 3 — the needs-your-eye substrate): this task shipped the underlying list only, not the three surfaces this paragraph names. InboxTriageDigest.needs_your_eye carries each cycle's ambiguous pairs, a new digest section ("Needs your eye (ambiguous — nothing applied, nothing forced)") renders them, and _meta/needs-your-eye.json persists them, overwritten every run so a resolved pair just stops reappearing rather than needing an explicit clear. The console surface and the morning-brief count this paragraph promises shipped separately as task 5 — see its own as-built note just below. A flagged pair's paths are claimed the moment it lands on the list, exempting it from that same run's occurrence-based promote and expire passes — an ambiguous pair sitting right at its TTL boundary can't expire in the very cycle that flagged it.

As built (task 5 of 9 — the console and morning-brief surfaces): the two remaining surfaces this paragraph names, completing the three the plan's own text promised. harness/skills/console/scripts/console.py gains section_needs_your_eye(vault), an honest-dark section reading the identical _meta/needs-your-eye.json task 3 already writes: dark (no file yet — the weekly cycle hasn't run on this machine since the list shipped), "nothing" (an empty list — every candidate resolved), or a bulleted list of ambiguous pairs by file name and reason. It joins the console's report dict ("needs_your_eye" key) and the terminal render order, right after Dreaming. scripts/health/session_brief.py gains count_needs_your_eye(vault), reading the same file, folded into build_brief()'s parked_clause — a clause like "· 1 dedup candidate needs your eye" appends after the existing parked-runs clause — and into the anti-fatigue signature string, so a count change alone re-surfaces the SessionStart line even when nothing else about the brief changed. Both readers are best-effort: a missing or malformed file returns zero/dark rather than raising, the same honest-dark convention every other console section and brief signal already follows. This also resolves the Autonomy design's own forward reference — its Session-start line bullet named the needs-your-eye count as reading only "the observability layer's own live signal... until the auto-organization needs-your-eye list ships and widens it"; the brief's count now reads both signals side by side (parked runs, then dedup candidates), additive rather than a replacement — see that design's own amendment log for the matching entry. Tests (task 5): scripts/test_needs_your_eye_surface.py, 3 new — a fixture ambiguous pair appears on all three surfaces (digest, console, brief count) after one triage cycle; resolving it (editing one note apart) clears all three on the next cycle; the console section renders honest-dark before any cycle has run.

The backlog drains like the link debt does: each cycle collapses a capped batch of the existing suffix families until none remain.

As built (task 3 — one scope boundary): pass 1 today collapses every qualifying exact family it finds in the inbox pool in a single run, with no batch cap of its own — the capped, cursor-tracked drain this paragraph describes is task 6's job, still pending, not something this task already does by having no cap today.

As built (task 6 — the suffix-backlog drain): shipped as dream._stage_suffix_backlog_drain(vault_path, entries, loaded) (a new _SUFFIX_BACKLOG_BATCH_CAP = 25 constant alongside it, matching the cap every other batched stage in this design uses), wired into run_dream()'s proposal list right after the link-improvement stage. It answers task 3's own note just above: the capped, drain-until-none-remain behavior that paragraph named as task 6's still-pending job is now real, scoped to the population task 3's own inbox pass never covers — active notes anywhere else in the vault that were already _1/_2 duplicated before task 2's write-time dedup guard existed to stop new ones from forming. The stage groups every status: active entry outside personal/_always-load/ (curated notes are never a collapse candidate here, same as everywhere else in this design) by dedup_guard.live_content_fingerprint — the identical live-recomputed hash the write-time guard and task 3's inbox pass already match candidates against, so a suffix family here means the same thing it means everywhere else in this design: content-identical modulo formatting. A bucket of two or more is a family. Each family inside a cycle's capped batch collapses into the canonical EARLIEST note (by created frontmatter, path-order tiebreak), and every other copy is marked status: superseded plus supersedes: <canonical path> — never deleted, and the survivor's own content is left untouched, exactly task 3's own inbox-collapse shape. suffix_backlog_drain joined AUTO_APPLY_STAGES (dream_confirm.py) on task 3's own already-approved basis for inbox_collapse: fingerprint-exact family membership is a content-hash fact, not a judgment call, so no fresh ruling was needed to let it auto-apply. One divergence from this paragraph's own wording worth naming: no persisted cursor tracks progress across cycles, unlike the link-backfill's own attempted-set (see the Linking-at-write-time section's task-6 note above) — a collapsed family's copies flip to superseded and drop out of the very grouping (status: active) the next cycle scans, so the backlog drains itself with no second piece of state to maintain. Verified by 3 tests in scripts/test_dream.py::SuffixBacklogDrainTests: an exact-cap collapse carries its remainder to a later cycle without reprocessing already-collapsed families; the survivor is always the earliest note and is left unmutated; and an _always-load note is never a candidate.

Tests (task 3): 8 new (ClusterAwareDedupTests, scripts/test_inbox_triage.py) — an exact family of 4 collapsing to 1 with copies verified still present on disk; a confident-yes fuzzy verdict collapsing; an unsure verdict landing on needs-your-eye; a confident-no verdict keeping both notes; a tier-unavailable pair landing on needs-your-eye with an assertion the judge is never called; the same-run expiry exemption; and the needs-your-eye state-file write-and-clear-on-resolve round trip. 5 pre-existing tests updated intent-preservingly.

As built (task 4 of 9 — the inbox-triage fold): shipped as a sibling sub-run, not a stage literally spliced into run_dream()'s own sequence. run_dream() stays exactly what its own contract already promised — propose-only, untouched by this task — because folding triage in as one more _stage_*() call would have made the propose-only entry point itself apply mutations, the one thing it's never supposed to do. Instead, dream.run_dream_and_auto_apply() — the wrapper dream.py's CLI and the weekly dream.yaml job actually invoke, not run_dream() directly — now also calls inbox_triage.run_inbox_triage_and_auto_apply() right alongside dreaming's own run, each with its own run_id, staging directory, and digest, but sharing the one revert_log instance dreaming's own run already opened, so a single cycle's undo surface covers both. A new include_inbox_triage: bool = True keyword lets a caller (today, just the test suite) run the wrapper without the fold; the fold itself is best-effort — a triage failure is caught and logged, never allowed to take the rest of the cycle down with it. DreamDigest.inbox_triage_run carries a small summary dict (run_id, proposals, auto_applied, needs_your_eye, digest_path) and renders as one line in dream's own digest header ("Inbox triage (folded into this cycle): ..."), so a cycle's digest names both runs without merging their content together. /memory inbox — the CLI verb — is untouched; it still opens the identical inbox_triage engine on demand, verified by a parity test that runs the same fixture both ways (weekly fold vs. standalone invocation) and asserts identical dispositions. Tests (task 4): 4 new (TriageFoldsIntoDreamingTests — a cycle processes the inbox without an explicit invocation, CLI parity against the weekly fold, run_dream() alone stays propose-only, the fold-disable kwarg), plus 1 pre-existing dream-job test updated intent-preservingly (a weekly cycle now stages two runs, not one, so the test selects dream's own digest by its header rather than assuming it's alone in the batch).

Linking also improves over time, cheaply. Each weekly cycle, a link-improvement sweep takes the notes that arrived or changed since the last cycle and asks two free questions of the graph snapshot and the vector index: which older notes sit near this one, and which clusters does it touch. Clear candidates get their links added directly — both directions, capped, revert-logged. Ambiguous candidates — similarity in the middle band — go to a cheap model for a yes or no, under a small per-cycle budget declared in the job manifest. When the model tier is unavailable or the budget is spent, the sweep keeps the deterministic links and moves on. Links are only ever added; the single removal case belongs to lint, and it is a link whose target is gone.

As built (task 4 — the weekly link-improvement sweep): the sweep is real and shipped, but the cheap-model band this paragraph names for the ambiguous middle band was never built. Research done before this task started confirmed no synchronous "ask a cheap model X, get yes or no back, capped by a budget" primitive exists anywhere in this codebase — the job manifest's budget_tokens field is parsed and never consumed — and every prior instance of this exact problem in this codebase (a cron or hook context needing LLM judgment: adapt_skills.py's deterministic-then-sub-agent split, orchestration_idle.py's explicit note that a hook fires outside the agent loop and cannot dispatch one, the first forward pass's deliberately deterministic-only v1, since retired) resolved it by staying deterministic rather than building a new primitive. With the operator's sign-off, the sweep ships deterministic-only: a new dream.cheap_model_tier_available() function (always returns False) is the explicit, testable seam a future build wires a real budget-and-call primitive to. Until then, every ambiguous-band candidate — similarity between write_time_linker.LINK_SIMILARITY_FLOOR (0.70) and CONFIDENT_SIMILARITY_THRESHOLD (0.85) — falls through to "left unlinked," exactly the outcome this paragraph already names for "budget exhausted or tier unavailable," just unconditionally today rather than conditionally on a budget that has nothing to spend from yet.

link_improvement also joined AUTO_APPLY_STAGES alongside compression and tidying (dream_confirm.py), auto-applying without an operator confirm() call. That rests on this same design's own already-approved words above — "Every shelf, archive, and link move auto-applies… none of it waits on you" — together with the plan's own Locked design call, "Additive only. No link removal happens in this part, ever." Both together are the fresh, separate operator ruling AUTO_APPLY_STAGES's own docstring requires before a new stage joins it.

The sweep runs on its own dedicated cursor (_read_link_sweep_cursor / _write_link_sweep_cursor, backed by _meta/link-sweep-cursor.json), deliberately not task 2's graph_snapshot.rebuild() touched_paths. An adversarial review caught a real bug in an earlier version that reused that signal: the write-time linker (task 3) already calls graph_snapshot.rebuild(vault, paths=[rel_path]) for every note it links, so any note that had already picked up an ordinary write-time link — the common case, and precisely the notes most likely to have qualifying neighbors — had its "changed" signal silently consumed before the weekly sweep ever ran. The dedicated cursor fixed it.

Clear candidates apply through write_time_linker.merge_related_slugs(), a new pure helper shared with task 3's apply(). It is fence-aware (it will not mutate a **Related:**-shaped line sitting inside a fenced code example) and last-match-wins (it will not silently drop one of two duplicate Related lines) — both bugs an adversarial review caught and fixed before this shipped. Proposals cap at _LINK_IMPROVEMENT_BATCH_CAP (25) per cycle, matching compression's own cap. There is no anomaly breaker scoped to this stage the way check_tidying_anomaly guards tidying specifically (see the "As built (part 1)" note below) — a fixed cap is the only safeguard today, a known and deliberately deferred gap, not an oversight.

Tidying on age — artifacts to the shelf, memories to the archive

Part 1, shipped 2026-07-18 — the section below describes what actually built; see Experience & Dreaming for the pipeline-level record. Retired since: the memory lane gave way to the lifecycle axis in filing v2 part 6, where the dreaming binary names a five-year-silent memory as an archive candidate and you archive it by hand, and the artifact shelf retired in agentm-vault plan 04. Nothing moves a shelved artifact now; the browse-surface meter still counts it.

Tidying treats work artifacts and memories differently, and both lanes are automatic.

Work artifacts — plans, project files, and the other non-memory documents — leave the eyeline two ways. Project artifacts tidy at close-out: the dev workflow already archives a finished plan and buries a finished project after harvest, and this design composes those conventions as they stand. Everything else moves to its tier's _shelf/ after a full year of never entering a conversation. A shelved artifact changes in no other way: everyday search still finds it, and one use brings it back to its folder on the next cycle. The shelf is a browse convention, not a search boundary — the one small engine change is that the recall walker includes _shelf/.

As built, "an artifact" is any corpus entry with no kind: frontmatter field at all — a real memory's kind: is a required field in save.py's locked entry contract, so its absence is what marks something as "the other non-memory documents" this section means, not a second, separately-invented classification. "Touch" reuses the exact mechanism the memory lane already tracks — a genuine recall.py hit — rather than a new "any injection into a conversation" tracker: nothing in this codebase implements that broader sense yet, and building it would mean instrumenting skills and hooks that live in the separate crickets repo, disproportionate to this one part. The design's own words above name the fuller intent; this is the scoped, buildable slice of it that shipped.

Memories skip the shelf and simply fade. A memory holds full strength for six months of silence, ranks at half strength to a year, an eighth to three years, and a sixteenth to five. Notes entering the final approach — past four and a half years of silence — appear in the digest one cycle before they move, a preview rather than a gate. Past five years without a single genuine recall, a memory moves to its tier's _archive/ — where it is finally allowed to be forgotten. An archived memory leaves everyday search, stays in the index, and answers an explicit archive search (--include-archive) whenever you ask. Nothing is ever deleted, and one genuine recall at any point resets the clock to zero.

Every move in both lanes auto-applies, lands in the revert log, and shows in the digest.

Lint

Current state: since agentm-vault plan 04 the nightly dream cycle runs lint as a report. It counts the mis-cased links it would repair and repairs none; /memory lint --apply writes that repair directly. The "lint" auto-apply lane and its revert-log path below are retired.

The lint engine walks the corpus and reports four things. Orphans are notes with no links in either direction. They feed the linker's backfill queue. Broken wikilinks get repaired when the fix is safe. A safe fix is a mis-cased target that gets corrected and revert-logged. The engine surfaces unsafe fixes for you to review. Removal has exactly one case — a broken link; a valid link is never removed. Contradictions include supersedes chains that disagree and status fields that conflict. The engine surfaces these for your manual resolution. Every note gets a quality score based on frontmatter completeness, link presence, and staleness. A vault-wide roll-up of these scores lands in the digest. The engine also flags kind: values that sit outside the registry. This list gives the standing canonicalization cleanup its worklist. You can run /memory lint on demand. Dreaming runs it weekly.

As built (task 7 — the lint engine): shipped as a new harness/skills/memory/scripts/lint.py, composing three existing signals into one report rather than building a fourth: vault_lint.py's structural check suite (which itself gains four checks this same task adds — see below), graph_snapshot.orphans() for the orphan list (already the pool dream.py's own link-improvement backfill drains directly; lint reports the identical signal for visibility, it doesn't create a second feed), and a new per-note quality score — a weighted composite of frontmatter completeness (0.4, from frontmatter_validator.validate()'s violation count), link presence (0.3, binary — orphan-set membership), and staleness (0.3, lifecycle.compute_decay_score's own axis) — rolled into a vault-wide mean that lands in run_dream()'s digest as a new "Lint:" line alongside the connectivity meter.

The plan's own scoped auto-repair — a mis-cased [[Wrong-Case]] target where [[Correct-Case]] is the sole vault-wide case-insensitive match, no alias, no anchor — is the one behavior beyond reporting. It applies via a precise span-based replacement (never a whole-file string .replace()) and skips anything inside a fenced code block, reusing write_time_linker's own fence-exclusion helpers. Everything else stays surfaced-only exactly as the plan's own "anything unsafe is surfaced instead" text requires: a genuinely broken link (no unique case-insensitive match), an aliased or anchored occurrence, and the three new contradiction checks vault_lint.py gained alongside this task — check_supersede_cycle (a supersedes chain looping back on itself), check_supersede_fork (two or more entries both claiming to supersede the same target), and check_dangling_supersession_status (status: superseded with no entry's supersedes: pointing at it) — plus check_kind_taxonomy, which wraps kind_registry.is_known() directly per this plan's own Constraints text rather than reimplementing the check. kind_registry.py's KNOWN_KINDS gained "howto" as a direct result: the first check to actually enforce the registry against a live vault found this repo's own scripts/verify-memory-roundtrip.sh CI fixture using it, unregistered since the 2026-07-10 seed — a real pre-existing gap fixed by registering the kind, not by weakening the new check.

dream.py's _stage_lint() calls lint.run_lint(vault_path) as a genuinely standalone scan — deliberately not threaded through run_dream()'s own entries/loaded snapshot — so the weekly stage and the on-demand CLI verb run the identical code path and can't drift, satisfying the plan's own CLI/weekly-parity verification requirement by construction rather than by a separate check. "lint" joined dream_confirm.AUTO_APPLY_STAGES, scoped narrowly: the stage only ever proposes kind="wikilink_repair" mutations, so joining the auto-apply set can never reach any of lint's other findings.

One design invariant this task did not build: the design's own Overview ("the single removal case is a link that is already broken") and this section's own text above ("Removal has exactly one case — a broken link; a valid link is never removed") both describe lint actually deleting a broken link whose target no longer exists. That never shipped — a genuinely broken link stays a surfaced finding, exactly like every other unsafe case, and nothing in lint.py or vault_lint.py removes a link. The plan's own task 7 text already scoped this narrower from the start (auto-correct-if-mis-cased, else surface — no removal clause), and neither of the two tasks left in this plan (8, the meters; 9, the remaining guards) picks it up. This stays designed-for, unscheduled. Re-audit trigger: revisit this note if a later task or plan actually builds broken-link removal — reconcile the Overview and this section's prose against what shipped rather than leaving them read as already-built.

Adversarial review caught two real defects before this landed, both fixed same-session. First, an early version repaired a mis-cased link with a whole-file string .replace(), which could silently rewrite a wikilink shown as a documentation example inside a fenced code block — fixed by switching to the span-based replacement above, with fence-exclusion reusing write_time_linker._fenced_ranges/_in_any_range, the identical pattern the link-improvement sweep already relies on. Second, the finding-suppression step (dropping a "broken link" error once its mis-cased occurrence auto-repairs) used a boolean "any match" check that could wrongly suppress an unrelated, still-broken finding — an aliased occurrence of the same wrong-cased target (deliberately never auto-repaired, since aliases are skipped for safety) renders an identical message to the repaired bare occurrence, because vault_lint.check_wikilinks strips the alias before rendering. Fixed with count-based suppression: only as many findings are dropped per (entry, wrong-target) pair as there are actual repaired occurrences. Both are covered by dedicated regression tests (FencedCodeRepairSafetyTests, AliasedCollisionFindingTests, scripts/test_lint.py).

Tests (task 7): 12 new in scripts/test_lint.py (SeededRotFixtureTests against the plan's own verification fixture — an orphan, a mis-cased link, a genuinely broken link, a contradiction, an out-of-registry kind; FencedCodeRepairSafetyTests and AliasedCollisionFindingTests for the two review-caught fixes above; CliAndWeeklyStageParityTests asserting the CLI and the weekly stage agree on the same fixture; WeeklyAutoApplyIntegrationTests for the revert-logged auto-apply path); 8 new in scripts/test_vault_lint.py for the four new checks (2 each — a positive and a negative case); scripts/test_dream.py and scripts/test_dream_confirm.py each had one pre-existing AUTO_APPLY_STAGES-list assertion widened intent-preservingly to include "lint".

Guarding the automation

Current state: the global mutation budget, the anomaly breaker and the sampled audit retired in agentm-vault plan 04 with the auto-apply lanes they guarded, and dream_confirm.py with them. The graph-snapshot cross-check still runs inside lint.

Four guards keep the automation honest. A sampled audit runs on a higher model tier: each period it takes a random sample of the cycle's applied links and merges and renders agree or disagree. The disagreement rate is a meter; past a threshold, the ambiguous bands narrow toward deterministic-only on their own, and the console flags the change. A global mutation budget bounds each cycle across every stage combined, on top of the per-stage caps. An anomaly breaker watches the trailing average: a cycle that proposes several times the usual mutation count applies nothing at all and flags the console instead. And lint checks the graph snapshot against a live extraction sample each cycle, since three features now lean on it.

As built (part 1, updated task 4): the global mutation budget and the anomaly breaker exist today, scoped to the tidying stage's own moves — dream_confirm.auto_apply_batch's batch_cap already bounds every auto-apply stage combined (compression, tidying, and — as of task 4 — link_improvement together, since it joined AUTO_APPLY_STAGES too), and dream_confirm.check_tidying_anomaly trips when a cycle's tidying-proposal count exceeds three times a trailing eight-cycle baseline, applying nothing from that stage rather than a capped partial batch. link_improvement has no equivalent anomaly breaker of its own yet — only its fixed _LINK_IMPROVEMENT_BATCH_CAP (25) per-cycle cap, named in its own as-built note above — a known, deliberately deferred gap that task 9 (below) leaves exactly as-is by design, scoping its own generalization to this part's own stages. The sampled higher-tier audit and the fuller guard suite over dedup/lint land with part 3, task 9 — see the three as-built notes immediately below.

As built (task 6 — the cross-stage auto-apply collision guard): adversarial review of this task surfaced a real bug that predates it and isn't scoped to dedup at all — this is simply the first time two auto-apply stages could plausibly collide, since suffix_backlog_drain is the second stage (after tidying) capable of proposing a mutation against a note the same cycle another stage also touches. Both stages compute their proposals against one frozen pre-cycle snapshot with no awareness of each other; applying both blindly — confirmed by a reproduction — could resurrect a note tidying had just archived, back at its old path, carrying the stale frontmatter tidying's own move had already superseded. The fix lives in dream_confirm.auto_apply_batch (its own docstring carries the full account): the apply loop now tracks every path an already-applied proposal in the current batch has touched, and skips — leaves pending, doesn't consume a batch_cap slot — any later proposal whose paths overlap one already applied this cycle. A skipped proposal is re-evaluated fresh on a later cycle, once the earlier change has settled, by which point the relevant stage's own scan either no longer sees the path as a candidate or proposes a fresh, correctly based mutation against it. This closes a gap the guards above don't otherwise cover: the per-stage caps and the global mutation budget bound how much any ONE stage moves in a cycle, but nothing before this fix bounded two DIFFERENT stages acting on the same note in the same cycle. The guard is general — it protects any future pair of auto-apply stages, not just this one. Verified end-to-end by CrossStageAutoApplyCollisionTests in scripts/test_dream.py (reproducing the real tidying/suffix-backlog-drain collision through the actual run_dream_and_auto_apply() pipeline), plus 3 focused unit tests in scripts/test_dream_confirm.py::CrossStageCollisionGuardTests exercising the skip logic directly.

As built (task 9 of 9 — the generalized anomaly breaker). dream_confirm.check_tidying_anomaly — tidying-only since part 1 — generalized into dream_confirm.check_stage_anomaly(vault_path, stage, current_count), taking the stage name as a parameter and reading/writing a separate _meta/<stage>-cycle-history.json sidecar per stage, so one stage's spike can never trip or poison another's baseline. check_tidying_anomaly itself survives as a thin stage="tidying" wrapper over the generalized function — byte-identical behavior, same history-file path, no existing call site or test needed to change. dream.run_dream_and_auto_apply() now runs every stage named in a new _ANOMALY_WATCHED_STAGES = ("tidying", "suffix_backlog_drain", "lint") tuple through its own check_stage_anomaly call before applying anything, excluding any tripped stage from that cycle's auto-apply set while leaving every other stage unaffected — the digest carries one flagged section per tripped stage (_render_digest's old tidying_anomaly parameter widened into anomalies: dict[str, AnomalyCheckResult], one section rendered per tripped entry) and _meta/dream-anomaly-latest.json changed shape from a single flat dict to a list of per-stage records, one per stage that tripped that cycle. link_improvement is deliberately NOT in _ANOMALY_WATCHED_STAGES — this task's own plan text scopes the extension to "this part's own dedup/lint mutations," not part 2's linking stage, so the gap this design's own task-4 note above already names (link_improvement has only its fixed per-cycle cap, no anomaly breaker) stays exactly as documented, not silently widened. Verified by StageAnomalyGeneralizationTests (scripts/test_dream_confirm.py, 3 tests: the generalized function trips/doesn't-trip/records history identically to the old tidying-only one, and check_tidying_anomaly is proven to be the thin wrapper) and MultiStageAnomalyBreakerIntegrationTests (scripts/test_dream.py, 1 test: a real suffix_backlog_drain spike trips independently of tidying through the actual run_dream_and_auto_apply() pipeline, tidying's own baseline untouched).

As built (task 9 of 9 — the graph-snapshot cross-check). New lint._graph_snapshot_cross_check(vault_path, model) compares each sampled entry's PERSISTED outgoing edges (graph_snapshot.outgoing(), part 2's own snapshot) against a LIVE re-extraction from its current content (graph.extract_edges_for_paths()), flagging any disagreement as a graph-snapshot-drift finding. Ordering is load-bearing and required a run_dream() reorder beyond this task's own module: lint.run_lint()'s own graph_snapshot.rebuild() call reconciles the whole vault against live content as a side effect of running lint at all, so the cross-check has to run and capture its findings BEFORE that rebuild — otherwise it would compare a freshly-rebuilt snapshot against itself moments later and never find anything. Wiring the cross-check into the weekly cycle surfaced a second rebuild call that would have broken the same invariant even with lint.py's own internal ordering fixed: dream.py's _stage_link_improvement (part 2, task 4) also does an unconditional full-vault graph_snapshot.rebuild() as its own side effect, and run_dream()'s hand-wired stage sequence ran link-improvement before lint. Fixed by reordering run_dream() so _stage_lint() runs FIRST, before every other stage — the only way the cross-check sees genuine week-old drift rather than a snapshot no other stage has touched yet this cycle.

The batch itself needed a real fix, caught by adversarial review of the first version: the sample was a plain sorted(all_rels)[:cap] slice with no persisted rotation, so any entry past the alphabetically-first 25 was PERMANENTLY unreachable no matter how many cycles ran — a starvation bug, not a coverage gap that would eventually self-correct. Fixed with a persisted rotation cursor (_read_cross_check_attempted / _write_cross_check_attempted, backed by _meta/graph-snapshot-cross-check-state.json), mirroring dream._read_backfill_attempted's identical pool-rotation shape from part 2's link-backfill: each cycle samples up to _GRAPH_SNAPSHOT_CROSS_CHECK_BATCH_CAP (25) entries the current pass hasn't attempted yet, and once every entry has had a turn, the pass resets and starts over. This is a general lesson worth carrying into any future batched check in this design or elsewhere: a capped batch drawn from a fixed sort-and-slice, with no persisted cursor, silently starves everything past the cap forever — the fix is always a persisted rotation cursor, not a bigger cap or a smarter sort. Deterministic order within a pass (sorted rel path), not one true-random sample per cycle, matching the design's own capped-batch convention elsewhere rather than the literal word "sample." Verified by GraphSnapshotCrossCheckTests (scripts/test_lint.py, 6 tests, including two regression tests pinned directly to the rotation fix: a fixture with more than 25 entries proves every entry eventually gets checked across successive cycles, and a fixture proves the same entry is never checked twice in one pass before the rest of the pool has had a turn).

As built (task 9 of 9 — the sampled higher-tier audit and band narrowing). New dream_confirm.higher_tier_model_available() — always False, for the identical reason dream.cheap_model_tier_available() (part 2, task 4) is: research before this task confirmed no synchronous "ask a model X, get a verdict back" primitive exists anywhere in this codebase, the same finding, same seam shape, same fail-closed contract as every other consumer of that pattern in this design. judge_applied_mutation(summary) raises if ever called — unreachable until the tier ships, never a silent guess. run_sampled_audit(vault_path, applied_items) samples up to SAMPLED_AUDIT_BATCH_CAP (25) of the cycle's applied items, deterministically (sorted by index, draining coverage over successive cycles the same way the graph-snapshot cross-check above does — not one true-random draw per run, despite this section's own "random sample" wording above), asks the higher tier for a verdict on each, and computes a disagreement rate. Above SAMPLED_AUDIT_DISAGREEMENT_THRESHOLD (0.2, i.e. >20%), it narrows the ambiguous band by raising write_time_linker's own similarity floor: a persisted override (_meta/link-band-narrowing.json) is written with the floor stepped up by a fixed _LINK_BAND_NARROWING_STEP (0.05), capped just below CONFIDENT_SIMILARITY_THRESHOLD (0.85) so narrowing can approach but never cross into the already-confident band. New write_time_linker.link_similarity_floor(vault_path=None) is the reader every consumer now calls instead of the raw LINK_SIMILARITY_FLOOR constant — it falls back to that constant when no override file exists — and dream.py's _stage_link_improvement (part 2) was updated to call it, so a narrowing this task writes actually changes the weekly sweep's real behavior, not just a number nobody reads.

dream.run_dream_and_auto_apply() gathers the cycle's actual applied link_improvement items from the real batch.items, plus the folded inbox-triage sub-run's applied inbox_merge items from triage_batch.items — "links and merges," matching this section's own wording — and passes both into run_sampled_audit. The result lands on a new DreamDigest.sampled_audit field, is written to _meta/sampled-audit-latest.json (overwritten every cycle, the same "never goes stale" convention as dream-auto-expired-latest.json), and renders as a "Sampled audit:" line in the digest. New console.section_sampled_audit(vault) reads that same pointer file — honest-dark before any cycle has run, "nothing sampled this cycle" (not an error) while the tier stays unavailable, and a ⚠ ambiguous bands narrowed this cycle flag appended to the line whenever a narrowing fires — this is the "flagging the console" this section's own opening paragraph requires.

Because the higher-tier model tier is always unavailable, every REAL cycle samples nothing, records no disagreement, and never narrows — a named, tracked gap exactly like every other consumer of this codebase's model-tier seam (dream.cheap_model_tier_available(), part 2 task 4; dream_confirm.higher_tier_model_available(), here), not a silently-dropped feature. Tests exercise the full sampling/disagreement-rate/narrowing logic end-to-end by patching higher_tier_model_available to True and supplying verdicts — the same convention dream.py's judge_fuzzy_merge tests (part 3, task 3) already established. Verified by SampledAuditTests (scripts/test_dream_confirm.py, 6 tests, including a proof that a narrowing above threshold actually writes the directive file and a proof that successive narrowings never cross the CONFIDENT_SIMILARITY_THRESHOLD cap no matter how many cycles trip in a row), test_narrowed_floor_excludes_a_pair_that_would_otherwise_qualify (scripts/test_dream.py::LinkImprovementStageTests — proving _stage_link_improvement actually reads the narrowed floor and excludes a pair it would otherwise have linked, not just that the reader function works in isolation), test_sampled_audit_gathers_a_real_applied_link_improvement_item (scripts/test_dream.py::LinkImprovementIntegrationTests — closing a coverage gap adversarial review flagged: proving the real batch.items dict shape from an actual applied link-improvement proposal flows correctly into run_sampled_audit, not just a synthetic dict shaped to match), and the standalone scripts/test_sampled_audit_surface.py::SampledAuditConsoleSurfaceTests (4 tests, directly proving the plan's own verification text — the console section renders honest-dark before any cycle, reports "nothing" honestly at zero samples, and is flagged exactly when the bands narrow).

With task 9, all four guards this section names are real and all three parts of this design are shipped. See the Document History entry below for the part-3, design-closing landing.

The meters

Two numbers say whether this design is working. Organic connectivity started at 13.8%, and reads 30.0% as of 2026-07-23 — 177 of 590 notes, against 32 generated links counted separately. This is the share of notes with at least one real, non-generated link. The meter counts the linker's additions separately. This separate count keeps the main number honest. The browse-surface counts track the eyeline directly. They count three states: live notes per folder, shelved, and archived. Both metrics land in the digest each cycle. They also land in the health dashboard's FRIDAY family. The acceptance test is your own sentence: browsing agentm's memory shows live, current notes, and aged material sits in the archive, still there on request.

As built (task 7 — the connectivity meter): shipped exactly the half of this section that was in this part's task list — the two link-connectivity numbers, computed independently and landing in the digest every cycle. dream._connectivity_meter(loaded) returns organic_connectivity (+ organically_linked_count) and generated_link_count as two separately-computed values, never derived from one another. Organic connectivity is the share of notes with at least one real, non-generated link — a wikilink, or a supersedes: frontmatter edge — that survives after every **Related:** line is stripped out first, since that's the only line the linker itself ever writes. Detection reuses graph.extract_edges on the stripped content, so a [[wikilink]] sitting inside a fenced code block or an inline code span is excluded from organic connectivity exactly the way the typed-edge graph already excludes it — never counted as a real link there either. generated_link_count is the total wikilink count across every non-fenced **Related:** line in the corpus — the linker's own contribution, tracked as its own number so it can never inflate the organic figure it's supposed to be judged against. One deliberate design point worth naming: an ingest batch's reading-order nav and doc-backlink wikilinks (task 5) count toward organic connectivity, not generated — they're structural links ingest itself authors at write time, not additions the write-time linker or the sweep made; the meter's job is specifically to keep the linker's contribution isolated, not every automated link in the vault. Both numbers merge into run_dream()'s existing corpus_stats and render in the digest header every cycle: "Connectivity: X% organic (N of M notes with ≥1 real, non-generated link) · K generated link(s) (counted separately)." Verified by 6 tests in scripts/test_dream.py::ConnectivityMeterTests (pure content parsing against fixture corpora, never touches sqlite-vec, never skips): a note whose only link is a generated Related-line link doesn't count toward organic; a note with both an organic prose link and generated Related links moves both counts independently; a fenced wikilink counts toward neither; a supersedes: frontmatter edge counts as organic; an empty corpus reports zero on both without crashing; and a real run_dream() call renders both numbers into the digest text.

Honest scope note — what this task did not build. This section's own forward-looking prose above also names the browse-surface counts (live/shelved/archived per folder) and landing in "the health dashboard's FRIDAY family." Neither shipped here — this part's task list (PLAN-auto-org-write-time-linking.md task 7) scoped only the two link-connectivity numbers into the digest. The browse-surface counts are closer to part 1's shelf/archive machinery than to this part's linking work, and the health-dashboard wiring belongs to the health-score family build; both stay designed-for. Re-audit trigger: revisit this note if a later part or a health-family build lands the browse-surface counts or the FRIDAY-family dashboard wiring — reconcile here rather than leaving this section's prose reading as fully built. Resolved by task 8 of 9 — see the as-built note directly below.

As built (task 8 of 9 — the meters, fully reported). This is the task the re-audit trigger above was waiting on. Both halves of this section's forward-looking prose are now real: the browse-surface counts land in the digest alongside connectivity, and both meters have a genuine home on the health dashboard.

dream._browse_surface_counts(vault_path, entries) is the new function, wired into run_dream()'s corpus_stats right after the existing _connectivity_meter call. It counts three states directly rather than deriving any one from the other two: live is every entry in this module's own corpus scan that doesn't sit under _shelf; shelved is the subset of that same scan that does — the corpus scan this module has always used already walks _shelf and already excludes _archive, so shelved falls out of the existing scan for free; archived needs its own separate walk, since _archive is deliberately excluded from every other stage's corpus (that exclusion is the whole point of archiving — an archived note stops showing up in the everyday walk) and so was never reachable from entries at all. The digest gained one new line, right under the connectivity line and following its own defensive .get()-safe rendering convention so a missing or partial corpus_stats dict never raises: "Browse surface: N live, N shelved, N archived."

The section's other forward-looking claim — landing "in the health dashboard's FRIDAY family" — needed a real answer, not just a wiring exercise, because that family doesn't literally exist. scripts/health/health_score.py locks exactly eight family/axis names in FAMILY_WEIGHTS (memory persist+recall, plan-adherence+drift, verification honesty, capability function, memory freshness+experience, efficiency, docs+voice health, safety/recoverability); there is no ninth "FRIDAY" axis, and this task doesn't add one. Read as informal plan language for "the health dashboard, in service of tracking FRIDAY-ladder progress," both meters landed under the existing memory freshness+experience axis — the same axis verify-dreaming.sh already reports against for part 2's own dashboard-integration precedent. No code or design text anywhere suggests a literal new family was intended; if a future reader disagrees with this reading, that's the thing to re-litigate, not the wiring itself.

Verified end-to-end by the new scripts/verify-auto-org-meters.sh (the same jsonl_emit.sh-based health-signal convention verify-dreaming.sh uses), wired into check-all.sh and as a CI step in tests-linux.yml/tests-mac.yml right after verify-dreaming (no Windows wiring, matching verify-dreaming itself). It seeds a scratch vault with a linked pair, an unlinked note, a shelved artifact, and an archived memory, drives a real run_dream() cycle, and asserts: the connectivity ratio and all three browse-surface counts land correctly in corpus_stats; both meters render as digest lines; and — the plan's own acceptance test, verbatim — the archived note's content is still readable on disk afterward, never deleted. Unit coverage lives in scripts/test_dream.py::BrowseSurfaceCountsTests (4 tests: the three states counted correctly, archived content stays readable, an empty vault reports all zero, both meters land in the digest).

Resolved by task 9 of 9 — the remaining automation guards (the generalized anomaly breaker, the graph-snapshot cross-check, and the sampled higher-tier audit) shipped the same day; see the three as-built notes in the Guarding-the-automation section above. This design's status now flips to launched, and the note at the top of this document records all three parts shipped.

Where things go

The archive convention, the tier layout, and the entry contract live in the memory-system design (wiki/designs/agentm-memory-system.md). This design adds the moves that tidy notes and the links that connect them; the notes keep their shape. The shelf is its one addition to the layout, recorded in the landing amendment. The project close-out conventions the artifact lane composes live in the dev workflow's own designs. The staging, confirmation, and revert machinery belongs to dreaming. The experience design describes this machinery (wiki/designs/agentm-experience-and-dreaming.md).

Alternatives Considered

  • A dedicated nightly runner job for archival. Rejected: Dreaming owns whole-corpus maintenance. It already handles staging, confirmation, reverting, and auto-apply. A second corpus walker duplicates this work. A weekly schedule is fast enough. Nothing about a cold note requires an urgent response.
  • LLM-judged linking and archiving. Rejected for v1: Deterministic signals like similarity, decay, and the graph are cheap and explainable. The engine already computes them. An LLM judge can act as a second opinion later if the deterministic signal is uncertain.
  • Hard-deleting expired cruft. Rejected outright: Prune always resolves to archive. It never resolves to delete. We maintain this standing invariant.
  • Obsidian community plugins for auto-linking. Rejected: The engine owns the corpus. It works the exact same way on every host. A plugin forks the mechanism per host and skips the staging discipline.
  • Do nothing. Rejected: Connectivity is flat. The browse surface degrades every month. You named this pain directly.

Dependencies

We rely on several shipped systems. These include the lifecycle decay layer, the vector index, and the typed-edge graph. We also rely on dreaming's staging, confirm, and revert flow, its batch caps, the runner, and the kind registry. The capture design runs on the same arc and hands ingest batches to the linker. Inbox triage re-homes into the weekly cycle — its machinery reused as a dreaming stage, its hand-run command kept as an on-demand door. The cheap-model band and the sampled audit ride the model-and-effort routing tiers; every other action here makes zero model calls. Downstream, the canonicalization cleanup consumes lint's kind report. The auto-organization meters feed the health family on the same arc.

Migrations

This design requires no destructive migrations. The backfill and the first cycles run capped, revert-logged, and digest-reported; nothing waits on your review to proceed. The decay sidecar and the index are already in place. Existing _archive/ content stays exactly where it is.

Technical Debt & Risks

  • The stepped rank schedule, the one-year artifact bar, the five-year memory bar, the similarity floor, and the link cap are calibration defaults. They deliberately lean conservative, and they have no real-use data behind them. Re-audit trigger: The first two live cycles.
  • Retuning the rank curve changes retrieval order vault-wide. The change merges only if the pinned retrieval eval holds — the standing rule for any ranking change.
  • Over-linking is the linker's failure mode. Too many weak links make the graph noisy. The cap and the floor bound this behavior. The connectivity meter counts generated links separately. This prevents the linker from inflating its own success number.
  • Shelving an artifact you still want in the eyeline is the likeliest miss, and it is low-stakes: it stays in everyday search, the digest names every move, the revert log undoes it, and one use brings it back. Archiving a memory wrongly is rarer still at a five-year bar, and the archive search plus the revert log recover it.
  • Contradiction detection is heuristic and advisory. It misses real contradictions and flags false ones. It only surfaces problems and never acts on them.
  • The backfill touches old notes at scale. The per-cycle cap and the revert log bound the blast radius for each cycle.
  • The cheap-model band could add weak links or run up cost. The band is narrow, the additions are capped and counted separately by the meter, the job manifest carries a hard per-cycle budget, and the sweep degrades to deterministic-only when the budget is spent or the tier is unavailable. The sampled higher-tier audit measures its judgment, and the bands narrow on their own when disagreement climbs.
  • A runaway cycle could churn the vault inside the per-stage caps. The global mutation budget and the anomaly breaker bound it: an abnormal cycle applies nothing and flags the console.
  • A wrong merge is dedup's failure mode. A collapse keeps the surviving note's content and marks the copies superseded rather than deleting anything, every disposition is revert-logged and named in the digest, and the per-cycle cap bounds each pass. Ambiguous candidates are never forced: they stay in the inbox and go on the needs-your-eye list. Re-audit trigger: the first backlog-drain cycles.

Quality Attributes

Reliability

The engine caps and revert-logs every scheduled action, and reports each one in the digest. Every action is resumable. A cycle that moves nothing is a normal cycle.

Data Integrity

Shelf and archive are moves, never deletes. The index covers both, so nothing becomes unfindable. The engine journals every mutation for revert.

Latency

The linker runs asynchronously after the save returns. It works the same way embedding does. Saving a note never waits on the linker. The weekly cycle carries everything else.

Testability

Each action gets a deterministic fixture. The linker uses a synthetic corpus with known neighbors. The archive stage uses notes with pinned decay scores. Lint uses seeded rot. The connectivity meter has its own test. The supervised first passes over the real vault provide the acceptance evidence.

Project management

Work estimates

This design has three parts. Part one builds the tidying stage — the stepped rank curve (shadow-run, then eval-gated), the memory archive with its digest preview, the artifact shelf, and the recall-walker change (Medium) — shipped 2026-07-18. Part two builds the write-time linker, the link-improvement sweep with its graph snapshot and cheap-model band, the ingest handoff, and the backfill (Medium) — shipped 2026-07-18. Part three builds dedup — the write-time guard, the cluster-aware weekly pass with its fuzzy-merge judge, the triage fold into the cycle, the needs-your-eye surface, the suffix-backlog drain — plus the lint engine, /memory lint, the meters, and the automation guards (Medium) — shipped 2026-07-19, closing out this design.

Documentation Plan

This design lifts to wiki/designs/agentm-auto-organization.md during part one, and both parent designs gain their pointer amendments in the same landing: the memory-system design's archive-and-prune section flips its designed-for tail to point here, and the experience design's dreaming section records the new stages. The how-to documentation covers Tune-The-Archive. This includes the floors, the caps, and bringing a note back. The reference documentation covers runner-jobs and gate updates as new checks land. We add a row to Completed-Features.md for each release.

Launch Plans

We plan three named minor releases. These are "the shelf and the archive", "write-time linking", and "self-healing lint". The ladder lands in ROADMAP-MASTER § FRIDAY. We set dates when each release cuts.

As built: all three cut. Part one shipped 2026-07-18 as "Old notes out of the way" (FRIDAY ladder feature 5). Part two shipped 2026-07-18 as "Notes that link themselves" (FRIDAY ladder feature 6). Part three shipped 2026-07-19 under a broader name than this section's own "self-healing lint" — the actual named release is "Fix duplicates and broken links" (FRIDAY ladder feature 7, per PLAN-auto-org-dedup-and-lint.md's own Release section), reflecting that the shipped scope covers dedup end-to-end plus lint together, not lint alone. This is the last agentm-lane feature before the operator-run arc-exit demo.

Operations

Monitoring and Alerting

The digest carries each cycle's proposals and applies. It also carries both meters. The health dashboard's FRIDAY family carries connectivity and the browse-surface counts (as built: this is the memory freshness+experience axis — no literal ninth family exists; see the Meters section's task-8 as-built note above). The console carries the needs-your-eye list of ambiguous duplicates, and the morning brief carries its count. The engine reports a quiet cycle as quiet. It never hides a quiet cycle.

Logging Plan

Dreaming's staging artifacts and revert journal record every proposal and apply. The linker logs its additions for each note. Lint reports land as dated artifacts in _meta/.

Rollback Strategy

Every shelf, archive, and link move reverts through the revert log. A write-time link is plain text on the newly saved note. You delete the "Related" line to undo it. Disabling the three components returns the vault to today's behavior. You have no state migration to unwind.

Document History

Date Change Status
2026-07-17 Authored (F1 session) from the operator's brief (AUTO-ORG-BRIEF.md), the self-healing-lint backlog row, the connectivity audit baselines, and the memory-system design's archive convention — organized in processing order per the capture design's precedent. Vehicle decision: dreaming owns the weekly actions (a second corpus walker would duplicate its staging and revert plumbing). Gating decision: supervised confirm-gated first passes; auto-apply only on a fresh operator ruling at that review. The two-step voice pass (Gemini via agy, Claude-verified) ran before first operator review, per the standing process. Quality Attributes walked in full; Security, Privacy, Scalability, Abuse, Accessibility, i18n, and Compliance omitted as low-relevance. First review round (operator, same day): the decay timeline made deliberately conservative — archive floor 0.05→0.02 plus a new 180-day minimum age — and the design's relationship to the memory system stated explicitly (its lifecycle tail built out as a child document, spanning two parents; pointer amendments in both at landing). Second review round (operator, same day): the lifecycle became two-stage — shelf then archive — and every confirmation gate came out: nothing is ever deleted, so every move auto-applies with revert log and digest reporting. Third review round (operator, same day): the timelines stretched to the operator's stepped schedule — a memory holds full strength for six months of silence, half to a year, an eighth to three years, a sixteenth to five, and archives past five years, with no shelf for memories — while the shelf narrowed to non-memory artifacts (project artifacts tidy at close-out per the existing dev-workflow conventions; everything else shelves after one year of never entering a conversation, returning on one use). The rank-curve retune replaces the 30-day half-life and is eval-gated per the standing ranking rule. Fourth review round (operator, same day): linking now improves over time — a weekly link-improvement sweep connects new arrivals to related older content, drawing free candidates from a newly persisted device-local graph snapshot plus the vector index, with a budget-capped cheap-model yes/no reserved for the ambiguous band and deterministic-only degradation when the budget is spent; links are only ever added, removal stays reserved for broken links. Fifth review round (operator, same day): deduplication became a first-class concern after the operator found suffix families (_1, _2) piling up in the inbox — a write-time dedup guard (fingerprint + near-match reinforces instead of duplicating), a cluster-aware weekly pass extending triage's merge and dreaming's dedup to cover inbox and corpus together, and a capped suffix-backlog drain; approving this design is the fresh ruling that extends auto-apply to dreaming's dedup under these bounds. Sixth review round (operator, same day): inbox triage folds into the weekly dreaming cycle as a stage — automatic, no longer operator-invoked, closing the historical split (the two already shared their merge machinery underneath) — with /memory inbox kept as the on-demand door; ambiguous duplicate candidates are never forced, staying in the inbox and surfacing on a console needs-your-eye list, in the digest, and as a morning-brief count. Seventh review round (operator, same day): five risk mitigations folded — a sampled higher-tier audit over applied links and merges with an auto-narrowing disagreement threshold; a cheap-judge verdict required for fuzzy merges while fingerprint-exact collapses stay deterministic; shadow-mode for the rank-curve retune ahead of its eval gate; a one-cycle digest preview before any archive move; and a global mutation budget plus an anomaly breaker that applies nothing on an abnormal cycle. The operator approved the design as final. Later the same day, the FRIDAY-AGENTM build session ran /design translate, splitting the design into 3 parts (shelf-and-archive, write-time-linking, dedup-and-lint; parts/ files at agentm-auto-organization/parts/ alongside this doc) and /design sequence, landing three executable plans at _harness/queued-plans/PLAN-auto-org-shelf-and-archive.md, PLAN-auto-org-write-time-linking.md, PLAN-auto-org-dedup-and-lint.md, each grounded against the live agentm codebase. Grounding surfaced that two pieces the design assumes as infrastructure don't exist yet as of this pass — a vector-index nearest-neighbor query function and a persisted device-local typed-edge graph snapshot (today's graph.py::extract_edges() is stateless, recomputed fresh per caller) — both are named as real build inside PLAN-auto-org-write-time-linking.md rather than assumed pre-existing. final
2026-07-18 Lifted to wiki/designs/ (part 1 landing, FRIDAY ladder feature 5). Governance stamp applied (governs: populated with the modules part 1 built out — dream.py, dream_confirm.py, lifecycle.py, scripts/health/eval_v6_retrieval.py). Added a landing note and inline "as built" callouts in the Tidying-on-age and Guarding-the-automation sections recording what part 1 actually shipped, without rewriting the design's own forward-looking prose elsewhere — parts 2 and 3 remain designed-for, so status holds at final. Both parent designs (agentm-memory-system.md, agentm-experience-and-dreaming.md) gained pointer amendments the same day. Cross-model prose pass: Claude-only — the prose-pass skill's own prose_pass.py script (the mechanism that drives agy) isn't present in the installed design plugin (0.4.0) or the crickets source tree despite being referenced by both the skill spec and its how-to page; agy itself is installed and authenticated, but the orchestrating script it would run through doesn't exist to invoke. Per the skill's own documented fallback, applied the same discipline by hand instead of the two-step pass. Flagging the missing script as a crickets-repo gap, out of scope for this plan. final
2026-07-18 Part 2 landing ("Notes that link themselves," FRIDAY ladder feature 6) — all 7 tasks of PLAN-auto-org-write-time-linking.md now shipped: the vector-index nearest-neighbor query (task 1), the persisted typed-edge graph snapshot (task 2), the write-time linker (task 3 — deferred to the next embedding-queue drain rather than inline-in-save.py, after a confirmed regression the design's own "As built (task 3)" note records), the weekly link-improvement sweep (task 4 — deterministic-only; the cheap-model band for the ambiguous middle band was never built, an explicit tracked seam rather than a silent drop), the ingest-batch handoff (task 5 — shipped exactly as this design's text describes, the only task in this part with no divergence), the backfill (task 6 — folded into the sweep's own stage rather than a second one), and the connectivity meter (task 7 — the two link-connectivity numbers, computed independently, landing in the digest every cycle). Governance stamp applied (governs: gains vec_index.py, graph_snapshot.py, write_time_linker.py, and save.py — the modules part 2 built out or touched). Each task's own as-built note was added inline as it shipped through the build session (see the Linking-at-write-time and Meters sections above); this row is the part-level landing entry, matching the precedent part 1 set above. Two forward-looking claims in this document's own prose were not built by this part and stay designed-for, called out explicitly in the Meters section's own "As built (task 7)" note: the browse-surface counts (live/shelved/archived per folder) and the health dashboard's FRIDAY-family wiring — neither was in this part's task list. Part 3 (dedup + lint) is now the only piece of this design left designed-for; status holds at final, not launched, until it ships. Both parent designs (agentm-memory-system.md, agentm-experience-and-dreaming.md) gain matching amendment-log entries the same day. final
2026-07-19 Part 3 landing ("Fix duplicates and broken links," FRIDAY ladder feature 7) — all 9 tasks of PLAN-auto-org-dedup-and-lint.md now shipped, closing out the entire 3-part design. The content-fingerprint hash and write-time dedup guard (tasks 1-2 — exact-match only; the design's own "or a strong near-match" clause is a deliberate, documented non-build, ruled out by this same plan's own Locked design call), the cluster-aware weekly collapse with its fuzzy-merge judge seam (task 3), the inbox-triage fold into the weekly dreaming cycle (task 4), the needs-your-eye surface across console, digest, and morning-brief (task 5), the suffix-backlog drain plus the cross-stage auto-apply collision guard adversarial review surfaced along the way (task 6), the lint engine and /memory lint (task 7), the meters fully reported — connectivity and the browse-surface counts, landed under the existing memory freshness+experience health axis since no literal ninth "FRIDAY" family exists (task 8) — and the remaining automation guards (task 9): the anomaly breaker generalized from a tidying-only check into dream_confirm.check_stage_anomaly, now also watching suffix_backlog_drain and lint (deliberately not link_improvement — that gap stays as originally documented); a graph-snapshot cross-check catching drift between the persisted snapshot and a live re-extraction, which required reordering run_dream() so lint runs first (before link-improvement's own full-vault rebuild erases the very drift the check exists to catch) and, after an adversarial review caught a real starvation bug in the first version's fixed sort-and-slice sample, a persisted rotation cursor mirroring the link-backfill's own pattern; and a sampled higher-tier audit that narrows the ambiguous linking band automatically once disagreement crosses 20%, flagging the console — all deterministic-only today, since no higher-tier-model primitive exists anywhere in this codebase yet, a named and tracked gap rather than a silent drop. status flips from final to launched with this landing — see the top-of-document note, now updated. Governance stamp applied (governs: gains fingerprint.py, dedup_guard.py, inbox_triage.py, lint.py, and harness/skills/console/scripts/console.py — the modules part 3 built out or touched). Each task's own as-built note was added inline as it shipped through the build session (see the Deduplication, Lint, Guarding-the-automation, and Meters sections above); this row is the part-level, design-closing landing entry, matching the precedent part 1 and part 2 set above. Both parent designs (agentm-memory-system.md, agentm-experience-and-dreaming.md) gain matching amendment-log entries the same day, resolving the "reconcile here as it lands" re-audit triggers each was carrying for this design's remaining parts. launched
2026-07-24 Post-FRIDAY connectivity figure recorded (Loose Ends Release 5). This design's own headline metric had gone stale in its body: it still read "Organic connectivity starts at 13.8%" and, in the opening framing, present-tense "13.8% of notes carry an organic link" — the design-time baseline, not the current state. The meter this design shipped (part 2, task 7) has been running every dreaming cycle since, and the 2026-07-23 real-vault run reports 30.0% organic (177 of 590 notes) with 32 generated links counted separately. Both body claims now carry the current figure with its date, and the opening baseline is stated in past tense as the motivation it was. No re-measure was needed — the Loose Ends sweep inventory listed this item as "organic-connectivity re-measure", but the measurement already existed in the vault's own digest; only the recording was outstanding. The agentm-memory-system.md amendment that quotes "13.8% baseline" is left as written: it is a historical entry describing what was true at its own date, and amendment logs record what was true then. launched
2026-08-10 Part 2's linking is retired: the write-time linker and the weekly link-improvement sweep are removed. Both ranked **Related:** candidates out of the vector index, and that stack was removed after its own pre-registered experiment ruled against it — see week-1 experiment for the measurement and the diagnosis (the live index held 0 rows against 8,684 notes). Why they could not simply be re-pointed: the design's own recorded call was to defer linking to the drain precisely so it could reuse the embedding already being computed, rather than pay a cold model load on the save path. With no drain there is no free embedding, so keeping the feature would have meant taking the regression that call exists to avoid. Operator-confirmed. What survives: every **Related:** line already written stays on disk untouched, and the markup parsers dream.py and lint.py still need (_fenced_ranges, _in_any_range, and the two Related regexes) moved to a new markdown_spans.py — none of them ever needed an embedding. The graph snapshot stays and is still rebuilt; consolidate.py and this design's own orphan reporting still read it. dream_confirm's sampled audit still measures its disagreement rate, but no longer narrows a similarity band that no longer exists. Also gone from part 3: dedup_guard.find_vault_duplicate, the write-time dedup guard, which resolved a fingerprint through the removed index's own entry_meta table; the weekly cluster pass this design already specifies now owns those duplicates alone. Part 3's inbox guard, a plain frontmatter scan, is unaffected. governs: swaps the deleted write_time_linker.py for markdown_spans.py. status holds at final. Re-audit trigger: any future proposal to auto-generate links, which would need a relatedness signal this repo no longer has — notes_link_discovery.py's TF-IDF pass over the Obsidian notes is the nearest surviving one, and it is a different corpus. final
2026-09-06 Part 3's inbox-triage fold and needs-your-eye surface retire (v9.20.0, PR #575). Tasks 3, 4 and 5 of PLAN-auto-org-dedup-and-lint.md built the cluster-aware weekly pass, folded the triage engine into the dreaming cycle, and gave the ambiguous-band list its three surfaces. Filing v2 later removed memory/_inbox/, so the pass had nothing to walk and the list was never written again; the three readers reported dark or zero from that day. The engine, the fold and the surfaces are gone. The design's other parts are untouched — the tidying stage, the archive, the link-improvement pass and the meters all still run. Why not keep the surfaces against a future producer: a surface nobody can act on is indistinguishable from one with nothing to say. Re-audit if the ambiguous band starts costing something on the live corpus.
2026-09-11 The dream cycle's automation retires (agentm-vault plan 04, task 5). dream.py now reads, reports and proposes, and changes no note. Retired from this design's machinery: the tidying stage and its artifact shelf (it moved your kind-less documents), every auto-apply lane with its batch cap and the cross-stage collision guard, the lint repair lane (the cycle counts mis-cased links; /memory lint --apply still repairs them), the anomaly breaker, and the sampled audit with its band narrowing. dream_confirm.py is deleted. Dedup lands as a Possible twins section on the needs-review map. The top note now says what still runs, its old build summary living on in the 2026-07-18 and 2026-07-19 rows, and each affected section gains a current-state note; the design text itself stays as built. governs: drops dream_confirm.py, and inbox_triage.py, deleted on 2026-09-06. The row above's "the link-improvement pass … still run" was already untrue: that pass left on 2026-08-10. Why not keep the auto-apply lanes for the moves nobody disputes: the vault design ruled that a proposal you act on by hand is the same act with git as the undo, tidying's shelf moved your own documents, and lint's one safe repair stays a flag away on demand. Re-audit trigger: a class of fix you find yourself making by hand every week, which is the evidence an automatic lane would need. launched

Clone this wiki locally