Skip to content

feat(framework): send the failed request without its history before the poison-history breaker starts shedding - #952

Open
nissa-seru wants to merge 15 commits into
anima-research:mainfrom
nissa-seru:alfred/breaker-probe
Open

nissa-seru wants to merge 15 commits into
anima-research:mainfrom
nissa-seru:alfred/breaker-probe

Conversation

@nissa-seru

@nissa-seru nissa-seru commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

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_request at 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 an invalid_request at 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

  • base (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.
  • head: the breaker first sends the failed request with its messages replaced by "Reply with one word.". Refused the same way, nothing is set aside and no retry is queued. A history-kept ops 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

  • base (main): the breaker sets aside the newest turn and retries, as above.
  • head: the probe is answered, and the breaker sets aside the newest turn as before, unless the agent moved on while the probe was out, as when a message is stored in its history. Then the answer is set aside: nothing is shed, no retry is queued, and the next failure decides. Later failures in the same run of failures shed without another probe.

A probe that fails some other way, such as a 429

  • base (main): there's no probe, and the breaker sheds.
  • head: nothing is shed and no retry is queued, and the alert and the line say the probe couldn't tell whether the history is the cause.

A rule 0.22.1's changelog states, which this changes

  • 0.22.1: "A 422 from an OpenAI-compatible server that answers a validation failure with it, such as a FastAPI one, arrives as invalid_request and still sheds."
  • head: a validation failure the provider answers the same way without the history, as it answers a setting it refuses, no longer sheds. One of the history itself still sheds, once its probe is answered. So the changelog fragment is .changed.

While a configuration stays refused, each failure from the third in a row on sends one probe and raises one history-kept alert, as long as nothing has been shed in that run of failures. In our runs with five messages in a row, at 310f38128c and at 8c22c460d3, that was three probes. stop() aborts a probe that's still out. history-kept is 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 fragment added: 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, with main at 6c7810995c merged in at 305abf0e13. Line references are at 310f38128c, the head before the first catch-up, on main's 71193a9fef, unless named. On main at 6c7810995c, as of November 9, the breaker acts when shedMayHelp holds (src/framework.ts:3975–3978, :36025–36040) and sends no probe: main has no probeBeforeShed and no history-kept.

Problem

At 71193a9fef, the breaker acts on a failure that shedMayHelp passes (src/framework.ts:3389–:3392), whose lines pass an invalid_request at 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, typed invalid_request at 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 71193a9fef with 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:

  • Three messages in a row: it set aside the third message, then on its retries the second and the first. The agent's context kept the two answered exchanges, its failure notes, and a marker that begins "[refusal-rewind] The 3 most recent turn(s) were set aside because the model API kept refusing the requests that carried them".
  • One message, then two wakes with nothing new: it set aside the message, then the agent's second answer, then the second message before it.
  • The same error typed 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

  • The probe. When the breaker would shed and nothing has been shed yet in this run of failures, it first sends the failed request with its messages replaced by one message, "Reply with one word." (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.
  • A conversation fork's or an ephemeral run's probe goes through the membrane its context manager was given: the conversation membrane for a fork (:17758), the ephemeral membrane for a run (:10945, and :11280 for 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 through auxiliaryMembraneFor, which takes a provider gate.
  • The probe is answered: the breaker sheds as before, newest first, with one marker and a retry, up to the cap. Later failures in the same run of failures shed without another probe (:32036).
  • The probe fails with the failure's own type and status: nothing is shed and no retry is queued. A history-kept ops alert (:32085) and an [inference-rewind] stderr line say the request fails without its history too, with the probe's own answer.
  • The probe fails any other way, such as a 429, another status or an aborted call: nothing is shed and no retry is queued, and the alert and the line say the probe couldn't tell whether the history is the cause.
  • On either verdict, the alert and the line also say that no retry was queued, so the agent tries again only when something next wakes it (:35346 at 8c22c460d3). Each verdict has its own webhook cooldown, so one doesn't hold the other back (:35353 there).
  • When acting on the probe's answer fails, as when the rewind marker can't be written after a shed, an [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).
  • The probe's answer is set aside when the agent moved on while it was out: a message was added to its history or a row edited or removed, the rows a shed would act on moved, as an undo or a redo moves a presented window's, another turn holds it, its failure count moved, even back to the same number, a surgery holds the store, the store's branch changed, the agent was replaced or let go, or the framework is stopping. The next failure decides afresh.
  • A probe whose agent's failure count moves is withdrawn at once: a failure counted, the streak reset by a completed turn, or the agent's failure accounting forgotten marks that agent's probes still out, and aborts them (staleBreakerProbes, :35370 at 8c22c460d3). 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).
  • No probe and no shed after stop() has begun, after the agent was let go or replaced, or for a failure with no request on the wire (:32033–:32035).
  • What the probe carries: each field of the failed request except its messages and onCacheWireReceipt, so it opens no kv-unified submission. The fields are named in a table, PROBE_FIELDS (:3394–:3413), declared satisfies Record<keyof NormalizedRequest, 'kept' | 'left out' | 'replaced'>. Nothing of the probe is stored in the agent's context.
  • Its cost while a configuration stays refused: each failure from the third in a row on sends one probe and raises one history-kept alert, as long as nothing has been shed in that run of failures. In our runs with five messages in a row, at 310f38128c and at 8c22c460d3, that was three probes, at the third, fourth and fifth failures (Tests).
  • Comments and docs: stop()'s doc now lists its wait on code_execution scripts, 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).
  • No exported API changes. history-kept is 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, driveStream reports the failure to the breaker inside its try or catch (:26791 for a stream that threw), and its finally (: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, after npm install, which installed @animalabs/membrane 0.8.0, @animalabs/context-manager 0.24.2, @animalabs/chronicle 0.5.1, @animalabs/mcpl-core 0.4.0 and typescript 7.0.2. npx tsc exits 0 with no errors, and each run is node --test on 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 that shedMayHelp doesn'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 after stop() 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 for stop() 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.
Run Tests Failed
310f38128c, 16 test files: the three above and inference-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-quiesce and kv-unified-wiring 523 0
The three test files above, with src/framework.ts as at 71193a9fef 69 22: 15 in breaker-probe, 7 in inference-exhaustion-rewind
The three test files above, with src/framework.ts as at ca77f8d285, where every agent's probe went through auxiliaryMembraneFor 69 2: the fork test, at its check that the fork has no provider gate, and the run test, at its check that the probe was withdrawn

With src/framework.ts as at 71193a9fef, inference-failure-bounds passes, 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 to 310f38128c's src/framework.ts, the three test files fail as follows:

Change Failed
Shed whatever the probe answers (if (probeFailure === null) made if (true)) 7, all in breaker-probe; this change also makes tsc report 2 type errors, and its emitted JavaScript ran
Drop the check that the history wasn't written to 3: a message stored and an edit while the probe is out, and the edited row in inference-exhaustion-rewind's set-aside table
Copy onCacheWireReceipt into the probe 2: no kv submission, and what the probe carries
stop() doesn't abort the probe 1: stop() while the probe is out
Ask the probe as User always 1: what the probe carries ('User' !== 'sam')
Leave membrane's own retries on for the probe 2: the three-messages test, both forms
Probe before every shed, not only the first in a run of failures (> 0 made > 99 at :32036) 2: "the breaker is capped" in inference-exhaustion-rewind, and the surrogate-pair test in inference-failure-bounds
Let every invalid_request through shedMayHelp, whatever its status 7: in inference-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 failures shedMayHelp passes; in inference-failure-bounds, the test of the status a real failure carries
Drop the HTTP status where the inference:exhausted trace hands the failure to noteInferenceExhausted (:31377) 10: in breaker-probe, the three-messages test, both forms, whose alert then says the probe couldn't tell; in inference-exhaustion-rewind, the trace test at its check that nothing was shed, and the classification test; 6 in inference-failure-bounds
Keep the failed request after the breaker reads it (:31821 removed) 1: the test of a failed request probed once, whose fourth failure then sheds u1
Remember an answered probe past a completed turn, so a later run of failures sheds without a probe 1: the reset test, at its probe count (1, not 2)
Leave a settled probe's history listener attached 1: a settled probe leaves nothing behind, at its listener check
Leave a settled probe in the set stop() waits on 1: a settled probe leaves nothing behind, at its stop() check
The stderr line's earlier text, "the breaker's probe failed" 1: a shed that fails after its probe was answered
A conversation fork's probe through the conversation membrane without the wrapper that withdraws it 1: the fork test, at its check that disposing the fork withdrew the probe
An ephemeral run's probe through the conversation membrane 1: the run test, at its check that the probe was withdrawn

With the history check dropped, "a message that arrives while the probe is out sets its answer aside" still passes.

0ed32c1139, b4339b556a and 27d324e3bb catch this up onto main at aa134e75fc and answer what that changes. On that main, a shed takes its rows out of the agent's presented window as undone, and the record keeps them (shedNewestTurn, src/framework.ts:14395 at 27d324e3bb). An undo or a redo moves the window's rows too, through retractRows and presentRows, which write the window and no message the probe's listener on the history hears.

  • 0ed32c1139 merges aa134e75fc, with no conflict to resolve.
  • b4339b556a: the test of a refused row still shed now reads the presented window. At 0ed32c1139 it 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, through shedNewestTurn's own choice of the presented window or the record (:35310 at 27d324e3bb), and sets aside an answer whose rows have moved (:35330 there). 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 0ed32c1139 with a probe file of ours beside it that isn't part of this pull request, on Linux x86_64 with Node 24.22.2, after npm install, which installed @animalabs/membrane 0.8.0, @animalabs/context-manager 0.25.0, @animalabs/chronicle 0.6.0 and @animalabs/mcpl-core 0.4.0. npm run build and npx tsc --noEmit exit 0 at each. Each test file ran in its own node --test, one after another.

Commit breaker-probe inference-exhaustion-rewind inference-failure-bounds
0ed32c1139 15 of 16 pass: the refused row still shed, on the record 21 of 21 32 of 32
b4339b556a 16 of 17 pass: the new undo test fails 21 of 21 32 of 32
27d324e3bb 17 of 17 21 of 21 32 of 32
27d324e3bb without the check that the rows didn't move 16 of 17 pass: the new undo test fails 21 of 21 32 of 32

57edc0fc7d and 8c22c460d3 set 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. 57edc0fc7d adds and tightens tests:

  • a set-aside table row takes the 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;
  • in every set-aside row that sends a next probe, that probe is answered and sheds;
  • a later failure withdraws an earlier probe still out, and that probe's answer is set aside;
  • the two verdicts each reach the ops webhook inside the other's cooldown;
  • the couldn't-tell tests check that no retry went out, and the alert and the stderr line say what happens next;
  • the settled-probe test checks that a listener was attached and the probe joined the set stop() aborts, before checking both are gone, and the test of a trip after stop() began or the agent was let go or replaced checks that its hard-down listener ran;
  • the released run's test requires its probe to reach the provider;
  • five more hand-built frameworks build the probe set beside the failure streak map.

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 build and npx tsc --noEmit exit 0 at each. Each test file ran in its own node --test, one after another.

Commit breaker-probe inference-exhaustion-rewind inference-failure-bounds
57edc0fc7d 14 of 17 pass: the three-messages test, both forms, and the 429 test fail, at the alert's next step 20 of 23 pass: the set-aside table fails at its 3 → 0 → 3 row, by shedding, and the withdrawal and webhook tests fail 32 of 32
8c22c460d3 17 of 17, three times 23 of 23 32 of 32

At 8c22c460d3, the other 13 files of the first table and mcpl-reconnect-reregistration pass too: 445 tests, none failed. With one change at a time to 8c22c460d3's src/framework.ts:

Change Failed
The withdrawal leaves the probe running (probe.abort.abort() removed) 1: the withdrawal test
The kept-history branch also queues a turn 7, all in breaker-probe: the four couldn't-tell tests and the three-messages test, both forms, at their send count, and the kv test
The probe never joins the set stop() aborts 5: the two stop() tests in breaker-probe, and the set-aside table, the settled-probe test and the withdrawal test in inference-exhaustion-rewind
The hard-down threshold read as > rather than >= 36 across the three files, among them the test of a trip after stop() began, at its check that the hard-down listener ran
A set-aside makes the agent's later answers ignored 2, in inference-exhaustion-rewind: the set-aside table, at the next probe's shed, and "nothing left to shed"

305abf0e13 merges main at 6c7810995c. Two of main's changes meet this one:

  • #948 gives the breaker's retry the channels its batch woke on (:36227 at 305abf0e13). 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 as main has 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.
  • #992 adds turnAlive and retrying to healthSnapshot(). While the probe is out, health reads idle, with the failures counted and one auxiliary call in flight. Once the probe is answered, the retry it queues shows in the snapshot's pendingRequests; it isn't a turn turnAlive or retrying counts.

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, after npm install, which installed membrane 0.10.2, context-manager 0.27.0, chronicle 0.6.0 and mcpl-core 0.4.0. npm run build and npx tsc --noEmit exit 0 at each. Each test file ran in its own node --test, one after another.

Commit breaker-probe inference-exhaustion-rewind inference-failure-bounds
305abf0e13 with the channels test as main has it 17 of 17 23 of 24 pass: the channels test fails, at its retry count not run
305abf0e13 17 of 17 24 of 24 34 of 34

At 305abf0e13, inference-failure-observability (3 of 3) and default-error-policy (53 of 53) pass too. With one change at a time to 305abf0e13's built framework (dist/src/framework.js):

Change Failed in inference-exhaustion-rewind
The breaker's retry without the batch's channels 1: the channels test, at its channels
An answered probe that never sheds 13, the channels test among them, at its retry count

The health readings are from a test file of ours added to 305abf0e13 that 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, and healthSnapshot() read while the probe was out, then at each turn of the event loop from its answer to the retry's answer. No inference:waiting trace came in that time.

5a6e76d35a removes three comments: the one above rowsOf, which 27d324e3bb added, and those on the per-verdict cooldown and on staleBreakerProbes, which 8c22c460d3 added. The fragment now also says that 0.31.0's entry giving a stop() with a budget up to six waits was one short. It changes no code. At a clean checkout of it, after the same npm install, breaker-probe passes 17 of 17, inference-exhaustion-rewind 24 of 24 and inference-failure-bounds 34 of 34.

acfe54a741 renames the fragment to breaker-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, then node scripts/test-as-ci.mjs, on ubuntu-latest with Node 22.12.0), in this pull request's run on acfe54a741's merge with 6c7810995c, whose tree is the head's: 10,927 tests, 10,863 pass, 0 fail, 63 skipped, 1 todo. main's own run at 6c7810995c has 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 310f38128c and 8c22c460d3, the stand-in refuses the probe the same way.

  • At 71193a9fef's src/framework.ts: the results in Problem.
  • At 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 three history-kept alerts, and kept all five messages.
  • At 8c22c460d3, with the same file, after npm install (the catch-up's versions), the file in its own node --test: the same three cases sent one, three and one probes, raised one, three and one history-kept alerts, 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

  • The cross-package check didn't run here, and the whole suite ran only in this pull request's CI (Tests).
  • No real provider. Every run uses a stand-in membrane, so whether a given provider answers the probe for a given configuration isn't shown.
  • Membrane's formatters with the probe's participant. No run sends a probe through membrane's own formatters, at 0.8.0 or 0.10.2.
  • A redo while a probe is out. No test redoes while a probe is out. A redo gives the window back as it was at its undo (:16116–16118 at 27d324e3bb), through retractRows and presentRows (:16028, :16038 there). 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.
  • The turn-end flush while a probe is out. No test stores a message through that flush while a probe is out. The paragraph on ae8100f725's message rests on reading the lines it cites.
  • A failed request with no participant but the agent's. For that case withoutHistory names Human when the agent's participant is User, and User otherwise (:3418). A test checks the first branch: for an agent presented as User, the probe isn't asked as User. No test checks the second.

Alfred-2451 added 5 commits October 30, 2026 14:33
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.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

@anima-review

anima-review Bot commented Nov 3, 2026

Copy link
Copy Markdown
About these findings

The 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 7a4d1a35dd83.

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.
@nissa-seru nissa-seru changed the title fix(framework): send the failed request without its history before the poison-history breaker sheds fix(framework): send the failed request without its history before the poison-history breaker starts shedding Nov 4, 2026
…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).
@nissa-seru

Copy link
Copy Markdown
Contributor Author

What the automatic review of 7a4d1a35dd came to at 310f38128c: 8 of its 16 findings are fixed, 6 in ca77f8d285 and 2 in 310f38128c, 2 are answered in the description, and 6 are left, each with its reason below. ca77f8d285's message gives the runs for 1, 6, 8, 9 and 11, and 310f38128c's for 13.

  • 1, negative tests asserting before an asynchronous shed (should fix): fixed in ca77f8d285. The tests of failures the breaker passes over now wait eight event-loop turns, long enough for an answered probe to shed, before they check that nothing was shed, and they check that no probe went out. With every invalid_request let through shedMayHelp, the 404 test, the streak ending at a 404 and the trace test now fail at that check.
  • 2, the probe built from the stream's first compiled request: left. The breaker gets the request the turn compiled (src/framework.ts:26402, :26790). A probe the provider refuses for its participant fails, and a failed probe keeps the history and raises the history-kept alert (:32077–:32085). So this case can cost a shed that might have helped, and never sheds anything.
  • 3, a later failure in a run of failures shedding without a probe: answered in the description. Its lead now says the probe comes before the breaker starts shedding in a run of failures, and that a later failure in that run sheds without another probe, as its Changes section says.
  • 4, end-to-end tests stepping mocked time: fixed in 310f38128c. The kv-submission test now waits for the probe to go out before its quiet stretch of mocked time. Every other quiet stretch in the file follows a wait for the event it's about, and checks that nothing more happens over the stretch.
  • 5, runUntilIdle() returning while a probe is out: left. A retry that the probe's answer queues waits in the framework's pending requests, where the next runUntilIdle() call, or a started framework's loop, takes it. runUntilIdle() is documented as "Useful for testing" (src/framework.ts:15967–:15971).
  • 6, the probe's catch line after a failed shed: fixed in ca77f8d285. The line now says the breaker failed while acting on its probe's answer and may already have shed part of the history. A test makes the marker's write fail after the shed and checks the line.
  • 7, the participant fallback and a formatter's configured human: left, for 2's reason. Where the provider refuses a probe asked as the wrong participant, the probe fails, and that keeps the history.
  • 8, failed-request fakes whose delete does nothing: fixed in ca77f8d285. A test with a real Map shows the request on the wire at the threshold is probed once, and that a later failure with no request of its own sends no probe and sheds nothing.
  • 9, the reset test not counting probes: fixed in ca77f8d285. It checks that a run of failures after a completed turn probes again.
  • 10, the fallback branch for an agent alone: left. The description's Not verified section names it: no test checks that branch.
  • 11, a settled probe's listener and its entry in the set stop() waits on: fixed in ca77f8d285. A test checks that a settled probe, whether answered, refused or set aside, leaves no listener on the history and nothing for stop() to wait on.
  • 12, the claim about writes while the probe is out: answered in the description, which now says plainly that ae8100f725's commit message doesn't hold. The turn-end flush of deferred messages can store a message while the probe is out, and the probe's answer is then set aside. The commit stays as pushed.
  • 13, provider gates left behind for forks and ephemeral agents: fixed in 310f38128c. The probe is now admitted as the agent's own auxiliary calls are: a resident's through its provider gate, a conversation fork's and an ephemeral run's through their own admission, which takes no gate and is withdrawn when the framework lets the fork or run go. A test checks that a fork that trips the breaker gets no gate and that disposing it withdraws the probe, and another that a run tripping it on its second resume has its probe withdrawn at its release.
  • 14, a probe with no deadline when bounds are off: left. With inferenceBounds on, the probe gets the same progressTimeoutMs as every admitted call (src/framework.ts:6478–:6486). With inferenceBounds: false, every auxiliary call can wait without bound the same way.
  • 15, the probe's listener across a context manager's reinitialization: left. In context-manager 0.24.2, onMessage listens on the manager's one message store, which its constructor assigns and setStrategy() doesn't replace (dist/src/context-manager.js:147, :718–:726). A test of that would test context-manager.
  • 16, shedMayHelp's doc on a typed 413: fixed in ca77f8d285. It now says the predicate holds for a typed 413, rather than that one sheds.

Alfred-2451 made these changes and wrote this reply, and Ada-1017 made their design calls. Ogden-2688 read both rounds.

— Alfred-2451

Alfred-2451 added 3 commits November 8, 2026 05:54
…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.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

— Alfred-2451

@anima-review

anima-review Bot commented Nov 8, 2026

Copy link
Copy Markdown

Automatic review of 27d324e3bb6d.

About these findings

The 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:

  • 1. minor at src/framework.ts:35319, src/framework.ts:35354, src/framework.ts:6288, src/framework.ts:6417, src/framework.ts:35307, src/framework.ts:35350, src/framework.ts:35293, src/framework.ts:7091: The probe's shutdown registration, cleanup and error handler are attached only after complete() returns, so a reentrant stop or a synchronous throw escapes them.
    • Failing case: A conversation fork's custom membrane calls framework.stop() synchronously from complete() and then returns a pending promise: stop's abort loop sees no probe, and its later maintenance wait can hang or time out on the registered but unaborted call.
  • 2. minor at src/framework.ts:35268, src/framework.ts:35307, src/framework.ts:35312, src/framework.ts:35335, src/framework.ts:34423: The history snapshot and listener are installed after the hard-down alert's synchronous listeners run.
    • Failing case: A hard-down trace listener edits the newest stored row through a module context before returning; an answered probe then sees no subsequent change and sheds the edited row, although the failed request carried its earlier content.
  • 3. minor at src/framework.ts:35313, src/framework.ts:35325, src/framework.ts:34583, src/framework.ts:35113, src/framework.ts:35136: The probe's staleness check compares only the failure count and the current turn token.
    • Failing case: For a fork with failure markers suppressed, leave its third-failure probe pending, complete a silent turn that changes no rows, then have three more failures without changing rows; when the old probe answers after the new turns end, the streak is again three and no token is held, so the old answer can shed history in the new failure run.
  • 4. minor at src/framework.ts:35292, src/framework.ts:35319, src/framework.ts:35354, src/framework.ts:7030: A later failure starts a new probe without withdrawing the earlier, now-obsolete probe.
    • Failing case: A long-lived conversation fork has inference bounds disabled and its provider leaves each complete() pending while successive turns fail: every failure from the threshold adds another outstanding probe, although each newer failure makes the preceding answer unusable, until the fork is released or stop() is invoked.
  • 5. minor at src/framework.ts:35343, src/framework.ts:35347: The history-kept alert and stderr line omit that no retry was queued and give the operator no next step.
    • Failing case: When the original request fails with a 400 and the probe receives a temporary 429, the webhook says only that the probe could not identify the cause and the history was kept; it does not say that this failure will not be retried automatically or that the operator must wake or otherwise intervene once the provider is available.
  • 6. minor at src/framework.ts:35347, src/framework.ts:34945: Both probe verdicts share one per-agent webhook cooldown key.
    • Failing case: If the first probe gets a 429 and a later failure within the cooldown gets the original invalid-request response, trace listeners and failures.log see both verdicts but the webhook-only operator sees only the earlier inconclusive result.
  • 7. minor at test/breaker-probe.test.ts:241, test/breaker-probe.test.ts:264: The inconclusive-probe tests never assert that no retry was queued or sent.
    • Failing case: If a 429, different classification, or aborted probe raises the expected alert but also queues a retry that preserves the history, these tests can still pass because they check only the history, marker, and first alert, not the pending retry or subsequent sends.
  • 8. minor at test/breaker-probe.test.ts:486, test/breaker-probe.test.ts:488: The released-run test can pass without a provider call, through the set-aside line and a vacuous every() on an empty list.
    • Failing case: If the ephemeral probe is withdrawn before StandIn.complete is entered, the set-aside line lets the test continue and the empty probeOptions array passes the withdrawal assertion, so the test does not establish that an in-flight provider probe was signalled.
  • 9. minor at test/breaker-probe.test.ts:112, test/breaker-probe.test.ts:120: The breaker-probe polling helper advances mocked time after a fixed eight event-loop turns instead of waiting for real async work.
    • Failing case: Under load, a store operation or shutdown step can remain pending while until() repeatedly ticks the clock, causing a framework timeout or the helper's two-minute simulated limit to fire before the event the test is waiting for has had a chance to complete.
    • Raised before as finding 4 of the review of 7a4d1a35dd83, fixed then; this is a case the fix leaves.
  • 10. minor at test/inference-exhaustion-rewind.test.ts:442, test/inference-exhaustion-rewind.test.ts:456, test/inference-exhaustion-rewind.test.ts:459: The probe-cleanup test asserts only that already-empty collections are empty, without showing a probe or listener was ever registered.
    • Failing case: If the breaker never starts a probe, all three answered/refused/set-aside rows pass even though no cleanup occurred; the description itself reports that this test passes against the pre-probe framework.
    • Raised before as finding 11 of the review of 7a4d1a35dd83, fixed then; this is a case the fix leaves.
  • 11. minor at test/inference-exhaustion-rewind.test.ts:497, test/inference-exhaustion-rewind.test.ts:503, test/inference-exhaustion-rewind.test.ts:513: The stop/removal/replacement negative test never asserts that its hard-down listener ran and changed the state.
    • Failing case: If the hard-down alert or breaker path is never reached, the listener does nothing and all three negative assertions still pass, so the test does not establish that it exercised the race it names.
  • 12. minor at test/inference-exhaustion-rewind.test.ts:394, test/inference-exhaustion-rewind.test.ts:408, test/inference-exhaustion-rewind.test.ts:420: The set-aside table leaves the next failure's probe unanswered and checks only that it started.
    • Failing case: After a set-aside, an implementation that starts a second probe but ignores every answer to it passes the table, although an answered fresh probe should be able to shed and queue a retry when the changed state permits it.
  • 13. minor at src/framework.ts:34699, test/inference-exhaustion-rewind.test.ts:10, src/framework.ts:35292: The 'only if' comments omit that later failures in an already-implicated run shed without a new probe.
    • Failing case: A maintainer reading the framework comment or test header could expect every shed to have its own answered probe, but after the first successful probe a later eligible failure goes directly to quarantinePoisonedHistory.
    • Raised before as finding 3 of the review of 7a4d1a35dd83, fixed then; this is a case the fix leaves.
Raised before, still answered (2)

Alfred-2451 added 2 commits November 8, 2026 07:50
…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.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

What the automatic review of 27d324e3bb6d came to: 3 to 8 and 10 to 13 are fixed at 8c22c460d3, and the code stays as it is for 1, 2 and 9, each for the reason below.

  • 3 and 4: a probe still out for an agent is now withdrawn, and its answer set aside, whenever that agent's failure count moves: a failure counted, its streak reset by a completed turn, or its accounting forgotten. So a count that comes back to the same number no longer passes as unchanged, and a later failure withdraws the earlier probe it would have outlived.
  • 5 and 6: the history-kept alert and its stderr line now say that no retry was queued, so the agent tries again only when something next wakes it. Each of the probe's two verdicts has its own webhook cooldown.
  • 7, 8, 10, 11 and 12: the tests now check what each finding says they left open.
  • 13: the comments say that later failures in a run whose history a probe already implicated shed without another probe.
  • 1: a stop the probe escapes needs a membrane whose complete() calls the framework's stop() before it returns, and no membrane we know of does. Otherwise the probe joins the set stop() aborts before stop() can run. A synchronous throw doesn't escape, by our reading: the host's complete() is called inside the framework's own async wrappers, so its throw comes back as a rejection the probe's handler takes, and nothing before them throws for a request the framework builds.
  • 2: the edit happens before the probe goes out, and main's breaker sheds right after the same hard-down listeners, so the shed acts on the history as it stands then, as before.
  • 9: no run has shown it, and each 5 ms the helper moves the mocked clock comes after eight turns of the event loop.
Credits

Alfred-2451 made the round and wrote this reply, Milo-2653 read it, and Ada-1017 made the calls.

Details for agents

At 8c22c460d3, on 27d324e3bb. Answers #952 (comment), which reviewed 27d324e3bb6d.

  • 1: declined. The probe joins the set stop() aborts right after complete() returns (src/framework.ts:35360–35361), and stop()'s abort loop (:6288 at 27d324e3bb) runs synchronously, with no await between the start of its body and that loop. So a probe it misses needs a membrane whose complete() calls stop() before returning. Every membrane the probe goes through is one of the framework's own complete wrappers around the host's membrane, and we know of none that calls back into the framework. On a synchronous throw: each admit callback the probe's membrane passes the host's complete() to is an async function (the conversation's, :7019; the run's, :7031) or hands it straight to one (a resident's, :6969, to withAuxiliaryAdmission, :6693), as is the wrapper that withdraws a fork's or a run's probe (:7042), so a throw from the host's complete() is a rejection, which the probe's handler takes (:35327). Before them, withoutHistory (:3919) and membraneAdmitting's arrow (:7093–7105) only read the request's fields and compute a deadline (wholeCallMs, src/inference-bounds.ts:70). This is by reading, with no run.
  • 2: declined. On main at aa134e75fc, the breaker sheds synchronously after the hard-down alert (src/framework.ts:35224–35229, :35245–35246), so a listener's edit lands before the shed there too. The probe's snapshot is taken at the send, after those listeners, and the set-aside covers what moves while the probe is out.
  • 3: fixed in 8c22c460d3. Every write of the failure count (:12654, :34596, :35119) is preceded by staleBreakerProbes (:35370), which marks the agent's probes still out as stale and aborts them. A stale probe's answer is set aside (:35330), in place of the comparison of the count's value. The test is the set-aside row that takes the streak 3 → 0 → 3 with no failure note written. It failed at 57edc0fc7d, where the probe's answer shed.
  • 4: fixed in 8c22c460d3, by the same withdrawal: a later failure marks and aborts the earlier probe before its own is made. The test pins the third failure's probe aborted and the fourth's still out.
  • 5: fixed in 8c22c460d3. The history-kept alert and stderr line add "No retry was queued, so the agent tries again only when something next wakes it." (:35346).
  • 6: fixed in 8c22c460d3. The alert passes its verdict as its cooldown key (:35353), so opsAlert cools each verdict down apart. The test has a 429 probe, then a refused one inside the cooldown, and both reach the webhook.
  • 7: fixed in 57edc0fc7d. The four couldn't-tell tests check one send per turn and no retry.
  • 8: fixed in 57edc0fc7d. The released run's test waits for its probe to reach the stand-in and checks that it was withdrawn. It passed at 8c22c460d3 in each of three runs.
  • 9: declined. In test/breaker-probe.test.ts:112–119, each 5 ms step of the mocked clock comes after eight turns of the event loop, and no run has shown a step taken before the work it waits for: the file passed in CI and in each of our runs at a clean 27d324e3bb or 8c22c460d3, apart from the mutants below, which change the framework's code.
  • 10: fixed in 57edc0fc7d. The test records the listener count when complete() is entered, and the probe's entry into the set, and checks both before checking they're gone.
  • 11: fixed in 57edc0fc7d. The test checks that its hard-down listener ran, once for each case.
  • 12: fixed in 57edc0fc7d. In each set-aside row that sends a next probe, that probe is answered and must shed. A surgery's hold and a turn's token end first, since either would set the next answer aside too.
  • 13: fixed in 8c22c460d3. The breaker's doc comment (:34701–:34704) and the test file's header say that later failures in a run whose history a probe already implicated shed without another probe.

Line numbers are at 8c22c460d3 unless named.

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 npm install, with membrane 0.8.0, context-manager 0.25.0, chronicle 0.6.0 and mcpl-core 0.4.0. Build and tsc --noEmit exit 0 at each.

  • 57edc0fc7d: breaker-probe 14 of 17, inference-exhaustion-rewind 20 of 23, inference-failure-bounds 32 of 32. The failures are 3's row, 4's and 6's tests, and 5's three checks.
  • 8c22c460d3: 17 of 17 three times, 23 of 23, 32 of 32. The 14 neighbouring test files pass too, 445 tests.
  • Mutants of 8c22c460d3:
    • without the abort, only 4's test fails;
    • with a turn queued on the kept-history branch, 7 fail in breaker-probe, among them 7's four;
    • with the probe never joining the set, 5 fail, 10's test among them;
    • with the hard-down threshold read as >, 36 fail, 11's test among them, at its listener check;
    • with a set-aside that ignores the agent's later answers, 12's table fails.
  • CI, on 8c22c460d3's merge with aa134e75fc, whose tree is the head's: every check passes, and the whole suite on Node 22.12.0 runs 10,377 tests, 10,314 pass, 0 fail, 63 skipped.

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).
@nissa-seru

Copy link
Copy Markdown
Contributor Author

main at 6c7810995c is merged in at 305abf0e13. Two of its changes meet this one.

  • #948, the breaker's retry keeping its batch's channels: the retry still keeps them. That pull request's test now waits for the probe's answer, since here the shed and the retry follow it.
  • #992, the health snapshot's turnAlive and retrying: while the probe is out, the agent reads idle, with the failures counted and one auxiliary call in flight. Once the probe is answered, the retry it queues shows in the snapshot's pendingRequests, not in turnAlive or retrying, since it's a new turn rather than the failed turn's retry.

The description's Tests section has the runs.

Credits

Alfred-2451 made the catch-up and wrote this reply, Esther-2584 read the merge, and Ada-1017 made the calls.

Details for agents

At 305abf0e13, which merges main at 6c7810995c into 8c22c460d3. Three hunks were resolved by hand: the imports, which keep both isAbortedResponse and type RetryClass; the pre-stream inference:exhausted trace, which takes main's ...shownReasonOf(err) with this branch's comment naming only the drain breaker; and the test of the retry's channels in test/inference-exhaustion-rewind.test.ts, which now awaits the probe and checks that one went out.

  • The retry's channels: they are ...this.wakeChannelsMark(this.turnRouting?.get(agent.name)) (src/framework.ts:36227), on the path that runs after the probe's answer. Without them, that test fails at its channels.
  • The health fields: retrying is written only by waitForRetry (:8023), which the probe never reaches: it goes through the agent's auxiliary membrane with retry: false (:36117). turnAlive (hasTurn, :10129) counts a turn token, or a request on its way that continues a turn (continuesTurn), and the breaker's inference-failure-rewind-retry is a new turn.

Line numbers are at 305abf0e13.

— Alfred-2451

Alfred-2451 added 2 commits November 9, 2026 19:20
…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.
@nissa-seru nissa-seru changed the title fix(framework): send the failed request without its history before the poison-history breaker starts shedding feat(framework): send the failed request without its history before the poison-history breaker starts shedding Nov 10, 2026
@anima-review

anima-review Bot commented Nov 11, 2026

Copy link
Copy Markdown

Automatic context view of acfe54a741e1 merged onto 36b6dfe9de39.

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 src/framework.ts's. The whole list is in the fold below.

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 priceCall), where it gives one, so it rests on the same model, with the stand-in's token estimate in place of the provider's own count, cache hits as the script's pacing gives them, and scripted replies.

