Skip to content

fix(memory): keep loop rules resident and trim whole entries (#1443) - #1446

Merged
ozand merged 5 commits into
mainfrom
feat/1443-memory-resident-rules
Sep 9, 2026
Merged

fix(memory): keep loop rules resident and trim whole entries (#1443)#1446
ozand merged 5 commits into
mainfrom
feat/1443-memory-resident-rules

Conversation

@ozand

@ozand ozand commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Summary

Resolves #1443 by replacing positional tail slicing of memory/index.md with a structural resident/remainder policy:

  • The memory index preamble, ## Facts heading, and the five operational rules entries remain resident: Identity, Write target, DO NOT touch, Rules, and Key paths.
  • Remaining index lines are selected as complete entries from newest to oldest and restored in source order. No byte/character tail slice can cut a token or remove the resident rules block.
  • Memory-fit diagnostics are carried into ContextBuilder.last_fit["memory_index"], so dropped-entry count and byte counts are visible alongside the fix(context): degrade the prompt visibly instead of failing the cycle (#1438) #1440 prompt-fit ladder.
  • max_chars is unchanged at 4000.
  • The instance memory/index.md is not modified.

Live-size accounting (addendum)

Measured read-only on the live host against the 8,680-byte memory/index.md, with max_chars=4000:

resident rules block:       371 bytes
remainder before:          8,309 bytes
remainder kept after:     3,614 bytes
new memory context:        3,985 bytes
old tail context:          4,000 bytes
net change:                  -15 bytes
whole entries dropped:        28

Thus the resident rules are restored without increasing the memory section; the new result is 15 bytes smaller than the broken tail result. This is compatible with the measured 461-character prompt slack (23,539 / 24,000, 98.08%) and does not invoke the #1440 degradation ladder merely because the rules are restored.

Interaction with #1440

#1440 continues to operate on the assembled sections after get_memory_context. This change makes the memory section structurally safe before that ladder runs and adds memory_index accounting to the existing fit record. The ladder remains responsible for total prompt overflow; the memory reader no longer silently loses the front of the index or reports its post-truncation size as if no content had been dropped.

Tests

  • Added regression coverage for the discriminating shape: rules at the head, filler at the tail, small cap; all resident rules survive and no partial marker appears.
  • Added context-fit coverage asserting dropped memory entries are visible in last_fit["memory_index"].
  • Focused validation: 31 passed (test_memory_v2.py, test_context_prompt_fit.py, test_system_prompt_sections.py, test_log_format_style.py).
  • Full pytest: 3582 passed, 18 skipped, 4 failed; the four failures are exactly the required Windows baseline in test_bridge_cycle_tags.py and test_bridge_locking.py.
  • Clean origin/main baseline check: tests/test_memory_v2.py passed before the new regression was applied.

Files changed

  • nanobot/agent/memory.py
  • nanobot/agent/context.py
  • tests/test_memory_v2.py

An unrelated upstream #1441 log-format guard was restored in the branch after the milestone WIP history; no #1441 production/test behavior is part of this PR diff against current origin/main.

ozand and others added 5 commits September 9, 2026 04:37
Work-in-progress snapshot committed by the operator after the agent's
session was interrupted by a gateway 429. Not reviewed, not test-run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The resident block is selected by matching label text in memory/index.md --
an artifact the INSTANCE owns and can rename. A rename returns that rule to
the droppable remainder, which is precisely the failure this policy exists
to prevent, and it would happen silently: an unmatched label reads exactly
like a matched one.

last_index_fit now carries resident_matched, resident_expected and
resident_missing, and a mismatch logs a warning naming the missing labels.
Reported, never asserted -- the memory reader must not fail a cycle over
index text it does not own.

Tests pin both directions: a renamed "[DO NOT touch]" entry reports 4 of 5
with the missing label named, and an intact index reports 5 of 5 so the
counter cannot pass vacuously.

Verified against the live 8,680-byte index: 5/5 matched, kept_chars 3985,
28 entries dropped -- agreeing with the PR's accounting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ozand

ozand commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

Reviewed and accepted; one commit added on top (86f937c).

The byte accounting is the right deliverable and it holds — I re-ran the policy against the live 8,680-byte index independently: kept_chars=3985, dropped_entries=28, net −15 bytes against the old tail. All five resident labels present in the output.

What I added, and why. The resident block is selected by matching label text in memory/index.md — an artifact the instance owns and can rename. A rename silently returns that rule to the droppable remainder, which is exactly the failure this policy exists to prevent, and nothing would have said so: an unmatched label reads identically to a matched one. That is the same class as the line-number and glob keys that failed open in #1321 and #1285.

last_index_fit now carries resident_matched / resident_expected / resident_missing, with a warning naming the missing labels. Reported, never asserted — the memory reader must not fail a cycle over index text it does not own. Tests pin both directions: a renamed [DO NOT touch] reports 4 of 5 with the label named, and an intact index reports 5 of 5 so the counter cannot pass vacuously.

One edge left open deliberately. How each session should go (facts/session-flow.md) is still dropped — I verified it is absent from the live-index output. It appeared in #1443's evidence list of entries falling off, but the Proposed direction section named five categories and this PR implements exactly those. Expanding the resident set is a policy call with a permanent cost (every added entry becomes un-droppable), so I am not making it silently. Noted on the issue instead.

Focused suites after my commit: 34 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.

The memory block keeps the last 4000 bytes, so the loop never sees its own Identity, Rules or DO NOT touch

1 participant