switchroom 0.18.9 → 0.18.11

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 (65) hide show
  1. package/dist/agent-scheduler/index.js +1 -0
  2. package/dist/auth-broker/index.js +198 -13
  3. package/dist/cli/notion-write-pretool.mjs +1 -0
  4. package/dist/cli/switchroom.js +214 -83
  5. package/dist/cli/ui/apple-touch-icon.png +0 -0
  6. package/dist/cli/ui/favicon-32.png +0 -0
  7. package/dist/cli/ui/favicon.ico +0 -0
  8. package/dist/host-control/main.js +760 -257
  9. package/dist/vault/approvals/kernel-server.js +3 -1
  10. package/dist/vault/broker/server.js +3 -1
  11. package/package.json +1 -1
  12. package/profiles/_base/start.sh.hbs +119 -37
  13. package/profiles/_shared/dev-protocol.md.hbs +42 -0
  14. package/skills/dev-protocol/SKILL.md +131 -0
  15. package/telegram-plugin/README.md +2 -1
  16. package/telegram-plugin/admin-commands/dispatch.test.ts +40 -2
  17. package/telegram-plugin/admin-commands/index.ts +6 -1
  18. package/telegram-plugin/bridge/bridge.ts +23 -1
  19. package/telegram-plugin/bridge/crash-breadcrumb.ts +42 -0
  20. package/telegram-plugin/chat-lock.ts +13 -0
  21. package/telegram-plugin/dist/bridge/bridge.js +24 -1
  22. package/telegram-plugin/dist/gateway/gateway.js +1832 -263
  23. package/telegram-plugin/dist/server.js +29 -2
  24. package/telegram-plugin/fallback-card-collapse.ts +131 -0
  25. package/telegram-plugin/gateway/bridge-dead-watchdog.ts +546 -0
  26. package/telegram-plugin/gateway/effort-command.ts +47 -3
  27. package/telegram-plugin/gateway/gateway.ts +1435 -211
  28. package/telegram-plugin/gateway/model-command.ts +94 -8
  29. package/telegram-plugin/gateway/pending-session-command.ts +365 -0
  30. package/telegram-plugin/gateway/permission-timeout.ts +25 -0
  31. package/telegram-plugin/gateway/resume-inbound-builder.ts +23 -3
  32. package/telegram-plugin/gateway/session-model-file.ts +166 -23
  33. package/telegram-plugin/gateway/stop-command.ts +56 -0
  34. package/telegram-plugin/photo-precheck.ts +201 -0
  35. package/telegram-plugin/quota-watch.ts +141 -2
  36. package/telegram-plugin/registry/subagents-schema.ts +26 -3
  37. package/telegram-plugin/registry/subagents.test.ts +67 -0
  38. package/telegram-plugin/retry-api-call.ts +31 -0
  39. package/telegram-plugin/subagent-watcher.ts +392 -1
  40. package/telegram-plugin/tests/bridge-dead-watchdog.test.ts +576 -0
  41. package/telegram-plugin/tests/buffer-gate-broadened.test.ts +11 -5
  42. package/telegram-plugin/tests/chat-lock-unhandled-rejection.test.ts +101 -0
  43. package/telegram-plugin/tests/crash-breadcrumb.test.ts +57 -0
  44. package/telegram-plugin/tests/effort-command.test.ts +59 -2
  45. package/telegram-plugin/tests/fallback-card-collapse.test.ts +104 -0
  46. package/telegram-plugin/tests/gateway-pending-command-wiring.test.ts +124 -0
  47. package/telegram-plugin/tests/gateway-secret-detect.test.ts +7 -1
  48. package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +19 -11
  49. package/telegram-plugin/tests/model-command.test.ts +46 -3
  50. package/telegram-plugin/tests/pending-session-command.test.ts +322 -0
  51. package/telegram-plugin/tests/permission-timeout.test.ts +26 -0
  52. package/telegram-plugin/tests/permission-verdict-resume-guard.test.ts +16 -0
  53. package/telegram-plugin/tests/photo-dimension-fallback.test.ts +129 -0
  54. package/telegram-plugin/tests/photo-precheck.test.ts +240 -0
  55. package/telegram-plugin/tests/photo-reroute-wiring.test.ts +85 -0
  56. package/telegram-plugin/tests/quota-watch.test.ts +225 -0
  57. package/telegram-plugin/tests/session-model-file.test.ts +101 -2
  58. package/telegram-plugin/tests/stop-command.test.ts +234 -0
  59. package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +27 -9
  60. package/telegram-plugin/tests/subagent-watcher-resurrection.test.ts +398 -0
  61. package/telegram-plugin/tests/subagent-watcher-stall-terminal.test.ts +172 -0
  62. package/telegram-plugin/tests/worker-activity-feed.test.ts +37 -0
  63. package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +18 -4
  64. package/telegram-plugin/welcome-text.ts +4 -3
  65. package/telegram-plugin/worker-activity-feed.ts +27 -0
