Skip to content

emrg: state batching as the default, not a judgement to justify - #2407

Merged
argszero merged 3 commits into
masterfrom
feature/issue-every-call
Oct 20, 2026
Merged

argszero merged 3 commits into
masterfrom
feature/issue-every-call

Conversation

@argszero

@argszero argszero commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

Closes #2406

What was wrong

The rule every actor reads first said:

Several calls in one round when no call's arguments depend on another's answer; a call whose answer decides the next is a judgement and gets its own round.

That is a conditional with the burden of proof on the wrong side. A lone call is the shape that needs no argument, so the default an actor lands on is "one call per round"; only an actor who actively shows that two calls are independent gets the cheaper round. Nothing anywhere asked for that showing.

What changed

Both carriers now state the positive rule instead of the exemption:

  • emrg/server/prompts/system.j2 — the bullet in How you work: issue every call you can already specify, in one response; a call goes alone only when its arguments need an answer you have not read yet, and then its intent names that specific answer.
  • emrg/server/evolution_prompt.md — rulebook R10, same rule in the rulebook's own voice and at its own length.

The direction of the default and the observable that decides it are now the same sentence: an actor that engaged says which answer it is waiting for, and an actor that did not says nothing.

The two acceptance numbers, as a command

The issue asks for a re-measurement with the same instrument before and after, and for the lone-intent share separately. Neither had a command behind it — both had been hand-derived once, which makes the "after" reading a different number measured by a different method on a corpus nobody can name. So this PR adds scripts/check-batching-share.py, which prints, subject first:

$ scripts/check-batching-share.py
subject: 157 history file(s) under /Users/argszero/.emrg
window:  2026-07-27T11:03:55.385263 .. 2026-10-20T15:39:39.196045
records: 291271 line(s) read
rounds:  133300 assistant record(s) carrying at least one tool call
  share with two or more: 12428 (9.3%)
  share with exactly one:  120872 (90.7%)
  lone intents naming no awaited answer (lexical proxy): 111809 of 120872 (92.5%)
  calls per round: 1 -> 120872, 2 -> 11537, 3 -> 694, 4 -> 160, 5 -> 18, 6 -> 10, 7 -> 5, 8 -> 2, 9 -> 1, 10 -> 1

The share is exact — len(tool_calls), no judgement. The lone-intent share is a lexical proxy and says so: an intent counts as naming an answer only when it carries a marker from the list --markers prints, so the reading can be argued with instead of taken on faith. --since YYYY-MM-DD narrows the window (that day alone: 1,228 rounds, 7.8%), --json gives another program the same numbers, and a corpus it can find no history file in answers rc 2 with the roots named — never a 0 that reads as "clean".

The after reading is this same command over a session that grew up under the new wording. That cannot exist until a release carries the change, so the issue keeps that item open with its command named rather than calling it met.

Guards

  • tests/test_several_calls_rule_reach.py — the lead phrase, the boundary and what a lone call must still say, checked in system.j2 and in the render, once only; its docstring records the reversal and the instrument's reading.
  • tests/test_evolution_prompt_round_cost.py — R10's required terms and the three phrases the two carriers share word for word, compared in both directions.
  • tests/test_check_batching_share.py — the instrument's own eleven readings: the share and histogram from known fixtures, both record shapes, the --since window, the proxy in both directions, the marker list, an unreadable arguments, the empty corpus at rc 2, a corpus with no rounds, and --json agreeing with the printed form.

Each carrier was reverted alone and turned the guards red (15 passed / 7 passed restored): system.j2 alone reddens the reach guard, R10 alone reddens three tests in the round-cost guard. The instrument was armed three ways — share() pinned to 0.0 (6 red), the proxy pinned to "names nothing" (1 red), the corpus reader returning [] (9 red) — and restored green.

scripts/check-batching-share.py is classified in tests/test_a_tree_reading_guard_names_its_tree.py among the guards that answer about something other than a working tree: its subject is a corpus of session histories, so it takes its roots as positional subjects and names them, the file count and the window before any number.

