@openagents-org/agent-launcher 0.2.183 → 0.2.185
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/icons/codearts.svg +9 -0
- package/package.json +1 -1
- package/registry.json +68 -0
- package/src/adapters/aider.js +2 -9
- package/src/adapters/amp.js +3 -15
- package/src/adapters/antigravity.js +3 -20
- package/src/adapters/base.js +307 -19
- package/src/adapters/claude.js +63 -47
- package/src/adapters/cline.js +2 -13
- package/src/adapters/codearts.js +316 -0
- package/src/adapters/codebuddy.js +3 -21
- package/src/adapters/codex.js +3 -13
- package/src/adapters/commandcode.js +3 -21
- package/src/adapters/copilot.js +2 -26
- package/src/adapters/cursor.js +8 -13
- package/src/adapters/deepseek.js +33 -68
- package/src/adapters/gemini.js +3 -20
- package/src/adapters/goose.js +4 -14
- package/src/adapters/hermes.js +2 -10
- package/src/adapters/index.js +4 -1
- package/src/adapters/kimi-stream.js +44 -2
- package/src/adapters/kimi.js +118 -34
- package/src/adapters/llm-direct.js +17 -6
- package/src/adapters/mini.js +3 -14
- package/src/adapters/nanoclaw.js +25 -13
- package/src/adapters/openclaw.js +41 -0
- package/src/adapters/opencode.js +66 -48
- package/src/adapters/openworker.js +21 -14
- package/src/adapters/pi.js +22 -29
- package/src/daemon.js +76 -5
- package/src/installer.js +48 -16
- package/src/paths.js +187 -92
- package/src/probe-mode.js +36 -0
- package/src/wsl.js +270 -46
package/src/adapters/gemini.js
CHANGED
|
@@ -68,12 +68,6 @@ class GeminiAdapter extends BaseAdapter {
|
|
|
68
68
|
} catch {}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
async _onControlAction(action, _payload) {
|
|
72
|
-
if (action === 'stop') {
|
|
73
|
-
await this._stopAllProcesses();
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
71
|
async _stopProcess(proc) {
|
|
78
72
|
if (!proc || proc.exitCode !== null) return;
|
|
79
73
|
try {
|
|
@@ -96,20 +90,6 @@ class GeminiAdapter extends BaseAdapter {
|
|
|
96
90
|
} catch {}
|
|
97
91
|
}
|
|
98
92
|
|
|
99
|
-
async _stopAllProcesses() {
|
|
100
|
-
const entries = Object.entries(this._channelProcesses);
|
|
101
|
-
if (!entries.length) return;
|
|
102
|
-
this._log(`Stopping ${entries.length} running process(es)...`);
|
|
103
|
-
for (const [channel, proc] of entries) {
|
|
104
|
-
await this._stopProcess(proc);
|
|
105
|
-
delete this._channelProcesses[channel];
|
|
106
|
-
delete this._channelQueues[channel];
|
|
107
|
-
try {
|
|
108
|
-
await this.sendStatus(channel, 'Execution stopped by user');
|
|
109
|
-
} catch {}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
93
|
_findNodeBin() {
|
|
114
94
|
const home = os.homedir();
|
|
115
95
|
const candidates = IS_WINDOWS
|
|
@@ -316,6 +296,9 @@ class GeminiAdapter extends BaseAdapter {
|
|
|
316
296
|
return;
|
|
317
297
|
}
|
|
318
298
|
|
|
299
|
+
// Stopped while this turn was being prepared: start nothing (no tokens).
|
|
300
|
+
if (this._stoppedBeforeStart(msgChannel)) return;
|
|
301
|
+
|
|
319
302
|
try {
|
|
320
303
|
const resolved = this._resolveToNodeCmd(cmd[0]);
|
|
321
304
|
if (resolved) {
|
package/src/adapters/goose.js
CHANGED
|
@@ -287,20 +287,7 @@ class GooseAdapter extends BaseAdapter {
|
|
|
287
287
|
return redactSecrets(text, this._secrets);
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
// --
|
|
291
|
-
|
|
292
|
-
async _onControlAction(action, payload) {
|
|
293
|
-
if (action === 'stop') {
|
|
294
|
-
const channel = (payload && typeof payload === 'object') ? payload.channel : null;
|
|
295
|
-
if (channel) {
|
|
296
|
-
await this._stopChannel(channel, 'Execution stopped by user.');
|
|
297
|
-
} else {
|
|
298
|
-
await this._stopAll('Execution stopped by user.');
|
|
299
|
-
}
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
await super._onControlAction(action, payload);
|
|
303
|
-
}
|
|
290
|
+
// -- teardown -----------------------------------------------------------
|
|
304
291
|
|
|
305
292
|
async _stopChannel(channel, message) {
|
|
306
293
|
const proc = this._channelProcesses[channel];
|
|
@@ -490,6 +477,9 @@ class GooseAdapter extends BaseAdapter {
|
|
|
490
477
|
this._log(`CLI: goose run --output-format stream-json --name ${sessionName}`
|
|
491
478
|
+ `${resume ? ' --resume' : ''} --no-profile --with-builtin developer`);
|
|
492
479
|
|
|
480
|
+
// Stopped while this turn was being prepared: start nothing (no tokens).
|
|
481
|
+
if (this._stoppedBeforeStart(channel)) return Promise.resolve(null);
|
|
482
|
+
|
|
493
483
|
return new Promise((resolve) => {
|
|
494
484
|
let proc;
|
|
495
485
|
try {
|
package/src/adapters/hermes.js
CHANGED
|
@@ -283,6 +283,8 @@ class HermesAdapter extends BaseAdapter {
|
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
async _runHermes(prompt, channelName) {
|
|
286
|
+
// Stopped while this turn was being prepared: start nothing (no tokens).
|
|
287
|
+
if (this._stoppedBeforeStart(channelName)) return '';
|
|
286
288
|
const resumeId = this._channelSessions[channelName];
|
|
287
289
|
const args = this._buildHermesCmd(prompt, resumeId);
|
|
288
290
|
this._log(`Running hermes (profile=${this.hermesProfile}, channel=${channelName}, resume=${!!resumeId})`);
|
|
@@ -357,16 +359,6 @@ class HermesAdapter extends BaseAdapter {
|
|
|
357
359
|
} catch {}
|
|
358
360
|
}
|
|
359
361
|
|
|
360
|
-
async _onControlAction(action, _payload) {
|
|
361
|
-
if (action === 'stop') {
|
|
362
|
-
for (const [channel, proc] of Object.entries(this._channelProcesses)) {
|
|
363
|
-
await this._stopProcess(proc);
|
|
364
|
-
delete this._channelProcesses[channel];
|
|
365
|
-
try { await this.sendStatus(channel, 'Execution stopped by user'); } catch {}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
362
|
// ------------------------------------------------------------------
|
|
371
363
|
// Message handler
|
|
372
364
|
// ------------------------------------------------------------------
|
package/src/adapters/index.js
CHANGED
|
@@ -26,6 +26,7 @@ const AntigravityAdapter = require('./antigravity');
|
|
|
26
26
|
const CommandCodeAdapter = require('./commandcode');
|
|
27
27
|
const OpenWorkerAdapter = require('./openworker');
|
|
28
28
|
const CodeBuddyAdapter = require('./codebuddy');
|
|
29
|
+
const CodeArtsAdapter = require('./codearts');
|
|
29
30
|
|
|
30
31
|
const ADAPTER_MAP = {
|
|
31
32
|
openclaw: OpenClawAdapter,
|
|
@@ -49,11 +50,12 @@ const ADAPTER_MAP = {
|
|
|
49
50
|
commandcode: CommandCodeAdapter,
|
|
50
51
|
openworker: OpenWorkerAdapter,
|
|
51
52
|
codebuddy: CodeBuddyAdapter,
|
|
53
|
+
codearts: CodeArtsAdapter,
|
|
52
54
|
};
|
|
53
55
|
|
|
54
56
|
/**
|
|
55
57
|
* Create an adapter instance for the given agent type.
|
|
56
|
-
* @param {string} type - Agent type (openclaw, claude, codex, opencode, nanoclaw, cursor, hermes, gemini, kimi, aider, goose, copilot, cline, amp, mini-swe-agent, pi, deepseek, antigravity, commandcode, openworker, codebuddy)
|
|
58
|
+
* @param {string} type - Agent type (openclaw, claude, codex, opencode, nanoclaw, cursor, hermes, gemini, kimi, aider, goose, copilot, cline, amp, mini-swe-agent, pi, deepseek, antigravity, commandcode, openworker, codebuddy, codearts)
|
|
57
59
|
* @param {object} opts - Adapter constructor options
|
|
58
60
|
* @returns {BaseAdapter}
|
|
59
61
|
*/
|
|
@@ -87,6 +89,7 @@ module.exports = {
|
|
|
87
89
|
AntigravityAdapter,
|
|
88
90
|
CommandCodeAdapter,
|
|
89
91
|
OpenWorkerAdapter,
|
|
92
|
+
CodeArtsAdapter,
|
|
90
93
|
createAdapter,
|
|
91
94
|
ADAPTER_MAP,
|
|
92
95
|
};
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
|
|
27
27
|
'use strict';
|
|
28
28
|
|
|
29
|
+
const crypto = require('crypto');
|
|
30
|
+
|
|
29
31
|
// ---------------------------------------------------------------------------
|
|
30
32
|
// Version / product classification
|
|
31
33
|
// ---------------------------------------------------------------------------
|
|
@@ -270,6 +272,30 @@ function buildKimiEnv(agentEnv) {
|
|
|
270
272
|
return { env, viaEnvProvider: true };
|
|
271
273
|
}
|
|
272
274
|
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// Session journal location
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* The directory Kimi Code CLI files a working directory's sessions under:
|
|
281
|
+
* `<KIMI_CODE_HOME>/sessions/<key>/<session id>/`. Mirrors the CLI's own
|
|
282
|
+
* `encodeWorkDirKey` (v0.42.0): `wd_` + a slug of the last path segment + `_` +
|
|
283
|
+
* the first 12 hex of sha256 over the path with `\` turned into `/` and no
|
|
284
|
+
* trailing slash. Case is kept, so `C:\Users\86177` → `wd_86177_5d6fa4a9b53d`.
|
|
285
|
+
*/
|
|
286
|
+
function encodeKimiWorkDirKey(workDir) {
|
|
287
|
+
const normalized = String(workDir).replace(/\\/g, '/').replace(/\/+$/, '');
|
|
288
|
+
let slug = (normalized.split('/').pop() ?? normalized)
|
|
289
|
+
.toLowerCase()
|
|
290
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
291
|
+
.replace(/^-+|-+$/g, '')
|
|
292
|
+
.slice(0, 40)
|
|
293
|
+
.replace(/^-+|-+$/g, '');
|
|
294
|
+
if (slug === '' || slug === '.' || slug === '..') slug = 'workspace';
|
|
295
|
+
const hash = crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 12);
|
|
296
|
+
return `wd_${slug}_${hash}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
273
299
|
// ---------------------------------------------------------------------------
|
|
274
300
|
// Error classification
|
|
275
301
|
// ---------------------------------------------------------------------------
|
|
@@ -292,13 +318,28 @@ function extractStderrError(stderrText) {
|
|
|
292
318
|
|
|
293
319
|
/**
|
|
294
320
|
* Classify a failed run into a user-facing message.
|
|
295
|
-
* @param {{code: number|null, signal: string|null, stderrText: string, retryMessage: string}} info
|
|
321
|
+
* @param {{code: number|null, signal: string|null, stderrText: string, retryMessage: string, timedOutMs?: number}} info
|
|
322
|
+
* `timedOutMs` is set when the adapter's idle watchdog stopped the run after
|
|
323
|
+
* that long without any sign of progress.
|
|
296
324
|
* @returns {{kind: string, userMessage: string}}
|
|
297
325
|
*/
|
|
298
326
|
function classifyKimiError(info) {
|
|
299
|
-
const { code, signal, stderrText, retryMessage } = info || {};
|
|
327
|
+
const { code, signal, stderrText, retryMessage, timedOutMs } = info || {};
|
|
300
328
|
const detail = extractStderrError(stderrText) || redactSecrets(retryMessage || '');
|
|
301
329
|
|
|
330
|
+
// The watchdog's own stop wins: the signal it leaves behind (SIGINT on
|
|
331
|
+
// Windows) explains nothing, and stderr by then holds whatever tools printed.
|
|
332
|
+
if (timedOutMs) {
|
|
333
|
+
const minutes = Math.max(1, Math.round(timedOutMs / 60000));
|
|
334
|
+
return {
|
|
335
|
+
kind: 'timeout',
|
|
336
|
+
userMessage:
|
|
337
|
+
`Kimi showed no progress for about ${minutes} min and was stopped. ` +
|
|
338
|
+
'Send the request again, or break a long task into smaller steps.' +
|
|
339
|
+
(detail ? `\n\nDetails: ${detail}` : ''),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
302
343
|
if (/auth_error|401|unauthorized|invalid.{0,20}(api.?key|token)|credential/i.test(detail)) {
|
|
303
344
|
return {
|
|
304
345
|
kind: 'auth',
|
|
@@ -351,6 +392,7 @@ module.exports = {
|
|
|
351
392
|
toolPreview,
|
|
352
393
|
buildKimiArgs,
|
|
353
394
|
buildKimiEnv,
|
|
395
|
+
encodeKimiWorkDirKey,
|
|
354
396
|
extractStderrError,
|
|
355
397
|
classifyKimiError,
|
|
356
398
|
DEFAULT_KIMI_MODEL,
|
package/src/adapters/kimi.js
CHANGED
|
@@ -47,6 +47,7 @@ const {
|
|
|
47
47
|
redactSecrets,
|
|
48
48
|
classifyKimiVersion,
|
|
49
49
|
classifyKimiError,
|
|
50
|
+
encodeKimiWorkDirKey,
|
|
50
51
|
} = require('./kimi-stream');
|
|
51
52
|
|
|
52
53
|
const IS_WINDOWS = process.platform === 'win32';
|
|
@@ -54,10 +55,13 @@ const IS_WINDOWS = process.platform === 'win32';
|
|
|
54
55
|
const DEFAULT_BASE_URL = 'https://api.moonshot.ai/v1';
|
|
55
56
|
const DEFAULT_MODEL = 'kimi-k2.6';
|
|
56
57
|
|
|
57
|
-
// Idle watchdog: if
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
58
|
+
// Idle watchdog: if a run shows no sign of life this long we nudge the user;
|
|
59
|
+
// after MAX consecutive silences we kill the (possibly hung) process. Sign of
|
|
60
|
+
// life is any stdout or stderr, or a write to the run's session journal. The
|
|
61
|
+
// journal is the only trace a subagent leaves: print mode never forwards its
|
|
62
|
+
// events, so a main agent waiting on an `Agent` tool call is otherwise mute.
|
|
63
|
+
// Kimi's provider retry backoff can reach ~1 min between attempts, so the kill
|
|
64
|
+
// threshold stays generous.
|
|
61
65
|
const WATCHDOG_INTERVAL_MS = 15_000;
|
|
62
66
|
const WATCHDOG_NUDGE_AT = 2; // ~30s of silence → "still working"
|
|
63
67
|
const WATCHDOG_MAX = 20; // ~5 min of silence → kill
|
|
@@ -67,6 +71,27 @@ const WATCHDOG_MAX = 20; // ~5 min of silence → kill
|
|
|
67
71
|
const VERSION_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
68
72
|
const _kimiVersionCache = new Map(); // binPath -> { version, product, at }
|
|
69
73
|
|
|
74
|
+
/** Newest mtime (ms) of a directory and everything under it, `depth` levels deep. */
|
|
75
|
+
function newestMtimeMs(dir, depth) {
|
|
76
|
+
let newest = 0;
|
|
77
|
+
let entries;
|
|
78
|
+
try {
|
|
79
|
+
newest = fs.statSync(dir).mtimeMs;
|
|
80
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
81
|
+
} catch {
|
|
82
|
+
return newest;
|
|
83
|
+
}
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
const p = path.join(dir, entry.name);
|
|
86
|
+
if (entry.isDirectory()) {
|
|
87
|
+
if (depth > 0) newest = Math.max(newest, newestMtimeMs(p, depth - 1));
|
|
88
|
+
} else {
|
|
89
|
+
try { newest = Math.max(newest, fs.statSync(p).mtimeMs); } catch {}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return newest;
|
|
93
|
+
}
|
|
94
|
+
|
|
70
95
|
class KimiAdapter extends LlmDirectAdapter {
|
|
71
96
|
constructor(opts) {
|
|
72
97
|
super({
|
|
@@ -106,6 +131,10 @@ class KimiAdapter extends LlmDirectAdapter {
|
|
|
106
131
|
this._channelProcesses = {}; // channel → in-flight child process
|
|
107
132
|
this._stoppingChannels = new Set();
|
|
108
133
|
this._loggedCliMode = false;
|
|
134
|
+
// Watchdog timings live on the instance so tests can run them in ms.
|
|
135
|
+
this._watchdogIntervalMs = WATCHDOG_INTERVAL_MS;
|
|
136
|
+
this._watchdogNudgeAt = WATCHDOG_NUDGE_AT;
|
|
137
|
+
this._watchdogMax = WATCHDOG_MAX;
|
|
109
138
|
this._sessionsFile = path.join(
|
|
110
139
|
os.homedir(), '.openagents', 'sessions',
|
|
111
140
|
`${this.workspaceId}_${this.agentName}_kimi.json`,
|
|
@@ -170,24 +199,6 @@ class KimiAdapter extends LlmDirectAdapter {
|
|
|
170
199
|
// ------------------------------------------------------------------
|
|
171
200
|
|
|
172
201
|
async _onControlAction(action, payload) {
|
|
173
|
-
if (action === 'stop') {
|
|
174
|
-
const channel = (payload && typeof payload === 'object') ? payload.channel : null;
|
|
175
|
-
if (channel && this._channelProcesses[channel]) {
|
|
176
|
-
this._stoppingChannels.add(channel);
|
|
177
|
-
await this._stopProcess(this._channelProcesses[channel]);
|
|
178
|
-
delete this._channelProcesses[channel];
|
|
179
|
-
delete this._channelQueues[channel];
|
|
180
|
-
try { await this.sendResponse(channel, 'Execution stopped by user.'); } catch {}
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
if (Object.keys(this._channelProcesses).length) {
|
|
184
|
-
await this._stopAllProcesses('Execution stopped by user.');
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
// No CLI runs in flight — fall through to the direct-API stop.
|
|
188
|
-
await super._onControlAction(action, payload);
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
202
|
if (action === 'restart') {
|
|
192
203
|
const channel = (payload && typeof payload === 'object') ? payload.channel : null;
|
|
193
204
|
if (channel) {
|
|
@@ -577,8 +588,9 @@ class KimiAdapter extends LlmDirectAdapter {
|
|
|
577
588
|
if (result.userStopped) return;
|
|
578
589
|
|
|
579
590
|
// Stale-session handling: a resume that died/erred with nothing useful →
|
|
580
|
-
// clear and retry fresh once.
|
|
581
|
-
|
|
591
|
+
// clear and retry fresh once. A watchdog stop is not a stale session, and
|
|
592
|
+
// retrying it would just repeat the long task.
|
|
593
|
+
if (resumeId && !result.ok && !result.anyOutput && !result.timedOutMs && attempt === 0) {
|
|
582
594
|
this._log(`Resume of session ${resumeId} failed — clearing and retrying fresh`);
|
|
583
595
|
this._clearSession(channel);
|
|
584
596
|
continue;
|
|
@@ -598,6 +610,7 @@ class KimiAdapter extends LlmDirectAdapter {
|
|
|
598
610
|
signal: result.exitSignal,
|
|
599
611
|
stderrText: result.stderrText,
|
|
600
612
|
retryMessage: result.retryMessage,
|
|
613
|
+
timedOutMs: result.timedOutMs,
|
|
601
614
|
});
|
|
602
615
|
try { await this.sendError(channel, userMessage); } catch {}
|
|
603
616
|
} else {
|
|
@@ -611,16 +624,55 @@ class KimiAdapter extends LlmDirectAdapter {
|
|
|
611
624
|
try { return fs.statSync(p).isDirectory(); } catch { return false; }
|
|
612
625
|
}
|
|
613
626
|
|
|
627
|
+
/**
|
|
628
|
+
* Snapshot the session journals that exist before a run starts, so the
|
|
629
|
+
* watchdog can tell this run's writes from other sessions' in the same
|
|
630
|
+
* working directory. Kimi files them under
|
|
631
|
+
* <KIMI_CODE_HOME>/sessions/<workdir key>/<session id>/.
|
|
632
|
+
*/
|
|
633
|
+
_watchKimiSessions(env, workingDir, args) {
|
|
634
|
+
const home = env.KIMI_CODE_HOME || path.join(os.homedir(), '.kimi-code');
|
|
635
|
+
const bucket = path.join(home, 'sessions', encodeKimiWorkDirKey(workingDir));
|
|
636
|
+
let known = [];
|
|
637
|
+
try { known = fs.readdirSync(bucket); } catch {}
|
|
638
|
+
const s = args.indexOf('-S');
|
|
639
|
+
return { bucket, known: new Set(known), resumeId: s >= 0 ? args[s + 1] : null };
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Newest write (ms) inside this run's session journals: the resumed session,
|
|
644
|
+
* plus any session created since the run started. A second fresh run in the
|
|
645
|
+
* same directory can be counted too; that only ever keeps a run alive
|
|
646
|
+
* longer, never stops one early. 0 when there is nothing to look at.
|
|
647
|
+
*/
|
|
648
|
+
_latestKimiSessionWrite(watch) {
|
|
649
|
+
let names;
|
|
650
|
+
try { names = fs.readdirSync(watch.bucket); } catch { return 0; }
|
|
651
|
+
let latest = 0;
|
|
652
|
+
for (const name of names) {
|
|
653
|
+
const resumed = !!watch.resumeId &&
|
|
654
|
+
(name === watch.resumeId || name === `session_${watch.resumeId}`);
|
|
655
|
+
if (watch.known.has(name) && !resumed) continue;
|
|
656
|
+
latest = Math.max(latest, newestMtimeMs(path.join(watch.bucket, name), 3));
|
|
657
|
+
}
|
|
658
|
+
return latest;
|
|
659
|
+
}
|
|
660
|
+
|
|
614
661
|
/**
|
|
615
662
|
* Spawn one `kimi -p … --output-format stream-json` run, stream-parse it,
|
|
616
663
|
* and resolve a summary:
|
|
617
664
|
* { ok, finalText, sessionId, anyOutput, userStopped,
|
|
618
|
-
* exitCode, exitSignal, stderrText, retryMessage }
|
|
619
|
-
* `ok` is exit code 0; `finalText` is the LAST assistant text of the turn
|
|
665
|
+
* exitCode, exitSignal, stderrText, retryMessage, timedOutMs }
|
|
666
|
+
* `ok` is exit code 0; `finalText` is the LAST assistant text of the turn;
|
|
667
|
+
* `timedOutMs` is non-zero when the idle watchdog stopped the run.
|
|
620
668
|
*/
|
|
621
669
|
_runKimi(channel, kimiBin, args, workingDir) {
|
|
670
|
+
// Stopped while this turn was being prepared: start nothing (no tokens).
|
|
671
|
+
if (this._stoppedBeforeStart(channel)) return Promise.resolve({ userStopped: true });
|
|
622
672
|
const { env: cleanEnv } = buildKimiEnv(this.agentEnv || process.env);
|
|
623
673
|
const [cmd, ...spawnArgs] = this._spawnableCmd(kimiBin, args);
|
|
674
|
+
// Before the spawn, so the session this run creates shows up as new.
|
|
675
|
+
const sessionWatch = this._watchKimiSessions(cleanEnv, workingDir, args);
|
|
624
676
|
|
|
625
677
|
this._log(`Spawning kimi in ${workingDir}: ${redactArgs([kimiBin, ...args]).join(' ')}`);
|
|
626
678
|
|
|
@@ -644,6 +696,7 @@ class KimiAdapter extends LlmDirectAdapter {
|
|
|
644
696
|
exitSignal: null,
|
|
645
697
|
stderrText: '',
|
|
646
698
|
retryMessage: '',
|
|
699
|
+
timedOutMs: 0,
|
|
647
700
|
};
|
|
648
701
|
|
|
649
702
|
return new Promise((resolve) => {
|
|
@@ -651,6 +704,8 @@ class KimiAdapter extends LlmDirectAdapter {
|
|
|
651
704
|
let watchdogTimer = null;
|
|
652
705
|
let fallbackTimer = null;
|
|
653
706
|
let lastDataMs = Date.now();
|
|
707
|
+
let seenSessionWriteMs = lastDataMs;
|
|
708
|
+
let loggedSessionAlive = false;
|
|
654
709
|
let silences = 0;
|
|
655
710
|
let queue = Promise.resolve();
|
|
656
711
|
|
|
@@ -713,8 +768,11 @@ class KimiAdapter extends LlmDirectAdapter {
|
|
|
713
768
|
});
|
|
714
769
|
|
|
715
770
|
// stderr: `error: ...` lines carry the fatal cause; collected for
|
|
716
|
-
// classification, never surfaced verbatim as an assistant reply.
|
|
771
|
+
// classification, never surfaced verbatim as an assistant reply. Tool
|
|
772
|
+
// progress also lands here in print mode, so it counts as life.
|
|
717
773
|
proc.stderr.on('data', (chunk) => {
|
|
774
|
+
lastDataMs = Date.now();
|
|
775
|
+
silences = 0;
|
|
718
776
|
state.stderrText += chunk.toString('utf-8');
|
|
719
777
|
if (state.stderrText.length > 64 * 1024) {
|
|
720
778
|
state.stderrText = state.stderrText.slice(-32 * 1024);
|
|
@@ -722,21 +780,47 @@ class KimiAdapter extends LlmDirectAdapter {
|
|
|
722
780
|
});
|
|
723
781
|
|
|
724
782
|
// Idle watchdog
|
|
783
|
+
const intervalMs = this._watchdogIntervalMs;
|
|
725
784
|
watchdogTimer = setInterval(async () => {
|
|
726
785
|
if (settled) return;
|
|
727
786
|
const elapsed = Date.now() - lastDataMs;
|
|
728
|
-
if (elapsed <
|
|
787
|
+
if (elapsed < intervalMs) { silences = 0; return; }
|
|
788
|
+
// Both pipes are quiet. A subagent still working only shows up in the
|
|
789
|
+
// session journal, so a fresh write there resets the count too.
|
|
790
|
+
const wrote = this._latestKimiSessionWrite(sessionWatch);
|
|
791
|
+
if (wrote > seenSessionWriteMs) {
|
|
792
|
+
seenSessionWriteMs = wrote;
|
|
793
|
+
lastDataMs = Date.now();
|
|
794
|
+
silences = 0;
|
|
795
|
+
if (!loggedSessionAlive) {
|
|
796
|
+
loggedSessionAlive = true;
|
|
797
|
+
this._log(`Watchdog: kimi quiet on ${channel} but its session is still being written — keeping it alive`);
|
|
798
|
+
}
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
729
801
|
silences++;
|
|
730
|
-
lastDataMs
|
|
731
|
-
|
|
802
|
+
// Deliberately NOT touching lastDataMs here: it marks the last real
|
|
803
|
+
// sign of life, and the data handlers above already reset `silences`
|
|
804
|
+
// themselves. Refreshing it on a silent tick parked the next tick's
|
|
805
|
+
// `elapsed` right on the interval boundary, where an early-firing
|
|
806
|
+
// timer (routine on Windows) read as "data just arrived" and reset
|
|
807
|
+
// the count — so a genuinely hung run was never stopped.
|
|
808
|
+
if (silences === this._watchdogNudgeAt) {
|
|
732
809
|
try { await this.sendStatus(channel, 'Still working...'); } catch {}
|
|
733
810
|
}
|
|
734
|
-
if (silences >=
|
|
735
|
-
|
|
736
|
-
|
|
811
|
+
if (silences >= this._watchdogMax) {
|
|
812
|
+
// Stop ticking before the (awaited) kill: _stopProcess can take
|
|
813
|
+
// seconds to escalate, and a tick landing inside that wait would
|
|
814
|
+
// re-log, re-kill and keep inflating timedOutMs past the budget.
|
|
815
|
+
clearInterval(watchdogTimer);
|
|
816
|
+
const silentMs = silences * intervalMs;
|
|
817
|
+
this._log(`Watchdog: kimi silent ${Math.round(silentMs / 1000)}s on ${channel} — killing`);
|
|
818
|
+
// Its own field, not stderr: tool noise already there used to leave
|
|
819
|
+
// the user with nothing but the signal name.
|
|
820
|
+
state.timedOutMs = silentMs;
|
|
737
821
|
await this._stopProcess(proc);
|
|
738
822
|
}
|
|
739
|
-
},
|
|
823
|
+
}, intervalMs);
|
|
740
824
|
|
|
741
825
|
// Finalize only once BOTH the process has exited AND stdout has ended,
|
|
742
826
|
// so the final buffered lines are never dropped (same race as Cline on
|
|
@@ -62,13 +62,21 @@ class LlmDirectAdapter extends BaseAdapter {
|
|
|
62
62
|
this._activeRequests.clear();
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
/**
|
|
66
|
+
* A stop ends this channel's open completion requests, and only those —
|
|
67
|
+
* the same agent may be answering in another thread. Subclasses that also
|
|
68
|
+
* run child processes (kimi) get those stopped by super.
|
|
69
|
+
*/
|
|
70
|
+
async _stopChannelWork(channel) {
|
|
71
|
+
let aborted = 0;
|
|
72
|
+
for (const req of this._activeRequests) {
|
|
73
|
+
if (req._oaChannel !== channel) continue;
|
|
74
|
+
try { req.destroy(new Error('LLM API request stopped')); } catch {}
|
|
75
|
+
this._activeRequests.delete(req);
|
|
76
|
+
aborted++;
|
|
71
77
|
}
|
|
78
|
+
const processes = await super._stopChannelWork(channel);
|
|
79
|
+
return aborted && processes === 'idle' ? 'stopped' : processes;
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
/** The model this runner actually calls with — see `_model` above. */
|
|
@@ -114,6 +122,8 @@ class LlmDirectAdapter extends BaseAdapter {
|
|
|
114
122
|
return;
|
|
115
123
|
}
|
|
116
124
|
|
|
125
|
+
// Stopped while this turn was being prepared: start nothing (no tokens).
|
|
126
|
+
if (this._stoppedBeforeStart(msgChannel)) return;
|
|
117
127
|
const responseText = await this._callCompletionApi(content, msgChannel);
|
|
118
128
|
|
|
119
129
|
if (responseText) {
|
|
@@ -205,6 +215,7 @@ class LlmDirectAdapter extends BaseAdapter {
|
|
|
205
215
|
});
|
|
206
216
|
});
|
|
207
217
|
|
|
218
|
+
req._oaChannel = channel;
|
|
208
219
|
this._activeRequests.add(req);
|
|
209
220
|
req.on('error', (err) => {
|
|
210
221
|
this._activeRequests.delete(req);
|
package/src/adapters/mini.js
CHANGED
|
@@ -297,22 +297,9 @@ class MiniSweAgentAdapter extends BaseAdapter {
|
|
|
297
297
|
}
|
|
298
298
|
|
|
299
299
|
// ------------------------------------------------------------------
|
|
300
|
-
//
|
|
300
|
+
// Process teardown (BaseAdapter._stopChannelWork calls _stopProcess)
|
|
301
301
|
// ------------------------------------------------------------------
|
|
302
302
|
|
|
303
|
-
async _onControlAction(action, payload) {
|
|
304
|
-
if (action === 'stop') {
|
|
305
|
-
for (const [channel, proc] of Object.entries(this._channelProcesses)) {
|
|
306
|
-
this._stoppingChannels.add(channel);
|
|
307
|
-
await this._stopProcess(proc);
|
|
308
|
-
delete this._channelProcesses[channel];
|
|
309
|
-
try { await this.sendStatus(channel, 'Execution stopped by user'); } catch {}
|
|
310
|
-
}
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
await super._onControlAction(action, payload);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
303
|
async _stopProcess(proc) {
|
|
317
304
|
if (!proc || proc.exitCode !== null) return;
|
|
318
305
|
try {
|
|
@@ -460,6 +447,8 @@ class MiniSweAgentAdapter extends BaseAdapter {
|
|
|
460
447
|
}
|
|
461
448
|
|
|
462
449
|
_spawnMini(cmd, msgChannel) {
|
|
450
|
+
// Stopped while this turn was being prepared: start nothing (no tokens).
|
|
451
|
+
if (this._stoppedBeforeStart(msgChannel)) return Promise.resolve({ text: '', error: null });
|
|
463
452
|
return new Promise((resolve, reject) => {
|
|
464
453
|
const env = this._buildSubprocessEnv();
|
|
465
454
|
|
package/src/adapters/nanoclaw.js
CHANGED
|
@@ -248,6 +248,12 @@ class NanoClawAdapter extends BaseAdapter {
|
|
|
248
248
|
await this._autoTitleChannel(channel, content);
|
|
249
249
|
await this.sendStatus(channel, 'thinking...');
|
|
250
250
|
|
|
251
|
+
// Stopped while this turn was being prepared: inject nothing (no tokens).
|
|
252
|
+
if (this._stoppedBeforeStart(channel)) {
|
|
253
|
+
if (this._activeTurn.get(channel) === turnId) this._activeTurn.delete(channel);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
251
257
|
// Inject and await the turn.
|
|
252
258
|
this._injectedIds.add(msgId);
|
|
253
259
|
let injected = false;
|
|
@@ -278,14 +284,25 @@ class NanoClawAdapter extends BaseAdapter {
|
|
|
278
284
|
// never kills the Agent Group, the NanoClaw host, or any shared container.
|
|
279
285
|
// ------------------------------------------------------------------
|
|
280
286
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
287
|
+
// BaseAdapter._handleUserStop drives these. A named channel is always
|
|
288
|
+
// detached (its epoch moves even if nothing was in flight), matching what
|
|
289
|
+
// the stop meant here before it moved into the base class.
|
|
290
|
+
async _stopChannelWork(channel) {
|
|
291
|
+
const wasActive = this._activeTurn.has(channel)
|
|
292
|
+
|| this._pending.has(proto.platformIdFor(this.workspaceId, channel));
|
|
293
|
+
this._detachChannel(channel);
|
|
294
|
+
return wasActive ? 'stopped' : 'idle';
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
_channelsWithWork() {
|
|
298
|
+
return [...new Set([...super._channelsWithWork(), ...this._activeChannels()])];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** The container task is not cancelled, only let go of — say so. */
|
|
302
|
+
_stopNoticeText(outcome) {
|
|
303
|
+
if (outcome === 'failed') return super._stopNoticeText(outcome);
|
|
304
|
+
return 'Stopped \u2014 replies from the previous task are dropped; a new message starts a fresh '
|
|
305
|
+
+ 'NanoClaw session. The previous task may keep running in the background.';
|
|
289
306
|
}
|
|
290
307
|
|
|
291
308
|
_activeChannels() {
|
|
@@ -329,11 +346,6 @@ class NanoClawAdapter extends BaseAdapter {
|
|
|
329
346
|
const state = this._pending.get(platformId);
|
|
330
347
|
if (state && state.settle) state.settle({ detached: true });
|
|
331
348
|
if (this._bridge) this._bridge.sendCancel(platformId, null); // best-effort notify
|
|
332
|
-
this.sendStatus(
|
|
333
|
-
channel,
|
|
334
|
-
'Stopped — replies from the previous task are dropped; a new message starts a fresh NanoClaw session. The previous task may keep running in the background.',
|
|
335
|
-
{ nanoclaw_state: 'detached' },
|
|
336
|
-
).catch(() => {});
|
|
337
349
|
}
|
|
338
350
|
|
|
339
351
|
/** A queued NanoClaw reply was dropped (overflow / expiry / corrupt) — surface it. */
|