@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openagents-org/agent-launcher",
3
- "version": "0.2.142",
3
+ "version": "0.2.143",
4
4
  "description": "OpenAgents Launcher — install, configure, and run AI coding agents from your terminal",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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 || '';
@@ -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 escape = false;
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 (escape) { escape = false; continue; }
264
- if (ch === '\\' && inStr) { escape = true; continue; }
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) => { if (!settled) { settled = true; 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
- return Daemon._readPid(path.join(configDir, 'daemon.pid'));
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);