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.
- package/bin/git-agent-attribution-hook.sh +144 -0
- package/dist/agent-scheduler/index.js +55 -0
- package/dist/auth-broker/index.js +116 -6
- package/dist/cli/notion-write-pretool.mjs +55 -0
- package/dist/cli/switchroom.js +2055 -1193
- package/dist/host-control/main.js +117 -7
- package/dist/vault/approvals/kernel-server.js +114 -6
- package/dist/vault/broker/server.js +114 -6
- package/package.json +6 -2
- package/profiles/_base/cron-session.sh.hbs +8 -0
- package/profiles/_base/start.sh.hbs +105 -0
- package/telegram-plugin/card-layout.ts +328 -0
- package/telegram-plugin/dist/bridge/bridge.js +93 -1
- package/telegram-plugin/dist/gateway/gateway.js +2009 -1166
- package/telegram-plugin/dist/server.js +96 -1
- package/telegram-plugin/edit-flood-fuse.ts +637 -56
- package/telegram-plugin/flood-429-ledger.ts +526 -0
- package/telegram-plugin/flood-circuit-breaker.ts +18 -0
- package/telegram-plugin/gateway/flood-reply-queue.ts +168 -0
- package/telegram-plugin/gateway/gateway.ts +58 -68
- package/telegram-plugin/gateway/narrative-lane.ts +14 -0
- package/telegram-plugin/gateway/outbound-send-path.ts +36 -0
- package/telegram-plugin/gateway/outbox-sweep.ts +183 -6
- package/telegram-plugin/gateway/pinned-message-handler.ts +12 -16
- package/telegram-plugin/gateway/status-pin-retarget.ts +72 -36
- package/telegram-plugin/gateway/status-pin-store.ts +58 -9
- package/telegram-plugin/gateway/worker-pin-reaper.ts +56 -7
- package/telegram-plugin/llm-error-present.ts +61 -2
- package/telegram-plugin/model-unavailable.ts +8 -0
- package/telegram-plugin/operator-events.ts +72 -5
- package/telegram-plugin/outbound-class.ts +81 -0
- package/telegram-plugin/provider-credit.ts +237 -0
- package/telegram-plugin/scripts/bun-test-ci.sh +36 -6
- package/telegram-plugin/send-gate.ts +24 -2
- package/telegram-plugin/status-no-truncate.ts +10 -48
- package/telegram-plugin/status-pin-driver.ts +33 -45
- package/telegram-plugin/status-pin.ts +18 -1
- package/telegram-plugin/tests/card-golden.test.ts +69 -0
- package/telegram-plugin/tests/card-lifecycle-render.test.ts +362 -0
- package/telegram-plugin/tests/card-type-distinguishability.test.ts +187 -164
- package/telegram-plugin/tests/card-variants.golden.txt +211 -0
- package/telegram-plugin/tests/card-variants.ts +366 -0
- package/telegram-plugin/tests/edit-flood-fuse-ban-awareness.test.ts +316 -0
- package/telegram-plugin/tests/edit-flood-fuse-default-deny.test.ts +319 -0
- package/telegram-plugin/tests/edit-flood-fuse.test.ts +11 -2
- package/telegram-plugin/tests/feed-edit-rate-ceiling.test.ts +462 -0
- package/telegram-plugin/tests/fixtures/real-429-stream.ts +220 -0
- package/telegram-plugin/tests/flood-429-ledger.test.ts +278 -0
- package/telegram-plugin/tests/flood-429-recorder-wiring.test.ts +128 -0
- package/telegram-plugin/tests/flood-reply-queue.test.ts +418 -0
- package/telegram-plugin/tests/outbox-sweep-flood-breaker.test.ts +221 -0
- package/telegram-plugin/tests/pinned-card-collapse.test.ts +19 -24
- package/telegram-plugin/tests/pinned-message-handler.test.ts +15 -15
- package/telegram-plugin/tests/provider-credit-402.test.ts +243 -0
- package/telegram-plugin/tests/status-pin-api.test.ts +11 -11
- package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +36 -37
- package/telegram-plugin/tests/status-pin-lifecycle.test.ts +602 -0
- package/telegram-plugin/tests/status-pin-retarget.test.ts +90 -62
- package/telegram-plugin/tests/status-pin-service-message-suppression.test.ts +7 -3
- package/telegram-plugin/tests/status-pin-store.test.ts +109 -60
- package/telegram-plugin/tests/status-pin.test.ts +56 -5
- package/telegram-plugin/tests/test-runner-coverage.test.ts +133 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +12 -10
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +23 -29
- package/telegram-plugin/tests/worker-feed-pin-persistence.test.ts +56 -59
- package/telegram-plugin/tests/worker-feed-terminal-edit-class.test.ts +335 -0
- package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +1 -1
- package/telegram-plugin/tool-activity-summary.ts +239 -365
- package/telegram-plugin/uat/assertions.ts +22 -11
- package/telegram-plugin/uat/feed-matcher.test.ts +24 -17
- package/telegram-plugin/worker-activity-feed.ts +105 -47
- package/vendor/hindsight-memory/CLAUDE.md +45 -0
- package/vendor/hindsight-memory/scripts/lib/config.py +33 -0
- package/vendor/hindsight-memory/scripts/recall.py +176 -7
- package/vendor/hindsight-memory/scripts/tests/test_config_recall_passthrough_env.py +170 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_min_score.py +464 -0
- 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
|
-
|
|
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
|
|