@openagents-org/agent-launcher 0.2.143 → 0.2.145

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.
@@ -19,7 +19,7 @@ const { execSync, spawn } = require('child_process');
19
19
  const BaseAdapter = require('./base');
20
20
  const { formatAttachmentsForPrompt, SESSION_DEFAULT_RE, generateSessionTitle } = require('./utils');
21
21
  const { buildClaudeSystemPrompt, buildClaudeSkillMd } = require('./workspace-prompt');
22
- const { defaultAgentWorkdir, whichBinary, getEnhancedEnv } = require('../paths');
22
+ const { defaultAgentWorkdir, whichBinary, whereBinary } = require('../paths');
23
23
 
24
24
  const IS_WINDOWS = process.platform === 'win32';
25
25
 
@@ -37,6 +37,12 @@ class ClaudeAdapter extends BaseAdapter {
37
37
  this._channelSessions = {}; // channel → Claude CLI session_id
38
38
  this._channelProcesses = {}; // channel → child process
39
39
  this._stoppingChannels = new Set();
40
+ // Channels that have already announced "Execution stopped by user." for the
41
+ // current stop. Two paths race to post it (the control-action handler that
42
+ // kills the process, and the in-flight message handler that sees
43
+ // pp.userStopped after exit), so this dedups to a single notice. Reset when
44
+ // a new message starts processing in the channel.
45
+ this._stopNoticeSent = new Set();
40
46
  this._persistentProcs = {}; // channel → { proc, lineBuffer, pendingLines, idleTimer, messageResolve }
41
47
  this._IDLE_TIMEOUT_MS = 60 * 60 * 1000; // 1 hour
42
48
  this._WATCHDOG_INTERVAL_MS = 15_000; // 15s between checks
@@ -84,9 +90,7 @@ class ClaudeAdapter extends BaseAdapter {
84
90
  await this._stopProcess(proc);
85
91
  delete this._channelProcesses[channel];
86
92
  delete this._channelQueues[channel];
87
- try {
88
- await this.sendResponse(channel, 'Execution stopped by user.');
89
- } catch {}
93
+ await this._postStopNotice(channel);
90
94
  } else {
91
95
  for (const pp of Object.values(this._persistentProcs)) pp.userStopped = true;
92
96
  await this._stopAllProcesses('Execution stopped by user.');
@@ -258,6 +262,18 @@ class ClaudeAdapter extends BaseAdapter {
258
262
  );
259
263
  }
260
264
 
265
+ /**
266
+ * Post "Execution stopped by user." at most once per channel for a given
267
+ * stop. The control-action handler and the in-flight message handler both
268
+ * race to announce a stop; without this guard the user sees it twice. The
269
+ * guard is reset when a new message starts processing in the channel.
270
+ */
271
+ async _postStopNotice(channel) {
272
+ if (!channel || this._stopNoticeSent.has(channel)) return;
273
+ this._stopNoticeSent.add(channel);
274
+ try { await this.sendResponse(channel, 'Execution stopped by user.'); } catch {}
275
+ }
276
+
261
277
  async _stopAllProcesses(completionMessage = 'Execution stopped.') {
262
278
  for (const channel of Object.keys(this._persistentProcs)) {
263
279
  this._killPersistentProc(channel);
@@ -288,7 +304,9 @@ class ClaudeAdapter extends BaseAdapter {
288
304
  for (const c of candidates) {
289
305
  if (fs.existsSync(c)) return c;
290
306
  }
291
- return 'node';
307
+ // Fall back to the node already running this daemon (absolute, always valid).
308
+ // Bare 'node' can be off-PATH in a packaged daemon or CI runner.
309
+ return process.execPath;
292
310
  }
293
311
 
294
312
  /**
@@ -343,24 +361,13 @@ class ClaudeAdapter extends BaseAdapter {
343
361
  const portableCandidate = path.join(portableBin, `claude${ext}`);
344
362
  if (fs.existsSync(portableCandidate)) return portableCandidate;
345
363
 
346
- // Tier 1: PATH search. Use the ENRICHED env so the lookup sees the same
347
- // node-version-manager / homebrew / npm-global dirs the launcher adds —
348
- // a packaged Electron daemon's own PATH is minimal, which is why `which
349
- // claude` came up empty and the agent reported "claude CLI not found".
350
- // windowsHide stops a console window from flashing.
351
- try {
352
- const env = getEnhancedEnv();
353
- if (IS_WINDOWS) {
354
- const r = execSync('where claude.cmd 2>nul || where claude.exe 2>nul || where claude 2>nul', {
355
- encoding: 'utf-8', timeout: 5000, windowsHide: true, env,
356
- });
357
- const hit = r.split(/\r?\n/)[0].trim();
358
- if (hit) return hit;
359
- } else {
360
- const hit = execSync('which claude', { encoding: 'utf-8', timeout: 5000, windowsHide: true, env }).trim();
361
- if (hit) return hit;
362
- }
363
- } catch {}
364
+ // Tier 1: PATH search via a codepage-safe lookup (whereBinary forces UTF-8
365
+ // output + verifies existence, so a non-ASCII/Chinese username isn't mangled
366
+ // into an ENOENT). Uses the ENRICHED env so a packaged daemon's minimal PATH
367
+ // still sees the node-version-manager / homebrew / npm-global dirs the
368
+ // launcher adds — that's why a bare `which claude` came up empty before.
369
+ const viaWhere = whereBinary('claude');
370
+ if (viaWhere) return viaWhere;
364
371
 
365
372
  // Tier 2: Next to current Node.js interpreter (npm global)
366
373
  const nodeBinDir = path.dirname(process.execPath);
@@ -558,15 +565,9 @@ class ClaudeAdapter extends BaseAdapter {
558
565
  const oaPortable = path.join(home3, '.openagents', 'nodejs', 'node_modules', '.bin', `openagents${oaExt}`);
559
566
  if (fs.existsSync(oaPortable)) oaBin = oaPortable;
560
567
  }
561
- if (!oaBin) try {
562
- if (IS_WINDOWS) {
563
- oaBin = execSync('where openagents.cmd 2>nul || where openagents.exe 2>nul || where openagents 2>nul', {
564
- encoding: 'utf-8', timeout: 5000,
565
- }).split(/\r?\n/)[0].trim();
566
- } else {
567
- oaBin = execSync('which openagents', { encoding: 'utf-8', timeout: 5000 }).trim();
568
- }
569
- } catch {}
568
+ // Codepage-safe lookup so a non-ASCII/Chinese username in the path isn't
569
+ // mangled into an ENOENT (see whereBinary).
570
+ if (!oaBin) oaBin = whereBinary('openagents');
570
571
  if (!oaBin) {
571
572
  this._log('Could not find openagents binary — MCP tools may not be available');
572
573
  mcpCommand = 'openagents';
@@ -937,6 +938,7 @@ class ClaudeAdapter extends BaseAdapter {
937
938
 
938
939
  const msgChannel = msg.sessionId || this.channelName;
939
940
  this._stoppingChannels.delete(msgChannel);
941
+ this._stopNoticeSent.delete(msgChannel);
940
942
  const sender = msg.senderName || msg.senderType || 'user';
941
943
  this._log(`Processing message from ${sender} in ${msgChannel}: ${content.slice(0, 80)}...`);
942
944
 
@@ -1001,7 +1003,7 @@ class ClaudeAdapter extends BaseAdapter {
1001
1003
  }
1002
1004
  if (existingPP.userStopped) {
1003
1005
  if (!existingPP.everPostedAnything) {
1004
- try { await this.sendResponse(msgChannel, 'Execution stopped by user.'); } catch {}
1006
+ await this._postStopNotice(msgChannel);
1005
1007
  }
1006
1008
  return;
1007
1009
  }
@@ -1101,7 +1103,7 @@ class ClaudeAdapter extends BaseAdapter {
1101
1103
  this._log(`Process exited during first message (attempt ${attempt + 1}), userStopped=${pp.userStopped}`);
1102
1104
  if (pp.userStopped) {
1103
1105
  if (!pp.everPostedAnything) {
1104
- try { await this.sendResponse(msgChannel, 'Execution stopped by user.'); } catch {}
1106
+ await this._postStopNotice(msgChannel);
1105
1107
  }
1106
1108
  break;
1107
1109
  }