Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 23 additions & 13 deletions scripts/check-memory-rotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ def _read_by_of(cells: list[str]) -> str | None:
#: answers "32 of 32" while the file it is embedded from grows every cycle.
CYCLE_SECTION = re.compile(r"^##\s+Cycle\b")

#: The opening of a **per-cycle narration block** above the bookmark's table.
#: The opening of a **per-cycle narration block**, wherever it sits in the bookmark.
#:
#: A cycle's note is usually a paragraph, not a heading: it opens with its own id in a code
#: span and the next cycle replaces it. `CYCLE_SECTION` cannot see that shape, and measured
Expand Down Expand Up @@ -456,12 +456,19 @@ def cycle_sections(path: Path) -> list[str]:


def narration_blocks(path: Path) -> list[str]:
"""The per-cycle narration above the bookmark's table, one entry per block.
"""The bookmark's per-cycle narration, one entry per block, above the table or below it.

A block is a run of non-blank lines, and it counts as narration when its **first** line
opens with a cycle id in a code span (:data:`CYCLE_NARRATION`) - the shape a cycle's own
note has taken. The reading stops at the table's first row, because below it the file is
rows and not prose.
note has taken. A table row ends a *block*; it does not end the reading, because the
bookmark is not obliged to keep its prose above its table.

That boundary was once a `break`, on the assumption that below the table the file is
rows and not prose. Measured 2026-11-07 on this host's own bookmark, the assumption
failed and took the reading with it: **29** runs holding **39,411** characters of
per-cycle notes had accumulated below the table, and the structure reading answered
`0 cycle section(s) and 1 narration block(s) above the table` while exiting 0. Growth
that the file's own rule forbids had simply moved past the line the reader stopped at.

This exists because :func:`cycle_sections` reads only `## Cycle` headings, and the
ordinary note carries none: a cycle replaces the paragraph that came before it rather
Expand All @@ -487,7 +494,10 @@ def narration_blocks(path: Path) -> list[str]:
current: list[str] = []
for line in text.splitlines():
if _is_table_line(line):
break # the table is what the file exists for
if current: # a row ends a block; it does not end the reading
blocks.append(current)
current = []
continue
if line.strip():
current.append(line)
elif current:
Expand All @@ -499,7 +509,7 @@ def narration_blocks(path: Path) -> list[str]:


def structure_faults(headings: list[str]) -> list[str]:
"""The faults in the bookmark's structure **above** its table, not in its rows.
"""The faults in the bookmark's structure, as against the faults in its rows.

