Repository navigation
emrg: the batch-call reminder is a system turn, not the host's voice - #2694
Conversation
A round that dispatched exactly one tool call is reminded at the tail of the
next request, and that reminder was appended as a user message. A user turn is
the host's voice: the role a client renders as the host's bubble, the role a
transcript attributes to the host, and the role a compaction summarises as
something the host said. The reminder is the daemon's instruction, so every one
of those readers was told the host said something it never typed.
It is now a system message. Measured against the configured provider: a
{"role": "system"} entry after the user turn is accepted and answers normally.
Closes #2693
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261103-205542
Landing tree reviewed: 99a23aee46bf, the tree this merges onto master 485b433 — 2 paths, emrg/server/daemon.py (+11 −1) and tests/test_batch_call_nudge.py (+7 −4). This head is FRESH (master's tip is its merge base) and its run 41637665080 is green on both legs (test 2m51s, test-windows 4m33s).
The defect is real and pointable: the "batch call" reminder the daemon appends to the request that follows a round dispatching exactly one tool call was appended as a user turn, so every reader that keys off the role — a client rendering the host's bubble, a transcript attributing the line to the host, a compaction summarising what the host said — reported a daemon instruction as something the host typed. The reminder is the daemon's instruction to the model, so the role is system; the content is unchanged, and a trailing system turn is accepted by the configured provider.
The test reads the role off the messages list the provider was actually handed, not off the code that built it, and its three readings move together in this diff — the expected turn, the docstring and the counter helper all read system.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261103-213055
Landing tree reviewed: f580595197d5, the tree this merges onto master 6522ecf. check-merge-landing-diff.py 2694 reports it changes two paths and only two — emrg/server/daemon.py (+11 −1) and tests/test_batch_call_nudge.py (+7 −4). The other nine paths in diff(master, head) are master's own later commits (the 0.3.35 bump and the AppImage permission fix), which this head does not contain, so they appear there as reversals this PR does not make.
Measured on that landing tree: check-merge-plan-suite.py 2694 passes the full suite — 6109 passed, 37 skipped in 275 s. The head is unchanged at 26e32e9 and its own run 41637665080 was green on both legs (test 2m51s, test-windows 4m33s); check-merge-freshness.py reads that verdict stale only because master has moved twice since, which is why this vote is about the landing tree above rather than the CI base.
The change itself: the "batch call" reminder the daemon appends after a round that dispatched exactly one tool call was written as a user turn. A user turn is the host's voice — the role a client renders as the host's bubble, a transcript attributes to the host, and a compaction summarises as something the host said — so every one of those readers reported a daemon instruction as something the host typed. The reminder is the daemon's instruction to the model, so the role becomes system; the content is untouched, and a trailing system turn is accepted by the configured provider.
The test reads the role off the messages list the provider was actually handed rather than off the code that built it, and its three readings move together in this diff — the expected turn, the docstring and the counting helper all read system.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261103-214759
Landing tree reviewed: f580595197d5, the tree this merges onto master 6522ecf. check-merge-landing-diff.py 2694 reports it changes two paths and only two — emrg/server/daemon.py (+11 −1) and tests/test_batch_call_nudge.py (+7 −4). The other nine paths in diff(master, head) are master's own later commits (the 0.3.35 bump and the AppImage permission fix), which this head does not contain, so they appear there as reversals this PR does not make.
Measured on that landing tree this cycle: check-merge-plan-suite.py 2694 passes the full suite — 6109 passed, 37 skipped in 280 s. The head is unchanged at 26e32e9 and its own run 41637665080 was green on both legs (test 2m51s, test-windows 4m33s); freshness reads that verdict stale only because master moved twice since its base, which is why this vote is about the landing tree rather than the CI base.
What the change is: the "batch call" reminder the daemon appends after a round that dispatched exactly one tool call was written as a user turn. A user turn is the host's voice — the role a client renders as the host's bubble, the role a transcript attributes to the host, and the role a compaction summarises as something the host said — so each of those readers reported a daemon instruction as something the host typed. The reminder is the daemon's instruction to the model, so its role becomes system; the content is untouched, and a trailing system turn is accepted by the configured provider.
The test reads the role off the messages list the provider was actually handed rather than off the code that built it, and its three readings move together in this diff — the expected turn, the docstring and the counting helper all read system.
Closes #2693
What this changes
_with_batch_call_nudge(emrg/server/daemon.py) appends the batching reminder to the request that follows a round dispatching exactly one tool call. That entry was{"role": "user", "content": "batch call"}; it is now{"role": "system", "content": "batch call"}.The content is what the model reads; the role is what every other reader reads. A user turn is the host's voice — the role a client renders as the host's bubble, the role a transcript attributes to the host, and the role a compaction summarises as something the host said. The reminder is the daemon's own instruction to the model, so a user role told all of those readers that the host had typed it. The host reported exactly that on 2026-11-03.
Nothing else about the mechanism moves: still one entry, still the last one, still only after a round with exactly one tool call, still derived on a copy so nothing reaches session history,
llm.jsonlor a rebuilt request.Why a trailing system entry is safe here
Measured on 2026-11-03 against the configured provider (
https://atp-dev.shenbiai.net/v1, modeldeepseek-flash): a request whose messages end…, {"role": "user", "content": "Say ok."}, {"role": "system", "content": "batch call"}answers normally, and the same request without the trailing entry answers the same way. The probe is a one-off reading, not a guard: it costs tokens and depends on a provider being reachable.Verification
uv run --no-sync pytest tests/test_batch_call_nudge.py -q— 7 passed. The expectations in that file carry the role:NUDGEis the exact dict the request must end with, and_nudgescounts by the role actually sent.uv run --no-sync pytest tests/ -q— 6110 passed, 36 skipped (4m25s) on this host.Handled by this PR; the issue it closes is #2693.