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.
- package/dist/agent-scheduler/index.js +1 -0
- package/dist/auth-broker/index.js +198 -13
- package/dist/cli/notion-write-pretool.mjs +1 -0
- package/dist/cli/switchroom.js +214 -83
- package/dist/cli/ui/apple-touch-icon.png +0 -0
- package/dist/cli/ui/favicon-32.png +0 -0
- package/dist/cli/ui/favicon.ico +0 -0
- package/dist/host-control/main.js +760 -257
- package/dist/vault/approvals/kernel-server.js +3 -1
- package/dist/vault/broker/server.js +3 -1
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +119 -37
- package/profiles/_shared/dev-protocol.md.hbs +42 -0
- package/skills/dev-protocol/SKILL.md +131 -0
- package/telegram-plugin/README.md +2 -1
- package/telegram-plugin/admin-commands/dispatch.test.ts +40 -2
- package/telegram-plugin/admin-commands/index.ts +6 -1
- package/telegram-plugin/bridge/bridge.ts +23 -1
- package/telegram-plugin/bridge/crash-breadcrumb.ts +42 -0
- package/telegram-plugin/chat-lock.ts +13 -0
- package/telegram-plugin/dist/bridge/bridge.js +24 -1
- package/telegram-plugin/dist/gateway/gateway.js +1832 -263
- package/telegram-plugin/dist/server.js +29 -2
- package/telegram-plugin/fallback-card-collapse.ts +131 -0
- package/telegram-plugin/gateway/bridge-dead-watchdog.ts +546 -0
- package/telegram-plugin/gateway/effort-command.ts +47 -3
- package/telegram-plugin/gateway/gateway.ts +1435 -211
- package/telegram-plugin/gateway/model-command.ts +94 -8
- package/telegram-plugin/gateway/pending-session-command.ts +365 -0
- package/telegram-plugin/gateway/permission-timeout.ts +25 -0
- package/telegram-plugin/gateway/resume-inbound-builder.ts +23 -3
- package/telegram-plugin/gateway/session-model-file.ts +166 -23
- package/telegram-plugin/gateway/stop-command.ts +56 -0
- package/telegram-plugin/photo-precheck.ts +201 -0
- package/telegram-plugin/quota-watch.ts +141 -2
- package/telegram-plugin/registry/subagents-schema.ts +26 -3
- package/telegram-plugin/registry/subagents.test.ts +67 -0
- package/telegram-plugin/retry-api-call.ts +31 -0
- package/telegram-plugin/subagent-watcher.ts +392 -1
- package/telegram-plugin/tests/bridge-dead-watchdog.test.ts +576 -0
- package/telegram-plugin/tests/buffer-gate-broadened.test.ts +11 -5
- package/telegram-plugin/tests/chat-lock-unhandled-rejection.test.ts +101 -0
- package/telegram-plugin/tests/crash-breadcrumb.test.ts +57 -0
- package/telegram-plugin/tests/effort-command.test.ts +59 -2
- package/telegram-plugin/tests/fallback-card-collapse.test.ts +104 -0
- package/telegram-plugin/tests/gateway-pending-command-wiring.test.ts +124 -0
- package/telegram-plugin/tests/gateway-secret-detect.test.ts +7 -1
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +19 -11
- package/telegram-plugin/tests/model-command.test.ts +46 -3
- package/telegram-plugin/tests/pending-session-command.test.ts +322 -0
- package/telegram-plugin/tests/permission-timeout.test.ts +26 -0
- package/telegram-plugin/tests/permission-verdict-resume-guard.test.ts +16 -0
- package/telegram-plugin/tests/photo-dimension-fallback.test.ts +129 -0
- package/telegram-plugin/tests/photo-precheck.test.ts +240 -0
- package/telegram-plugin/tests/photo-reroute-wiring.test.ts +85 -0
- package/telegram-plugin/tests/quota-watch.test.ts +225 -0
- package/telegram-plugin/tests/session-model-file.test.ts +101 -2
- package/telegram-plugin/tests/stop-command.test.ts +234 -0
- package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +27 -9
- package/telegram-plugin/tests/subagent-watcher-resurrection.test.ts +398 -0
- package/telegram-plugin/tests/subagent-watcher-stall-terminal.test.ts +172 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +37 -0
- package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +18 -4
- package/telegram-plugin/welcome-text.ts +4 -3
- 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({
|