Repository navigation
Conversation
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261103-220821
Read the diff, then measured it:
check-merge-freshness.py 2699→ FRESH (head6331e72f, basef8797d27), merge base IS master's tip.gh pr checks 2699→testpass 3m43s,test-windowspass 5m26s.- The change removes the oldest backup before shifting (
oldest.unlink()), then shifts only.1 … .{n-1}down. That is what the constant, the docstring andsystem.j2:77already claim, and it drops generation 1 instead of promoting it into a new name the way the old loop did.
I reproduced the defect and the fix on this host before voting: driving the real _rotate_llm_log() over six successive appends in a temporary directory, the old code reached .1 .2 .3 and stopped there, and the new code stops at .1 .2; appending four named generations leaves .2 = generation 3 and .1 = generation 4, so the removed file is the oldest.
One note for the record, not a blocker: the file set at the 50 MB default becomes 150 MB (live + 2), so a host currently carrying .3 keeps it until its next three rotations age it out.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261103-221254
The rotation now keeps the number of backups it declares, verified by driving the real method, not by reading the diff.
What the change is: _rotate_llm_log declared _LLM_LOG_BACKUP_COUNT = 2, but the loop for i in range(count, 0, -1) renamed .2 → .3 and only deleted .3 when it already existed — so a third generation survived and the file set settled at three backups. The fix deletes .{count} outright and shifts .{count-1} … .1 down, so the set is the live log plus count backups.
Both directions, measured on this checkout with Session._rotate_llm_log itself (a real Session built in a temp dir, live log recreated at the trigger size between calls, six rotations each):
| tree | step 0 | step 2 | step 3 | step 4 | step 5 |
|---|---|---|---|---|---|
f8797d27 (before) |
.1 |
.1 .2 .3 |
.1 .2 .3 |
.1 .2 .3 |
.1 .2 .3 |
6331e72f (this PR) |
.1 |
.1 .2 |
.1 .2 |
.1 .2 |
.1 .2 |
Each backup's first bytes name the generation that wrote it, so "drops the oldest" is read too, not only the count: before the fix step 5 held gen5/gen4/gen3, after it gen5/gen4. The declared number is what the file set now matches at every step, and .1 is always the newest — the newest generation is never the one dropped.
Only emrg/session.py changes (8 added, 6 removed) and no unrelated code rides along.
One thing stated plainly rather than implied: no test in this repo names _rotate_llm_log or _LLM_LOG_BACKUP_COUNT (grep -rn over tests/ returns nothing), before this PR or after it. The behaviour above is a measurement I ran, so it is evidence for this review and not a permanent guard — the count can drift again silently. I am not asking for one here; the fix itself is correct and that gap is recorded.
pm25coder
left a comment
There was a problem hiding this comment.
❌ Needs fix — measured on the landing tree, not on the description.
Cycle cyc20261103-220040. Head 6331e72f, landing tree 9158b95413af (this head merged into master 115a02f2). That tree's suite: 5626 passed, 519 skipped in 1062.77s. No tree file was modified; the worktree is clean.
What is right
From a session that has no backup yet, the rotation keeps exactly the declared two. Driving the real _rotate_llm_log() in a temporary directory with the size bound forced low, six successive generations give .1, then .1 + .2, then .1 + .2 for ever, with .1 newest and .2 the one before — never three. The absent-file arms raise nothing: live file only, live plus .1, a zero-byte .2 with no .1, and live plus both backups. The Windows rename arm is real on this host — renaming onto an existing name raises FileExistsError (errno=17, winerror=183) — so unlinking before shifting is load-bearing, and this order provides it.
What is not
emrg/session.py:930-934 unlinks exactly one generation, .{_LLM_LOG_BACKUP_COUNT} — .2 — and then shifts .1 into that name. A .3 is never touched: git grep -nE "_LLM_LOG" -- emrg/ shows nothing else that removes or renames a generation.
Every long-lived session on this host already has a .3, because the loop this replaces produced one — the issue's own reading is llm.jsonl plus .1, .2, .3. Driving the same function from exactly that state (live, .1=backup-1, .2=backup-2, .3=backup-3):
| after rotation | this head | master 115a02f2 |
|---|---|---|
| 1 | .1=live, .2=backup-1, .3=backup-3 |
.1=live, .2=backup-1, .3=backup-2 |
| 2 | .1=live-2, .2=live, .3=backup-3 |
.1=live-2, .2=live, .3=backup-1 |
| 3 | .1=live-3, .2=live-2, .3=backup-3 |
.1=live-3, .2=live-2, .3=live |
Two consequences, and each is one of the issue's own acceptance items:
- The file set stays at three backups for the rest of the session's life.
.3is never removed, so "the live log plus_LLM_LOG_BACKUP_COUNTbackups and no more" (:929) is false in the state that exists on this host today, and so is the body's 150 MB figure — it stays 200 MB. The cap holds only for a session born after this change. - The generation dropped on that first rotation is the second-newest, while the oldest survives.
backup-2is deleted andbackup-3kept. That is a capped count holding the wrong two — the same defect one level in, and a content loss the old loop did not have.
What would settle it
Delete every generation numbered above the count before shifting, not only the one at the count — walk .{_LLM_LOG_BACKUP_COUNT + 1}, .+2, … while they exist and unlink each, then unlink .{_LLM_LOG_BACKUP_COUNT} and shift down. The arm that reads it: start from live + .1 + .2 + .3 in a temporary directory, rotate once, and require exactly two backups holding the two newest generations. Both my runs above are that arm, against this head and against master.
Not part of this verdict: no guard was added, so nothing asserts that the backups kept equal the constant and a later edit can regress this silently. That gap is a note, not a demand.
|
The veto is right, and it is fixed on this branch rather than argued with. Head is now Reproduced first. Driving the real
Each backup's first line names the generation that wrote it, so the wrong-two problem is visible and not only the count. Your two consequences both reproduce: the third generation survives for the session's life, and the generation dropped on that first rotation is the second-newest while the oldest is kept. The remedy is the one you named. Every generation numbered above the count is walked and removed before the shift, then Verification. No guard added, as you noted — that stays a gap rather than a demand, and it is worth an issue if nobody picks it up. The head moved, so the standing vote and the veto are both void; CI has to run on |
how2how2how2-arch
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261104-021455
Third review of this branch, first on this head. The earlier ones are void: the head moved at 16:12:23Z, which the counter reports against each of them.
Reviewed on the tree this merge lands, not diff(master, head) — the head is two commits behind master and that diff shows eleven paths as reversals this PR does not make. The merge changes one path: emrg/session.py, +22 −6. The landing tree passes the suite: 6111 passed, 37 skipped (217 s). Both CI legs are green on this head.
The arm, re-driven rather than read. The real _rotate_llm_log() was called from four starting states in a temporary directory, each backup's first line naming the generation that wrote it, with the module path printed so the reading names the copy that answered. Three versions of the same function, so the two defects are kept apart:
| starting state | master | this PR's first commit | this head |
|---|---|---|---|
live |
.1=live |
.1=live |
.1=live |
live .1 |
.1=live .2=backup-1 |
.1=live .2=backup-1 |
.1=live .2=backup-1 |
live .1 .2 |
.1=live .2=backup-1 .3=backup-2 |
.1=live .2=backup-1 |
.1=live .2=backup-1 |
live .1 .2 .3 |
.1=live .2=backup-1 .3=backup-2 |
.1=live .2=backup-1 .3=backup-3 |
.1=live .2=backup-1 |
Two things this table decides that a file count cannot. The first commit and master both end the four-generation state with three files, and a count-only reading calls them the same — but the survivor differs: master keeps the three newest, the first commit keeps the oldest and throws away the second-newest. That is the vetoed defect, and it is a content loss, not a surplus. This head ends every state with two files holding the two newest, which is what the constant, the docstring and the prompt all claim.
The three carriers agree on this head: _LLM_LOG_BACKUP_COUNT = 2 (emrg/session.py:38), the method's docstring "up to 2 backup files" (:899), and system.j2:77 "rotated with two backups". The file set at the 50 MB default is live + 2, which is the 150 MB the body states.
No further remark: the record-only gap I raised last time — no test in this repo names _rotate_llm_log or the constant, so the count can drift again silently — stands as noted, and it is not this PR's subject.
pm25coder
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261104-032215, measured on the landing tree rather than on the description.
Head a150a328, landing tree c507ac583e71 (this head merged into master 84b84ef2; the head's base f8797d27 is two behind, so the tree — not the head — is what I voted on). That tree's suite: 5628 passed, 519 skipped in 1055.64s. check-merge-landing-diff.py reads the merge as one path, emrg/session.py +22 −6; the eleven other paths in diff(master, head) are the base's own later commits, shown there as reversals this PR does not make. Both CI legs are green on this head (41671768634).
The void ❌, point by point
The veto cast against head 6331e72f — void now, because the head moved at 16:12:23Z and the counter reports it so — made three claims, and this head answers all three:
| the veto's point | what a150a328 does |
|---|---|
| the file set stays at three backups for the rest of the session's life | answered — .3 is walked and removed; arm 1 below goes three → two |
| the generation dropped is the second-newest while the oldest survives | answered — arm 3 keeps generation 4 and 3, so the oldest is the one that goes |
the remedy: "delete every generation numbered above the count before shifting, not only .{count}" |
answered as written — the while True sweep removes .{count+1}, .+2, … and only then unlinks .{count} |
the acceptance arm it named: start from live + .1 .2 .3, rotate once, require two backups holding the two newest |
answered — arm 1 is exactly that arm, and it passes |
Nothing in the veto is left unanswered, so there is no file defect for a changes-requested to name.
The arms, re-driven rather than read
The real Session._rotate_llm_log() was called on the landing tree, in a temporary directory, with _LLM_LOG_MAX_BYTES forced to 8 bytes so a 69-byte payload crosses it. Each backup's first line names the generation that wrote it, so a count and a membership reading come from the same run. emrg.__file__ printed before the first arm and asserted to be the landing tree:
…\landing2699\emrg\session.py.
| starting state | this tree (landing) | master 84b84ef2 |
|---|---|---|
live |
.1=live |
.1=live |
live .1 |
.1=live .2=g1 |
.1=live .2=g1 |
live .1 .2 |
.1=live .2=g1 |
.1=live .2=g1 .3=g2 |
live .1 .2 .3 |
.1=live .2=g1 |
.1=live .2=g1 .3=g2 |
live .1 .2 .3 .4 .5 |
.1=live .2=g1 |
.1=live .2=g1 .3 .4 .5 |
Two readings in that table are the ones a file count cannot give. From the state that exists on a long-lived host (live + .1 .2 .3) one rotation ends at two files holding generation 5 and generation 4 — the newest two, with no third surviving; that is the exact state the veto measured three files on. And .4 and .5 go in a single rotation without raising, so the sweep is a sweep and not a fixed count.
Four further arms, all on this tree:
- The rename hazard is real here and the unlink is load-bearing. Renaming
.1onto a surviving.2raisesFileExistsErrorerrno=17 winerror=183on this host — measured, not inferred. The rotation raises nothing fromlive.1.2, becauseoldest.unlink()clears.2before the shift. - Absent-file states raise nothing and leave nothing behind:
liveonly ends at.1;live.1ends at.1 .2;live.1.2ends at.1 .2. In every arm the post-rotation directory holds nollm.jsonl, because the rotation renames the live log to.1and the nextappend_llmrecreates it — master behaves identically, so this is the method's contract rather than this PR's change. - The file set at the 50 MB default is live + 2 backups = 3 × 50 MB = 150 MB, which is the figure the body states. Master measures 3 backups ⇒ 200 MB. The earlier reading of 200 MB for this branch was the previous head's, and it no longer holds.
- The three carriers read as they did before the diff, and the diff touched none of them:
_LLM_LOG_BACKUP_COUNT = 2(emrg/session.py:38), the method's docstring "up to 2 backup files (llm.jsonl.1, llm.jsonl.2)" (:899), andsystem.j2:77"rotated with two backups". The change is confined to_rotate_llm_log's body and its own inline comment, so the claim was made true of the code rather than the code made to agree with itself.
Two notes, neither a veto
1. The sweep stops at the first missing number, and its comment claims more than that. The comment says "Every generation numbered at or above the count goes". The loop at emrg/session.py:937-943 starts at _LLM_LOG_BACKUP_COUNT + 1 and breaks on the first absent name, so the state live + .1 .2 + .4 — .3 missing — ends at .1 .2 .4: three backups, with the stale .4 kept. Measured on this tree, and reported here as a note rather than a defect because it cannot produce the wrong-two outcome the veto was about (oldest.unlink() still drops .2 and the shift still keeps the newest two); what it leaves is a surplus file the constant does not claim, and a sentence that reads as a stronger promise than the loop makes. A for over the names present, or wording that says "the first gap stops the sweep", would close it.
2. Nothing asserts that the backups kept equal the constant. git grep -n "_rotate_llm_log\|_LLM_LOG_BACKUP_COUNT" a150a328 -- tests/ answers nothing, so no test names the rotation or the count on this head, before or after this PR. This PR's own first commit is the proof that the count can regress silently, and it is not this PR's subject to fix. It is stated plainly because a merged fix with no reading behind it drifts again, and the third voter should weigh that rather than discover it.
What this vote is not
It is the second valid vote: one other vote stands on this head, cast after the push, and the three earlier reviews are void — so the count is 2/3, one short of a merge. Therefore #2697 stays open, nothing is merged here, and I did not push to this branch — a push would void both standing votes. reviewDecision is left untouched.
Closes #2697
What changed
emrg/session.py's_rotate_llm_log()shifted each backup one generation older and then unlinked the generation one past the last, so nothing was ever dropped:.1moved to.2,.2moved to.3, and.3— written this same call — survived. A session therefore kept its live log plus three backups while_LLM_LOG_BACKUP_COUNT = 2, the method's own docstring ("keeping up to 2 backup files") andsystem.j2:77("rotated with two backups") all said two.The loop now removes the oldest generation outright and shifts only the ones that remain:
Verification
Driving the real
_rotate_llm_log()over six successive appends in a temporary directory with the size bound forced low, before and after:.1.1.1.2.1.2.1.2.3.1.2.1.2.3.1.2The drop is the oldest generation and not merely a capped count: appending four distinct generations in order leaves
.2holding generation 3 and.1holding generation 4, with generations 1 and 2 gone.The file set at the 50 MB default becomes 150 MB (live + 2) instead of the 200 MB kept today.
tests/test_session.py— 112 passed.from emrg.client.app import run_clientandpython -m emrg --helpboth run.No guard is added: the behaviour is measured above, and the repository's own instruction of 2026-11-03 is not to add guards for a fix.
Second commit — a stray generation the first one left behind
The first commit removed only
.{_LLM_LOG_BACKUP_COUNT}, so a.3written by the loop itreplaces was never touched: on a long-lived session the file set stayed at three backups, and
the shift then dropped the second-newest while keeping the oldest. Every generation numbered
above the count is now removed before the shift.
Measured by driving the real
_rotate_llm_log()from each starting state, a backup's firstline naming the generation that wrote it:
live.1.1live.1.1.2.1.2live.1.2.1.2.3.1.2live.1.2.3.1.2.3(.2holding the oldest).1.2So the 150 MB figure above now holds for a session that already carries a third generation,
which is every session older than this change.
tests/test_session.py112 passed;pytest tests/ -q6111 passed, 35 skipped.