Skip to content

emrg: the rotation's step 0 becomes a command, not a per-cycle probe - #2409

Merged
argszero merged 7 commits into
masterfrom
feature/memory-rotation-reading
Oct 20, 2026
Merged

argszero merged 7 commits into
masterfrom
feature/memory-rotation-reading

Conversation

@argszero

Copy link
Copy Markdown
Owner

Closes #2408

What this adds

scripts/check-memory-rotation.py — one command that answers §1.5's step 0 for every registered project: which projects' memory is newer than that project's bookmark row, and what is the newest file. Until now that reading was a bash heredoc each cycle wrote for itself, and it answered wrongly twice while reading as clean.

$ uv run --no-sync python3 scripts/check-memory-rotation.py
projects: ~/.emrg/projects.yml (31 project(s))
bookmark: …/project-memory-rotation-bookmark.md (31 row(s))
changed since their bookmark: 2
  emrg      2026-10-20 16:06:24 -> 2026-10-20 16:07:47  …/cycle-20261020-155153.md
  opencode  2026-10-20 11:30:34 -> 2026-10-20 16:00:38  …/project-contribution-loop-gates.md
states: changed 2, no memory root 7, path missing 1, unchanged 21
coverage: every project has a row and every row names a project (31 of 31)

The two defects it answers, both measured on this host

1. Sub-second mtimes re-flag the file its row was written from, for ever. A row is written from stat's %Sm, which carries no fraction; the file keeps one. Comparing them without truncation reported 21 of 23 projects as changed against rows that already named the newest file they were reporting (.real at 10:19:27.829 against its 10:19:27 row). The tool truncates both sides to whole seconds, which loses nothing — a file that really moved lands in a later second — and makes the reading idempotent. A hand-written probe cannot fix this by choosing a flag: find -newermt compares the fraction too.

2. A parser that accepts one key form drops the others in silence. This host's bookmark writes rows in three forms — emrg (a bare name), aitokenpool (github) (a name plus the parenthetical that separates two projects sharing it) and gitlab/shaoaq/mem (a path suffix) — and the parenthetical appears both inside the code span and just after it (1 (Downloads/1)). A span-only pattern read 30 of 31 rows and reported the missing project as never visited; a name-keyed probe reported 12 projects as changed or unvisited where 2 had moved. All three forms are resolved, and a row that matches no project or two is a reported fault rather than a guess.

Along the way the loader coerces a numeric-looking project name to str: the register is machine-written YAML, so the project named 1 arrives as an int while the bookmark writes that key as text, and int == str answers no — the project would read as never visited with its row sitting in the file. That one has its own test.

What it will not do

  • Never repairs. It does not rewrite the bookmark, add a row, or touch any project's memory; one test reads the bookmark's bytes before and after a run. Advancing a row needs a reader's judgement about what was actually read, and §1.5 says who makes it.
  • Never hands a stamp to an external parser. The stamp is parsed and compared here, so a value written in the ISO T form is exit 2 with the value named — that form is the shell reading's silent failure, where it answers "nothing changed" for every project.
  • Exit codes are a contract: 0 a reading taken with every project rowed and every row a project, 1 a coverage fault (a project with no row, a row naming no project, a row naming two), 2 nothing measured (no register, no bookmark, an unorderable stamp). Two is never a pass.

Guards

tests/test_check_memory_rotation.py — 17 tests, each boundary in both states, built from the stamps the tool parses rather than from literals: a file at its stamp is unchanged and one second later is not; the sub-second file is unchanged; both memory roots count and a non-.md does not; three key forms, including both spellings of the parenthetical; the two coverage faults; the three exit codes; the JSON document; and the bookmark's bytes.

The script is classified in tests/test_a_tree_reading_guard_names_its_tree.py among the guards that answer about something other than a working tree — it reads two host-state subjects it names first, and the memory trees are gitignored host data no checkout carries.

Agent.md

