cordis-plugin-empty-response-retry
Verified@argszero/cordis-plugin-empty-response-retry Β· v0.5.0 Β· MIT
Makes an unattributed model attempt retryable, for the one seam that can tell. Three reports, one missing evidence. Discussion #8321: a `dsh-llm-pi-ai` route that relays an upstream empty response ends the turn, because `classifyPiAiError` has no rule mat
Install
dsh plugin add @argszero/cordis-plugin-empty-response-retry Confirm the layer applied with dsh --profile default --dump-config β see the install guide.
Source
Tags
Readme
@argszero/cordis-plugin-empty-response-retry
Makes an unattributable model failure retryable β for the one seam that sees an attempt and everything it produced.
Answers four reports of the same missing evidence:
- deepseek-ai/deepseek-harness discussion #8321
β an empty response the adapter could not name at all (
PI_AI_ERROR). - deepseek-ai/deepseek-harness discussion #9352
β a
400with an empty body, namedINVALID_REQUESTfrom the client's own status line. - deepseek-ai/deepseek-harness discussion #9397 β a wording pi-ai itself retries that the adapter's rule chain cannot place, so it falls to the same catch-all.
- deepseek-ai/deepseek-harness discussion #9402
β a Tool call whose streamed arguments never became JSON, so the adapter threw
MALFORMED_RESPONSEand the turn ended with nothing durable on it.
npm install @argszero/cordis-plugin-empty-response-retry
The gap
There are four ways an attempt reaches the retry executor under a code nobody can act on, and each was reported separately (#8321, #9352, #9397 and #9402). This plugin handles all four, because they are one fact: the code was chosen without a diagnosis that names what actually failed β and, in the third case, the diagnosis existed upstream and was simply not carried across, while in the fourth the adapter did name something, but named its own reading of the bytes rather than anything that happened at the provider.
Report #8321: the adapter could not name the wording at all
A route served by dsh-llm-pi-ai behind OpenRouter (api: openai-completions)
answered a turn with nothing at all, and the turn ended:
attempt t3s2 β error: PI_AI_ERROR Provider returned an empty response
AGENT ERROR t3: Provider returned an empty response
Seven attempt-level failures across two days on that route, and β this is the
part that matters β not one llm/retry event. dsh-llm-retry was mounted
(the reporter confirmed it from llm/retry-started events on other failures), so
the executor was running. It simply never accepted the step.
The reason is one word in a table. llm-retry decides on the failure's code,
and the code this path produces is not in the default retryable set.
The chain, link by link
The provider relays an empty response. pi-ai's OpenAI-completions adapter catches it and stores the text on the terminal
errorevent'serrorMessageβ the provider's own words, verbatim.The classifier cannot place the wording.
classifyPiAiError(packages/llm/llm-pi-ai/src/stream.ts) is a chain of wordings β401|403, quota,429,413,400,5xx, timeout, stream truncation, transport β and none of them matches "Provider returned an empty response". The text falls to the catch-all:return 'PI_AI_ERROR'The failure reaches the retry executor unchanged. The finish chunk is
{ type: 'finish', reason: { kind: 'error', failure } }, and the agent loop forwardsfinish.failureverbatim intoagent/request-error.The executor declines it.
} else if (!policy.retryableCodes.includes(failure.code)) { return next() }The default table does not carry that code.
packages/llm/llm/src/retry-policy.ts:const DEFAULT_RETRYABLE_CODES = Object.freeze([ EMPTY_RESPONSE_CODE, 'RATE_LIMIT', 'SERVER', 'TIMEOUT', 'TRANSPORT', ])PI_AI_ERRORis absent, so the step fails and the turn ends.
Why that is a bug rather than a policy
The contradiction is inside one file. mapStopReason maps a locally detected
zero-content completion β a stop with no content blocks β to EMPTY_RESPONSE,
which the default policy retries. The same outcome, arriving as provider text
instead of as a pi-ai stop reason, maps to PI_AI_ERROR, which it does not.
EMPTY_RESPONSE_CODE's own docstring states the principle the two paths
disagree about (packages/llm/llm/src/error.ts):
The attempt produced nothing durable, so retry policy treats it as safe to repeat.
An upstream-relayed empty response is exactly "nothing durable".
Report #9352: the rule matched the client's own sentence
The second report reaches the same place from the other side of the classifier β a rule did match, but its only evidence was a number the HTTP client had written about itself.
A gateway answered 400 with an empty body. The HTTP client surfaced its own
sentence for that β 400 status code (no body); the Anthropic and OpenAI SDKs
share APIError.makeMessage, which writes exactly that when it has a status and
no body β and classifyPiAiError's rule for the number (/\b400\b|invalid.?request/i)
matched the status inside the client's own sentence. The turn was reported as
INVALID_REQUEST:
assistant/attempt β¦ "failure":{"message":"400 status code (no body)","code":"INVALID_REQUEST"}
INVALID_REQUEST is in no retryable table, and no recovery path watches it
either β compaction keys on CONTEXT_WINDOW_EXCEEDED alone. So every later
turn re-sent the same transcript, reached the same gateway state and failed at
step 1: a twelve-minute provider blip bricked the session. The reporter replayed
the identical request afterwards β 898 messages, 27 tools, 2.26 MB β and the
gateway answered 200 twice, with a prompt-token count 0.07 % off the
original. The request was never invalid.
INVALID_REQUEST here is not a name for what happened; it is a name for the one
word the client managed to write. That is the same missing evidence as #8321, and
it deserves the same treatment: repeat the step a bounded number of times.
Report #9397: the diagnosis existed upstream
The third report arrives as one missing wording. It is really a divergence between two tables, and that is what this release acts on.
A route behind a gateway ended a turn with:
assistant/attempt β¦ "failure":{"message":"exceeded request buffer limit while retrying upstream","code":"PI_AI_ERROR"}
That sentence is Envoy's upstream-retry buffer limit β a 507 the gateway
answered because it could not hold the request it was retrying. It carries no
status number, and classifyPiAiError's 5xx rule is /\b5\d\d\b/: it needs
the digits the gateway dropped. No other rule matches either, so the wording
falls to the catch-all PI_AI_ERROR β which is in no retry table β and the turn
ends.
The reporter proposed one more alternative in the SERVER rule, and asked for it
to stay literal, because the neighbouring 413 rule exists for a failure
whose text shares both words β Failed to buffer the request body: length limit exceeded β and means the opposite. That rule's own comment says why:
A rejected request body (gateway or provider size cap): resending the same request cannot succeed, so it is invalid, not transient.
The observation generalizes. pi-ai decides retryability from a table of its
own, and the harness adapter's classifier is a second, partial transcription of
it. isRetryableAssistantError (@earendil-works/pi-ai/dist/utils/retry.js)
tests the message against RETRYABLE_PROVIDER_ERROR_PATTERN β 47 alternatives at
0.85.1, 49 at 0.87.1 (the reporter's version), 55 at 1.1.0 β after
declining anything matching NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN (quota,
billing, subscription limits). Measured against the installed table:
| alternatives | the chain places | it cannot place | |
|---|---|---|---|
0.85.1 (the 0.1.6 / 0.1.7 lines) |
47 | 24 | 23 |
0.87.1 (the 0.2.0-rc line) |
49 | 25 | 24 |
1.1.0 |
55 | 25 | 30 |
Every unplaceable one falls to the catch-all and is never repeated β this
report's sentence beside overloaded, service unavailable, too many requests,
getaddrinfo, ENOTFOUND, reset before headers and the rest. test/packaging.spec.mjs
holds that measurement as an assertion against the installed package, so a quiet
upstream rewrite that closes the gap is visible rather than discovered β and the
floor it asserts is the smallest gap across the supported lines (23, at
0.85.1), because the arm runs on whichever line was installed and the gap moves
with the table.
The report's framing β one more wording in the SERVER rule β would fix one
line of that table. The framing here is the direction the two tables were built
in: the retry decision is already in the SDK the harness calls, and what is
missing is to read the same table on the way out.
What this plugin does
It observes the public llm/stream waterfall β the one seam that sees an
attempt and every chunk it produced β and, when an attempt ends in an
unattributed error and produced nothing at all, rewrites that finish's
failure.code to the harness' own EMPTY_RESPONSE. The retry executor then
accepts the step on its own, unchanged code gate.
// what the harness sees before
{ type: 'finish', reason: { kind: 'error', failure: {
code: 'PI_AI_ERROR', message: 'Provider returned an empty response' } } }
// β¦and after
{ type: 'finish', reason: { kind: 'error', failure: {
code: 'EMPTY_RESPONSE', message: 'Provider returned an empty response' } } }
The same rewrite handles the second report:
// before β the client's sentence, and a code that names only the client
{ type: 'finish', reason: { kind: 'error', failure: {
code: 'INVALID_REQUEST', message: '400 status code (no body)' } } }
// β¦and after
{ type: 'finish', reason: { kind: 'error', failure: {
code: 'EMPTY_RESPONSE', message: '400 status code (no body)' } } }
Only the code changes. The provider's message, status and
providerRetryAfterMs are carried through untouched, because the session log is
where a maintainer reads what the provider said and the code is the only field
the policy reads.
"Unattributed" has four shapes, and they are one principle β a code chosen without a diagnosis that names what actually failed:
PI_AI_ERRORβ the adapter's catch-all. No rule matched the provider's text at all. This iscodes, for report #8321.INVALID_REQUESTreached from the client's own bodyless status line. A rule matched, but its only evidence was the status the client wrote. This isstatusOnlyCodes, for report #9352, gated on the message bystatusOnlyMessages: a400that carries the provider's words carries a diagnosis, and is left exactly as it is.PI_AI_ERRORwhose message is a wording pi-ai itself retries. A third case, and the widest of the three: the adapter could not place the wording, but pi-ai could β the harness's classifier is a second, partial transcription of the SDK's own retry table, and the gap between them is 23 wordings at0.85.1, 24 at0.87.1and 30 at1.1.0(see report #9397 above). This istransientCodes(default:PI_AI_ERROR),transientMessages(the server-shaped alternatives) andtransientTargetCode(default:SERVER, the code the chain would have written had the rule seen the digits the gateway dropped):// before β a wording pi-ai retries, on a code no retry table admits { type: 'finish', reason: { kind: 'error', failure: { code: 'PI_AI_ERROR', message: 'exceeded request buffer limit while retrying upstream' } } } // β¦and after { type: 'finish', reason: { kind: 'error', failure: { code: 'SERVER', message: 'exceeded request buffer limit while retrying upstream' } } }Unlike the first two, this recognition does not require the attempt to have produced nothing: the code it writes is about the route, not about output, and the harness's own gate proves it needs no such condition β
dsh-llm-retrydecides onpolicy.retryableCodes.includes(failure.code)and nothing else, so aSERVERthe adapter reports for a real503is repeated whether or not the stream got as far as a token. The third recognition restores that classification; it does not invent a stricter one.MALFORMED_RESPONSErefused for arguments the adapter could not read. The one recognition whose code the adapter did choose β but for a refusal about serialization rather than about the request. This isunparseableCallCodes(default:MALFORMED_RESPONSEalone), for report #9402:// before β the Tool call never became JSON, so the adapter threw at message_stop { type: 'block-start', index: 0, blockType: 'tool-call' } { type: 'tool-call-delta', index: 0, id: 'call_1', name: 'read_file', argumentsDelta: '{"path":' } { type: 'block-end', index: 0, block: { type: 'tool-call', id: 'call_1', name: 'read_file', arguments: '{"path":' } } { type: 'finish', reason: { kind: 'error', failure: { code: 'MALFORMED_RESPONSE', message: 'DeepSeek Messages stream: tool input is invalid JSON' } } } // β¦and after β same chunks, and only the code changes { type: 'finish', reason: { kind: 'error', failure: { code: 'EMPTY_RESPONSE', message: 'DeepSeek Messages stream: tool input is invalid JSON' } } }Its gate is the strongest of the four, and it is a fact about the attempt rather than about the failure, so it lives in the decision rather than in the code list: every Tool call the attempt assembled must be one the adapter cannot read, at least one must exist, and no text or reasoning may have arrived beside them. A
MALFORMED_RESPONSEthat never assembled a Tool call at all β an undecodable SSE envelope, a duplicatemessage_start, a delta with no open block, a typed field of the wrong type β is declined here, because the plugin will not report a shape it did not observe. So is one that assembled a readable call beside the corrupt one (the adapter validates in order and throws on the first it cannot read, so such a turn exists), and so is one that streamed text first. All three keep the adapter's own code.The reading of "unreadable" is the adapter's own, quoted from its
message_stopbranch:JSON.parse, then a plain record. The second half is not decoration β itsobject()helper rejectsnull, an array, a string and a number as firmly as it rejects a syntax error.test/deepseek-malformed.spec.mjsholds the two predicates against each other over a table of argument strings, running both: the adapter's verdict from its stream, the plugin's from the exported function.The raw arguments are not lost by declining to look further β the attempt is recorded before the failure, so the corrupt JSON is durable in
assistant/attempt.streamand a retry loses nothing (#9402, ask 3).
Every recognition consults vetoMessages first β the alternatives pi-ai declares
non-retryable (its NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN: quota, billing,
subscription limits) β because pi-ai itself declines those before it consults
the retryable table, and a limit that resets in hours is not the safe repeat this
principle is about.
This is the structural fix all four reports preferred over adding one more regex to a chain with a five-report history (#4361, #6595, #7691, #8321, #9352), every one of which enumerated a new wording. It is at the layer that actually knows how much was produced, and it is wording-independent in the sense that matters: for the first two recognitions it never asks what the provider said, only whether anyone said anything, and the fourth asks not what the failure means but what the attempt actually assembled.
For the second recognition the message gate is unavoidable β INVALID_REQUEST is
also the correct code for a genuine malformed request, and only the message can
tell the two apart. The gate fails in the safe direction: any wording other than
the client's own bodyless sentence declines, and declining leaves today's
behaviour exactly as it is.
What it cannot do, and does not pretend to. Report #9352 also asks for the
HTTP status, headers and a body snippet to be persisted, and for a cheap recovery
after repeated identical failures. Neither is reachable from a plugin:
mapStopReason puts only message and code on the failure, so the status is
gone before any listener sees it, and compaction is core's. What this plugin
contributes is the first ask β an unclassifiable bodyless 4xx is repeated a
bounded number of times instead of ending the turn. For the one provider whose
bodyless 400/413 is known to mean context overflow (Cerebras, in pi-ai's
isContextOverflow), the decision is made upstream of classifyPiAiError, so the
failure arrives here already reported as CONTEXT_WINDOW_EXCEEDED and is not this
plugin's to touch.
What it refuses
The two deliberate terminal states
PI_AI_ERROR is the adapter's catch-all, and two of its producers are terminal
on purpose. The adapter's own docstring names them: a pi-ai stream that ended
pending or deferred maps to PI_AI_ERROR, because the answer is not a
failure but a not-yet β the request will be answered or billed later, and
re-sending it is not the safe repeat the principle is about.
A route-wide retryableCodes: [..., PI_AI_ERROR] widens those too; this plugin
does not. They are the default exceptMessages.
The plugin cannot see which branch of mapStopReason wrote the message, so the
exclusion keys on wording β on the narrowest fact that is true of those two and
of nothing else: every message dsh-llm-pi-ai authors for this family begins
with its own pi-ai prefix, while a provider-relayed message does not. The
prefix rule fails in the safe direction. If upstream rewords one of them, or if
some provider echoes text beginning with pi-ai , the plugin declines to act and
the attempt keeps the code the adapter chose β which is what happens today.
Matching the two sentences exactly would fail the other way: a reworded pending
would start being retried. test/packaging.spec.mjs checks the default against
every pi-ai -prefixed literal in the installed adapter bundle, so a fourth
producer in the same voice is caught by the suite rather than in the field.
Everything else
- The attempt produced content. Any block boundary, non-empty token delta or
Tool call before the finish means something became durable, and re-sending the
request could duplicate it. Note this is not the harness'
isVisibleChunkpredicate, which answers a different question β what a transcript reader sees β and therefore treats Tool calls as protocol rather than content. A Tool call is durable, so a durability test must not ignore it. - A
MALFORMED_RESPONSEthat is not the fourth shape. A refusal that never assembled a Tool call, a turn that assembled a call the adapter could read beside the corrupt one, and a turn that streamed text or reasoning first are all left with the adapter's own code. The fourth recognition is the only one whose code the adapter chose deliberately, so it is the only one that has to argue from what the attempt assembled rather than from the code alone β see the four-shape list above. - The failure is one the adapter named from something the provider said.
AUTH,CONTEXT_WINDOW_EXCEEDED,INVALID_CREDENTIAL,QUOTA,RATE_LIMITand the rest are somebody else's to recover from. Only codes listed incodes(default: the catch-allPI_AI_ERRORalone), instatusOnlyCodes(default:INVALID_REQUESTalone, and only in its bodyless status-line form) or intransientCodes(default: the catch-all again, and only for a wording pi-ai's own retry table carries) are touched. - A message pi-ai declares non-retryable.
vetoMessagesis consulted on every recognition: quota, billing and subscription-limit wordings are left exactly as the adapter wrote them, because pi-ai itself declines them before it consults its retry table and a limit that resets in hours is not the safe repeat this principle is about. - A pi-ai-retryable wording this plugin does not claim. The third recognition
ships the server-shaped half of pi-ai's table only. The transport-shaped
alternatives (
getaddrinfo,ENOTFOUND,reset before headers,websocket β¦, β¦) and the throttle-shaped ones (too many requests,ResourceExhausted) are left alone, becauseSERVERwould be the wrong name for them and a wrong name in a durable log is the thing this plugin exists to stop writing.transientMessagesis the knob for an operator who wants them. - A
400that carries the provider's words.INVALID_REQUESTis the one named code this plugin will widen, and only when the message is nothing but the HTTP client's own bodyless status line. A400with a body β the provider telling you why β is a diagnosis, and is left exactly as the adapter wrote it. As with everything else here, the gate fails toward today's behaviour. - A cancellation. An
abortedfinish is the caller's decision, not a provider failure. - A success. A
stop,max-tokensortool-callsfinish is never touched.
Install and mount
Publish-time behaviour is on by default; nothing else is required.
# the bundle patch this package ships (cordis.patch.yml)
- insert:
- id: empty-response-retry
name: '@argszero/cordis-plugin-empty-response-retry'
Or, from a profile of your own:
plugins:
empty-response-retry:
module: '@argszero/cordis-plugin-empty-response-retry'
Start with mode: warn to measure how often your own traffic hits the rule
before letting it change behaviour β the warning names the route, the adapter's
code, and the provider's message.
Configuration
| key | default | meaning |
|---|---|---|
mode |
'error' |
'error' rewrites the finish; 'warn' only logs; 'off' forwards everything unchanged. |
codes |
['PI_AI_ERROR'] |
Failure codes whose zero-output attempts are widened unconditionally β the adapter's catch-alls. |
statusOnlyCodes |
['INVALID_REQUEST'] |
Failure codes widened only when the message is the HTTP client's own bodyless status line. |
statusOnlyMessages |
the client's β¦ status code (no body) sentence, and an empty JSON container |
Regular-expression sources; a failure whose code is in statusOnlyCodes is widened only when its message matches one. Anchored at the end of the message. |
transientCodes |
['PI_AI_ERROR'] |
Failure codes whose message is checked against pi-ai's own retry table β the third recognition (#9397). |
transientMessages |
the server-shaped half of pi-ai's RETRYABLE_PROVIDER_ERROR_PATTERN, at every version the supported lines resolve |
Regular-expression sources; a failure whose code is in transientCodes and whose message matches one is rewritten to transientTargetCode. One entry (currently experiencing high demand, upstream's addition at 0.87.0) is only in the newest line's table β see NEWER_LINE_TRANSIENT_MESSAGES below. |
transientTargetCode |
'SERVER' |
Code written for the third recognition β the name the classifier would have chosen had the wording carried the status number. |
unparseableCallCodes |
['MALFORMED_RESPONSE'] |
Failure codes the fourth recognition (#9402) may consider β admitted on the code, then gated on the attempt's whole output being Tool calls the adapter cannot read. |
vetoMessages |
pi-ai's NON_RETRYABLE_PROVIDER_LIMIT_ERROR_PATTERN (quota, billing, subscription limits) |
Regular-expression sources; a failure whose message matches any is left alone, on every recognition. Consulted before the retryable list, as pi-ai itself does. |
exceptMessages |
['^pi-ai '] |
Regular-expression sources; a failure whose message matches any is left exactly as the adapter wrote it. |
targetCode |
'EMPTY_RESPONSE' |
Code written on a widened finish by the first two and the fourth recognitions. |
A code listed in both codes and statusOnlyCodes is widened unconditionally,
as codes describes β there is nothing left to gate. The same holds for a code
listed in both codes and unparseableCallCodes: the fourth recognition's shape
gate belongs to its recognition, and an operator who listed a code under codes
has asked for admission on the code alone.
Every field is validated at mount time and a bad value throws rather than silently disabling the guard.
plugins:
empty-response-retry:
module: '@argszero/cordis-plugin-empty-response-retry'
config:
mode: warn
Design notes
Streaming-preserving by construction. The verdict needs only the terminal
chunk and a four-field record accumulated from the chunks before it, so nothing is
buffered: every chunk is yielded the moment it arrives, chunk objects are passed
through by identity, and at most the last one is rewritten. The guard never
reorders, never delays, and keeps no state between attempts β the record is a
local of one pass over one stream, and two overlapping attempts each fold their
own (a Tool call is judged at the block-end the adapter assembled, which is the
one chunk that carries the complete arguments).
The rewrite is a copy, not a mutation. The chunk a listener receives may be frozen (a loop-built request arrives deep-frozen), and every other listener on the same waterfall keeps the chunk it was handed.
One effect, one place. Searching the tree, EMPTY_RESPONSE_CODE is consumed
by exactly one thing: DEFAULT_RETRYABLE_CODES. Widening a finish to it does not
change how the turn is rendered, logged or replayed β it only makes the attempt
eligible for the policy the harness already applies to a locally detected empty
completion.
What this does not do. It does not change retryableCodes, maxRetries or
backoff; those belong to each provider's retryPolicy and are read by
dsh-llm-retry. It does not touch a code the adapter named from something the
provider said; the one named code it will widen (INVALID_REQUEST) is gated on
the message being nothing but the client's own bodyless status line. It does not
patch, wrap or monkey-patch the adapter β it is a listener on a documented
waterfall, so removing the plugin restores the previous behaviour exactly.
Compatibility
peerDependencies:
"@deepseek-ai/dsh-llm": ">=0.1.6-alpha.1 <0.1.7 || >=0.1.7-alpha.1 <0.2.0 || >=0.2.0-rc.1 <0.3.0"
Verified by running this suite against each line β 0.1.6, 0.1.7 and 0.2.0-rc
(npm run test:probe-lines, which installs the newest published build of every
|| segment and re-runs the whole suite there). A line is not listed until it has
been run.
The lines do not run the same pi-ai, and until 0.5.0 the suite did not know
that. 0.1.6 and 0.1.7 resolve 0.85.1; 0.2.0-rc resolves 0.87.1; the two
tables differ by exactly two alternatives. The shipped transient defaults and the
gap floor had both been derived from 0.87.1, so on the two oldest lines one
default could not be re-derived and one floor was one short.
A third arm failed for an unrelated reason, and it is the one worth naming
because it disabled more than itself. The fixture that wires the installed
adapter passed the credential hook under the name the newest line reads
(resolveAuth, returning headers); the two oldest lines still call it
resolveApiKey, returning the key. The adapter therefore called undefined as
its credential resolver, the request died inside the transport wrapper as
TRANSPORT β one layer above the MALFORMED_RESPONSE refusal that suite is
about β and every arm gated on measuring that refusal went quiet rather than
failing loudly. Three arms, two causes, and all three were the suite asking a
question that had only ever been answered about the newest line. 0.5.0 fixes
the suite rather than the claim:
- the defaults are split into the wordings every supported line's table
carries and
NEWER_LINE_TRANSIENT_MESSAGESβ the one entry upstream added at0.87.0(currently experiencing high demand); - the wording arms now assert provenance per installed line: a shared entry must be in the installed table and be one pi-ai retries, while a newer-line entry must be there iff the installed pi-ai is new enough, and must be one pi-ai genuinely declines when it is not;
- the gap floor is the smallest gap across the supported lines (
23), not the newest line's (24); - the pinned-adapter arm no longer compares a manifest field against a version written in the test file β it asserts that the adapter npm installed is the one the manifest pins, which is the invariant a probe that rewrites the manifest for each line still satisfies, and the skip message now names the build actually under test;
- the adapter fixture supplies the credential hook under both spellings
(
resolveAuthandresolveApiKey), because the installed adapter reads exactly one of them and ignores the other β so the behaviour arms now run on every line instead of being skipped on two of them.
The one place the rewrite is wider than the installed table, stated plainly:
on a 0.1.x line, a failure worded currently experiencing high demand is not
one that line's pi-ai would retry, and this plugin makes it retryable anyway. That
is deliberate β upstream reached the same judgement one minor later, about the same
server-overload class the recognition exists for, and the recognition's gate is the
code and the wording rather than the emission, so what it acts on is a route that
could not serve the request. An operator who would rather stay strictly inside
their own line's table passes transientMessages without that entry; the export is
NEWER_LINE_TRANSIENT_MESSAGES, and the runtime default is the union, so nothing
changes for anyone who does not ask.
Requires Node ^22.19 || >=24. There are no runtime dependencies.
Licence
MIT.