@cruxy/cli 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/agent/loop.js +16 -1
- package/dist/agent/session.js +62 -9
- package/dist/approval/classify.js +170 -40
- package/dist/approval/prompt.js +52 -6
- package/dist/approval/service.js +1 -11
- package/dist/budget/session-budget.js +10 -1
- package/dist/checkpoint/coverage.js +147 -4
- package/dist/cli/commands/limits.js +76 -0
- package/dist/cli/commands/pr.js +10 -1
- package/dist/cli/commands/rollback.js +10 -2
- package/dist/cli/commands/run.js +19 -3
- package/dist/cli/commands/sessions.js +156 -0
- package/dist/cli/program.js +4 -0
- package/dist/cli/repl.js +25 -0
- package/dist/cli/session-factory.js +31 -10
- package/dist/config/schema.js +141 -9
- package/dist/constants.js +12 -2
- package/dist/errors/constructors.js +49 -44
- package/dist/jobs/manager.js +269 -17
- package/dist/mcp/client.js +16 -0
- package/dist/render/limits-report.js +213 -0
- package/dist/render/limits-view.js +125 -0
- package/dist/sandbox/service.js +9 -0
- package/dist/sandbox/types.js +15 -0
- package/dist/session/index.js +3 -1
- package/dist/session/list.js +20 -6
- package/dist/session/log.js +120 -21
- package/dist/session/prune.js +106 -0
- package/dist/session/resume.js +5 -0
- package/dist/subagent/orchestrator.js +71 -31
- package/dist/subagent/spawn-tool.js +11 -4
- package/dist/tools/schema-depth.js +18 -0
- package/dist/tui/limits-panel.js +53 -30
- package/dist/usage/collect.js +20 -1
- package/dist/usage/summary.js +48 -1
- package/dist/usage/types.js +27 -0
- package/package.json +2 -2
package/dist/config/schema.js
CHANGED
|
@@ -2,7 +2,24 @@ import { z } from "zod";
|
|
|
2
2
|
import { LOG_LEVELS } from "../utils/logger.js";
|
|
3
3
|
import { MODEL_TIERS } from "../brand/voice.js";
|
|
4
4
|
import { TASK_CLASSES } from "../routing/types.js";
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The backends a session can run on. ONE, deliberately.
|
|
7
|
+
*
|
|
8
|
+
* This used to be `["cruxy", "openai", "custom"]`. Neither extra value could
|
|
9
|
+
* ever produce a working session: `createProvider` implements `cruxy` and
|
|
10
|
+
* throws {@link NotImplementedError} for anything else, so `provider: "openai"`
|
|
11
|
+
* passed config validation and then failed at session start — a promise the
|
|
12
|
+
* schema made and the SDK refused. `OpenAICompatibleProvider`, the only code
|
|
13
|
+
* that could have honoured it, was removed in #260; "reserved for the future"
|
|
14
|
+
* is not a claim this enum can support when the implementation it named is
|
|
15
|
+
* gone.
|
|
16
|
+
*
|
|
17
|
+
* Rejecting them HERE is the point: a bad provider is now a config-load error
|
|
18
|
+
* that names the valid values, instead of a NotImplementedError thrown after
|
|
19
|
+
* onboarding has already run. Adding a backend means adding the value back
|
|
20
|
+
* together with the provider that serves it — never ahead of it.
|
|
21
|
+
*/
|
|
22
|
+
export const ProviderSchema = z.enum(["cruxy"]);
|
|
6
23
|
export const ModelConfigSchema = z
|
|
7
24
|
.object({
|
|
8
25
|
provider: ProviderSchema.default("cruxy"),
|
|
@@ -220,6 +237,73 @@ export const CheckpointConfigSchema = z
|
|
|
220
237
|
retention: z.number().int().positive().default(10),
|
|
221
238
|
})
|
|
222
239
|
.strict();
|
|
240
|
+
/**
|
|
241
|
+
* The floor under `sessions.retention`.
|
|
242
|
+
*
|
|
243
|
+
* The `--resume` picker (`PICKER_LIMIT`) and the TUI sidebar
|
|
244
|
+
* (`SIDEBAR_SESSIONS`) each offer ten rows. A retention below that would let
|
|
245
|
+
* both surfaces list sessions a later prune has already decided are
|
|
246
|
+
* expendable — offering a row and then deleting it is worse than never showing
|
|
247
|
+
* it. `session/retention-floor.test.ts` pins this against the two constants
|
|
248
|
+
* themselves, so raising either surface without raising the floor fails.
|
|
249
|
+
*
|
|
250
|
+
* Rejected rather than silently raised: a config that says 5 and behaves as 10
|
|
251
|
+
* is a config that lies, and `loadConfig` already treats a bad value as a hard
|
|
252
|
+
* error everywhere else.
|
|
253
|
+
*/
|
|
254
|
+
export const SESSION_RETENTION_FLOOR = 10;
|
|
255
|
+
/**
|
|
256
|
+
* Session persistence and retention (#257).
|
|
257
|
+
*
|
|
258
|
+
* The stores either side of this one have always bounded themselves —
|
|
259
|
+
* checkpoints at 10, run history at 50 — and sessions did not. Every session
|
|
260
|
+
* file ever written was still on disk, and nothing could turn recording off.
|
|
261
|
+
*
|
|
262
|
+
* WHY AGE IS THE PRIMARY BOUND AND COUNT IS THE BACKSTOP. "Old sessions I will
|
|
263
|
+
* never resume" is how people actually think about this, and once a session
|
|
264
|
+
* that recorded nothing stops being written at all (the lazy-meta change that
|
|
265
|
+
* had to land first), every remaining file is a real conversation for which age
|
|
266
|
+
* is the honest signal. The count cap stays because age alone does not protect
|
|
267
|
+
* a directory where someone runs forty sessions a day.
|
|
268
|
+
*
|
|
269
|
+
* WHY NOT BYTES. `statSync` hands us the size for free, so cost is not the
|
|
270
|
+
* objection — meaning is. Pruning on bytes makes whether YOUR session survives
|
|
271
|
+
* depend on how chatty an unrelated one was, which is not a rule anyone can
|
|
272
|
+
* hold in their head. `cruxy sessions` reports bytes, because seeing where the
|
|
273
|
+
* disk went is exactly what a report is for; nothing prunes on them.
|
|
274
|
+
*
|
|
275
|
+
* PER PROJECT, matching `projectDir` and how listing already works. A global
|
|
276
|
+
* cap would need the cross-project scan #172 item 2 declined.
|
|
277
|
+
*/
|
|
278
|
+
export const SessionsConfigSchema = z
|
|
279
|
+
.object({
|
|
280
|
+
/**
|
|
281
|
+
* Master switch. When false nothing is recorded: no file, no `--resume`,
|
|
282
|
+
* no sidebar. The session runs entirely in memory.
|
|
283
|
+
*/
|
|
284
|
+
enabled: z.boolean().default(true),
|
|
285
|
+
/**
|
|
286
|
+
* How many sessions to keep per project; older ones are pruned oldest-first.
|
|
287
|
+
* Never below {@link SESSION_RETENTION_FLOOR} — see the note there.
|
|
288
|
+
*/
|
|
289
|
+
retention: z
|
|
290
|
+
.number()
|
|
291
|
+
.int()
|
|
292
|
+
.min(SESSION_RETENTION_FLOOR, {
|
|
293
|
+
message: `sessions.retention must be at least ${SESSION_RETENTION_FLOOR} — ` +
|
|
294
|
+
`the --resume picker and the TUI sidebar both offer that many rows, and ` +
|
|
295
|
+
`a lower retention would list sessions that are already marked for deletion`,
|
|
296
|
+
})
|
|
297
|
+
.default(50),
|
|
298
|
+
/**
|
|
299
|
+
* Prune sessions untouched for this many days. Measured on the file's
|
|
300
|
+
* MTIME, never on `meta.startedAt`: a conversation begun forty days ago and
|
|
301
|
+
* resumed this morning is live, and an age check on when it BEGAN would
|
|
302
|
+
* delete it out from under the user.
|
|
303
|
+
*/
|
|
304
|
+
maxAgeDays: z.number().int().positive().default(30),
|
|
305
|
+
})
|
|
306
|
+
.strict();
|
|
223
307
|
/**
|
|
224
308
|
* Test-execution loop (C.13): how the agent runs the project's test suite and
|
|
225
309
|
* iterates on failures. The command is detected from package.json when unset;
|
|
@@ -517,7 +601,11 @@ export const UsageConfigSchema = z
|
|
|
517
601
|
* arguments over the network (https + cert-validated + pinned to a public IP; http
|
|
518
602
|
* only for a loopback dev server) and treats its responses as untrusted data. A
|
|
519
603
|
* url's trust also binds its resolved IP set at trust time, so a later IP-set
|
|
520
|
-
* change re-gates
|
|
604
|
+
* change re-gates. NEITHER transport's fingerprint covers the server's CODE —
|
|
605
|
+
* both hash the invocation (command/args/env, url, credential ref, header names),
|
|
606
|
+
* so a trusted server that later ships different code is still trusted. Trust
|
|
607
|
+
* means "I accept running this", not "this is still what I trusted"; see
|
|
608
|
+
* `mcp/types.ts`.
|
|
521
609
|
*/
|
|
522
610
|
export const McpServerSchema = z
|
|
523
611
|
.object({
|
|
@@ -577,6 +665,40 @@ function isHttps(url) {
|
|
|
577
665
|
* model names scrubbed, and results are NEVER persisted. The tool list a server
|
|
578
666
|
* advertises is bounded (count + per-tool description/schema size) so a hostile
|
|
579
667
|
* server can't blow the context budget.
|
|
668
|
+
*
|
|
669
|
+
* THE THREE BOUNDS HAVE CEILINGS OF THEIR OWN (#237). Each was `positive()` with
|
|
670
|
+
* no maximum, which made the protection exactly as good as the number the user
|
|
671
|
+
* typed: `maxSchemaBytes: 100000000` validated cleanly, and a cap whose docstring
|
|
672
|
+
* says a hostile server "can't flood context" then does not do that. The failure
|
|
673
|
+
* moved downstream to a provider request-size rejection or a blown context
|
|
674
|
+
* budget — a much worse place to find out than config load, where the answer is
|
|
675
|
+
* one line.
|
|
676
|
+
*
|
|
677
|
+
* These ceilings are a SANITY BOUND, NOT A PROTOCOL LIMIT, and the difference
|
|
678
|
+
* matters because `MAX_SCHEMA_DEPTH` (`tools/schema-depth.ts`) bounds the same
|
|
679
|
+
* schemas and is the opposite kind of number. That one mirrors a real gateway
|
|
680
|
+
* constraint: exceed it and the
|
|
681
|
+
* whole request fails, so it is not ours to choose and must never be raised to
|
|
682
|
+
* make a config work. These three mirror nothing upstream — no external validator
|
|
683
|
+
* enforces them and no request breaks at 65 KiB. They are a statement about what
|
|
684
|
+
* configuration is worth honoring, so they are picked to be obviously generous
|
|
685
|
+
* (8x, 16x, and 8x the shipped defaults) rather than tight. Different numbers in
|
|
686
|
+
* the same spirit would be just as correct; having no number at all was not.
|
|
687
|
+
*
|
|
688
|
+
* Rejected rather than silently clamped, on `SESSION_RETENTION_FLOOR`'s rule: a
|
|
689
|
+
* config that says 100000000 and behaves as 65536 is a config that lies. The cost
|
|
690
|
+
* is that an over-ceiling value is a hard `loadConfig` error, and the schema is
|
|
691
|
+
* `.strict()`, so it fails the whole CLI rather than just MCP. That is affordable
|
|
692
|
+
* HERE and would not be everywhere: these keys are documented nowhere, are not
|
|
693
|
+
* env-settable, and `initConfig` writes the defaults — so every generated config
|
|
694
|
+
* passes, and `setValue` rejects an over-ceiling `cruxy config set` at the write
|
|
695
|
+
* with the path and the bound named. Contrast `UsageConfigSchema`, which refuses
|
|
696
|
+
* the same hard error for the opposite reason: those keys were a documented
|
|
697
|
+
* feature people really had set, so rejecting them would hand a working user a
|
|
698
|
+
* CLI that will not start.
|
|
699
|
+
*
|
|
700
|
+
* The two timeouts above are deliberately left unbounded: a long timeout costs
|
|
701
|
+
* patience rather than memory, and a user may have a legitimately slow server.
|
|
580
702
|
*/
|
|
581
703
|
export const McpConfigSchema = z
|
|
582
704
|
.object({
|
|
@@ -592,15 +714,24 @@ export const McpConfigSchema = z
|
|
|
592
714
|
/** Fail a single `tools/call` if the server does not respond within this many
|
|
593
715
|
* ms — the connection is kept, only the one call errors. */
|
|
594
716
|
requestTimeout: z.number().int().positive().default(30000),
|
|
595
|
-
/**
|
|
596
|
-
*
|
|
597
|
-
|
|
717
|
+
/**
|
|
718
|
+
* Max tools accepted from ONE server; extras are dropped with a visible note
|
|
719
|
+
* (a hostile server can't advertise thousands of tools to flood context).
|
|
720
|
+
*
|
|
721
|
+
* PER SERVER, which is not the number that decides the context budget. Ten
|
|
722
|
+
* trusted servers at the default advertise up to 320 tools between them, and
|
|
723
|
+
* nothing caps that sum — this bound stops one server flooding the list, not
|
|
724
|
+
* a large `servers` map adding up. Configuring many servers is a deliberate
|
|
725
|
+
* act with a visible cost, so it is bounded by the user rather than here.
|
|
726
|
+
*/
|
|
727
|
+
maxToolsPerServer: z.number().int().positive().max(256).default(32),
|
|
598
728
|
/** Max characters kept from a single tool's description; the rest is truncated
|
|
599
|
-
* with a visible marker. */
|
|
600
|
-
maxDescriptionChars: z.number().int().positive().default(1024),
|
|
729
|
+
* with a visible marker. A description past the 16 KiB ceiling is not one. */
|
|
730
|
+
maxDescriptionChars: z.number().int().positive().max(16_384).default(1024),
|
|
601
731
|
/** Max bytes kept from a single tool's advertised JSON input schema; an
|
|
602
|
-
* over-cap schema is replaced with a permissive one and a visible note.
|
|
603
|
-
|
|
732
|
+
* over-cap schema is replaced with a permissive one and a visible note. One
|
|
733
|
+
* tool's schema at the 64 KiB ceiling is already ~16k tokens of context. */
|
|
734
|
+
maxSchemaBytes: z.number().int().positive().max(65_536).default(8192),
|
|
604
735
|
})
|
|
605
736
|
.strict();
|
|
606
737
|
/**
|
|
@@ -660,6 +791,7 @@ export const CruxyConfigSchema = z
|
|
|
660
791
|
index: IndexConfigSchema.default({}),
|
|
661
792
|
lsp: LspConfigSchema.default({}),
|
|
662
793
|
checkpoint: CheckpointConfigSchema.default({}),
|
|
794
|
+
sessions: SessionsConfigSchema.default({}),
|
|
663
795
|
subagent: SubagentConfigSchema.default({}),
|
|
664
796
|
jobs: JobsConfigSchema.default({}),
|
|
665
797
|
test: TestConfigSchema.default({}),
|
package/dist/constants.js
CHANGED
|
@@ -26,8 +26,18 @@ export const CONFIG_FILE_NAME = "config.json";
|
|
|
26
26
|
export const CREDENTIALS_FILE_NAME = "credentials.json";
|
|
27
27
|
/** Onboarding state + completion marker under the global dir (U.6). */
|
|
28
28
|
export const ONBOARDING_FILE_NAME = "onboarding.json";
|
|
29
|
-
/**
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Where a user creates a Cruxy gateway key (printed during onboarding).
|
|
31
|
+
*
|
|
32
|
+
* `cruxy.ai`, not `app.cruxy.in` — the app moved and the old host now only
|
|
33
|
+
* `308`s here. Printed verbatim into the terminal, where a redirect buys
|
|
34
|
+
* nothing: a URL a user copies by hand or reads aloud should be the one that
|
|
35
|
+
* serves the page, and this one outlived the redirect that was covering for it.
|
|
36
|
+
*
|
|
37
|
+
* Deliberately NOT `api.cruxy.in`. The gateway did not move, and nothing in
|
|
38
|
+
* this package that names it should be changed alongside this.
|
|
39
|
+
*/
|
|
40
|
+
export const CREATE_KEY_URL = "https://cruxy.ai";
|
|
31
41
|
/** Project-level config filenames, checked in order. */
|
|
32
42
|
export const PROJECT_CONFIG_FILENAMES = [
|
|
33
43
|
"cruxy.config.json",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ApiError, AuthError, BudgetExhaustedError, InvalidRequestError, NetworkError, OverloadedError, RateLimitError, } from "@cruxy/sdk";
|
|
1
|
+
import { ApiError, AuthError, BudgetExhaustedError, InvalidRequestError, NetworkError, OverloadedError, RateLimitError, ToolSchemaRejectedError, } from "@cruxy/sdk";
|
|
2
2
|
import { scrubModelNames } from "../brand/index.js";
|
|
3
3
|
// Imported from the leaf module DIRECTLY, not via `config/index.js`:
|
|
4
4
|
// `config/credentials.ts` imports this file, so going through the barrel would
|
|
@@ -245,16 +245,32 @@ export function apiError(underlying) {
|
|
|
245
245
|
* moment", and they point at the issue tracker with a code to quote, because a
|
|
246
246
|
* report is the one action that actually moves this forward.
|
|
247
247
|
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
248
|
+
* WHEN A TOOL IS AT FAULT the gateway now says so in fields rather than in
|
|
249
|
+
* prose. A schema over one of its caps answers `invalid_tool_schema` carrying
|
|
250
|
+
* `tool`, `bound` and `limit`, which the SDK reads onto
|
|
251
|
+
* {@link ToolSchemaRejectedError} — so the tool name, the cap that was hit and
|
|
252
|
+
* the number to fit under are all read off the structured half of a contract
|
|
253
|
+
* whose `code` MAY NOT change, rather than parsed back out of English that MAY.
|
|
254
|
+
*
|
|
255
|
+
* That name is the single most useful token in the whole error: it turns "cruxy
|
|
256
|
+
* is broken" into a filed issue someone can act on, and `bound`/`limit` turn the
|
|
257
|
+
* report into one a fix can start from. All three ride in `meta` for exactly
|
|
258
|
+
* that reason, and all three are optional — the gateway omits them when no cap
|
|
259
|
+
* was reached, and an older one omits them entirely.
|
|
260
|
+
*
|
|
261
|
+
* The name is still passed through the model-name scrub before it is shown. It
|
|
262
|
+
* is our own harness's function name and cannot plausibly be an upstream model
|
|
263
|
+
* id, but the U.8 gag holding BY CONSTRUCTION on every path out of here is worth
|
|
264
|
+
* more than the one call it costs — this used to be true only because the name
|
|
265
|
+
* was cut out of an already-scrubbed string.
|
|
253
266
|
*/
|
|
254
267
|
export function apiRequestRejected(underlying) {
|
|
255
268
|
const status = underlying instanceof ApiError ? underlying.status : undefined;
|
|
256
269
|
const cause = scrubbedMessageOf(underlying);
|
|
257
|
-
const
|
|
270
|
+
const rejected = underlying instanceof ToolSchemaRejectedError ? underlying : undefined;
|
|
271
|
+
const tool = rejected?.tool === undefined ? undefined : scrubModelNames(rejected.tool);
|
|
272
|
+
const bound = rejected?.bound;
|
|
273
|
+
const limit = rejected?.limit;
|
|
258
274
|
return new CruxyError({
|
|
259
275
|
code: ErrorCode.ApiRequestRejected,
|
|
260
276
|
title: tool
|
|
@@ -273,46 +289,11 @@ export function apiRequestRejected(underlying) {
|
|
|
273
289
|
meta: {
|
|
274
290
|
...(status !== undefined ? { status } : {}),
|
|
275
291
|
...(tool !== undefined ? { tool } : {}),
|
|
292
|
+
...(bound !== undefined ? { bound } : {}),
|
|
293
|
+
...(limit !== undefined ? { limit } : {}),
|
|
276
294
|
},
|
|
277
295
|
});
|
|
278
296
|
}
|
|
279
|
-
/**
|
|
280
|
-
* The tool name in a gateway rejection, if it named one.
|
|
281
|
-
*
|
|
282
|
-
* The gateway's tool-schema validator prefixes its complaint with the offending
|
|
283
|
-
* function — `tool "apply_patch": parameters nests deeper than 8 levels` — so
|
|
284
|
-
* one quoted token after the word `tool` is the whole pattern. Anything else
|
|
285
|
-
* yields `undefined` and the caller falls back to generic wording: a WRONG tool
|
|
286
|
-
* name in a bug report is worse than none, so this never guesses.
|
|
287
|
-
*
|
|
288
|
-
* ── TEMPORARY COUPLING, AND IT IS THE WRONG KIND ────────────────────────────
|
|
289
|
-
*
|
|
290
|
-
* This reads the gateway's `error` MESSAGE, and the gateway's own contract
|
|
291
|
-
* (cruxy-ai/api, `internal/httpx/errcode.go`) says the message MAY change while
|
|
292
|
-
* the `code` MAY NOT. So this parses the half that is explicitly allowed to move
|
|
293
|
-
* under us — the exact coupling the code/message split exists to prevent.
|
|
294
|
-
*
|
|
295
|
-
* It is deliberate and bounded: today the code is the generic `invalid_request`,
|
|
296
|
-
* shared with bad JSON, a missing field and an unknown model, so the message is
|
|
297
|
-
* the ONLY thing distinguishing "this build's tool harness is permanently
|
|
298
|
-
* unusable" from "this one request was malformed". The tool name is the single
|
|
299
|
-
* most actionable token in the error and it is worth having; a regex that fails
|
|
300
|
-
* closed is the cheapest way to have it.
|
|
301
|
-
*
|
|
302
|
-
* Failing closed is what makes the risk acceptable. If the gateway rewords, this
|
|
303
|
-
* returns `undefined`, the caller drops to generic wording, and the error is
|
|
304
|
-
* still correct — less specific, never wrong. Nothing downstream branches on it.
|
|
305
|
-
*
|
|
306
|
-
* The real fix is server-side and filed as cruxy-ai/api#183: a distinct 400 code
|
|
307
|
-
* for harness-bound rejections (the `invalid_schema` precedent already exists
|
|
308
|
-
* for `response_format`), with the tool name as a STRUCTURED FIELD rather than a
|
|
309
|
-
* message prefix. When that lands, match on the code, read the field, and delete
|
|
310
|
-
* this function — do not "improve" the regex.
|
|
311
|
-
*/
|
|
312
|
-
function toolNamedIn(message) {
|
|
313
|
-
const match = /\btool "([^"]+)"/.exec(message ?? "");
|
|
314
|
-
return match?.[1];
|
|
315
|
-
}
|
|
316
297
|
export function apiRateLimit(underlying) {
|
|
317
298
|
const retryAfterMs = underlying instanceof RateLimitError ? underlying.retryAfterMs : undefined;
|
|
318
299
|
return new CruxyError({
|
|
@@ -1604,3 +1585,27 @@ export function classifyProviderError(underlying, ctx = {}) {
|
|
|
1604
1585
|
return apiError(underlying);
|
|
1605
1586
|
return null;
|
|
1606
1587
|
}
|
|
1588
|
+
/**
|
|
1589
|
+
* The typed pool denial behind an error, or `null` if it is not one.
|
|
1590
|
+
*
|
|
1591
|
+
* LIVES HERE BECAUSE IT HAS TWO CALLERS (cli#245). It was written for the
|
|
1592
|
+
* fan-out seam (cli#243) and is needed byte-for-byte by the background-job
|
|
1593
|
+
* executor, which flattened the same 429 into a message string and lost
|
|
1594
|
+
* `window`, `resetAt` and `miraAvailable` — the three fields that make a denial
|
|
1595
|
+
* actionable, and the last of which is the only one that unblocks someone now.
|
|
1596
|
+
* A second copy of this in `jobs/` would be two places that decide what a pool
|
|
1597
|
+
* denial is, and they would disagree the first time the SDK grows a class.
|
|
1598
|
+
*
|
|
1599
|
+
* Two shapes reach here and both are the same fact: the raw SDK
|
|
1600
|
+
* `BudgetExhaustedError` from the run's own request, and — when a caller that
|
|
1601
|
+
* already converted one re-throws — the {@link CruxyError} it produced. Mapping
|
|
1602
|
+
* goes through {@link classifyProviderError} so there is still ONE place that
|
|
1603
|
+
* knows which SDK class means what.
|
|
1604
|
+
*/
|
|
1605
|
+
export function poolDenial(err) {
|
|
1606
|
+
if (CruxyError.is(err)) {
|
|
1607
|
+
return err.code === ErrorCode.BudgetExhausted ? err : null;
|
|
1608
|
+
}
|
|
1609
|
+
const typed = classifyProviderError(err);
|
|
1610
|
+
return typed?.code === ErrorCode.BudgetExhausted ? typed : null;
|
|
1611
|
+
}
|