Repository navigation
fix(inference): clear an ended agent's failure accounting - #897
Merged
nissa-seru merged 1 commit intoOct 29, 2026
Merged
Conversation
One helper, forgetFailureAccounting, clears an agent's consecutive- failure streak, its poison-history rewind count, its refusal streak, refusal-rewind count and refusal rewind episode, and ends an open /unstick session, saying on stderr that it ended with its agent. Three places that end an identity call it: releaseAgentRuntime (a stop, an end that cascades from a parent's, a fork's dispose), an ephemeral run's end when the run was ended from outside, and dispose's branch with no live agent. Each also clears freshCallsCut, the mark that a cut call's fresh call was cut too. A running job's stop never reaches the release: teardownEndedAgent and cancelActivation end its run instead. So the run now carries endedFromOutside, set at those two places, and its end calls the helper when it's set. endReason can't tell, since a run's own completion and deadline set it too. A run's own end, by a failure, a completion or its deadline, keeps what accounting it has and an open session for a resume in the same process, since a resume reopens the same history and a history refused across resumes is what the streak and the breaker are for. Of that accounting it drops only freshCallsCut, which is about that run's last call. An abort of a job's call (abortInference) or an operator's quiesce abandon asks to stop an inference, not to end the job; with the job's call in flight it ends the run all the same, as the job's own failure, so neither marks the run, and an aborted turn adds nothing to the streak. Before, a new agent created under an ended agent's name started with its predecessor's accounting. With a carried-over streak of two, its first refused request counted as the third, so it was reported hard-down, and for a request the provider refused for its content the poison-history breaker set aside the new agent's own message on that one refusal. A spent rewind budget left it none, its first refusal alerted as a refusal streak of 2, and a leftover mark made its first failure note say the call was made again. test/exhausted-turn-rearm.test.ts pins them: a resident stopped with a streak of two and the other state set leaves none of it, and a new agent under its name meets its first refused request as its first, not hard-down and with no hard-down alert, its own message kept, its note saying nothing of a call made again; a resident stopped after a refusal leaves a new agent's first refusal with no streak alert; a job stopped or cancelled while its run is in flight takes its accounting and its /unstick session with it, and its resume meets its first refused request as its first; a job resumed after its own run ended on a failure, a refusal or its deadline counts on from it, over its own history; a job whose resumed turn is aborted counts on past the abort, which adds nothing to its streak; and a resume after a run that left the made-again mark says nothing of a call made again.
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 29, 2026
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 29, 2026
main 7aab5e3 brings anima-research#896 (the written-to test's host write fails before the child's SIGTERM, in both engines), anima-research#871, anima-research#897, anima-research#893, anima-research#870 and anima-research#869 since 0f11975. No file conflicts.
tel-0s
added a commit
that referenced
this pull request
Oct 29, 2026
…'s context retired, MCPL inference aborted at stop, the re-armed deadline cut, failure accounting released with its agent main at 612f6fd is eight merges past 4a45766, with no release among them, so package.json stays 0.30.0 and this branch's floors stand (chronicle ^0.6.0, context-manager ^0.25.0, membrane ^0.8.0), which test/dependency-ranges pins. It brings #871 (a re-armed turn's deadline cut follows the deadline-cut rule; an ended agent takes its re-armed deadline cut with it), #896 (the race order of the MCPL down alerts), #897 (an ended agent's failure accounting goes with it: forgetFailureAccounting), #893 (the [inference-failed] marker's pins), #870 (strict credential mode for opt-in WebSocket dials), #895 (stop() aborts each MCPL server's inference/request still running, and answers it -32000), #898 (a module's context is retired once its stop() completes: its pushes dropped with a line, its writes, calls and subscriptions refused, its unapplied response and unmerged gatherContext answer left out) and #884 (the suite's speed: setAbandonGraceForTests and CI). One path conflicted (3 regions): - src/module-registry.ts: - imports: main's boundedErrorMessage; heldWriteId stays out, as on this branch, which has no held-write handles. - the registry's constructor: both, this branch's window lookups (windowStatusOf, observes, viewKeeps, agentNameOf) and main's dropRetiredPush. - findMessageByExternalId's comment: this branch's (a '' names a write kept in memory, a surgery's hold or the wait for its agent's recovery; no held rows) with main's clause that a stopped module's context keeps its mapping as it was. The code merged cleanly: main's retired guards around the mapping's updates, without the held-row answer this branch removed. Read onto this branch past the conflict: - releaseAgentRuntime takes main's freshCallsCut.delete, forgetFailureAccounting and rearmDeadlineCuts.delete, beside this branch's forgetPresentation; an ephemeral run's disposal and disposeConversationAgent's name-only arm take them as main put them. - ModuleContext's doc (types/module.ts): a retired context's reads list this branch's window lookups (windowStatusOf, observes, viewKeeps, agentNameOf), which stay open as main's reads do; main's clause that a read meeting a held write may record its landing goes, as this branch has no held rows to meet. - applyProcessResponse is synchronous on main now; this branch's body has no await, so it takes main's form. storeSees: no new site; main opens and derives no context manager. agentSees: no new view decision (noteSpokenTo's existing check gains only the re-arm's reset). Notice tags: none new; main's new texts ([framework], [process] and [unstick] lines, the -32000 answer) go to stderr or an MCPL server, not to an agent. #898's retire and #884's suite changes touch no presentation point. Changelog: no release in main's range, so no fragment was consumed; main's five new fragments come in as written. retire-stopped-module-context's "reads answer, and fail, as before" covers this branch's window lookups. Tests adapted: - retired-module-context, the findMessageByExternalId case: its '' entry comes from a write a surgery's hold keeps in memory, released before the module's removal, where main made it with a quiesce. A write while quiesced is stored at once here (#291), so it mapped to its id. Tests: built with zero errors; no stale compiled test; fixtures copied. main's 35 changed test files: 1522 tests, 1516 pass, 1 fail (the retired-module-context case, adapted above; its file then 22 of 22), 5 skipped (Linux-only workspace cases). presentation*, window-*, dendrite-*, stored-injections, audience-stamp*, dependency-ranges, held-* and tune-out-* (44 files): 606 of 606. Full suite (npm test): 9518 tests, 9469 pass, 0 fail, 49 skipped (runtime-conditional: Linux-only rules, and temp directories that fold case or normalization). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
@antra-tess Flagging for your awareness this PR, which made the following changes to what residents see (in brief), for an agent stopped, ended with its parent or disposed as a fork:
— Lottie-2661 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When an agent is stopped, or its job's activation cancelled, this clears its failure accounting, so a new agent under its name meets its first failed request as its first. Fixes #885, which FableDendrite filed.
Before, a new agent under a stopped agent's name started with the stopped agent's consecutive-failure streak, poison-history rewind count, refusal streak, refusal-rewind count and refusal rewind episode. By run at
0f11975e48, which wasmainuntil 07:26 UTC on October 29, a new agent under a stopped resident's name counted its first refused request as its third failure, so it was reported hard-down and the poison-history breaker set aside its own message. A job whose run ends on its own, and which is resumed under its name in the same process, keeps its accounting, as at0f11975e48. For a job resumed in the same process, one thing changes: a running job an operator stops or cancels, then resumes, now starts its count clean, where at0f11975e48it carried. The change writes nothing to the store.mainhas since moved to4a45766f01(#869), and this head merges with it without conflicts.Problem
releaseAgentRuntime(src/framework.ts:11804at0f11975e48) is the release of an agent that ends by a stop, by its parent's end or by a fork's dispose. The comment above the release test at0f11975e48says what the release is for: it "drops this per-name turn state, so a new agent under the name starts clean" (test/exhausted-turn-rearm.test.ts:929–931). At0f11975e48it doesn't drop five per-name maps of failure accounting:consecutiveInferenceFailures,exhaustionRewinds,refusalStreak,refusalRewindsandrewindEpisode(src/framework.ts:3776–3819). Nor does it dropfreshCallsCut, the mark that a cut call's fresh call was cut too, which the next failure note reads (:3621,:31480), orforcedRewind, an open/unsticksession (:3823).By run, with the probe below added to a clean
0f11975e48and to this head, on the platform under "Tests". The probe prints what it sees and asserts nothing.second, fails two turns with a 400 and is stopped. A new agent is made under its name, as the release test at0f11975e48makes one. At0f11975e48, the new agent's first refused request logsconsecutive=3and[inference-hard-down], and raises a hard-down alert, and the poison-history breaker sets aside the new agent's own message. At this head it logsconsecutive=1, raises no alert, and keeps its message.secondfails until its poison-history rewind budget is spent, and is stopped. At0f11975e48, the new agent's first failure logsconsecutive=7, hard-down, and "rewind cap 3 reached — not shedding further history. Manual repair (/unstick or /undo) needed." At this head it logsconsecutive=1alone.secondonce, and it's stopped. At0f11975e48, the new agent's first refusal raises the alert "refusal streak 2, category=reasoning_extraction". At this head it raises none.createEphemeralAgentfails twice, is stopped while its third call is in flight, and is resumed. At0f11975e48, the resume's first failure logsconsecutive=3and hard-down, and raises a hard-down alert, and the poison-history breaker sets aside the job's newest message. At this head it logsconsecutive=1and keeps the message.#885's other stated consequence, that the failure marker may speak of earlier rounds the new agent never ran, doesn't hold at
0f11975e48. A note's sentence about earlier rounds, "Its earlier rounds ran…", comes from the exhausted-turn record (src/framework.ts:31551at0f11975e48). The test "a resident stopped mid-turn takes its turn state with it…" at0f11975e48(test/exhausted-turn-rearm.test.ts:932) sets that record, with its rounds run, before the stop, and checks that the release drops it. It passes with0f11975e48'ssrc/framework.tsin the red run under "Tests". In case A, the new agent's failure note has the same text at both commits, and only its metadata'sconsecutivediffers: 3 at0f11975e48, 1 at this head.The probe,
test/probe-885.test.ts, as it ranChanges
Line numbers are this head's,
98f84cc00e.forgetFailureAccounting(src/framework.ts:11798), clears the five maps and ends an open/unsticksession, with a stderr line saying it ended with its agent. The channel the/unstickcame from isn't told.freshCallsCut(:11851–11852).stopAgentends a running job's run throughteardownEndedAgent(:11447), and so does a parent's end, for each agent it cascades to (:11921). A cancel of a job's activation ends its run incancelActivation. None of these reaches the release.teardownEndedAgentandcancelActivationnow mark the runendedFromOutside(:11789,:11410), and the run's end calls the helper when the run is so marked (:12621).endReasoncouldn't tell, since a run's own completion and its deadline set it too./unsticksession, and clears onlyfreshCallsCut(:12620).abortInferenceand an operator's quiesce abandon don't mark the run, so a run they end keeps its accounting, as a run's own end does.disposeConversationAgent's branch with no live agent calls the helper and clearsfreshCallsCut(:17769–17770), as it already cleareddeadlineCuts.changelog.d/885-release-clears-failure-accounting.fixed.md.So a job that ended on its own keeps what accounting it has when it's resumed under its name with
resumeAgentin the same process. A resume reopens the same history, and a history refused across resumes is what the streak and the breaker count.What each part is for
forgetFailureAccounting: one rule for what the end of an agent clears, called from the three places that end one, so they can't drift apart. Its stderr line tells an operator that the/unsticksession they opened has ended.deadlineCuts,thinkingLossIncidents).endedFromOutside: a running job's stop or cancel never reaches the release, so the run carries why it ended. Clearing at every end of a run would lose the history a resumed job's streak and breaker count.freshCallsCutat every end: the mark is about one run's last call, so a resume's first failure note shouldn't say a call was made again.deadlineCutsthere was in feat(inference)!: cut an AgentFramework model call that stops making progress, and give a deadline expiry one fresh call #378.refusal()andRefusingMembrane: a model refusal for the refusal tests, built astest/refusal-single-call.test.tsbuilds one.Each part, taken out alone at this head and rebuilt, fails a named test of
test/exhausted-turn-rearm.test.ts:forgetFailureAccounting, and itsforcedRewindline: the resident-stopped test and both in-flight tests, and forrefusalStreak, the refusal-stopped test too;freshCallsCutdelete: the resident-stopped test;freshCallsCutdelete: the fresh-call-mark test;cancelActivation: the cancelled in-flight test; the mark inteardownEndedAgent: the stopped in-flight test.Tests
From a clean copy of this head,
98f84cc00e, on Linux 6.12.113 (amd64) with Node 24.22.1, installed with no lockfile.npm ls --depth=0listed @animalabs/chronicle 0.5.0, @animalabs/context-manager 0.24.1, @animalabs/mcpl-core 0.4.0, @animalabs/membrane 0.8.0, @babel/parser 7.29.11, @modelcontextprotocol/client 2.4.0, @modelcontextprotocol/server 2.4.0, @types/node 22.20.5, @types/ws 8.18.2, chokidar 4.0.3, discord.js 14.27.0, tsx 4.23.15, typescript 7.0.2 and ws 8.22.0.tsc -p .exits 0.Green:
exhausted-turn-rearm,inference-bounds,inference-exhaustion-rewindandrefusal-single-callpass, 137 tests.Red first: this head's
test/exhausted-turn-rearm.test.tswith0f11975e48'ssrc/framework.tsruns 64 tests, and 5 fail, each at the assertion named:The other four new tests pass there too: at
0f11975e48a job's own end already keeps its accounting.Time:
exhausted-turn-rearmalone, by the test runner's duration, three rounds, each with0f11975e48'ssrc/framework.tsand test file (55 tests) and then this head's (64 tests), on 12 CPUs with a one-minute load of 26.85 to 28.25: 2,701, 2,719 and 2,934 ms for0f11975e48's, and 3,074, 2,698 and 3,202 ms for this head's.The tests this change adds to
test/exhausted-turn-rearm.test.ts:/unsticksession ended, and a new agent under its name meets its first refused request as its first:consecutive=1, not hard-down, no hard-down alert, its own message kept, and its failure note saying nothing of a call made again;/unsticksession behind, and its resume meets its first refused request as its first;Not verified
refusalRewindsandrewindEpisode's effect on a new agent. The tests check only that they're cleared. No run had a new agent meet a refusal auto-rewind underautoRewind, or the consolidated rewind marker.teardownEndedAgentfor each agent it cascades to (src/framework.ts:11921), asstopAgentdoes (:11447).freshCallsCutwithin one live agent. A failure note that reads the mark clears it (src/framework.ts:31498). No run had a turn set the mark and then end with no failure note.mainatf8c45174, as of 07:14 UTC on October 29, the subagent module names each agent it spawns with a time stamp (nameStamp(),src/modules/subagent-module.ts:3207), and resumes only a job that a host restart, its deadline or a failure ended (resumeRefusal,:940–956). Other hosts weren't read.What this leaves
cancelActivationfrom a job's owninference:exhaustedlistener, after the job's run has failed on its own, marks the run as ended from outside, so its end clears accounting a resume should keep. That's path 4 in A stop from a listener of an agent's own trace leaves its work acting under its name #894.changelog.d/885-release-clears-failure-accounting.fixed.md.Nigel-2405 made the change, and Ada-1017 made its design calls. Desmond-2579 found that
freshCallsCutwas left over. Stanley-2495 read the change and its commit message. Duncan-2410 checked by reading that each part is needed, against a smallest change he wrote from the purpose before reading the diff, and cut nothing. Our internal review read it in three rounds, and Ada chose which findings to fix. Fiona-2757 wrote this description.