switchroom 0.19.48 → 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.
Files changed (50) hide show
  1. package/dist/agent-scheduler/index.js +18 -1
  2. package/dist/auth-broker/index.js +19 -2
  3. package/dist/buzz-gateway/index.js +9207 -0
  4. package/dist/cli/notion-write-pretool.mjs +18 -1
  5. package/dist/cli/switchroom.js +63 -4
  6. package/dist/host-control/main.js +20 -3
  7. package/dist/vault/approvals/kernel-server.js +19 -2
  8. package/dist/vault/broker/server.js +19 -2
  9. package/package.json +4 -3
  10. package/profiles/_base/start.sh.hbs +78 -1
  11. package/profiles/default/CLAUDE.md.hbs +1 -1
  12. package/skills/dev-protocol/SKILL.md +30 -1
  13. package/skills/switchroom-architecture/SKILL.md +5 -0
  14. package/skills/switchroom-cli/SKILL.md +1 -1
  15. package/telegram-plugin/dist/bridge/bridge.js +7 -4
  16. package/telegram-plugin/dist/gateway/gateway.js +1149 -247
  17. package/telegram-plugin/dist/server.js +7 -4
  18. package/telegram-plugin/gateway/boot-briefing-builder.ts +458 -0
  19. package/telegram-plugin/gateway/boot-briefing-wiring.ts +170 -0
  20. package/telegram-plugin/gateway/buzz-mirror.ts +329 -0
  21. package/telegram-plugin/gateway/buzz-type-guards.ts +34 -0
  22. package/telegram-plugin/gateway/channel-route.ts +272 -0
  23. package/telegram-plugin/gateway/gateway.ts +73 -81
  24. package/telegram-plugin/gateway/inbound-spool.ts +33 -1
  25. package/telegram-plugin/gateway/ipc-protocol.ts +81 -2
  26. package/telegram-plugin/gateway/ipc-server.ts +197 -2
  27. package/telegram-plugin/gateway/outbound-send-path.ts +37 -1
  28. package/telegram-plugin/gateway/pending-turn-env.ts +61 -0
  29. package/telegram-plugin/gateway/stream-render.ts +21 -0
  30. package/telegram-plugin/gateway/subagent-handback-marker.ts +12 -0
  31. package/telegram-plugin/gateway/user-failure-notices.ts +172 -0
  32. package/telegram-plugin/history.ts +15 -0
  33. package/telegram-plugin/llm-error-present.ts +9 -4
  34. package/telegram-plugin/model-unavailable.ts +4 -0
  35. package/telegram-plugin/operator-events.fixtures.json +12 -12
  36. package/telegram-plugin/operator-events.ts +81 -9
  37. package/telegram-plugin/session-tail.ts +7 -1
  38. package/telegram-plugin/tests/boot-briefing-builder.test.ts +604 -0
  39. package/telegram-plugin/tests/buzz-mirror.test.ts +242 -0
  40. package/telegram-plugin/tests/buzz-origin-stamp-gate.test.ts +159 -0
  41. package/telegram-plugin/tests/channel-route.test.ts +306 -0
  42. package/telegram-plugin/tests/inbound-spool.test.ts +47 -0
  43. package/telegram-plugin/tests/ipc-server-buzz-dedup.test.ts +124 -0
  44. package/telegram-plugin/tests/ipc-server-buzz-peer.test.ts +269 -0
  45. package/telegram-plugin/tests/operator-events-session-tail.test.ts +63 -0
  46. package/telegram-plugin/tests/operator-events.test.ts +71 -7
  47. package/telegram-plugin/tests/user-failure-notices.test.ts +165 -0
  48. package/telegram-plugin/voice-normalize-text.ts +5 -0
  49. package/vendor/hindsight-memory/scripts/directive_verify.py +4 -0
  50. 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
- if (claudeKind === 'unknown-5xx') {
256
- return { kind: 'overload_529', source: 'anthropic' }
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-4xx or
114
- * unknown-5xx based on HTTP status, defaulting to unknown-4xx.
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-4xx'
133
+ return 'unknown-5xx'
121
134
  }
122
135
  }
123
136
 
124
137
  function classifyInner(raw: unknown): OperatorEventKind {
125
- if (raw == null) return 'unknown-4xx'
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
- return 'unknown-4xx'
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
- { text: '🔐 Reauth', callback_data: `op:reauth:${encodeURIComponent(ev.agent)}` },
523
- { text: '❌ Dismiss', callback_data: `op:dismiss:${encodeURIComponent(ev.agent)}` },
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
- const transient = kind === 'rate-limited'
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