@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/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
|
@@ -114,12 +114,54 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
114
114
|
|
|
115
115
|
this._opencodeBinary = this._findOpencodeBinary();
|
|
116
116
|
if (this._opencodeBinary) {
|
|
117
|
-
this._log(`Using
|
|
117
|
+
this._log(`Using ${this._cliLabel()} subprocess mode: ${this._opencodeBinary}`);
|
|
118
118
|
} else {
|
|
119
|
-
this._log(
|
|
119
|
+
this._log(`${this._cliLabel()} binary not found. ${this._installHint()}`);
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
// ------------------------------------------------------------------
|
|
124
|
+
// What differs between OpenCode and the CLIs built on it
|
|
125
|
+
// ------------------------------------------------------------------
|
|
126
|
+
//
|
|
127
|
+
// Vendors ship their own agents on top of OpenCode (Huawei's CodeArts Agent
|
|
128
|
+
// is one — see codearts.js). They keep its `run --format json` stream, its
|
|
129
|
+
// sessions and its failure shapes, and change how the CLI is found, launched
|
|
130
|
+
// and authenticated. These hooks are exactly that difference.
|
|
131
|
+
|
|
132
|
+
/** Product name used in logs and user-facing errors. */
|
|
133
|
+
_cliLabel() {
|
|
134
|
+
return 'OpenCode';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
_installHint() {
|
|
138
|
+
return `Install with: npm install -g opencode-ai@${OPENCODE_PINNED_VERSION}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** User-facing message per failure category. */
|
|
142
|
+
_failureMessages() {
|
|
143
|
+
return FAILURE_MESSAGES;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Whether a run may not start without a model (OpenCode hangs without one). */
|
|
147
|
+
_modelRequired() {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Arguments after the binary for one `run`. */
|
|
152
|
+
_buildRunArgs({ runCwd, model, sessionId }) {
|
|
153
|
+
// Preflight guarantees a resolvable model; pin it explicitly — without it
|
|
154
|
+
// opencode hangs waiting for interactive provider/model selection.
|
|
155
|
+
const args = ['run', '--format', 'json', '--dir', runCwd, '--model', model];
|
|
156
|
+
if (sessionId) args.push('--session', sessionId);
|
|
157
|
+
return args;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Environment the CLI is spawned with. */
|
|
161
|
+
_spawnEnv() {
|
|
162
|
+
return { ...(this.agentEnv || process.env) };
|
|
163
|
+
}
|
|
164
|
+
|
|
123
165
|
/**
|
|
124
166
|
* Migrate sessions file from old location to agent home.
|
|
125
167
|
*/
|
|
@@ -195,32 +237,6 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
195
237
|
return path.join(this.agentHome, `sessions-${slug}.json`);
|
|
196
238
|
}
|
|
197
239
|
|
|
198
|
-
async _onControlAction(action, payload) {
|
|
199
|
-
if (action === 'stop') {
|
|
200
|
-
const channel = (payload && typeof payload === 'object') ? payload.channel : null;
|
|
201
|
-
if (channel) {
|
|
202
|
-
const proc = this._channelProcesses[channel];
|
|
203
|
-
const hadQueuedWork = !!this._channelQueues[channel]?.length;
|
|
204
|
-
if (proc) {
|
|
205
|
-
this._log(`Stopping process for channel=${channel}`);
|
|
206
|
-
this._stoppingChannels.add(channel);
|
|
207
|
-
await this._stopProcess(proc);
|
|
208
|
-
delete this._channelProcesses[channel];
|
|
209
|
-
}
|
|
210
|
-
delete this._channelQueues[channel];
|
|
211
|
-
if (proc || hadQueuedWork) {
|
|
212
|
-
try {
|
|
213
|
-
await this.sendResponse(channel, 'Execution stopped by user.');
|
|
214
|
-
} catch {}
|
|
215
|
-
}
|
|
216
|
-
} else {
|
|
217
|
-
await this._stopAllProcesses('Execution stopped by user.');
|
|
218
|
-
}
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
await super._onControlAction(action, payload);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
240
|
/**
|
|
225
241
|
* Write workspace skill to OpenCode's skill directory for auto-discovery.
|
|
226
242
|
*/
|
|
@@ -793,30 +809,25 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
793
809
|
try { this._ensureCustomProviderConfig(runCwd); } catch (e) {
|
|
794
810
|
this._log(`Gateway provider config failed (continuing): ${e.message}`);
|
|
795
811
|
}
|
|
796
|
-
const cmd = [binary, 'run', '--format', 'json', '--dir', runCwd];
|
|
797
|
-
|
|
798
|
-
// Preflight guarantees a resolvable model; pin it explicitly — without it
|
|
799
|
-
// opencode hangs waiting for interactive provider/model selection.
|
|
800
812
|
const model = this._resolveModel();
|
|
801
|
-
cmd.push('--model', model);
|
|
802
|
-
|
|
803
813
|
const sessionId = this._channelSessions[msgChannel];
|
|
814
|
+
const args = this._buildRunArgs({ runCwd, model, sessionId });
|
|
815
|
+
|
|
804
816
|
let fullPrompt;
|
|
805
817
|
if (sessionId) {
|
|
806
818
|
fullPrompt = content;
|
|
807
|
-
cmd.push('--session', sessionId);
|
|
808
819
|
} else {
|
|
809
820
|
this._ensureWorkspaceSkill(msgChannel);
|
|
810
821
|
const context = this._buildSystemContext(msgChannel);
|
|
811
822
|
fullPrompt = `${context}\n\n---\n\n${content}`;
|
|
812
823
|
}
|
|
813
824
|
|
|
814
|
-
this._log(`CLI: ${binary}
|
|
825
|
+
this._log(`CLI: ${binary} ${args.join(' ')}`);
|
|
815
826
|
|
|
816
|
-
const spawnEnv =
|
|
827
|
+
const spawnEnv = this._spawnEnv();
|
|
817
828
|
|
|
818
|
-
let spawnBinary =
|
|
819
|
-
let spawnArgs =
|
|
829
|
+
let spawnBinary = binary;
|
|
830
|
+
let spawnArgs = args;
|
|
820
831
|
// Only the npm `.cmd` shim needs a cmd.exe host. The native opencode.exe
|
|
821
832
|
// (preferred by _findOpencodeBinary) is spawned directly — no console host.
|
|
822
833
|
if (IS_WINDOWS && spawnBinary.toLowerCase().endsWith('.cmd')) {
|
|
@@ -824,6 +835,9 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
824
835
|
spawnBinary = process.env.COMSPEC || 'cmd.exe';
|
|
825
836
|
}
|
|
826
837
|
|
|
838
|
+
// Stopped while this turn was being prepared: start nothing (no tokens).
|
|
839
|
+
if (this._stoppedBeforeStart(msgChannel)) return Promise.resolve('');
|
|
840
|
+
|
|
827
841
|
return new Promise((resolve, reject) => {
|
|
828
842
|
const proc = spawn(spawnBinary, spawnArgs, {
|
|
829
843
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -905,7 +919,7 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
905
919
|
|
|
906
920
|
if (signal) {
|
|
907
921
|
this._log(`opencode killed by signal ${signal} after ${TIMEOUT_MS / 1000}s (output: ${!!stdout})`);
|
|
908
|
-
const cls =
|
|
922
|
+
const cls = this.constructor._classifyFailure({ code, signal, stdout, stderr, stdoutErr });
|
|
909
923
|
// A signal almost always means our own timeout fired; only override to
|
|
910
924
|
// a more specific provider error if stdout/stderr clearly show one.
|
|
911
925
|
const category = (cls.category === 'unknown_error' || cls.category === 'process_crashed')
|
|
@@ -914,14 +928,14 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
914
928
|
}
|
|
915
929
|
|
|
916
930
|
if (code !== 0) {
|
|
917
|
-
const cls =
|
|
931
|
+
const cls = this.constructor._classifyFailure({ code, signal, stdout, stderr, stdoutErr });
|
|
918
932
|
this._log(`opencode exited code ${code} → [${cls.category}]: ${OpenCodeAdapter._redact(cls.diagnostic).slice(0, 300)}`);
|
|
919
933
|
return finish(reject, this._failure(cls.category, cls.diagnostic, cls.detail));
|
|
920
934
|
}
|
|
921
935
|
|
|
922
936
|
// Exit 0.
|
|
923
937
|
if (stdout) this._persistSessionId(msgChannel, stdout);
|
|
924
|
-
const outcome =
|
|
938
|
+
const outcome = this.constructor._outcomeForCleanExit({ stdout, stderr, stdoutErr, responseState });
|
|
925
939
|
if (outcome.text) return finish(resolve, outcome.text);
|
|
926
940
|
const f = outcome.failure;
|
|
927
941
|
this._log(`opencode exit 0 → [${f.category}]: ${OpenCodeAdapter._redact(f.diagnostic).slice(0, 300)}`);
|
|
@@ -951,7 +965,7 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
951
965
|
|
|
952
966
|
if (endedOnTool) {
|
|
953
967
|
if (stdoutErr) {
|
|
954
|
-
const cls =
|
|
968
|
+
const cls = this._classifyFailure({ code: 0, signal: null, stdout, stderr, stdoutErr });
|
|
955
969
|
const vague = ['unknown_error', 'process_crashed', 'empty_response'].includes(cls.category);
|
|
956
970
|
return {
|
|
957
971
|
failure: {
|
|
@@ -977,7 +991,7 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
977
991
|
// provider — it can be a structured error on stdout, an incomplete
|
|
978
992
|
// stream, or a genuinely empty completion. Classify rather than guess.
|
|
979
993
|
if (stdoutErr) {
|
|
980
|
-
const cls =
|
|
994
|
+
const cls = this._classifyFailure({ code: 0, signal: null, stdout, stderr, stdoutErr });
|
|
981
995
|
return { failure: { category: cls.category, diagnostic: `exit 0 with error event: ${cls.diagnostic || ''}`, detail: cls.detail } };
|
|
982
996
|
}
|
|
983
997
|
const emptyCat = OpenCodeAdapter._emptyExitCategory(stdout);
|
|
@@ -1010,7 +1024,7 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
1010
1024
|
if (!probe.executable) {
|
|
1011
1025
|
return { ok: false, category: 'cli_not_executable', diagnostic: 'opencode --version did not run' };
|
|
1012
1026
|
}
|
|
1013
|
-
const vclass =
|
|
1027
|
+
const vclass = this.constructor._classifyVersion(probe.version);
|
|
1014
1028
|
if (vclass === 'unsupported') {
|
|
1015
1029
|
return {
|
|
1016
1030
|
ok: false,
|
|
@@ -1022,7 +1036,7 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
1022
1036
|
// 'unknown' (unparseable) and 'degraded' (newer than tested) still run.
|
|
1023
1037
|
|
|
1024
1038
|
const model = this._resolveModel();
|
|
1025
|
-
if (!model) return { ok: false, category: 'model_missing' };
|
|
1039
|
+
if (!model && this._modelRequired()) return { ok: false, category: 'model_missing' };
|
|
1026
1040
|
|
|
1027
1041
|
const cred = this._credentialState();
|
|
1028
1042
|
if (cred === 'missing') return { ok: false, category: 'credential_missing' };
|
|
@@ -1140,10 +1154,14 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
1140
1154
|
* normal reply. Carries `error_category` in metadata so the UI can route it.
|
|
1141
1155
|
*/
|
|
1142
1156
|
async _sendClassifiedError(channel, category, detail) {
|
|
1143
|
-
|
|
1157
|
+
// A run the user stopped reads as "stopped mid-task"; it is not an error.
|
|
1158
|
+
if (this._isMuted(channel)) return;
|
|
1159
|
+
const messages = this._failureMessages();
|
|
1160
|
+
const base = messages[category] || messages.unknown_error;
|
|
1144
1161
|
const safe = detail ? OpenCodeAdapter._redact(detail).trim() : '';
|
|
1145
1162
|
const body = safe ? `${base}\n\n> ${safe}` : base;
|
|
1146
|
-
const
|
|
1163
|
+
const label = this._cliLabel();
|
|
1164
|
+
const headline = category === 'incomplete_run' ? `${label} stopped mid-task` : `${label} couldn't run`;
|
|
1147
1165
|
const content = `⚠️ **${headline}** — ${body}`;
|
|
1148
1166
|
try {
|
|
1149
1167
|
await this.client.sendMessage(this.workspaceId, channel, this.token, content, {
|
|
@@ -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
|
}
|
|
@@ -369,11 +391,57 @@ class Daemon {
|
|
|
369
391
|
*/
|
|
370
392
|
async _setModelEnv(type, model) {
|
|
371
393
|
const val = String(model || '');
|
|
372
|
-
const nativeVar = { claude: 'ANTHROPIC_MODEL', gemini: 'GEMINI_MODEL' }[type];
|
|
373
394
|
await this._runAgn(['env', type, '--set', `LLM_MODEL=${val}`]);
|
|
395
|
+
const nativeVar = { claude: 'ANTHROPIC_MODEL', gemini: 'GEMINI_MODEL' }[type] || this._nativeModelVar(type);
|
|
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
|
+
|
|
424
|
+
/**
|
|
425
|
+
* The agent's own model setting, for a type whose registry declares one and
|
|
426
|
+
* does not map LLM_MODEL onto anything itself (CodeArts reads CODEARTS_MODEL
|
|
427
|
+
* and nothing else). A type with a resolve_env rule for LLM_MODEL already
|
|
428
|
+
* gets there through env resolution, so it is left alone.
|
|
429
|
+
*/
|
|
430
|
+
_nativeModelVar(type) {
|
|
431
|
+
try {
|
|
432
|
+
const entry = this.registry.getEntry(type);
|
|
433
|
+
if (!entry) return null;
|
|
434
|
+
const rules = (entry.resolve_env && entry.resolve_env.rules) || [];
|
|
435
|
+
if (rules.some((r) => r && r.from === 'LLM_MODEL')) return null;
|
|
436
|
+
const field = (entry.env_config || []).find(
|
|
437
|
+
(f) => f && /_MODEL$/.test(f.name || '') && f.name !== 'LLM_MODEL',
|
|
438
|
+
);
|
|
439
|
+
return field ? field.name : null;
|
|
440
|
+
} catch {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
377
445
|
async _refreshRuntimes() {
|
|
378
446
|
try {
|
|
379
447
|
const r = await this._runAgn(['runtimes', '--json']);
|
|
@@ -456,6 +524,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
456
524
|
if (args.model) await this._setModelEnv(type, args.model);
|
|
457
525
|
if (args.baseUrl) await this._runAgn(['env', type, '--set', `LLM_BASE_URL=${args.baseUrl}`]);
|
|
458
526
|
await this._applyConfigMap(type, args.config);
|
|
527
|
+
this._saveNodeAgentEnv(name, type, args);
|
|
459
528
|
// Attach to this node's workspace BY SLUG: the daemon already knows
|
|
460
529
|
// which workspace this command came from, so there is no token to
|
|
461
530
|
// pass and no /v1/token/resolve round-trip to fail (the outage class
|
|
@@ -498,6 +567,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
498
567
|
await this._runAgn(['remove', name]);
|
|
499
568
|
const rc = await this._runAgn(['create', name, '--type', type, '--install', '--path', newDir]);
|
|
500
569
|
if (rc.code !== 0) throw new Error(rc.stderr || rc.stdout || 'recreate failed');
|
|
570
|
+
this._saveNodeAgentEnv(name, type, args);
|
|
501
571
|
const rc2 = await this._runAgn([
|
|
502
572
|
'connect', name, '--workspace', n.workspace_slug || n.workspace_id,
|
|
503
573
|
]);
|
|
@@ -507,6 +577,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
507
577
|
// through daemon.cmd, and writeCommand() OVERWRITES that file — the
|
|
508
578
|
// start could clobber the unread stop (agent keeps the stale env) or
|
|
509
579
|
// interleave with the poll so the agent stops and never restarts.
|
|
580
|
+
this._saveNodeAgentEnv(name, type, args);
|
|
510
581
|
await this.restartAgent(name);
|
|
511
582
|
}
|
|
512
583
|
ok = true;
|
|
@@ -1701,4 +1772,4 @@ async _runNodeCommand(n, cmd) {
|
|
|
1701
1772
|
}
|
|
1702
1773
|
}
|
|
1703
1774
|
|
|
1704
|
-
module.exports = { Daemon };
|
|
1775
|
+
module.exports = { Daemon, filesystemRoots };
|