switchroom 0.18.9 → 0.18.10

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 (62) 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 +28 -4
  5. package/dist/host-control/main.js +3 -2
  6. package/dist/vault/approvals/kernel-server.js +2 -1
  7. package/dist/vault/broker/server.js +2 -1
  8. package/package.json +1 -1
  9. package/profiles/_base/start.sh.hbs +119 -37
  10. package/profiles/_shared/dev-protocol.md.hbs +42 -0
  11. package/skills/dev-protocol/SKILL.md +131 -0
  12. package/telegram-plugin/README.md +2 -1
  13. package/telegram-plugin/admin-commands/dispatch.test.ts +40 -2
  14. package/telegram-plugin/admin-commands/index.ts +6 -1
  15. package/telegram-plugin/bridge/bridge.ts +23 -1
  16. package/telegram-plugin/bridge/crash-breadcrumb.ts +42 -0
  17. package/telegram-plugin/chat-lock.ts +13 -0
  18. package/telegram-plugin/dist/bridge/bridge.js +24 -1
  19. package/telegram-plugin/dist/gateway/gateway.js +1831 -263
  20. package/telegram-plugin/dist/server.js +29 -2
  21. package/telegram-plugin/fallback-card-collapse.ts +131 -0
  22. package/telegram-plugin/gateway/bridge-dead-watchdog.ts +546 -0
  23. package/telegram-plugin/gateway/effort-command.ts +47 -3
  24. package/telegram-plugin/gateway/gateway.ts +1435 -211
  25. package/telegram-plugin/gateway/model-command.ts +94 -8
  26. package/telegram-plugin/gateway/pending-session-command.ts +365 -0
  27. package/telegram-plugin/gateway/permission-timeout.ts +25 -0
  28. package/telegram-plugin/gateway/resume-inbound-builder.ts +23 -3
  29. package/telegram-plugin/gateway/session-model-file.ts +166 -23
  30. package/telegram-plugin/gateway/stop-command.ts +56 -0
  31. package/telegram-plugin/photo-precheck.ts +201 -0
  32. package/telegram-plugin/quota-watch.ts +141 -2
  33. package/telegram-plugin/registry/subagents-schema.ts +26 -3
  34. package/telegram-plugin/registry/subagents.test.ts +67 -0
  35. package/telegram-plugin/retry-api-call.ts +31 -0
  36. package/telegram-plugin/subagent-watcher.ts +392 -1
  37. package/telegram-plugin/tests/bridge-dead-watchdog.test.ts +576 -0
  38. package/telegram-plugin/tests/buffer-gate-broadened.test.ts +11 -5
  39. package/telegram-plugin/tests/chat-lock-unhandled-rejection.test.ts +101 -0
  40. package/telegram-plugin/tests/crash-breadcrumb.test.ts +57 -0
  41. package/telegram-plugin/tests/effort-command.test.ts +59 -2
  42. package/telegram-plugin/tests/fallback-card-collapse.test.ts +104 -0
  43. package/telegram-plugin/tests/gateway-pending-command-wiring.test.ts +124 -0
  44. package/telegram-plugin/tests/gateway-secret-detect.test.ts +7 -1
  45. package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +19 -11
  46. package/telegram-plugin/tests/model-command.test.ts +46 -3
  47. package/telegram-plugin/tests/pending-session-command.test.ts +322 -0
  48. package/telegram-plugin/tests/permission-timeout.test.ts +26 -0
  49. package/telegram-plugin/tests/permission-verdict-resume-guard.test.ts +16 -0
  50. package/telegram-plugin/tests/photo-dimension-fallback.test.ts +129 -0
  51. package/telegram-plugin/tests/photo-precheck.test.ts +240 -0
  52. package/telegram-plugin/tests/photo-reroute-wiring.test.ts +85 -0
  53. package/telegram-plugin/tests/quota-watch.test.ts +225 -0
  54. package/telegram-plugin/tests/session-model-file.test.ts +101 -2
  55. package/telegram-plugin/tests/stop-command.test.ts +234 -0
  56. package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +27 -9
  57. package/telegram-plugin/tests/subagent-watcher-resurrection.test.ts +398 -0
  58. package/telegram-plugin/tests/subagent-watcher-stall-terminal.test.ts +172 -0
  59. package/telegram-plugin/tests/worker-activity-feed.test.ts +37 -0
  60. package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +18 -4
  61. package/telegram-plugin/welcome-text.ts +4 -3
  62. package/telegram-plugin/worker-activity-feed.ts +27 -0
