botmux 2.51.1 → 2.53.0

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 (165) hide show
  1. package/README.en.md +22 -265
  2. package/README.md +21 -296
  3. package/dist/adapters/backend/session-backend-selector.d.ts +5 -1
  4. package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -1
  5. package/dist/adapters/backend/session-backend-selector.js +15 -1
  6. package/dist/adapters/backend/session-backend-selector.js.map +1 -1
  7. package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
  8. package/dist/adapters/backend/tmux-backend.js +3 -0
  9. package/dist/adapters/backend/tmux-backend.js.map +1 -1
  10. package/dist/adapters/backend/types.d.ts +22 -0
  11. package/dist/adapters/backend/types.d.ts.map +1 -1
  12. package/dist/adapters/backend/types.js +7 -1
  13. package/dist/adapters/backend/types.js.map +1 -1
  14. package/dist/adapters/backend/zellij-backend.d.ts +132 -0
  15. package/dist/adapters/backend/zellij-backend.d.ts.map +1 -0
  16. package/dist/adapters/backend/zellij-backend.js +375 -0
  17. package/dist/adapters/backend/zellij-backend.js.map +1 -0
  18. package/dist/adapters/backend/zellij-observe-backend.d.ts +62 -0
  19. package/dist/adapters/backend/zellij-observe-backend.d.ts.map +1 -0
  20. package/dist/adapters/backend/zellij-observe-backend.js +218 -0
  21. package/dist/adapters/backend/zellij-observe-backend.js.map +1 -0
  22. package/dist/adapters/cli/claude-code.d.ts +39 -5
  23. package/dist/adapters/cli/claude-code.d.ts.map +1 -1
  24. package/dist/adapters/cli/claude-code.js +53 -31
  25. package/dist/adapters/cli/claude-code.js.map +1 -1
  26. package/dist/adapters/cli/registry.d.ts +2 -1
  27. package/dist/adapters/cli/registry.d.ts.map +1 -1
  28. package/dist/adapters/cli/registry.js +3 -1
  29. package/dist/adapters/cli/registry.js.map +1 -1
  30. package/dist/adapters/cli/seed.d.ts +29 -0
  31. package/dist/adapters/cli/seed.d.ts.map +1 -0
  32. package/dist/adapters/cli/seed.js +63 -0
  33. package/dist/adapters/cli/seed.js.map +1 -0
  34. package/dist/adapters/cli/types.d.ts +17 -1
  35. package/dist/adapters/cli/types.d.ts.map +1 -1
  36. package/dist/bot-registry.d.ts +31 -1
  37. package/dist/bot-registry.d.ts.map +1 -1
  38. package/dist/bot-registry.js +31 -0
  39. package/dist/bot-registry.js.map +1 -1
  40. package/dist/cli.d.ts.map +1 -1
  41. package/dist/cli.js +37 -27
  42. package/dist/cli.js.map +1 -1
  43. package/dist/config.d.ts +7 -1
  44. package/dist/config.d.ts.map +1 -1
  45. package/dist/config.js +8 -0
  46. package/dist/config.js.map +1 -1
  47. package/dist/core/ask-hook/registry.d.ts.map +1 -1
  48. package/dist/core/ask-hook/registry.js +4 -0
  49. package/dist/core/ask-hook/registry.js.map +1 -1
  50. package/dist/core/command-handler.d.ts +4 -1
  51. package/dist/core/command-handler.d.ts.map +1 -1
  52. package/dist/core/command-handler.js +100 -8
  53. package/dist/core/command-handler.js.map +1 -1
  54. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  55. package/dist/core/dashboard-ipc-server.js +37 -0
  56. package/dist/core/dashboard-ipc-server.js.map +1 -1
  57. package/dist/core/dispatch.d.ts +33 -0
  58. package/dist/core/dispatch.d.ts.map +1 -1
  59. package/dist/core/dispatch.js +26 -0
  60. package/dist/core/dispatch.js.map +1 -1
  61. package/dist/core/session-discovery.d.ts +13 -4
  62. package/dist/core/session-discovery.d.ts.map +1 -1
  63. package/dist/core/session-discovery.js +5 -5
  64. package/dist/core/session-discovery.js.map +1 -1
  65. package/dist/core/session-manager.d.ts +10 -0
  66. package/dist/core/session-manager.d.ts.map +1 -1
  67. package/dist/core/session-manager.js +43 -18
  68. package/dist/core/session-manager.js.map +1 -1
  69. package/dist/core/types.d.ts +5 -2
  70. package/dist/core/types.d.ts.map +1 -1
  71. package/dist/core/types.js.map +1 -1
  72. package/dist/core/worker-pool.d.ts +1 -1
  73. package/dist/core/worker-pool.d.ts.map +1 -1
  74. package/dist/core/worker-pool.js +22 -9
  75. package/dist/core/worker-pool.js.map +1 -1
  76. package/dist/core/zellij-adopt-discovery.d.ts +28 -0
  77. package/dist/core/zellij-adopt-discovery.d.ts.map +1 -0
  78. package/dist/core/zellij-adopt-discovery.js +255 -0
  79. package/dist/core/zellij-adopt-discovery.js.map +1 -0
  80. package/dist/core/zellij-session-discovery.d.ts +73 -0
  81. package/dist/core/zellij-session-discovery.d.ts.map +1 -0
  82. package/dist/core/zellij-session-discovery.js +259 -0
  83. package/dist/core/zellij-session-discovery.js.map +1 -0
  84. package/dist/daemon.d.ts +3 -0
  85. package/dist/daemon.d.ts.map +1 -1
  86. package/dist/daemon.js +145 -13
  87. package/dist/daemon.js.map +1 -1
  88. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  89. package/dist/dashboard/web/bot-defaults.js +114 -0
  90. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  91. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  92. package/dist/dashboard/web/i18n.js +23 -1
  93. package/dist/dashboard/web/i18n.js.map +1 -1
  94. package/dist/dashboard/web/sessions.d.ts.map +1 -1
  95. package/dist/dashboard/web/sessions.js +1 -0
  96. package/dist/dashboard/web/sessions.js.map +1 -1
  97. package/dist/dashboard/web/workflows.js +1 -1
  98. package/dist/dashboard/web/workflows.js.map +1 -1
  99. package/dist/dashboard-web/app.js +449 -426
  100. package/dist/dashboard.js +20 -0
  101. package/dist/dashboard.js.map +1 -1
  102. package/dist/i18n/en.d.ts.map +1 -1
  103. package/dist/i18n/en.js +15 -1
  104. package/dist/i18n/en.js.map +1 -1
  105. package/dist/i18n/zh.d.ts.map +1 -1
  106. package/dist/i18n/zh.js +16 -2
  107. package/dist/i18n/zh.js.map +1 -1
  108. package/dist/im/lark/card-builder.d.ts +8 -3
  109. package/dist/im/lark/card-builder.d.ts.map +1 -1
  110. package/dist/im/lark/card-builder.js +74 -5
  111. package/dist/im/lark/card-builder.js.map +1 -1
  112. package/dist/im/lark/card-handler.d.ts.map +1 -1
  113. package/dist/im/lark/card-handler.js +72 -10
  114. package/dist/im/lark/card-handler.js.map +1 -1
  115. package/dist/im/lark/event-dispatcher.d.ts +12 -0
  116. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  117. package/dist/im/lark/event-dispatcher.js +39 -31
  118. package/dist/im/lark/event-dispatcher.js.map +1 -1
  119. package/dist/im/lark/grant-command.d.ts +26 -0
  120. package/dist/im/lark/grant-command.d.ts.map +1 -1
  121. package/dist/im/lark/grant-command.js +142 -3
  122. package/dist/im/lark/grant-command.js.map +1 -1
  123. package/dist/im/lark/grant-pending.d.ts +7 -4
  124. package/dist/im/lark/grant-pending.d.ts.map +1 -1
  125. package/dist/im/lark/grant-pending.js +12 -6
  126. package/dist/im/lark/grant-pending.js.map +1 -1
  127. package/dist/services/codex-app-threads.d.ts +20 -0
  128. package/dist/services/codex-app-threads.d.ts.map +1 -0
  129. package/dist/services/codex-app-threads.js +165 -0
  130. package/dist/services/codex-app-threads.js.map +1 -0
  131. package/dist/services/grant-prefs-store.d.ts +23 -0
  132. package/dist/services/grant-prefs-store.d.ts.map +1 -0
  133. package/dist/services/grant-prefs-store.js +94 -0
  134. package/dist/services/grant-prefs-store.js.map +1 -0
  135. package/dist/services/grant-store.d.ts +34 -2
  136. package/dist/services/grant-store.d.ts.map +1 -1
  137. package/dist/services/grant-store.js +160 -9
  138. package/dist/services/grant-store.js.map +1 -1
  139. package/dist/services/quota-dedup.d.ts +33 -0
  140. package/dist/services/quota-dedup.d.ts.map +1 -0
  141. package/dist/services/quota-dedup.js +67 -0
  142. package/dist/services/quota-dedup.js.map +1 -0
  143. package/dist/setup/bot-config-editor.d.ts +1 -1
  144. package/dist/setup/bot-config-editor.d.ts.map +1 -1
  145. package/dist/setup/bot-config-editor.js +5 -4
  146. package/dist/setup/bot-config-editor.js.map +1 -1
  147. package/dist/setup/ensure-zellij.d.ts +48 -0
  148. package/dist/setup/ensure-zellij.d.ts.map +1 -0
  149. package/dist/setup/ensure-zellij.js +93 -0
  150. package/dist/setup/ensure-zellij.js.map +1 -0
  151. package/dist/types.d.ts +9 -3
  152. package/dist/types.d.ts.map +1 -1
  153. package/dist/utils/anchor-serializer.d.ts +3 -2
  154. package/dist/utils/anchor-serializer.d.ts.map +1 -1
  155. package/dist/utils/anchor-serializer.js +20 -5
  156. package/dist/utils/anchor-serializer.js.map +1 -1
  157. package/dist/utils/transient-snapshot.js +2 -2
  158. package/dist/utils/transient-snapshot.js.map +1 -1
  159. package/dist/worker.js +124 -30
  160. package/dist/worker.js.map +1 -1
  161. package/dist/workflows/attempt-resume.d.ts +1 -1
  162. package/dist/workflows/attempt-resume.d.ts.map +1 -1
  163. package/dist/workflows/attempt-resume.js +1 -1
  164. package/dist/workflows/attempt-resume.js.map +1 -1
  165. package/package.json +1 -1
