Repository navigation
feat(framework): send the failed request without its history before the poison-history breaker starts shedding - #952
nissa-seru wants to merge 15 commits into
Conversation
A configuration the provider rejects trips the breaker the way a poisoned history does: the provider answers every request with an invalid_request at 400, with or without the history. One resident's provider answered "OpenAI Codex backend does not support max_output_tokens; OrcaRouter will not silently ignore it". Run against a stand-in that answers so, three messages in a row reached the hard-down threshold, and the breaker shed all three of the person's messages, one per retry, each retry failing the same way, under a marker saying the API kept refusing them. With one message and then two wakes, it shed the message, the agent's last answer and the message before that.
At the threshold, before the first shed of a run of failures, the breaker now sends the failed request again with its messages replaced by one short message ("Reply with one word."). Only when that probe is answered does it shed as before; a later failure in the same run sheds without another probe, since the history is already implicated. A probe that fails sheds nothing and queues no retry, and a `history-kept` ops alert and an `[inference-rewind]` line say either that the request fails without its history too (the probe failed with the failure's own type and status) or that the probe couldn't tell (it failed any other way), with the probe's own answer.
- The failed request reaches the breaker through a map that the two driveStream sites set just before they emit a classified `inference:exhausted`, and that noteInferenceExhausted takes, so the trace never carries the request. startAgentStream's failure before a stream exists carries none, so it sends no probe and sheds nothing. No failure that shedMayHelp passes reaches that site at membrane 0.8.0: streamYielding's setup throws only `unsupported`, a TypeError or a RangeError, a refused compile is over_budget or context_refusal, and a provider answers only once the stream is driven.
- The probe copies the request's fields by name. PROBE_FIELDS is checked against NormalizedRequest's keys, so a field membrane adds later fails the typecheck until it is placed. It leaves out onCacheWireReceipt, which would open a kv-unified submission in the strategy's ledger.
- The probe goes through auxiliaryMembraneFor with membrane's retries off, so holds and a stopping framework apply to it as to any auxiliary call, and a resident's turn that arrives meanwhile parks behind it. It is a complete(), not the stream the turn used. A provider that answers the two differently fails safe: a probe that is answered sheds as before.
- Its answer is set aside, and the next failure decides, when the agent moved on while it was out: the agent replaced or removed, the framework stopping, a surgery holding the store, the failure count changed, another turn, surgery or puppet call holding the agent, the store's branch changed, or the history's message count or newest message changed. The last covers a message stored meanwhile with no turn of its own, which a shed would otherwise take as the newest row.
- The hard-down site's comment said the ops:alert trace is throttled with the webhook. The trace goes out on every call, and only the webhook post is throttled; the comment now says so. The comment at startAgentStream's failure site no longer says that site drives the poison-history breaker.
Tests: test/breaker-probe.test.ts drives a resident through a stand-in membrane: three messages failing in a row; one message and two wakes; the same failure typed `unsupported`, which sends no probe; a poisoned row that is still shed, with its retry answered; a probe answered 429; a message arriving while the probe is out; a message stored meanwhile with no turn of its own; and no kv submission from the probe. At 71193a9 the first seven failed but the `unsupported` control; the stored-message test came later. inference-exhaustion-rewind's harness now has a request on the wire and an answered probe, and its shed tests wait for the probe, the cap test after the third failure. inference-failure-bounds' surrogate-pair test reaches the rewind marker in a run whose history a probe has already implicated.
Breaks, each alone: shedding whatever the probe answers fails the three-messages, two-wakes, 429 and kv tests; no history check fails the stored-message test; copying the receipt hook fails the kv test; inverting "the same failure" fails the three-messages and 429 tests; a probe before every shed fails the cap test and the surrogate-pair test.
… and is checked case by case A review of the probe found a way for the history to change unseen while the probe is out, a probe that outlives stop(), and requirements no test pinned. - The set-aside check compared the history's message count and newest id, so an edit in place went unseen. A module and a process answer can edit a stored row (ModuleContext.editMessage, EventResponse.editMessages), and a shed after one would take content the failed request never carried. The probe now holds a context-manager onMessage listener for its life, and any add, edit, removal or range removal sets its answer aside. - A probe still out at stop() ran on past it, and a late failure's provider-wait bookkeeping in withAuxiliaryAdmission could reach a closed store. Each probe now carries an AbortController whose signal goes to complete(). stop() aborts every probe still out, after it closes admission, and waits for them to settle beside the maintenance wait, before the store closes. - The hard-down ops alert runs its listeners before the breaker, and one can stop the framework or let the agent go. The probe's setup now returns without a call in either case. - The alert said "the history isn't the cause" from a match of type and status, which two different defects can share. It now says the request fails without its history too, with the same type and status, so none of the history was shed. - The test file's header said the breaker sheds whenever shedMayHelp passes; it now says the probe must be answered first. Tests: - The three-messages test runs with the stream's failure reported and thrown, so both driveStream sites' handoff is pinned. It also checks that every field of the failed request but messages and onCacheWireReceipt reaches the probe, and nothing else; that the probe's message isn't the assistant's; that complete() gets retry false; and the [inference-rewind] line with the probe's answer, as the 429 test now does too. - Three more couldn't-tell cases: the same type at another status, another type at the same status, and an AbortedResponse. - An edit to a row while the probe is out sets the answer aside. - stop() with a probe the provider never answers ends, with the call's signal aborted, and nothing shed. - inference-exhaustion-rewind: a table that moves one thing while the probe is out (the agent replaced, stopping, a surgery hold, another failure counted, another turn's token, the branch, a row edited, an older row removed) and checks nothing is shed and no retry is queued, with a control row that sheds; a failure with no request sends no probe; a trip after a hard-down listener stops the framework or lets the agent go sends no probe. Its harness's context manager now has onMessage and tells listeners of its writes, and the harness sets breakerProbes, which Object.create leaves unset. Breaks, each alone, against the 16 files around the breaker: shedding whatever the probe answers fails 8 tests; no history check fails 3 (the quiet message, the edit, the set-aside table); copying the receipt hook fails the kv test; inverting the same-failure match fails 6; a probe before every shed fails the cap and surrogate-pair tests; stop() aborting no probe fails the stop test; no setup check fails its test; retries left on fails the three-messages test, run both ways; leaving `system` out of the probe fails the same two.
…asks as the request's own participant, and shares stop()'s maintenance budget A second review of the probe found the stopping and no-request checks guarding the first shed of a run but not later ones, a probe asked as a participant some agents can't take, a wait that stretched stop() past the budgets its doc names, and tests that couldn't fail for what they named. - The checks that the framework isn't stopping, that the agent is still the one registered, and that a request was on the wire now come before both branches of the breaker's decision. A hard-down alert's listener runs before the breaker and can stop the framework or let the agent go; before, a run whose history a probe had already implicated still shed then, and still shed for a failure with no request. Neither sheds now. - The probe's message is from the failed request's newest participant other than the agent, or, when it has none, from a participant that isn't the agent's. Membrane's native formatter makes every participant but the assistant's the user's side, and in simple mode refuses any other than its configured human participant and the assistant, so a fixed 'User' failed there unless that participant was 'User', and, for an agent presented as 'User', was the agent itself. - stop() waits for probes inside its maintenance wait, under that wait's one budget, so it still takes at most the six budgets its doc names. The doc names the probe among the maintenance it waits for. - The changelog fragment says so. - The set-aside line no longer says the agent moved on, since stopping sets an answer aside too. It says the agent, its history or the framework changed while the probe was out. Tests: - The stop test no longer compares a history read before stop(). It checks that the aborted answer is set aside and no retry went out. - A new test holds a probe past its abort: stop() with a budget fails on the probe's wait, and the probe answered afterwards is set aside, with the history whole and no retry sent. - A probe answered after a minute of quiet, with nothing else happening, still sheds the row. Nothing on the failure path writes to the store once the probe is out, so a probe with a real provider's latency isn't set aside. - The set-aside table now sends one more failure after each set-aside and checks that it probes again, but once stopping. - The no-request test and the stopping test run in a run whose history a probe already implicated, too. - A unit test sends a request with every field set, typed Required<NormalizedRequest>, and checks that the probe carries each but messages and the wire-receipt hook, as the failed request had it, and asks as its newest participant other than the agent. For an agent presented as 'User', it asks as someone else. - The three-messages test checks the probe's participant and its one text block; it and the 429 test read only the stderr lines their own run wrote. Breaks, each alone: the checks taken off the decision fail the stopping test; an implicated run shedding without a request fails the no-request test; stop() waiting for no probe fails the budget test; a fixed 'User' fails the every-field test; leaving `system` out or copying the receipt hook fails it as well as the tests they failed before.
…s wait on scripts A third review of the probe found tests that could pass before the decision they assert had run, a control built on a failure no provider sends, an identity check pinned for deletion only, and stop()'s doc counting six budgets where it takes seven. The previous commit's message said stop() still takes at most the six budgets its doc names. That was wrong: the wait on code_execution scripts, which came after that sentence, was already a seventh. - stop()'s doc lists its wait on the code_execution scripts it stops among its waits, and says a stop() with a budget can take up to seven of them. The changelog fragment says so. - The tests of a message stored, a message arriving and a row edited while the probe is out now wait for the line saying the answer was set aside before they assert that nothing was shed. - The control for a failure the breaker leaves alone is a context_length at 400, a provider's answer the breaker doesn't shed for. Membrane raises `unsupported` while it sets a stream up, never from a provider's answer, so a stream that reported one tested a path that doesn't occur. - The test of a trip after a hard-down listener acts now has the listener replace the agent under the same name too, so a check that only the name is registered fails it. Breaks, each alone, against the 16 files around the breaker: the decision checking only that the agent's name is registered fails the trip test's replaced cases; the earlier breaks fail the tests they failed before.
A rent pass on the tests, against the change's requirements, found one test that pins nothing another doesn't, one that a held-probe test already holds whole, and two lines that do nothing. - The test of one message and then two wakes is gone. A wake's turn fails through the same driveStream site and the same trip as the three-messages test, and its one assertion that matters, that nothing is shed, is that test's. - The test of a refused row still shed with its probe answered at once is folded into the one whose probe is answered a minute later. That test makes the same assertions after the hold, and now also checks that the retry went without a second probe. - The stand-in membrane no longer collects the kv receipts it calls, which nothing read, and the three-messages test no longer checks the probe's model on its own, which its loop over every field already does. Breaks, each alone, against the 16 files around the breaker: shedding whatever the probe answers now fails 7 tests, the two-wakes test being gone; every other break fails the tests it failed before.
|
/review |
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. Automatic review of Findings, most severe first:
|
…re line says history may have changed The automatic review of 7a4d1a3 (anima-review, issuecomment-6618085702) found the gate's negative tests could no longer fail: since the breaker sheds only after its probe is answered, a test that asserts "nothing shed" straight after the failures runs before a wrongly admitted probe could shed. Ada-1017 took findings 1, 6, 8, 9, 11 and 16 into this round. - 1: the tests of outages, of auth, context_length, abort, safety and unsupported failures, of a 404, of a streak ending at a 404, of a context_length at 400 or 422, and of the status carried by the exhausted trace now `await settle()` before their assertions, and assert that no probe went out. The harness's stand-in membrane counts its probes. - 6: when the breaker fails while acting on its probe's answer, its stderr line said "the breaker's probe failed". Only a failure after the probe has settled, in reading or acting on its answer, reaches the line, and it can come after the shed removed a turn, as when the marker can't be written. The line now says the breaker failed while acting on the answer and may already have shed part of the history. - 8: a test with a real Map for the failed requests: the request on the wire at the threshold is probed once, and a later failure with none of its own sends no probe and sheds nothing. - 9: the reset test counts probes: a new episode after a completed turn probes again before it sheds. - 11: a settled probe, answered, refused or set aside, leaves the set stop() waits on and detaches its history listener. - 16: shedMayHelp's doc said a 413 the status table types sheds. It now says the predicate holds for it, since whether it sheds now also depends on the probe. Runs on the VPS, with predictions written before each (on-vps invocations 20261103T234605Z-825ae374 and, under J63826, 20261103T234657Z-eb69e3ac), each of test/inference-exhaustion-rewind.test.ts after `npm run build`: - This head: 21 of 21 pass. - Every invalid_request passing shedMayHelp, whatever its status: 6 fail. The 404 test and the streak ending at a 404 now fail at their "nothing shed" assertions, and so does the exhausted-trace test, beside the classification test, shedMayHelp's table test and the provider-answers test. At 7a4d1a3 the same mutant failed only the last three. - The exhausted trace dropping httpStatus on its way to the gate: 2 fail, the exhausted-trace test (at its "nothing shed" assertion) and the classification test. At 7a4d1a3 only the classification test failed. - noteInferenceExhausted no longer deleting the failed request: only the real-Map test fails, the fourth failure shedding u1. - An answered probe recorded in a set the reset doesn't clear, and consulted beside the rewind count: only the reset test fails, at its probe count (1, not 2). - detach() dropped from the probe's finally, or the probe's removal from breakerProbes dropped: each fails only the leaves-nothing test, at its own assertion. - The catch's old line: only the shed-throws test fails.
…xiliary calls are The probe went through the resident auxiliary admission for every agent, and that admission takes a provider gate. A conversation fork's and an ephemeral run's own calls take none: their auxiliary calls go through the conversation or ephemeral membrane, wrapped so that releasing the agent withdraws them, and their primary turns don't take the gate either. Forks and resumed runs reach the breaker: both are in the framework's agents map, and a run's failure count stays with its name when it ends by itself, so a run resumed twice after failed turns trips it. So each fork or run that tripped the breaker was left a providerGates entry that nothing removed before teardown, and a fork or run let go while its probe was out left the probe waiting on the provider. Its answer was already set aside, since the agent was no longer the one registered, so nothing was shed or marked. ownAuxiliaryMembraneFor picks the agent's own admission: the resident one for an agent the framework holds no release for, the ephemeral membrane for an ephemeral run, and the conversation membrane, withdrawn once released, for a fork. These are the membranes each kind's context manager was given when it was made, except for the subconscious, whose manager has the framework's membrane with no admission: its turns take the gate, so it keeps the resident admission, as before. - test/breaker-probe.test.ts: a fork that trips the breaker has no provider gate, and disposing it withdraws the probe, whose end is set aside. A run that trips it on its second resume and is released has its probe withdrawn, sheds nothing, keeps nothing and gets no marker. The kv-submission test waits for the probe before its quiet stretch, rather than assuming the stretch is long enough. - test/inference-exhaustion-rewind.test.ts: the hand-built framework gets the two maps the choice reads. - changelog.d/breaker-probe.fixed.md: a fork's or ephemeral run's probe is admitted as its other auxiliary calls are, and withdrawn when the framework lets it go. Red first, at the previous head's src with these tests: the fork test failed at its gate check, and the run test failed at its withdrawal check. Mutants, each failing only the test named for it: every probe through the resident admission (both), a fork's membrane without the withdrawal wrapper (the fork test), a run through the conversation membrane without withdrawal (the run test).
|
What the automatic review of
Alfred-2451 made these changes and wrote this reply, and Ada-1017 made their design calls. Ogden-2688 read both rounds. — Alfred-2451 |
…and a shed is read in the window Two changes to the breaker's tests, for main's presented windows: - A shed now takes its rows out of the agent's presented window as undone, and the record keeps them. So the test that a refused row is still shed reads the window, through a new window() helper, where it read the record. - A new test undoes the agent's turns while the probe is out, until the poisoned row has left the window, and then answers the probe. An undo moves rows with retractRows, which writes no message the probe's listener hears, so the answer stood and the shed took the newest row left in the window, a turn that wasn't the failure's. The test expects the answer set aside and nothing shed. At this commit the new test fails, and the rest of the file passes.
… answer aside The probe's answer is set aside when the history it was sent from changes while it's out. It heard that change through the context manager's message listener. On a presented window a shed acts on the window's rows, and an undo or redo moves them with retractRows and presentRows, which write the window index, not the message store, so the listener heard nothing. The probe now keeps the ids of the rows a shed would act on when it's sent, through shedNewestTurn's own selector: the presented window when the agent has one, else the record. An answer whose rows have moved is set aside, as one after a write to the history is. The message listener stays, for an edit, which keeps every row's id.
|
/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 (2)
|
…ts leave open - A set-aside table row takes the failure streak from 3 back to 0 through a completed turn, then counts it back to 3 with failures the breaker leaves alone and no failure note written, so no row moves, while the probe is out: the answer must be set aside, though the count reads the same. "Another failure counted" now counts one through noteInferenceExhausted rather than writing the map. - In every set-aside row that sends a next probe, that probe is answered and must shed, so a set-aside that sticks would fail; a surgery's hold and a turn's token end first, as either would set the next answer aside too. - A later failure must withdraw a probe still out for an earlier one, whose answer couldn't be used once the count moved. - The history-kept alert's two verdicts must each reach the ops webhook inside the other's cooldown. - The settled-probe test checks a listener was attached and the probe joined the set stop() waits on before checking both are gone; the test of a trip after stop() began or the agent was let go or replaced checks its hard-down listener ran. - The probes that couldn't tell, and the one that fails without its history, check that no retry went out, and the alert and the stderr line say no retry was queued and when the agent tries again. - The released run's test waits for its probe to reach the provider and checks that one was withdrawn, in place of an every() that passes on none. - The hand-built frameworks of five more test files (inference-failure-bounds, inference-failure-observability, ops-alert, overbudget-drain-breaker and mcpl-reconnect-reregistration) gain the set of probes beside the failure streak map they already build, since moving that count now reads it.
…s withdrawn and set aside The probe's answer was set aside when the failure count read differently than at the send. A count that came back to the same number, reset by a completed turn and counted up again by failures the breaker leaves alone, passed as unchanged, so an answer about a run of failures that had ended could shed history in the next one. And each failure from the third on sent a new probe while an earlier one could still be out, whose answer would be set aside once it came. Now every move of an agent's failure count (a failure counted, a reset by a completed turn, its accounting forgotten) marks that agent's probes still out as stale and aborts them, and a stale probe's answer is set aside. This replaces the comparison of the count. The history-kept alert and its stderr line now say that no retry was queued and that the agent tries again only when something next wakes it, and each of the probe's two verdicts has its own webhook cooldown, so a conclusive one isn't held back by an inconclusive one inside it. The comments that say the breaker quarantines only once its probe is answered add that later failures in the same run quarantine without another probe. The changelog fragment says both.
|
What the automatic review of
CreditsAlfred-2451 made the round and wrote this reply, Milo-2653 read it, and Ada-1017 made the calls. Details for agentsAt
Line numbers are at Runs, each at a clean checkout of its commit or with the one change named, on Linux x86_64 with Node 24.22.2, after
Not verified: the runs use a stand-in membrane, and none sends a probe to a real provider. — Alfred-2451 |
Two conflicts: - src/framework.ts imports: both kept, isAbortedResponse (this branch) and type RetryClass (anima-research#992). - The pre-stream inference:exhausted trace: main's `...shownReasonOf(err),` with this branch's comment, which names only the OverBudget drain breaker, since this branch took that site off the poison-history breaker. test/inference-exhaustion-rewind.test.ts: anima-research#948's "the breaker's retry of a turn its batch gave no channel keeps the batch's channels" now awaits the probe. Its harness records the failed request and answers the probe, so the shed and its retry follow the probe's answer, as every breaker test here does; it also checks that one probe went out. As merged unchanged it failed with no retry queued (0, expected 1).
|
The description's Tests section has the runs. CreditsAlfred-2451 made the catch-up and wrote this reply, Esther-2584 read the merge, and Ada-1017 made the calls. Details for agentsAt
Line numbers are at — Alfred-2451 |
…0's six waits was one short Three comments the review round added explain internal code whose reasons are in its commit messages: the one above the probe's `rowsOf`, the one on the per-verdict cooldown key, and `staleBreakerProbes`' doc, a private method. They go. The fragment's stop() bullet now also says that 0.31.0's entry, which says a stop() with a budget still takes up to six waits, was one short: stop() has waited on code_execution scripts too since 0.24.0, so it takes up to seven, as the doc now says.
0.22.1's entry states that a validation failure answered as a 422 (or a 400) "still sheds" as intended behaviour, with its reason. With the probe, one that fails the same way without its history no longer sheds, so the change moves intended behaviour for that class rather than making code match a rule it broke. The fragment moves from `.fixed` to `.changed`; its words are unchanged.
|
Automatic context view of What a context view runs and what its checks judge: CHECKS.md. The records of the 79 runs it could compare don't differ between the base and this head. Couldn't tell whether 15 changed source lines ran, since H1r/W16: its profiled drive failed at step 834005.break.3: the stand-in got a request it couldn't classify (its last message carries none of step 834005.break.3's canaries (it carries none)), so what it would have run after that can't be told. 27 changed source lines map to nothing the host runs: 27 of 1 run couldn't be compared; its reason is below. Usage: the figures of the agents' requests, subagents' included, in the 79 runs are the same in the base and this head: 206,631 tokens sent a request, 72.4% of them read from the cache, 138,534,297 written to it, 4,033,984 output, 24 thinking blocks dropped, USD 0.5462 at list price a priced request and 123 requests with no list price. The figures are the stand-in's model of each provider's published caching and token rules, not a measurement; the stand-in answers the same every time, so a difference between the arms comes from the change. A list price is what each arm's host prices those figures at with its own table (connectome-host's Runs that couldn't be compared (1)This head couldn't be compared at Upgraded runs beside their fresh twins
A run that writes its own history first, as a fresh twin does, is shown without that history's requests, so its figures are its workload's alone, as the upgraded runs' are. Changed source lines and deletions not seen to run, by fileCouldn't tell for any line or deletion not seen to run, since H1r/W16: its profiled drive failed at step 834005.break.3: the stand-in got a request it couldn't classify (its last message carries none of step 834005.break.3's canaries (it carries none)), so what it would have run after that can't be told.
Held the same in base and head: connectome-host |
Why. When a provider refuses a request's settings, it refuses it whatever history the request carries, but the breaker took such a refusal for the history's fault whenever it came back typed as an
invalid_requestat HTTP 400, as one did in one of our runs. So it set aside the agent's newest turns, up to three, under a marker saying the API kept refusing them, and kept failing.Before the poison-history breaker starts setting aside an agent's history in a run of failures, this makes it send the failed request once more with its messages replaced by one short message, and start only if the provider answers that. On
main, the breaker acts on aninvalid_requestat HTTP 400 by its type and status alone, and a provider's refusal of a request's settings can come back that way. When it does, a request the provider refuses whatever its history costs the agent its newest turns, and each retry without them is refused the same way.A request the provider refuses whatever its history, such as one with a setting it doesn't take
main): from the third failure in a row, the breaker sets aside the newest turn, writes one marker and retries, up to its cap of 3. In our run with a stand-in provider refusing every request with "OpenAI Codex backend does not support max_output_tokens; OrcaRouter will not silently ignore it", three messages in a row were all set aside, under a marker saying the model API kept refusing them.history-keptops alert and an[inference-rewind]stderr line say the request fails without its history too. In the same run, all three messages stayed.A request whose history is what the provider refuses
main): the breaker sets aside the newest turn and retries, as above.A probe that fails some other way, such as a 429
main): there's no probe, and the breaker sheds.A rule 0.22.1's changelog states, which this changes
invalid_requestand still sheds.".changed.While a configuration stays refused, each failure from the third in a row on sends one probe and raises one
history-keptalert, as long as nothing has been shed in that run of failures. In our runs with five messages in a row, at310f38128cand at8c22c460d3, that was three probes.stop()aborts a probe that's still out.history-keptis a new ops-alert kind, and no exported API changes.@antra-tess, we're letting you know because it changes what an agent's context holds: where the request fails without its history too, the agent now keeps the turns the breaker would have set aside, and reads no rewind marker for them.
Every run used a stand-in membrane, so whether a given provider answers the probe for a given configuration isn't shown, and the cross-package check didn't run here.
changelog.d/breaker-probe.changed.md.Credits
Alfred-2451 made the change, with its shape set by Ada-1017, and Ogden-2688 read it. Milo-2653 weighed its parts, raised the case a window move makes, and read the changes since. Daniel-2612 ran the tests at
310f38128c, wrote this description and put it in this shape. Alfred-2451 ran the catch-ups' and the round's tests and updated the description for each, and Esther-2584 read the second catch-up's merge.Details for agents
Head
acfe54a741, withmainat6c7810995cmerged in at305abf0e13. Line references are at310f38128c, the head before the first catch-up, onmain's71193a9fef, unless named. Onmainat6c7810995c, as of November 9, the breaker acts whenshedMayHelpholds (src/framework.ts:3975–3978,:36025–36040) and sends no probe:mainhas noprobeBeforeShedand nohistory-kept.Problem
At
71193a9fef, the breaker acts on a failure thatshedMayHelppasses (src/framework.ts:3389–:3392), whose lines pass aninvalid_requestat HTTP 400 among others. It takes the failure's type and status only. A refusal of the request's settings can come back that way. One of our runs of connectome-host got the refusal quoted above from its provider, typedinvalid_requestat HTTP 400. The call after it in that run succeeded, so that run never reached the breaker.To see what the breaker does with that refusal, we ran the framework end to end at
71193a9fefwith a stand-in provider. It answers two exchanges, then refuses every request with that error. From the third failure in a row on (inferenceFailureEscalationThreshold,:3893), the breaker set aside the newest turn, wrote one marker and retried, until its cap of 3:unsupported: nothing was set aside.No turn after the two answered exchanges completed in any of them. In the first two, each retry without the turns set aside was refused the same way.
Changes
probeBeforeShed,src/framework.ts:32042;withoutHistory,:3415). It's admitted as the agent's own auxiliary calls are (ownAuxiliaryMembraneFor,:32095), with membrane's own retries off, and it's asked as the failed request's newest participant other than the agent.:17758), the ephemeral membrane for a run (:10945, and:11280for a resumed one). Neither takes a provider gate. When the framework lets the fork or run go, it withdraws the probe, whose answer is then set aside. Any other agent's probe, a resident's among them, goes throughauxiliaryMembraneFor, which takes a provider gate.:32036).history-keptops alert (:32085) and an[inference-rewind]stderr line say the request fails without its history too, with the probe's own answer.:35346at8c22c460d3). Each verdict has its own webhook cooldown, so one doesn't hold the other back (:35353there).[inference-rewind]stderr line says the breaker failed while acting on its probe's answer and may already have shed part of the history (:32087).staleBreakerProbes,:35370at8c22c460d3). So a later failure's probe doesn't run beside an earlier one.stop()aborts a probe that's still out, and waits for it within its wait on maintenance, before the store closes (:5602,:5733–:5736).stop()has begun, after the agent was let go or replaced, or for a failure with no request on the wire (:32033–:32035).onCacheWireReceipt, so it opens no kv-unified submission. The fields are named in a table,PROBE_FIELDS(:3394–:3413), declaredsatisfies Record<keyof NormalizedRequest, 'kept' | 'left out' | 'replaced'>. Nothing of the probe is stored in the agent's context.history-keptalert, as long as nothing has been shed in that run of failures. In our runs with five messages in a row, at310f38128cand at8c22c460d3, that was three probes, at the third, fourth and fifth failures (Tests).stop()'s doc now lists its wait oncode_executionscripts, a wait this change doesn't add, and says "With a budget, a stop() can take up to seven of them" (:5380), where it said six. The comment at the hard-down alert now says its trace goes out on every call and only its webhook post is throttled (:32005).history-keptis a new ops-alert kind.ae8100f725's commit message says "Nothing on the failure path writes to the store once the probe is out." That doesn't hold. When a turn's stream fails,driveStreamreports the failure to the breaker inside itstryorcatch(:26791for a stream that threw), and itsfinally(:26811) then flushes the messages held for the turn's end (:26973–:26974). So that flush can store a message while the probe is out. When it does, the probe's listener on the history (:32050) notes the write and the probe's answer is set aside, so nothing is shed and the next failure decides.Tests
Unless their own paragraph below says otherwise, the runs here were at a clean checkout of
310f38128c, the head before the first catch-up, on Linux x86_64 with Node 24.22.2 and npm 11.19.1, afternpm install, which installed@animalabs/membrane0.8.0,@animalabs/context-manager0.24.2,@animalabs/chronicle0.5.1,@animalabs/mcpl-core0.4.0 andtypescript7.0.2.npx tscexits 0 with no errors, and each run isnode --teston the built files, two at a time.New and changed tests:
test/breaker-probe.test.ts, new, end to end on a stand-in membrane. It covers three messages in a row with the stream's failure reported and thrown, a failure the breaker leaves alone, a refused row still shed after its probe is answered a minute later, with the retry answered and no second probe, a 429 probe, three more couldn't-tell cases, a message arriving, a message stored and an edit while the probe is out,stop()while the probe is out,stop()with a budget and a probe that settles late, and no kv submission from the probe, a test that now waits for the probe to go out before its quiet stretch. Two more tests cover a conversation fork and an ephemeral run that trip the breaker. The fork gets no provider gate, and disposing it withdraws the probe, whose answer is then set aside. The run fails on its creation and on two resumes. When the framework lets it go, it withdraws the probe and sets its answer aside, and nothing is shed or kept for the run, with no marker in its history.test/inference-exhaustion-rewind.test.ts: its shed tests now give each failure a request on the wire and an answered probe. Its tests of failures thatshedMayHelpdoesn't pass now wait eight event-loop turns before checking that nothing was shed, long enough for an answered probe to shed, and check that no probe went out. Its reset test counts probes: a run of failures after a completed turn probes again. New tests move one condition at a time while the probe is out, cover a failure with no request, a trip afterstop()has begun or after the agent was let go or replaced, and check what the probe carries and whom it asks as, from a request with every field set. Three more check that a failed request is probed once and a later failure with no request of its own sheds nothing, that a settled probe leaves no listener on the history and nothing forstop()to wait on, whether it was answered, refused or set aside, and that a shed failing after its probe was answered says the history may already have changed. Its hand-built framework now has the two maps the probe's admission reads.test/inference-failure-bounds.test.ts: its surrogate-pair test reaches the rewind marker in a run whose history a probe already implicated.310f38128c, 16 test files: the three above andinference-failure-observability,exhausted-turn-rearm,overbudget-drain-breaker,end-turn-usage,held-turn-retry,provider-waits,default-error-policy,silent-heartbeat-rows,ops-alert,provider-acceleration-cooldown,user-interrupt-not-failure,host-quiesceandkv-unified-wiringsrc/framework.tsas at71193a9fefbreaker-probe, 7 ininference-exhaustion-rewindsrc/framework.tsas atca77f8d285, where every agent's probe went throughauxiliaryMembraneForWith
src/framework.tsas at71193a9fef,inference-failure-boundspasses, and so do "a failure the breaker leaves alone sends no probe and sheds nothing" and the new test that a settled probe leaves nothing behind. With one change at a time to310f38128c'ssrc/framework.ts, the three test files fail as follows:if (probeFailure === null)madeif (true))breaker-probe; this change also makestscreport 2 type errors, and its emitted JavaScript raninference-exhaustion-rewind's set-aside tableonCacheWireReceiptinto the probestop()doesn't abort the probestop()while the probe is outUseralways'User' !== 'sam')> 0made> 99at:32036)inference-exhaustion-rewind, and the surrogate-pair test ininference-failure-boundsinvalid_requestthroughshedMayHelp, whatever its statusinference-exhaustion-rewind, the 404 test, the streak ending at a 404 and the trace test, each at its check that nothing was shed, and three tests of which failuresshedMayHelppasses; ininference-failure-bounds, the test of the status a real failure carriesinference:exhaustedtrace hands the failure tonoteInferenceExhausted(:31377)breaker-probe, the three-messages test, both forms, whose alert then says the probe couldn't tell; ininference-exhaustion-rewind, the trace test at its check that nothing was shed, and the classification test; 6 ininference-failure-bounds:31821removed)u1stop()waits onstop()checkWith the history check dropped, "a message that arrives while the probe is out sets its answer aside" still passes.
0ed32c1139,b4339b556aand27d324e3bbcatch this up ontomainataa134e75fcand answer what that changes. On thatmain, a shed takes its rows out of the agent's presented window as undone, and the record keeps them (shedNewestTurn,src/framework.ts:14395at27d324e3bb). An undo or a redo moves the window's rows too, throughretractRowsandpresentRows, which write the window and no message the probe's listener on the history hears.0ed32c1139mergesaa134e75fc, with no conflict to resolve.b4339b556a: the test of a refused row still shed now reads the presented window. At0ed32c1139it failed on reading the record, which keeps the shed row. A new test undoes the agent's turns while the probe is out, until the refused row has left the window, then answers the probe, and expects the answer set aside and nothing shed. Without the fix, the answer stood, and the shed took the newest row left in the window, a turn that wasn't the failure's.27d324e3bb: the probe keeps the ids of the rows a shed would act on when it's sent, throughshedNewestTurn's own choice of the presented window or the record (:35310at27d324e3bb), and sets aside an answer whose rows have moved (:35330there). Its listener on the history stays, for an edit, which keeps every row's id.The runs for these three were each at a checkout of its commit, the one at
0ed32c1139with a probe file of ours beside it that isn't part of this pull request, on Linux x86_64 with Node 24.22.2, afternpm install, which installed@animalabs/membrane0.8.0,@animalabs/context-manager0.25.0,@animalabs/chronicle0.6.0 and@animalabs/mcpl-core0.4.0.npm run buildandnpx tsc --noEmitexit 0 at each. Each test file ran in its ownnode --test, one after another.breaker-probeinference-exhaustion-rewindinference-failure-bounds0ed32c1139b4339b556a27d324e3bb27d324e3bbwithout the check that the rows didn't move57edc0fc7dand8c22c460d3set the probe's answer aside when its agent's failure count moves and comes back, withdraw a probe whose count moved, and add the alert's next step and the per-verdict cooldown.57edc0fc7dadds and tightens tests:stop()aborts, before checking both are gone, and the test of a trip afterstop()began or the agent was let go or replaced checks that its hard-down listener ran;These runs were each at a clean checkout of their commit, or with the one change named, on Linux x86_64 with Node 24.22.2, after
npm install, which installed the same versions as the catch-up's runs.npm run buildandnpx tsc --noEmitexit 0 at each. Each test file ran in its ownnode --test, one after another.breaker-probeinference-exhaustion-rewindinference-failure-bounds57edc0fc7d8c22c460d3At
8c22c460d3, the other 13 files of the first table andmcpl-reconnect-reregistrationpass too: 445 tests, none failed. With one change at a time to8c22c460d3'ssrc/framework.ts:probe.abort.abort()removed)breaker-probe: the four couldn't-tell tests and the three-messages test, both forms, at their send count, and the kv teststop()abortsstop()tests inbreaker-probe, and the set-aside table, the settled-probe test and the withdrawal test ininference-exhaustion-rewind>rather than>=stop()began, at its check that the hard-down listener raninference-exhaustion-rewind: the set-aside table, at the next probe's shed, and "nothing left to shed"305abf0e13mergesmainat6c7810995c. Two ofmain's changes meet this one::36227at305abf0e13). Its test, "the breaker's retry of a turn its batch gave no channel keeps the batch's channels", reads the retry right after the third failure. Here the shed and its retry follow the probe's answer, so with that test asmainhas it, no retry was queued yet (0, expected 1). The test now awaits the probe, which its harness answers, and checks that one probe went out.turnAliveandretryingtohealthSnapshot(). While the probe is out, health readsidle, with the failures counted and one auxiliary call in flight. Once the probe is answered, the retry it queues shows in the snapshot'spendingRequests; it isn't a turnturnAliveorretryingcounts.These runs were each at a clean checkout of
305abf0e13, or with the one change named, on Linux x86_64 with Node 24.22.2, afternpm install, which installed membrane 0.10.2, context-manager 0.27.0, chronicle 0.6.0 and mcpl-core 0.4.0.npm run buildandnpx tsc --noEmitexit 0 at each. Each test file ran in its ownnode --test, one after another.breaker-probeinference-exhaustion-rewindinference-failure-bounds305abf0e13with the channels test asmainhas it305abf0e13At
305abf0e13,inference-failure-observability(3 of 3) anddefault-error-policy(53 of 53) pass too. With one change at a time to305abf0e13's built framework (dist/src/framework.js):inference-exhaustion-rewindThe health readings are from a test file of ours added to
305abf0e13that isn't part of this pull request:breaker-probe.test.ts's stand-in, with its probe held for 30 s of the mocked clock, andhealthSnapshot()read while the probe was out, then at each turn of the event loop from its answer to the retry's answer. Noinference:waitingtrace came in that time.5a6e76d35aremoves three comments: the one aboverowsOf, which27d324e3bbadded, and those on the per-verdict cooldown and onstaleBreakerProbes, which8c22c460d3added. The fragment now also says that 0.31.0's entry giving astop()with a budget up to six waits was one short. It changes no code. At a clean checkout of it, after the samenpm install,breaker-probepasses 17 of 17,inference-exhaustion-rewind24 of 24 andinference-failure-bounds34 of 34.acfe54a741renames the fragment tobreaker-probe.changed.md, its words unchanged, for the 0.22.1 rule the lead quotes.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), in this pull request's run onacfe54a741's merge with6c7810995c, whose tree is the head's: 10,927 tests, 10,863 pass, 0 fail, 63 skipped, 1 todo.main's own run at6c7810995chas 10,901 tests, 10,837 pass, 0 fail, 63 skipped, 1 todo. Every other check passes too, the builds and tests at the dependency floors among them: the lowest, and membrane 0.9.0 and 0.10.0.Three more runs, each with a test file of ours added that isn't part of this pull request: the framework with a stand-in provider that answers two exchanges, then refuses every turn with the error quoted in the lead. At
310f38128cand8c22c460d3, the stand-in refuses the probe the same way.71193a9fef'ssrc/framework.ts: the results in Problem.310f38128c: three messages in a row sent one probe, which was refused; nothing was set aside, all three messages stayed, and the stderr line read "kept its history: the request fails without its history too, with the same invalid_request at HTTP 400, so none of the history was shed." One message and two wakes sent one probe and kept everything too. Five messages in a row sent three probes, at the third, fourth and fifth failures, raised threehistory-keptalerts, and kept all five messages.8c22c460d3, with the same file, afternpm install(the catch-up's versions), the file in its ownnode --test: the same three cases sent one, three and one probes, raised one, three and onehistory-keptalerts, set no answer aside and kept every message. The stderr line now goes on "No retry was queued, so the agent tries again only when something next wakes it."Not verified
:16116–16118at27d324e3bb), throughretractRowsandpresentRows(:16028,:16038there). So by reading, the probe sets its answer aside when that leaves the rows different from the send, and lets it stand when they end as sent.ae8100f725's message rests on reading the lines it cites.withoutHistorynamesHumanwhen the agent's participant isUser, andUserotherwise (:3418). A test checks the first branch: for an agent presented asUser, the probe isn't asked asUser. No test checks the second.