metame-cli 1.5.8 → 1.5.10
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/index.js +19 -2
- package/package.json +1 -1
- package/scripts/daemon-bridges.js +36 -44
- package/scripts/daemon-checkpoints.js +38 -24
- package/scripts/daemon-claude-engine.js +238 -58
- package/scripts/daemon-command-router.js +6 -125
- package/scripts/daemon-command-session-route.js +7 -1
- package/scripts/daemon-engine-runtime.js +8 -1
- package/scripts/daemon-exec-commands.js +36 -25
- package/scripts/daemon-message-pipeline.js +268 -0
- package/scripts/daemon-ops-commands.js +12 -10
- package/scripts/daemon-reactive-lifecycle.js +421 -0
- package/scripts/daemon-session-store.js +24 -24
- package/scripts/daemon-task-scheduler.js +90 -112
- package/scripts/daemon-utils.js +55 -0
- package/scripts/daemon-warm-pool.js +162 -0
- package/scripts/daemon-worktrees.js +129 -0
- package/scripts/daemon.js +31 -3
- package/scripts/docs/orphan-files-review.md +72 -0
- package/scripts/hooks/intent-auto-rules.js +50 -0
- package/scripts/verify-reactive-claude-md.js +101 -0
- package/scripts/daemon.yaml +0 -356
|
@@ -33,7 +33,13 @@ function createCommandSessionResolver(deps) {
|
|
|
33
33
|
function normalizeRouteCwd(cwd) {
|
|
34
34
|
if (!cwd) return null;
|
|
35
35
|
try {
|
|
36
|
-
|
|
36
|
+
let s = String(cwd);
|
|
37
|
+
// Expand ~ to HOME before resolving (path.resolve does not handle ~)
|
|
38
|
+
if (s.startsWith('~/') || s === '~') {
|
|
39
|
+
const home = process.env.HOME || require('os').homedir();
|
|
40
|
+
s = s === '~' ? home : path.join(home, s.slice(2));
|
|
41
|
+
}
|
|
42
|
+
return path.resolve(s);
|
|
37
43
|
} catch {
|
|
38
44
|
return String(cwd);
|
|
39
45
|
}
|
|
@@ -239,8 +239,15 @@ function parseCodexStreamEvent(line) {
|
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
function buildClaudeArgs(options = {}) {
|
|
242
|
-
const { model = ENGINE_MODEL_CONFIG.claude.main, readOnly = false, session = {} } = options;
|
|
242
|
+
const { model = ENGINE_MODEL_CONFIG.claude.main, readOnly = false, session = {}, addDirs } = options;
|
|
243
243
|
const args = ['-p', '--model', model];
|
|
244
|
+
// --add-dir: grant file access to additional directories (e.g. worktrees)
|
|
245
|
+
// without changing session storage location (which follows cwd).
|
|
246
|
+
if (Array.isArray(addDirs)) {
|
|
247
|
+
for (const dir of addDirs) {
|
|
248
|
+
if (dir) args.push('--add-dir', dir);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
244
251
|
if (readOnly) {
|
|
245
252
|
const readOnlyTools = ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch', 'Task'];
|
|
246
253
|
for (const tool of readOnlyTools) args.push('--allowedTools', tool);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { classifyTaskUsage } = require('./usage-classifier');
|
|
4
4
|
const { normalizeModel } = require('./daemon-task-scheduler');
|
|
5
|
+
const { resolveEngineModel } = require('./daemon-engine-runtime');
|
|
5
6
|
const { createCommandSessionResolver } = require('./daemon-command-session-route');
|
|
6
7
|
|
|
7
8
|
function createExecCommandHandler(deps) {
|
|
@@ -12,7 +13,7 @@ function createExecCommandHandler(deps) {
|
|
|
12
13
|
HOME,
|
|
13
14
|
checkCooldown,
|
|
14
15
|
activeProcesses,
|
|
15
|
-
|
|
16
|
+
pipeline,
|
|
16
17
|
findTask,
|
|
17
18
|
checkPrecondition,
|
|
18
19
|
buildProfilePreamble,
|
|
@@ -214,20 +215,29 @@ function createExecCommandHandler(deps) {
|
|
|
214
215
|
}
|
|
215
216
|
|
|
216
217
|
if (text === '/stop') {
|
|
217
|
-
|
|
218
|
-
if (
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const signal = proc.killSignal || 'SIGTERM';
|
|
227
|
-
try { process.kill(-proc.child.pid, signal); } catch { try { proc.child.kill(signal); } catch { /* */ } }
|
|
228
|
-
await bot.sendMessage(chatId, '⏹ Stopping current engine task...');
|
|
218
|
+
const _pl = pipeline && pipeline.current;
|
|
219
|
+
if (_pl) {
|
|
220
|
+
_pl.clearQueue(chatId);
|
|
221
|
+
const stopped = _pl.interruptActive(chatId);
|
|
222
|
+
if (stopped) {
|
|
223
|
+
await bot.sendMessage(chatId, '⏹ Stopping current engine task...');
|
|
224
|
+
} else {
|
|
225
|
+
await bot.sendMessage(chatId, 'No active task to stop.');
|
|
226
|
+
}
|
|
229
227
|
} else {
|
|
230
|
-
|
|
228
|
+
// Fallback: direct activeProcesses manipulation (pipeline not yet initialized)
|
|
229
|
+
const proc = activeProcesses.get(chatId);
|
|
230
|
+
if (proc && proc.child) {
|
|
231
|
+
proc.aborted = true;
|
|
232
|
+
const signal = proc.killSignal || 'SIGTERM';
|
|
233
|
+
try { process.kill(-proc.child.pid, signal); } catch { try { proc.child.kill(signal); } catch { /* */ } }
|
|
234
|
+
await bot.sendMessage(chatId, '⏹ Stopping current engine task...');
|
|
235
|
+
} else if (proc && proc.child === null) {
|
|
236
|
+
proc.aborted = true;
|
|
237
|
+
await bot.sendMessage(chatId, '⏹ Stopping (pre-spawn phase)...');
|
|
238
|
+
} else {
|
|
239
|
+
await bot.sendMessage(chatId, 'No active task to stop.');
|
|
240
|
+
}
|
|
231
241
|
}
|
|
232
242
|
return true;
|
|
233
243
|
}
|
|
@@ -235,16 +245,17 @@ function createExecCommandHandler(deps) {
|
|
|
235
245
|
// /quit — restart session process (reloads MCP/config, keeps same session)
|
|
236
246
|
if (text === '/quit') {
|
|
237
247
|
// Stop running task if any
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
+
const _pl = pipeline && pipeline.current;
|
|
249
|
+
if (_pl) {
|
|
250
|
+
_pl.clearQueue(chatId);
|
|
251
|
+
_pl.interruptActive(chatId);
|
|
252
|
+
} else {
|
|
253
|
+
const proc = activeProcesses.get(chatId);
|
|
254
|
+
if (proc && proc.child) {
|
|
255
|
+
proc.aborted = true;
|
|
256
|
+
const signal = proc.killSignal || 'SIGTERM';
|
|
257
|
+
try { process.kill(-proc.child.pid, signal); } catch { try { proc.child.kill(signal); } catch { /* */ } }
|
|
258
|
+
}
|
|
248
259
|
}
|
|
249
260
|
const { session } = getActiveSession(chatId);
|
|
250
261
|
const name = session ? getSessionName(session.id) : null;
|
|
@@ -380,7 +391,7 @@ function createExecCommandHandler(deps) {
|
|
|
380
391
|
saveState(state2);
|
|
381
392
|
} else {
|
|
382
393
|
// Claude: warm up the new session immediately via --session-id
|
|
383
|
-
const model =
|
|
394
|
+
const model = resolveEngineModel('claude', daemonCfg);
|
|
384
395
|
const initArgs = ['-p', '--session-id', newSession.id, '--model', model];
|
|
385
396
|
if (daemonCfg.dangerously_skip_permissions) initArgs.push('--dangerously-skip-permissions');
|
|
386
397
|
const preamble = buildProfilePreamble();
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* daemon-message-pipeline.js
|
|
5
|
+
*
|
|
6
|
+
* Per-chatId message pipeline with interrupt-collect-flush semantics.
|
|
7
|
+
*
|
|
8
|
+
* Behavior:
|
|
9
|
+
* 1. First message → process immediately
|
|
10
|
+
* 2. Follow-up while busy → SIGINT pause, start collecting
|
|
11
|
+
* 3. More follow-ups → keep collecting (no timer yet)
|
|
12
|
+
* 4. Paused task dies → start debounce timer (3s)
|
|
13
|
+
* 5. Each new message resets the 3s debounce
|
|
14
|
+
* 6. Debounce fires → flush ALL collected messages as ONE prompt → ONE reply
|
|
15
|
+
* 7. Messages during flush processing → collect again, repeat cycle
|
|
16
|
+
*
|
|
17
|
+
* Priority messages (/stop, /quit, 停) bypass everything and execute immediately.
|
|
18
|
+
*
|
|
19
|
+
* Public API:
|
|
20
|
+
* processMessage(chatId, text, ctx) — enqueue & serialize
|
|
21
|
+
* isActive(chatId) — check if a message is being processed
|
|
22
|
+
* interruptActive(chatId) — abort the active process
|
|
23
|
+
* clearQueue(chatId) — drop pending messages + cancel collecting
|
|
24
|
+
* getQueueLength(chatId) — number of pending messages
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
function createMessagePipeline(deps) {
|
|
28
|
+
const {
|
|
29
|
+
activeProcesses,
|
|
30
|
+
handleCommand,
|
|
31
|
+
resetCooldown,
|
|
32
|
+
log,
|
|
33
|
+
} = deps;
|
|
34
|
+
|
|
35
|
+
// Per-chatId Promise chain tail — ensures serial execution
|
|
36
|
+
const chains = new Map(); // chatId -> Promise
|
|
37
|
+
|
|
38
|
+
// Track the original message text being processed (for merge context)
|
|
39
|
+
const activeTexts = new Map(); // chatId -> string
|
|
40
|
+
|
|
41
|
+
// Per-chatId burst collection state
|
|
42
|
+
const collecting = new Map(); // chatId -> { messages: string[], ctx, timer, chainDead: boolean }
|
|
43
|
+
|
|
44
|
+
// chatIds where flush is processing — new messages go to collecting, not interrupt
|
|
45
|
+
const resumed = new Set();
|
|
46
|
+
|
|
47
|
+
const DEBOUNCE_MS = 5000;
|
|
48
|
+
|
|
49
|
+
// Messages that must bypass everything and execute immediately
|
|
50
|
+
const STOP_RE = /^\/stop(\s|$)/i;
|
|
51
|
+
const QUIT_RE = /^\/quit$/i;
|
|
52
|
+
|
|
53
|
+
function _isPriorityMessage(text) {
|
|
54
|
+
const trimmed = (text || '').trim();
|
|
55
|
+
// Only hard-stop commands bypass. Natural language interrupts (等一下/hold on)
|
|
56
|
+
// are handled by command-router and should NOT bypass collecting — they would
|
|
57
|
+
// silently drop collected messages.
|
|
58
|
+
return STOP_RE.test(trimmed) || QUIT_RE.test(trimmed);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Core: process / collect / flush ──────────────────────────────
|
|
62
|
+
|
|
63
|
+
function processMessage(chatId, text, ctx) {
|
|
64
|
+
// Priority messages bypass everything
|
|
65
|
+
if ((chains.has(chatId) || collecting.has(chatId)) && _isPriorityMessage(text)) {
|
|
66
|
+
log('INFO', `Pipeline: priority bypass "${text.trim()}" for ${chatId}`);
|
|
67
|
+
_cancelCollecting(chatId);
|
|
68
|
+
return _processOne(chatId, text, ctx);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Currently collecting → accumulate
|
|
72
|
+
if (collecting.has(chatId)) {
|
|
73
|
+
const c = collecting.get(chatId);
|
|
74
|
+
c.messages.push(text);
|
|
75
|
+
// Only reset debounce if chain is already dead (timer active)
|
|
76
|
+
if (c.chainDead) _resetDebounce(chatId, c);
|
|
77
|
+
log('INFO', `Pipeline: collecting follow-up for ${chatId} (${c.messages.length} pending)`);
|
|
78
|
+
return Promise.resolve();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Pipeline idle → start processing
|
|
82
|
+
if (!chains.has(chatId)) {
|
|
83
|
+
activeTexts.set(chatId, text);
|
|
84
|
+
const p = _processOne(chatId, text, ctx)
|
|
85
|
+
.finally(() => {
|
|
86
|
+
activeTexts.delete(chatId);
|
|
87
|
+
chains.delete(chatId);
|
|
88
|
+
resumed.delete(chatId);
|
|
89
|
+
// If messages were collected during processing, start debounce now
|
|
90
|
+
if (collecting.has(chatId)) {
|
|
91
|
+
const c = collecting.get(chatId);
|
|
92
|
+
c.chainDead = true;
|
|
93
|
+
_resetDebounce(chatId, c);
|
|
94
|
+
log('INFO', `Pipeline: chain ended, starting debounce for ${chatId} (${c.messages.length} collected)`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
chains.set(chatId, p);
|
|
98
|
+
return p;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Pipeline busy + already flushed once → keep collecting (don't interrupt again)
|
|
102
|
+
if (resumed.has(chatId)) {
|
|
103
|
+
_addToCollecting(chatId, text, ctx);
|
|
104
|
+
log('INFO', `Pipeline: collecting (post-resume) for ${chatId}`);
|
|
105
|
+
return Promise.resolve();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Pipeline busy, first follow-up → interrupt and start collecting
|
|
109
|
+
return _startCollecting(chatId, text, ctx);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Interrupt-Collect-Flush ────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Gracefully pause the running task (SIGINT = ESC equivalent).
|
|
116
|
+
*/
|
|
117
|
+
function _pauseActive(chatId) {
|
|
118
|
+
const proc = activeProcesses.get(chatId);
|
|
119
|
+
if (proc && proc.child) {
|
|
120
|
+
proc.aborted = true;
|
|
121
|
+
proc.abortReason = 'merge-pause';
|
|
122
|
+
try { process.kill(-proc.child.pid, 'SIGINT'); } catch { try { proc.child.kill('SIGINT'); } catch { /* */ } }
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
if (proc && proc.child === null) {
|
|
126
|
+
proc.aborted = true;
|
|
127
|
+
proc.abortReason = 'merge-pause';
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Pause the running task and start collecting.
|
|
135
|
+
* No debounce timer — timer starts only after chain dies (.finally).
|
|
136
|
+
*/
|
|
137
|
+
function _startCollecting(chatId, text, ctx) {
|
|
138
|
+
_pauseActive(chatId);
|
|
139
|
+
const originalText = activeTexts.get(chatId);
|
|
140
|
+
const msgs = originalText ? [originalText, text] : [text];
|
|
141
|
+
const c = { messages: msgs, ctx, timer: null, chainDead: false };
|
|
142
|
+
collecting.set(chatId, c);
|
|
143
|
+
// NO timer here — wait for chain to die
|
|
144
|
+
log('INFO', `Pipeline: paused & collecting for ${chatId} (original: "${(originalText || '').slice(0, 30)}")`);
|
|
145
|
+
ctx.bot.sendMessage(chatId, '⏸ 已暂停,继续连发,我会合并后继续').catch(() => {});
|
|
146
|
+
return Promise.resolve();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Add a message to collecting (create if needed).
|
|
151
|
+
*/
|
|
152
|
+
function _addToCollecting(chatId, text, ctx) {
|
|
153
|
+
if (!collecting.has(chatId)) {
|
|
154
|
+
collecting.set(chatId, { messages: [text], ctx, timer: null, chainDead: false });
|
|
155
|
+
} else {
|
|
156
|
+
collecting.get(chatId).messages.push(text);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Reset the debounce timer for burst collection.
|
|
162
|
+
*/
|
|
163
|
+
function _resetDebounce(chatId, c) {
|
|
164
|
+
if (c.timer) clearTimeout(c.timer);
|
|
165
|
+
c.timer = setTimeout(() => _flushCollected(chatId), DEBOUNCE_MS);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Debounce fired — merge ALL collected messages and process as ONE call.
|
|
170
|
+
*/
|
|
171
|
+
function _flushCollected(chatId) {
|
|
172
|
+
const c = collecting.get(chatId);
|
|
173
|
+
if (!c || c.messages.length === 0) {
|
|
174
|
+
collecting.delete(chatId);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const merged = _buildMergedPrompt(c.messages);
|
|
178
|
+
const ctx = c.ctx;
|
|
179
|
+
const count = c.messages.length;
|
|
180
|
+
collecting.delete(chatId);
|
|
181
|
+
|
|
182
|
+
resumed.add(chatId);
|
|
183
|
+
log('INFO', `Pipeline: flushing ${count} collected messages for ${chatId}`);
|
|
184
|
+
const p = processMessage(chatId, merged, ctx);
|
|
185
|
+
if (p && typeof p.catch === 'function') {
|
|
186
|
+
p.catch(err => log('ERROR', `Pipeline: flush error for ${chatId}: ${err.message}`));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Build the merged prompt from collected messages.
|
|
192
|
+
*/
|
|
193
|
+
function _buildMergedPrompt(messages) {
|
|
194
|
+
if (messages.length === 1) return messages[0];
|
|
195
|
+
return messages.join('\n');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Cancel any active collecting state (clear timer + delete).
|
|
200
|
+
*/
|
|
201
|
+
function _cancelCollecting(chatId) {
|
|
202
|
+
const c = collecting.get(chatId);
|
|
203
|
+
if (c) {
|
|
204
|
+
if (c.timer) clearTimeout(c.timer);
|
|
205
|
+
collecting.delete(chatId);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── Process one message ────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Process a single message by delegating to handleCommand.
|
|
213
|
+
*/
|
|
214
|
+
async function _processOne(chatId, text, ctx) {
|
|
215
|
+
if (resetCooldown) resetCooldown(chatId);
|
|
216
|
+
const { bot, config, executeTaskByName, senderId, readOnly } = ctx;
|
|
217
|
+
try {
|
|
218
|
+
return await handleCommand(bot, chatId, text, config, executeTaskByName, senderId, readOnly);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
log('ERROR', `Pipeline: error processing message for ${chatId}: ${err.message}`);
|
|
221
|
+
return { ok: false, error: err.message };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Query / Control API ────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
function isActive(chatId) {
|
|
228
|
+
return chains.has(chatId) || collecting.has(chatId);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function interruptActive(chatId) {
|
|
232
|
+
const proc = activeProcesses.get(chatId);
|
|
233
|
+
if (proc && proc.child) {
|
|
234
|
+
proc.aborted = true;
|
|
235
|
+
// SIGINT = graceful stop (like ESC), preserves session context for --resume
|
|
236
|
+
try { process.kill(-proc.child.pid, 'SIGINT'); } catch { try { proc.child.kill('SIGINT'); } catch { /* */ } }
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
if (proc && proc.child === null) {
|
|
240
|
+
proc.aborted = true;
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function clearQueue(chatId) {
|
|
247
|
+
_cancelCollecting(chatId);
|
|
248
|
+
resumed.delete(chatId);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function getQueueLength(chatId) {
|
|
252
|
+
const c = collecting.get(chatId);
|
|
253
|
+
return c ? c.messages.length : 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
processMessage,
|
|
258
|
+
isActive,
|
|
259
|
+
interruptActive,
|
|
260
|
+
clearQueue,
|
|
261
|
+
getQueueLength,
|
|
262
|
+
// Expose internals for testing
|
|
263
|
+
_chains: chains,
|
|
264
|
+
_collecting: collecting,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
module.exports = { createMessagePipeline };
|
|
@@ -11,7 +11,7 @@ function createOpsCommandHandler(deps) {
|
|
|
11
11
|
log,
|
|
12
12
|
loadConfig,
|
|
13
13
|
loadState,
|
|
14
|
-
|
|
14
|
+
pipeline,
|
|
15
15
|
activeProcesses,
|
|
16
16
|
getSession,
|
|
17
17
|
getSessionForEngine,
|
|
@@ -37,18 +37,20 @@ function createOpsCommandHandler(deps) {
|
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
function clearMessageQueue(chatId) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (q.timer) clearTimeout(q.timer);
|
|
43
|
-
messageQueue.delete(chatId);
|
|
44
|
-
}
|
|
40
|
+
const _pl = pipeline && pipeline.current;
|
|
41
|
+
if (_pl) { _pl.clearQueue(chatId); }
|
|
45
42
|
}
|
|
46
43
|
|
|
47
44
|
function interruptActiveProcess(chatId) {
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
const _pl = pipeline && pipeline.current;
|
|
46
|
+
if (_pl) {
|
|
47
|
+
_pl.interruptActive(chatId);
|
|
48
|
+
} else {
|
|
49
|
+
const proc = activeProcesses.get(chatId);
|
|
50
|
+
if (proc && proc.child) {
|
|
51
|
+
proc.aborted = true;
|
|
52
|
+
try { process.kill(-proc.child.pid, 'SIGINT'); } catch { proc.child.kill('SIGINT'); }
|
|
53
|
+
}
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
|