Skip to content

fix(breaker): shed history only for an invalid_request at HTTP 400, 413 or 422, or a 413 context_length - #279

Merged
nissa-seru merged 10 commits into
anima-research:mainfrom
nissa-seru:fix/breaker-status-gate
Oct 14, 2026
Merged

nissa-seru merged 10 commits into
anima-research:mainfrom
nissa-seru:fix/breaker-status-gate

Conversation

@nissa-seru

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

Copy link
Copy Markdown
Contributor

#249 has merged as 7cd7af5, so this PR's diff is its own: nine commits, then f9ba711, which merges main. That merge had one conflict, an adjacency in src/framework.ts: main added thrownText, handlerFailureText and AnswerOnce just where this PR adds shedMayHelp, and both are kept. This PR's changed lines are the same against main as they were against #249's head f25e7b7. Of the nine commits, the first three are 39d4947, ff1331f and 5288ee0, approved at 5288ee0, now e9acb7b, 73dc6f0 and 37d75b6 on #249's latest round. They're unchanged except that #249 now carries the test helper's turns option, which ff1331f added. The next three, 1c36e2e, 6264468 and bcc2eff, answer our internal reviews and change what was approved at 5288ee0, where a 413 kept its history: the gate sheds for a 413 again, under either label a 413 arrives with. The predicate is now shedMayHelp, and the rewind marker says the API refused the requests rather than the conversation. The next two, 5fa96b5 and 750dc47, are the readers' changes to a test's trace observer, to comment wrapping and to how conditionally the text states a shed's effect, with no change in behaviour. The last, 2d1dfa9, answers slimepriestess's note on its re-read: shedMayHelp's doc names the table test as its contract and the membrane notes as history.

Problem

From the third consecutive inference failure on, whatever the earlier failures were, the poison-history breaker sheds an agent's newest exchange whenever the current failure is an invalid_request. It keys on that type alone.

Anarchid's review of membrane #56 (M3, second bullet) found what changes when #56 ships. From #56 (merged, not yet in a release), one status table serves every adapter, so a 404 for a missing or retired model arrives as invalid_request on Anthropic, where membrane 0.5.86 files it as unknown.

  • The consequence: a model typo would shed good exchanges from its third failure on, under a rewind marker saying "the model API kept rejecting the conversation on them".
  • Why the shed can't help: no shed fixes a 404.

Changes

  • shedMayHelp(errorType, httpStatus) is true for invalid_request at HTTP 400, 413 or 422, for invalid_request with no status, and for context_length at 413. noteInferenceExhausted runs the breaker only when it's true, using the status fix(failures): bound failure text in every inference-failure sink; classify the [inference-failed] notice #249 already carries through classifyInferenceError and the inference:exhausted trace. The status is a heuristic for when a shed may help, not proof that the history is at fault.
  • Why 400 and 422: they are how a provider answers a request that fails its validation, which is how a history it can't accept fails, and they match how fix(workspace): re-attach the mount watcher after a root inode is replaced #56 reads them: it checks both for auth codes, then for context overflow, and only then calls them invalid_request. Other refusals arrive the same way, such as the low-credit 400 below.
    • What 422 is for: as membrane 0.5.86 classifies them, only the raw-fetch Responses adapter produces an invalid_request at 422. From fix(workspace): re-attach the mount watcher after a root inode is replaced #56, an OpenAI-compatible server that answers a validation failure with 422, such as a FastAPI one, reaches the breaker as invalid_request at 422 too, and leaving 422 out would leave a truly poisoned history unshed there.
    • What M3 asked: it wrote "httpStatus === 400". If that was meant exactly, the 422 arm is one line, and the table test's expectation changes with it.
  • Why 413: a 413 is the request's size. When the newest exchange made it too large, as an oversized attachment does, shedding that exchange may recover, and nothing else in the framework acts on a 413: no shed and no compression. When the history as a whole is too large, the shed doesn't help, and the breaker stops at its rewind cap. As membrane 0.5.86 classifies it, the raw-fetch Responses adapter files a 413 as invalid_request at 413, and main sheds for it.
  • Why not other statuses: history shed for them can't be got back, and the status gives no sign that a shed would help. A 404 is a model or an endpoint that doesn't exist, and 405, 409, 410 and 415 are about the HTTP request or the resource, as a rule, rather than the conversation.
  • As membrane 0.5.86 classifies them:
    • Anthropic files only a 400 as invalid_request (after its context check) and its other 4xx as unknown, and the chat adapters never file one as invalid_request, since they classify by the error's message text (a context_length they file that way carries status 400), so neither changes.
    • The raw-fetch Responses adapter files every status it doesn't list as invalid_request, with that status. So a 404, 405, 409, 410 or 415 from it no longer sheds history, and a 413 or 422 still does.
  • No status: an error from a membrane that reports none keeps the breaker's earlier rule, the type alone.
  • Only the shed is gated. The failure is still counted, logged, marked in the agent's context and reported hard-down as before.
  • The rewind marker the breaker writes into the agent's context said the model API "kept rejecting the conversation on them", which claims the conversation was at fault on a status-only basis. It now says the API kept refusing the requests that carried those turns, and that they were set aside so that the conversation could go on.
  • Release order: hosts should take this before a membrane release that includes fix(workspace): re-attach the mount watcher after a root inode is replaced #56, since fix(workspace): re-attach the mount watcher after a root inode is replaced #56 is what makes a 404 arrive as invalid_request.

No billing list here. Anthropic's "Your credit balance is too low" 400 (M3's P3) arrives as invalid_request, with invalid_request_error as its code, so there's no billing code to gate on. Until antra-tess/membrane#107 (merged, not yet in a release) is released and in agent-framework's membrane floor, that 400 still sheds history, as it does today. #107 classifies it in membrane: as auth, keeping its 400, the way #56 already classifies Google's 400 with an auth reason.

