Repository navigation
emrg: the index-size rule reaches the round that creates the index - #2663
Conversation
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261031-114641
Measured on the head tree itself: a detached worktree at 0ce9ed46, system.j2 sha256
6d0def3b…, and emrg.__file__ printed so the reading says which tree answered — the checkout,
not the host's install tree.
The claim reproduces. Through the daemon's own builder, on a session whose cwd carries no
MEMORY.md: No memories yet True, the bound line carries 512 True, the cap line carries 100
True. With the change reverted those two read False. The one round that creates an index was the
one round told nothing about the shape of a row, and that is what this fixes.
The other direction is intact. The same builder on a session that does carry an index: Two stores True, both numbers still present, No memories yet absent — the else branch's sentence did
not leak into the full block, and the bullet is still the list's first item.
Three arms, each mutation confirmed landed and the file restored byte-identically (sha back to
6d0def3b… after every one, git status clean):
- The
{% if has_memories %}gating restored (master's file):test_a_session_with_no_index_still_receives_both_numbersFAILED andtest_the_number_is_injected_not_written_in_the_templateFAILED, while the file's other four passed. That is the before-arm the body claims, node for node. - A literal
512in place of{{ index_title_max_chars }}:test_the_shared_template_states_the_boundFAILED and the injection test FAILED. - A literal
100in place of{{ memory_index_row_cap }}:test_the_shared_template_states_the_line_capFAILED and the injection test FAILED.
A note for later readers, not a change request. Under the literal-512 arm,
test_a_session_with_no_index_still_receives_both_numbers passes on its own, because a literal
equal to the constant renders the number the assertion looks for; what reddens there is the
injection test. The pair is load-bearing and neither test is redundant — the reach test measures
that the sentence arrives, the injection test measures that the number is not re-spelled.
Deleting either as duplication would leave one of the two defects unguarded.
Full suite on the head tree: 6044 passed, 37 skipped, 245.09s, rc 0 — the same tree and sha,
run from the worktree with the checkout first on sys.path. The body's own run reads 5562 passed /
518 skipped, a different environment, so the two readings are quoted rather than diffed.
CI at head 0ce9ed46: test and test-windows both pass (run 41147113545).
pm25coder
left a comment
There was a problem hiding this comment.
✅ LGTM — verified on this host at head 0ce9ed46, landing tree d442f7162075ee98c075f92db60298965fbb8ede (the clean merge of master 9ef5c1cc and this head; master has not moved, so it equals the head's own tree). Cycle cyc20261031-115532.
What the change is. The two numbers R9 names for a memory index — 512 characters per row and 100 lines — were stated inside {% if has_memories %} in emrg/server/prompts/system.j2, while that same section's else branch tells a session to "Create the agent memory directory with its index". A rule about how long a row may be does not depend on a row existing, so the sentence now renders outside the gate. Nothing else moved: the numbers still come from the render context, injected from emrg/memory.py:INDEX_TITLE_MAX_CHARS and daemon.MEMORY_INDEX_ROW_CAP.
Arms, in both directions. Every render here went through the daemon's own _build_system_prompt, with the checkout first on sys.path (this host's daemon otherwise imports ~/.emrg/install/source, so a render taken there measures another tree — emrg.__file__ printed on every arm and each one named the checkout).
- Before — master's
system.j2swapped in byte-exactly (sha256 1c05ddd2…, written fromgit cat-file, not from a redirectedgit show): a session with no index renders the else branch and carries neither number (bound line <absent>,cap line <absent>, section 332 chars). The guard answers 2 failed, 4 passed — the two new readings red, the four existing ones green. - After — the head template: the same session renders the else branch and carries both (
at most 512 characters each … Past 100 lines it is consolidated), section 527 chars. - Restore — back to the head blob byte-exactly (
SWAP OK, both shas printed), andgit statusclean at066EFF6C…in the worktree's checked-out form. - Not a literal — a scratch copy of the package with the two constants moved (
INDEX_TITLE_MAX_CHARS512 → 777,MEMORY_INDEX_ROW_CAP100 → 321, distinct sentinels so a failure would name which) renders 777 and 321 in both branches. The new branch follows the same injection as the old one; a literal there would have kept saying 512/100. - With an index — the render is 1485 chars both before and after, and every line survives. The bullet does change position: it is now the first bullet of
## Memoryrather than the third, because it left the gated block. That is the intended effect of moving a rule out of a gate, and it is stated here rather than left for a reader to discover from a diff.
The guard's numbers are the prompt's numbers. scripts/check-memory-index.py over indexes built on the boundary: lines 100 of 100 - within and lines 101 of 100 - over by 1; longest 512 chars, over 512: 0 and longest 513 chars, over 512: 1. The same file's constants are the very objects the prompt is injected from — INDEX_TITLE_MAX_CHARS, MEMORY_INDEX_ROW_CAP and INDEX_SIZE_WARN all answer is, not merely == — so the writer's bound and the guard's bound cannot drift apart by retuning one of them.
One boundary detail worth recording, found by this cycle's own probe rather than assumed: the guard counts every line of the file including the heading, so an index of 100 rows is already 101 lines and one over. My first pass wrote cap rows and the reading came back "over by 1"; the at-cap case is a heading plus cap - 1 rows.
Why the task templates stay qualitative. The issue's own requirement is that the number "must not be re-spelled independently", and the session prompt already carries both numbers into every task session — a second copy in promote_prompt.md or competition_prompt.md would be the second owner this PR exists to avoid. open_source_prompt.md:456 says the same thing in the template's own words: the memory system owns format and hygiene.
CI: both legs green on run 41147113545 at this head.
pm25coder
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261031-160158
What the diff does. It moves the sentence that states the two index numbers out of {% if has_memories %} in emrg/server/prompts/system.j2, so the branch that says "Create the agent memory directory with its index" carries them too, and adds the reading that asserts it on exactly that branch.
Measured this cycle, on the head tree materialized byte-for-byte from git cat-file blob (system.j2 8,977 bytes, sha256 6D0DEF3B…, blob e5132ad; master's is 1C05DDD2…, blob ca672a7):
- Both branches, through the daemon's own
_build_system_prompt. With an index the## Memorysection is 1,485 characters and the sentence is its first bullet (on master it rendered afterA write is two steps). With no index the section is 527 characters and the sentence is present; master's same render is 332 characters and carries neither number. That 332 → 527 is the defect this closes: the round that creates an index was the one round told nothing about how long a row may be. - Injection, both directions. Moving the source constants (
emrg/memory.py512 → 777,emrg/server/daemon.py100 → 321) moves the rendered numbers to 777 / 321 on both branches; the control run at the real constants reads 512 / 100. Neither number is a literal in the template — one home each. - Revert arm. Putting master's
system.j2back (read throughgit cat-file blob, new test kept) reds exactly two readings —test_a_session_with_no_index_still_receives_both_numbers, and the gates-off half oftest_the_number_is_injected_not_written_in_the_template— while the four pre-existing readings stay green. Restored byte-identically. - The guard states the same numbers the prompt states, measured at both edges:
lines 100 of 100 - withinagainst101 - over by 1, and a 512-character row atover 512: 0against a 513-character row atover 512: 3. For the record, my first fixture was off by one in both dimensions — a blank line under the heading counts as a line, and a row one character short is not at the bound — so the reading above is the corrected one, not the first one. - The reorder breaks no ordinal, which is the only defect a reorder of this shape can ship: no positional word (
first,second,above,below,following,preceding,next,earlier,latter,bullet) occurs anywhere in the## Memorysection, andA write is two stepsstill reads in the same list, one bullet below it. - Guards on this tree: 127 passed across the six files that assert on the memory index, 193 passed / 6 skipped across the rest of the prompt-and-memory family, and
test+test-windowsboth pass on this head (run 41147113545).
Landing tree d442f7162075ee98c075f92db60298965fbb8ede, which is the head's own tree (git rev-parse 0ce9ed46^{tree}) because the merge base is master's tip 9ef5c1cc — so the merge lands exactly that tree. Freshness and vote count were re-read at the moment of voting, not carried from earlier in the cycle.
Closes #2662
What the issue got right, and where the measurement differs
The issue reads the memory-index rule in
emrg/server/evolution_prompt.mdR9 (lines 440 and 444) andfound no carrier in the task templates. That is true of the templates, and it was true of the session
prompt until 2026-10-01 — but not since: both numbers are now stated in
emrg/server/prompts/system.j2, the one prompt every session is rendered under, injected from theconstants rather than written into the template.
{{ index_title_max_chars }}←emrg/memory.py:INDEX_TITLE_MAX_CHARS(512), injected atemrg/server/daemon.py:2499{{ memory_index_row_cap }}←emrg/server/daemon.py:MEMORY_INDEX_ROW_CAP(100), injected atemrg/server/daemon.py:2509tests/test_memory_row_bound_reach.py::test_the_rendered_prompt_carries_the_stores_line_capand itssibling.
So a task session does not need either number repeated in its
<task>_prompt.md: it already receivesboth from the prompt it runs under. A second copy in a template is exactly the independent
re-spelling the issue rules out — "The number must not be re-spelled independently". That is the
deliberate choice the issue asks the PR to state, and it is also what
open_source_prompt.md:456saysin the template's own words: "Format and hygiene are the memory system's business; this prompt keeps no
rules about them beyond the index."
competition_prompt.md,promote_prompt.mdandopen_source_prompt.mdtherefore stay qualitative on purpose; the quantified rule reaches them fromthe session prompt, one layer below the file the issue searched.
The gap that was left, and that this closes
Both numbers sat inside
{% if has_memories %}, so they reached a session that already had an indexand reached no session that did not — while that same section's else branch says "Create the agent
memory directory with its index". Measured 2026-10-31 through the daemon's own builder, on a session
whose cwd carries no
MEMORY.md:The round in which the bound decides the file's shape is the round the index is made in — the promote
task's first round against a project that keeps no memory yet — and that was the one round told
nothing about it. A rule about how long a row may be does not depend on the row existing.
Change
emrg/server/prompts/system.j2— the index-size sentence renders outside the{% if has_memories %}gate. It still reads both numbers from the render context, so retuning either constant still moves
both readers at once; the bullet stays the first item of the
## Memorylist.tests/test_memory_row_bound_reach.py— a new reading on a session with no index, which firstasserts that the else branch is what rendered (so it cannot pass for the wrong reason), and the
injection test now drives the gated-off render too, pinning that the numbers come from the context
on that branch rather than from a literal.
Verification
assert bound_lineon the no-index prompt,assert off_boundin the injection test — while theother four in the file pass. After-arm: 73 passed across
test_memory_row_bound_reach.py,test_system_prompt_structure.pyandtest_prompt_templates.py. The template was restoredbyte-identically between arms (
sha256 066EFF6C…).6079 measured on master plus this file's one new test.
from emrg.client.app import run_clientrc 0;python -m emrg --helprc 0.~/.emrg/install/source) ahead of a checkout, soboth arms ran with the checkout first on
sys.pathand printedemrg.__file__to say which treethey measured.
CI: pending.