A separate list from the coverage faults because they are a different subject with a
different remedy: a coverage fault is repaired by writing a row, this one by writing a
Expand Down Expand Up @@ -532,10 +542,10 @@ def structure_faults(headings: list[str]) -> list[str]:
def narration_faults(blocks: list[str]) -> list[str]:
"""The faults in the bookmark's per-cycle narration: it keeps **one**, replaced in place.

The same subject as :func:`structure_faults` - growth above the table - given the same
treatment and for the same reason: a fault, never a refusal, so every row is still
measured and the exit code carries the file-shape complaint without throwing away a
valid rotation reading.
The same subject as :func:`structure_faults` - growth of either shape, wherever in the
file it sits - given the same treatment and for the same reason: a fault, never a refusal,
so every row is still measured and the exit code carries the file-shape complaint without
throwing away a valid rotation reading.

One block is the contract's shape (the current cycle's note) and none is a minimal or
first-ever bookmark. Only a second block is the growth the file's own rule forbids, and
Expand All @@ -548,7 +558,7 @@ def narration_faults(blocks: list[str]) -> list[str]:
return []
named = ", ".join(blocks[:5]) + (f", and {len(blocks) - 5} more" if len(blocks) > 5 else "")
return [
f"the bookmark carries {len(blocks)} per-cycle narration blocks above its table "
f"the bookmark carries {len(blocks)} per-cycle narration blocks "
f"({named}): it keeps one and replaces it in place - an appended note is the same "
f"growth the cycle-section reading forbids, in the shape that reading cannot see "
f"(measured 2026-11-05: 43 blocks over 729 lines and 94,482 characters, while the "
Expand Down Expand Up @@ -864,12 +874,12 @@ def render(reading: dict, show_all: bool) -> str:
if reading["structure"]:
out.append(f"structure faults: {len(reading['structure'])}")
out.extend(f" {fault}" for fault in reading["structure"])
out.append("the rows above are still readable - this is the prose above them: keep one "
out.append("the rows above are still readable - this is the prose around them: keep one "
"cycle section and one narration block, and replace them in place, never "
"append a second")
else:
out.append(f"structure: {reading['cycle_sections']} cycle section(s) and "
f"{reading['narration_blocks']} narration block(s) above the table - "
f"{reading['narration_blocks']} narration block(s) in the file - "
f"the file keeps at most one of each, replaced in place")
return "\n".join(out)

Expand Down
49 changes: 49 additions & 0 deletions tests/test_check_memory_rotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,55 @@ def test_a_second_narration_block_is_a_fault_the_section_reading_cannot_show(mod
assert mod.main(["--agents", str(register), "--bookmark", str(three)]) == 1


def test_a_narration_block_below_the_table_is_read_too(mod, tmp_path):
"""Measured 2026-11-07 on this host's own bookmark: 29 runs, 39,411 characters below it.

The reading used to stop at the table's first row, on the assumption that below it the
file is rows and not prose. The assumption failed the moment a cycle appended its note
after the table instead of before it: the file reached 358 lines carrying 39,411
characters of per-cycle notes while the structure reading answered `0 cycle section(s)
and 1 narration block(s) above the table` and exited 0. Growth the file's own rule
forbids had moved past the line the reader stopped at, so the boundary is a block
boundary and not the end of the reading.

Both directions are asserted, because a reader that counts any prose after the table
would fault the paragraph a bookmark uses to state its own rule.
"""
root = _root(tmp_path, "belowtable")
_write_memory(Path(root), ".emrg/memory/MEMORY.md", _stamp(mod))
register = _agents(tmp_path / "agents.yml", [{"name": "belowtable", "path": root}])

appended = tmp_path / "appended.md"
appended.write_text(
"# Bookmark\n\n"
"`cyc20261102-120000` the note a cycle replaces.\n\n"
"| agent | newest read | files |\n| --- | --- | --- |\n"
f"| `belowtable` | {BASE} | 0 |\n"
"\n`cyc20261103-120000` a second note, appended after the table.\n", encoding="utf-8")
reading = mod.read(register, appended)
assert reading["faults"] == [], reading["faults"]
assert reading["narration_blocks"] == 2, (
"a note below the table is not counted, so growth appended after the table is "
"invisible to the reading that exists to catch it")
assert len(reading["structure"]) == 1, reading["structure"]
assert mod.main(["--agents", str(register), "--bookmark", str(appended)]) == 1

rule = tmp_path / "rule.md"
rule.write_text(
"# Bookmark\n\n"
"`cyc20261102-120000` the note a cycle replaces.\n\n"
"| agent | newest read | files |\n| --- | --- | --- |\n"
f"| `belowtable` | {BASE} | 0 |\n"
"\nThe rows above are replaced in place; this paragraph states the rule.\n",
encoding="utf-8")
kept = mod.read(register, rule)
assert kept["narration_blocks"] == 1, (
"prose after the table that no cycle id opens is the bookmark stating its own rule, "
"not a cycle's note")
assert kept["structure"] == [], kept["structure"]
assert mod.main(["--agents", str(register), "--bookmark", str(rule)]) == 0


def test_contract_prose_and_a_mid_sentence_cycle_id_are_not_narration(mod, tmp_path):
"""The other directions: only a block that **opens** on the code span is a cycle's note.

Expand Down
Loading