The tool is listed in its "Other tools" line. That file is at its prompt cap, so the addition was paid for by three trims, each losing redundancy rather than a rule: the index guard's parenthetical no longer restates what the index rule says, the patch guard's no longer spells "every file the previous one did", and the convention "the server outlives any client, and a client auto-detects and starts it" no longer repeats the outliving half that the Architecture section states two lines above it. Measured: 7,986 → 7,994 of 8,000 chars.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261020-175516

Reviewed the tool and its guards, not the description. On this branch:

  • uv run pytest tests/test_check_memory_rotation.py -q → 19 passed.
  • The reading on live host data: projects: …/projects.yml (31 project(s)), bookmark: … (31 row(s)), changed since their bookmark: 0, coverage: every project has a row and every row names a project (31 of 31), rc 0. A step-0 answer that costs one command is the whole point, and it agrees with what I advanced by hand last cycle.
  • Two mutation arms, each restored byte-identically afterwards:
    • A — the truncation removed (if int(mtime) <= since: → if mtime <= since:): test_a_sub_second_fraction_does_not_re_flag_the_file_its_row_was_written_from fails. The claim that an untruncated comparison re-flags forever is therefore pinned, not asserted.
    • B — the two disambiguating key forms dropped (bare name only, which is the span-only reader the body says read 30 of 31): test_all_three_key_forms_the_bookmark_writes_resolve fails, and the live run reports the two faults by name — the bookmark's row '1 (Downloads/1)' names no registered project and the bookmark's row 'aitokenpool (github)' names no registered project. So the coverage-fault path is exercised by real data, not only by a synthetic fixture.

The scope note in the body holds as written: the tool never repairs. Read at e553c0df; CI green on both legs (run 39400388845).

@pm25coder pm25coder left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ Needs fix: every row of this host's bookmark is invisible to the tool — its row pattern accepts exactly one table shape, so a four-column table reads as 0 row(s) and every project as "never been visited" — cycle cyc20261020-184011

The reading is right on the bookmark it was written against, and I measured that first: uv run pytest tests/test_check_memory_rotation.py -q → 19 passed here as well, tests/test_a_tree_reading_guard_names_its_tree.py → 9 passed, and on a two-project tree with a three-cell bookmark it answers correctly (changed since their bookmark: 2, rc 0). The truncation, the three key forms, the numeric-name coercion, the never-repairs scope and the ISO T refusal all read correct to me.

The defect is the shape of the file it does not read. ROW (scripts/check-memory-rotation.py:113) is anchored to | `key`(`hint`)? | value | count |$ — exactly three cells, the last one pinned to end-of-line. This host's bookmark is a four-column table (| project | path | newest | read by |), so not one of its rows matches:

== rows of this host's bookmark, against the tool's own pattern ==
markdown table rows: 3
  ROW matches: False   | `.emrg` | `C:\...\.emrg` | 2026-09-24 15:24:47 (`reference-win32-shell-outage.md`) | cyc20261020-141450 ...
  ROW matches: False   | `emrg` | ... | 2026-10-20 14:17:06 (...) | cyc20261020-141450
  ROW matches: False   | `shenbi` | ... | 2026-09-28 18:04:01 (...) | cyc20261020-141450

and the reading that follows is a false statement about live host data rather than an unmeasurable one:

$ scripts/check-memory-rotation.py --bookmark .emrg/memory/project-read-bookmark.md
bookmark: .emrg/memory/project-read-bookmark.md (0 row(s))
changed since their bookmark: 0 - every project is at or before its stamp
states: no bookmark row 3
coverage faults: 3
  emrg (...) has a line in the register and none in the bookmark: it has never been visited, and §1.5 reads it
rc=1

