@openagents-org/agent-launcher 0.2.184 → 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/package.json +1 -1
- 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/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/kimi.js +2 -18
- 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 +5 -26
- package/src/adapters/openworker.js +21 -14
- package/src/adapters/pi.js +22 -29
- package/src/daemon.js +54 -4
- package/src/wsl.js +60 -8
package/src/adapters/kimi.js
CHANGED
|
@@ -199,24 +199,6 @@ class KimiAdapter extends LlmDirectAdapter {
|
|
|
199
199
|
// ------------------------------------------------------------------
|
|
200
200
|
|
|
201
201
|
async _onControlAction(action, payload) {
|
|
202
|
-
if (action === 'stop') {
|
|
203
|
-
const channel = (payload && typeof payload === 'object') ? payload.channel : null;
|
|
204
|
-
if (channel && this._channelProcesses[channel]) {
|
|
205
|
-
this._stoppingChannels.add(channel);
|
|
206
|
-
await this._stopProcess(this._channelProcesses[channel]);
|
|
207
|
-
delete this._channelProcesses[channel];
|
|
208
|
-
delete this._channelQueues[channel];
|
|
209
|
-
try { await this.sendResponse(channel, 'Execution stopped by user.'); } catch {}
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
if (Object.keys(this._channelProcesses).length) {
|
|
213
|
-
await this._stopAllProcesses('Execution stopped by user.');
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
// No CLI runs in flight — fall through to the direct-API stop.
|
|
217
|
-
await super._onControlAction(action, payload);
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
202
|
if (action === 'restart') {
|
|
221
203
|
const channel = (payload && typeof payload === 'object') ? payload.channel : null;
|
|
222
204
|
if (channel) {
|
|
@@ -685,6 +667,8 @@ class KimiAdapter extends LlmDirectAdapter {
|
|
|
685
667
|
* `timedOutMs` is non-zero when the idle watchdog stopped the run.
|
|
686
668
|
*/
|
|
687
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 });
|
|
688
672
|
const { env: cleanEnv } = buildKimiEnv(this.agentEnv || process.env);
|
|
689
673
|
const [cmd, ...spawnArgs] = this._spawnableCmd(kimiBin, args);
|
|
690
674
|
// Before the spawn, so the session this run creates shows up as new.
|
|
@@ -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. */
|
package/src/adapters/openclaw.js
CHANGED
|
@@ -41,6 +41,9 @@ class OpenClawAdapter extends BaseAdapter {
|
|
|
41
41
|
super(opts);
|
|
42
42
|
this.openclawAgentId = opts.openclawAgentId || 'main';
|
|
43
43
|
this.disabledModules = opts.disabledModules || new Set();
|
|
44
|
+
// channel → the `openclaw agent` run in flight, so a stop can end it
|
|
45
|
+
// instead of letting it spend tokens until it finishes on its own.
|
|
46
|
+
this._channelProcesses = {};
|
|
44
47
|
|
|
45
48
|
// Find the openclaw binary — always use CLI/gateway mode for full tool support
|
|
46
49
|
this._openclawBinary = this._findOpenclawBinary();
|
|
@@ -250,6 +253,7 @@ class OpenClawAdapter extends BaseAdapter {
|
|
|
250
253
|
|
|
251
254
|
try {
|
|
252
255
|
const responseText = await this._runCliAgent(content, msgChannel);
|
|
256
|
+
if (this._stoppingChannels.has(msgChannel)) return;
|
|
253
257
|
|
|
254
258
|
if (responseText) {
|
|
255
259
|
await this.sendResponse(msgChannel, responseText);
|
|
@@ -268,6 +272,11 @@ class OpenClawAdapter extends BaseAdapter {
|
|
|
268
272
|
|
|
269
273
|
_runCliAgent(userMessage, channel) {
|
|
270
274
|
return new Promise((resolve, reject) => {
|
|
275
|
+
// Stopped while this turn was being prepared: start nothing (no tokens).
|
|
276
|
+
if (this._stoppedBeforeStart(channel)) {
|
|
277
|
+
resolve('');
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
271
280
|
// Re-check binary if not found at construction time (installed after daemon started)
|
|
272
281
|
if (!this._openclawBinary) {
|
|
273
282
|
this._openclawBinary = this._findOpenclawBinary();
|
|
@@ -383,8 +392,14 @@ class OpenClawAdapter extends BaseAdapter {
|
|
|
383
392
|
env: spawnEnv,
|
|
384
393
|
cwd: this.workingDir || process.env.HOME || '/',
|
|
385
394
|
timeout: 600000,
|
|
395
|
+
// Its own process group, so a stop reaches the tools it started too.
|
|
396
|
+
detached: !IS_WINDOWS,
|
|
386
397
|
windowsHide: true,
|
|
387
398
|
});
|
|
399
|
+
this._channelProcesses[channel] = proc;
|
|
400
|
+
const unregister = () => {
|
|
401
|
+
if (this._channelProcesses[channel] === proc) delete this._channelProcesses[channel];
|
|
402
|
+
};
|
|
388
403
|
if (proc.stdout) proc.stdout.on('data', (d) => { output += d; });
|
|
389
404
|
|
|
390
405
|
// Poll stderr file every 500ms for tool events
|
|
@@ -423,6 +438,7 @@ class OpenClawAdapter extends BaseAdapter {
|
|
|
423
438
|
}, 600000);
|
|
424
439
|
|
|
425
440
|
proc.on('error', (err) => {
|
|
441
|
+
unregister();
|
|
426
442
|
if (settled) return;
|
|
427
443
|
settled = true;
|
|
428
444
|
clearInterval(pollInterval);
|
|
@@ -432,6 +448,7 @@ class OpenClawAdapter extends BaseAdapter {
|
|
|
432
448
|
reject(err);
|
|
433
449
|
});
|
|
434
450
|
proc.on('exit', (code) => {
|
|
451
|
+
unregister();
|
|
435
452
|
if (settled) return;
|
|
436
453
|
settled = true;
|
|
437
454
|
clearInterval(pollInterval);
|
|
@@ -457,6 +474,11 @@ class OpenClawAdapter extends BaseAdapter {
|
|
|
457
474
|
const hasPayloads = allOutput.includes('"payloads"');
|
|
458
475
|
this._log(`CLI parse: hasPayloads=${hasPayloads}, total=${allOutput.length}b`);
|
|
459
476
|
|
|
477
|
+
// Killed by a user stop: the stop has been announced, nothing to report.
|
|
478
|
+
if (this._stoppingChannels.has(channel)) {
|
|
479
|
+
resolve('');
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
460
482
|
if (code !== 0) {
|
|
461
483
|
reject(new Error(`CLI exited ${code}: ${allOutput.slice(-300)}`));
|
|
462
484
|
return;
|
|
@@ -466,6 +488,25 @@ class OpenClawAdapter extends BaseAdapter {
|
|
|
466
488
|
});
|
|
467
489
|
}
|
|
468
490
|
|
|
491
|
+
/** End a run and everything it started (BaseAdapter._stopChannelWork calls this). */
|
|
492
|
+
async _stopProcess(proc) {
|
|
493
|
+
if (!proc || proc.exitCode !== null) return;
|
|
494
|
+
try {
|
|
495
|
+
if (IS_WINDOWS) {
|
|
496
|
+
try { execSync(`taskkill /F /T /PID ${proc.pid}`, { timeout: 5000 }); } catch {}
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
try { process.kill(-proc.pid, 'SIGTERM'); } catch { proc.kill('SIGTERM'); }
|
|
500
|
+
await new Promise((resolve) => {
|
|
501
|
+
const timeout = setTimeout(() => {
|
|
502
|
+
try { process.kill(-proc.pid, 'SIGKILL'); } catch { proc.kill('SIGKILL'); }
|
|
503
|
+
resolve();
|
|
504
|
+
}, 5000);
|
|
505
|
+
proc.on('exit', () => { clearTimeout(timeout); resolve(); });
|
|
506
|
+
});
|
|
507
|
+
} catch {}
|
|
508
|
+
}
|
|
509
|
+
|
|
469
510
|
_parseCliOutput(output, resolve) {
|
|
470
511
|
const text = output.trim();
|
|
471
512
|
if (!text) { resolve(''); return; }
|
package/src/adapters/opencode.js
CHANGED
|
@@ -237,32 +237,6 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
237
237
|
return path.join(this.agentHome, `sessions-${slug}.json`);
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
async _onControlAction(action, payload) {
|
|
241
|
-
if (action === 'stop') {
|
|
242
|
-
const channel = (payload && typeof payload === 'object') ? payload.channel : null;
|
|
243
|
-
if (channel) {
|
|
244
|
-
const proc = this._channelProcesses[channel];
|
|
245
|
-
const hadQueuedWork = !!this._channelQueues[channel]?.length;
|
|
246
|
-
if (proc) {
|
|
247
|
-
this._log(`Stopping process for channel=${channel}`);
|
|
248
|
-
this._stoppingChannels.add(channel);
|
|
249
|
-
await this._stopProcess(proc);
|
|
250
|
-
delete this._channelProcesses[channel];
|
|
251
|
-
}
|
|
252
|
-
delete this._channelQueues[channel];
|
|
253
|
-
if (proc || hadQueuedWork) {
|
|
254
|
-
try {
|
|
255
|
-
await this.sendResponse(channel, 'Execution stopped by user.');
|
|
256
|
-
} catch {}
|
|
257
|
-
}
|
|
258
|
-
} else {
|
|
259
|
-
await this._stopAllProcesses('Execution stopped by user.');
|
|
260
|
-
}
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
await super._onControlAction(action, payload);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
240
|
/**
|
|
267
241
|
* Write workspace skill to OpenCode's skill directory for auto-discovery.
|
|
268
242
|
*/
|
|
@@ -861,6 +835,9 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
861
835
|
spawnBinary = process.env.COMSPEC || 'cmd.exe';
|
|
862
836
|
}
|
|
863
837
|
|
|
838
|
+
// Stopped while this turn was being prepared: start nothing (no tokens).
|
|
839
|
+
if (this._stoppedBeforeStart(msgChannel)) return Promise.resolve('');
|
|
840
|
+
|
|
864
841
|
return new Promise((resolve, reject) => {
|
|
865
842
|
const proc = spawn(spawnBinary, spawnArgs, {
|
|
866
843
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -1177,6 +1154,8 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
1177
1154
|
* normal reply. Carries `error_category` in metadata so the UI can route it.
|
|
1178
1155
|
*/
|
|
1179
1156
|
async _sendClassifiedError(channel, category, detail) {
|
|
1157
|
+
// A run the user stopped reads as "stopped mid-task"; it is not an error.
|
|
1158
|
+
if (this._isMuted(channel)) return;
|
|
1180
1159
|
const messages = this._failureMessages();
|
|
1181
1160
|
const base = messages[category] || messages.unknown_error;
|
|
1182
1161
|
const safe = detail ? OpenCodeAdapter._redact(detail).trim() : '';
|
|
@@ -536,28 +536,26 @@ class OpenWorkerAdapter extends BaseAdapter {
|
|
|
536
536
|
}
|
|
537
537
|
|
|
538
538
|
// ------------------------------------------------------------------
|
|
539
|
-
//
|
|
539
|
+
// Stop / shutdown
|
|
540
540
|
// ------------------------------------------------------------------
|
|
541
541
|
|
|
542
|
-
async _onControlAction(action, payload) {
|
|
543
|
-
if (action === 'stop') {
|
|
544
|
-
const channel = (payload && payload.channel) || null;
|
|
545
|
-
if (channel) {
|
|
546
|
-
await this._stopChannel(channel, 'Execution stopped.');
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
await this._stopAllChannels();
|
|
550
|
-
return;
|
|
551
|
-
}
|
|
552
|
-
return super._onControlAction(action, payload);
|
|
553
|
-
}
|
|
554
|
-
|
|
555
542
|
stop() {
|
|
556
543
|
super.stop();
|
|
557
544
|
this._serverStopped = true;
|
|
558
545
|
void this._stopAllChannels('Agent stopped.').finally(() => this._stopServer());
|
|
559
546
|
}
|
|
560
547
|
|
|
548
|
+
/** A turn here is a socket to the engine, not a child process. */
|
|
549
|
+
async _stopChannelWork(channel) {
|
|
550
|
+
if (!this._channelSockets[channel]) return super._stopChannelWork(channel);
|
|
551
|
+
await this._stopChannel(channel, null);
|
|
552
|
+
return 'stopped';
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
_channelsWithWork() {
|
|
556
|
+
return [...new Set([...super._channelsWithWork(), ...Object.keys(this._channelSockets)])];
|
|
557
|
+
}
|
|
558
|
+
|
|
561
559
|
async _stopChannel(channel, message) {
|
|
562
560
|
const socket = this._channelSockets[channel];
|
|
563
561
|
if (!socket) return;
|
|
@@ -637,6 +635,10 @@ class OpenWorkerAdapter extends BaseAdapter {
|
|
|
637
635
|
* @returns {Promise<object>} { texts, error, interrupted, userStopped, sent }
|
|
638
636
|
*/
|
|
639
637
|
_runTurn(channel, server, sessionId, workingDir, text, { planMode, model }) {
|
|
638
|
+
// Stopped while this turn was being prepared: start nothing (no tokens).
|
|
639
|
+
if (this._stoppedBeforeStart(channel)) {
|
|
640
|
+
return Promise.resolve({ texts: [], error: null, interrupted: false, userStopped: true, sent: false });
|
|
641
|
+
}
|
|
640
642
|
return new Promise((resolve) => {
|
|
641
643
|
const url = sessionUrl({
|
|
642
644
|
port: server.port,
|
|
@@ -686,6 +688,11 @@ class OpenWorkerAdapter extends BaseAdapter {
|
|
|
686
688
|
|
|
687
689
|
const sendTurn = () => {
|
|
688
690
|
if (sentMessage) return;
|
|
691
|
+
// The handshake takes a moment; a stop inside it still sends nothing.
|
|
692
|
+
if (this._stoppedBeforeStart(channel)) {
|
|
693
|
+
finish({});
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
689
696
|
sentMessage = true;
|
|
690
697
|
// Anything observed before this point belongs to the turn we cleared out
|
|
691
698
|
// of the way (see the `ready.running` branch), not to ours — carrying it
|
package/src/adapters/pi.js
CHANGED
|
@@ -147,9 +147,6 @@ class PiAdapter extends BaseAdapter {
|
|
|
147
147
|
// channel → child process (BaseAdapter/stop paths read this)
|
|
148
148
|
this._channelProcesses = {};
|
|
149
149
|
this._stoppingChannels = new Set();
|
|
150
|
-
// Dedup for "Execution stopped by user." — the control handler and the
|
|
151
|
-
// in-flight message handler both race to announce it (see claude.js).
|
|
152
|
-
this._stopNoticeSent = new Set();
|
|
153
150
|
|
|
154
151
|
this._sessionsFile = path.join(
|
|
155
152
|
os.homedir(), '.openagents', 'sessions',
|
|
@@ -524,24 +521,6 @@ class PiAdapter extends BaseAdapter {
|
|
|
524
521
|
// ------------------------------------------------------------------
|
|
525
522
|
|
|
526
523
|
async _onControlAction(action, payload) {
|
|
527
|
-
if (action === 'stop') {
|
|
528
|
-
const channel = (payload && typeof payload === 'object') ? payload.channel : null;
|
|
529
|
-
if (channel) {
|
|
530
|
-
const pp = this._persistentProcs[channel];
|
|
531
|
-
if (pp) pp.userStopped = true;
|
|
532
|
-
if (this._channelProcesses[channel]) {
|
|
533
|
-
this._log(`Stopping Pi for channel=${channel}`);
|
|
534
|
-
this._stoppingChannels.add(channel);
|
|
535
|
-
await this._abortChannel(channel);
|
|
536
|
-
delete this._channelQueues[channel];
|
|
537
|
-
await this._postStopNotice(channel);
|
|
538
|
-
}
|
|
539
|
-
} else {
|
|
540
|
-
for (const pp of Object.values(this._persistentProcs)) pp.userStopped = true;
|
|
541
|
-
await this._stopAllProcesses('Execution stopped by user.');
|
|
542
|
-
}
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
524
|
if (action === 'restart') {
|
|
546
525
|
const channel = (payload && typeof payload === 'object') ? payload.channel : null;
|
|
547
526
|
if (channel) {
|
|
@@ -594,11 +573,20 @@ class PiAdapter extends BaseAdapter {
|
|
|
594
573
|
}
|
|
595
574
|
}
|
|
596
575
|
|
|
597
|
-
/**
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
576
|
+
/**
|
|
577
|
+
* Stop ONE channel's Pi turn (BaseAdapter._handleUserStop drives this).
|
|
578
|
+
* `userStopped` goes on first so the turn that is winding down reports a
|
|
579
|
+
* stop rather than a failure.
|
|
580
|
+
*/
|
|
581
|
+
async _stopChannelWork(channel) {
|
|
582
|
+
const pp = this._persistentProcs[channel];
|
|
583
|
+
if (pp) pp.userStopped = true;
|
|
584
|
+
// Abort goes over the RPC channel of the persistent record; a bare child
|
|
585
|
+
// with no record is simply killed.
|
|
586
|
+
if (!pp || !this._channelProcesses[channel]) return super._stopChannelWork(channel);
|
|
587
|
+
this._log(`Stopping Pi for channel=${channel}`);
|
|
588
|
+
await this._abortChannel(channel);
|
|
589
|
+
return 'stopped';
|
|
602
590
|
}
|
|
603
591
|
|
|
604
592
|
/**
|
|
@@ -1346,7 +1334,6 @@ class PiAdapter extends BaseAdapter {
|
|
|
1346
1334
|
const channel = msg.sessionId || this.channelName || 'general';
|
|
1347
1335
|
|
|
1348
1336
|
this._stoppingChannels.delete(channel);
|
|
1349
|
-
this._stopNoticeSent.delete(channel);
|
|
1350
1337
|
|
|
1351
1338
|
if (!content && !attachments.length) return;
|
|
1352
1339
|
|
|
@@ -1400,6 +1387,9 @@ class PiAdapter extends BaseAdapter {
|
|
|
1400
1387
|
browserEnabled,
|
|
1401
1388
|
});
|
|
1402
1389
|
|
|
1390
|
+
// Stopped while this turn was being prepared: start nothing (no tokens).
|
|
1391
|
+
if (this._stoppedBeforeStart(channel)) return;
|
|
1392
|
+
|
|
1403
1393
|
let pp;
|
|
1404
1394
|
try {
|
|
1405
1395
|
pp = await this._ensureProc(channel, workingDir, systemPrompt);
|
|
@@ -1408,6 +1398,9 @@ class PiAdapter extends BaseAdapter {
|
|
|
1408
1398
|
return;
|
|
1409
1399
|
}
|
|
1410
1400
|
|
|
1401
|
+
// Starting the process takes a moment; the prompt is what costs tokens.
|
|
1402
|
+
if (this._stoppedBeforeStart(channel)) return;
|
|
1403
|
+
|
|
1411
1404
|
// Fresh per-turn accounting.
|
|
1412
1405
|
pp.msgChannel = channel;
|
|
1413
1406
|
pp.turnTexts = [];
|
|
@@ -1453,7 +1446,7 @@ class PiAdapter extends BaseAdapter {
|
|
|
1453
1446
|
pp.turnInFlight = false;
|
|
1454
1447
|
|
|
1455
1448
|
if (pp.userStopped || this._stoppingChannels.has(channel)) {
|
|
1456
|
-
|
|
1449
|
+
await this._finishUserStop(channel);
|
|
1457
1450
|
pp.userStopped = false;
|
|
1458
1451
|
return;
|
|
1459
1452
|
}
|
|
@@ -1470,7 +1463,7 @@ class PiAdapter extends BaseAdapter {
|
|
|
1470
1463
|
async _reportTurnFailure(pp, channel, error) {
|
|
1471
1464
|
const raw = (error && error.message) || String(error);
|
|
1472
1465
|
if (error instanceof PiCancelledError && (pp.userStopped || this._stoppingChannels.has(channel))) {
|
|
1473
|
-
await this.
|
|
1466
|
+
await this._finishUserStop(channel);
|
|
1474
1467
|
return;
|
|
1475
1468
|
}
|
|
1476
1469
|
const { kind, userMessage } = classifyPiError(raw);
|
package/src/daemon.js
CHANGED
|
@@ -23,6 +23,27 @@ function maskApiKey(key) {
|
|
|
23
23
|
return key.slice(0, 4) + '...' + key.slice(-4);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Top-level filesystem locations the workspace folder picker can jump to.
|
|
28
|
+
* Windows has one root per mounted drive; POSIX systems have a single root.
|
|
29
|
+
* Keeping this as a small pure helper makes the Windows behaviour testable on
|
|
30
|
+
* every CI platform.
|
|
31
|
+
*/
|
|
32
|
+
function filesystemRoots(platform = os.platform(), exists = fs.existsSync) {
|
|
33
|
+
if (platform !== 'win32') return ['/'];
|
|
34
|
+
|
|
35
|
+
const roots = [];
|
|
36
|
+
for (let code = 'A'.charCodeAt(0); code <= 'Z'.charCodeAt(0); code++) {
|
|
37
|
+
const root = `${String.fromCharCode(code)}:\\`;
|
|
38
|
+
try {
|
|
39
|
+
if (exists(root)) roots.push(root);
|
|
40
|
+
} catch {
|
|
41
|
+
// A disconnected or unreadable mapped drive should not break heartbeat.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return roots;
|
|
45
|
+
}
|
|
46
|
+
|
|
26
47
|
/**
|
|
27
48
|
* Agent process lifecycle manager.
|
|
28
49
|
*
|
|
@@ -327,8 +348,9 @@ class Daemon {
|
|
|
327
348
|
}
|
|
328
349
|
|
|
329
350
|
/**
|
|
330
|
-
* Filesystem hint for the working-directory picker: home
|
|
331
|
-
* (non-hidden) subfolders,
|
|
351
|
+
* Filesystem hint for the working-directory picker: home, its immediate
|
|
352
|
+
* (non-hidden) subfolders, and the device's filesystem roots. Roots let a
|
|
353
|
+
* Windows user switch drives instead of being trapped on the home drive.
|
|
332
354
|
*/
|
|
333
355
|
_buildFs() {
|
|
334
356
|
try {
|
|
@@ -338,7 +360,7 @@ class Daemon {
|
|
|
338
360
|
.map((d) => d.name)
|
|
339
361
|
.sort((a, b) => a.localeCompare(b))
|
|
340
362
|
.slice(0, 100);
|
|
341
|
-
return { home, dirs };
|
|
363
|
+
return { home, dirs, roots: filesystemRoots() };
|
|
342
364
|
} catch {
|
|
343
365
|
return {};
|
|
344
366
|
}
|
|
@@ -374,6 +396,31 @@ class Daemon {
|
|
|
374
396
|
if (nativeVar) await this._runAgn(['env', type, '--set', `${nativeVar}=${val}`]);
|
|
375
397
|
}
|
|
376
398
|
|
|
399
|
+
/**
|
|
400
|
+
* A node command targets one agent. The launcher may already have saved an
|
|
401
|
+
* instance-level model/key/URL, which wins over the type env written above.
|
|
402
|
+
* Mirror the command onto that instance so the running adapter and the
|
|
403
|
+
* launcher's "This Computer" row both see the requested settings.
|
|
404
|
+
*/
|
|
405
|
+
_saveNodeAgentEnv(name, type, args) {
|
|
406
|
+
const changes = {};
|
|
407
|
+
if (args.apiKey && !args.useDeviceCredentials) changes.LLM_API_KEY = args.apiKey;
|
|
408
|
+
if (args.baseUrl !== undefined) changes.LLM_BASE_URL = args.baseUrl;
|
|
409
|
+
if (args.model !== undefined) {
|
|
410
|
+
const existing = this.config.getAgent(name);
|
|
411
|
+
if (!existing) throw new Error(`Agent '${name}' not found after create`);
|
|
412
|
+
// Old instance-level native model keys can outrank LLM_MODEL in the
|
|
413
|
+
// launcher's display and in CLIs without a resolve_env model rule.
|
|
414
|
+
for (const key of Object.keys(existing.env || {})) {
|
|
415
|
+
if (key === 'MODEL' || key.endsWith('_MODEL')) changes[key] = '';
|
|
416
|
+
}
|
|
417
|
+
changes.LLM_MODEL = String(args.model || '');
|
|
418
|
+
const nativeVar = { claude: 'ANTHROPIC_MODEL', gemini: 'GEMINI_MODEL' }[type] || this._nativeModelVar(type);
|
|
419
|
+
if (nativeVar) changes[nativeVar] = String(args.model || '');
|
|
420
|
+
}
|
|
421
|
+
if (Object.keys(changes).length) this.config.updateAgentEnv(name, changes);
|
|
422
|
+
}
|
|
423
|
+
|
|
377
424
|
/**
|
|
378
425
|
* The agent's own model setting, for a type whose registry declares one and
|
|
379
426
|
* does not map LLM_MODEL onto anything itself (CodeArts reads CODEARTS_MODEL
|
|
@@ -477,6 +524,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
477
524
|
if (args.model) await this._setModelEnv(type, args.model);
|
|
478
525
|
if (args.baseUrl) await this._runAgn(['env', type, '--set', `LLM_BASE_URL=${args.baseUrl}`]);
|
|
479
526
|
await this._applyConfigMap(type, args.config);
|
|
527
|
+
this._saveNodeAgentEnv(name, type, args);
|
|
480
528
|
// Attach to this node's workspace BY SLUG: the daemon already knows
|
|
481
529
|
// which workspace this command came from, so there is no token to
|
|
482
530
|
// pass and no /v1/token/resolve round-trip to fail (the outage class
|
|
@@ -519,6 +567,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
519
567
|
await this._runAgn(['remove', name]);
|
|
520
568
|
const rc = await this._runAgn(['create', name, '--type', type, '--install', '--path', newDir]);
|
|
521
569
|
if (rc.code !== 0) throw new Error(rc.stderr || rc.stdout || 'recreate failed');
|
|
570
|
+
this._saveNodeAgentEnv(name, type, args);
|
|
522
571
|
const rc2 = await this._runAgn([
|
|
523
572
|
'connect', name, '--workspace', n.workspace_slug || n.workspace_id,
|
|
524
573
|
]);
|
|
@@ -528,6 +577,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
528
577
|
// through daemon.cmd, and writeCommand() OVERWRITES that file — the
|
|
529
578
|
// start could clobber the unread stop (agent keeps the stale env) or
|
|
530
579
|
// interleave with the poll so the agent stops and never restarts.
|
|
580
|
+
this._saveNodeAgentEnv(name, type, args);
|
|
531
581
|
await this.restartAgent(name);
|
|
532
582
|
}
|
|
533
583
|
ok = true;
|
|
@@ -1722,4 +1772,4 @@ async _runNodeCommand(n, cmd) {
|
|
|
1722
1772
|
}
|
|
1723
1773
|
}
|
|
1724
1774
|
|
|
1725
|
-
module.exports = { Daemon };
|
|
1775
|
+
module.exports = { Daemon, filesystemRoots };
|