@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 +1 -1
- package/src/adapters/claude.js +33 -2
- package/src/adapters/opencode.js +58 -4
package/package.json
CHANGED
package/src/adapters/claude.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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);
|
package/src/adapters/opencode.js
CHANGED
|
@@ -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} ${
|
|
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:
|
|
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
|
-
|
|
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
|