Skip to content

emrg: the index-size rule reaches the round that creates the index - #2663

Merged
pm25coder merged 1 commit into
masterfrom
fix/index-bound-reaches-the-creating-round
Oct 31, 2026
Merged

pm25coder merged 1 commit into
masterfrom
fix/index-bound-reaches-the-creating-round

Conversation

@pm25coder

Copy link
Copy Markdown
Collaborator

Closes #2662

What the issue got right, and where the measurement differs

The issue reads the memory-index rule in emrg/server/evolution_prompt.md R9 (lines 440 and 444) and
found 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 the
constants rather than written into the template.

  • {{ index_title_max_chars }} ← emrg/memory.py:INDEX_TITLE_MAX_CHARS (512), injected at
    emrg/server/daemon.py:2499
  • {{ memory_index_row_cap }} ← emrg/server/daemon.py:MEMORY_INDEX_ROW_CAP (100), injected at
    emrg/server/daemon.py:2509
  • read out of the prompt a session receives, not out of the template, by
    tests/test_memory_row_bound_reach.py::test_the_rendered_prompt_carries_the_stores_line_cap and its
    sibling.

So a task session does not need either number repeated in its <task>_prompt.md: it already receives
both 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:456 says
in 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.md and
open_source_prompt.md therefore stay qualitative on purpose; the quantified rule reaches them from
the 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 index
and 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:

has 'No memories yet': True
has pointer-list line: False
carries row bound   512 : False
carries line cap    100 : False

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 ## Memory list.
  • tests/test_memory_row_bound_reach.py — a new reading on a session with no index, which first
    asserts 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

  • Before-arm: with this PR's template edit reverted to master's file, the two new readings fail —
    assert bound_line on the no-index prompt, assert off_bound in the injection test — while the
    other four in the file pass. After-arm: 73 passed across test_memory_row_bound_reach.py,
    test_system_prompt_structure.py and test_prompt_templates.py. The template was restored
    byte-identically between arms (sha256 066EFF6C…).
  • Full suite on this tree: 5562 passed, 518 skipped, 909.01s, rc 0 — 6080 collected, which is the
    6079 measured on master plus this file's one new test.
  • from emrg.client.app import run_client rc 0; python -m emrg --help rc 0.
  • On this host the daemon imports the install tree (~/.emrg/install/source) ahead of a checkout, so
    both arms ran with the checkout first on sys.path and printed emrg.__file__ to say which tree
    they measured.

CI: pending.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

✅ 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_numbers FAILED and test_the_number_is_injected_not_written_in_the_template FAILED, while the file's other four passed. That is the before-arm the body claims, node for node.
  • A literal 512 in place of {{ index_title_max_chars }}: test_the_shared_template_states_the_bound FAILED and the injection test FAILED.
  • A literal 100 in place of {{ memory_index_row_cap }}: test_the_shared_template_states_the_line_cap FAILED 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 pm25coder left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

✅ 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.j2 swapped in byte-exactly (sha256 1c05ddd2…, written from git cat-file, not from a redirected git 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), and git status clean at 066EFF6C… in the worktree's checked-out form.
  • Not a literal — a scratch copy of the package with the two constants moved (INDEX_TITLE_MAX_CHARS 512 → 777, MEMORY_INDEX_ROW_CAP 100 → 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 ## Memory rather 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 pm25coder left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

✅ 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 ## Memory section is 1,485 characters and the sentence is its first bullet (on master it rendered after A 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.py 512 → 777, emrg/server/daemon.py 100 → 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.j2 back (read through git 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 of test_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 - within against 101 - over by 1, and a 512-character row at over 512: 0 against a 513-character row at over 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 ## Memory section, and A write is two steps still 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-windows both 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.

@pm25coder
pm25coder merged commit d7960c8 into master Oct 31, 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

Development

Successfully merging this pull request may close these issues.

emrg: the task templates keep an index by a bound only the evolution prompt states

2 participants