@@ -31,6 +31,7 @@ import { buildEffectiveToolSchemas, LINEAR_ENV } from './tool-filter.js'
31
31
  import type { InboundMessage, PermissionEvent, StatusEvent } from '../gateway/ipc-protocol.js'
32
32
  import { matchesAllowRule } from '../permission-rule.js'
33
33
  import { createOutstandingPermissionLedger } from './permission-ledger.js'
34
+ import { appendCrashBreadcrumb } from './crash-breadcrumb.js'
34
35
 
35
36
  installPluginLogger()
36
37
 
@@ -107,7 +108,7 @@ const TOOL_SCHEMAS = [
107
108
  quote: { type: 'boolean', description: 'Opt out of the default quote-reply behavior. Default: true. Pass false to send a bare message with no quote reference. Ignored when reply_to is explicitly set.' },
108
109
  message_thread_id: { type: 'string', description: 'Forum topic thread ID. Auto-applied from the last inbound message in the same chat if not specified.' },
109
110
  origin_turn_id: { type: 'string', description: 'In a forum supergroup, pass back the origin_turn_id attribute from the <channel> message you are answering. It pins the reply to that message\'s topic even if another topic\'s turn started meanwhile. Omit in DMs / single-topic chats.' },
110
- files: { type: 'array', items: { type: 'string' }, description: 'Absolute file paths to attach. Images send as photos; other types as documents. Max 50MB each.' },
111
+ files: { type: 'array', items: { type: 'string' }, description: 'Absolute file paths to attach. Images send as photos; other types as documents. Max 50MB each. Telegram rejects photos with extreme dimensions (aspect ratio over ~10:1, width+height over 10000px, or over 10MB) — very tall images like full-page screenshots are auto-rerouted as documents; crop or split them first if the user should see them inline as photos.' },
111
112
  format: { type: 'string', enum: ['html', 'markdownv2', 'text'], description: "Rendering mode. 'html' (default) converts markdown to Telegram HTML." },
112
113
  disable_web_page_preview: { type: 'boolean', description: 'Disable link preview thumbnails. Default: true.' },
113
114
  protect_content: { type: 'boolean', description: 'When true, Telegram prevents the message from being forwarded or saved.' },
@@ -895,8 +896,29 @@ process.on('SIGINT', () => {
895
896
  setTimeout(() => process.exit(0), 500)
896
897
  })
897
898
 
899
+ // #3033 — the bridge process must survive stray errors, and when it can't,
900
+ // it must leave a diagnosable trace. Claude Code NEVER respawns a dead MCP
901
+ // server: if this process exits, the reply tool vanishes from the session
902
+ // (`No such tool available`) and only a full container restart recovers the
903
+ // chat surface (2026-07-11 clerk incident — the gateway crashed, its
904
+ // supervisor brought it back in 1s, but the bridge had died in the same
905
+ // window and the agent was mute for 7 minutes until the operator bounced
906
+ // the container). Claude Code also drops MCP-server stderr after startup,
907
+ // so both handlers persist a breadcrumb to STATE_DIR/bridge-crash.log.
908
+ const CRASH_LOG_PATH = join(STATE_DIR, 'bridge-crash.log')
909
+
898
910
  process.on('unhandledRejection', (err) => {
899
911
  process.stderr.write(`telegram bridge: unhandled rejection: ${err}\n`)
912
+ appendCrashBreadcrumb(CRASH_LOG_PATH, 'unhandledRejection', err)
913
+ })
914
+
915
+ process.on('uncaughtException', (err) => {
916
+ // Log-and-continue, mirroring the unhandledRejection posture. Risky in
917
+ // general, but the alternative is strictly worse here: process death is
918
+ // unrecoverable by design (see above), while the IPC client's reconnect
919
+ // loop can heal any gateway-connection damage on its own.
920
+ process.stderr.write(`telegram bridge: uncaught exception (continuing): ${(err as Error)?.stack ?? err}\n`)
921
+ appendCrashBreadcrumb(CRASH_LOG_PATH, 'uncaughtException', err)
900
922
  })
901
923
 
902
924
  async function main(): Promise<void> {
@@ -0,0 +1,42 @@
1
+ /**
2
+ * crash-breadcrumb.ts — persistent last-gasp diagnostics for the MCP
3
+ * bridge process (#3033).
4
+ *
5
+ * Why this exists: Claude Code captures an MCP server's stderr only
6
+ * around connection startup — anything the bridge writes later is
7
+ * dropped. In the 2026-07-11 clerk incident the bridge process died
8
+ * within seconds of a gateway crash and the cause was unrecoverable:
9
+ * no stderr, no log, nothing. And a dead bridge is terminal — Claude
10
+ * Code never respawns a failed MCP server, so the chat surface stays
11
+ * down (`No such tool available: mcp__switchroom-telegram__reply`)
12
+ * until a full container restart.
13
+ *
14
+ * This module appends a bounded breadcrumb line to
15
+ * `STATE_DIR/bridge-crash.log` from the bridge's uncaughtException /
16
+ * unhandledRejection handlers so the NEXT incident is diagnosable.
17
+ * Append-only, best-effort, never throws.
18
+ */
19
+
20
+ import { appendFileSync, statSync, renameSync } from 'node:fs'
21
+
22
+ /** Rotate once past ~1MB — a crash-looping bridge must not fill the disk. */
23
+ const MAX_LOG_BYTES = 1024 * 1024
24
+
25
+ export function appendCrashBreadcrumb(
26
+ logPath: string,
27
+ kind: 'uncaughtException' | 'unhandledRejection',
28
+ err: unknown,
29
+ now: Date = new Date(),
30
+ ): void {
31
+ try {
32
+ try {
33
+ if (statSync(logPath).size > MAX_LOG_BYTES) renameSync(logPath, `${logPath}.1`)
34
+ } catch { /* first write, or rotation raced — append anyway */ }
35
+ const detail = err instanceof Error ? (err.stack ?? err.message) : String(err)
36
+ // Single line per event (stack newlines folded) — greppable, bounded.
37
+ const folded = detail.replace(/\s*\n\s*/g, ' | ').slice(0, 4000)
38
+ appendFileSync(logPath, `${now.toISOString()} ${kind} pid=${process.pid} ${folded}\n`)
39
+ } catch {
40
+ /* best-effort: a failing breadcrumb must never make the crash worse */
41
+ }
42
+ }
@@ -70,6 +70,19 @@ export function createChatLock(): ChatLock {
70
70
  const tracked = next.finally(() => {
71
71
  if (chains.get(key) === tracked) chains.delete(key)
72
72
  })
73
+ // `tracked` is a SECOND promise derived from `next`, retained only in
74
+ // the `chains` map for ordering. The caller awaits `next` (and handles
75
+ // its rejection); `tracked` is picked up by the NEXT queued call via
76
+ // `prior.then(fn, fn)`. But when this is the TAIL call on the key —
77
+ // the common single-reply case — nothing ever attaches a handler to
78
+ // `tracked`, so a rejected `next` surfaces as an UNHANDLED REJECTION
79
+ // and (per the gateway's unhandledRejection policy) crashes + reboots
80
+ // the whole gateway. This bit any failing send that was last in its
81
+ // (chat,thread) lane: sendPhoto PHOTO_INVALID_DIMENSIONS, sendMediaGroup,
82
+ // editMessageText MESSAGE_TOO_LONG, deleteMessage "can't be deleted",
83
+ // etc. Swallow the rejection on the internal tracking promise ONLY —
84
+ // the caller's `next` still carries the real error for normal handling.
85
+ tracked.catch(() => {})
73
86
  chains.set(key, tracked)
74
87
  return next
75
88
  }
@@ -24681,6 +24681,22 @@ function createOutstandingPermissionLedger() {
24681
24681
  };
24682
24682
  }
24683
24683
 
24684
+ // bridge/crash-breadcrumb.ts
24685
+ import { appendFileSync as appendFileSync2, statSync as statSync5, renameSync as renameSync2 } from "node:fs";
24686
+ var MAX_LOG_BYTES = 1024 * 1024;
24687
+ function appendCrashBreadcrumb(logPath, kind, err, now = new Date) {
24688
+ try {
24689
+ try {
24690
+ if (statSync5(logPath).size > MAX_LOG_BYTES)
24691
+ renameSync2(logPath, `${logPath}.1`);
24692
+ } catch {}
24693
+ const detail = err instanceof Error ? err.stack ?? err.message : String(err);
24694
+ const folded = detail.replace(/\s*\n\s*/g, " | ").slice(0, 4000);
24695
+ appendFileSync2(logPath, `${now.toISOString()} ${kind} pid=${process.pid} ${folded}
24696
+ `);
24697
+ } catch {}
24698
+ }
24699
+
24684
24700
  // bridge/bridge.ts
24685
24701
  installPluginLogger();
24686
24702
  var STATE_DIR = process.env.TELEGRAM_STATE_DIR ?? join4(homedir3(), ".claude", "channels", "telegram");
@@ -24730,7 +24746,7 @@ var TOOL_SCHEMAS = [
24730
24746
  quote: { type: "boolean", description: "Opt out of the default quote-reply behavior. Default: true. Pass false to send a bare message with no quote reference. Ignored when reply_to is explicitly set." },
24731
24747
  message_thread_id: { type: "string", description: "Forum topic thread ID. Auto-applied from the last inbound message in the same chat if not specified." },
24732
24748
  origin_turn_id: { type: "string", description: "In a forum supergroup, pass back the origin_turn_id attribute from the <channel> message you are answering. It pins the reply to that message's topic even if another topic's turn started meanwhile. Omit in DMs / single-topic chats." },
24733
- files: { type: "array", items: { type: "string" }, description: "Absolute file paths to attach. Images send as photos; other types as documents. Max 50MB each." },
24749
+ files: { type: "array", items: { type: "string" }, description: "Absolute file paths to attach. Images send as photos; other types as documents. Max 50MB each. Telegram rejects photos with extreme dimensions (aspect ratio over ~10:1, width+height over 10000px, or over 10MB) \u2014 very tall images like full-page screenshots are auto-rerouted as documents; crop or split them first if the user should see them inline as photos." },
24734
24750
  format: { type: "string", enum: ["html", "markdownv2", "text"], description: "Rendering mode. 'html' (default) converts markdown to Telegram HTML." },
24735
24751
  disable_web_page_preview: { type: "boolean", description: "Disable link preview thumbnails. Default: true." },
24736
24752
  protect_content: { type: "boolean", description: "When true, Telegram prevents the message from being forwarded or saved." },
@@ -25380,9 +25396,16 @@ process.on("SIGINT", () => {
25380
25396
  cleanup();
25381
25397
  setTimeout(() => process.exit(0), 500);
25382
25398
  });
25399
+ var CRASH_LOG_PATH = join4(STATE_DIR, "bridge-crash.log");
25383
25400
  process.on("unhandledRejection", (err) => {
25384
25401
  process.stderr.write(`telegram bridge: unhandled rejection: ${err}
25385
25402
  `);
25403
+ appendCrashBreadcrumb(CRASH_LOG_PATH, "unhandledRejection", err);
25404
+ });
25405
+ process.on("uncaughtException", (err) => {
25406
+ process.stderr.write(`telegram bridge: uncaught exception (continuing): ${err?.stack ?? err}
25407
+ `);
25408
+ appendCrashBreadcrumb(CRASH_LOG_PATH, "uncaughtException", err);
25386
25409
  });
25387
25410
  async function main() {
25388
25411
  ipc = await createIpcClient({