fix(memory): journal the index accounting, and stop three ways of losing it (#1447) - #1450
Merged
Conversation
…ing it #1447 asked for one thing — get `memory_index` onto the ledger. Probing the path first turned up two more defects in the same function, both worse: 1. STALE, not absent. When memory/index.md was deleted, `get_memory_context` returned "" without touching `last_index_fit`, so the record kept the PREVIOUS build's byte counts. A cycle with no memory would have journalled another cycle's accounting as its own. Verified: after deleting the index, `source_chars` still read 3654. 2. An unreadable index RAISED. `index.read_text()` propagated UnicodeDecodeError out through build_system_prompt. memory/index.md is instance-owned, so the loop could fail its own cycle by writing one bad byte into its own file. ADR-002/#1173 is explicit that an unreadable input is a value, not an exception. 3. The allowlist ate the signal. ContextBuilder copied six named keys out of the record, and #1443 added `resident_matched` / `resident_expected` / `resident_missing` after that list was written — so the report that a rule entry stopped matching never reached a caller at all. Copying the whole record removes the class, not just this instance of it. `last_index_fit` now always carries a `status`: present / missing / empty / unavailable, with the exception type as `reason` for the last. The bridge journals `memory_index` on the `system_prompt` row ALONGSIDE `sections`, never instead of it. Grouper safety: this adds a key to an existing row rather than a new phase. loop_metrics_report._group_cycles allowlists phases (#1374 ratchet) and every outcome reader filters `phase == "outcome"`, so nothing keys on the presence of a system_prompt field. Five of the six new tests fail against the unfixed sources; the sixth is the behaviour-preservation test for `sections`, which must pass in both states. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1447.
The issue asked for one thing: get
memory_indexonto the ledger. Probing the path before editing turned up two further defects in the same function, both more severe than the one filed.1. The record went stale, which is worse than absent
When
memory/index.mdwas deleted,get_memory_contextreturned""without touchinglast_index_fit, so the record kept the previous build's byte counts. A cycle with no memory would have journalled another cycle's accounting as its own. Verified before fixing:Wrong data presented as current is a worse failure than no data.
2. An unreadable index raised out of prompt construction
memory/index.mdis instance-owned. The loop could fail its own cycle by writing one bad byte into its own file. ADR-002 / #1173 is explicit that an unreadable input is a value, not an exception.3. An allowlist ate the signal added by #1443
ContextBuildercopied six named keys out of the record. #1443 addedresident_matched/resident_expected/resident_missingafter that list was written, so the report that a rule entry stopped matching never reached a caller at all — the guard's output was filtered out on the way to its reader. Copying the whole record removes the class rather than this instance of it.What changed
last_index_fitnow always carries astatus:present/missing/empty/unavailable, with the exception type asreasonfor the last. Those four are exactly the states ADR-002 requires a caller be able to tell apart.The bridge journals
memory_indexon thephase: "system_prompt"row alongsidesections, never instead of it — existing readers ofsections.memoryare untouched, pinned by a test.Grouper safety, checked not assumed
This adds a key to an existing row rather than a new phase, but #1374 is the precedent for new keys minting phantom entries.
scripts/loop_metrics_report.py:160allowlists phases (the #1374 ratchet) and every outcome reader filtersphase == "outcome"explicitly, so nothing keys on the presence of asystem_promptfield.Tests
Six new. Five fail against the unfixed sources — verified in an isolated copy, not by stashing. The sixth is the
sectionsbehaviour-preservation test, which must pass in both states and does.Full suite:
4 failed, 3595 passed, 18 skipped. The four are the known Windows baseline, failing identically on a cleanorigin/main.