@@ -24286,6 +24286,25 @@ function createOutstandingPermissionLedger() {
24286
24286
  };
24287
24287
  }
24288
24288
 
24289
+ // bridge/crash-breadcrumb.ts
24290
+ import { appendFileSync as appendFileSync3, statSync as statSync6, renameSync as renameSync4 } from "node:fs";
24291
+ function appendCrashBreadcrumb(logPath, kind, err, now = new Date) {
24292
+ try {
24293
+ try {
24294
+ if (statSync6(logPath).size > MAX_LOG_BYTES)
24295
+ renameSync4(logPath, `${logPath}.1`);
24296
+ } catch {}
24297
+ const detail = err instanceof Error ? err.stack ?? err.message : String(err);
24298
+ const folded = detail.replace(/\s*\n\s*/g, " | ").slice(0, 4000);
24299
+ appendFileSync3(logPath, `${now.toISOString()} ${kind} pid=${process.pid} ${folded}
24300
+ `);
24301
+ } catch {}
24302
+ }
24303
+ var MAX_LOG_BYTES;
24304
+ var init_crash_breadcrumb = __esm(() => {
24305
+ MAX_LOG_BYTES = 1024 * 1024;
24306
+ });
24307
+
24289
24308
  // bridge/bridge.ts
24290
24309
  var exports_bridge = {};
24291
24310
  import { dirname as dirname3, join as join5 } from "path";
@@ -24394,7 +24413,7 @@ async function main() {
24394
24413
  }
24395
24414
  await mcp.connect(new StdioServerTransport);
24396
24415
  }