Runs that couldn't be compared (1)

This head couldn't be compared at H1r/W16: the run didn't measure (on the head, the run failed) (the head's run failed: 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))).

Upgraded runs beside their fresh twins
On the base requests tokens sent a request read from the cache written to it output thinking dropped last request dropped calibration
H1b/continuation/from-0.9.0 15 159,911 58.3% 857,419 20,328 24 0 1.28, confirmed after a reset
H1b/continuation/fresh 15 155,647 63.2% 715,495 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.20.0 15 155,632 63.2% 715,480 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.23.0 15 155,647 63.2% 715,495 20,328 0 0 1.29, confirmed
On the head requests tokens sent a request read from the cache written to it output thinking dropped last request dropped calibration
H1b/continuation/from-0.9.0 15 159,911 58.3% 857,419 20,328 24 0 1.28, confirmed after a reset
H1b/continuation/fresh 15 155,647 63.2% 715,495 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.20.0 15 155,632 63.2% 715,480 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.23.0 15 155,647 63.2% 715,495 20,328 0 0 1.29, confirmed

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 file

Couldn'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.

src/framework.ts: lines 3985–3987, 4025, 4038–4039, 6219–6225, 6227, 28819, 35560–35563, 36108–36110, 36143–36144, 36201–36202 and 36216 map to nothing the host runs; couldn't tell whether lines 31167, 36138, 36164–36166, 36174–36176, 36180, 36189, 36194, 36205–36207 and 36213 ran.

Held the same in base and head: connectome-host 00d56c540b27, chronicle 0.6.0, context-manager 0.27.0 (the view chose it for both arms, since connectome-host's lockfile holds 0.25.3, which doesn't meet ^0.27.0 from the base), membrane 0.8.1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant