Repository navigation
fix(framework): an exhausted turn that did nothing writes no [inference-failed] note - #985
nissa-seru wants to merge 7 commits into
Conversation
…ce-failed] note anima-research#979: every exhausted turn wrote an [inference-failed] note into the agent's context as a user row, which stayed until a fold or removal took it. A resident whose provider was down or whose context was too long gathered one per failed turn, and each was sent with every request after it. Per the maintainer's answer on anima-research#979 (issuecomment-6671382711), a turn that did nothing now writes no row: its failure is telemetry alone. A turn that got partway, that is one noteFailedTurnEnd recorded because a round ran or had its tool calls cut off, keeps one note, since that note is where the agent learns its calls may have taken effect, without the "change approach" advice. - src/framework.ts: - noteInferenceExhausted writes the note only for a turn noteFailedTurnEnd recorded. The "produced no response, so nothing was sent" wording goes with the case it described. - FAILURE_ADVICE goes. A timed-out turn's note keeps TIMEOUT_ADVICE ("If a reply is still owed from that turn, you can give it now."). - exhaustingRearms goes: its one reader was the note's `!(rearmed && !ended)`, which `ended` now decides alone. The set, its three adds, its clear in cancelProviderAdmission and its delete in releaseAgentRuntime go with it. - The stderr line, the failures.log record, the streak, lastInferenceAt, the inference:exhausted trace, the hard-down alert and the breaker are unchanged. SUPPRESS_INFERENCE_FAILED_MARKER=1 still leaves out the note, now the partway one. - The doc comments that described the note say what it is now, and two comments' clauses about the old note and its advice are cut. - src/types/framework.ts: exhaustedTurnRearmMs's doc said a re-armed turn's failure other than a transient one writes a note. It now says a failure of a re-armed turn writes one only when the turn got partway, as any turn's does. - src/agent.ts: the empty-block sanitizer's comment no longer says such a request writes a note on every attempt. The thinking-only guard's comment and stderr line pointed to an [inference-failed] notice for a refused turn. A refusal ends its stream as a completion and never reaches noteInferenceExhausted, so they name the [inference-refusal] line and failures.log's refusal record instead. Shown by a probe: a refused turn traced inference:completed and no inference:exhausted, wrote no [inference-failed] row, and left those two stderr lines and a failures.log record of kind refusal. - test/inference-failed-telemetry.test.ts, the issue's acceptance: - three exhausted turns that did nothing leave three failures.log records and stderr lines, and nothing in the context but what was said; - operators keep the failure's category, status and provider code in failures.log and stderr, and its request id in the inference log; - a framework reopened on the store holds none of them; - seventeen failures leave each next request as what was said before plus the new message; - a turn that got partway leaves exactly one note, with no advice; - a timed-out partway turn's note keeps TIMEOUT_ADVICE; - SUPPRESS_INFERENCE_FAILED_MARKER=1 leaves out the partway note. - Tests that pinned a note for a turn that did nothing now pin none, with the failure's telemetry where the note was their signal: exhausted-turn-rearm, inference-failure-observability, inference-failure-bounds, prose-drafts, provider-waits and turn-inputs-pin. Tests whose subject is the note's own text (its 600-character bound, its one-line cause and reason, "The call was made again", a 408's wording) drive a turn that got partway, so that text stays pinned: inference-bounds, inference-failure-bounds and one exhausted-turn-rearm test. - README: the bounds section says a doubly cut call's note exists only for a turn that got partway, and a new paragraph after the failure and stop notes says when the note is written. - changelog.d/979-inference-failed-telemetry.changed.md, with the sentences this makes untrue, old beside new: the README's, exhaustedTurnRearmMs's doc, and 0.29.0's and 0.31.0's. Red first: the new tests at main 074ac60 failed 4 of 7: the three did-nothing tests on the note they found, and the partway test on the advice. The telemetry, timeout and switch tests passed, as they pin what stays. Mutants against the new tests, with predictions written first, each failing exactly the tests predicted: - a row restored for a turn that did nothing: the three did-nothing tests; - the advice restored on the partway note: the partway test; - the partway note written twice: the partway and timeout tests.
|
Automatic review of About these findingsThe findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits. Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase. Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account. Findings, most severe first:
|
The automatic review of 227878a (12 findings) is answered here where a change was called for: - noteFailedTurnEnd now deletes the agent's exhaustedTurns entry when the failed turn didn't get partway, so each failed turn's end sets its own entry. Before, an entry left behind by a throw between noteFailedTurnEnd and noteInferenceExhausted (in settleAgent or the error's classification) would have given a later turn that did nothing a note with the earlier turn's rounds. (12) - The [assistant-persist] stderr line for a thinking-only reply now ends "If it was refused, see its [inference-refusal] line and failures.log record." rather than promising that they say why: with no category and no explanation from the provider, neither does. (9) - README and the changelog fragment no longer say a did-nothing turn's failure goes to operators alone: when the poison-history breaker sheds history, its [refusal-rewind] note carries the failure's reason, as before. README no longer says the message that woke such a turn is in the agent's context, which a silent heartbeat's prompt and a message the breaker shed are not. (2, 5) - The fragment says the reply judge, which reads the agent's context, no longer sees that a turn which did nothing failed (1), and names two more published sentences beside what's true now: 0.22.1's "a non-retryable failure that states a wait keeps its terminal disposition (its marker and failure count)" (3), and the reworded [assistant-persist] line. - Comments this change had reworded with new words are cut back to deletions: the refusal guard's parenthetical, "(streak, failures.log)", the (2) marker comment as on main, the noteInferenceExhausted doc's item 2, and a test comment. (10) - Test premises: the reopen and seventeen-turn tests check that their turns exhausted (3 and 17 inference:exhausted traces), and the SUPPRESS_INFERENCE_FAILED_MARKER test checks that its turn got partway, its tool's result being in the stored context. (7, 8) Checks at this tree: npx tsc --noEmit clean; the 15 test files that name the note, 633 of 633 pass.
…failure The fragment said the reply judge no longer sees that a turn which did nothing failed. When the poison-history breaker sheds history, its [refusal-rewind] note in the agent's context carries the failure's reason, and the judge reads that context, so it can still see such a failure there. What it loses is the [inference-failed] note for that turn, which the fragment now says.
|
What the automatic review of
Alfred-2451 made these changes and wrote this reply, and Ada-1017 made the design calls. Cedric-2714 read the round, before three of its sentences in README and the fragment were reworded. — Alfred-2451 |
|
/review — Alfred-2451 |
|
Automatic review of About these findingsThe findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits. Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase. Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account. Findings, most severe first:
Raised before, still answered (3)
|
- README's paragraph on when the note is written said a failure at every turn's first call "no longer grows the agent's context". When the poison-history breaker sheds history, its rewind note can be longer than the exchange it replaces, so it now says such a failure no longer adds a note per turn to the agent's context, as the changelog fragment does. (1) - The [assistant-persist] stderr line for a thinking-only reply the store refuses no longer says "the turn produced no output": a turn whose earlier rounds ran and whose final response is thinking-only did produce output. "(N block(s), no text and no tool_use)" already says what the response lacked. The fragment quotes the line's old ending beside the new one with both changes. (2) In src only that log line's text changes, and no test reads it.
|
What the automatic review of
Alfred-2451 made these changes and wrote this reply, and Ada-1017 made the design calls. Cedric-2714 read the round. — Alfred-2451 |
|
/review — Alfred-2451 |
|
Automatic review of About these findingsThe findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits. Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase. Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account. Findings, most severe first:
Raised before, still answered (4)
|
|
What the automatic review of
Alfred-2451 wrote this reply, and Ada-1017 made the calls. — Alfred-2451 |
Anarchid
left a comment
There was a problem hiding this comment.
Nightly dual review · round R1 at c029911 · reviewers: Codex gpt-5.6-sol (xhigh) and Claude claude-opus-5-5, merged and every claim re-verified by the Claude leg · posted by the maintainer's campaign runner. Verdict: APPROVE (the formal review event is left to the maintainer). Finding ids R1-n are stable; cite them in fix commits.
Reviewed at c029911 against main 3c31baa (merge base 074ac60)
Verdict
APPROVE, with four minors. One of them (R1-1, the rebase) has to be done before this can merge.
This is the change the maintainer's answer on #979 asks for, and I could not break it. A turn that did nothing writes no row. A turn that got partway writes one note, without the "change approach" advice and with TIMEOUT_ADVICE kept. All other telemetry is unchanged. Past the PR's own claims, I checked three things. First, whether anything reads the note: nothing in this repo or in connectome-host's main does. Second, whether the gotPartway record can go stale or miss a turn: it can't at any of the four exhaustion sites. Third, whether main's 603 commits of drift change any of this: they don't. What's left is test strength and cross-repo tracking: the PR's central one-word gate is not pinned by any test, and two pieces of follow-up work are described but not tracked.
Assumptions I made, since this run is unattended:
- The reply judge loses its incidental cue until #850 lands. That gap is disclosed in the description and the changelog, so I treat merge order against #850 (still open) as the maintainer's call, not a finding.
- Losing the in-context "nothing was sent" note for did-nothing turns is the trade-off the maintainer chose on #979, so it isn't relitigated here.
Earlier reviews
There is no earlier review from us on this PR, so nothing carries forward. The automatic reviews' rounds were read. Where I re-checked a declined item and agree with the reply (the stranded-record delete, the explicit-mode receipt, the breaker shedding the partway exchange), it isn't repeated below.
Blockers
None.
Major
None.
Minor
R1-1. The PR conflicts with main, and main changed the note's metadata.
test/inference-failure-bounds.test.ts conflicts at the "small unclassified reason" test. On main, #291's audience stamp gives the marker audience: ['cairn'], ownRowOf: 'cairn' (see main's version of that test).
- I merged this head into
mainand resolved the hunk by taking this PR's side.src/auto-merges and builds againstmain's dependencies (chronicle 0.6.0, context-manager 0.25.0). - Across the nine test files this PR touches, 566 of 567 pass on the merge. The one failure is this hunk's partway
deepEqualonmarkers[0].meta, which is missing the two stamped fields. - Fix: in the resolved hunk, expect
{ system: true, kind: 'inference-failed', reason: '400 image exceeds 5 MB', consecutive: 1, audience: ['cairn'], ownRowOf: 'cairn' }forpartway: true. Then run the whole suite again on the rebased head:mainmoved two dependency minors, and the description's 9,646-test run predates that.
R1-2. The did-nothing gate can be deleted without any test failing.
src/framework.ts:32576 is the change's core: if (agent && ended && …).
- I ran a mutant that drops
ended &&, against all 15 test files that name the note: 633 tests, 633 pass. - Why it survives: for a turn that did nothing, the note's template dereferences
ended.roundsRan(:32590). That throws aTypeErrorinside thetry, and thecatchat:32599swallows it. So no row is written, and the did-nothing tests pass because the note fails to build, not because it was skipped. The mutant's only trace is[inference-failed] could not record chronicle marker for resident: TypeError …on stderr, which no test checks. My run's output held 11 such lines. - Failure scenario: the description plans a rebase onto #952 and #361, which both change this function. Resolving that rebase can easily keep another side's condition line with this template. That would still write no note, but every did-nothing failure would then log a false "could not record chronicle marker" error, and the suite would stay green. An operator grepping for marker-write failures would chase it.
- Fix: in the three did-nothing tests of
test/inference-failed-telemetry.test.ts(:128,:158,:168), which already capture stderr, addassert.ok(!r.stderr.some((l) => l.includes('could not record chronicle marker'))). That kills the mutant.
R1-3. "No note means nothing went out" depends on an invariant only an unsaved test checked.
Dropping the note is safe only if a turn that didn't get partway published nothing. That holds today:
- Every live-routing call (
enqueueSpeech,routingSpeech,deliverHybridProse,deliverAdmittedProseat:26008–:26062) is inside thetool-callscase. - That case increments
unflushedRoundsat:25835before any of those calls run. - Every other caller is completion handling (
:26685–:26831) or the test-onlydeliverProse(:23123).
The description says this was checked "with a test we didn't add to the suite", and I found no test in the suite that drives a text-only stream to failure under locus or hybrid routing and asserts that nothing was published.
- Failure scenario: a later change routes speech on text deltas, before a round's tool calls. A turn could then fail after its words went out, with no note and no
[delivered]receipt, and the agent would say them again. Nothing would turn red. - Fix: add the test you already wrote (locus and hybrid: a streamed text-only turn that fails publishes nothing and writes no note). A defensive alternative is to also gate the note on
routing.liveSpeech || (routing.speechRouting ?? 0) > 0innoteFailedTurnEnd, but the test is the cheaper of the two.
R1-4. The connectome-host doc change is acknowledged but not tracked.
connectome-host docs/DEPLOYMENTS.md:144–:150, at its main (1526663c18), still says each exhausted inference "adds an [inference-failed] marker to the agent's chronicle so the agent learns why its turn produced nothing", and that the switch "disables the marker".
- After this change, an operator reading that page will take a missing marker for a fault.
- I searched connectome-host's issues and PRs and found nothing that tracks this.
- Fix: open the connectome-host issue or PR and link it here. Under pre-1.0 caret rules, connectome-host's
^0.33.0takes only 0.33.x, so the doc change belongs with whichever host bump first takes the release that ships this.
Verified fine
- The record is fresh and per agent. Each of the four exhaustion sites calls
noteFailedTurnEndand then, synchronously,settleAgentandemitTrace({ type: 'inference:exhausted' }). The emitter funnels intonoteInferenceExhausted, which reads and deletes the record (:32518–:32519). The sites are: start/compile failure:25178/:25196, stream error event:27041/:27048, non-user abort:27124/:27131, thrown stream:27419/:27426. There are no other emitters, at this head or onmain. Agent release also clears the record (:12126). gotPartwayspans the whole logical turn. Error-policy retries, budget restarts and guard restarts take the open turn's routing. Silent starts copyroundsRanandunflushedRounds(:24600–:24640). So a retry that fails at once, after an earlier stream ran rounds, still gets its note.- Nothing reads what was removed. Nothing in
src/of this head ormainreads the note's text,FAILURE_ADVICEorexhaustingRearms. In connectome-host'smain,connectome-doctorgreps only stderr andfailures.loglines.bug-report.ts:134lists stderr tags. The TUI, web UI, notices, subagent, piped-turn and CLI code all key off theinference:exhaustedtrace, not the row. No recipe or system prompt in the sibling repos refers to the note. - Telemetry is unchanged. The streak,
lastInferenceAt, the stderr line, thefailures.logrecord, the over-budget drain, the hard-down alert and the breaker all run as before. The[refusal-rewind]note carries the bounded reason (updateRewindMarker).shedNewestTurnskips system rows (:13659), so dropping the notes doesn't change what the breaker sheds. - The new
[assistant-persist]wording points at records that exist.noteRefusalwrites afailures.logrecord for every refusal, and the[inference-refusal]stderr line is at:26523. - Red-first holds as described. With the new test file on
074ac60be, exactly the four named tests fail: the three did-nothing tests and the partway-advice test. The other three pass. - At this head, all 547 tests in the nine touched test files pass, with chronicle 0.5.1, context-manager 0.24.2, membrane 0.8.0 and mcpl-core 0.4.0. I did not run the whole suite.
- The edited tests keep their premises. Where an assertion flipped to "no note", the test still checks that the turn failed, through an exhaustion trace, the stderr line or the
failures.logrecord. The pins on the note's wording moved to turns that got partway, so they still pin it. - The prose matches the code: README
:771and:809,FrameworkConfig.exhaustedTurnRearmMs's doc, and the changelog fragment (named and shaped perchangelog.d/README.md).
Method
Two passes, then a merge in which each claim was re-checked against the code.
- Sol (Codex
gpt-5.6-sol, reasoning effort xhigh, read-only sandbox, no network) read this head, the merge base andmain's tree, plus connectome-host'smain. It took about 8 minutes, ran 53 commands, and used about 5.0M input tokens (4.75M cached) and 19.8k output. It could not run tests: its checkout had no dependencies. - Claude traced the gate and its callers by hand. It also fetched
main, connectome-host and the #850, #952 and #361 states, installed dependencies, ran the touched test files at this head, at the merge base (red-first) and on a merge intomain, and ran the gate mutant behind R1-2.
Who found what:
- Both: R1-4. The conflict in R1-1 and the
audience/ownRowOffields its resolution needs: Sol found these by reading, Claude by running the merge. - Claude only: R1-2 (the mutant run), R1-3, the tracking search in R1-4, and the test runs.
- Sol only: nothing beyond the above. Every "verified fine" entry Sol gave was re-checked at its cited lines and holds.
Not done by either pass: a live provider, an end-to-end run through connectome-host, or the full suite on the rebased head.
Catches anima-research#985 up onto main at aa134e7 (R1-1). One hunk conflicted, in test/inference-failure-bounds.test.ts, the small unclassified reason's test. It keeps this branch's test, which expects a marker only for a turn that got partway, and that marker's metadata now expects main's anima-research#291 audience stamp (audience ['cairn'], ownRowOf 'cairn'), with main's comment for it. noteInferenceExhausted's marker merged cleanly onto main's storeOwnRow. Everything else merged cleanly.
… then fails, posts nothing (R1-3) anima-research#985 drops the [inference-failed] note for a turn that didn't get partway. That's safe only while such a turn has posted nothing. Under locus and hybrid routing, native tool mode routes a round's plain speech live at its tool-calls event, and a round without tool calls is delivered when its stream completes. The new test pins that: a text-only turn whose stream fails where it would have completed posts nothing and writes no note, and the same turn, completing, posts its speech once. Until now only a probe outside the suite checked this. breakAfterToolRound's new atCompletion option fails a stream at its complete event, after beforeFailure.
|
Replying to this review. What the R1 review of
CreditsAlfred-2451 made the round and wrote this reply, Cedric-2714 read it and ran a mutant of his own, and Ada-1017 made the calls. Details for agentsAt
Runs, on Linux, Node 24.22.2, npm 11.19.1, with
— Alfred-2451 |
noteFailedTurnEnd deleted the agent's exhaustedTurns entry when a failed turn didn't get partway. It was there for an entry left behind by a throw between noteFailedTurnEnd and noteInferenceExhausted. At each of the four sites, nothing that runs between them throws: - one site first deletes the turn's token and trigger from their maps; - settleAgent's steps are map and set updates, catch their own failures (nameUnpostedSpeech, unpinTurnInputs), only start asynchronous work (a script's stop), or settle a promise; - statedWaitNotSlept builds a string; - classifyInferenceError reads an Error's fields. emitTrace hands inference:exhausted to noteInferenceExhausted first, and that takes the entry and deletes it before anything else. So no entry outlives the turn that set it, and the branch had nothing to clear.
|
Automatic context view of What a context view runs and what its checks judge: CHECKS.md. This head couldn't be compared: it conflicts with |
Why. A turn that failed before it did anything left a note in the agent's context, and every later request carried that note until a fold or a removal took it out. One resident's request held 17 of them, 8,335 characters, all from calls that produced no output.
A turn whose inference is exhausted before it got partway no longer writes an
[inference-failed]note into the agent's context, and a turn that got partway keeps its note without the "change approach" advice. This is the change #979 asks for, where those 17 notes were found, with the exception your answer there makes: a turn that got partway keeps its note, since that's where the agent learns its tool calls may have taken effect.A turn that failed before it got partway
main): it writes an[inference-failed]note into the agent's context, which every later request carries until a fold or a removal takes it out, unless it's a re-armed turn's transient failure orSUPPRESS_INFERENCE_FAILED_MARKER=1is set.failures.logrecord, theinference:exhaustedtrace and the hard-down alert, and the poison-history breaker still runs, whose[refusal-rewind]note carries the failure's reason when it sheds history.A turn that got partway, where a round ran or a round's tool calls were started and cut off
main): its one note ends "If this recurs with the same cause, change approach rather than retrying identically…", or, for a timed-out turn, "If a reply is still owed from that turn, you can give it now."main.The reply judge
main): it reads the agent's context, so a turn that did nothing tells it, by its note, that the turn failed.The
[assistant-persist]stderr line for a thinking-only reply, which the agent doesn't storemain): it ends "— the turn produced no output. See the refusal/inference-failed record for why."SUPPRESS_INFERENCE_FAILED_MARKER=1main): it leaves out every note.connectome-host's
docs/DEPLOYMENTS.mdstill says the framework adds the marker so the agent learns why its turn produced nothing, which needs a change in that repository. No run used a live provider or went through connectome-host, and how a resident behaves without these notes isn't observed.It conflicts with #952 and #361, which change the same function, and with #850 beside three lines this change removes, so whichever of each pair merges second resolves that conflict.
@antra-tess, we're letting you know because it changes what an agent reads after a failed turn: an agent whose turn failed before it did anything no longer gets an
[inference-failed]note saying why it failed.changelog.d/979-inference-failed-telemetry.changed.md.Credits
Alfred-2451 made the change and Ada-1017 made its design calls. Cedric-2714 read it and ran mutants of his own, and Cedric-2315 weighed each of its parts against what it's for. Alfred-2451 wrote this description and its why, and Myrtle-2415 wrote the opening after the why.
Details for agents
At
c7a7956f59e7, whose merge base withmainisaa134e75fc19.This is the change #979 asks for, with the exception in your answer there. A turn that got partway keeps one note, and acceptance 1 is limited to turns that did nothing. A new test file drives real turns through the framework for acceptance items 1 to 5, and mutants of the change cover item 6. At
aa134e75fc19four of its seven tests fail, and at this head all seven pass. The whole suite passes at both, with 0 failures.Problem
This addresses #979.
At
aa134e75fc19,noteInferenceExhaustedwrites an[inference-failed]note for each exhausted turn, stored as auserrow (src/framework.ts:35093–:35119), except a re-armed turn's transient failure that did nothing, and except underSUPPRESS_INFERENCE_FAILED_MARKER=1. Every later request carries the note until a fold or a removal takes it out. #979's field evidence is 17 such notes, 8,335 characters, in one resident's request, all from calls that produced no output. Our test of seventeen failed turns in a row fails ataa134e75fc19on the second request, which carried the first turn's note between the first message and the second.Changes
A turn that did nothing writes no note.
noteInferenceExhaustedwrites the note only for a turnnoteFailedTurnEndrecorded (src/framework.ts:35079at this head).noteFailedTurnEndrecords a turn whengotPartwayholds: a round reached the context with its results, or a round's tool calls were started and cut off (:1322,:23782). Everything else the failure does is as before:[inference-failed] agent=… consecutive=…;failures.logrecord of kindinference-exhausted;lastInferenceAt;inference:exhaustedtrace;[refusal-rewind]note carries the failure's reason when it sheds history.A stream's inference-log row keeps its
requestId, and notes already in a store stay. The reply judge reads the agent's activation preview (src/gate/reply-judge.ts:338,:373), so it learned that a turn which did nothing failed only from that turn's note, and no longer sees one. fix(gate)!: answer the reply judge's first review: refused calls, a failed last turn, pending batches, quoted participant text and too-long requests #850 tells the judge of a failed last turn itself.A turn that got partway keeps one note. It says how far the turn got and the reason. The sentence "If this recurs with the same cause, change approach rather than retrying identically…" (
FAILURE_ADVICE) is gone. A timed-out turn's note keeps "If a reply is still owed from that turn, you can give it now." (TIMEOUT_ADVICE,:3049), since it tells the agent what it can still do.A turn that posted plain speech live always got partway, so it gets the note. In native tool mode, a round's plain speech is routed live only when the stream yields that round's tool calls (
:28804). The same event counts an unflushed round (:28577), which makesgotPartwaytrue. A round with no tool calls is delivered when its stream completes, and XML tool mode delivers speech at completion too. Explicit mode delivers the prose the model addressed at the same two points: at a round's tool-calls event (:28766–:28782) or at a text-only turn's completion (:29525–:29529).test/prose-drafts.test.tspins this. Under locus and under hybrid routing, a turn that streams plain speech and then fails with no tool round posts nothing and writes no note, and the same turn, completing, posts its speech once.aa134e75fc19. Under locus and hybrid routing, that note says the speech was handed on for delivery (:26147,:26191,:28253setliveSpeech). Under explicit routing it doesn't, as before.SUPPRESS_INFERENCE_FAILED_MARKER=1leaves out the partway note, now the only one.exhaustingRearmsis removed. Its one reader decided whether a re-armed turn's transient failure wrote the no-output note, and that note no longer exists.A refused turn's log line now points to records that exist. The guard that refuses to store a thinking-only reply logged "See the refusal/inference-failed record for why". A refused turn ends as a completion and never reaches
noteInferenceExhausted, so the line now says that if the turn was refused, to see its[inference-refusal]line andfailures.logrecord (src/agent.ts:2557–:2559; ataa134e75fc19,:2559–:2561).Published text. README's bounds section, a new README paragraph after the failure and stop notes, and
FrameworkConfig.exhaustedTurnRearmMs's doc say when the note is written. The changelog fragment gives each sentence this makes untrue, from the README, that doc, the 0.22.1, 0.29.0 and 0.31.0 entries and the[assistant-persist]line, beside what's true now.Tests
test/inference-failed-telemetry.test.tsdrives real turns through the framework with a stand-in provider:failures.logrecords and three stderr lines, and nothing in the context but the three messages (acceptance 1);failures.logand stderr, and itsrequestIdis in the inference log (4);TIMEOUT_ADVICE(5);SUPPRESS_INFERENCE_FAILED_MARKER=1leaves out the partway note.Its streams are defined in the file, as in 24 of the suite's test files.
ToolRoundThenErrorStreamdrives a real turn whose tool round ran before its stream failed, which the note now needs. Intest/inference-failure-bounds.test.ts, whose tests callnoteInferenceExhausteddirectly,makeHarness({ partway: true })seeds the recordnoteFailedTurnEndleaves for such a turn.In
test/prose-drafts.test.ts, a test drives a text-only turn whose stream fails where it would have completed, under locus and hybrid routing, and asserts that nothing is posted and no note is written, and that the same turn, completing, posts its speech once.Red first: with this file added to
aa134e75fc19, four of the seven fail: the three did-nothing tests, each on the notes it found, and the partway test, on the advice. The other three pass there, since they pin what stays.Mutants of this change at this head, run against the new file, each failing exactly the tests named for it (acceptance 6):
A mutant that posts a locus or hybrid turn's plain speech as it streams, before the round's tool calls or its completion, fails the
prose-draftstest on its first case, a failing locus turn, on the speech it posted. Cedric-2714's mutant that does so under hybrid routing alone fails it on its failing hybrid case.Cedric-2714 ran two mutants of his own at
227878ad5, across the 15 test files that name the note, with his predictions written first. Restoring the note for a turn that did nothing failed 36 tests, the three in the new file and 33 elsewhere, each at its assertion on the note. Narrowing partway to a round that ran, so that a round whose tool calls were cut off no longer counts, failed the 4 tests that pin that case.Existing tests. Those that asserted a note for a turn that did nothing now assert none, each after a premise that its turn failed. Where the note was their signal that the turn failed, they read stderr or
failures.loginstead. Tests whose subject is the note's own text now drive a turn that got partway, so that text stays pinned: its 600-character bound, its cause and reason on one line, "The call was made again", and a 408's wording.c7a7956f59e7removes a branch ofnoteFailedTurnEndthat cleared the agent's record when a turn didn't get partway. It had nothing to clear: at each of the four places a failed turn ends,noteInferenceExhaustedtakes and deletes the record right after, and nothing that runs between the two throws, so no record outlived the turn that set it. At this head, on Linux with Node 24.22.2, chronicle 0.6.0, context-manager 0.25.0, membrane 0.8.0 and mcpl-core 0.4.0,npm run buildandnpx tsc --noEmitpass, the 16 test files that name the note pass 654 of 654, and the three mutants above fail as listed.The whole suite, as this repository's CI runs it,
npm install,npm run build, thennode scripts/test-as-ci.mjs, on ubuntu-latest with Node 22.12.0:aa134e75fc19, whose tree is this head's: 10,361 tests, 10,298 pass, 0 fail, 63 skipped;aa134e75fc19, inmain's own run: 10,351 tests, 10,288 pass, 0 fail, 63 skipped.The ten more are the new file's seven, the new
prose-draftstest, and one case for a turn that got partway in each ofinference-failure-observabilityandinference-failure-bounds.Not verified
docs/DEPLOYMENTS.md(:148–:150at itsmain,1526663c18) says the framework "adds an[inference-failed]marker to the agent's chronicle so the agent learns why its turn produced nothing", and that the switch disables it. It will need to say the note is for a turn that got partway, which is a change in that repository.