switchroom 0.19.26 β†’ 0.19.27

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.
Files changed (77) hide show
  1. package/bin/git-agent-attribution-hook.sh +144 -0
  2. package/dist/agent-scheduler/index.js +55 -0
  3. package/dist/auth-broker/index.js +116 -6
  4. package/dist/cli/notion-write-pretool.mjs +55 -0
  5. package/dist/cli/switchroom.js +2055 -1193
  6. package/dist/host-control/main.js +117 -7
  7. package/dist/vault/approvals/kernel-server.js +114 -6
  8. package/dist/vault/broker/server.js +114 -6
  9. package/package.json +6 -2
  10. package/profiles/_base/cron-session.sh.hbs +8 -0
  11. package/profiles/_base/start.sh.hbs +105 -0
  12. package/telegram-plugin/card-layout.ts +328 -0
  13. package/telegram-plugin/dist/bridge/bridge.js +93 -1
  14. package/telegram-plugin/dist/gateway/gateway.js +2009 -1166
  15. package/telegram-plugin/dist/server.js +96 -1
  16. package/telegram-plugin/edit-flood-fuse.ts +637 -56
  17. package/telegram-plugin/flood-429-ledger.ts +526 -0
  18. package/telegram-plugin/flood-circuit-breaker.ts +18 -0
  19. package/telegram-plugin/gateway/flood-reply-queue.ts +168 -0
  20. package/telegram-plugin/gateway/gateway.ts +58 -68
  21. package/telegram-plugin/gateway/narrative-lane.ts +14 -0
  22. package/telegram-plugin/gateway/outbound-send-path.ts +36 -0
  23. package/telegram-plugin/gateway/outbox-sweep.ts +183 -6
  24. package/telegram-plugin/gateway/pinned-message-handler.ts +12 -16
  25. package/telegram-plugin/gateway/status-pin-retarget.ts +72 -36
  26. package/telegram-plugin/gateway/status-pin-store.ts +58 -9
  27. package/telegram-plugin/gateway/worker-pin-reaper.ts +56 -7
  28. package/telegram-plugin/llm-error-present.ts +61 -2
  29. package/telegram-plugin/model-unavailable.ts +8 -0
  30. package/telegram-plugin/operator-events.ts +72 -5
  31. package/telegram-plugin/outbound-class.ts +81 -0
  32. package/telegram-plugin/provider-credit.ts +237 -0
  33. package/telegram-plugin/scripts/bun-test-ci.sh +36 -6
  34. package/telegram-plugin/send-gate.ts +24 -2
  35. package/telegram-plugin/status-no-truncate.ts +10 -48
  36. package/telegram-plugin/status-pin-driver.ts +33 -45
  37. package/telegram-plugin/status-pin.ts +18 -1
  38. package/telegram-plugin/tests/card-golden.test.ts +69 -0
  39. package/telegram-plugin/tests/card-lifecycle-render.test.ts +362 -0
  40. package/telegram-plugin/tests/card-type-distinguishability.test.ts +187 -164
  41. package/telegram-plugin/tests/card-variants.golden.txt +211 -0
  42. package/telegram-plugin/tests/card-variants.ts +366 -0
  43. package/telegram-plugin/tests/edit-flood-fuse-ban-awareness.test.ts +316 -0
  44. package/telegram-plugin/tests/edit-flood-fuse-default-deny.test.ts +319 -0
  45. package/telegram-plugin/tests/edit-flood-fuse.test.ts +11 -2
  46. package/telegram-plugin/tests/feed-edit-rate-ceiling.test.ts +462 -0
  47. package/telegram-plugin/tests/fixtures/real-429-stream.ts +220 -0
  48. package/telegram-plugin/tests/flood-429-ledger.test.ts +278 -0
  49. package/telegram-plugin/tests/flood-429-recorder-wiring.test.ts +128 -0
  50. package/telegram-plugin/tests/flood-reply-queue.test.ts +418 -0
  51. package/telegram-plugin/tests/outbox-sweep-flood-breaker.test.ts +221 -0
  52. package/telegram-plugin/tests/pinned-card-collapse.test.ts +19 -24
  53. package/telegram-plugin/tests/pinned-message-handler.test.ts +15 -15
  54. package/telegram-plugin/tests/provider-credit-402.test.ts +243 -0
  55. package/telegram-plugin/tests/status-pin-api.test.ts +11 -11
  56. package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +36 -37
  57. package/telegram-plugin/tests/status-pin-lifecycle.test.ts +602 -0
  58. package/telegram-plugin/tests/status-pin-retarget.test.ts +90 -62
  59. package/telegram-plugin/tests/status-pin-service-message-suppression.test.ts +7 -3
  60. package/telegram-plugin/tests/status-pin-store.test.ts +109 -60
  61. package/telegram-plugin/tests/status-pin.test.ts +56 -5
  62. package/telegram-plugin/tests/test-runner-coverage.test.ts +133 -0
  63. package/telegram-plugin/tests/worker-activity-feed.test.ts +12 -10
  64. package/telegram-plugin/tests/worker-feed-coalesce.test.ts +23 -29
  65. package/telegram-plugin/tests/worker-feed-pin-persistence.test.ts +56 -59
  66. package/telegram-plugin/tests/worker-feed-terminal-edit-class.test.ts +335 -0
  67. package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +1 -1
  68. package/telegram-plugin/tool-activity-summary.ts +239 -365
  69. package/telegram-plugin/uat/assertions.ts +22 -11
  70. package/telegram-plugin/uat/feed-matcher.test.ts +24 -17
  71. package/telegram-plugin/worker-activity-feed.ts +105 -47
  72. package/vendor/hindsight-memory/CLAUDE.md +45 -0
  73. package/vendor/hindsight-memory/scripts/lib/config.py +33 -0
  74. package/vendor/hindsight-memory/scripts/recall.py +176 -7
  75. package/vendor/hindsight-memory/scripts/tests/test_config_recall_passthrough_env.py +170 -0
  76. package/vendor/hindsight-memory/scripts/tests/test_recall_min_score.py +464 -0
  77. package/vendor/hindsight-memory/settings.json +1 -1
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Card layout core β€” the ONE place a status/progress card is composed.
3
+ *
4
+ * Every card the gateway paints (πŸ€– agent, πŸ›  single worker, πŸ›  WORKERS
5
+ * combined, and their overflow/terminal variants) is expressed as a
6
+ * {@link CardSpec}: fixed `chrome` lines, N `sections` (each an optional
7
+ * header plus a windowed `βœ“`/`β†’` step trail), and fixed `footer` lines. The
8
+ * renderers in `tool-activity-summary.ts` / `worker-activity-feed.ts` are
9
+ * CONFIGURATIONS of this module, not parallel implementations.
10
+ *
11
+ * Two things used to exist twice and agreed only by hand (#3844):
12
+ *
13
+ * 1. **Header composition.** The combined card built its own row header
14
+ * instead of going through `renderActivityHeader`, so the metrics run
15
+ * (`{elapsed} Β· {n} tools Β· {t} tok Β· {model}`) was written out in two
16
+ * places. It is now composed exactly once, by {@link metricsRun}, which
17
+ * the agent header, the worker header, the combined row header and the
18
+ * combined glance line all read.
19
+ * 2. **Char-budget enforcement.** The combined card carried its own
20
+ * row-shrinking loop instead of the status card's `fitCardToBudget`.
21
+ * There is now exactly one budget enforcer β€” {@link fitCardToBudget} β€”
22
+ * a generic "render at progressively deeper shrink levels, take the
23
+ * first that fits" loop. Each card supplies what a shrink level MEANS
24
+ * (drop the oldest bullet / drop the newest row); nothing else compares
25
+ * a rendered card against `STATUS_CARD_CHAR_BUDGET`.
26
+ *
27
+ * Nothing in here knows about workers, turns, or Telegram transport β€” it is
28
+ * pure string composition, so a card variant can be rendered (and asserted)
29
+ * without any of the feed machinery.
30
+ */
31
+
32
+ import { escapeMarkdown, stripMarkdown, truncate, stackCardLines } from './card-format.js'
33
+ import { formatModelLabel } from './model-label.js'
34
+ import { STATUS_CARD_CHAR_BUDGET, STATUS_LINE_MAX, STATUS_ROLLING_LINES } from './status-no-truncate.js'
35
+
36
+ // ─── Metrics composition (the single header vocabulary) ─────────────────────
37
+
38
+ /** Format elapsed milliseconds for display in the activity header (e.g. "12s", "2m05s"). */
39
+ export function formatFeedElapsed(ms: number): string {
40
+ const s = Math.floor(ms / 1000)
41
+ if (s < 60) return `${s}s`
42
+ const m = Math.floor(s / 60)
43
+ return `${m}m${(s % 60).toString().padStart(2, '0')}s`
44
+ }
45
+
46
+ /**
47
+ * Compact token-count formatter for the activity card's metrics line:
48
+ * <1000 β†’ raw ("940")
49
+ * β‰₯1000, <1e6 β†’ one-decimal k ("12.4k", "1.0k")
50
+ * β‰₯1e6 β†’ one-decimal M ("1.2M")
51
+ * Negative / non-finite inputs clamp to "0". The caller appends " tok".
52
+ */
53
+ export function formatTokenCount(n: number): string {
54
+ if (!Number.isFinite(n) || n <= 0) return '0'
55
+ if (n < 1000) return String(Math.floor(n))
56
+ if (n < 1_000_000) {
57
+ // Round to the displayed 1-decimal k FIRST: inputs in [999_950, 999_999]
58
+ // round to "1000.0k", which must promote into the M branch rather than
59
+ // render a nonsense "1000.0k". Fall through when the rounded k reaches 1000.
60
+ const k = Number((n / 1000).toFixed(1))
61
+ if (k < 1000) return `${k.toFixed(1)}k`
62
+ }
63
+ return `${(n / 1_000_000).toFixed(1)}M`
64
+ }
65
+
66
+ /**
67
+ * The ` Β· {N} tok` metrics segment, or '' when there are no tokens to show.
68
+ * A 0 / undefined total (a worker that emitted no usage β€” e.g. a non-Claude
69
+ * transcript) OMITS the segment entirely so the line stays clean; every card
70
+ * that shows tokens reads this one predicate so they never diverge.
71
+ */
72
+ export function tokenSegment(totalTokens: number | undefined): string {
73
+ if (totalTokens == null || totalTokens <= 0) return ''
74
+ return ` Β· ${formatTokenCount(totalTokens)} tok`
75
+ }
76
+
77
+ /** `tool` / `tools`, pluralised once for every surface that prints a tool count. */
78
+ export function toolWord(n: number): string {
79
+ return n === 1 ? 'tool' : 'tools'
80
+ }
81
+
82
+ /** Terminal/running state a card (or a row on it) can report. */
83
+ export type CardState = 'running' | 'done' | 'failed' | 'incomplete'
84
+
85
+ /**
86
+ * The metrics RUN β€” the dot-separated `{elapsed} Β· {n} tools Β· {t} tok Β· {model}`
87
+ * text that every card header carries, WITHOUT the italic wrapper (callers own
88
+ * the wrapper because they place the leading separator differently).
89
+ *
90
+ * This is the single composition point the audit (#3844) found forked: the
91
+ * combined card's row header spelled the same run out by hand, so a change to
92
+ * (say) token placement had to be made twice and agreed only by inspection.
93
+ *
94
+ * `running` puts elapsed FIRST (it is the live fact); a terminal state leads
95
+ * with the state word and moves elapsed to the tail, so a failed or reaped
96
+ * worker never reads as done.
97
+ */
98
+ export function metricsRun(
99
+ elapsedMs: number,
100
+ toolCount: number,
101
+ state: CardState,
102
+ totalTokens?: number,
103
+ model?: string,
104
+ ): string {
105
+ const elapsed = formatFeedElapsed(elapsedMs)
106
+ const tokPart = tokenSegment(totalTokens)
107
+ const modelLabel = formatModelLabel(model)
108
+ const modelPart = modelLabel != null ? ` Β· ${escapeMarkdown(modelLabel)}` : ''
109
+ return state === 'running'
110
+ ? `${elapsed} Β· ${toolCount} ${toolWord(toolCount)}${tokPart}${modelPart}`
111
+ : `${state} Β· ${toolCount} ${toolWord(toolCount)}${tokPart} Β· ${elapsed}${modelPart}`
112
+ }
113
+
114
+ /**
115
+ * Line 1 of a card: `<emoji> <b>LABEL</b> Β· <i>description</i>` (description
116
+ * optional). Shared by the agent card, the worker card and the static
117
+ * superseded notice, so the card's type chrome is composed in one place.
118
+ */
119
+ export function renderCardTitleLine(emoji: string, label: string, description: string): string {
120
+ const descPart = description.length > 0 ? ` Β· _${escapeMarkdown(description)}_` : ''
121
+ return `${emoji} **${escapeMarkdown(label)}**${descPart}`
122
+ }
123
+
124
+ /**
125
+ * Render a two-line header for an activity card.
126
+ *
127
+ * Line 1: `<emoji> <b>Label</b> Β· <i>description</i>` (description optional)
128
+ * Line 2: the metrics run, italicised.
129
+ *
130
+ * Returns a two-element array of ready Telegram markdown lines (no trailing
131
+ * newline).
132
+ */
133
+ export function renderActivityHeader(
134
+ emoji: string,
135
+ label: string,
136
+ description: string,
137
+ elapsedMs: number,
138
+ toolCount: number,
139
+ state: CardState,
140
+ model?: string,
141
+ totalTokens?: number,
142
+ ): [string, string] {
143
+ return [
144
+ renderCardTitleLine(emoji, label, description),
145
+ `_${metricsRun(elapsedMs, toolCount, state, totalTokens, model)}_`,
146
+ ]
147
+ }
148
+
149
+ // ─── Per-line truncation pipeline ───────────────────────────────────────────
150
+ //
151
+ // Per RAW line, in this EXACT order:
152
+ // 1. stripMarkdown(raw)
153
+ // 2. .replace(/\s+/g, ' ').trim()
154
+ // 3. truncate(_, STATUS_LINE_MAX)
155
+ // 4. escapeMarkdown(_) ← escape is ALWAYS the last per-line op.
156
+ // Escaping last is load-bearing: clipping an already-escaped string can split
157
+ // a markdown escape (\* β†’ \), which renders wrong.
158
+
159
+ /** Clean + clip + escape a single raw step line. Returns ready-to-wrap markdown. */
160
+ export function escapeStepLine(raw: string): string {
161
+ const cleaned = stripMarkdown(raw).replace(/\s+/g, ' ').trim()
162
+ return escapeMarkdown(truncate(cleaned, STATUS_LINE_MAX))
163
+ }
164
+
165
+ /** The cleaned (unescaped) form of a raw line β€” the input to `truncate`. */
166
+ export function cleanStepLine(raw: string): string {
167
+ return stripMarkdown(raw).replace(/\s+/g, ' ').trim()
168
+ }
169
+
170
+ // ─── Card spec ──────────────────────────────────────────────────────────────
171
+
172
+ /**
173
+ * How a FINISHED step bullet is drawn.
174
+ * 'struck' β€” `~~_βœ“ step_~~`, the normal card body.
175
+ * 'plain' β€” `_step_`, used by the char-budget shrink levels where the
176
+ * strike/βœ“ chrome is spent bytes on a card already over the wire
177
+ * limit.
178
+ */
179
+ export type DoneStyle = 'struck' | 'plain'
180
+
181
+ /** One section of a card: an optional header plus a windowed step trail. */
182
+ export interface CardSection {
183
+ /** Ready lines emitted above this section's steps (the combined row header). */
184
+ header?: string[]
185
+ /** ALREADY-ESCAPED step lines, oldest β†’ newest. */
186
+ steps: string[]
187
+ /** Trailing steps shown; the remainder collapses to a `+N earlier…` marker. */
188
+ window: number
189
+ /** When true every step renders done β€” no live `β†’` bullet. */
190
+ allDone?: boolean
191
+ /** Heartbeat suffix rendered INSIDE the newest in-progress bullet. */
192
+ liveSuffix?: string
193
+ /**
194
+ * Literal prefix on EVERY line this section emits (marker, bullets,
195
+ * placeholder), OUTSIDE the markdown spans so it never lands inside an
196
+ * emphasis run.
197
+ */
198
+ indent?: string
199
+ /**
200
+ * Token before a done step and inside the overflow marker. `'βœ“ '` on a normal
201
+ * body; `''` for the `↳` nested-child block, whose `↳` glyph already marks it.
202
+ */
203
+ doneMark?: string
204
+ /** See {@link DoneStyle}. Default 'struck'. */
205
+ doneStyle?: DoneStyle
206
+ /** Emitted (with `indent`) when this section has no steps at all. */
207
+ placeholder?: string
208
+ }
209
+
210
+ /** A whole card, as fixed chrome + sections + fixed footer. */
211
+ export interface CardSpec {
212
+ /** Lines above every section β€” the card header, or the combined glance line. */
213
+ chrome?: string[]
214
+ sections: CardSection[]
215
+ /** Lines below every section β€” `βœ“ N steps`, the result block, the spill line. */
216
+ footer?: string[]
217
+ }
218
+
219
+ /**
220
+ * Emit one section's lines into `out`. This is the single `βœ“`/`β†’` bullet
221
+ * emitter: every step line on every card comes through here.
222
+ */
223
+ export function emitSection(out: string[], section: CardSection): void {
224
+ const indent = section.indent ?? ''
225
+ const doneMark = section.doneMark ?? 'βœ“ '
226
+ const doneStyle = section.doneStyle ?? 'struck'
227
+ const allDone = section.allDone === true
228
+ const liveSuffix = section.liveSuffix ?? ''
229
+ if (section.header != null) out.push(...section.header)
230
+ if (section.steps.length === 0) {
231
+ if (section.placeholder != null) out.push(`${indent}${section.placeholder}`)
232
+ return
233
+ }
234
+ const shown = section.steps.slice(-Math.max(1, section.window))
235
+ const hidden = section.steps.length - shown.length
236
+ if (hidden > 0) out.push(`${indent}_${doneMark}+${hidden} earlier…_`)
237
+ const lastIdx = shown.length - 1
238
+ shown.forEach((s, i) => {
239
+ if (!allDone && i === lastIdx) {
240
+ out.push(`${indent}**β†’ ${s}${liveSuffix}**`)
241
+ return
242
+ }
243
+ out.push(doneStyle === 'plain' ? `${indent}_${s}_` : `${indent}~~_${doneMark}${s}_~~`)
244
+ })
245
+ }
246
+
247
+ /**
248
+ * Shared step-feed emitter (back-compat signature). Appends `βœ“`/`β†’` bullet
249
+ * lines to `out` for the given ALREADY-ESCAPED step strings. Thin wrapper over
250
+ * {@link emitSection}.
251
+ */
252
+ export function renderStepFeed(
253
+ out: string[],
254
+ steps: string[],
255
+ allDone: boolean,
256
+ liveSuffix = '',
257
+ window: number = STATUS_ROLLING_LINES,
258
+ indent = '',
259
+ ): void {
260
+ emitSection(out, { steps, window, allDone, liveSuffix, indent })
261
+ }
262
+
263
+ /** Flatten a spec to its ordered card lines. */
264
+ export function cardSpecLines(spec: CardSpec): string[] {
265
+ const out: string[] = [...(spec.chrome ?? [])]
266
+ for (const section of spec.sections) emitSection(out, section)
267
+ out.push(...(spec.footer ?? []))
268
+ return out
269
+ }
270
+
271
+ /**
272
+ * Join card lines with GFM hard breaks so the styled prose lines don't collapse
273
+ * onto one visual line in the rich-message renderer β€” see `stackCardLines`.
274
+ *
275
+ * `collapseSafe` is unconditional here: every card this module renders is a
276
+ * PINNED surface, and Telegram's pinned bar shows a pinned message collapsed to
277
+ * one line with the newlines dropped and nothing substituted (#3666).
278
+ */
279
+ export function stackCard(lines: string[]): string {
280
+ return stackCardLines(lines, { collapseSafe: true })
281
+ }
282
+
283
+ /** Render a spec straight to wire text, with no budget enforcement. */
284
+ export function renderCardSpec(spec: CardSpec): string {
285
+ return stackCard(cardSpecLines(spec))
286
+ }
287
+
288
+ // ─── The one char-budget enforcer ───────────────────────────────────────────
289
+
290
+ /** One shrink level's rendered candidate. */
291
+ export interface CardCandidate {
292
+ spec: CardSpec
293
+ /**
294
+ * An additional card-specific ceiling this candidate violates (the combined
295
+ * card's total body-line budget). A candidate that fits the char budget but
296
+ * sets this is still rejected, so both ceilings are enforced by one loop.
297
+ */
298
+ overflow?: boolean
299
+ }
300
+
301
+ /**
302
+ * THE char-budget backstop β€” the only place a rendered card is measured against
303
+ * `STATUS_CARD_CHAR_BUDGET`.
304
+ *
305
+ * `build(level)` renders the card at progressively deeper shrink levels
306
+ * (level 0 = the full card). The first level whose rendered text fits the
307
+ * budget and clears its own `overflow` flag wins; if even `maxLevel` does not
308
+ * fit, that deepest render is returned β€” a card is always emitted, never
309
+ * dropped.
310
+ *
311
+ * What a level MEANS is the caller's business and is genuinely card-specific:
312
+ * the status card drops its oldest bullet (and, at its deepest level, truncates
313
+ * an oversized newest bullet), while the combined card drops its newest worker
314
+ * row into `+M more working…`. The MECHANISM β€” render, measure, accept or go
315
+ * deeper β€” exists only here.
316
+ */
317
+ export function fitCardToBudget(
318
+ build: (level: number) => CardCandidate,
319
+ maxLevel: number,
320
+ ): string {
321
+ let text = ''
322
+ for (let level = 0; level <= maxLevel; level++) {
323
+ const candidate = build(level)
324
+ text = renderCardSpec(candidate.spec)
325
+ if (candidate.overflow !== true && text.length <= STATUS_CARD_CHAR_BUDGET) return text
326
+ }
327
+ return text
328
+ }
@@ -23029,6 +23029,90 @@ function isLitellmProxyAuthMisconfig(text) {
23029
23029
  return lower.includes("fallback") && lower.includes("x-api-key");
23030
23030
  }
23031
23031
 
23032
+ // provider-credit.ts
23033
+ var PROVIDER_CREDIT_REGISTRY = [
23034
+ {
23035
+ id: "openrouter",
23036
+ label: "OpenRouter",
23037
+ vaultKey: "openrouter/api-key",
23038
+ consoleUrl: "https://openrouter.ai/credits",
23039
+ action: "Top up the OpenRouter balance (or raise the key\u2019s credit limit) in the OpenRouter console.",
23040
+ markers: ["openrouter", "openrouter.ai"]
23041
+ },
23042
+ {
23043
+ id: "openai",
23044
+ label: "OpenAI",
23045
+ vaultKey: "openai/api-key",
23046
+ consoleUrl: "https://platform.openai.com/settings/organization/billing",
23047
+ action: "Add credit / raise the billing limit in the OpenAI console.",
23048
+ markers: ["api.openai.com", "openai.", "openaiexception", "openai_api"]
23049
+ },
23050
+ {
23051
+ id: "perplexity",
23052
+ label: "Perplexity",
23053
+ vaultKey: "perplexity/api-key",
23054
+ consoleUrl: "https://www.perplexity.ai/settings/api",
23055
+ action: "Top up the Perplexity API balance (or re-issue the key) in the Perplexity console.",
23056
+ markers: ["perplexity", "api.perplexity.ai", "pplx"]
23057
+ }
23058
+ ];
23059
+ var CREDIT_EXHAUSTION_SIGNALS = [
23060
+ "payment_required",
23061
+ "payment required",
23062
+ "insufficient credits",
23063
+ "insufficient_credits",
23064
+ "more credits are required",
23065
+ "requires more credits",
23066
+ "add more credits",
23067
+ "out of credits",
23068
+ "insufficient_quota",
23069
+ "exceeded your current quota",
23070
+ "billing_hard_limit_reached",
23071
+ "billing hard limit",
23072
+ "insufficient balance",
23073
+ "insufficient_balance",
23074
+ "credit balance is too low",
23075
+ "credit balance too low",
23076
+ "credit_balance_too_low",
23077
+ "no credits remaining",
23078
+ "quota exceeded for credits"
23079
+ ];
23080
+ var CREDIT_EXHAUSTION_STATUSES = [402];
23081
+ var MAX_SCAN_CHARS = 16384;
23082
+ function sample(text) {
23083
+ if (typeof text !== "string" || text.length === 0)
23084
+ return "";
23085
+ return (text.length > MAX_SCAN_CHARS ? text.slice(0, MAX_SCAN_CHARS) : text).toLowerCase();
23086
+ }
23087
+ function isProviderCreditStatus(status) {
23088
+ return typeof status === "number" && CREDIT_EXHAUSTION_STATUSES.includes(status);
23089
+ }
23090
+ function hasCreditExhaustionWording(text) {
23091
+ const lower = sample(text);
23092
+ if (lower.length === 0)
23093
+ return false;
23094
+ return CREDIT_EXHAUSTION_SIGNALS.some((s) => lower.includes(s));
23095
+ }
23096
+ function attributeProvider(text) {
23097
+ const lower = sample(text);
23098
+ if (lower.length === 0)
23099
+ return null;
23100
+ for (const entry of PROVIDER_CREDIT_REGISTRY) {
23101
+ if (entry.markers.some((m) => lower.includes(m)))
23102
+ return entry;
23103
+ }
23104
+ return null;
23105
+ }
23106
+ function detectProviderCreditExhaustion(text, status) {
23107
+ if (isProviderCreditStatus(status)) {
23108
+ return { provider: attributeProvider(text), via: "status" };
23109
+ }
23110
+ if (hasCreditExhaustionWording(text)) {
23111
+ return { provider: attributeProvider(text), via: "wording" };
23112
+ }
23113
+ return null;
23114
+ }
23115
+
23032
23116
  // operator-events.ts
23033
23117
  function classifyClaudeError(raw) {
23034
23118
  try {
@@ -23062,9 +23146,16 @@ ${message}`)) {
23062
23146
  if (errorType === "invalid_api_key" || errorCode === "invalid_api_key" || sdkCode === "invalid_api_key" || message.toLowerCase().includes("invalid_api_key") || message.toLowerCase().includes("invalid api key")) {
23063
23147
  return "credentials-invalid";
23064
23148
  }
23065
- if (errorType === "credit_balance_too_low" || errorCode === "credit_balance_too_low" || sdkCode === "credit_balance_too_low" || message.toLowerCase().includes("credit_balance_too_low") || message.toLowerCase().includes("credit balance")) {
23149
+ const creditScanText = `${errorType}
23150
+ ${errorCode}
23151
+ ${sdkCode}
23152
+ ${message}`;
23153
+ if ((errorType === "credit_balance_too_low" || errorCode === "credit_balance_too_low" || sdkCode === "credit_balance_too_low" || message.toLowerCase().includes("credit_balance_too_low") || message.toLowerCase().includes("credit balance")) && attributeProvider(creditScanText) == null) {
23066
23154
  return "credit-exhausted";
23067
23155
  }
23156
+ if (detectProviderCreditExhaustion(creditScanText, status) != null) {
23157
+ return "provider-credit-exhausted";
23158
+ }
23068
23159
  if (errorType === "rate_limit_error" || errorCode === "rate_limit_error" || sdkCode === "rate_limit_error" || message.toLowerCase().includes("rate_limit_error") || message.toLowerCase().includes("rate limit")) {
23069
23160
  return "rate-limited";
23070
23161
  }
@@ -23103,6 +23194,7 @@ var OPERATOR_ACTIONABLE_KINDS = new Set([
23103
23194
  "credentials-expired",
23104
23195
  "credentials-invalid",
23105
23196
  "credit-exhausted",
23197
+ "provider-credit-exhausted",
23106
23198
  "proxy-misconfig"
23107
23199
  ]);
23108
23200