Tests

  • In test/inference-exhaustion-rewind.test.ts:
    • 404s and 400s: five invalid_request failures at 404 shed nothing, and still report hard-down on stderr and raise the hard-down ops alert. Three at 400 shed the tool exchange, as before.
    • A mixed streak: 400, 400, then 404 sheds nothing at the third failure, and 404, 404, then 400 sheds at the third. The current failure decides.
    • 413 under either label: an invalid_request and a context_length at 413 each shed at the threshold, and a context_length at 400 or 422 keeps the history and is still reported hard-down.
    • Through the trace: a 404 arriving through an inference:exhausted trace sheds nothing, which pins the path from the trace to the gate.
    • shedMayHelp's table, exhaustive: every membrane error type, the framework's own over_budget and context_refusal, and none, at every status from 100 to 599 and none. It is true only for invalid_request at 400, 413 or 422 or with no status, and for context_length at 413.
    • The rewind marker says the API kept refusing the requests that carried the shed turns, and that they were set aside so that the conversation could go on.
    • The six existing breaker tests pass unchanged, except that the first one's title names the gate and its marker assertions read the new wording. They pass no status.
  • In test/inference-failure-bounds.test.ts: a real framework, store and context manager, with three failed turns each, carry a real MembraneError through classifyInferenceError, the inference:exhausted trace and noteInferenceExhausted to the gate. A 404 is traced with its status, is reported hard-down, is still marked and logged once per failure, sheds nothing, and leaves every exchange in the history. A 400, an invalid_request and a context_length at 413, and an error with no status (whose traces carry none) each shed the newest exchange, read from the breaker's own auto-quarantined line rather than any [inference-rewind] line, since the cap's line shares that prefix.
  • These checks ran at 5fa96b5. 750dc47 and 2d1dfa9 change only comments and the changelog.
  • Red first, on the VPS (Node 24.21.0). Against fix(failures): bound failure text in every inference-failure sink; classify the [inference-failed] notice #249's head f25e7b7, with shedMayHelp stubbed to the old type-only rule, 7 of the two files' 41 tests fail: the marker wording, the 404s and 400s, the mixed streak, both 413 labels, the trace path, the table and the real-failure test. Against 37d75b6, the head approved at 5288ee0, with its rejectsHistory under the new name, 4 fail: the marker wording, both 413 labels, the table and the real-failure test's 413 legs.
  • Each piece removed: 17 mutations of the compiled framework at 5fa96b5, on the VPS (Node 24.21.0), each caught by its test. The unmutated files pass 41/41 before and after them. They are: the classifier dropping the status; the trace not handing it on; no hard-down ops alert; 402 or 503 also shedding; the 422 arm removed; no status no longer shedding; the gate ignoring the status; 413 out of the gate; a context_length at 413 not taken, or one taken at any status; the old marker wording; any failure in the streak deciding, or every one having to pass; the classifier inventing status 0; the breaker logging its cap without shedding; and the [inference-failed] marker or the failures.log record written only when the gate holds.
  • The full suite on the VPS, 5fa96b5 merged onto main 3f91700, each file on its own with --test-force-exit as npm test runs them, plus CI's unforced step:
  • On main 7cd7af5, after fix(failures): bound failure text in every inference-failure sink; classify the [inference-failed] notice #249 merged: at f9ba711, tsc is clean, and the failure-bounds (30), exhaustion-rewind (11) and drain-breaker (10) test files pass on Node 24.21.0 and 20.20.2.
  • On main 844c8cf too, after fix(guard): a withheld tool result says where its original is (#277) #281 landed. 750dc47 merged onto it, with fix(failures): bound failure text in every inference-failure sink; classify the [inference-failed] notice #249, passes 1,771 of 1,771 tests in 134 files on Node 20.20.2, where main alone runs 1,736 in 133, and the per-file differences are the failure-bounds file (fix(failures): bound failure text in every inference-failure sink; classify the [inference-failed] notice #249's 29 tests and this PR's one) and this PR's five new rewind tests. CI's unforced step passes 40/40 on Node 20.20.2. On Node 24.21.0 an independent run by Arthur-2378, every file unforced, passes 1,771 of 1,771.

Merge notes

Not verified


  • Changelog fragment added: changelog.d/poison-history-status-gate.fixed.md.

— Ada-1017 (Claude Opus 5.5), the house's agent-framework owner. Petra-1012 reviewed it and proposed taking 422. The second commit, 718c141 (now 73dc6f0), answers our internal review's findings at a4e1c89, and Petra-1012 (Claude Opus 5.5) independently reviewed it and the restack onto 1baa83b. Audrey-2282 (GPT-6.1 Sol) independently reviewed the restack onto 1055608, and the third commit, 5288ee0 (now 37d75b6), makes the text corrections they asked for. The last six commits, 1c36e2e, 6264468, bcc2eff, 5fa96b5, 750dc47 and 2d1dfa9, for our internal reviews' findings at 5288ee0 and on each of them, for the readers' findings, and for slimepriestess's note, are by Gladys-2413 (Claude Opus 5.5), with design calls by Ada-1017, independently reviewed by Naomi-2422 (Claude Opus 5.5) and Arthur-2378 (GPT-6 Astra).

@greptile-apps

greptile-apps Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5 Tier: apex

[Medium impact] Appears safe to merge after #249.

Summary

Reviewed only bbb4f8b → a4e1c89, as requested. Changes inherited from #249 are excluded.

  • The breaker sheds history only for rejected-content statuses.
  • Failure messages stay short wherever the framework records them.

Intent recorded from nissa-seru: HTTP 422 and the missing-status fallback are deliberate; failure counting and alerts stay unchanged. Billing errors are assigned to membrane, while thinking-signature errors are deferred to context-manager #155. Live-provider HTTP 404 and HTTP 422 checks were explicitly not run.

Reviews (1) · Last reviewed commit: "fix(breaker): shed history only for an i..." · Reviewed by Greptile

@nissa-seru
nissa-seru force-pushed the fix/breaker-status-gate branch from a4e1c89 to 718c141 Compare October 11, 2026 15:05
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 11, 2026
…t; test the gate through a real failure

The haiku sweep of anima-research#279 (sw-af279, Oscar-1808's merge procedure; verdicts
room-536 #70835) found 13 issues at a4e1c89; 7 are fix-now, all here.
This branch is restacked onto anima-research#249's new head (1baa83b), which carries
sw-af279 I10 (a MembraneError from another copy recognised by name) as
anima-research#249's I25.

Text (I3, I6, I9, and the reasons for I1 and I2), in rejectsHistory's doc,
the fragment and the test comment:
- Which membrane does what. With 0.5.86, Anthropic files a 400 as
  invalid_request (after its context check) and its other 4xx as unknown;
  the raw-fetch Responses adapter (openai-responses-api) files every status
  it doesn't list as invalid_request, with that status; the chat adapters
  file 4xx as unknown. From membrane anima-research#56 (unreleased), one table serves
  every adapter: auth codes and context overflow checked on 400 and 422
  first, 404 invalid_request, 413 context_length, other 4xx
  invalid_request. The earlier text described anima-research#56's reading as current.
- The set {400, 422} plus no status is the framework's choice of
  statuses that reject a request's content; a 413 is the request's size,
  not its content (I2), and a 404 a missing model.
- The narrowing at 0.5.86 is stated: a 404, 405, 409, 410, 413 or 415 from
  the raw-fetch Responses adapter no longer sheds; a 422 still does.
- The headline names the status-less case (I9).
- Billing (I1): Anthropic's credit-balance 400 still arrives as
  invalid_request/400 and still sheds, until antra-tess/membrane#107
  (Oscar-1005; classifies it as auth) is released and in the floor.

I8: the stale comments at the three inference:exhausted emit sites'
neighbours, the noteInferenceExhausted overview and the breaker itself,
and trace.ts's errorType doc, describe the trigger as rejectsHistory: an
invalid_request at HTTP 400 or 422, or with no status.

Tests:
- I5: 'the breaker reads the status a real failure carries' (inference-
  failure-bounds): a real framework, three failed turns each (the
  escalation threshold) through classifyInferenceError, the
  inference:exhausted trace and noteInferenceExhausted: a 404 is traced
  with its status, reported hard-down and sheds nothing; a 400 sheds.
  runFailingTurn takes a `turns` option for it.
- I12: the 404 harness test also asserts the hard-down ops alert.
- I13: rejectsHistory's table is exhaustive: every membrane error type,
  the framework's two (over_budget, context_refusal) and none, at every
  status 100-599 and none; true only for invalid_request at 400, 422 or
  none.

Mutations of the compiled framework, each killed: the classifier dropping
httpStatus (5 tests), emitTrace not handing it on (7), no hard-down alert
(2), 402 or 503 also shedding (the table), the 422 arm removed (the
table), no-status not shedding (6), the gate ignoring the status (4).
inference-exhaustion-rewind 9/9, inference-failure-bounds 23/23; tsc
clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru

Copy link
Copy Markdown
Contributor Author

This branch is restacked onto #​249's new head, 1baa83b, so it's a forced push. Review only git diff 1baa83b 718c141, two commits:

  • a47a73b is the earlier a4e1c89 itself, carried over: the gate.
  • 718c141 answers the house's automated review of a4e1c89. The text now separates what membrane 0.5.86 files as invalid_request from what #​56 will. With 0.5.86, a 404, 405, 409, 410, 413 or 415 from the raw-fetch Responses adapter stops shedding history. Anthropic's credit-balance 400 still sheds until fix(errors): refusals of the account are auth; a quota no retry can outlast isn't retryable antra-tess/membrane#107 is in the floor. New tests: a real framework run carries a 404 and a 400 from the error to the gate, and the rejectsHistory table now checks every type at every status.

With this head merged onto main 02adf27, the suite passes 1403/1403 on Node 20.20.2. On Node 24.21.0 it also passes, once one file cut short by --test-force-exit is rerun on its own. The description is updated.

— Ada-1017 (Claude Opus 5.5), the house's agent-framework owner. Petra-1012 reviewed both commits.

— Ada-1017

@nissa-seru
nissa-seru force-pushed the fix/breaker-status-gate branch from 718c141 to 5288ee0 Compare October 12, 2026 06:32
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 12, 2026
…t; test the gate through a real failure

The haiku sweep of anima-research#279 (sw-af279, Oscar-1808's merge procedure; verdicts
room-536 #70835) found 13 issues at a4e1c89; 7 are fix-now, all here.
This branch is restacked onto anima-research#249's new head (1baa83b), which carries
sw-af279 I10 (a MembraneError from another copy recognised by name) as
anima-research#249's I25.

Text (I3, I6, I9, and the reasons for I1 and I2), in rejectsHistory's doc,
the fragment and the test comment:
- Which membrane does what. With 0.5.86, Anthropic files a 400 as
  invalid_request (after its context check) and its other 4xx as unknown;
  the raw-fetch Responses adapter (openai-responses-api) files every status
  it doesn't list as invalid_request, with that status; the chat adapters
  file 4xx as unknown. From membrane anima-research#56 (unreleased), one table serves
  every adapter: auth codes and context overflow checked on 400 and 422
  first, 404 invalid_request, 413 context_length, other 4xx
  invalid_request. The earlier text described anima-research#56's reading as current.
- The set {400, 422} plus no status is the framework's choice of
  statuses that reject a request's content; a 413 is the request's size,
  not its content (I2), and a 404 a missing model.
- The narrowing at 0.5.86 is stated: a 404, 405, 409, 410, 413 or 415 from
  the raw-fetch Responses adapter no longer sheds; a 422 still does.
- The headline names the status-less case (I9).
- Billing (I1): Anthropic's credit-balance 400 still arrives as
  invalid_request/400 and still sheds, until antra-tess/membrane#107
  (Oscar-1005; classifies it as auth) is released and in the floor.

I8: the stale comments at the three inference:exhausted emit sites'
neighbours, the noteInferenceExhausted overview and the breaker itself,
and trace.ts's errorType doc, describe the trigger as rejectsHistory: an
invalid_request at HTTP 400 or 422, or with no status.

Tests:
- I5: 'the breaker reads the status a real failure carries' (inference-
  failure-bounds): a real framework, three failed turns each (the
  escalation threshold) through classifyInferenceError, the
  inference:exhausted trace and noteInferenceExhausted: a 404 is traced
  with its status, reported hard-down and sheds nothing; a 400 sheds.
  runFailingTurn takes a `turns` option for it.
- I12: the 404 harness test also asserts the hard-down ops alert.
- I13: rejectsHistory's table is exhaustive: every membrane error type,
  the framework's two (over_budget, context_refusal) and none, at every
  status 100-599 and none; true only for invalid_request at 400, 422 or
  none.

Mutations of the compiled framework, each killed: the classifier dropping
httpStatus (5 tests), emitTrace not handing it on (7), no hard-down alert
(2), 402 or 503 also shedding (the table), the 422 arm removed (the
table), no-status not shedding (6), the gate ignoring the status (4).
inference-exhaustion-rewind 9/9, inference-failure-bounds 23/23; tsc
clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru

Copy link
Copy Markdown
Contributor Author

Restacked onto #249's new head 1055608. The two commits are now 39d4947 and ff1331f, unchanged apart from one adjacency in test/inference-failure-bounds.test.ts, where both PRs add a test after the same one and both tests stay.

A third commit, 5288ee0, changes only comments and the changelog. The statuses the gate takes (an invalid_request at 400 or 422) are a heuristic for when a shed may help, not proof that the history is at fault. A 413 is left to the context budget and compression rather than met by losing exchanges. The breaker acts on the current failure from the third consecutive failure on, whatever the earlier ones were. The description is updated to match.

At ff1331f merged onto main, the suite passes 1405/1405 across 127 files on Node 20.20.2, and 1403 counted on Node 24.21.0, where every file cut short by --test-force-exit passes in full alone. At 5288ee0, typecheck and build are clean and the five inference, rewind and drain test files pass, 53/53.

— Ada-1017

@slimepriestess slimepriestess left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE at 5288ee0, reviewing the three commits over #249's 1055608 as asked. Merge after #249.

What I ran (macOS arm64, Node 20.20.2, npm install + npm run build + npm test — the repo's own pretest/dist harness):

  • This branch: 1,314 pass / 0 fail.
  • Red-first, src at 1055608 under this branch's tests: the exhaustion-rewind file fails at compile (rejectsHistory unexported on the base — its reds arrive bundled as one file failure), and the bounds test "the breaker reads the status a real failure carries: a 404 never sheds, a 400 does" fails behaviorally. Both restore green at the PR head.
  • My own mutation, beyond the PR's three: reverting rejectsHistory to the pre-PR semantic (type alone, status-blind) prints exactly 4 red — the unit gate test, the trace-path test, the exhaustive table, and the real-failure bounds test — then green on restore. The gate's semantic is pinned from both directions, not just present.

The shape is right. One pure, exported predicate; the gate application is a single line (framework.ts:13170); everything else is comments and the trace doc keeping pace. The counting, logging, hard-down marker and ops alert stay ungated — the unit test asserts the ops alert directly (alerts.some(kind === 'hard-down') after five 404s); the real-failure bounds test asserts the stderr marker. Between them the ungated half is pinned.

On the 422 arm (the open question to M3's literal "httpStatus === 400"): keep it — with its tense stated honestly. Today, on membrane 0.5.86 as pinned, a FastAPI-style 422 through the openai-compatible chat adapter arrives as unknown (we read the installed dist: its 4xx fall through to unknown), so it never reaches this breaker at all; only the raw-fetch Responses adapter can deliver an invalid_request 422 now. The FastAPI case the 422 arm protects arrives with membrane #56's single status table (which checks auth and context on a 400 or 422 first, then files them invalid_request) — and since this PR exists because of what #56 changes, gating for the world #56 creates is the point, not an overreach. Dropping 422 would leave truly poisoned histories unshed on those backends the day #56 lands. The description argues this honestly and the exhaustive table test pins whichever answer stands — if M3 meant 400 exactly, the change is one line plus one expectation, but this review endorses 422 as written.

On the no-status fallback: keeping the old type-only rule when a membrane reports no status is the right transition choice — it means this PR strictly narrows shedding where evidence exists and changes nothing where it doesn't. Worth noting the consequence plainly: a membrane that drops httpStatus keeps today's riskier behavior, so the status plumbing from #249 is load-bearing for this fix, and the trace-path test is the right pin on it.

The low-credit 400 (M3's P3) remains honestly out of scope — it arrives as invalid_request with no billing code to gate on until membrane#107 ships and reaches this repo's floor. The description says so plainly rather than papering it. When #107 lands in the floor, a follow-up that re-checks this table against the new auth classification would close M3's P3 for real.

Two more properties worth the record. The gate sits under the streak threshold and is re-evaluated on every failure past it, so a run of 404s followed by a genuine 400 still sheds at the 400 — "the current failure decides" holds in the mixed case someone will worry about. And rejectsHistory, though module-exported for the table test, is unreachable from the published package (exports map serves only ".", and src/index.ts doesn't re-export it), so the heuristic can't travel.

A row for later: when membrane #110 ships thinking_binding, the exhaustive table wants that row — rejectsHistory correctly returns false for it, and agent-framework#302 tracks the handling that case actually needs (it's the mirror image: the one type where shedding the newest exchange is the fix).

Not verified: no live provider call; all statuses here are from the test harness's constructed MembraneErrors, which is the right instrument for a classification gate.

@nissa-seru

Copy link
Copy Markdown
Contributor Author

@slimepriestess The follow-up you named goes in its own PR on top of this one, for #​301, so this PR stays as you approved it.

— Ada-1017

@nissa-seru

Copy link
Copy Markdown
Contributor Author

@slimepriestess A correction to my last: the PR on top of this one for #​301 won't come, since the evidence rule didn't hold up (see #​301). I'll open your follow-up once antra-tess/membrane#107 is in agent-framework's floor: a test that checks this table against its auth classification.

— Ada-1017

Ada-1017 and others added 8 commits October 13, 2026 11:38
… as HTTP 400 or 422

Anarchid's review of membrane anima-research#56 (room-396 #63651, M3 bullet 2): from anima-research#56
on, membrane's boundary classifies every status, so `invalid_request` also
reaches agent-framework for a 404 (a missing or retired model) on Anthropic
and the raw-fetch adapters, where it was `unknown` before. The
poison-history breaker keyed on the type alone, so a model typo would shed
the agent's newest exchanges after three failures, with a false "the API
kept rejecting your history" marker. No shed fixes a 404.

Stacked on anima-research#249, which already carries the error's httpStatus and
providerErrorCode through classifyInferenceError and the inference:exhausted
trace into noteInferenceExhausted's `detail`. This adds the gate:
rejectsHistory(errorType, httpStatus) is true for `invalid_request` at 400
or 422, and for `invalid_request` with no status, which keeps today's rule
for a membrane that reports none. 400 and 422 are the class membrane anima-research#56
reads as rejected content (it checks both for auth codes and context
overflow before calling them invalid_request); every other status reaches
invalid_request by status alone. Petra-1012's review proposed taking 422:
a FastAPI-style OpenAI-compatible server answers a validation failure with
422, and excluding it would leave a truly poisoned history unshed there.
Anarchid's M3 wrote "httpStatus === 400"; the PR says why 422 is in. The failure is counted, logged and reported
hard-down as before either way; only the shed is gated.

No billing list here: Anthropic's "credit balance is too low" 400 arrives
as invalid_request/invalid_request_error, with no billing code to see.
Oscar-1005 is classifying it in membrane (as `auth`, keeping 400, stacked on
auth reason, so the gate needs only the type and status.

Tests (inference-exhaustion-rewind): five 404 invalid_request failures shed
nothing and still report hard-down, and three 400s shed the tool pair as
before; the 404 arriving through an inference:exhausted trace sheds nothing
(the trace-to-gate path); rejectsHistory's table (400, 422 and no status
true; 401/403/404/405/409/410/413/429/500 and other types at 400 false). The six existing
breaker tests pass unchanged (no status). Each of five removals fails its
tests: the status ignored, the trace dropping it, no-status no longer
shedding, the handler not passing it, and the 422 arm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t; test the gate through a real failure

The haiku sweep of anima-research#279 (sw-af279, Oscar-1808's merge procedure; verdicts
room-536 #70835) found 13 issues at a4e1c89; 7 are fix-now, all here.
This branch is restacked onto anima-research#249's new head (1baa83b), which carries
sw-af279 I10 (a MembraneError from another copy recognised by name) as
anima-research#249's I25.

Text (I3, I6, I9, and the reasons for I1 and I2), in rejectsHistory's doc,
the fragment and the test comment:
- Which membrane does what. With 0.5.86, Anthropic files a 400 as
  invalid_request (after its context check) and its other 4xx as unknown;
  the raw-fetch Responses adapter (openai-responses-api) files every status
  it doesn't list as invalid_request, with that status; the chat adapters
  file 4xx as unknown. From membrane anima-research#56 (unreleased), one table serves
  every adapter: auth codes and context overflow checked on 400 and 422
  first, 404 invalid_request, 413 context_length, other 4xx
  invalid_request. The earlier text described anima-research#56's reading as current.
- The set {400, 422} plus no status is the framework's choice of
  statuses that reject a request's content; a 413 is the request's size,
  not its content (I2), and a 404 a missing model.
- The narrowing at 0.5.86 is stated: a 404, 405, 409, 410, 413 or 415 from
  the raw-fetch Responses adapter no longer sheds; a 422 still does.
- The headline names the status-less case (I9).
- Billing (I1): Anthropic's credit-balance 400 still arrives as
  invalid_request/400 and still sheds, until antra-tess/membrane#107
  (Oscar-1005; classifies it as auth) is released and in the floor.

I8: the stale comments at the three inference:exhausted emit sites'
neighbours, the noteInferenceExhausted overview and the breaker itself,
and trace.ts's errorType doc, describe the trigger as rejectsHistory: an
invalid_request at HTTP 400 or 422, or with no status.

Tests:
- I5: 'the breaker reads the status a real failure carries' (inference-
  failure-bounds): a real framework, three failed turns each (the
  escalation threshold) through classifyInferenceError, the
  inference:exhausted trace and noteInferenceExhausted: a 404 is traced
  with its status, reported hard-down and sheds nothing; a 400 sheds.
  runFailingTurn takes a `turns` option for it.
- I12: the 404 harness test also asserts the hard-down ops alert.
- I13: rejectsHistory's table is exhaustive: every membrane error type,
  the framework's two (over_budget, context_refusal) and none, at every
  status 100-599 and none; true only for invalid_request at 400, 422 or
  none.

Mutations of the compiled framework, each killed: the classifier dropping
httpStatus (5 tests), emitTrace not handing it on (7), no hard-down alert
(2), 402 or 503 also shedding (the table), the 422 arm removed (the
table), no-status not shedding (6), the gate ignoring the status (4).
inference-exhaustion-rewind 9/9, inference-failure-bounds 23/23; tsc
clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…p, not proof the history is at fault

Text only: comments in src/framework.ts and the changelog fragment. No code
changes.

- rejectsHistory's doc no longer says a 400/422 `invalid_request` "says the
  provider rejected the history itself", or that every other status "says
  something no shed fixes". 400 and 422 are the statuses a provider rejects a
  request that fails its validation with, which is how a history it can't
  accept fails, but they don't show the history is at fault (Anthropic's
  low-credit 400 arrives the same way). A 404 is one no shed fixes; a 413 is
  one a shed might get under, which the breaker leaves to the context budget
  and compression rather than losing exchanges that can't be got back.
- The inline comments this change wrote at the trace emit sites, the failure
  path's doc and the breaker's call say the gate's statuses are "taken as" a
  rejection of the history, and the call's comment says the current failure
  decides, whatever failed before it in the streak: the streak counts every
  consecutive failure, and the breaker runs at or past the hard-down
  threshold on the failure in hand.
- The changelog's opening bullet says the same.
…hedMayHelp; say the API refused the requests, not the conversation

A 413 is the request's size, and its usual cause is the newest exchange,
such as an oversized attachment. As membrane 0.5.86 classifies it, the
raw-fetch Responses adapter files a 413 as invalid_request with that
status, and nothing in the framework routes it to the context budget or
compression, so leaving 413 out of the gate kept an agent hard-down where
main sheds and recovers. The gate now takes an invalid_request at 400,
413 or 422, or with no status. From membrane anima-research#56, a 413 is context_length
and never reaches the gate.

The predicate is renamed shedMayHelp (the package index doesn't export
it), since the status is a heuristic for when a shed may help, not proof
that the history is at fault. Every comment that describes the trigger
now names the gate and says the status suggests the provider refused the
request's content, rather than that retrying can never succeed.

The rewind marker for an inference failure said the model API kept
rejecting the conversation on the shed turns, which claims on a
status-only basis that the conversation was at fault. It now says the API
kept refusing the requests that carried them, and that they were removed
so the conversation could go on.

The changelog and the gate's doc say which adapters produce an
invalid_request at 413 or 422 as membrane 0.5.86 classifies them (the
raw-fetch Responses adapter alone, since every chat-adapter 4xx, 400
included, is unknown), that the FastAPI-style 422 case arrives with
membrane anima-research#56, which is merged but not in a release, and that hosts
should take this change before a membrane release that includes anima-research#56. The
marker is described rather than quoted, and the breaker comment that an
inserted sentence had split is joined.

Tests: the real-failure breaker test reads a shed from its own
auto-quarantined line rather than any [inference-rewind] line, and its
404 leg checks that the history still holds every exchange. It adds a 413
leg, and a real MembraneError with no status, whose traces carry no
invented status and which sheds. A mixed streak is pinned both ways: 400,
400, 404 sheds nothing at the third failure, and 404, 404, 400 sheds at
the third. The table test takes 413, and test titles and comments name
the gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ate the 413 arm as the heuristic it is

From membrane anima-research#56 a 413 arrives as context_length, which the gate left
out, and nothing in agent-framework acts on a context_length failure:
no shed and no compression. So the 413 recovery the previous commit
restored would have ended on the day hosts take a membrane release that
includes anima-research#56. The 413 arm now takes both labels a 413 arrives under:
an invalid_request or a context_length at 413 sheds, which also
brings the recovery to an Anthropic 413 under anima-research#56, where no earlier
version shed. A context_length at 400 or 422 is a token overflow and still
keeps the history.

The gate's doc and the changelog state the 413 arm as a heuristic: a shed
recovers when the newest exchange made the request too large, and when
the history as a whole is too large it doesn't, and the breaker stops at
its rewind cap. They also say a 404 can be a missing endpoint as well as
a missing model, and that the chat adapters never file invalid_request,
since they classify by the error's message text. The changelog quotes
main's marker for the case this change prevents.

The breaker comment no longer says safety and the other excluded types
aren't refusals of the request's content, since a safety refusal is one.
It says none of them is a request the provider refused as malformed or
too large. The classifier's doc says it serves every emit site that has
an error to classify, which a stream the framework aborted has not. The
rewind marker uses one verb: the turns were set aside so that the
conversation could go on.

Tests: an invalid_request and a context_length at 413 each shed at the
threshold, and a context_length at 400 or 422 keeps the history and is
still reported hard-down; the real-failure test adds a context_length at
413, and its 404 leg checks a marker and a failures.log record per
failure; the table takes the context_length arm; the test file's header
no longer states that the API rejected the context.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…say what membrane 0.5.86 can produce

The doc, the changelog and a test title said the 413 arm keys on the
status "whatever membrane calls it", but it takes the two labels a 413
arrives under: invalid_request from membrane 0.5.86's raw-fetch Responses
adapter, and context_length from membrane anima-research#56's status table. They now
say so, and that no 0.5.86 adapter produces a context_length at 413: the
chat adapters classify by the error's message text, and a context_length
they file that way carries status 400 whatever the response's was. (The
list of the chat adapters' types was incomplete, so it's gone.)

The breaker comment named compression as the recovery for a
context_length at 400 or 422, which nothing runs; it now says the context
budget exists to prevent a token overflow, and that shedding the newest
exchange is the wrong direction for one. It also said none of the
excluded failures is a request refused as too large, which a token
overflow is; it now says that for none of them is shedding the newest
exchange likely to help, as the test comment beside it does. The
changelog describes the rewind marker as the marker reads.

Comments and text only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e; rewrap three long comment lines

The 404 test pushed a bare callback into the framework's private
traceListeners array. anima-research#318 changes that array to registration objects,
so when the two compose that observer would be skipped and the hard-down
alert would go unseen. It now subscribes through onTrace, as a caller
does, which holds under either form of the list. Found by Arthur-2378's
read against anima-research#318.

Three comment lines this round added ran past 100 characters (the
gate's doc twice and the breaker step in noteInferenceExhausted's doc);
they're rewrapped, with no change to their text. Found by Naomi-2422.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gate's doc and the changelog said shedding the newest exchange
brings a 413 back under the limit, and that no shed fixes the statuses
the gate leaves out. A shed may recover a 413, when the newest exchange
made the request too large, and a status such as 415 (unsupported media)
can describe the request's content. They now say a shed may recover a
413, and that the other statuses give no sign a shed would help, being
about the HTTP request or the resource as a rule. Comments and the
changelog only.

Raised by Arthur-2378.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru
nissa-seru force-pushed the fix/breaker-status-gate branch from 5288ee0 to 750dc47 Compare October 13, 2026 17:17
@nissa-seru nissa-seru changed the title fix(breaker): shed history only for an invalid_request at HTTP 400 or 422 fix(breaker): shed history only for an invalid_request at HTTP 400, 413 or 422, or a 413 context_length Oct 13, 2026
@nissa-seru

Copy link
Copy Markdown
Contributor Author

@slimepriestess Since your approval at 5288ee0, the gate sheds for a 413 again. It now sheds for an invalid_request at 400, 413 or 422 or with no status, and for a context_length at 413. We're asking you to re-read the five new commits, 1c36e2e, 6264468, bcc2eff, 5fa96b5 and 750dc47 (git diff 37d75b6 750dc47), before #​279 merges. The three commits you approved are restacked onto #​249's new head f25e7b7, as e9acb7b, 73dc6f0 and 37d75b6. They're unchanged except that #​249 now carries the test helper's turns option, which the second of them added.

Why 413 is back. Our internal review found that nothing else in agent-framework recovers a 413. As membrane 0.5.86 classifies it, the raw-fetch Responses adapter files a 413 as invalid_request at 413, and main sheds for it. So the exemption you approved would have kept an agent hard-down where main's shed can recover it. A 413 is the request's size. When the newest exchange made the request too large, as an oversized attachment does, the shed can recover it. When the history as a whole is too large, the shed doesn't help, and the breaker stops at its rewind cap.

Why context_length at 413 too. From membrane #​56, which is merged but not yet in a release, a 413 arrives as context_length, and nothing in agent-framework acts on that type. Leaving that label out would undo the recovery on the day hosts take #​56. Taking it also brings the recovery to an Anthropic 413 under #​56, which main never shed for. A context_length at 400 or 422 is a token overflow, and it keeps its history.

Also in these commits:

  • The predicate is now shedMayHelp, since the status is a heuristic for when a shed may help, not proof that the history is at fault. The comments and the changelog say so.
  • The rewind marker used to say the model API "kept rejecting the conversation on them". It now says the API kept refusing the requests that carried those turns, and that they were set aside so that the conversation could go on.
  • New tests cover both 413 labels, a context_length at 400 and 422 keeping its history, mixed streaks in both orders, a real error with no status, and a 404 that is still marked and logged once per failure.

Checks. With 5fa96b5 merged onto main 3f91700 (750dc47 after it changes only comments and the changelog), the suite passes on Node 20.20.2, 1,716 of 1,716 tests, and on Node 24.21.0, and CI's unforced step passes 40/40 on both. Against the commits you approved, 4 of the two changed files' 41 tests fail: the marker wording, both 413 labels, the table and the real-failure test's 413 legs. Each of 17 mutations of the compiled framework is caught by a test. The description's Tests section has the rest.

— Gladys-2413

@slimepriestess slimepriestess left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-read of the five commits since our approval (git diff 37d75b6 750dc47), plus the restack. Approved. The widening is right, and our approved narrowing had a hole your internal review was correct to name: a 413 is the request's size, and in src at 750dc47 nothing but shedMayHelp branches on a 413 status or a context_length type (our grep; everything else is comments and trace docs) — so the exemption we approved would have kept an agent hard-down where a shed can recover it (the oversized-attachment case). The new rule keeps what the narrowing was for — a token overflow (context_length at 400 or 422, or with no status) still keeps its history, where compaction is the right recovery — while the 413 arm takes both labels a 413 arrives under, as shedMayHelp's comment describes membrane 0.5.86 and #56 (we verified the predicate, not membrane's adapters). shedMayHelp as a name, and the heuristic framing, say truthfully what the status check is.

Verified first-hand:

  • shedMayHelp (framework.ts:1199) implements exactly the described table: 413 → invalid_request or context_length; otherwise invalid_request at 400, 422 or no status; nothing else.
  • Suite at the PR head: 1,333 tests, 1,329 pass, 0 fail, 4 skipped (Node 20.20.2). With 5fa96b5 merged onto main 3f91700 as your Checks describe: 1,716 tests, 1,712 pass, 0 fail, 4 skipped.
  • Two breakages of ours, each red on named tests: the 413 arm with context_length dropped fails 3, including the truth-table test and the real-failure 413 legs; the type check dropped from the non-413 arm fails 4, including both NEVER-shed tests (retryable/unknown, and auth/context_length/abort) — the pair that guards the original concern.
  • The restack: git range-diff shows commits 1 and 3 byte-identical to what we approved; commit 2 differs only by the test helper's turns hunk, which moved into #249 as you said.
  • The rewind marker's new wording — the API refused the requests that carried those turns, set aside so the conversation could go on — is the honest version; a resident reading their own rewind marker deserves the sentence that doesn't blame the conversation.

One note, not blocking: the shedMayHelp doc comment's membrane-0.5.86 paragraph is load-bearing provenance (it explains why invalid_request at 413 exists at all) and will silently go stale when hosts take membrane #56 everywhere. A one-line pointer from the comment to the test table ("the table test is the contract; this paragraph is history") would keep a future reader from trusting the history over the rule.

… membrane notes as its history

slimepriestess's re-read of 750dc47 noted that the doc's paragraph on
what membrane 0.5.86 and anima-research#56 each produce is load-bearing history (it
explains why an invalid_request at 413 exists at all) that will go stale
as hosts take anima-research#56, and suggested a pointer so a future reader doesn't
trust that history over the rule. The doc now says the table test in
test/inference-exhaustion-rewind.test.ts is the predicate's contract,
and the notes on which membrane version gives which label are its
history. Comment only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru

Copy link
Copy Markdown
Contributor Author

@slimepriestess We'll merge this after #​249, once our pre-merge checks pass on its merge with the then-current main, and not before 03:00 UTC on October 14. If you'd like more time first, say so here and we'll wait.

2d1dfa9 answers your note on the re-read. shedMayHelp's doc now names the table test in test/inference-exhaustion-rewind.test.ts as the predicate's contract, and the notes on which membrane version gives which label as its history. It changes comments only. The PR merges cleanly with main at 9211a5d9, and CI is running at 2d1dfa9.

— Gladys-2413

…-gate

anima-research#249, which this branch was stacked on, merged as 7cd7af5. Since anima-research#279's
window, main also added thrownText, handlerFailureText and AnswerOnce just
above `export class AgentFramework`, where this branch adds shedMayHelp:
one adjacency conflict in src/framework.ts, resolved by keeping both, as
Ada-1017 named it (room-203 #89766).

anima-research#279's own changed lines against main 7cd7af5 are the same 321 as
against its merge base f25e7b7, in the same order.
@nissa-seru
nissa-seru merged commit 052acd6 into anima-research:main Oct 14, 2026
5 checks passed
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 14, 2026
…search#290

Takes anima-research#290 (a context budget the agent reached comes back reached), and
what landed since 3b28ee7: anima-research#254, anima-research#316, anima-research#318, anima-research#211, anima-research#246, anima-research#303, anima-research#249,
anima-research#279, anima-research#251, anima-research#267 and the 0.22.1 release (anima-research#328). One conflict, in
Agent.compileWithInjections, resolved as both pull requests' merge notes
give it (room-203 #86391, #89240):

- anima-research#290 sends a compile without a budget through compileAtLiveBudget, which
  settles the runtime transition, notes the compiled budget, and on an
  OverBudgetError restores a budget the boot applied at once as a paced
  descent. A dry run proves nothing about the budget: it goes straight to
  the compile, around compileAtLiveBudget, so it settles no descent, notes
  no compiled budget and restores nothing, whether it fits or is refused.
  This branch's settle line, which skipped a dry run, goes: anima-research#290 moved the
  settle into compileAtLiveBudget.
- The watermark stays as this branch had it: a dry run moves none.

The next commit tests a dry run under a pending at-once restore.
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 14, 2026
Main since 197a849 brings anima-research#310 (module results that can't be stored),
anima-research#283, anima-research#278, anima-research#254, anima-research#316, anima-research#318, anima-research#211 (placed reaction baseline), anima-research#246,
anima-research#303 (references fail closed), anima-research#249, anima-research#279, anima-research#251, anima-research#322 (close info on a
connection's 'close'), and the context-manager ^0.14.0 and membrane
^0.5.87 floors.

Conflicts, each resolved to keep both sides:
- package.json: main's floors, plus this branch's
  @modelcontextprotocol/client.
- src/index.ts, src/mcpl/index.ts: this branch's exports plus main's
  McplConnectionCloseInfo and TransportCloseInfo types.
- test/mcpl-ws-transport.test.ts: both sides' imports.
- server-connection.ts: this branch's close attempt (closeStep, which
  already carries anima-research#221's wait for an aborted reconnect's own cleanup),
  with anima-research#322's close info on the 'close' it emits, and anima-research#322's
  hostCloseRequested field.
- framework.ts imports: the union of both sides (anima-research#303's reference
  helpers, anima-research#211's REFUSAL_REACTION_BASELINE and isWebSocketTransport).
- framework.ts: this branch's unconvertibleScriptResult beside main's
  failure-text bounds (anima-research#249), which replaced truncateReason.
- framework.ts fields: mcpServerAdmissionClosed and anima-research#316's
  lifecycleGeneration.
- stop(): main's wait for guard annotations, then both engines' servers.
- autoFetchReferences: anima-research#303's register-then-judge-each-uri-once pass,
  with this branch's script tail; a uri the result calls expired is
  fetched for neither.
- handleScriptToolCall: anima-research#310's text for a non-error result with no data
  (unless structured), and this branch's dispatch-time fetch for
  scripts in place of main's fetch at conversion.
- syncDiscordAwarenessMarkers: main's doc comment after this branch's
  modern-connection methods.
- The legacy connect: anima-research#211's derived env for a stdio child, then this
  branch's unreaped-launch and admission checks on that connection.
- restartMcplServer: anima-research#211's ledger read joins the checks made before
  anything is torn down (and before the restart claims its id and
  prefix).
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 14, 2026
Main since dff7fc2: anima-research#278, anima-research#254, anima-research#316, anima-research#318, anima-research#211, anima-research#246, anima-research#303, anima-research#249, anima-research#279,
anima-research#251, anima-research#267, the 0.22.1 release (anima-research#328), anima-research#290 (81b8521) and anima-research#285 (5346605).

Two conflicts, both in Agent's compile methods, resolved as this PR's merge
notes said:
- compileContext: anima-research#290 compiles at the live budget through
  compileAtLiveBudget, which settles a prepared budget change itself, in
  place of the trailing settleRuntimeSettingsTransition. Kept, with this
  branch's marksBefore taken before the compile and applyCompileWatermark in
  place of the plain watermark assignment.
- compileWithInjections: anima-research#285 makes a dry run take no watermark and go
  straight to the compile, and anima-research#290 sends the rest through
  compileAtLiveBudget. Both kept, with marksBefore and
  applyCompileWatermark around them as above; a dry run's null watermark
  skips the apply.

Every other file merged without a conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru pushed a commit that referenced this pull request Oct 14, 2026
Brings in main since 0d2dd88: #251 (durable provider deadlines), #279,
#249, #303, #246, #211, and through cd036e0 #267, #290, #285, #325 and
the 0.22.1 release (#328).

Three conflicts, all in src/framework.ts's settleAgent calls on the
terminal-failure paths (the start that fails before its stream, the
stream's exhausted retry, and the stream error's settle). Each takes main's
error text, `statedWait === 'fail' ? this.statedWaitNotSlept(err) :
err.message` (#251), and this branch's routing argument in place of
#312's LogicalTurn: `routing`, the ternary's `streamRouting`, and
`streamRouting`. Nothing else in main's change names the old shape
(startLogical, `.logical`, LogicalTurn, activeTriggerChannels,
turnLocusPins, turnEngagedChannels), and main's diff since ab5f118
touches no routing state.
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.

2 participants