@openagents-org/agent-launcher 0.2.138 → 0.2.139

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.138",
3
+ "version": "0.2.139",
4
4
  "description": "OpenAgents Launcher — install, configure, and run AI coding agents from your terminal",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -710,6 +710,7 @@ class ClaudeAdapter extends BaseAdapter {
710
710
  messageResolve: null,
711
711
  msgChannel: channel,
712
712
  lastResponseText: [],
713
+ lastErrorText: '',
713
714
  hasToolUseSinceLastText: false,
714
715
  postedThinking: false,
715
716
  everPostedAnything: false,
@@ -783,7 +784,12 @@ class ClaudeAdapter extends BaseAdapter {
783
784
  this._saveSessions();
784
785
  }
785
786
  if (event.is_error) {
786
- this._log(`Claude error: ${String(event.result || '').slice(0, 200)}`);
787
+ // Capture the error so _handleMessage can surface it to the user.
788
+ // Without this, an auth/API failure (e.g. 401 invalid token) is only
789
+ // logged and the user just sees "No response generated", hiding the
790
+ // real cause and making it nearly impossible to diagnose.
791
+ pp.lastErrorText = String(event.result || '').trim();
792
+ this._log(`Claude error: ${pp.lastErrorText.slice(0, 200)}`);
787
793
  }
788
794
  if (pp.messageResolve) {
789
795
  pp.messageResolve({ resultEvent: event });
@@ -847,6 +853,7 @@ class ClaudeAdapter extends BaseAdapter {
847
853
  */
848
854
  _sendToPersistentProc(pp, content) {
849
855
  pp.lastResponseText = [];
856
+ pp.lastErrorText = '';
850
857
  pp.hasToolUseSinceLastText = false;
851
858
  pp.postedThinking = false;
852
859
  pp.everPostedAnything = false;
@@ -878,6 +885,20 @@ class ClaudeAdapter extends BaseAdapter {
878
885
  });
879
886
  }
880
887
 
888
+ /**
889
+ * Turn a raw Claude `result` error into a user-facing message. Auth failures
890
+ * (401 / invalid token) are the most common real-world cause and are otherwise
891
+ * invisible — add a concrete hint about which env vars to check.
892
+ */
893
+ _formatClaudeError(text) {
894
+ const msg = String(text || '').trim() || 'Claude returned an error.';
895
+ if (/401|403|authenticate|invalid.*(token|key|api)|无效的?\s*(令牌|密钥|key)/i.test(msg)) {
896
+ return `Claude authentication failed: ${msg}\n\n` +
897
+ 'Check this agent\'s API key and Base URL in the launcher — the key may be invalid or expired.';
898
+ }
899
+ return `Claude error: ${msg}`;
900
+ }
901
+
881
902
  async _handleMessage(msg) {
882
903
  let content = (msg.content || '').trim();
883
904
  const attachments = msg.attachments || [];
@@ -933,6 +954,8 @@ class ClaudeAdapter extends BaseAdapter {
933
954
  const fullResponse = existingPP.lastResponseText.join('\n').trim();
934
955
  if (fullResponse) {
935
956
  try { await this.sendResponse(msgChannel, fullResponse); } catch {}
957
+ } else if (existingPP.lastErrorText) {
958
+ try { await this.sendError(msgChannel, this._formatClaudeError(existingPP.lastErrorText)); } catch {}
936
959
  }
937
960
  this._resetIdleTimer(msgChannel);
938
961
  if (!msg._todoNudge) {
@@ -1017,7 +1040,11 @@ class ClaudeAdapter extends BaseAdapter {
1017
1040
  continue;
1018
1041
  }
1019
1042
  if (!pp.everPostedAnything) {
1020
- try { await this.sendResponse(msgChannel, 'No response generated. Please try again.'); } catch {}
1043
+ if (pp.lastErrorText) {
1044
+ try { await this.sendError(msgChannel, this._formatClaudeError(pp.lastErrorText)); } catch {}
1045
+ } else {
1046
+ try { await this.sendResponse(msgChannel, 'No response generated. Please try again.'); } catch {}
1047
+ }
1021
1048
  }
1022
1049
  break;
1023
1050
  }
@@ -1052,6 +1079,10 @@ class ClaudeAdapter extends BaseAdapter {
1052
1079
 
1053
1080
  if (finalResponse) {
1054
1081
  try { await this.sendResponse(msgChannel, finalResponse); } catch {}
1082
+ } else if (pp.lastErrorText) {
1083
+ // Claude finished with an error and no assistant text (e.g. 401 invalid
1084
+ // token). Surface it instead of silently dropping the turn.
1085
+ try { await this.sendError(msgChannel, this._formatClaudeError(pp.lastErrorText)); } catch {}
1055
1086
  }
1056
1087
 
1057
1088
  this._resetIdleTimer(msgChannel);
@@ -22,6 +22,10 @@ const { whichBinary, getEnhancedEnv } = require('../paths');
22
22
 
23
23
  const IS_WINDOWS = process.platform === 'win32';
24
24
 
25
+ // Max wall-clock for a single `opencode run`. When it fires, Node kills the
26
+ // child with SIGTERM, the 'close' handler sees signal='SIGTERM' / code=null.
27
+ const TIMEOUT_MS = 300000; // 5 minutes
28
+
25
29
  class OpenCodeAdapter extends BaseAdapter {
26
30
  /**
27
31
  * @param {object} opts - BaseAdapter opts plus:
@@ -360,6 +364,32 @@ class OpenCodeAdapter extends BaseAdapter {
360
364
  // Subprocess execution
361
365
  // ------------------------------------------------------------------
362
366
 
367
+ /**
368
+ * Build the `--model` value (provider/model) for `opencode run`.
369
+ *
370
+ * CRITICAL: opencode-ai does NOT read the OPENCODE_MODEL env var. When no
371
+ * model is given on the command line AND none is set in opencode.json, the
372
+ * non-interactive `run` command hangs forever waiting for interactive
373
+ * provider/model selection — it emits zero output until the spawn timeout
374
+ * kills it with SIGTERM (surfaced as the misleading "exited with code null").
375
+ * Passing `--model` explicitly is what makes headless runs actually work.
376
+ */
377
+ _resolveModel() {
378
+ const env = this.agentEnv || process.env;
379
+ const model = (env.OPENCODE_MODEL || env.LLM_MODEL || '').trim();
380
+ if (!model) return '';
381
+ // Already provider-qualified (e.g. "openai/gpt-4o", "anthropic/claude-…").
382
+ if (model.includes('/')) return model;
383
+ // Infer the provider from which key/base URL the env resolved to. The
384
+ // registry maps LLM_* → OPENAI_* for OpenAI-compatible endpoints, so
385
+ // "openai" is the right default; only switch for an Anthropic endpoint.
386
+ const baseUrl = (env.OPENAI_BASE_URL || env.LLM_BASE_URL || '').toLowerCase();
387
+ const provider = (env.ANTHROPIC_API_KEY || baseUrl.includes('anthropic'))
388
+ ? 'anthropic'
389
+ : 'openai';
390
+ return `${provider}/${model}`;
391
+ }
392
+
363
393
  _runOpencode(content, msgChannel) {
364
394
  const binary = this._opencodeBinary || this._findOpencodeBinary();
365
395
  if (binary) this._opencodeBinary = binary;
@@ -371,6 +401,19 @@ class OpenCodeAdapter extends BaseAdapter {
371
401
 
372
402
  const cmd = [binary, 'run', '--format', 'json', '--dir', this.agentHome];
373
403
 
404
+ // Pin the model explicitly — without it opencode hangs waiting for
405
+ // interactive selection (see _resolveModel). Harmless when resuming a
406
+ // session (opencode keeps the session's model); essential for new ones.
407
+ const model = this._resolveModel();
408
+ if (model) {
409
+ cmd.push('--model', model);
410
+ } else {
411
+ this._log(
412
+ 'WARNING: no model configured (set LLM_MODEL / OPENCODE_MODEL). ' +
413
+ 'opencode may hang or produce no response.'
414
+ );
415
+ }
416
+
374
417
  const sessionId = this._channelSessions[msgChannel];
375
418
  let fullPrompt;
376
419
  if (sessionId) {
@@ -382,7 +425,7 @@ class OpenCodeAdapter extends BaseAdapter {
382
425
  fullPrompt = `${context}\n\n---\n\n${content}`;
383
426
  }
384
427
 
385
- this._log(`CLI: ${binary} ${cmd.slice(1, 5).join(' ')} ...`);
428
+ this._log(`CLI: ${binary} run --format json --dir … --model ${model || '(none)'}`);
386
429
 
387
430
  const spawnEnv = { ...(this.agentEnv || process.env) };
388
431
 
@@ -400,7 +443,7 @@ class OpenCodeAdapter extends BaseAdapter {
400
443
  stdio: ['pipe', 'pipe', 'pipe'],
401
444
  env: spawnEnv,
402
445
  cwd: this.agentHome,
403
- timeout: 300000, // 5 minutes
446
+ timeout: TIMEOUT_MS,
404
447
  // Never let a console window appear. Besides being ugly, an attached
405
448
  // console makes opencode start its interactive TUI and hang instead of
406
449
  // running headless — the root cause of "stuck on thinking…" on Windows.
@@ -428,11 +471,22 @@ class OpenCodeAdapter extends BaseAdapter {
428
471
 
429
472
  proc.on('error', (err) => finish(reject, err));
430
473
  // 'close' (not 'exit') fires after stdout/stderr have fully drained, so we
431
- // never parse a truncated response.
432
- proc.on('close', (code) => {
474
+ // never parse a truncated response. The handler receives (code, signal):
475
+ // when the spawn timeout kills the child, code is null and signal is the
476
+ // signal name — report that as a timeout rather than "exited with code null".
477
+ proc.on('close', (code, signal) => {
433
478
  stdout = stdout.trim();
434
479
  stderr = stderr.trim();
435
480
 
481
+ if (signal) {
482
+ this._log(`opencode killed by signal ${signal} after ${TIMEOUT_MS / 1000}s (no output: ${!stdout})`);
483
+ return finish(reject, new Error(
484
+ stderr ? stderr.slice(0, 500)
485
+ : `opencode timed out after ${TIMEOUT_MS / 1000}s with no response. ` +
486
+ 'This usually means no model/provider is configured — check LLM_MODEL / LLM_API_KEY.'
487
+ ));
488
+ }
489
+
436
490
  if (code !== 0) {
437
491
  this._log(`opencode exited with code ${code}: ${stderr.slice(0, 300)}`);
438
492
  // Surface the real failure instead of a generic "No response" so the