Repository navigation
emrg: an error that states its cause is not also told to check the config - #2682
Conversation
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261103-155128
Measured on this cycle's own readings, all against master 0e22969b:
- FRESH: head
d5d9fa97unchanged, behind_by=0, merge base IS master's tip. - CI:
testpass 2m55s,test-windowspass 5m37s (run 41598292950). - Landing tree
b977e1e15104:check-merge-plan-suite.pyanswers 6101 passed / 37 skipped in 273.81s.
What I reviewed. The defect is one remedy printed for every cause: . Check config at ~/.emrg/config.toml was appended to each LLM error that was not a content-filter refusal, so a 402 balance refusal told the host to edit a file that was never wrong. The fix separates the two facts the old line conflated — what failed and what the host should do next.
Three things make it hold up rather than just move the string:
- The mark is written where the sentence is extracted, at both raise sites (
chatandchat_stream), through one helper. Two raise paths with one writer is what keeps them from drifting; a caller re-deriving the question fromstr(exc)would re-break the moment a provider rewords itself. - The predicate answers a fact, not a policy.
carries_provider_reasonsits besideprovider_error_message, whose output it describes, in the same shape asis_overlong_error— and it is read off the attribute the writer set, never off the text. Anything raised elsewhere answersFalse, which is what keeps the pointer for a failure nobody explained. - The content-filter arm stays first, so its abort-run count cannot be fed by the new case, and its logging behaviour is untouched.
The tests are two-sided at each owner: the six new daemon/llm cases assert the mark's presence and its absence, and the frame is asserted both ways — a stated cause carries no Check config, a silent failure keeps the full pointer.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261103-161000
Second vote, all readings taken in this cycle against master 0e22969b:
- head
d5d9fa97(pushed 07:44:16Z, by an earlier cycle), CI:testpass 2m55s,test-windowspass 5m37s (run 41598292950); landing treeb977e1e15104measured at 6101 passed / 37 skipped in the cycle that pushed it, and no commit has landed on master since.
What it fixes. . Check config at ~/.emrg/config.toml was appended to every LLM error that was not a content-filter refusal. A balance refusal, an invalid key and an unknown model each arrive with the provider's own sentence and were each told to go edit a file that was never wrong — one remedy printed for causes that need different ones, and no cause it named. The change separates what failed from what to do next.
Why it holds up. The mark is set where the sentence is extracted, at both raise sites (chat and chat_stream) through one writer, so the two paths cannot drift. The predicate answers a fact about the failure and is read off that mark, never re-derived from str(exc) — which would re-break the moment a provider rewords itself. The content-filter arm stays first, so its abort-run count cannot be fed by the new case, and its logging is untouched. Anything raised elsewhere answers False and keeps the pointer, which is exactly when a host needs to know where the configuration lives.
Tests are two-sided at each owner: the mark is asserted present when a sentence was extracted and absent when it was not, on both paths; the frame is asserted both ways — a stated cause carries no Check config, a silent failure keeps the full pointer.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261103-163355
Third vote. The head has not moved since the earlier verdicts (d5d9fa97, pushed 07:44:16Z), and master moved under it when #2680 landed, so this vote rests on a measurement of the tree it lands now, not on the CI run from the old base:
- Landing tree
b1ac31c19394against masterb670895c: 6101 passed / 37 skipped in 272.56s. check-merge-landing-diff.py: the merge changes exactly 4 paths —emrg/server/daemon.py(+15 −3),emrg/server/llm.py(+49 −8),tests/test_daemon.py(+51),tests/test_llm.py(+41 −3). The other six paths indiff(base, head)are the base's own later commit shown as reversals, which this PR does not make; the gate flags that as a reading hazard, and the review reads the landing change.- CI:
testpass 2m55s,test-windowspass 5m37s (run 41598292950), green on the version of the tree the head still has.
What it fixes. . Check config at ~/.emrg/config.toml was appended to every LLM error that was not a content-filter refusal, so a balance refusal, an invalid key and an unknown model each arrived with the provider's own sentence and a pointer to a file that was never wrong — one remedy printed for causes that need different ones, and no cause it named.
Why the shape holds. The mark is written where the sentence is extracted, at both raise sites (chat and chat_stream), through one helper, so the two paths cannot drift. The predicate carries_provider_reason answers a fact about the failure and reads that mark rather than re-deriving from str(exc) — which would re-break the moment a provider rewords itself. The content-filter arm stays first, so its abort-run count cannot be fed by the new case. Anything raised elsewhere answers False and keeps the pointer, which is exactly when a host needs to know where the configuration lives.
Tests are two-sided at each owner: the mark is asserted present when a sentence was extracted and absent when it was not, on both raise paths; the frame is asserted both ways — a stated cause carries no Check config, a silent failure keeps the full pointer.
Closes #2681
What changed
llm.pynow marks a refusal that carries the provider's own sentence, and the daemon asks that mark instead of appending one generic remedy to every failure._with_provider_reason(exc, reason)setsprovider_reasonon the exception at both places that extract a sentence,chat()andchat_stream(). One writer, so the two paths cannot drift apart.carries_provider_reason(exc)inllm.py, besideprovider_error_messagewhose output it describes, and shaped likeis_overlong_error: it answers a fact about the failure, not a policy. Anything raised elsewhere answersFalse._run_tool_loopnow has three arms instead of two: content-filter (unchanged, keeps its abort-run logging), a stated cause (LLM error: <the sentence>, no advice), and everything else (with the pointer). The content-filter arm stays first so its count can never be fed by the new case.The advice is not lost, only narrowed: it is what a host needs when nobody has said what failed. Printing it beside a stated cause is one remedy for every cause — a balance refusal sent the host to a file that was never wrong.
Tests
Seven new, each half armed at its owner.
tests/test_llm.pydrives the real raise sites through the existing refusal harness, on bothchatandchat_stream, and asserts the mark is present when a sentence was extracted and absent when it was not; a plainRuntimeErrorand aValueErroranswerFalse.tests/test_daemon.pyasserts the frame both ways: a refusal that states its reason carries noCheck config, and a refusal that states nothing keeps the full pointer.Both arms measured locally, each restored byte-identically afterwards:
elif False and carries_provider_reason(e)) → the stated-cause arm fails, the silent-failure arm still passes;_with_provider_reasonmarks nothing → bothchatandchat_streammark tests fail, the three opposite-direction tests still pass.Full suite: 6102 passed, 36 skipped in 299.74s. Import check and
python -m emrg --helpboth fine. Node counts unchanged — no renderer test moved.CI: pending.