switchroom 0.19.47 → 0.20.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/dist/agent-scheduler/index.js +22 -1
- package/dist/auth-broker/index.js +26 -2
- package/dist/buzz-gateway/index.js +9207 -0
- package/dist/cli/notion-write-pretool.mjs +22 -1
- package/dist/cli/switchroom.js +91 -9
- package/dist/host-control/main.js +27 -3
- package/dist/vault/approvals/kernel-server.js +26 -2
- package/dist/vault/broker/server.js +26 -2
- package/package.json +4 -3
- package/profiles/_base/start.sh.hbs +78 -1
- package/profiles/default/CLAUDE.md.hbs +1 -1
- package/skills/dev-protocol/SKILL.md +30 -1
- package/skills/switchroom-architecture/SKILL.md +5 -0
- package/skills/switchroom-cli/SKILL.md +1 -1
- package/telegram-plugin/dist/bridge/bridge.js +7 -4
- package/telegram-plugin/dist/gateway/gateway.js +1156 -247
- package/telegram-plugin/dist/server.js +7 -4
- package/telegram-plugin/gateway/boot-briefing-builder.ts +458 -0
- package/telegram-plugin/gateway/boot-briefing-wiring.ts +170 -0
- package/telegram-plugin/gateway/buzz-mirror.ts +329 -0
- package/telegram-plugin/gateway/buzz-type-guards.ts +34 -0
- package/telegram-plugin/gateway/channel-route.ts +272 -0
- package/telegram-plugin/gateway/gateway.ts +73 -81
- package/telegram-plugin/gateway/inbound-spool.ts +33 -1
- package/telegram-plugin/gateway/ipc-protocol.ts +81 -2
- package/telegram-plugin/gateway/ipc-server.ts +197 -2
- package/telegram-plugin/gateway/outbound-send-path.ts +37 -1
- package/telegram-plugin/gateway/pending-turn-env.ts +61 -0
- package/telegram-plugin/gateway/stream-render.ts +21 -0
- package/telegram-plugin/gateway/subagent-handback-marker.ts +12 -0
- package/telegram-plugin/gateway/user-failure-notices.ts +172 -0
- package/telegram-plugin/history.ts +15 -0
- package/telegram-plugin/llm-error-present.ts +9 -4
- package/telegram-plugin/model-unavailable.ts +4 -0
- package/telegram-plugin/operator-events.fixtures.json +12 -12
- package/telegram-plugin/operator-events.ts +81 -9
- package/telegram-plugin/session-tail.ts +7 -1
- package/telegram-plugin/tests/boot-briefing-builder.test.ts +604 -0
- package/telegram-plugin/tests/buzz-mirror.test.ts +242 -0
- package/telegram-plugin/tests/buzz-origin-stamp-gate.test.ts +159 -0
- package/telegram-plugin/tests/channel-route.test.ts +306 -0
- package/telegram-plugin/tests/inbound-spool.test.ts +47 -0
- package/telegram-plugin/tests/ipc-server-buzz-dedup.test.ts +124 -0
- package/telegram-plugin/tests/ipc-server-buzz-peer.test.ts +269 -0
- package/telegram-plugin/tests/operator-events-session-tail.test.ts +63 -0
- package/telegram-plugin/tests/operator-events.test.ts +71 -7
- package/telegram-plugin/tests/user-failure-notices.test.ts +165 -0
- package/telegram-plugin/voice-normalize-text.ts +5 -0
- package/vendor/hindsight-memory/scripts/directive_verify.py +4 -0
- package/vendor/hindsight-memory/scripts/recall.py +7 -2
|
@@ -252,10 +252,15 @@ function classifyKindAndSource(text: string): { kind: LlmErrorKind; source: LlmE
|
|
|
252
252
|
if (claudeKind === 'rate-limited') {
|
|
253
253
|
return { kind: 'rate_limit', source: 'anthropic' }
|
|
254
254
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
255
|
+
// NOTE: we deliberately do NOT map the taxonomy's `unknown-5xx` to
|
|
256
|
+
// `overload_529` here. `classifyClaudeError` is called above with only
|
|
257
|
+
// {message, type} (no HTTP status), so `unknown-5xx` at this point is the
|
|
258
|
+
// taxonomy's neutral no-status DEFAULT — not evidence of a genuine 529/5xx.
|
|
259
|
+
// A real Anthropic overload/5xx always carries recognizable wording and is
|
|
260
|
+
// resolved earlier by detectModelUnavailable (step 3). Fabricating
|
|
261
|
+
// "Anthropic is overloaded (529) — retrying automatically" for an
|
|
262
|
+
// unrecognized error would be a false diagnosis, so it falls through to the
|
|
263
|
+
// honest `unknown` kind.
|
|
259
264
|
return { kind: 'unknown', source: 'anthropic' }
|
|
260
265
|
}
|
|
261
266
|
|
|
@@ -474,6 +474,10 @@ export function detectModelUnavailable(
|
|
|
474
474
|
'socket hang up',
|
|
475
475
|
'request timed out',
|
|
476
476
|
'connection refused',
|
|
477
|
+
// Mid-response stream aborts reaching Anthropic surface these markers
|
|
478
|
+
// (a transport failure, not an account/quota fault) — treat as network.
|
|
479
|
+
'connection closed',
|
|
480
|
+
'mid-response',
|
|
477
481
|
'getaddrinfo',
|
|
478
482
|
]
|
|
479
483
|
if (networkSignals.some(s => lower.includes(s))) {
|
|
@@ -119,18 +119,6 @@
|
|
|
119
119
|
"_source": "Bare 403 with no recognised type",
|
|
120
120
|
"status": 403,
|
|
121
121
|
"message": "Forbidden"
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
"_source": "null input — always unknown-4xx",
|
|
125
|
-
"_value": null
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
"_source": "Completely empty object",
|
|
129
|
-
"_value": {}
|
|
130
|
-
},
|
|
131
|
-
{
|
|
132
|
-
"_source": "Non-object string",
|
|
133
|
-
"_value": "something went wrong"
|
|
134
122
|
}
|
|
135
123
|
],
|
|
136
124
|
"unknown-5xx": [
|
|
@@ -146,6 +134,18 @@
|
|
|
146
134
|
"type": "service_unavailable",
|
|
147
135
|
"message": "Service temporarily unavailable"
|
|
148
136
|
}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"_source": "null input — no status, neutral 5xx default (never a fabricated 4xx)",
|
|
140
|
+
"_value": null
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"_source": "Completely empty object — no status → unknown-5xx",
|
|
144
|
+
"_value": {}
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"_source": "Non-object string — no status → unknown-5xx",
|
|
148
|
+
"_value": "something went wrong"
|
|
149
149
|
}
|
|
150
150
|
]
|
|
151
151
|
}
|
|
@@ -73,6 +73,17 @@ export const OPERATOR_EVENT_KINDS = [
|
|
|
73
73
|
'mcp-dependency-blocked',
|
|
74
74
|
'quota-exhausted',
|
|
75
75
|
'rate-limited',
|
|
76
|
+
/**
|
|
77
|
+
* A TRANSPORT / network failure reaching Anthropic — Claude Code emits
|
|
78
|
+
* `error: "server_error"` (or `api_error`, which Anthropic documents as an
|
|
79
|
+
* HTTP 500) with NO `apiErrorStatus` on a mid-response stream abort. It is a
|
|
80
|
+
* pipe/transport failure, NOT an account, auth, or quota fault: retryable, and
|
|
81
|
+
* recovered by Claude's own internal retry. Handled by the gateway's dedicated
|
|
82
|
+
* calm path (no broadcast card; record for /status + a turn-end-gated plain
|
|
83
|
+
* user notice; a burst escalates to ONE operator-only card) — deliberately
|
|
84
|
+
* NEVER a Reauth card. See `gateway/user-failure-notices.ts`.
|
|
85
|
+
*/
|
|
86
|
+
'transport-transient',
|
|
76
87
|
'agent-crashed',
|
|
77
88
|
'agent-restarted-unexpectedly',
|
|
78
89
|
'unknown-4xx',
|
|
@@ -110,19 +121,21 @@ export interface InlineKeyboardMarkup {
|
|
|
110
121
|
* Classify an error value from any source (Anthropic SDK throw, JSONL error
|
|
111
122
|
* field, etc.) into an OperatorEventKind.
|
|
112
123
|
*
|
|
113
|
-
* CONTRACT: never throws. Unfamiliar shapes fall through to unknown-
|
|
114
|
-
* unknown-
|
|
124
|
+
* CONTRACT: never throws. Unfamiliar shapes fall through to unknown-5xx or
|
|
125
|
+
* unknown-4xx based on HTTP status. With NO status the neutral default is
|
|
126
|
+
* unknown-5xx — an unrecognized shape with no HTTP status must NOT fabricate a
|
|
127
|
+
* "4xx" label (whose card historically carried a wrong Reauth remedy).
|
|
115
128
|
*/
|
|
116
129
|
export function classifyClaudeError(raw: unknown): OperatorEventKind {
|
|
117
130
|
try {
|
|
118
131
|
return classifyInner(raw)
|
|
119
132
|
} catch {
|
|
120
|
-
return 'unknown-
|
|
133
|
+
return 'unknown-5xx'
|
|
121
134
|
}
|
|
122
135
|
}
|
|
123
136
|
|
|
124
137
|
function classifyInner(raw: unknown): OperatorEventKind {
|
|
125
|
-
if (raw == null) return 'unknown-
|
|
138
|
+
if (raw == null) return 'unknown-5xx'
|
|
126
139
|
|
|
127
140
|
// Extract common fields defensively — never throw on bad shapes.
|
|
128
141
|
const obj = typeof raw === 'object' ? (raw as Record<string, unknown>) : {}
|
|
@@ -258,13 +271,48 @@ function classifyInner(raw: unknown): OperatorEventKind {
|
|
|
258
271
|
return 'agent-restarted-unexpectedly'
|
|
259
272
|
}
|
|
260
273
|
|
|
274
|
+
// TRANSPORT / network failure reaching Anthropic. Claude Code emits
|
|
275
|
+
// `error: "server_error"` (and `api_error`, documented as HTTP 500) with NO
|
|
276
|
+
// `apiErrorStatus` on a mid-response stream abort — a pipe/transport failure,
|
|
277
|
+
// not an account/auth/quota fault. Without this branch it fell through to the
|
|
278
|
+
// status fallback below and FABRICATED `unknown-4xx`, whose card carried a
|
|
279
|
+
// wrong Reauth remedy and broadcast to every chat. It is retryable (Claude
|
|
280
|
+
// retries internally), so it maps to the calm `transport-transient` kind.
|
|
281
|
+
//
|
|
282
|
+
// Placed AFTER the auth-wording matches above ON PURPOSE: a genuine auth
|
|
283
|
+
// failure that upstream mislabels `server_error` still classifies as a
|
|
284
|
+
// credential fault. Matched only on EXACT type/code equality (never a message
|
|
285
|
+
// substring) so an unrelated error whose text merely mentions "server error"
|
|
286
|
+
// is untouched.
|
|
287
|
+
//
|
|
288
|
+
// GUARDED to status-less OR 5xx shapes (`status == null || status >= 500`): a
|
|
289
|
+
// mid-response abort carries NO status, and a genuine 500 is server-side. But
|
|
290
|
+
// LiteLLM stamps a generic `type: "api_error"` onto WRAPPED upstream faults
|
|
291
|
+
// that DO carry a 4xx status (e.g. a 401), and session-tail threads that
|
|
292
|
+
// `apiErrorStatus` through as `status`. Without this guard such a 401 would be
|
|
293
|
+
// swallowed as transport-transient (silent, no card) when it must reach the
|
|
294
|
+
// status fallback below and surface an operator card. The status wins.
|
|
295
|
+
if (
|
|
296
|
+
(status == null || status >= 500) &&
|
|
297
|
+
(errorType === 'server_error' ||
|
|
298
|
+
errorCode === 'server_error' ||
|
|
299
|
+
sdkCode === 'server_error' ||
|
|
300
|
+
errorType === 'api_error' ||
|
|
301
|
+
errorCode === 'api_error' ||
|
|
302
|
+
sdkCode === 'api_error')
|
|
303
|
+
) {
|
|
304
|
+
return 'transport-transient'
|
|
305
|
+
}
|
|
306
|
+
|
|
261
307
|
// Fallback: HTTP status-based.
|
|
262
308
|
if (status != null) {
|
|
263
309
|
if (status >= 400 && status < 500) return 'unknown-4xx'
|
|
264
310
|
if (status >= 500 && status < 600) return 'unknown-5xx'
|
|
265
311
|
}
|
|
266
312
|
|
|
267
|
-
|
|
313
|
+
// No recognized shape and NO status: neutral 5xx default. Do NOT claim 4xx —
|
|
314
|
+
// a fabricated client-error label mis-routed a Reauth card (the bug this fixes).
|
|
315
|
+
return 'unknown-5xx'
|
|
268
316
|
}
|
|
269
317
|
|
|
270
318
|
function extractString(obj: Record<string, unknown>, key: string): string | null {
|
|
@@ -508,6 +556,12 @@ export function renderOperatorEvent(ev: OperatorEvent): RenderResult {
|
|
|
508
556
|
},
|
|
509
557
|
}
|
|
510
558
|
|
|
559
|
+
// Dismiss-only, matching unknown-5xx / proxy-misconfig. The Reauth button
|
|
560
|
+
// was REMOVED: unknown-4xx is a catch-all for unrecognized client-error
|
|
561
|
+
// shapes, and re-auth is the wrong remedy for almost all of them. Post-RFC-H
|
|
562
|
+
// the auth-broker is the SOLE writer of `.credentials.json`, so a per-agent
|
|
563
|
+
// reauth is re-mirrored over anyway. Reauth buttons now live ONLY on the
|
|
564
|
+
// credentials-* cards, where the diagnosis IS a login fault.
|
|
511
565
|
case 'unknown-4xx':
|
|
512
566
|
return {
|
|
513
567
|
text: [
|
|
@@ -518,10 +572,28 @@ export function renderOperatorEvent(ev: OperatorEvent): RenderResult {
|
|
|
518
572
|
.join('\n'),
|
|
519
573
|
keyboard: {
|
|
520
574
|
inline_keyboard: [
|
|
521
|
-
[
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
575
|
+
[{ text: '❌ Dismiss', callback_data: `op:dismiss:${encodeURIComponent(ev.agent)}` }],
|
|
576
|
+
],
|
|
577
|
+
},
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// transport-transient is normally handled by the gateway's dedicated calm
|
|
581
|
+
// path (no broadcast card; a burst escalates via renderTransportEscalationCard
|
|
582
|
+
// in gateway/user-failure-notices.ts). This case exists so the switch stays
|
|
583
|
+
// exhaustive and any incidental render is calm + Dismiss-only — NEVER a
|
|
584
|
+
// Reauth button (a dropped stream is not a credential fault).
|
|
585
|
+
case 'transport-transient':
|
|
586
|
+
return {
|
|
587
|
+
text: [
|
|
588
|
+
`🔌 **Transient transport error** for **${agent}**.`,
|
|
589
|
+
detail ? `\`${detail}\`` : '',
|
|
590
|
+
`A mid-response stream to Anthropic dropped. Will retry automatically.`,
|
|
591
|
+
]
|
|
592
|
+
.filter(Boolean)
|
|
593
|
+
.join('\n'),
|
|
594
|
+
keyboard: {
|
|
595
|
+
inline_keyboard: [
|
|
596
|
+
[{ text: '❌ Dismiss', callback_data: `op:dismiss:${encodeURIComponent(ev.agent)}` }],
|
|
525
597
|
],
|
|
526
598
|
},
|
|
527
599
|
}
|
|
@@ -1075,7 +1075,13 @@ export function detectErrorInTranscriptLine(
|
|
|
1075
1075
|
// with no retry state is ambiguous → treat as in-flight and suppress
|
|
1076
1076
|
// (the silence-poke covers a genuinely stuck turn; a false card is
|
|
1077
1077
|
// the bug we are fixing, a missed ambiguous card costs nothing).
|
|
1078
|
-
|
|
1078
|
+
// `transport-transient` joins the transient family so Claude's retry
|
|
1079
|
+
// annotations gate `terminal`: a mid-response abort Claude is still retrying
|
|
1080
|
+
// (retryAttempt < maxRetries) is in-flight, not terminal, and must NOT count
|
|
1081
|
+
// toward the operator escalation counter (which is bounded to ≥3 TERMINAL
|
|
1082
|
+
// events). The primary bug shape (`isApiErrorMessage`) returns earlier with
|
|
1083
|
+
// terminal:true by construction, so it is unaffected by this.
|
|
1084
|
+
const transient = kind === 'rate-limited' || kind === 'transport-transient'
|
|
1079
1085
|
const retry = extractRetryState(obj)
|
|
1080
1086
|
const terminal = !transient
|
|
1081
1087
|
? true
|