@@ -0,0 +1,73 @@
1
+ export interface LayoutPane {
2
+ /** Foreground command (argv0) zellij introspected for this pane, e.g. "claude". */
3
+ command: string;
4
+ /** Explicit pane name if set (zellij `name=`), else undefined. */
5
+ name?: string;
6
+ /** Absolute cwd of the pane (layout base cwd joined with the pane's relative cwd). */
7
+ cwd?: string;
8
+ /** Command args, when present in the dump. */
9
+ args: string[];
10
+ }
11
+ export interface ListedPane {
12
+ /** "terminal_<n>" — the id used to target zellij `action` commands. */
13
+ paneId: string;
14
+ isPlugin: boolean;
15
+ isFloating: boolean;
16
+ title?: string;
17
+ terminalCommand?: string | null;
18
+ }
19
+ export interface DiscoveredCli {
20
+ session: string;
21
+ paneId: string;
22
+ command: string;
23
+ cwd?: string;
24
+ args: string[];
25
+ title?: string;
26
+ }
27
+ /**
28
+ * Parse `zellij action dump-layout` output, returning only the panes that have
29
+ * a foreground `command=` (i.e. real terminal panes running something) in
30
+ * document order. Template sections (new_tab_template / swap_*_layout) are cut
31
+ * off first — their bare `pane` nodes have no command and would otherwise add
32
+ * noise. Plugin panes (tab-bar / status-bar / about) have no command= and are
33
+ * naturally excluded.
34
+ */
35
+ export declare function parseDumpLayoutPanes(kdl: string): LayoutPane[];
36
+ export interface LeafPane {
37
+ /** Foreground command (argv0) if the pane is running one; undefined for an
38
+ * idle shell pane (zellij emits a bare `pane` with no command=). */
39
+ command?: string;
40
+ name?: string;
41
+ cwd?: string;
42
+ args: string[];
43
+ }
44
+ /**
45
+ * Parse `zellij action dump-layout` into the ordered list of LEAF terminal
46
+ * panes — both command-bearing panes AND idle bare shell panes — skipping
47
+ * plugin panes (tab-bar/status-bar/about), container panes (splits), and the
48
+ * floating subtree. Preserving bare panes is what makes a positional join to
49
+ * `list-panes` correct: a bare shell pane that sorts before the CLI pane would
50
+ * otherwise shift every command pane onto the wrong pane id (the bug Codex
51
+ * found). Templates (new_tab_template / swap_*) are cut off first.
52
+ *
53
+ * zellij always pretty-prints dump-layout one node per line, so a brace-stack
54
+ * line walk is reliable here.
55
+ */
56
+ export declare function parseDumpLayoutLeafPanes(kdl: string): LeafPane[];
57
+ /** Parse `zellij action list-panes --json` into a flat list (document order). */
58
+ export declare function parseListPanesJson(json: string): ListedPane[];
59
+ /**
60
+ * Join dump-layout command panes with list-panes terminal panes by document
61
+ * order: the i-th command pane ↔ the i-th non-plugin terminal pane (sorted by
62
+ * id). zellij assigns pane ids in creation order and walks the tree in a stable
63
+ * order, so this aligns for normal layouts. Returns one DiscoveredCli per
64
+ * command pane that could be bound to an id.
65
+ */
66
+ export declare function joinPanes(session: string, layoutPanes: LayoutPane[], listed: ListedPane[]): DiscoveredCli[];
67
+ /** Names of live (non-exited) zellij sessions. */
68
+ export declare function listLiveSessions(): string[];
69
+ /** Discover CLIs running in one session. */
70
+ export declare function discoverSessionClis(session: string): DiscoveredCli[];
71
+ /** Discover CLIs across every live zellij session. */
72
+ export declare function discoverAllClis(): DiscoveredCli[];
73
+ //# sourceMappingURL=zellij-session-discovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zellij-session-discovery.d.ts","sourceRoot":"","sources":["../../src/core/zellij-session-discovery.ts"],"names":[],"mappings":"AA4BA,MAAM,WAAW,UAAU;IACzB,mFAAmF;IACnF,OAAO,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sFAAsF;IACtF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAID;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,EAAE,CA6C9D;AAED,MAAM,WAAW,QAAQ;IACvB;yEACqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,EAAE,CAuDhE;AAED,iFAAiF;AACjF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,EAAE,CAW7D;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,aAAa,EAAE,CAmB3G;AAID,kDAAkD;AAClD,wBAAgB,gBAAgB,IAAI,MAAM,EAAE,CAS3C;AAUD,4CAA4C;AAC5C,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,EAAE,CAKpE;AAED,sDAAsD;AACtD,wBAAgB,eAAe,IAAI,aAAa,EAAE,CAEjD"}
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Zellij session discovery for /adopt — find CLIs already running inside a
3
+ * user's zellij sessions and the pane needed to drive them.
4
+ *
5
+ * Why this exists (and why it's not just `list-panes`): zellij's
6
+ * `list-panes --json` exposes pane ids + geometry but NOT the running command,
7
+ * cwd, or pid, and its `terminal_command` is null for anything the user started
8
+ * interactively (typed `claude` into a shell — the common case). The data we
9
+ * need is instead surfaced by zellij's **session-resurrection** machinery: to be
10
+ * able to restore a session after a reboot, zellij continuously introspects each
11
+ * pane's *foreground process command + cwd* and exposes it via
12
+ * `zellij action dump-layout`. That's where we read "what CLI is in this pane".
13
+ *
14
+ * Discovery pipeline:
15
+ * 1. dump-layout → per-pane { command, args, cwd } (detection)
16
+ * 2. list-panes --json → per-pane { id: terminal_<n> } (drive target)
17
+ * 3. order/geometry join → bind command ↔ pane id
18
+ * 4. (caller) /proc descent under the pane shell → pid → ~/.claude/sessions/<pid>.json
19
+ *
20
+ * Parsers here are pure (string in, struct out) so they unit-test without a
21
+ * live zellij. The order-join (step 3) is robust for normal single/few-pane
22
+ * layouts; exotic multi-tab/floating arrangements may need the geometry/proc
23
+ * cross-check the caller layers on top.
24
+ */
25
+ import { execFileSync } from 'node:child_process';
26
+ import { isAbsolute, join as pathJoin } from 'node:path';
27
+ import { zellijEnv } from '../setup/ensure-zellij.js';
28
+ const TEMPLATE_MARKERS = /\b(new_tab_template|swap_tiled_layout|swap_floating_layout)\b/;
29
+ /**
30
+ * Parse `zellij action dump-layout` output, returning only the panes that have
31
+ * a foreground `command=` (i.e. real terminal panes running something) in
32
+ * document order. Template sections (new_tab_template / swap_*_layout) are cut
33
+ * off first — their bare `pane` nodes have no command and would otherwise add
34
+ * noise. Plugin panes (tab-bar / status-bar / about) have no command= and are
35
+ * naturally excluded.
36
+ */
37
+ export function parseDumpLayoutPanes(kdl) {
38
+ // Drop the template tail so we only see live tab content.
39
+ const tmplIdx = kdl.search(TEMPLATE_MARKERS);
40
+ const body = tmplIdx >= 0 ? kdl.slice(0, tmplIdx) : kdl;
41
+ const lines = body.split('\n');
42
+ const panes = [];
43
+ // Layout base cwd is a NODE: `cwd "..."` (space). Pane cwd is an ATTRIBUTE:
44
+ // `cwd="..."` (equals). The first node-form cwd is the layout base.
45
+ let layoutCwd;
46
+ const baseCwdMatch = body.match(/^\s*cwd\s+"([^"]*)"/m);
47
+ if (baseCwdMatch)
48
+ layoutCwd = baseCwdMatch[1];
49
+ let pending = null;
50
+ const flush = () => { if (pending) {
51
+ panes.push(pending);
52
+ pending = null;
53
+ } };
54
+ for (const line of lines) {
55
+ const trimmed = line.trim();
56
+ // A pane that runs a command (attributes can appear in any order).
57
+ if (/^pane\b/.test(trimmed) && /\bcommand=/.test(trimmed)) {
58
+ flush();
59
+ const command = attr(trimmed, 'command');
60
+ const name = attr(trimmed, 'name');
61
+ const cwdAttr = attr(trimmed, 'cwd');
62
+ pending = {
63
+ command: command ?? '',
64
+ name: name ?? undefined,
65
+ cwd: resolveCwd(layoutCwd, cwdAttr),
66
+ args: [],
67
+ };
68
+ // Single-line pane (closed on same line, no block) — flush immediately.
69
+ if (trimmed.includes('}') && !trimmed.endsWith('{'))
70
+ flush();
71
+ continue;
72
+ }
73
+ // args node inside the current pane block: `args "a" "b" …`
74
+ if (pending && /^args\b/.test(trimmed)) {
75
+ pending.args = [...trimmed.matchAll(/"((?:[^"\\]|\\.)*)"/g)].map(m => unescapeKdl(m[1]));
76
+ continue;
77
+ }
78
+ // Next pane / tab / closing — a new `pane` (without command) ends the block.
79
+ if (pending && /^pane\b/.test(trimmed))
80
+ flush();
81
+ }
82
+ flush();
83
+ return panes;
84
+ }
85
+ /**
86
+ * Parse `zellij action dump-layout` into the ordered list of LEAF terminal
87
+ * panes — both command-bearing panes AND idle bare shell panes — skipping
88
+ * plugin panes (tab-bar/status-bar/about), container panes (splits), and the
89
+ * floating subtree. Preserving bare panes is what makes a positional join to
90
+ * `list-panes` correct: a bare shell pane that sorts before the CLI pane would
91
+ * otherwise shift every command pane onto the wrong pane id (the bug Codex
92
+ * found). Templates (new_tab_template / swap_*) are cut off first.
93
+ *
94
+ * zellij always pretty-prints dump-layout one node per line, so a brace-stack
95
+ * line walk is reliable here.
96
+ */
97
+ export function parseDumpLayoutLeafPanes(kdl) {
98
+ const tmplIdx = kdl.search(TEMPLATE_MARKERS);
99
+ const body = tmplIdx >= 0 ? kdl.slice(0, tmplIdx) : kdl;
100
+ const baseCwdMatch = body.match(/^\s*cwd\s+"([^"]*)"/m);
101
+ const layoutCwd = baseCwdMatch ? baseCwdMatch[1] : undefined;
102
+ const stack = [];
103
+ const leaves = [];
104
+ const inFloating = () => stack.some(f => f.isFloating);
105
+ const emit = (command, name, cwdAttr, args) => {
106
+ if (inFloating())
107
+ return;
108
+ leaves.push({ command, name, cwd: resolveCwd(layoutCwd, cwdAttr), args });
109
+ };
110
+ for (const raw of body.split('\n')) {
111
+ const line = raw.trim();
112
+ if (!line)
113
+ continue;
114
+ if (line === '}') {
115
+ const f = stack.pop();
116
+ // A pane frame that contained neither a plugin nor child panes is a leaf
117
+ // terminal pane (its block held only props like args/start_suspended).
118
+ if (f?.isPane && !f.hasPlugin && !f.hasChildPane && !inFloating()) {
119
+ leaves.push({ command: f.command, name: f.name, cwd: resolveCwd(layoutCwd, f.cwdAttr), args: f.args });
120
+ }
121
+ continue;
122
+ }
123
+ const opensBlock = line.endsWith('{');
124
+ if (line.startsWith('plugin')) {
125
+ if (stack.length && stack[stack.length - 1].isPane)
126
+ stack[stack.length - 1].hasPlugin = true;
127
+ if (opensBlock)
128
+ stack.push({ isPane: false, isFloating: false, args: [], hasPlugin: false, hasChildPane: false });
129
+ continue;
130
+ }
131
+ if (line.startsWith('pane')) {
132
+ if (stack.length && stack[stack.length - 1].isPane)
133
+ stack[stack.length - 1].hasChildPane = true;
134
+ const command = attr(line, 'command');
135
+ const name = attr(line, 'name');
136
+ const cwdAttr = attr(line, 'cwd');
137
+ if (opensBlock) {
138
+ stack.push({ isPane: true, isFloating: false, command, name, cwdAttr, args: [], hasPlugin: false, hasChildPane: false });
139
+ }
140
+ else {
141
+ emit(command, name, cwdAttr, []); // bare leaf, no block
142
+ }
143
+ continue;
144
+ }
145
+ if (opensBlock) {
146
+ // tab / floating_panes / swap_* / other container
147
+ stack.push({ isPane: false, isFloating: line.startsWith('floating_panes'), args: [], hasPlugin: false, hasChildPane: false });
148
+ continue;
149
+ }
150
+ if (line.startsWith('args') && stack.length && stack[stack.length - 1].isPane) {
151
+ stack[stack.length - 1].args = [...line.matchAll(/"((?:[^"\\]|\\.)*)"/g)].map(m => unescapeKdl(m[1]));
152
+ }
153
+ }
154
+ return leaves;
155
+ }
156
+ /** Parse `zellij action list-panes --json` into a flat list (document order). */
157
+ export function parseListPanesJson(json) {
158
+ let arr;
159
+ try {
160
+ arr = JSON.parse(json);
161
+ }
162
+ catch {
163
+ return [];
164
+ }
165
+ if (!Array.isArray(arr))
166
+ return [];
167
+ return arr.map((p) => ({
168
+ paneId: `terminal_${p.id}`,
169
+ isPlugin: !!p.is_plugin,
170
+ isFloating: !!p.is_floating,
171
+ title: typeof p.title === 'string' ? p.title : undefined,
172
+ terminalCommand: p.terminal_command ?? null,
173
+ }));
174
+ }
175
+ /**
176
+ * Join dump-layout command panes with list-panes terminal panes by document
177
+ * order: the i-th command pane ↔ the i-th non-plugin terminal pane (sorted by
178
+ * id). zellij assigns pane ids in creation order and walks the tree in a stable
179
+ * order, so this aligns for normal layouts. Returns one DiscoveredCli per
180
+ * command pane that could be bound to an id.
181
+ */
182
+ export function joinPanes(session, layoutPanes, listed) {
183
+ const terminals = listed
184
+ .filter(p => !p.isPlugin)
185
+ .sort((a, b) => paneNum(a.paneId) - paneNum(b.paneId));
186
+ const out = [];
187
+ for (let i = 0; i < layoutPanes.length; i++) {
188
+ const lp = layoutPanes[i];
189
+ const tp = terminals[i];
190
+ if (!tp)
191
+ break;
192
+ out.push({
193
+ session,
194
+ paneId: tp.paneId,
195
+ command: lp.command,
196
+ cwd: lp.cwd,
197
+ args: lp.args,
198
+ title: tp.title,
199
+ });
200
+ }
201
+ return out;
202
+ }
203
+ // ─── Runtime (shells out to zellij) ─────────────────────────────────────────
204
+ /** Names of live (non-exited) zellij sessions. */
205
+ export function listLiveSessions() {
206
+ try {
207
+ const out = execFileSync('zellij', ['list-sessions', '--no-formatting'], {
208
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000, env: zellijEnv(),
209
+ });
210
+ return out.split('\n').map(l => l.trim())
211
+ .filter(l => l.length > 0 && !/EXITED/i.test(l))
212
+ .map(l => l.split(/\s+/)[0]).filter(Boolean);
213
+ }
214
+ catch {
215
+ return [];
216
+ }
217
+ }
218
+ function zellijAction(session, args) {
219
+ try {
220
+ return execFileSync('zellij', ['--session', session, 'action', ...args], {
221
+ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 4000, env: zellijEnv(),
222
+ });
223
+ }
224
+ catch {
225
+ return null;
226
+ }
227
+ }
228
+ /** Discover CLIs running in one session. */
229
+ export function discoverSessionClis(session) {
230
+ const layout = zellijAction(session, ['dump-layout']);
231
+ const panesJson = zellijAction(session, ['list-panes', '--json']);
232
+ if (!layout || !panesJson)
233
+ return [];
234
+ return joinPanes(session, parseDumpLayoutPanes(layout), parseListPanesJson(panesJson));
235
+ }
236
+ /** Discover CLIs across every live zellij session. */
237
+ export function discoverAllClis() {
238
+ return listLiveSessions().flatMap(discoverSessionClis);
239
+ }
240
+ // ─── helpers ────────────────────────────────────────────────────────────────
241
+ function attr(line, key) {
242
+ const m = line.match(new RegExp(`\\b${key}="((?:[^"\\\\]|\\\\.)*)"`));
243
+ return m ? unescapeKdl(m[1]) : undefined;
244
+ }
245
+ function resolveCwd(base, paneCwd) {
246
+ if (!paneCwd)
247
+ return base;
248
+ if (isAbsolute(paneCwd))
249
+ return paneCwd;
250
+ return base ? pathJoin(base, paneCwd) : paneCwd;
251
+ }
252
+ function paneNum(paneId) {
253
+ const m = paneId.match(/(\d+)$/);
254
+ return m ? Number(m[1]) : 0;
255
+ }
256
+ function unescapeKdl(s) {
257
+ return s.replace(/\\(.)/g, '$1');
258
+ }
259
+ //# sourceMappingURL=zellij-session-discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zellij-session-discovery.js","sourceRoot":"","sources":["../../src/core/zellij-session-discovery.ts"],"names":[],"mappings":"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"}
package/dist/daemon.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  export type { DaemonSession } from './core/types.js';
2
2
  import { WorkflowEventWatcher } from './workflows/fanout.js';
3
3
  import type { WorkflowRuntimeContext } from './workflows/runtime.js';
4
+ export declare function enforceMessageQuotaForCliInput(larkAppId: string, chatId: string, senderOpenId: string | undefined, messageId: string, anchor: string): Promise<boolean>;
5
+ export declare function grantRestrictedCommandText(larkAppId: string, chatId: string | undefined, senderOpenId: string | undefined, cmd: string): string | undefined;
6
+ export declare function grantRestrictedSlashCommandText(larkAppId: string, chatId: string | undefined, senderOpenId: string | undefined, cmd: string): string | undefined;
4
7
  export declare function attachWorkflowEventWatcher(runId: string, ctx?: WorkflowRuntimeContext): WorkflowEventWatcher;
5
8
  export declare function startDaemon(botIndex?: number): Promise<void>;
6
9
  //# sourceMappingURL=daemon.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AA0BA,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAmErD,OAAO,EAAE,oBAAoB,EAA6B,MAAM,uBAAuB,CAAC;AACxF,OAAO,KAAK,EAAE,sBAAsB,EAAiB,MAAM,wBAAwB,CAAC;AAyWpF,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,sBAAsB,GAAG,oBAAoB,CA0C5G;AA6lED,wBAAsB,WAAW,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyTlE"}
1
+ {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AA0BA,YAAY,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAsErD,OAAO,EAAE,oBAAoB,EAA6B,MAAM,uBAAuB,CAAC;AACxF,OAAO,KAAK,EAAE,sBAAsB,EAAiB,MAAM,wBAAwB,CAAC;AAsQpF,wBAAsB,8BAA8B,CAClD,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CA4ClB;AAED,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,GAAG,EAAE,MAAM,GACV,MAAM,GAAG,SAAS,CAIpB;AAED,wBAAgB,+BAA+B,CAC7C,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,GAAG,EAAE,MAAM,GACV,MAAM,GAAG,SAAS,CAGpB;AAyJD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,sBAAsB,GAAG,oBAAoB,CA0C5G;AAkpED,wBAAsB,WAAW,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA6TlE"}
package/dist/daemon.js CHANGED
@@ -27,7 +27,7 @@ import { buildTerminalUrl, setTerminalProxyPort } from './core/terminal-url.js';
27
27
  import { startTerminalProxy } from './core/terminal-proxy.js';
28
28
  import * as scheduler from './core/scheduler.js';
29
29
  import { scanMultipleProjects } from './services/project-scanner.js';
30
- import { buildRepoSelectCard, buildStreamingCard, getCliDisplayName } from './im/lark/card-builder.js';
30
+ import { buildQuotaExhaustedCard, buildRepoSelectCard, buildStreamingCard, getCliDisplayName } from './im/lark/card-builder.js';
31
31
  import { t as tr, botLocale, localeForBot } from './i18n/index.js';
32
32
  import { createCliAdapterSync } from './adapters/cli/registry.js';
33
33
  import { initWorkerPool, setActiveSessionsRegistry, forkWorker, killWorker, scheduleCardPatch, setCurrentCliVersion, CARD_POSTING_SENTINEL, parkStreamCard, closeSession as closeSessionHelper, ensureCliEnv, writableTerminalLinkFor, } from './core/worker-pool.js';
@@ -36,14 +36,16 @@ import { saveFrozenCards } from './services/frozen-card-store.js';
36
36
  import { DAEMON_COMMANDS, SESSIONLESS_DAEMON_COMMANDS, PASSTHROUGH_COMMANDS, handleCommand, parseSlashCommandInvocation, parseForceTopicInvocation } from './core/command-handler.js';
37
37
  import { findInheritablePeer } from './core/inherit-peer.js';
38
38
  import { isCallbackUrl, handleCallbackUrl } from './utils/user-token.js';
39
+ import { consumeQuota, removeChatGrant, removeGlobalGrant } from './services/grant-store.js';
40
+ import { abortCharge, commitCharge, beginCharge } from './services/quota-dedup.js';
39
41
  import { getSessionWorkingDir, getProjectScanDirs, expandHome, downloadResources, formatAttachmentsHint, buildNewTopicPrompt, buildFollowUpContent, buildBridgeInputContent, buildReforkPrompt, getAvailableBots, restoreActiveSessions, executeScheduledTask, persistStreamCardState, rememberLastCliInput, } from './core/session-manager.js';
40
42
  import { handleCardAction } from './im/lark/card-handler.js';
41
- import { executeWorkflowCommand, resolveBotSnapshot, } from './im/lark/workflow-slash-command.js';
43
+ import { executeWorkflowCommand, parseWorkflowCommand, resolveBotSnapshot, } from './im/lark/workflow-slash-command.js';
42
44
  import { workflowRunDetailUrl } from './im/lark/workflow-cards.js';
43
45
  import { buildWorkflowStartingCard, buildWorkflowProgressCard, buildAttemptDeeplinkEnricher, } from './im/lark/workflow-progress-card.js';
44
46
  import { EventLog as WorkflowEventLog } from './workflows/events/append.js';
45
47
  import { replay as replayWorkflow } from './workflows/events/replay.js';
46
- import { isBotMentioned, probeBotOpenId, startLarkEventDispatcher, writeBotInfoFile, canOperate, isKnownPeerBot, checkRequiredScopes } from './im/lark/event-dispatcher.js';
48
+ import { isBotMentioned, probeBotOpenId, startLarkEventDispatcher, writeBotInfoFile, canOperate, evaluateTalk, grantCommandRestriction, isKnownPeerBot, checkRequiredScopes } from './im/lark/event-dispatcher.js';
47
49
  import { learnFromMentions, resolveSender, flushIdentityCacheSync } from './im/lark/identity-cache.js';
48
50
  import { renderSenderTag } from './core/session-manager.js';
49
51
  import { markSessionActivity } from './core/session-activity.js';
@@ -222,6 +224,94 @@ async function sessionReply(anchor, content, msgType = 'text', larkAppId) {
222
224
  // Thread-scope (or unknown / legacy): reply in thread.
223
225
  return replyMessage(appId, anchor, content, msgType, true);
224
226
  }
227
+ async function revokeQuotaGrant(larkAppId, chatId, senderOpenId, ev) {
228
+ const result = ev.reason === 'chatGrant'
229
+ ? await removeChatGrant(larkAppId, chatId, senderOpenId)
230
+ : ev.reason === 'globalGrant'
231
+ ? await removeGlobalGrant(larkAppId, senderOpenId)
232
+ : { ok: true, removed: false };
233
+ if (!result.ok) {
234
+ logger.warn(`[quota:${larkAppId}] revoke after quota exhaustion failed: reason=${result.reason} user=${senderOpenId.substring(0, 12)} reasonType=${ev.reason}`);
235
+ }
236
+ }
237
+ async function notifyQuotaExhausted(larkAppId, anchor, senderOpenId, limit) {
238
+ if (typeof limit !== 'number')
239
+ return;
240
+ try {
241
+ await sessionReply(anchor, buildQuotaExhaustedCard(senderOpenId, limit, localeForBot(larkAppId)), 'interactive', larkAppId);
242
+ }
243
+ catch (err) {
244
+ logger.warn(`[quota:${larkAppId}] quota exhausted notify failed: ${err}`);
245
+ }
246
+ }
247
+ export async function enforceMessageQuotaForCliInput(larkAppId, chatId, senderOpenId, messageId, anchor) {
248
+ const ev = evaluateTalk(larkAppId, chatId, senderOpenId);
249
+ if (!ev.allowed) {
250
+ logger.debug(`[quota:${larkAppId}] dropping message ${messageId.substring(0, 12)} from non-allowed sender ${senderOpenId?.substring(0, 12) ?? '?'}`);
251
+ return false;
252
+ }
253
+ if (!ev.quotaKey)
254
+ return true;
255
+ if (!senderOpenId)
256
+ return false;
257
+ // 去重三态:'done' = 同条已成功扣费 → 放行(不重复扣);'pending' = 同条扣费 in-flight 未定论
258
+ // → fail-closed drop(绝不在定论前放行第二投);'fresh' = 首次见 → 继续扣费。
259
+ const charge = beginCharge(larkAppId, messageId);
260
+ if (charge === 'done')
261
+ return true;
262
+ if (charge === 'pending')
263
+ return false;
264
+ let quota;
265
+ try {
266
+ quota = await consumeQuota(larkAppId, ev.quotaKey);
267
+ }
268
+ catch (err) {
269
+ logger.warn(`[quota:${larkAppId}] consume failed; dropping message ${messageId.substring(0, 12)}: ${err}`);
270
+ abortCharge(larkAppId, messageId);
271
+ return false;
272
+ }
273
+ // 无额度记录(无限授权):放行;标 done 去重后续重投。
274
+ if (!quota.tracked) {
275
+ commitCharge(larkAppId, messageId);
276
+ return true;
277
+ }
278
+ // 已超额:fail-closed drop。**绝不 commit 成 done**(否则同条重投会被 'done' 直接放行,
279
+ // 在 revoke 自愈失败/竞态时绕过硬上限)——abortCharge 让重投重新走扣费判定(仍会被拒,
280
+ // 或在授权已收回时被上面的 evaluateTalk 闸拦掉)。
281
+ if (!quota.allow) {
282
+ abortCharge(larkAppId, messageId);
283
+ await revokeQuotaGrant(larkAppId, chatId, senderOpenId, ev);
284
+ await notifyQuotaExhausted(larkAppId, anchor, senderOpenId, quota.limit);
285
+ return false;
286
+ }
287
+ // 扣费成功才定论为 done。
288
+ commitCharge(larkAppId, messageId);
289
+ if (quota.exhausted) {
290
+ await revokeQuotaGrant(larkAppId, chatId, senderOpenId, ev);
291
+ await notifyQuotaExhausted(larkAppId, anchor, senderOpenId, quota.limit);
292
+ }
293
+ return true;
294
+ }
295
+ export function grantRestrictedCommandText(larkAppId, chatId, senderOpenId, cmd) {
296
+ return grantCommandRestriction(larkAppId, chatId, senderOpenId).blocked
297
+ ? tr('cmd.grant_restricted', { cmd }, localeForBot(larkAppId))
298
+ : undefined;
299
+ }
300
+ export function grantRestrictedSlashCommandText(larkAppId, chatId, senderOpenId, cmd) {
301
+ if (!/^\/[a-z][a-z0-9_-]*$/.test(cmd))
302
+ return undefined;
303
+ return grantRestrictedCommandText(larkAppId, chatId, senderOpenId, cmd);
304
+ }
305
+ async function replyGrantRestrictionIfNeeded(larkAppId, chatId, senderOpenId, anchor, cmd) {
306
+ const text = grantRestrictedCommandText(larkAppId, chatId, senderOpenId, cmd);
307
+ if (!text)
308
+ return false;
309
+ await sessionReply(anchor, text, 'text', larkAppId);
310
+ return true;
311
+ }
312
+ function forceTopicCommandLabel(content) {
313
+ return /^\/topic(?:\s|$)/i.test(content.trimStart()) ? '/topic' : '/t';
314
+ }
225
315
  // ─── PID file ────────────────────────────────────────────────────────────────
226
316
  function getPidFile() {
227
317
  const botIndex = process.env.BOTMUX_BOT_INDEX;
@@ -1384,8 +1474,12 @@ async function handleNewTopic(data, ctx) {
1384
1474
  // (already thread-scope) it's just a prefix strip — no routing change.
1385
1475
  // Empty prompt is allowed: the user can fill it in while the repo card is
1386
1476
  // pending (pendingFollowUps in handleThreadReply picks up subsequent text).
1477
+ const senderOpenId = data.sender?.sender_id?.open_id;
1387
1478
  const forceTopic = parseForceTopicInvocation(cmdContent);
1388
1479
  if (forceTopic) {
1480
+ if (await replyGrantRestrictionIfNeeded(larkAppId, chatId, senderOpenId, anchor, forceTopicCommandLabel(cmdContent))) {
1481
+ return;
1482
+ }
1389
1483
  if (scope === 'chat') {
1390
1484
  scope = 'thread';
1391
1485
  anchor = messageId;
@@ -1395,10 +1489,15 @@ async function handleNewTopic(data, ctx) {
1395
1489
  cmdContent = forceTopic.prompt;
1396
1490
  logger.info(`[/t] Force-topic invocation: prompt="${forceTopic.prompt.substring(0, 60)}" (scope=${scope}, anchor=${anchor.substring(0, 12)})`);
1397
1491
  }
1398
- const senderOpenId = data.sender?.sender_id?.open_id;
1492
+ // senderOpenId 已在上方(force-topic grant 限制前)声明;这里只补 master 新增的 senderUnionId。
1399
1493
  const senderUnionId = data.sender?.sender_id?.union_id;
1400
1494
  const botCfg = getBot(larkAppId).config;
1401
1495
  logger.info(`New session: "${content.substring(0, 60)}" (scope=${scope}, anchor=${anchor.substring(0, 12)}, resources: ${resources.length}, active: ${getActiveCount()}, messageId: ${messageId}, chatId: ${chatId})`);
1496
+ if (parseWorkflowCommand(cmdContent)) {
1497
+ if (await replyGrantRestrictionIfNeeded(larkAppId, chatId, senderOpenId, anchor, '/workflow')) {
1498
+ return;
1499
+ }
1500
+ }
1402
1501
  if (await handleWorkflowCommandIfAny(cmdContent, anchor, chatId, larkAppId, senderOpenId)) {
1403
1502
  return;
1404
1503
  }
@@ -1406,6 +1505,11 @@ async function handleNewTopic(data, ctx) {
1406
1505
  const invocation = parseSlashCommandInvocation(cmdContent);
1407
1506
  if (invocation) {
1408
1507
  const { cmd, content: commandContent } = invocation;
1508
+ const restrictedText = grantRestrictedSlashCommandText(larkAppId, chatId, senderOpenId, cmd);
1509
+ if (restrictedText) {
1510
+ await sessionReply(anchor, restrictedText, 'text', larkAppId);
1511
+ return;
1512
+ }
1409
1513
  if (PASSTHROUGH_COMMANDS.has(cmd)) {
1410
1514
  await sessionReply(anchor, tr('daemon.cmd_requires_session', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
1411
1515
  return;
@@ -1479,6 +1583,9 @@ async function handleNewTopic(data, ctx) {
1479
1583
  return;
1480
1584
  }
1481
1585
  }
1586
+ if (!await enforceMessageQuotaForCliInput(larkAppId, chatId, senderOpenId, messageId, anchor)) {
1587
+ return;
1588
+ }
1482
1589
  // Download attachments
1483
1590
  const { attachments, needLogin } = await downloadResources(larkAppId, messageId, resources);
1484
1591
  if (attachments.length > 0) {
@@ -1874,6 +1981,8 @@ async function handleThreadReply(data, ctx) {
1874
1981
  const content = parsed.content.trim();
1875
1982
  // Strip leading @<bot> mentions so "@bot /restart" is recognized as a command.
1876
1983
  const cmdContent = stripLeadingMentions(content, parsed.mentions);
1984
+ const threadSenderOpenId = parsed.senderId || data?.sender?.sender_id?.open_id;
1985
+ const threadChatId = ctxChatId ?? data?.message?.chat_id;
1877
1986
  // Intercept OAuth callback URLs (from /login flow)
1878
1987
  if (isCallbackUrl(content)) {
1879
1988
  const result = await handleCallbackUrl(content);
@@ -1885,13 +1994,31 @@ async function handleThreadReply(data, ctx) {
1885
1994
  return;
1886
1995
  }
1887
1996
  }
1888
- if (await handleWorkflowCommandIfAny(cmdContent, anchor, ctxChatId ?? data?.message?.chat_id, larkAppId, parsed.senderId || data?.sender?.sender_id?.open_id)) {
1997
+ const threadForceTopic = parseForceTopicInvocation(cmdContent);
1998
+ if (threadForceTopic) {
1999
+ if (await replyGrantRestrictionIfNeeded(larkAppId, threadChatId, threadSenderOpenId, anchor, forceTopicCommandLabel(cmdContent))) {
2000
+ return;
2001
+ }
2002
+ }
2003
+ if (parseWorkflowCommand(cmdContent)) {
2004
+ if (await replyGrantRestrictionIfNeeded(larkAppId, threadChatId, threadSenderOpenId, anchor, '/workflow')) {
2005
+ return;
2006
+ }
2007
+ }
2008
+ if (await handleWorkflowCommandIfAny(cmdContent, anchor, threadChatId, larkAppId, threadSenderOpenId)) {
1889
2009
  return;
1890
2010
  }
1891
2011
  // Intercept daemon commands
1892
2012
  const invocation = parseSlashCommandInvocation(cmdContent);
1893
2013
  if (invocation) {
1894
2014
  const { cmd, content: commandContent } = invocation;
2015
+ const existingDs = activeSessions.get(sessionKey(anchor, larkAppId));
2016
+ const effectiveThreadChatId = existingDs?.chatId ?? threadChatId;
2017
+ const restrictedText = grantRestrictedSlashCommandText(larkAppId, effectiveThreadChatId, threadSenderOpenId, cmd);
2018
+ if (restrictedText) {
2019
+ await sessionReply(anchor, restrictedText, 'text', larkAppId);
2020
+ return;
2021
+ }
1895
2022
  if (PASSTHROUGH_COMMANDS.has(cmd)) {
1896
2023
  // 语义边界(刻意保留,非疏漏):passthrough(/model /clear /compact 等)按
1897
2024
  // “发给 CLI 的对话输入”处理,因此不过下面 DAEMON_COMMANDS 的 oncall
@@ -1900,7 +2027,7 @@ async function handleThreadReply(data, ctx) {
1900
2027
  // 已存在的 session 发这些命令(清上下文/换模型,需已有活跃 worker,无法凭空
1901
2028
  // 拉起)。TODO(后续产品决策):是否把 CLI passthrough 也纳入 canOperate,
1902
2029
  // 收紧到与 daemon 命令同档;这会同时改变真人 oncall 成员的现有行为,应单独评估。
1903
- const ds = activeSessions.get(sessionKey(anchor, larkAppId));
2030
+ const ds = existingDs;
1904
2031
  if (ds?.worker && !ds.worker.killed) {
1905
2032
  // Mark a new turn so the CLI's response to /model, /clear, /compact, etc.
1906
2033
  // shows up as a fresh streaming card instead of silently PATCH-ing the
@@ -1918,10 +2045,7 @@ async function handleThreadReply(data, ctx) {
1918
2045
  if (DAEMON_COMMANDS.has(cmd)) {
1919
2046
  // canOperate gate for thread-reply daemon commands — required in every chat
1920
2047
  // (see spawn-path gate above). Denies chat-granted users management commands.
1921
- const existingDs = activeSessions.get(sessionKey(anchor, larkAppId));
1922
- const threadChatId = existingDs?.chatId ?? ctxChatId ?? data?.message?.chat_id;
1923
- const threadSenderOpenId = parsed.senderId || data?.sender?.sender_id?.open_id;
1924
- if (!canOperate(larkAppId, threadChatId, threadSenderOpenId)) {
2048
+ if (!canOperate(larkAppId, effectiveThreadChatId, threadSenderOpenId)) {
1925
2049
  sessionReply(anchor, tr('daemon.cmd_allowed_users_only', { cmd }, localeForBot(larkAppId)), 'text', larkAppId);
1926
2050
  return;
1927
2051
  }
@@ -1992,6 +2116,10 @@ async function handleThreadReply(data, ctx) {
1992
2116
  return;
1993
2117
  }
1994
2118
  }
2119
+ const quotaSenderOpenId = threadSenderOpenId;
2120
+ if (!await enforceMessageQuotaForCliInput(larkAppId, ctxChatId ?? data?.message?.chat_id, quotaSenderOpenId, parsed.messageId, anchor)) {
2121
+ return;
2122
+ }
1995
2123
  // Download attachments
1996
2124
  const effectiveAppId = ds?.larkAppId ?? larkAppId;
1997
2125
  const { attachments, needLogin } = await downloadResources(effectiveAppId, parsed.messageId, resources);
@@ -2534,9 +2662,13 @@ export async function startDaemon(botIndex) {
2534
2662
  const backendType = ds.larkAppId
2535
2663
  ? (getBot(ds.larkAppId).config.backendType ?? config.daemon.backendType)
2536
2664
  : config.daemon.backendType;
2537
- if (backendType === 'tmux') {
2538
- // Tmux mode: just kill the worker process — tmux session survives for re-attach.
2539
- // Worker's SIGTERM handler calls backend.kill() which only detaches.
2665
+ if (backendType === 'tmux' || backendType === 'zellij') {
2666
+ // Persistent backends (tmux / zellij): just kill the worker process —
2667
+ // the multiplexer session survives for re-attach. The worker's SIGTERM
2668
+ // handler calls backend.kill(), which only DETACHES. Going through
2669
+ // killWorker() instead would send {type:'close'} → destroySession() →
2670
+ // `zellij delete-session -f`, permanently erasing the session and
2671
+ // breaking daemon-restart reattach (the blocker Codex flagged).
2540
2672
  try {
2541
2673
  w.kill('SIGTERM');
2542
2674
  }