switchroom 0.13.52 → 0.13.54

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 (39) hide show
  1. package/dist/agent-scheduler/index.js +399 -213
  2. package/dist/auth-broker/index.js +576 -237
  3. package/dist/cli/drive-write-pretool.mjs +28 -13
  4. package/dist/cli/ms-365-write-pretool.mjs +259 -0
  5. package/dist/cli/skill-validate-pretool.mjs +72 -72
  6. package/dist/cli/switchroom.js +3241 -1382
  7. package/dist/host-control/main.js +396 -276
  8. package/dist/vault/approvals/kernel-server.js +8266 -8142
  9. package/dist/vault/broker/server.js +2894 -2770
  10. package/package.json +1 -1
  11. package/profiles/_base/start.sh.hbs +17 -0
  12. package/profiles/_shared/telegram-style.md.hbs +2 -0
  13. package/skills/switchroom-status/SKILL.md +8 -6
  14. package/telegram-plugin/chat-lock.ts +87 -19
  15. package/telegram-plugin/dist/bridge/bridge.js +112 -112
  16. package/telegram-plugin/dist/gateway/gateway.js +1283 -343
  17. package/telegram-plugin/dist/server.js +160 -160
  18. package/telegram-plugin/gateway/disconnect-flush.ts +32 -0
  19. package/telegram-plugin/gateway/gateway.ts +485 -72
  20. package/telegram-plugin/gateway/inbound-coalesce.ts +19 -6
  21. package/telegram-plugin/gateway/ipc-protocol.ts +37 -0
  22. package/telegram-plugin/gateway/ipc-server.ts +59 -0
  23. package/telegram-plugin/gateway/ms365-write-approval.test.ts +314 -0
  24. package/telegram-plugin/gateway/ms365-write-approval.ts +335 -0
  25. package/telegram-plugin/stream-reply-handler.ts +10 -8
  26. package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +116 -0
  27. package/telegram-plugin/tests/inbound-coalesce.test.ts +20 -4
  28. package/telegram-plugin/tests/ipc-validator.test.ts +61 -0
  29. package/telegram-plugin/tests/outbound-ordering.test.ts +228 -0
  30. package/telegram-plugin/tests/parallel-turns-deadlock-fix.test.ts +217 -0
  31. package/telegram-plugin/tests/slash-command-smart-split.test.ts +115 -0
  32. package/telegram-plugin/tests/typing-wrap.test.ts +65 -8
  33. package/telegram-plugin/typing-wrap.ts +43 -21
  34. package/vendor/hindsight-memory/scripts/lib/gateway_ipc.py +35 -0
  35. package/vendor/hindsight-memory/scripts/recall.py +164 -4
  36. package/vendor/hindsight-memory/scripts/retain.py +52 -0
  37. package/vendor/hindsight-memory/scripts/tests/test_gateway_ipc.py +42 -0
  38. package/vendor/hindsight-memory/scripts/tests/test_recall_topic_filter.py +139 -0
  39. package/profiles/default/CLAUDE.md +0 -122
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.13.52",
3
+ "version": "0.13.54",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -239,6 +239,23 @@ export HINDSIGHT_RECALL_CACHE_TTL_SECS={{hindsightRecallCacheTtlSecs}}
239
239
  {{#if (isNumber hindsightRecallMinOverlap)}}
240
240
  export HINDSIGHT_RECALL_MIN_OVERLAP={{hindsightRecallMinOverlap}}
241
241
  {{/if}}
242
+ # PR6 — supergroup-mode topic tagging. JSON map of {alias: thread_id}
243
+ # parsed by retain.py + recall.py to (a) stamp chat_id/thread_id/topic_alias
244
+ # into retained memory metadata and (b) emit a "Current topic: …" preamble
245
+ # on recall blocks so the model self-scopes. Empty / absent for fleet-shared
246
+ # or DM agents where the supergroup topology isn't in use.
247
+ {{#if hindsightTopicAliasesJsonQ}}
248
+ export HINDSIGHT_TOPIC_ALIASES_JSON={{{hindsightTopicAliasesJsonQ}}}
249
+ {{/if}}
250
+ # PR6 — topic filter mode for cross-topic memory recall. Default
251
+ # "soft-preamble": all topic-tagged memories surface and the model
252
+ # decides relevance via the preamble. "hard-filter": drop memories
253
+ # whose source thread_id differs from the active prompt's thread_id.
254
+ # Operators flip this when instrumentation (the active_thread_id /
255
+ # source_topics fields in recall_log.jsonl) shows binding failures.
256
+ {{#if hindsightTopicFilterMode}}
257
+ export HINDSIGHT_TOPIC_FILTER_MODE={{hindsightTopicFilterMode}}
258
+ {{/if}}
242
259
  # Wait for Hindsight API to be reachable before launching Claude, otherwise
243
260
  # the MCP server connection fails at startup with "1 MCP server failed".
244
261
  HINDSIGHT_WAIT=0
@@ -21,6 +21,8 @@ The 👀→🤔→🔥→👍 status reaction and the typing indicator are *ambi
21
21
 
22
22
  **Reactions ON your replies.** Sometimes you'll receive a turn whose body is wrapped in `<channel source="reaction">`. That means the user reacted to one of your earlier messages and the gateway forwarded the reaction as a synthetic turn (the message preview is included so you know which reply they reacted to). 👎 / ❌ are stop signals — pause, reconsider the approach, ask what's off. 👍 / ✅ are acknowledgements — keep going if mid-task, no extra reply needed. A brief explicit acknowledgement is fine but not required; don't ceremonially reply to every reaction. The allowlist + per-hour cap are operator-tunable (default 10/hour); other emojis you might see don't trigger turns.
23
23
 
24
+ **Topics are organizational, you are one identity.** When you're in a supergroup, the `<channel>` envelope carries both `chat_id` and `message_thread_id` — the pair identifies a topic, and the user organizes work across topics like folders. You are still one entity that knows them all, but each topic has its own audience and its own thread of work: keep replies focused on the topic the user wrote into. Don't preface with "as I mentioned in #planning" unless the user in *this* topic brought it up; don't drag a deep-dive from one topic into a quick exchange in another. Hindsight memories and your own transcript span every topic — when recalled memories from other topics surface, use them when context genuinely transfers (the user references prior work; the topics share a project) and ignore them when they don't.
25
+
24
26
  **Follow-ups while a turn is in flight.** Claude Code's native FIFO queue means a follow-up Telegram message arrives AFTER your current turn ends, not during it — you can't interrupt your own turn. Every follow-up becomes the next prompt you see. The plugin enriches the `<channel>` meta so you can classify correctly:
25
27
 
26
28
  - `queued="true"` — DEFAULT for mid-turn follow-ups (no prefix). Treat as a new, independent task. Do NOT reference the in-flight work — start fresh. Also fires when the user typed `/queue ` or `/q ` (legacy alias; the prefix is stripped from the body you see).
@@ -32,23 +32,25 @@ description: >
32
32
 
33
33
  When the user asks about agent status, what's running, uptime, or wants to see agent info, answer by running (or telling them to run) `switchroom agent list` — this is the canonical command for showing running agents, their uptime, and current state.
34
34
 
35
- ## Step 1 — Always mention `switchroom agent list` in your response
35
+ ## Step 1 — Always mention `switchroom status` in your response
36
36
 
37
- The answer to "what agents are running", "show me agent info", "list all switchroom agents", or any uptime question is the `switchroom agent list` command. Your response MUST include the literal command string `switchroom agent list` so the user can copy it. If you have Bash tool access, run it and include the output. If you do not have Bash access, or the command fails in the current environment, still tell the user explicitly:
37
+ The answer to "what agents are running", "show me agent info", "list all switchroom agents", or any uptime question is the `switchroom status` command (since v0.13.51). Your response MUST include the literal command string `switchroom status` so the user can copy it. If you have Bash tool access, run it and include the output. If you do not have Bash access, or the command fails in the current environment, still tell the user explicitly:
38
38
 
39
- > Run `switchroom agent list` from your switchroom project directory to see running agents, their uptime, and status.
39
+ > Run `switchroom status` from your switchroom project directory to see running agents (uptime + scheduler), known auth accounts, and per-agent MCP connection state.
40
40
 
41
- Do not respond with a PATH-not-found bailout or a "no config found" diagnosis without first giving the user the command — the eval environment may not have a config on cwd, but on the user's actual machine `switchroom agent list` is the right command.
41
+ Do not respond with a PATH-not-found bailout or a "no config found" diagnosis without first giving the user the command — the eval environment may not have a config on cwd, but on the user's actual machine `switchroom status` is the right command. (Pre-v0.13.51 the canonical command was `switchroom agent list` — that still works but only shows the Fleet section.)
42
42
 
43
43
  ## Step 2 — Try to run it
44
44
 
45
45
  If you have Bash tool access, run:
46
46
 
47
47
  ```bash
48
- switchroom agent list --json 2>/dev/null || switchroom agent list
48
+ switchroom status --json 2>/dev/null || switchroom status
49
49
  ```
50
50
 
51
- If that succeeds, parse the output and present the running agent list with full uptime, status, and model details (see Step 3). If it fails (e.g. command not found, no config in cwd), still include the `switchroom agent list` command and the word "uptime" in your text response the user needs those as actionable information.
51
+ `switchroom status` returns three sections: **Fleet** (per-agent uptime + scheduler), **Accounts** (broker-known auth accounts with active marker), and **MCPs** (per-agent MCP connection state). If you want to skip the MCP probe (slower because it does a docker exec per agent), pass `--no-mcp`.
52
+
53
+ If `switchroom status` fails (e.g. command not found, no config in cwd), fall back to `switchroom agent list` (older command, Fleet-only). Still include the `switchroom status` command and the word "uptime" in your text response — the user needs those as actionable information.
52
54
 
53
55
  ## Step 3 — For each agent, report running state and uptime
54
56
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Per-chat FIFO serialization for outbound Telegram API calls.
2
+ * Per-(chat, thread) FIFO serialization for outbound Telegram API calls.
3
3
  *
4
4
  * Without this, concurrent MCP tool handlers (reply, stream_reply, react,
5
5
  * edit_message, delete_message, pin_message, forward_message) all call
@@ -8,32 +8,69 @@
8
8
  * `stream_reply` edit, or a `react` can resolve before the `reply` it
9
9
  * reacts to.
10
10
  *
11
- * The fix is a per-chat promise chain: every handler acquires the lock
12
- * for its `chat_id`, dispatches its API call, then releases. Granularity
13
- * is `chat_id` (not chat+thread) — different chats run concurrently.
11
+ * The fix is a per-key promise chain: every handler acquires the lock
12
+ * for its `(chat_id, message_thread_id)` pair, dispatches its API call,
13
+ * then releases. Granularity moved from `chat_id` to `(chat, thread)`
14
+ * in PR2 of the supergroup-mode rollout (CPO ratified 2026-05-27,
15
+ * decision #8=B): for a supergroup agent owning many topics, the old
16
+ * per-chat lock artificially serialized cross-topic sends (an
17
+ * `#alerts` digest queueing behind eight `#planning` stream edits).
18
+ * Now different threads in the same chat dispatch concurrently;
19
+ * per-thread ordering is still preserved (`sendMessage` then
20
+ * `editMessageText` on the returned message_id can't race).
21
+ *
22
+ * Methods without `message_thread_id` in their options (edits,
23
+ * deletes, reactions, pins — Telegram infers thread from message_id)
24
+ * key by `chat_id` alone via the canonical `chatKey(chat, null)`
25
+ * collapse — same behavior as before for those callsites.
26
+ *
27
+ * **General-topic strip (PR4a of supergroup-mode):** Telegram's General
28
+ * topic has `id=1` at the MTProto layer, but the Bot API's send-side
29
+ * REJECTS `message_thread_id=1` with HTTP 400 "message thread not
30
+ * found" (see tdlib/telegram-bot-api#447). The wrapper detects an
31
+ * incoming `message_thread_id: 1`, strips it from the options bag
32
+ * before the underlying API call, AND normalizes the lock key as if
33
+ * the thread were null — since semantically id=1 IS the chat root
34
+ * for the Bot API send-side, treating it as a distinct lane would
35
+ * mean two General-topic sends in the same chat would race past
36
+ * each other. (`editMessageText` etc. don't accept `message_thread_id`
37
+ * at all, so this only fires for sends like `sendMessage` and
38
+ * `sendChatAction`.)
39
+ *
40
+ * Telegram's per-chat rate ceiling is still respected via grammY's
41
+ * autoRetry transformer; if two topics in the same chat both burst
42
+ * past the limit, grammY backs off per the Retry-After header — same
43
+ * worst-case behavior as the old per-chat lock, just hit differently.
44
+ * See docs/rfcs/supergroup-mode.md and the
45
+ * `tests/outbound-ordering.test.ts` 429-isolation row.
14
46
  */
15
47
 
48
+ import { chatKey } from './gateway/chat-key.js'
49
+
16
50
  export interface ChatLock {
17
- /** Run `fn` serialized against other work on the same `chatId`. */
18
- run<T>(chatId: string, fn: () => Promise<T>): Promise<T>
19
- /** Wrap a bot.api-shaped object so every method auto-locks on its first arg. */
51
+ /** Run `fn` serialized against other work on the same `key`. */
52
+ run<T>(key: string, fn: () => Promise<T>): Promise<T>
53
+ /**
54
+ * Wrap a bot.api-shaped object so every method auto-locks by
55
+ * `(chat_id, message_thread_id)` pair extracted from its arguments.
56
+ */
20
57
  wrapBot<B extends { api: Record<string, unknown> }>(bot: B): B
21
58
  }
22
59
 
23
60
  export function createChatLock(): ChatLock {
24
61
  const chains = new Map<string, Promise<unknown>>()
25
62
 
26
- function run<T>(chatId: string, fn: () => Promise<T>): Promise<T> {
27
- const prior = chains.get(chatId) ?? Promise.resolve()
63
+ function run<T>(key: string, fn: () => Promise<T>): Promise<T> {
64
+ const prior = chains.get(key) ?? Promise.resolve()
28
65
  // Swallow the prior result/error for the chain we're about to build on,
29
- // so one failure doesn't poison the whole chat's queue.
66
+ // so one failure doesn't poison the whole queue.
30
67
  const next = prior.then(fn, fn)
31
68
  // Keep the chain alive only while work is pending. When this call is
32
69
  // the tail, clear the map entry to avoid unbounded growth.
33
70
  const tracked = next.finally(() => {
34
- if (chains.get(chatId) === tracked) chains.delete(chatId)
71
+ if (chains.get(key) === tracked) chains.delete(key)
35
72
  })
36
- chains.set(chatId, tracked)
73
+ chains.set(key, tracked)
37
74
  return next
38
75
  }
39
76
 
@@ -45,14 +82,45 @@ export function createChatLock(): ChatLock {
45
82
  return function (this: unknown, ...args: unknown[]) {
46
83
  // By Telegram Bot API convention, the first positional arg of
47
84
  // every chat-scoped method is `chat_id`. Methods without one
48
- // (getMe, getFile, setMyCommands) fall through as a string of
49
- // their own which is fine; they don't need per-chat ordering.
85
+ // (getMe, getFile, setMyCommands) fall through to a global
86
+ // lane — they have no per-chat ordering constraint.
50
87
  const first = args[0]
51
- const key =
52
- typeof first === 'string' || typeof first === 'number'
53
- ? String(first)
54
- : '__global__'
55
- return run(key, () =>
88
+ if (typeof first !== 'string' && typeof first !== 'number') {
89
+ return run('__global__', () =>
90
+ (orig as (...a: unknown[]) => Promise<unknown>).apply(target, args),
91
+ )
92
+ }
93
+ const chatId = String(first)
94
+ // Try to extract `message_thread_id` from the LAST arg if it's
95
+ // a plain options object. Telegram API convention: options bag
96
+ // is the last positional. Arrays (e.g. `setMessageReaction`'s
97
+ // reactions list) are explicitly excluded — only plain objects
98
+ // can carry message_thread_id. Edits / deletes / reactions /
99
+ // pins infer thread from message_id; they fall through with
100
+ // `null` thread and serialize per-chat as before.
101
+ const last = args[args.length - 1]
102
+ const lastIsOpts =
103
+ last != null &&
104
+ typeof last === 'object' &&
105
+ !Array.isArray(last)
106
+ const threadFromOpts = lastIsOpts
107
+ ? (last as Record<string, unknown>).message_thread_id
108
+ : undefined
109
+ let tid = typeof threadFromOpts === 'number' ? threadFromOpts : null
110
+ // General-topic strip: id=1 is rejected by the Bot API on send.
111
+ // Strip from opts AND normalize the lock key as if thread were
112
+ // null (semantically id=1 IS chat-root for the send-side). See
113
+ // the file-header comment + RFC for the full rationale.
114
+ if (tid === 1) {
115
+ tid = null
116
+ // Rebuild args with a copy of opts that drops message_thread_id,
117
+ // so the underlying bot.api call won't receive the rejected
118
+ // value. Don't mutate the caller's opts object.
119
+ const cleanedOpts: Record<string, unknown> = { ...(last as Record<string, unknown>) }
120
+ delete cleanedOpts.message_thread_id
121
+ args = args.slice(0, -1).concat([cleanedOpts])
122
+ }
123
+ return run(chatKey(chatId, tid), () =>
56
124
  (orig as (...a: unknown[]) => Promise<unknown>).apply(target, args),
57
125
  )
58
126
  }