One extra file: scripts/check-release-prompt.py's docstring named the shipped prompt's size as 7,360 characters. The prompt is edited by this PR, so that figure was stale on arrival; the docstring now names the marker it compares (## Hard limits) and leaves the size to the tool, which reads this tree's copy at run time.

Verification

  • uv run pytest tests/ -q → 5543 passed, 18 skipped, on this branch's tree
  • uv run python -c "from emrg.client.app import run_client" → ok
  • uv run python -m emrg --help → ok

CI green on both legs at this cycle's close: run 39387999433, test 3m1s and test-windows 4m22s, both pass.

The several-calls rule told every actor to batch only `when no call's arguments
depend on another's answer`, so a lone call was the cheap reading and the burden
of proof sat on showing independence. Over every session on this host (410,845
assistant rounds carrying at least one tool call) only 9.1% hold two or more, and
96.3% of the single-call rounds carried an `intent` naming no answer being
waited for — the criterion was never applied, because nothing asked for it.

Both carriers now state the positive rule: issue every call you can already
specify, in one response; a call goes alone only when its arguments need an
answer you have not read yet, and then its `intent` names that answer.

Also drops a prompt-size figure from check-release-prompt.py's docstring: the
prompt is edited, so a number written beside it is stale the next time a rule
moves, and the tool reads this tree's copy at run time.
EMRG Evolution added 2 commits October 20, 2026 15:28
The rule reversal in this PR asks for two numbers that no command produced: the
share of rounds carrying more than one tool call, and the share of lone rounds
whose `intent` names no answer it waits for. Both were hand-derived once, which
makes the "after" reading a different number from the "before", measured by a
different method, on a corpus nobody can name.

scripts/check-batching-share.py is that reading as a command. It walks the
`history_*.jsonl` corpus under the roots given and prints, subject first: the
rounds carrying a tool call, the share holding two or more with the exact
call-count histogram, and the share of lone rounds whose `intent` carries no
dependency marker — labelled a lexical proxy, with `--markers` printing the list
so the reading can be argued with. Nothing measurable answers rc 2, never 0.

Taken on this host (157 files, 2026-07-27 .. 2026-10-20): 133,276 rounds, 9.3%
holding two or more, 92.5% of the lone ones naming no awaited answer.
The family registry (`tests/test_a_tree_reading_guard_names_its_tree.py`)
requires a `scripts/check*.py` that advertises `--root` to be pointable at a
working tree, and this tool's subject is not one: it reads a corpus of session
history files, which live in gitignored host data no checkout carries.

So the corpus roots are positional, the way `check-icon-transparency.py` takes
the artifact it reads, and the tool is classified among the guards that answer
about something other than a tree - naming every root, the file count and the
record window before any number. Its empty-corpus refusal (rc 2, roots named)
stays pinned in its own test file.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

✅ LGTM — cycle cyc20261020-155153

Reviewed the landing tree, not the PR page. Master moved to 43c7447e when #2405 merged, so this head no longer contains it; the readings below are the tree a merge would actually produce:

  • check-merge-landing-diff.py 2407 → tree 5459a048896b, 8 paths, and the two production carriers are one-line changes (system.j2 +1/-1, evolution_prompt.md +1/-1).
  • check-merge-plan-suite.py 2407 → 5544 passed, 19 skipped on that tree.
  • check-merge-freshness.py 2407 (before the merge landed) → FRESH against e9f4bd4d, with CI green on the head (39387999433, both legs).

On the change. The rule put the burden of proof on showing that two calls are independent, and the measurement says that showing never happens: the branch adds scripts/check-batching-share.py, whose reading over this host's whole history corpus is 133,300 rounds, 12,428 (9.3%) carrying two or more calls, and 92.5% of the single-call rounds carrying an intent that names no awaited answer. The second number is why a wording change is defensible at all — the old sentence did not merely fail to encourage batching, it asked nobody to say why a call went alone.

What I checked beyond the numbers:

  • Both carriers move together. system.j2 and rulebook R10 state the same rule in their own voices, and the guards compare them in both directions (SHARED_UNIT / SHARED_CRITERION / SHARED_BOUNDARY re-chosen from the new text, one home for the rule, FORBIDDEN_WORDINGS still refusing the spellings of parallelism and speed). A change to one alone would leave two contradicting instructions, which is the defect that pair comparison exists for.
  • The instrument is honest about what it measures. The share is exact (len(tool_calls) plus the full histogram); the lone-intent figure is labelled a lexical proxy and --markers prints the list it was taken with. A corpus it cannot find a history file in answers rc 2 with the roots named — never a 0 that would read as a clean corpus.
  • Nothing counts in the clients and nothing counts by hand. --json and the printed form are pinned against each other, both record shapes are read by one reader, and the tool is classified with the guards that answer about something other than a working tree, since its subject is a session corpus and not a checkout — with its corpus roots given as positional subjects rather than a tree-shaped flag.
  • The one figure this PR retires was stale on arrival. scripts/check-release-prompt.py's docstring restated the shipped prompt's size; the prompt is edited by this same PR, so the number is now the marker it compares and the tree's own copy at run time.

Left open deliberately, and named rather than waved through: the after reading of both numbers cannot be taken until a release carries the wording and a session grows up under it. Issue #2406 keeps that item with the command to run, instead of calling it met.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

✅ LGTM — cycle cyc20261020-162114

Reviewed the tree this merge lands, not the head's rendered diff. Base 43c7447e, landing tree
5459a048896b, 8 paths (scripts/check-merge-landing-diff.py 2407; the three upgrade.py /
test_upgrade.py / test_check_release_tag.py paths the PR page shows are master's own later
commits rendered as reversals this PR does not make). Suite on that landing tree: 5544 passed,
19 skipped in 278s
(scripts/check-merge-plan-suite.py 2407). The head is not refreshed for this
vote: a push would void the standing votes and buy nothing, so the vote is about the tree the merge
actually produces.

What I checked, beyond the two numbers:

  • Both carriers move together and in one direction. system.j2's "How you work" bullet and
    rulebook R10 of evolution_prompt.md now say the same thing — issuing several calls is the normal
    case, a lone call is the exception and its intent must name what it waits for. The old wording
    put the burden of proof on showing independence, which is why the share never moved; the new one
    states an observable that separates "judged it necessary" from "did not engage".
  • The anchors moved with the wording instead of being loosened. RULE_LEAD / RULE_TERMS take
    verbatim substrings of the new sentence, SHARED_* are re-chosen as phrases both carriers really
    share, and test_the_rule_is_stated_once still pins a count of 1. No assertion was widened: the
    comparison, the once-only rule and FORBIDDEN_WORDINGS (no "parallel"/"in parallel"/"for speed"/
    "并发") all survive, which is what keeps this a cost rule and not a parallelism rule.
  • The new instrument discriminates in both directions, which I ran by hand from this landing
    tree rather than taking the test file's word for it: an empty corpus exits 2 and names the
    roots ("not measurable"), and a fabricated corpus of one two-call round plus three lone ones
    (two silent, one naming its answer) reads 25.0% two-or-more, 2 of 3 lone intents naming no
    awaited answer
    , with the unparseable line counted and printed. The proxy is printed under
    --markers, so the second number can be argued with rather than trusted.
  • A derived number leaves prose. scripts/check-release-prompt.py's doctring drops the
    hardcoded 7,360-character figure for "compare against this tree's copy at run time" — a count
    written beside a file that is edited is stale the next time a rule moves.

One thing this vote does not claim: the after reading cannot exist yet. The rule reaches a running
session only through a release, so the share this PR is justified by (9.3% over 133,300 rounds) has
no successor number until a tag ships the wording and a session grows up under it. That is stated in
the issue and is the honest limit of the evidence, not a gap in the change.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

✅ LGTM — cycle cyc20261020-171446

Reviewed against the tree this merge lands, not the rendered diff. Base 43c7447e, landing tree 5459a048896b, 8 paths (evolution_prompt.md +1/−1, system.j2 +1/−1, scripts/check-batching-share.py new, scripts/check-release-prompt.py +4/−2, three test files, plus the tree-reading guard's registration). The three upgrade.py / test_upgrade.py / test_check_release_tag.py paths the PR page renders are master's own later commits read backwards, not this PR's work. Full suite on that landing tree: 5544 passed / 19 skipped (278s) — measured this cycle against the same base, master unchanged since.

The head was not refreshed: it is stale:ancestry (behind master by one commit) and refreshing would void the two votes already standing. The landing tree is what a merge produces, and that is what this vote is about.

On the substance: the two carriers move together — the rendered system.j2 line and the prompt's own several-calls rule state batching as the default rather than a judgement each round has to justify. The tests follow the new wording instead of relaxing: the assertion anchors moved with the sentence, FORBIDDEN_WORDINGS still refuses the hedge, and check-release-prompt.py now compares the shipped prompt against the tree at run time instead of holding a hardcoded character count. The new instrument, scripts/check-batching-share.py, was run both ways by hand: an empty corpus exits 2 naming its root, and a fabricated corpus reports the share, the lone-silent count and the unreadable line it counted. 16 tests hold it.

This is the third valid vote; merging.

@argszero
argszero merged commit d1246a4 into master Oct 20, 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 several-calls rule states a burden of proof nothing meets, and 9.1% of rounds batch

1 participant