24397
- var STATE_DIR, SOCKET_PATH, TOPIC_ID, AGENT_NAME, mcp, TOOL_SCHEMAS, EFFECTIVE_TOOL_SCHEMAS, sessionAllowRules, outstandingPermissions, permissionRearmEnabled, ipc = null, sessionTailEnabled, sessionTailHandle = null, ptyTailEnabled, ptyTailHandle = null;
24416
+ var STATE_DIR, SOCKET_PATH, TOPIC_ID, AGENT_NAME, mcp, TOOL_SCHEMAS, EFFECTIVE_TOOL_SCHEMAS, sessionAllowRules, outstandingPermissions, permissionRearmEnabled, ipc = null, sessionTailEnabled, sessionTailHandle = null, ptyTailEnabled, ptyTailHandle = null, CRASH_LOG_PATH;
24398
24417
  var init_bridge = __esm(async () => {
24399
24418
  init_server2();
24400
24419
  init_stdio2();
@@ -24406,6 +24425,7 @@ var init_bridge = __esm(async () => {
24406
24425
  init_ipc_client();
24407
24426
  init_tool_filter();
24408
24427
  init_permission_rule();
24428
+ init_crash_breadcrumb();
24409
24429
  installPluginLogger2();
24410
24430
  STATE_DIR = process.env.TELEGRAM_STATE_DIR ?? join5(homedir4(), ".claude", "channels", "telegram");
24411
24431
  SOCKET_PATH = process.env.SWITCHROOM_GATEWAY_SOCKET ?? join5(STATE_DIR, "gateway.sock");
@@ -24454,7 +24474,7 @@ var init_bridge = __esm(async () => {
24454
24474
  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." },
24455
24475
  message_thread_id: { type: "string", description: "Forum topic thread ID. Auto-applied from the last inbound message in the same chat if not specified." },
24456
24476
  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." },
24457
- files: { type: "array", items: { type: "string" }, description: "Absolute file paths to attach. Images send as photos; other types as documents. Max 50MB each." },
24477
+ 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." },
24458
24478
  format: { type: "string", enum: ["html", "markdownv2", "text"], description: "Rendering mode. 'html' (default) converts markdown to Telegram HTML." },
24459
24479
  disable_web_page_preview: { type: "boolean", description: "Disable link preview thumbnails. Default: true." },
24460
24480
  protect_content: { type: "boolean", description: "When true, Telegram prevents the message from being forwarded or saved." },
@@ -25019,9 +25039,16 @@ var init_bridge = __esm(async () => {
25019
25039
  cleanup();
25020
25040
  setTimeout(() => process.exit(0), 500);
25021
25041
  });
25042
+ CRASH_LOG_PATH = join5(STATE_DIR, "bridge-crash.log");
25022
25043
  process.on("unhandledRejection", (err) => {
25023
25044
  process.stderr.write(`telegram bridge: unhandled rejection: ${err}
25024
25045
  `);
25046
+ appendCrashBreadcrumb(CRASH_LOG_PATH, "unhandledRejection", err);
25047
+ });
25048
+ process.on("uncaughtException", (err) => {
25049
+ process.stderr.write(`telegram bridge: uncaught exception (continuing): ${err?.stack ?? err}
25050
+ `);
25051
+ appendCrashBreadcrumb(CRASH_LOG_PATH, "uncaughtException", err);
25025
25052
  });
25026
25053
  await main().catch((err) => {
25027
25054
  process.stderr.write(`telegram bridge: fatal: ${err}
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Reactive-path message collapse (#3031 PR 3).
3
+ *
4
+ * Pre-fix, a mid-turn 429 emitted up to three separate operator messages:
5
+ * 1. the ⚠️ model-unavailable card (sent BEFORE the fallback outcome is
6
+ * known — deliberate, see the promise-honesty note below),
7
+ * 2. the fleet-fallback success announcement ("Switched to <account> …"),
8
+ * 3. the resume/boot notice.
9
+ *
10
+ * This module owns the SAFE part of collapsing that flow: when the swap
11
+ * SUCCEEDS, the announcement is folded into an EDIT of the just-sent
12
+ * model-unavailable card (one evolving card) instead of a second message.
13
+ *
14
+ * CRITICAL constraint (2026-06-06→07 incident): the card is deliberately
15
+ * sent before the outcome is known, and a FAILURE notice must arrive as its
16
+ * own message on EVERY error path — the card promised an announcement, and
17
+ * an edit that never happens (or a swallowed failure) breaks that promise.
18
+ * Therefore:
19
+ * - `decideAnnouncementDelivery` returns 'edit' ONLY for a successful
20
+ * 'switched' outcome with a fresh, fallback-promising card on record.
21
+ * Every other outcome kind ('all-blocked', 'no-old-active',
22
+ * 'no-eligible-target', errors) is 'send' — a separate message, exactly
23
+ * the pre-fix behaviour.
24
+ * - The gateway falls back to a plain send when the edit itself throws.
25
+ *
26
+ * Pure module (injected clock, no Telegram/bot imports) so the decision
27
+ * logic is unit-testable at the seam.
28
+ */
29
+
30
+ /** One recorded model-unavailable card, per chat. */
31
+ export interface ModelUnavailableCardRecord {
32
+ /** Telegram message id of the sent card (null until the send resolves —
33
+ * callers should record only after the send succeeds). */
34
+ messageId: number;
35
+ /** The rendered card text as sent, so the edit can append to it. */
36
+ text: string;
37
+ /** Unix ms the card send resolved. */
38
+ atMs: number;
39
+ /** Did the card promise an in-flight auto-failover? Only such cards may
40
+ * absorb the announcement — a manual-hint card never promised one. */
41
+ promisedFallback: boolean;
42
+ }
43
+
44
+ /**
45
+ * Freshness ceiling for folding into a card. The fallback dispatcher runs
46
+ * seconds after the card in the normal case; a card older than this belongs
47
+ * to a previous incident and must not absorb an unrelated announcement.
48
+ *
49
+ * Known accepted race (#3035 review, finding 4): the record lands only
50
+ * AFTER the card send resolves, and stays editable for this whole window —
51
+ * so a LATER 'switched' outcome (a second dispatch inside the window) can
52
+ * edit a card whose incident was already answered. Bounded by this 5-min
53
+ * ceiling plus the one-shot `take` semantics (a card absorbs at most one
54
+ * announcement); worst case is one announcement folded into a slightly
55
+ * older card instead of arriving as a separate message — never a lost
56
+ * announcement.
57
+ */
58
+ export const CARD_COLLAPSE_MAX_AGE_MS = 5 * 60_000;
59
+
60
+ export interface ModelUnavailableCardRegistry {
61
+ /** Record the card most recently sent to `chatId` (replaces any prior). */
62
+ record(chatId: string, rec: ModelUnavailableCardRecord): void;
63
+ /**
64
+ * One-shot take: return the fresh, fallback-promising card for `chatId`
65
+ * and remove it (a card absorbs at most one announcement). Returns null —
66
+ * and clears the entry — when the record is stale or never promised a
67
+ * fallback.
68
+ */
69
+ take(chatId: string, now: number): ModelUnavailableCardRecord | null;
70
+ /** Number of chats with a recorded card (test/introspection helper). */
71
+ size(): number;
72
+ }
73
+
74
+ export function createModelUnavailableCardRegistry(
75
+ maxAgeMs: number = CARD_COLLAPSE_MAX_AGE_MS,
76
+ ): ModelUnavailableCardRegistry {
77
+ const cards = new Map<string, ModelUnavailableCardRecord>();
78
+ return {
79
+ record(chatId, rec) {
80
+ cards.set(chatId, rec);
81
+ },
82
+ take(chatId, now) {
83
+ const rec = cards.get(chatId);
84
+ if (!rec) return null;
85
+ cards.delete(chatId);
86
+ if (!rec.promisedFallback) return null;
87
+ if (now - rec.atMs > maxAgeMs) return null;
88
+ return rec;
89
+ },
90
+ size() {
91
+ return cards.size;
92
+ },
93
+ };
94
+ }
95
+
96
+ /**
97
+ * The outcome kinds `runFleetAutoFallback` can produce, plus 'error' for
98
+ * the dispatcher-threw path (broker unreachable, listState/markExhausted
99
+ * throw). Kept as a plain string union so this module stays import-free.
100
+ */
101
+ export type FallbackDeliveryOutcomeKind =
102
+ | 'switched'
103
+ | 'all-blocked'
104
+ | 'no-old-active'
105
+ | 'no-eligible-target'
106
+ | 'error';
107
+
108
+ /**
109
+ * Should the announcement for `outcomeKind` be delivered by EDITING the
110
+ * recorded model-unavailable card, or by SENDING a separate message?
111
+ *
112
+ * 'edit' is returned ONLY when the swap succeeded AND a fresh
113
+ * fallback-promising card exists for the chat. Every failure / no-op
114
+ * outcome is 'send' — the promise-honesty contract: a failure notice must
115
+ * arrive as its own message on every error path.
116
+ */
117
+ export function decideAnnouncementDelivery(
118
+ outcomeKind: FallbackDeliveryOutcomeKind,
119
+ card: ModelUnavailableCardRecord | null,
120
+ ): 'edit' | 'send' {
121
+ if (outcomeKind !== 'switched') return 'send';
122
+ return card !== null ? 'edit' : 'send';
123
+ }
124
+
125
+ /**
126
+ * The single evolving card: original card text plus the announcement,
127
+ * separated by a rule so the "before/after" reads as one incident timeline.
128
+ */
129
+ export function foldAnnouncementIntoCard(cardText: string, announcement: string): string {
130
+ return `${cardText}\n\n${announcement}`;
131
+ }