@openagents-org/agent-launcher 0.2.142 → 0.2.143
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/claude.js +14 -0
- package/src/adapters/opencode.js +124 -4
- package/src/cli.js +5 -0
- package/src/daemon.js +22 -1
package/package.json
CHANGED
package/src/adapters/claude.js
CHANGED
|
@@ -805,6 +805,20 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
805
805
|
if (pp.messageResolve) {
|
|
806
806
|
pp.messageResolve({ resultEvent: event });
|
|
807
807
|
pp.messageResolve = null;
|
|
808
|
+
} else {
|
|
809
|
+
// Background-triggered turn: the CLI produced output on its own after
|
|
810
|
+
// the user-initiated turn already resolved (e.g. a run_in_background
|
|
811
|
+
// task finished and fed its result back to the model). That text was
|
|
812
|
+
// streamed as `thinking` only — with no messageResolve waiting,
|
|
813
|
+
// _handleMessage never posts it as a `chat`, so the thread visually
|
|
814
|
+
// hangs on "thinking…/Working…". Finalize the accumulated text as a
|
|
815
|
+
// real answer so the turn settles on an actual message bubble.
|
|
816
|
+
const trailing = pp.lastResponseText.join('\n').trim();
|
|
817
|
+
if (trailing && !event.is_error) {
|
|
818
|
+
this._resetIdleTimer(pp.msgChannel);
|
|
819
|
+
try { await this.sendResponse(pp.msgChannel, trailing); } catch {}
|
|
820
|
+
}
|
|
821
|
+
pp.lastResponseText.length = 0;
|
|
808
822
|
}
|
|
809
823
|
} else if (eventType === 'system') {
|
|
810
824
|
const subtype = event.subtype || '';
|
package/src/adapters/opencode.js
CHANGED
|
@@ -45,6 +45,10 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
45
45
|
this._migrateSessionsFile();
|
|
46
46
|
this._loadSessions();
|
|
47
47
|
|
|
48
|
+
// Process tracking for stop control
|
|
49
|
+
this._channelProcesses = {}; // channel → child process
|
|
50
|
+
this._stoppingChannels = new Set();
|
|
51
|
+
|
|
48
52
|
this._opencodeBinary = this._findOpencodeBinary();
|
|
49
53
|
if (this._opencodeBinary) {
|
|
50
54
|
this._log(`Using OpenCode subprocess mode: ${this._opencodeBinary}`);
|
|
@@ -91,6 +95,32 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
91
95
|
} catch {}
|
|
92
96
|
}
|
|
93
97
|
|
|
98
|
+
async _onControlAction(action, payload) {
|
|
99
|
+
if (action === 'stop') {
|
|
100
|
+
const channel = (payload && typeof payload === 'object') ? payload.channel : null;
|
|
101
|
+
if (channel) {
|
|
102
|
+
const proc = this._channelProcesses[channel];
|
|
103
|
+
const hadQueuedWork = !!this._channelQueues[channel]?.length;
|
|
104
|
+
if (proc) {
|
|
105
|
+
this._log(`Stopping process for channel=${channel}`);
|
|
106
|
+
this._stoppingChannels.add(channel);
|
|
107
|
+
await this._stopProcess(proc);
|
|
108
|
+
delete this._channelProcesses[channel];
|
|
109
|
+
}
|
|
110
|
+
delete this._channelQueues[channel];
|
|
111
|
+
if (proc || hadQueuedWork) {
|
|
112
|
+
try {
|
|
113
|
+
await this.sendResponse(channel, 'Execution stopped by user.');
|
|
114
|
+
} catch {}
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
await this._stopAllProcesses('Execution stopped by user.');
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
await super._onControlAction(action, payload);
|
|
122
|
+
}
|
|
123
|
+
|
|
94
124
|
/**
|
|
95
125
|
* Write workspace skill to OpenCode's skill directory for auto-discovery.
|
|
96
126
|
*/
|
|
@@ -223,6 +253,11 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
223
253
|
try {
|
|
224
254
|
const responseText = await this._runOpencode(content, msgChannel);
|
|
225
255
|
|
|
256
|
+
if (this._stoppingChannels.has(msgChannel)) {
|
|
257
|
+
this._stoppingChannels.delete(msgChannel);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
226
261
|
if (responseText) {
|
|
227
262
|
await this.sendResponse(msgChannel, responseText);
|
|
228
263
|
} else {
|
|
@@ -234,6 +269,10 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
234
269
|
);
|
|
235
270
|
}
|
|
236
271
|
} catch (e) {
|
|
272
|
+
if (this._stoppingChannels.has(msgChannel)) {
|
|
273
|
+
this._stoppingChannels.delete(msgChannel);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
237
276
|
this._log(`Error handling message: ${e.message}`);
|
|
238
277
|
await this.sendError(msgChannel, `Error processing message: ${e.message}`);
|
|
239
278
|
}
|
|
@@ -256,12 +295,12 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
256
295
|
// Find matching brace
|
|
257
296
|
let depth = 0;
|
|
258
297
|
let inStr = false;
|
|
259
|
-
let
|
|
298
|
+
let escaped = false;
|
|
260
299
|
let start = pos;
|
|
261
300
|
for (let i = pos; i < raw.length; i++) {
|
|
262
301
|
const ch = raw[i];
|
|
263
|
-
if (
|
|
264
|
-
if (ch === '\\' && inStr) {
|
|
302
|
+
if (escaped) { escaped = false; continue; }
|
|
303
|
+
if (ch === '\\' && inStr) { escaped = true; continue; }
|
|
265
304
|
if (ch === '"') { inStr = !inStr; continue; }
|
|
266
305
|
if (inStr) continue;
|
|
267
306
|
if (ch === '{') depth++;
|
|
@@ -360,6 +399,72 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
360
399
|
}
|
|
361
400
|
}
|
|
362
401
|
|
|
402
|
+
/**
|
|
403
|
+
* Override BaseAdapter.stop so daemon shutdown also tears down in-flight
|
|
404
|
+
* opencode subprocesses cleanly.
|
|
405
|
+
*/
|
|
406
|
+
stop() {
|
|
407
|
+
this._stopAllProcesses(
|
|
408
|
+
'Task interrupted — daemon restarting. Send another message to continue.'
|
|
409
|
+
).catch(() => {});
|
|
410
|
+
super.stop();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async _stopProcess(proc) {
|
|
414
|
+
if (!proc || proc.exitCode !== null) return;
|
|
415
|
+
try {
|
|
416
|
+
if (IS_WINDOWS) {
|
|
417
|
+
try { proc.kill('SIGINT'); } catch {}
|
|
418
|
+
const exited = await new Promise((resolve) => {
|
|
419
|
+
if (proc.exitCode !== null) {
|
|
420
|
+
resolve(true);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const timeout = setTimeout(() => resolve(false), 1500);
|
|
424
|
+
proc.once('exit', () => { clearTimeout(timeout); resolve(true); });
|
|
425
|
+
});
|
|
426
|
+
if (!exited) {
|
|
427
|
+
try { execSync(`taskkill /F /T /PID ${proc.pid}`, { timeout: 5000 }); } catch {}
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
try { process.kill(-proc.pid, 'SIGTERM'); } catch {
|
|
431
|
+
proc.kill('SIGTERM');
|
|
432
|
+
}
|
|
433
|
+
await new Promise((resolve) => {
|
|
434
|
+
let done = false;
|
|
435
|
+
const finish = () => {
|
|
436
|
+
if (done) return;
|
|
437
|
+
done = true;
|
|
438
|
+
resolve();
|
|
439
|
+
};
|
|
440
|
+
const timeout = setTimeout(() => {
|
|
441
|
+
try { process.kill(-proc.pid, 'SIGKILL'); } catch {
|
|
442
|
+
proc.kill('SIGKILL');
|
|
443
|
+
}
|
|
444
|
+
const reapTimeout = setTimeout(finish, 1000);
|
|
445
|
+
proc.once('exit', () => { clearTimeout(reapTimeout); finish(); });
|
|
446
|
+
}, 1500);
|
|
447
|
+
proc.once('exit', () => { clearTimeout(timeout); finish(); });
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
} catch {}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async _stopAllProcesses(completionMessage = 'Execution stopped.') {
|
|
454
|
+
const entries = Object.entries(this._channelProcesses);
|
|
455
|
+
if (!entries.length) return;
|
|
456
|
+
this._log(`Stopping ${entries.length} running process(es)...`);
|
|
457
|
+
for (const [channel, proc] of entries) {
|
|
458
|
+
this._stoppingChannels.add(channel);
|
|
459
|
+
await this._stopProcess(proc);
|
|
460
|
+
delete this._channelProcesses[channel];
|
|
461
|
+
delete this._channelQueues[channel];
|
|
462
|
+
try {
|
|
463
|
+
await this.sendResponse(channel, completionMessage);
|
|
464
|
+
} catch {}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
363
468
|
// ------------------------------------------------------------------
|
|
364
469
|
// Subprocess execution
|
|
365
470
|
// ------------------------------------------------------------------
|
|
@@ -444,16 +549,26 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
444
549
|
env: spawnEnv,
|
|
445
550
|
cwd: this.agentHome,
|
|
446
551
|
timeout: TIMEOUT_MS,
|
|
552
|
+
detached: !IS_WINDOWS,
|
|
447
553
|
// Never let a console window appear. Besides being ugly, an attached
|
|
448
554
|
// console makes opencode start its interactive TUI and hang instead of
|
|
449
555
|
// running headless — the root cause of "stuck on thinking…" on Windows.
|
|
450
556
|
windowsHide: true,
|
|
451
557
|
});
|
|
452
558
|
|
|
559
|
+
this._channelProcesses[msgChannel] = proc;
|
|
560
|
+
|
|
453
561
|
let stdout = '';
|
|
454
562
|
let stderr = '';
|
|
455
563
|
let settled = false;
|
|
456
|
-
const finish = (fn, arg) => {
|
|
564
|
+
const finish = (fn, arg) => {
|
|
565
|
+
if (settled) return;
|
|
566
|
+
settled = true;
|
|
567
|
+
if (this._channelProcesses[msgChannel] === proc) {
|
|
568
|
+
delete this._channelProcesses[msgChannel];
|
|
569
|
+
}
|
|
570
|
+
fn(arg);
|
|
571
|
+
};
|
|
457
572
|
|
|
458
573
|
if (proc.stdout) proc.stdout.on('data', (d) => { stdout += d; });
|
|
459
574
|
if (proc.stderr) proc.stderr.on('data', (d) => { stderr += d; });
|
|
@@ -475,6 +590,11 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
475
590
|
// when the spawn timeout kills the child, code is null and signal is the
|
|
476
591
|
// signal name — report that as a timeout rather than "exited with code null".
|
|
477
592
|
proc.on('close', (code, signal) => {
|
|
593
|
+
if (this._stoppingChannels.has(msgChannel)) {
|
|
594
|
+
this._stoppingChannels.delete(msgChannel);
|
|
595
|
+
return finish(resolve, '');
|
|
596
|
+
}
|
|
597
|
+
|
|
478
598
|
stdout = stdout.trim();
|
|
479
599
|
stderr = stderr.trim();
|
|
480
600
|
|
package/src/cli.js
CHANGED
|
@@ -41,6 +41,9 @@ function parseArgs(argv) {
|
|
|
41
41
|
function getConnector(flags) {
|
|
42
42
|
const opts = {};
|
|
43
43
|
if (flags.config) opts.configDir = flags.config;
|
|
44
|
+
if (flags.endpoint || process.env.OPENAGENTS_ENDPOINT) {
|
|
45
|
+
opts.workspaceEndpoint = flags.endpoint || process.env.OPENAGENTS_ENDPOINT;
|
|
46
|
+
}
|
|
44
47
|
return new AgentConnector(opts);
|
|
45
48
|
}
|
|
46
49
|
|
|
@@ -358,6 +361,8 @@ async function cmdConnect(connector, flags, positional) {
|
|
|
358
361
|
if (pid) {
|
|
359
362
|
connector.sendDaemonCommand(`restart:${name}`);
|
|
360
363
|
print('Daemon notified');
|
|
364
|
+
} else {
|
|
365
|
+
print('Daemon is not running. Run `agn up` to bring this agent online.');
|
|
361
366
|
}
|
|
362
367
|
} catch (e) {
|
|
363
368
|
print(`Error: ${e.message}`);
|
package/src/daemon.js
CHANGED
|
@@ -327,7 +327,17 @@ class Daemon {
|
|
|
327
327
|
* Read daemon PID, returning null if not running.
|
|
328
328
|
*/
|
|
329
329
|
static readDaemonPid(configDir) {
|
|
330
|
-
|
|
330
|
+
const pidFile = path.join(configDir, 'daemon.pid');
|
|
331
|
+
const statusFile = path.join(configDir, 'daemon.status.json');
|
|
332
|
+
const pid = Daemon._readPid(pidFile);
|
|
333
|
+
if (!pid) return null;
|
|
334
|
+
|
|
335
|
+
if (!Daemon._isAlive(pid)) {
|
|
336
|
+
Daemon._cleanupStaleDaemonFiles(pidFile, statusFile);
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return pid;
|
|
331
341
|
}
|
|
332
342
|
|
|
333
343
|
// ---------------------------------------------------------------------------
|
|
@@ -873,6 +883,17 @@ class Daemon {
|
|
|
873
883
|
}
|
|
874
884
|
}
|
|
875
885
|
|
|
886
|
+
static _cleanupStaleDaemonFiles(pidFile, statusFile) {
|
|
887
|
+
Daemon._unlinkIfExists(pidFile);
|
|
888
|
+
Daemon._unlinkIfExists(statusFile);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
static _unlinkIfExists(filePath) {
|
|
892
|
+
if (fs.existsSync(filePath)) {
|
|
893
|
+
fs.unlinkSync(filePath);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
876
897
|
static _isAlive(pid) {
|
|
877
898
|
try {
|
|
878
899
|
process.kill(pid, 0);
|