switchroom 0.13.51 → 0.13.53
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 +317 -132
- package/dist/auth-broker/index.js +494 -156
- package/dist/cli/drive-write-pretool.mjs +18 -3
- package/dist/cli/switchroom.js +2452 -1114
- package/dist/host-control/main.js +246 -127
- package/dist/vault/approvals/kernel-server.js +8269 -8146
- package/dist/vault/broker/server.js +2811 -2688
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +17 -4
- package/profiles/_shared/agent-self-service.md.hbs +12 -22
- package/profiles/coding/CLAUDE.md.hbs +1 -1
- package/profiles/default/CLAUDE.md.hbs +8 -1
- package/profiles/executive-assistant/CLAUDE.md.hbs +1 -1
- package/profiles/health-coach/CLAUDE.md.hbs +1 -1
- package/skills/switchroom-status/SKILL.md +8 -6
- package/telegram-plugin/chat-lock.ts +87 -19
- package/telegram-plugin/dist/gateway/gateway.js +752 -120
- package/telegram-plugin/gateway/disconnect-flush.ts +32 -0
- package/telegram-plugin/gateway/gateway.ts +258 -55
- package/telegram-plugin/gateway/inbound-coalesce.ts +19 -6
- package/telegram-plugin/stream-reply-handler.ts +10 -8
- package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +116 -0
- package/telegram-plugin/tests/inbound-coalesce.test.ts +20 -4
- package/telegram-plugin/tests/outbound-ordering.test.ts +228 -0
- package/telegram-plugin/tests/parallel-turns-deadlock-fix.test.ts +217 -0
- package/telegram-plugin/tests/typing-wrap.test.ts +65 -8
- package/telegram-plugin/typing-wrap.ts +43 -21
package/package.json
CHANGED
|
@@ -568,16 +568,29 @@ for f in \
|
|
|
568
568
|
fi
|
|
569
569
|
done
|
|
570
570
|
|
|
571
|
+
# Fleet directory — epic #1850 / issue #1852. Extends Claude Code's
|
|
572
|
+
# native CLAUDE.md auto-discovery to `~/.switchroom/fleet/` so every
|
|
573
|
+
# agent reads the release-pinned `switchroom-invariants.md` (lane 1)
|
|
574
|
+
# and operator-owned `CLAUDE.md` (lane 2) from there. Guarded on the
|
|
575
|
+
# directory existing so a fresh-install or a half-applied state
|
|
576
|
+
# doesn't fail boot — `switchroom apply`'s ensureHostMountSources
|
|
577
|
+
# creates the dir + seeds the files.
|
|
578
|
+
SR_FLEET_DIR="$HOME/.switchroom/fleet"
|
|
579
|
+
SR_FLEET_ARG=""
|
|
580
|
+
if [ -d "$SR_FLEET_DIR" ]; then
|
|
581
|
+
SR_FLEET_ARG="--add-dir $SR_FLEET_DIR"
|
|
582
|
+
fi
|
|
583
|
+
|
|
571
584
|
{{#if useSwitchroomPlugin}}
|
|
572
585
|
if [ -n "$APPEND_PROMPT" ]; then
|
|
573
|
-
exec claude $CONTINUE_FLAG --dangerously-load-development-channels server:switchroom-telegram --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}}{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}} --append-system-prompt "$APPEND_PROMPT"{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
|
|
586
|
+
exec claude $CONTINUE_FLAG --dangerously-load-development-channels server:switchroom-telegram --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}} $SR_FLEET_ARG{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}} --append-system-prompt "$APPEND_PROMPT"{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
|
|
574
587
|
else
|
|
575
|
-
exec claude $CONTINUE_FLAG --dangerously-load-development-channels server:switchroom-telegram --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}}{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}}{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
|
|
588
|
+
exec claude $CONTINUE_FLAG --dangerously-load-development-channels server:switchroom-telegram --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}} $SR_FLEET_ARG{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}}{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
|
|
576
589
|
fi
|
|
577
590
|
{{else}}
|
|
578
591
|
if [ -n "$APPEND_PROMPT" ]; then
|
|
579
|
-
exec claude $CONTINUE_FLAG --channels plugin:telegram@claude-plugins-official --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}}{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}} --append-system-prompt "$APPEND_PROMPT"{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
|
|
592
|
+
exec claude $CONTINUE_FLAG --channels plugin:telegram@claude-plugins-official --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}} $SR_FLEET_ARG{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}} --append-system-prompt "$APPEND_PROMPT"{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
|
|
580
593
|
else
|
|
581
|
-
exec claude $CONTINUE_FLAG --channels plugin:telegram@claude-plugins-official --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}}{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}}{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
|
|
594
|
+
exec claude $CONTINUE_FLAG --channels plugin:telegram@claude-plugins-official --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}} $SR_FLEET_ARG{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}}{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
|
|
582
595
|
fi
|
|
583
596
|
{{/if}}
|
|
@@ -22,6 +22,9 @@ tools is to let you do the edit yourself.
|
|
|
22
22
|
| "what other agents are running here?" / "is there an agent that does X?" / "who handles Y?" | `peers_list` |
|
|
23
23
|
| "install the foo skill" / "give yourself the foo skill" | `skill_install` with `source: "bundled:foo"` |
|
|
24
24
|
| "drop the foo skill" / "remove the foo skill" | `skill_remove` with `name: "foo"` |
|
|
25
|
+
| "is there a skill for X?" | `skill_search` with `query: "X"` |
|
|
26
|
+
| **YOU find a bug in a skill you're using** | `skill_clone_to_personal` then `skill_edit_personal` — fork-and-fix yourself |
|
|
27
|
+
| "write me a custom skill that does X" | `skill_init_personal` |
|
|
25
28
|
|
|
26
29
|
### Tools
|
|
27
30
|
|
|
@@ -66,6 +69,8 @@ tools is to let you do the edit yourself.
|
|
|
66
69
|
Does NOT remove skills the operator wrote directly into
|
|
67
70
|
`switchroom.yaml` — those are removed by the operator only.
|
|
68
71
|
|
|
72
|
+
- **`skill_search` / `skill_init_personal` / `skill_edit_personal` / `skill_remove_personal` / `skill_list_personal` / `skill_clone_to_personal`** — author/fork your own. `files` is `{path: content}` JSON; allowlist `SKILL.md`, `scripts/*.{sh,py}`, `references/*.md`.
|
|
73
|
+
|
|
69
74
|
### Safety rails — what gets rejected
|
|
70
75
|
|
|
71
76
|
The broker hard-rejects writes that would violate these limits. Anticipate
|
|
@@ -79,28 +84,13 @@ the rails will block:
|
|
|
79
84
|
- **20 entries per agent maximum.** `E_QUOTA_EXCEEDED`. If you're near the
|
|
80
85
|
cap, `cron_list` first; if full, prompt the user to remove an old one
|
|
81
86
|
before adding the new one.
|
|
82
|
-
- **No `secrets:` on agent-authored entries
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
- **
|
|
88
|
-
|
|
89
|
-
gateway sets when spawning your CLI — calls passing
|
|
90
|
-
`agent: "<other-agent>"` that doesn't match the pin are rejected. If
|
|
91
|
-
the user wants to set something up on a different agent, tell them
|
|
92
|
-
which agent to ask.
|
|
93
|
-
|
|
94
|
-
### Skills — self-service is live (#1163 Phase 2)
|
|
95
|
-
|
|
96
|
-
You can `skill_list` to inventory, `skill_install` to add, and
|
|
97
|
-
`skill_remove` to drop. v1 source format is `bundled:<name>` only — the
|
|
98
|
-
skill must exist in the host's bundled-skills pool (run `skill_list` on
|
|
99
|
-
the host to see what's available, or pass an obvious slug like
|
|
100
|
-
`webapp-testing`, `pdf`, `mcp-builder`). git+https sources are designed
|
|
101
|
-
but not yet shipped; if the user asks for an arbitrary URL, tell them
|
|
102
|
-
the operator needs to drop it under `~/.switchroom/skills/<name>/` and
|
|
103
|
-
run `switchroom apply`.
|
|
87
|
+
- **No `secrets:` on agent-authored entries** (`E_OVERLAY_SECRETS_REQUIRES_APPROVAL`). Cron fires the prompt; you go through `vault_request_access` at runtime.
|
|
88
|
+
- **Cross-agent writes rejected** — you manage only your own schedule. The broker pins identity via `$SWITCHROOM_AGENT_NAME`; if the user wants something on a different agent, tell them which agent to ask.
|
|
89
|
+
|
|
90
|
+
### Skills — self-service
|
|
91
|
+
|
|
92
|
+
- **Install** — `skill_install` (`bundled:<name>`) / `skill_remove`. git+https deferred.
|
|
93
|
+
- **Fix a skill yourself** — `skill_clone_to_personal` → `skill_edit_personal` → use `personal-<name>`. Fork is durable + auto-mirrors to `~/.switchroom-config/` when present.
|
|
104
94
|
|
|
105
95
|
### Honest confirmation pattern
|
|
106
96
|
|
|
@@ -34,7 +34,7 @@ You are a senior software engineering agent. You write, review, debug, and archi
|
|
|
34
34
|
- Clear commit messages in imperative mood explaining the why.
|
|
35
35
|
- Atomic commits. PR descriptions explain what, why, and how to test.
|
|
36
36
|
|
|
37
|
-
{{
|
|
37
|
+
{{!-- telegram-style now in ~/.switchroom/fleet/switchroom-invariants.md --}}
|
|
38
38
|
|
|
39
39
|
## Memory — Hindsight
|
|
40
40
|
|
|
@@ -39,7 +39,14 @@ How you should decide what to do next. These are procedural rules, not vibe.
|
|
|
39
39
|
- **Weak or empty tool result is not a conclusion.** Vary the query, path, command, or source before deciding the thing isn't there.
|
|
40
40
|
- **Non-final turn:** use tools to advance, or ask the one clarifying question that unblocks safe progress. One question, not five.
|
|
41
41
|
|
|
42
|
-
{{
|
|
42
|
+
{{!--
|
|
43
|
+
Telegram-style guidance (the 5-beat pacing contract) lives in the
|
|
44
|
+
fleet invariants file at `~/.switchroom/fleet/switchroom-invariants.md`
|
|
45
|
+
and reaches the agent via Claude Code native CLAUDE.md discovery
|
|
46
|
+
(`--add-dir ~/.switchroom/fleet`). Do not re-include the
|
|
47
|
+
`{{> telegram-style}}` partial here — that would re-introduce the
|
|
48
|
+
session-level duplication the prompt redesign removed.
|
|
49
|
+
--}}
|
|
43
50
|
|
|
44
51
|
## Memory — Hindsight is your single backend
|
|
45
52
|
|
|
@@ -33,7 +33,7 @@ You help the user stay organized, prepared, and focused on high-leverage work. Y
|
|
|
33
33
|
- **Anticipate, don't just react.** Flag gaps proactively.
|
|
34
34
|
- **Be concise.** Lead with essentials. Details on request.
|
|
35
35
|
|
|
36
|
-
{{
|
|
36
|
+
{{!-- telegram-style now in ~/.switchroom/fleet/switchroom-invariants.md --}}
|
|
37
37
|
|
|
38
38
|
## Memory — Hindsight
|
|
39
39
|
|
|
@@ -27,7 +27,7 @@ You are a health and fitness coaching agent — an accountability partner, not a
|
|
|
27
27
|
## Boundaries
|
|
28
28
|
Recommend the user consult a professional for: persistent pain/injury, medical conditions, specialized nutrition plans, symptoms of overtraining or disordered eating. Say it plainly: "That's outside my lane — worth checking with your doctor."
|
|
29
29
|
|
|
30
|
-
{{
|
|
30
|
+
{{!-- telegram-style now in ~/.switchroom/fleet/switchroom-invariants.md --}}
|
|
31
31
|
|
|
32
32
|
## Memory — Hindsight
|
|
33
33
|
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
48
|
+
switchroom status --json 2>/dev/null || switchroom status
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
|
|
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-
|
|
12
|
-
* for its `chat_id
|
|
13
|
-
*
|
|
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 `
|
|
18
|
-
run<T>(
|
|
19
|
-
/**
|
|
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>(
|
|
27
|
-
const prior = chains.get(
|
|
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
|
|
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(
|
|
71
|
+
if (chains.get(key) === tracked) chains.delete(key)
|
|
35
72
|
})
|
|
36
|
-
chains.set(
|
|
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
|
|
49
|
-
//
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
}
|