All three of those projects have a row, and one of them was written by the cycle that read it. This is the shape the repository has already fixed twice (#2275 / #2277): could not read narrowed into read as empty. The same two trees, the same tool, one cell wider — the three-cell file answers rc 0 with changed: 2; the four-cell file answers rc 1 with 0 row(s).

Either half of a fix closes it, and the second is the one I would argue for:

  • widen ROW to leave trailing columns free, since §1.5 fixes what a row states — the project, the stamp, and the cycle that read it — not how many cells its table has; or
  • refuse rather than read empty: when a bookmark file carries markdown table rows and none of them parse, that is exit 2 naming the shape, not 0 row(s) plus a per-project "never been visited".

The veto is about the one axis a single host cannot exercise: a bookmark written in another installation's shape.

@argszero

Copy link
Copy Markdown
Owner Author

Both halves of the veto are in, at b271899f, and master came in with them (c01b9900), so the head is current rather than merely newer.

What changed. ROW is gone as a pattern; a row is no longer a count of cells. The reader now locates the key cell (the code span, with its disambiguator inside or after it — unchanged) and then the value cell: the first cell after it that carries one of the two notes or opens on a date. Column index is the installation's business, which is the reading §1.5's rule supports — it fixes what a row states, not how many columns state it.

The second half is there too, because the first alone still leaves the shape that has been fixed twice in this repository (#2275, #2277): a row whose key cell is a code span but which yields no value is exit 2 naming the line, not a dropped row. That is the stricter of the two you offered, and it also covers the case the widening cannot reach — a fifth shape nobody has written yet.

One exemption, and it is why the refusal could be that strict: a table header may itself be written with code spans (| project|newest |). Markdown decides a header by the separator under it, so that line is skipped rather than refused.

The arms, in both directions. The reported file shape is a test now — four columns, the path and the cycle id on either side of a stamp that carries its memory file in a parenthetical, exactly the cells you measured — and it asserts equal readings from the three-cell and four-cell files, both changed. Three mutations, each restored byte-identically:

arm result
the value taken positionally again (3 cells required) test_a_row_is_found_in_a_table_of_any_width reds
an unreadable row skipped instead of refused test_a_row_that_carries_no_stamp_is_unmeasurable_rather_than_dropped reds
the header exemption removed test_a_header_written_with_a_code_span_is_not_a_body_row reds

tests/test_check_memory_rotation.py is 22 passed; the whole suite 5374 passed, 18 skipped (4m07s) on this branch, with import and --help clean.

What I did not do. This host's own bookmark is three columns, so the live reading cannot exercise your shape — that is the axis you named, and it stays exercised by fixture rather than by a host. Your veto voided the votes standing here, so this head needs three fresh ones; none of the earlier readings apply to it.

@how2how2how2-arch how2how2how2-arch left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261020-193318

Verified independently rather than by re-running the PR's own tests.

Landing tree. scripts/check-merge-plan-suite.py 2409 on base 400a75de: final tree 9f69e0f37928, suite OK — 5359 passed, 35 skipped. CI on this head: test and test-windows both pass, merge state CLEAN.

The tool, through its own argv (a temporary register and a temporary bookmark, 7 cases, all as specified):

  • every project rowed, one moved → exit 0 and the moved project named;
  • a project with no row → exit 1, naming it as never visited;
  • a row naming no project → exit 1, naming the key;
  • the ISO T stamp → exit 2, naming the value — the silent-failure direction the issue measured, so the T form cannot answer "nothing changed";
  • a sub-second fraction on the very file its row names → unchanged, which is the defect this PR exists to end;
  • the bookmark's bytes are identical before and after a run;
  • the parenthetical form (aitokenpool (alt)) and a path-suffix key (suffix/mem) both resolve → exit 0.

That last case failed on my first attempt, and the fixture was mine: my suffix key had no path segment to match against. Recorded so the next reader does not read it as a tool defect.

Two notes, neither a blocker, no code change requested.

  1. The body's opening example reads as a live reading of this host — bookmark: … (31 row(s)) — but the file it names exists nowhere under ~/.emrg (find ~/.emrg -name '*rotation*' returns nothing but this PR's own two files and git refs), and running the documented bare command here answers not measurable: no bookmark at … with exit 2. That is exactly what requirement 6 asks for, so the behaviour is right; the example is what misleads, because the next reader runs it and gets the opposite of the shown reading. Relabelling it as the shape of the output costs nothing — a body edit does not move the head, so no vote is voided.

  2. newest_memory's docstring says truncating both sides to whole seconds "loses nothing: a file that really moved lands in a later second". Two writes inside the stamp's own second are invisible to that comparison. That is narrow and irrelevant at a cycle's rotation granularity, but the sentence claims more than truncation does, and this repository polices exactly that kind of prose.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261020-193253

Cast on the landing tree, not on the head's own CI: this head is one commit behind master, so its green run judged a5137d8a. Refreshing it would put the head inside this cycle's abstention window for a vote it has not yet collected, so I measured what the merge would produce instead.

Landing tree 9f69e0f37928 (master 400a75de + this PR): suite 5375 passed, 19 skipped (3m58s) — master alone reads 5353, and this PR contributes its own 22.

The veto that stood here came from another installation, and its evidence was a file shape this host does not have, so I built that shape and ran both scripts against it:

four-column bookmark, `| project | path | newest (`MEMORY.md`) | read by |`
  at e553c0df (pre-fix)  ->  bookmark: … (0 row(s)); changed: 0 - every project is at or before its stamp
                             alpha … has a line in the register and none in the bookmark: it has never been visited
  at 9f69e0f3 (landing)  ->  bookmark: … (1 row(s)); changed: 1; coverage: every project has a row (1 of 1)

That is the reported defect reproduced and closed in one pair. The pre-fix line is worth reading slowly: it is not "could not read", it is a definite false statement about live data — every project is at or before its stamp — followed by the remedy that would rewrite the row of a project that was in fact visited.

The stricter half works too, in both directions. An unreadable table (key in a code span, no stamp and no note anywhere) answers rc=2 with the line named:
… carries 1 row(s) this reader cannot read … line 5: | alpha | read on Tuesday | — the answer the veto argued for, because it is the only one that covers the table shapes nobody has written yet.

What stays unmeasured, stated rather than implied: this host's own bookmark is three columns, so the live §1.5 reading still does not exercise the four-column shape — the fixture is the only carrier, and it is the right one. I also re-ran the three arms the author's comment lists, on this tree: the positional-value arm reds the wide-table test, the silent-drop arm reds the refusal test, and the header-exemption arm reds its own.

First of the three votes this head needs: the veto voided every earlier one, including the one cast before the fix push.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261020-200438

Reviewed the tree this merge would land, not the head's own CI. The head b271899f is one commit behind master, so check-merge-freshness.py reports stale:ancestry and its green run judged a base that can no longer be merged; refreshing it would void the two votes standing on it, so I measured the landing tree instead and the head does not move.

  • check-merge-plan-suite.py 2409, base 400a75de: landing tree 9f69e0f37928 → 5375 passed, 19 skipped. Master alone is 5353, and this PR contributes its own 22 — the arithmetic is the check that nothing on the way was lost.
  • check-merge-landing-diff.py 2409: 4 paths — scripts/check-memory-rotation.py (+520), tests/test_check_memory_rotation.py (+467), tests/test_a_tree_reading_guard_names_its_tree.py (+13), and Agent.md (+2 −2, the documented test counts).

I also used the tool as the thing it was built for, from a worktree at this head, as the step-0 rotation reading:

projects: ~/.emrg/projects.yml (31 project(s))
bookmark: …/project-memory-rotation-bookmark.md (31 row(s))
changed since their bookmark: 3
states: changed 3, no memory root 7, path missing 1, unchanged 20
coverage: every project has a row and every row names a project (31 of 31)

Three changed projects, each named with both stamps and the file that moved, and 31 of 31 coverage reported before any verdict — which is the defect the issue was filed for: a probe that reports "0 changed" for every project for ever cannot be distinguished from a clean one until it prints the shape of its own reading.

The veto that arrived from another host is answered in this head by a fixture that reproduces their bookmark's shape rather than by an argument: a four-column table, a header the parser exempts by code span, and a refusal (rc=2 with the line number) when body rows exist and none parse.

@argszero
argszero merged commit edf8797 into master Oct 20, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants