botmux 2.64.0 → 2.65.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 (208) hide show
  1. package/README.en.md +1 -1
  2. package/README.md +3 -3
  3. package/dist/adapters/backend/herdr-backend.d.ts +8 -1
  4. package/dist/adapters/backend/herdr-backend.d.ts.map +1 -1
  5. package/dist/adapters/backend/herdr-backend.js +15 -2
  6. package/dist/adapters/backend/herdr-backend.js.map +1 -1
  7. package/dist/adapters/backend/tmux-backend.d.ts +17 -1
  8. package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
  9. package/dist/adapters/backend/tmux-backend.js +25 -4
  10. package/dist/adapters/backend/tmux-backend.js.map +1 -1
  11. package/dist/adapters/backend/types.d.ts +14 -0
  12. package/dist/adapters/backend/types.d.ts.map +1 -1
  13. package/dist/adapters/backend/types.js.map +1 -1
  14. package/dist/adapters/backend/zellij-backend.d.ts +12 -1
  15. package/dist/adapters/backend/zellij-backend.d.ts.map +1 -1
  16. package/dist/adapters/backend/zellij-backend.js +25 -8
  17. package/dist/adapters/backend/zellij-backend.js.map +1 -1
  18. package/dist/bot-registry.d.ts +39 -0
  19. package/dist/bot-registry.d.ts.map +1 -1
  20. package/dist/bot-registry.js +30 -0
  21. package/dist/bot-registry.js.map +1 -1
  22. package/dist/cli/send-dispatch.d.ts +23 -0
  23. package/dist/cli/send-dispatch.d.ts.map +1 -0
  24. package/dist/cli/send-dispatch.js +23 -0
  25. package/dist/cli/send-dispatch.js.map +1 -0
  26. package/dist/cli.d.ts.map +1 -1
  27. package/dist/cli.js +141 -58
  28. package/dist/cli.js.map +1 -1
  29. package/dist/config.d.ts +8 -6
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +8 -6
  32. package/dist/config.js.map +1 -1
  33. package/dist/core/ask-broker.d.ts +33 -0
  34. package/dist/core/ask-broker.d.ts.map +1 -1
  35. package/dist/core/ask-broker.js +58 -0
  36. package/dist/core/ask-broker.js.map +1 -1
  37. package/dist/core/ask-hook/claude-code.d.ts.map +1 -1
  38. package/dist/core/ask-hook/claude-code.js +15 -9
  39. package/dist/core/ask-hook/claude-code.js.map +1 -1
  40. package/dist/core/ask-hook/codex.d.ts.map +1 -1
  41. package/dist/core/ask-hook/codex.js +2 -1
  42. package/dist/core/ask-hook/codex.js.map +1 -1
  43. package/dist/core/ask-hook/opencode.d.ts.map +1 -1
  44. package/dist/core/ask-hook/opencode.js +9 -6
  45. package/dist/core/ask-hook/opencode.js.map +1 -1
  46. package/dist/core/ask-hook/types.d.ts +3 -1
  47. package/dist/core/ask-hook/types.d.ts.map +1 -1
  48. package/dist/core/ask-types.d.ts +13 -6
  49. package/dist/core/ask-types.d.ts.map +1 -1
  50. package/dist/core/ask-types.js.map +1 -1
  51. package/dist/core/command-handler.d.ts.map +1 -1
  52. package/dist/core/command-handler.js +255 -4
  53. package/dist/core/command-handler.js.map +1 -1
  54. package/dist/core/daemon-heartbeat.d.ts +15 -0
  55. package/dist/core/daemon-heartbeat.d.ts.map +1 -0
  56. package/dist/core/daemon-heartbeat.js +83 -0
  57. package/dist/core/daemon-heartbeat.js.map +1 -0
  58. package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
  59. package/dist/core/dashboard-ipc-server.js +80 -33
  60. package/dist/core/dashboard-ipc-server.js.map +1 -1
  61. package/dist/core/dispatch.d.ts +1 -23
  62. package/dist/core/dispatch.d.ts.map +1 -1
  63. package/dist/core/dispatch.js +1 -17
  64. package/dist/core/dispatch.js.map +1 -1
  65. package/dist/core/idle-worker-sweeper.d.ts +13 -0
  66. package/dist/core/idle-worker-sweeper.d.ts.map +1 -0
  67. package/dist/core/idle-worker-sweeper.js +42 -0
  68. package/dist/core/idle-worker-sweeper.js.map +1 -0
  69. package/dist/core/maintenance-schedule.d.ts +34 -0
  70. package/dist/core/maintenance-schedule.d.ts.map +1 -0
  71. package/dist/core/maintenance-schedule.js +72 -0
  72. package/dist/core/maintenance-schedule.js.map +1 -0
  73. package/dist/core/maintenance.d.ts +43 -0
  74. package/dist/core/maintenance.d.ts.map +1 -0
  75. package/dist/core/maintenance.js +160 -0
  76. package/dist/core/maintenance.js.map +1 -0
  77. package/dist/core/reply-target.d.ts +23 -0
  78. package/dist/core/reply-target.d.ts.map +1 -0
  79. package/dist/core/reply-target.js +47 -0
  80. package/dist/core/reply-target.js.map +1 -0
  81. package/dist/core/restart-report.d.ts +49 -0
  82. package/dist/core/restart-report.d.ts.map +1 -0
  83. package/dist/core/restart-report.js +98 -0
  84. package/dist/core/restart-report.js.map +1 -0
  85. package/dist/core/scheduler.d.ts.map +1 -1
  86. package/dist/core/scheduler.js +20 -0
  87. package/dist/core/scheduler.js.map +1 -1
  88. package/dist/core/session-manager.d.ts +26 -10
  89. package/dist/core/session-manager.d.ts.map +1 -1
  90. package/dist/core/session-manager.js +104 -26
  91. package/dist/core/session-manager.js.map +1 -1
  92. package/dist/core/session-marker.d.ts +13 -0
  93. package/dist/core/session-marker.d.ts.map +1 -0
  94. package/dist/core/session-marker.js +55 -0
  95. package/dist/core/session-marker.js.map +1 -0
  96. package/dist/core/types.d.ts +20 -1
  97. package/dist/core/types.d.ts.map +1 -1
  98. package/dist/core/types.js.map +1 -1
  99. package/dist/core/worker-budget.d.ts +19 -0
  100. package/dist/core/worker-budget.d.ts.map +1 -0
  101. package/dist/core/worker-budget.js +50 -0
  102. package/dist/core/worker-budget.js.map +1 -0
  103. package/dist/core/worker-pool.d.ts +5 -2
  104. package/dist/core/worker-pool.d.ts.map +1 -1
  105. package/dist/core/worker-pool.js +105 -12
  106. package/dist/core/worker-pool.js.map +1 -1
  107. package/dist/daemon.d.ts.map +1 -1
  108. package/dist/daemon.js +243 -38
  109. package/dist/daemon.js.map +1 -1
  110. package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
  111. package/dist/dashboard/web/bot-defaults.js +118 -0
  112. package/dist/dashboard/web/bot-defaults.js.map +1 -1
  113. package/dist/dashboard/web/i18n.d.ts.map +1 -1
  114. package/dist/dashboard/web/i18n.js +44 -0
  115. package/dist/dashboard/web/i18n.js.map +1 -1
  116. package/dist/dashboard/web/settings.d.ts.map +1 -1
  117. package/dist/dashboard/web/settings.js +84 -13
  118. package/dist/dashboard/web/settings.js.map +1 -1
  119. package/dist/dashboard-web/app.js +568 -503
  120. package/dist/dashboard.js +87 -7
  121. package/dist/dashboard.js.map +1 -1
  122. package/dist/global-config.d.ts +46 -0
  123. package/dist/global-config.d.ts.map +1 -1
  124. package/dist/global-config.js +115 -0
  125. package/dist/global-config.js.map +1 -1
  126. package/dist/i18n/en.d.ts.map +1 -1
  127. package/dist/i18n/en.js +72 -1
  128. package/dist/i18n/en.js.map +1 -1
  129. package/dist/i18n/zh.d.ts.map +1 -1
  130. package/dist/i18n/zh.js +72 -1
  131. package/dist/i18n/zh.js.map +1 -1
  132. package/dist/im/lark/ask-card.d.ts.map +1 -1
  133. package/dist/im/lark/ask-card.js +15 -1
  134. package/dist/im/lark/ask-card.js.map +1 -1
  135. package/dist/im/lark/card-builder.d.ts +17 -0
  136. package/dist/im/lark/card-builder.d.ts.map +1 -1
  137. package/dist/im/lark/card-builder.js +146 -0
  138. package/dist/im/lark/card-builder.js.map +1 -1
  139. package/dist/im/lark/card-handler.d.ts.map +1 -1
  140. package/dist/im/lark/card-handler.js +119 -4
  141. package/dist/im/lark/card-handler.js.map +1 -1
  142. package/dist/im/lark/client.d.ts +3 -2
  143. package/dist/im/lark/client.d.ts.map +1 -1
  144. package/dist/im/lark/client.js +28 -2
  145. package/dist/im/lark/client.js.map +1 -1
  146. package/dist/im/lark/event-dispatcher.d.ts +7 -17
  147. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  148. package/dist/im/lark/event-dispatcher.js +170 -50
  149. package/dist/im/lark/event-dispatcher.js.map +1 -1
  150. package/dist/im/lark/message-parser.d.ts +1 -0
  151. package/dist/im/lark/message-parser.d.ts.map +1 -1
  152. package/dist/im/lark/message-parser.js +1 -0
  153. package/dist/im/lark/message-parser.js.map +1 -1
  154. package/dist/im/lark/reply-mode-command.d.ts +2 -0
  155. package/dist/im/lark/reply-mode-command.d.ts.map +1 -0
  156. package/dist/im/lark/reply-mode-command.js +102 -0
  157. package/dist/im/lark/reply-mode-command.js.map +1 -0
  158. package/dist/services/bot-config-store.d.ts +144 -0
  159. package/dist/services/bot-config-store.d.ts.map +1 -0
  160. package/dist/services/bot-config-store.js +241 -0
  161. package/dist/services/bot-config-store.js.map +1 -0
  162. package/dist/services/card-prefs-store.d.ts +5 -0
  163. package/dist/services/card-prefs-store.d.ts.map +1 -1
  164. package/dist/services/card-prefs-store.js +47 -0
  165. package/dist/services/card-prefs-store.js.map +1 -1
  166. package/dist/services/chat-reply-mode-store.d.ts +28 -0
  167. package/dist/services/chat-reply-mode-store.d.ts.map +1 -0
  168. package/dist/services/chat-reply-mode-store.js +115 -0
  169. package/dist/services/chat-reply-mode-store.js.map +1 -0
  170. package/dist/services/hook-runner.d.ts +43 -0
  171. package/dist/services/hook-runner.d.ts.map +1 -0
  172. package/dist/services/hook-runner.js +394 -0
  173. package/dist/services/hook-runner.js.map +1 -0
  174. package/dist/services/restart-intent-store.d.ts +26 -0
  175. package/dist/services/restart-intent-store.d.ts.map +1 -0
  176. package/dist/services/restart-intent-store.js +84 -0
  177. package/dist/services/restart-intent-store.js.map +1 -0
  178. package/dist/services/session-lifecycle-hooks.d.ts +10 -0
  179. package/dist/services/session-lifecycle-hooks.d.ts.map +1 -0
  180. package/dist/services/session-lifecycle-hooks.js +66 -0
  181. package/dist/services/session-lifecycle-hooks.js.map +1 -0
  182. package/dist/services/session-store.d.ts +6 -0
  183. package/dist/services/session-store.d.ts.map +1 -1
  184. package/dist/services/session-store.js +25 -0
  185. package/dist/services/session-store.js.map +1 -1
  186. package/dist/skills/definitions.d.ts.map +1 -1
  187. package/dist/skills/definitions.js +28 -3
  188. package/dist/skills/definitions.js.map +1 -1
  189. package/dist/types.d.ts +33 -0
  190. package/dist/types.d.ts.map +1 -1
  191. package/dist/utils/install-info.d.ts +13 -0
  192. package/dist/utils/install-info.d.ts.map +1 -0
  193. package/dist/utils/install-info.js +56 -0
  194. package/dist/utils/install-info.js.map +1 -0
  195. package/dist/utils/listen-with-probe.d.ts +26 -0
  196. package/dist/utils/listen-with-probe.d.ts.map +1 -0
  197. package/dist/utils/listen-with-probe.js +64 -0
  198. package/dist/utils/listen-with-probe.js.map +1 -0
  199. package/dist/utils/web-terminal-listen.d.ts +30 -0
  200. package/dist/utils/web-terminal-listen.d.ts.map +1 -0
  201. package/dist/utils/web-terminal-listen.js +81 -0
  202. package/dist/utils/web-terminal-listen.js.map +1 -0
  203. package/dist/worker.js +71 -44
  204. package/dist/worker.js.map +1 -1
  205. package/dist/workflows/definition.d.ts +30 -30
  206. package/dist/workflows/events/payloads.d.ts +4 -4
  207. package/dist/workflows/events/schema.d.ts +156 -156
  208. package/package.json +1 -1
@@ -0,0 +1,394 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, readFileSync, statSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { config } from '../config.js';
5
+ import { findOnlineDaemon } from '../utils/daemon-discovery.js';
6
+ import { logger } from '../utils/logger.js';
7
+ export const HOOK_EVENTS = [
8
+ 'topic.new',
9
+ 'thread.reply',
10
+ 'outbound.send',
11
+ 'outbound.reply',
12
+ 'schedule.fired',
13
+ 'session.start',
14
+ 'session.exit',
15
+ 'session.idle',
16
+ 'session.requires_attention',
17
+ ];
18
+ const DEFAULT_TIMEOUT_MS = 5_000;
19
+ const CONTENT_PREVIEW_LIMIT = 600;
20
+ const CONTENT_FIELDS = ['content', 'message', 'description', 'finalOutput', 'lastScreenContent'];
21
+ let envHookCache = null;
22
+ let fileHookCache = null;
23
+ function isHookEvent(value) {
24
+ return typeof value === 'string' && HOOK_EVENTS.includes(value);
25
+ }
26
+ function normalizeStringList(value) {
27
+ if (typeof value === 'string' && value)
28
+ return [value];
29
+ if (Array.isArray(value)) {
30
+ const out = value.filter((v) => typeof v === 'string' && v.length > 0);
31
+ return out.length > 0 ? out : undefined;
32
+ }
33
+ return undefined;
34
+ }
35
+ function normalizeHookConfig(raw) {
36
+ if (!raw || typeof raw !== 'object')
37
+ return null;
38
+ const rec = raw;
39
+ if (!isHookEvent(rec.event))
40
+ return null;
41
+ if (typeof rec.command !== 'string' || rec.command.trim().length === 0)
42
+ return null;
43
+ const hook = {
44
+ event: rec.event,
45
+ command: rec.command,
46
+ };
47
+ if (typeof rec.timeoutMs === 'number' && Number.isFinite(rec.timeoutMs)) {
48
+ hook.timeoutMs = rec.timeoutMs;
49
+ }
50
+ if (rec.filter && typeof rec.filter === 'object') {
51
+ const filterRec = rec.filter;
52
+ const filter = {};
53
+ const chatId = normalizeStringList(filterRec.chatId);
54
+ const senderOpenId = normalizeStringList(filterRec.senderOpenId ?? filterRec.sender_open_id);
55
+ if (chatId)
56
+ filter.chatId = chatId;
57
+ if (senderOpenId)
58
+ filter.senderOpenId = senderOpenId;
59
+ if (filter.chatId || filter.senderOpenId)
60
+ hook.filter = filter;
61
+ }
62
+ if (rec.redact && typeof rec.redact === 'object') {
63
+ const redactRec = rec.redact;
64
+ const fullContentEventsRaw = Array.isArray(redactRec.fullContentEvents)
65
+ ? redactRec.fullContentEvents
66
+ : [];
67
+ const fullContentEvents = fullContentEventsRaw.filter(isHookEvent);
68
+ if (fullContentEvents.length > 0) {
69
+ hook.redact = { fullContentEvents };
70
+ }
71
+ }
72
+ return hook;
73
+ }
74
+ function readJsonHookArray(raw) {
75
+ const parsed = JSON.parse(raw);
76
+ if (!Array.isArray(parsed))
77
+ return [];
78
+ return parsed.map(normalizeHookConfig).filter((h) => !!h);
79
+ }
80
+ export function loadHookConfigs(opts = {}) {
81
+ const env = opts.env ?? process.env;
82
+ try {
83
+ if (env.BOTMUX_HOOKS_JSON) {
84
+ if (envHookCache?.raw === env.BOTMUX_HOOKS_JSON)
85
+ return envHookCache.hooks;
86
+ const hooks = readJsonHookArray(env.BOTMUX_HOOKS_JSON);
87
+ envHookCache = { raw: env.BOTMUX_HOOKS_JSON, hooks };
88
+ return hooks;
89
+ }
90
+ const hooksPath = env.BOTMUX_HOOKS_FILE || join(opts.dataDir ?? config.session.dataDir, 'hooks.json');
91
+ if (!existsSync(hooksPath))
92
+ return [];
93
+ const stats = statSync(hooksPath);
94
+ if (fileHookCache
95
+ && fileHookCache.path === hooksPath
96
+ && fileHookCache.mtimeMs === stats.mtimeMs
97
+ && fileHookCache.size === stats.size) {
98
+ return fileHookCache.hooks;
99
+ }
100
+ const hooks = readJsonHookArray(readFileSync(hooksPath, 'utf-8'));
101
+ fileHookCache = { path: hooksPath, mtimeMs: stats.mtimeMs, size: stats.size, hooks };
102
+ return hooks;
103
+ }
104
+ catch (err) {
105
+ logger.warn(`[hooks] Failed to load hook config: ${err?.message ?? String(err)}`);
106
+ return [];
107
+ }
108
+ }
109
+ export function prepareHookPayload(hook, rawPayload) {
110
+ const allowFullContent = !!hook.redact?.fullContentEvents?.includes(rawPayload.event);
111
+ const payload = { ...rawPayload };
112
+ for (const field of CONTENT_FIELDS) {
113
+ const value = payload[field];
114
+ if (typeof value !== 'string')
115
+ continue;
116
+ const lengthKey = `${field}Length`;
117
+ const truncatedKey = `${field}Truncated`;
118
+ payload[lengthKey] = value.length;
119
+ if (allowFullContent || value.length <= CONTENT_PREVIEW_LIMIT) {
120
+ payload[truncatedKey] = false;
121
+ continue;
122
+ }
123
+ payload[field] = value.slice(0, CONTENT_PREVIEW_LIMIT);
124
+ payload[truncatedKey] = true;
125
+ }
126
+ // Redact nested option text/label. session.requires_attention emits this
127
+ // as `optionsPreview` (see worker-pool.ts tui_prompt case); keep `options`
128
+ // as an alias so callers using either name get the same treatment.
129
+ for (const arrayField of ['optionsPreview', 'options']) {
130
+ const arrayValue = payload[arrayField];
131
+ if (!Array.isArray(arrayValue))
132
+ continue;
133
+ payload[arrayField] = arrayValue.map(item => {
134
+ if (!item || typeof item !== 'object')
135
+ return item;
136
+ const opt = { ...item };
137
+ for (const field of ['text', 'label']) {
138
+ const v = opt[field];
139
+ if (typeof v === 'string' && v.length > CONTENT_PREVIEW_LIMIT) {
140
+ opt[field] = v.slice(0, CONTENT_PREVIEW_LIMIT);
141
+ }
142
+ }
143
+ return opt;
144
+ });
145
+ }
146
+ return payload;
147
+ }
148
+ export function parseHookCommand(command) {
149
+ const tokens = [];
150
+ let current = '';
151
+ let quote = null;
152
+ let escaping = false;
153
+ for (const ch of command.trim()) {
154
+ if (escaping) {
155
+ current += ch;
156
+ escaping = false;
157
+ continue;
158
+ }
159
+ if (ch === '\\') {
160
+ escaping = true;
161
+ continue;
162
+ }
163
+ if (quote) {
164
+ if (ch === quote)
165
+ quote = null;
166
+ else
167
+ current += ch;
168
+ continue;
169
+ }
170
+ if (ch === '"' || ch === "'") {
171
+ quote = ch;
172
+ continue;
173
+ }
174
+ if (/\s/.test(ch)) {
175
+ if (current) {
176
+ tokens.push(current);
177
+ current = '';
178
+ }
179
+ continue;
180
+ }
181
+ current += ch;
182
+ }
183
+ if (escaping)
184
+ current += '\\';
185
+ if (quote)
186
+ throw new Error('Unterminated quote in hook command');
187
+ if (current)
188
+ tokens.push(current);
189
+ if (tokens.length === 0)
190
+ throw new Error('Empty hook command');
191
+ const [file, ...args] = tokens;
192
+ return { file, args };
193
+ }
194
+ function valueMatchesFilter(allowed, actual) {
195
+ if (!allowed)
196
+ return true;
197
+ if (!actual)
198
+ return false;
199
+ const list = Array.isArray(allowed) ? allowed : [allowed];
200
+ return list.includes(actual);
201
+ }
202
+ export function filterMatches(filter, payload) {
203
+ if (!filter)
204
+ return true;
205
+ const senderOpenId = payload.senderOpenId ?? payload.sender_open_id;
206
+ return valueMatchesFilter(filter.chatId, payload.chatId)
207
+ && valueMatchesFilter(filter.senderOpenId ?? filter.sender_open_id, senderOpenId);
208
+ }
209
+ function timeoutFor(hook) {
210
+ if (typeof hook.timeoutMs === 'number' && hook.timeoutMs >= 0)
211
+ return hook.timeoutMs;
212
+ return DEFAULT_TIMEOUT_MS;
213
+ }
214
+ async function runHookCommand(hook, payload, options = {}) {
215
+ let parsed;
216
+ try {
217
+ parsed = parseHookCommand(hook.command);
218
+ }
219
+ catch (err) {
220
+ return { ok: false, error: err?.message ?? String(err) };
221
+ }
222
+ return new Promise((resolve) => {
223
+ let settled = false;
224
+ let timedOut = false;
225
+ let stderr = '';
226
+ const child = spawn(parsed.file, parsed.args, {
227
+ shell: false,
228
+ stdio: ['pipe', 'ignore', 'pipe'],
229
+ // detached so we can kill the whole process group (grandchildren included)
230
+ detached: true,
231
+ env: {
232
+ // Minimal allowlist — avoids leaking secrets (LARK_APP_SECRET, API keys, etc.)
233
+ PATH: process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin',
234
+ HOME: process.env.HOME ?? '',
235
+ TMPDIR: process.env.TMPDIR ?? '/tmp',
236
+ TEMP: process.env.TEMP,
237
+ TMP: process.env.TMP,
238
+ SHELL: process.env.SHELL ?? '/bin/sh',
239
+ USER: process.env.USER,
240
+ LOGNAME: process.env.LOGNAME,
241
+ LANG: process.env.LANG,
242
+ LC_ALL: process.env.LC_ALL,
243
+ BOTMUX_HOOK_EVENT: payload.event,
244
+ },
245
+ });
246
+ if (options.fireAndForget) {
247
+ // Unref both the process handle and the stderr pipe. child.unref() alone
248
+ // still leaves piped stdio referenced, making short-lived CLI commands
249
+ // wait for hooks to finish.
250
+ child.unref();
251
+ child.stderr?.unref?.();
252
+ }
253
+ const settle = (result) => {
254
+ if (settled)
255
+ return;
256
+ settled = true;
257
+ clearTimeout(timer);
258
+ resolve(result);
259
+ };
260
+ const timer = setTimeout(() => {
261
+ timedOut = true;
262
+ try {
263
+ if (child.pid !== undefined) {
264
+ try {
265
+ process.kill(-child.pid, 'SIGTERM');
266
+ }
267
+ catch {
268
+ child.kill('SIGTERM');
269
+ }
270
+ setTimeout(() => {
271
+ if (!settled && child.pid !== undefined) {
272
+ try {
273
+ process.kill(-child.pid, 'SIGKILL');
274
+ }
275
+ catch {
276
+ child.kill('SIGKILL');
277
+ }
278
+ }
279
+ }, 250).unref();
280
+ }
281
+ else {
282
+ child.kill('SIGTERM');
283
+ }
284
+ }
285
+ catch { /* process may already be gone */ }
286
+ // Actively settle — don't wait for 'close' which may never fire if a
287
+ // grandchild process holds the stderr pipe open.
288
+ settle({ ok: false, timedOut: true, code: null, signal: null, error: 'hook timed out' });
289
+ }, timeoutFor(hook));
290
+ if (options.fireAndForget)
291
+ timer.unref();
292
+ child.stderr?.setEncoding('utf8');
293
+ child.stderr?.on('data', chunk => {
294
+ stderr += String(chunk);
295
+ if (stderr.length > 2_000)
296
+ stderr = stderr.slice(-2_000);
297
+ });
298
+ child.on('error', (err) => {
299
+ settle({ ok: false, timedOut, error: err.message });
300
+ });
301
+ child.on('close', (code, signal) => {
302
+ settle({
303
+ ok: code === 0 && !timedOut,
304
+ code,
305
+ signal,
306
+ timedOut,
307
+ error: code === 0 && !timedOut ? undefined : (stderr.trim() || `hook exited code=${code} signal=${signal ?? 'none'}`),
308
+ });
309
+ });
310
+ child.stdin?.end(JSON.stringify(payload), () => {
311
+ if (options.fireAndForget)
312
+ child.stdin?.unref?.();
313
+ });
314
+ });
315
+ }
316
+ export function emitHookEvent(event, body = {}) {
317
+ try {
318
+ const payload = {
319
+ ...body,
320
+ event,
321
+ emittedAt: new Date().toISOString(),
322
+ };
323
+ // CLI context: forward to the long-lived daemon so its event loop
324
+ // supervises the timeout/process-group kill. Short-lived `botmux send`
325
+ // can't enforce timeouts itself — fireAndForget unrefs the timer, so a
326
+ // runaway hook would survive as an orphan. The daemon stays alive, its
327
+ // timer fires reliably, and `process.kill(-pid)` cleans the whole group.
328
+ // Daemon process doesn't have BOTMUX_SESSION_ID set, so this gate
329
+ // naturally avoids recursive forward when emitHookEvent runs daemon-side.
330
+ if (process.env.BOTMUX_SESSION_ID && process.env.BOTMUX_LARK_APP_ID) {
331
+ void forwardEmitToDaemon(event, payload, process.env.BOTMUX_LARK_APP_ID);
332
+ return;
333
+ }
334
+ const hooks = loadHookConfigs().filter(hook => hook.event === event && filterMatches(hook.filter, payload));
335
+ if (hooks.length === 0)
336
+ return;
337
+ for (const [i, hook] of hooks.entries()) {
338
+ const hookPayload = prepareHookPayload(hook, payload);
339
+ const tag = `${event}[${i}] (${hook.command.slice(0, 60)})`;
340
+ void runHookCommand(hook, hookPayload, { fireAndForget: true }).then(result => {
341
+ if (!result.ok) {
342
+ logger.warn(`[hooks] ${tag} failed: ${result.error ?? `code=${result.code} signal=${result.signal ?? 'none'}`}`);
343
+ }
344
+ else {
345
+ logger.debug(`[hooks] ${tag} completed`);
346
+ }
347
+ }).catch((err) => {
348
+ logger.warn(`[hooks] ${tag} crashed: ${err?.message ?? String(err)}`);
349
+ });
350
+ }
351
+ }
352
+ catch (err) {
353
+ logger.warn(`[hooks] Failed to emit ${event}: ${err?.message ?? String(err)}`);
354
+ }
355
+ }
356
+ export function runHookCommandForTest(hook, payload) {
357
+ return runHookCommand(hook, payload);
358
+ }
359
+ const HOOK_FORWARD_FETCH_TIMEOUT_MS = 2_000;
360
+ /**
361
+ * CLI-side: hand off hook emission to the daemon so timeout enforcement and
362
+ * process-group cleanup work. Best-effort — daemon unreachable / 4xx / 5xx
363
+ * just log and drop, hooks are best-effort by contract.
364
+ */
365
+ async function forwardEmitToDaemon(event, payload, larkAppId) {
366
+ try {
367
+ const daemon = findOnlineDaemon(larkAppId);
368
+ if (!daemon) {
369
+ logger.debug(`[hooks] CLI forward: no daemon for ${larkAppId}, dropping ${event}`);
370
+ return;
371
+ }
372
+ const ctrl = new AbortController();
373
+ const timer = setTimeout(() => ctrl.abort(), HOOK_FORWARD_FETCH_TIMEOUT_MS);
374
+ timer.unref();
375
+ try {
376
+ const res = await fetch(`http://127.0.0.1:${daemon.ipcPort}/api/hooks/emit`, {
377
+ method: 'POST',
378
+ headers: { 'content-type': 'application/json' },
379
+ body: JSON.stringify({ event, payload }),
380
+ signal: ctrl.signal,
381
+ });
382
+ if (!res.ok) {
383
+ logger.warn(`[hooks] CLI forward ${event} → daemon: HTTP ${res.status}`);
384
+ }
385
+ }
386
+ finally {
387
+ clearTimeout(timer);
388
+ }
389
+ }
390
+ catch (err) {
391
+ logger.warn(`[hooks] CLI forward ${event} failed: ${err?.message ?? String(err)}`);
392
+ }
393
+ }
394
+ //# sourceMappingURL=hook-runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hook-runner.js","sourceRoot":"","sources":["../../src/services/hook-runner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ export type RestartKind = 'manual' | 'update';
2
+ export interface RestartIntent {
3
+ kind: RestartKind;
4
+ /** Present for kind==='update': the changelog/version delta to report. */
5
+ oldVersion?: string;
6
+ newVersion?: string;
7
+ /** ISO 8601 timestamp the breadcrumb was written. */
8
+ at: string;
9
+ }
10
+ /** Breadcrumbs older than this are stale (an aborted/failed restart left it)
11
+ * and never produce a report. */
12
+ export declare const RESTART_INTENT_FRESH_MS: number;
13
+ export declare function restartIntentPathIn(dir: string): string;
14
+ export declare function writeRestartIntentTo(dir: string, intent: RestartIntent): void;
15
+ /** Read + delete the breadcrumb. Always deletes (fresh, stale, or corrupt) so
16
+ * it fires at most once and never lingers into a later restart. Returns the
17
+ * intent only when it is fresh. */
18
+ export declare function consumeRestartIntentTo(dir: string, nowMs: number): RestartIntent | null;
19
+ /** Write a `manual` breadcrumb only when no *fresh* breadcrumb already exists —
20
+ * so a maintenance-written `update` breadcrumb is not clobbered
21
+ * by the `botmux restart` it spawns. */
22
+ export declare function writeManualIntentIfAbsentTo(dir: string, nowMs: number, atIso: string): void;
23
+ export declare function writeRestartIntent(intent: RestartIntent): void;
24
+ export declare function consumeRestartIntent(nowMs?: number): RestartIntent | null;
25
+ export declare function writeManualIntentIfAbsent(nowMs?: number): void;
26
+ //# sourceMappingURL=restart-intent-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"restart-intent-store.d.ts","sourceRoot":"","sources":["../../src/services/restart-intent-store.ts"],"names":[],"mappings":"AAeA,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE9C,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,WAAW,CAAC;IAClB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,EAAE,EAAE,MAAM,CAAC;CACZ;AAID;kCACkC;AAClC,eAAO,MAAM,uBAAuB,QAAc,CAAC;AAEnD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,IAAI,CAM7E;AAqBD;;oCAEoC;AACpC,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAQvF;AAED;;yCAEyC;AACzC,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAI3F;AAID,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAE9D;AAED,wBAAgB,oBAAoB,CAAC,KAAK,GAAE,MAAmB,GAAG,aAAa,GAAG,IAAI,CAErF;AAED,wBAAgB,yBAAyB,CAAC,KAAK,GAAE,MAAmB,GAAG,IAAI,CAE1E"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Restart-intent breadcrumb: a small file written just before an *intentional*
3
+ * restart (manual `botmux restart`, or an auto-update that restarts to apply).
4
+ * On the next daemon startup the primary daemon consumes it to decide whether
5
+ * to DM the owner a restart summary.
6
+ *
7
+ * A pm2 crash-autorestart (or machine reboot) writes no breadcrumb, so the
8
+ * fresh daemon stays silent — this is how we distinguish "crash" from
9
+ * "intentional restart" without a debounce. See core/maintenance.ts and
10
+ * core/restart-report.ts.
11
+ */
12
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import { config } from '../config.js';
15
+ const FILE = 'restart-intent.json';
16
+ /** Breadcrumbs older than this are stale (an aborted/failed restart left it)
17
+ * and never produce a report. */
18
+ export const RESTART_INTENT_FRESH_MS = 10 * 60_000;
19
+ export function restartIntentPathIn(dir) {
20
+ return join(dir, FILE);
21
+ }
22
+ export function writeRestartIntentTo(dir, intent) {
23
+ if (!existsSync(dir))
24
+ mkdirSync(dir, { recursive: true });
25
+ const path = restartIntentPathIn(dir);
26
+ const tmp = `${path}.${process.pid}.tmp`;
27
+ writeFileSync(tmp, JSON.stringify(intent, null, 2) + '\n');
28
+ renameSync(tmp, path);
29
+ }
30
+ function readRaw(dir) {
31
+ const path = restartIntentPathIn(dir);
32
+ if (!existsSync(path))
33
+ return null;
34
+ try {
35
+ const v = JSON.parse(readFileSync(path, 'utf-8'));
36
+ if (v && typeof v === 'object' && typeof v.kind === 'string' && typeof v.at === 'string') {
37
+ return v;
38
+ }
39
+ }
40
+ catch {
41
+ /* corrupt → treated as absent (and cleaned up by consume) */
42
+ }
43
+ return null;
44
+ }
45
+ function isFresh(intent, nowMs) {
46
+ const at = Date.parse(intent.at);
47
+ return Number.isFinite(at) && Math.abs(nowMs - at) <= RESTART_INTENT_FRESH_MS;
48
+ }
49
+ /** Read + delete the breadcrumb. Always deletes (fresh, stale, or corrupt) so
50
+ * it fires at most once and never lingers into a later restart. Returns the
51
+ * intent only when it is fresh. */
52
+ export function consumeRestartIntentTo(dir, nowMs) {
53
+ const intent = readRaw(dir);
54
+ const path = restartIntentPathIn(dir);
55
+ if (existsSync(path)) {
56
+ try {
57
+ rmSync(path);
58
+ }
59
+ catch { /* best-effort */ }
60
+ }
61
+ if (!intent)
62
+ return null;
63
+ return isFresh(intent, nowMs) ? intent : null;
64
+ }
65
+ /** Write a `manual` breadcrumb only when no *fresh* breadcrumb already exists —
66
+ * so a maintenance-written `update` breadcrumb is not clobbered
67
+ * by the `botmux restart` it spawns. */
68
+ export function writeManualIntentIfAbsentTo(dir, nowMs, atIso) {
69
+ const existing = readRaw(dir);
70
+ if (existing && isFresh(existing, nowMs))
71
+ return;
72
+ writeRestartIntentTo(dir, { kind: 'manual', at: atIso });
73
+ }
74
+ // ---- default-dir wrappers (production wiring) ----
75
+ export function writeRestartIntent(intent) {
76
+ writeRestartIntentTo(config.session.dataDir, intent);
77
+ }
78
+ export function consumeRestartIntent(nowMs = Date.now()) {
79
+ return consumeRestartIntentTo(config.session.dataDir, nowMs);
80
+ }
81
+ export function writeManualIntentIfAbsent(nowMs = Date.now()) {
82
+ writeManualIntentIfAbsentTo(config.session.dataDir, nowMs, new Date(nowMs).toISOString());
83
+ }
84
+ //# sourceMappingURL=restart-intent-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"restart-intent-store.js","sourceRoot":"","sources":["../../src/services/restart-intent-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { StreamStatus } from '../types.js';
2
+ import type { DaemonSession } from '../core/types.js';
3
+ import { type HookEvent } from './hook-runner.js';
4
+ type SessionLifecycleEvent = Extract<HookEvent, 'session.start' | 'session.exit' | 'session.idle' | 'session.requires_attention'>;
5
+ export declare function setSessionLifecycleShutdown(value: boolean): void;
6
+ export declare function emitSessionLifecycleHook(ds: DaemonSession, event: SessionLifecycleEvent, body?: Record<string, unknown>): boolean;
7
+ export declare function emitSessionStateTransitionHook(ds: DaemonSession, prevState: StreamStatus | undefined, newState: StreamStatus | undefined, body?: Record<string, unknown>): boolean;
8
+ export declare function __testOnly_resetSessionLifecycleHooks(): void;
9
+ export {};
10
+ //# sourceMappingURL=session-lifecycle-hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-lifecycle-hooks.d.ts","sourceRoot":"","sources":["../../src/services/session-lifecycle-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGjE,KAAK,qBAAqB,GAAG,OAAO,CAClC,SAAS,EACT,eAAe,GAAG,cAAc,GAAG,cAAc,GAAG,4BAA4B,CACjF,CAAC;AAOF,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAEhE;AAqBD,wBAAgB,wBAAwB,CACtC,EAAE,EAAE,aAAa,EACjB,KAAK,EAAE,qBAAqB,EAC5B,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACjC,OAAO,CAeT;AAED,wBAAgB,8BAA8B,CAC5C,EAAE,EAAE,aAAa,EACjB,SAAS,EAAE,YAAY,GAAG,SAAS,EACnC,QAAQ,EAAE,YAAY,GAAG,SAAS,EAClC,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACjC,OAAO,CAgBT;AAED,wBAAgB,qCAAqC,IAAI,IAAI,CAG5D"}
@@ -0,0 +1,66 @@
1
+ import { sessionAnchorId } from '../core/types.js';
2
+ import { emitHookEvent } from './hook-runner.js';
3
+ import { logger } from '../utils/logger.js';
4
+ const IDLE_DEDUP_WINDOW_MS = 10_000;
5
+ let shutdownInProgress = false;
6
+ const lastIdleEmits = new Map();
7
+ export function setSessionLifecycleShutdown(value) {
8
+ shutdownInProgress = value;
9
+ }
10
+ function lifecyclePayload(ds, body) {
11
+ const initCliId = ds.initConfig && 'cliId' in ds.initConfig ? ds.initConfig.cliId : undefined;
12
+ return {
13
+ sessionId: ds.session.sessionId,
14
+ chatId: ds.chatId,
15
+ chatType: ds.chatType,
16
+ larkAppId: ds.larkAppId,
17
+ scope: ds.scope,
18
+ anchor: sessionAnchorId(ds),
19
+ title: ds.currentTurnTitle ?? ds.session.title,
20
+ cliId: ds.session.cliId ?? initCliId,
21
+ workingDir: ds.workingDir ?? ds.session.workingDir,
22
+ hasHistory: ds.hasHistory,
23
+ spawnedAt: ds.spawnedAt,
24
+ lastMessageAt: ds.lastMessageAt,
25
+ ...body,
26
+ };
27
+ }
28
+ export function emitSessionLifecycleHook(ds, event, body = {}) {
29
+ if (event === 'session.exit') {
30
+ // Prune dedup state to prevent unbounded Map growth
31
+ const prefix = `:${ds.session.sessionId}:`;
32
+ for (const key of lastIdleEmits.keys()) {
33
+ if (key.includes(prefix))
34
+ lastIdleEmits.delete(key);
35
+ }
36
+ if (shutdownInProgress) {
37
+ logger.debug(`[hooks] session.exit suppressed during daemon shutdown (session ${ds.session.sessionId})`);
38
+ return false;
39
+ }
40
+ }
41
+ emitHookEvent(event, lifecyclePayload(ds, body));
42
+ return true;
43
+ }
44
+ export function emitSessionStateTransitionHook(ds, prevState, newState, body = {}) {
45
+ if (!newState || prevState === newState)
46
+ return false;
47
+ if (prevState !== 'idle' && newState !== 'idle')
48
+ return false;
49
+ const now = Date.now();
50
+ const key = `session.idle:${ds.session.sessionId}:${newState}`;
51
+ const last = lastIdleEmits.get(key);
52
+ if (last !== undefined && now - last < IDLE_DEDUP_WINDOW_MS)
53
+ return false;
54
+ lastIdleEmits.set(key, now);
55
+ return emitSessionLifecycleHook(ds, 'session.idle', {
56
+ prevState,
57
+ newState,
58
+ transition: newState === 'idle' ? 'enter' : 'exit',
59
+ ...body,
60
+ });
61
+ }
62
+ export function __testOnly_resetSessionLifecycleHooks() {
63
+ shutdownInProgress = false;
64
+ lastIdleEmits.clear();
65
+ }
66
+ //# sourceMappingURL=session-lifecycle-hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-lifecycle-hooks.js","sourceRoot":"","sources":["../../src/services/session-lifecycle-hooks.ts"],"names":[],"mappings":"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"}
@@ -33,4 +33,10 @@ export declare function findActiveSessionsByRoot(rootMessageId: string): Session
33
33
  * are routed by rootMessageId and not eligible for chat-scope inheritance.
34
34
  */
35
35
  export declare function findActiveChatScopeSessionsByChat(chatId: string): Session[];
36
+ /**
37
+ * Count active sessions across every bot's on-disk session file. A pure disk
38
+ * read (no in-memory state) so it's correct at daemon startup regardless of
39
+ * which bot owns this process — used by the restart-report DM after a restart.
40
+ */
41
+ export declare function countActiveSessionsOnDisk(dataDir?: string): number;
36
42
  //# sourceMappingURL=session-store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"session-store.d.ts","sourceRoot":"","sources":["../../src/services/session-store.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAM3C;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAIzC;AA6ED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAevH;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAGjE;AA2BD,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAUpD;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAO5E;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAIpD;AAED,wBAAgB,YAAY,IAAI,OAAO,EAAE,CAGxC;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,EAAE,CAEzE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,EAAE,CAE3E"}
1
+ {"version":3,"file":"session-store.d.ts","sourceRoot":"","sources":["../../src/services/session-store.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAM3C;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAIzC;AA6ED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAevH;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAGjE;AA2BD,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAUpD;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAO5E;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAIpD;AAED,wBAAgB,YAAY,IAAI,OAAO,EAAE,CAGxC;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,EAAE,CAEzE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,EAAE,CAE3E;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,MAA+B,GAAG,MAAM,CAY1F"}