@openagents-org/agent-launcher 0.2.150 → 0.2.152

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.
@@ -324,12 +324,15 @@ class CodexAdapter extends BaseAdapter {
324
324
  this._saveSessions();
325
325
  continue;
326
326
  } else {
327
- await this.sendResponse(msgChannel, 'No response generated. Please try again.');
327
+ // Surface the actual reason (turn.failed message, stderr, exit code)
328
+ // instead of a generic "no response" — mirrors the OpenCode adapter
329
+ // so auth/model/network problems are actionable from the chat.
330
+ await this._sendRunFailure(msgChannel, result);
328
331
  return;
329
332
  }
330
333
  } catch (e) {
331
334
  this._log(`Error in subprocess: ${e.message}`);
332
- await this.sendError(msgChannel, `Error: ${e.message}`);
335
+ await this.sendError(msgChannel, `⚠️ **Codex couldn't run** — ${CodexAdapter._redact(e.message)}`);
333
336
  return;
334
337
  }
335
338
  }
@@ -351,6 +354,7 @@ class CodexAdapter extends BaseAdapter {
351
354
  let hasToolUseSinceLastText = false;
352
355
  let lineBuffer = '';
353
356
  let stderrBuf = '';
357
+ let lastErrorMessage = '';
354
358
  let _pendingLines = Promise.resolve();
355
359
 
356
360
  if (proc.stderr) {
@@ -407,7 +411,12 @@ class CodexAdapter extends BaseAdapter {
407
411
  } else if (eventType === 'turn.failed') {
408
412
  const error = event.error || {};
409
413
  const errMsg = error.message || JSON.stringify(error);
414
+ lastErrorMessage = errMsg;
410
415
  this._log(`Turn failed: ${errMsg}`);
416
+ } else if (eventType === 'error') {
417
+ const errMsg = event.message || JSON.stringify(event);
418
+ lastErrorMessage = errMsg;
419
+ this._log(`Error event: ${errMsg}`);
411
420
  }
412
421
  };
413
422
 
@@ -442,6 +451,7 @@ class CodexAdapter extends BaseAdapter {
442
451
  responseText: responseTexts.join('\n').trim(),
443
452
  exitCode: code,
444
453
  stderr: stderrBuf,
454
+ errorMessage: lastErrorMessage,
445
455
  });
446
456
  });
447
457
 
@@ -452,6 +462,50 @@ class CodexAdapter extends BaseAdapter {
452
462
  });
453
463
  }
454
464
 
465
+ // ------------------------------------------------------------------
466
+ // Failure reporting
467
+ // ------------------------------------------------------------------
468
+
469
+ /**
470
+ * Post a user-visible failure carrying the actual reason a run produced no
471
+ * reply, instead of a generic "No response generated".
472
+ */
473
+ async _sendRunFailure(msgChannel, result) {
474
+ const detail = CodexAdapter._failureDetail(result);
475
+ const body = detail
476
+ ? `Codex failed to complete this run.\n\n> ${detail}`
477
+ : 'Codex finished without producing a reply. Please try again.';
478
+ await this.sendError(msgChannel, `⚠️ **Codex couldn't run** — ${body}`);
479
+ }
480
+
481
+ /**
482
+ * Pick the most informative failure detail from a run result:
483
+ * turn.failed / error event message → stderr tail → exit code. Redacted.
484
+ */
485
+ static _failureDetail({ errorMessage, stderr, exitCode } = {}) {
486
+ const stderrTail = String(stderr || '').trim().split('\n').slice(-5).join('\n').trim();
487
+ const raw = String(errorMessage || '').trim()
488
+ || stderrTail
489
+ || (exitCode ? `codex exited with code ${exitCode}` : '');
490
+ return CodexAdapter._redact(raw).slice(0, 500).trim();
491
+ }
492
+
493
+ /** Redact secrets (keys, tokens, bearer/authorization, query secrets) from diagnostics. */
494
+ static _redact(s) {
495
+ let out = String(s == null ? '' : s);
496
+ out = out
497
+ .replace(/\bsk-[A-Za-z0-9_-]{6,}/g, 'sk-[REDACTED]')
498
+ .replace(/\b(?:github_pat|gh[pousr])_[A-Za-z0-9_]{10,}/g, '[REDACTED_TOKEN]')
499
+ .replace(/\bxox[baprs]-[A-Za-z0-9-]{8,}/g, '[REDACTED_TOKEN]')
500
+ .replace(/\bAKIA[0-9A-Z]{12,}/g, '[REDACTED_KEY]')
501
+ .replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}/g, '[REDACTED_JWT]')
502
+ .replace(/(authorization|api[_-]?key|x-api-key|token|bearer|secret|password|passwd)(["'\s:=]+)([^\s"',}]+)/gi,
503
+ (m, k, sep) => `${k}${sep}[REDACTED]`)
504
+ .replace(/([?&](?:api[_-]?key|key|token|access_token)=)[^&\s"']+/gi, '$1[REDACTED]')
505
+ .replace(/\b[A-Za-z0-9_-]{40,}\b/g, '[REDACTED]');
506
+ return out;
507
+ }
508
+
455
509
  // ------------------------------------------------------------------
456
510
  // Direct HTTP mode (fallback when CLI not available)
457
511
  // ------------------------------------------------------------------
@@ -467,11 +521,11 @@ class CodexAdapter extends BaseAdapter {
467
521
  }
468
522
  await this.sendResponse(msgChannel, responseText);
469
523
  } else {
470
- await this.sendResponse(msgChannel, 'No response generated. Please try again.');
524
+ await this._sendRunFailure(msgChannel, {});
471
525
  }
472
526
  } catch (e) {
473
527
  this._log(`Error in direct API: ${e.message}`);
474
- await this.sendError(msgChannel, `Error: ${e.message}`);
528
+ await this.sendError(msgChannel, `⚠️ **Codex couldn't run** — ${CodexAdapter._redact(e.message)}`);
475
529
  }
476
530
  }
477
531
 
@@ -16,7 +16,7 @@ const { execSync, spawn } = require('child_process');
16
16
 
17
17
  const BaseAdapter = require('./base');
18
18
  const { formatAttachmentsForPrompt, SESSION_DEFAULT_RE, generateSessionTitle } = require('./utils');
19
- const { buildCursorSkillMd } = require('./workspace-prompt');
19
+ const { buildCursorSkillMd, workspaceSkillName } = require('./workspace-prompt');
20
20
  const { defaultAgentWorkdir, whichBinary, whereBinary } = require('../paths');
21
21
 
22
22
  const IS_WINDOWS = process.platform === 'win32';
@@ -308,7 +308,12 @@ class CursorAdapter extends BaseAdapter {
308
308
  const workDir = this.workingDir || defaultAgentWorkdir(this.agentName);
309
309
  const skillDir = path.join(workDir, '.cursor', 'skills');
310
310
  fs.mkdirSync(skillDir, { recursive: true });
311
- const skillFile = path.join(skillDir, 'openagents-workspace.md');
311
+ // Per-agent filename: agents sharing a working directory used to clobber
312
+ // one shared openagents-workspace.md, so uploads carried the identity of
313
+ // whichever agent wrote the file last. Drop that legacy file so a stale
314
+ // copy (with another agent's embedded identity) can never be read again.
315
+ const skillFile = path.join(skillDir, `${workspaceSkillName(this.agentName)}.md`);
316
+ try { fs.unlinkSync(path.join(skillDir, 'openagents-workspace.md')); } catch {}
312
317
 
313
318
  const skillContent = buildCursorSkillMd({
314
319
  endpoint: this.endpoint,
@@ -330,7 +335,20 @@ class CursorAdapter extends BaseAdapter {
330
335
  throw new Error('Cursor CLI not found. Install with: curl https://cursor.com/install -fsSL | bash');
331
336
  }
332
337
 
333
- const cmd = [agentBin, '-p', prompt, '--output-format', 'stream-json', '--trust', '--force'];
338
+ // Cursor has no system-prompt flag, so the CLI would otherwise learn its
339
+ // identity ONLY from whichever workspace skill it happens to open. With a
340
+ // shared working directory several agents' skills coexist (same
341
+ // description, different embedded identity), so every spawn must pin who
342
+ // this agent is and exactly which skill file is its interface — otherwise
343
+ // it can read a sibling's skill and upload under that agent's name.
344
+ const identityHeader =
345
+ `[workspace] You are agent '${this.agentName}'. For ANY workspace ` +
346
+ `operation (messages, files, agents, browser) read and follow ONLY the ` +
347
+ `'${workspaceSkillName(this.agentName)}' skill. Other ` +
348
+ `openagents-workspace-* skills belong to OTHER agents and carry the ` +
349
+ `wrong identity — never use them.\n\n`;
350
+
351
+ const cmd = [agentBin, '-p', identityHeader + prompt, '--output-format', 'stream-json', '--trust', '--force'];
334
352
 
335
353
  // Model selection
336
354
  const model = (this.agentEnv || process.env).CURSOR_MODEL;
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Pure helpers for the Claude adapter's decision log ("constraint pinning").
3
+ *
4
+ * The decision log is a per-channel knowledge entry that records every
5
+ * decision the user has confirmed. The adapter re-reads it on every message
6
+ * and injects it into the CLI system prompt — the only position that survives
7
+ * both the CLI's auto-compaction and a session reset. Everything here is
8
+ * side-effect free so it can be unit tested without an adapter instance.
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const crypto = require('crypto');
14
+
15
+ /** Max characters a single recap line may occupy before being cut. */
16
+ const RECAP_LINE_MAX_CHARS = 2000;
17
+
18
+ /** Default budget for the pinned-decisions block inside the system prompt. */
19
+ const PINNED_DECISIONS_MAX_CHARS = 4000;
20
+
21
+ /**
22
+ * Canonical knowledge-entry title for a channel's decision log. The slug the
23
+ * backend derives from it is NOT stable (collisions get a numeric suffix), so
24
+ * lookups must match on this exact title, never on the slug.
25
+ */
26
+ function decisionLogTitle(channelName) {
27
+ return `Decisions for channel ${channelName}`;
28
+ }
29
+
30
+ /**
31
+ * Stable hash of the decision log content. null/undefined and '' hash
32
+ * identically so "no entry yet" never reads as a change.
33
+ */
34
+ function hashDecisions(content) {
35
+ return crypto.createHash('sha256').update(String(content || '').trim(), 'utf-8').digest('hex');
36
+ }
37
+
38
+ /**
39
+ * Fingerprint of everything the system prompt pins from the decision log —
40
+ * the entry id AND the content — used to detect that a persistent CLI
41
+ * process was spawned with an outdated prompt. Content alone is not enough:
42
+ * a deleted-and-recreated log can carry identical content under a new id,
43
+ * and a prompt still holding the old id would send updates to a dead entry.
44
+ */
45
+ function decisionFingerprint(entryId, content) {
46
+ return crypto.createHash('sha256')
47
+ .update(`${entryId || ''}\u0000${String(content || '').trim()}`, 'utf-8')
48
+ .digest('hex');
49
+ }
50
+
51
+ /**
52
+ * Pick the channel's decision entry from a knowledge listing: exact-title
53
+ * matches only, earliest created_at wins (concurrent creates produce
54
+ * duplicates — the earliest is the one other turns have been updating).
55
+ * Returns { entry, duplicates } where duplicates counts the extra matches.
56
+ */
57
+ function pickDecisionEntry(entries, channelName) {
58
+ const title = decisionLogTitle(channelName);
59
+ const matches = (entries || []).filter((e) => e && e.title === title);
60
+ if (matches.length === 0) return { entry: null, duplicates: 0 };
61
+ const sorted = matches.slice().sort((a, b) => {
62
+ // ISO-8601 strings compare chronologically as strings; entries without a
63
+ // timestamp are anomalous and sort last so a dated original wins.
64
+ const ka = a.created_at || '\uffff';
65
+ const kb = b.created_at || '\uffff';
66
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
67
+ });
68
+ return { entry: sorted[0], duplicates: matches.length - 1 };
69
+ }
70
+
71
+ /**
72
+ * Fit the decision log into a character budget without ever cutting a line
73
+ * in half. Over budget, whole lines are kept from the head (earliest
74
+ * decisions) and the tail (latest decisions) with the middle dropped, so
75
+ * neither end of the log silently disappears.
76
+ * Returns { text, truncated, omitted }.
77
+ */
78
+ function renderPinnedDecisions(content, { maxChars = PINNED_DECISIONS_MAX_CHARS } = {}) {
79
+ const text = String(content || '').trim();
80
+ if (!text) return { text: '', truncated: false, omitted: 0 };
81
+ if (text.length <= maxChars) return { text, truncated: false, omitted: 0 };
82
+
83
+ const lines = text.split('\n');
84
+ // Reserve room for the omission marker inserted between head and tail.
85
+ const budget = Math.max(maxChars - 80, 0);
86
+ const headBudget = Math.floor(budget / 2);
87
+
88
+ const head = [];
89
+ let used = 0;
90
+ let i = 0;
91
+ for (; i < lines.length; i++) {
92
+ const cost = lines[i].length + 1;
93
+ if (used + cost > headBudget) break;
94
+ head.push(lines[i]);
95
+ used += cost;
96
+ }
97
+
98
+ const tail = [];
99
+ let j = lines.length - 1;
100
+ for (; j >= i; j--) {
101
+ const cost = lines[j].length + 1;
102
+ if (used + cost > budget) break;
103
+ tail.unshift(lines[j]);
104
+ used += cost;
105
+ }
106
+
107
+ const omitted = lines.length - head.length - tail.length;
108
+ if (omitted <= 0) {
109
+ // Everything fit after all (short middle) — no marker needed.
110
+ return { text: [...head, ...tail].join('\n'), truncated: false, omitted: 0 };
111
+ }
112
+ const marker = `[… ${omitted} middle line(s) omitted — read the decision log entry for the full list …]`;
113
+ return {
114
+ text: [...head, marker, ...tail].join('\n'),
115
+ truncated: true,
116
+ omitted,
117
+ };
118
+ }
119
+
120
+ /** A message qualifies for the recap when it is real chat with content. */
121
+ function isRecapEligible(msg, currentMessage) {
122
+ if (!msg) return false;
123
+ const mt = msg.messageType || 'chat';
124
+ if (mt === 'status' || mt === 'thinking' || mt === 'loading') return false;
125
+ const text = (msg.content || '').trim();
126
+ if (!text) return false;
127
+ if (text === currentMessage) return false;
128
+ return true;
129
+ }
130
+
131
+ function formatRecapLine(msg) {
132
+ const text = (msg.content || '').trim();
133
+ const who = msg.senderType === 'human'
134
+ ? (msg.senderName || 'user')
135
+ : (msg.senderName || 'agent');
136
+ const cut = text.length > RECAP_LINE_MAX_CHARS
137
+ ? text.slice(0, RECAP_LINE_MAX_CHARS) + '…'
138
+ : text;
139
+ return `[${who}] ${cut}`;
140
+ }
141
+
142
+ /**
143
+ * Build recap lines from the channel's opening messages (fetched ascending)
144
+ * and its most recent ones (fetched descending, already reversed to
145
+ * chronological). Head keeps the original requirement, tail keeps the live
146
+ * discussion; overlap is removed by event ID. An omission marker is inserted
147
+ * only when the two windows do not overlap, i.e. messages in between were
148
+ * actually dropped.
149
+ */
150
+ function sampleRecap(headMsgs, tailMsgs, currentMessage, { headKeep = 5, tailKeep = 15 } = {}) {
151
+ const head = (headMsgs || [])
152
+ .filter((m) => isRecapEligible(m, currentMessage))
153
+ .slice(0, headKeep);
154
+ const tail = (tailMsgs || [])
155
+ .filter((m) => isRecapEligible(m, currentMessage))
156
+ .slice(-tailKeep);
157
+
158
+ const headIds = new Set(head.map((m) => m.messageId).filter(Boolean));
159
+ const tailDeduped = tail.filter((m) => !m.messageId || !headIds.has(m.messageId));
160
+ const windowsOverlap = tailDeduped.length < tail.length;
161
+
162
+ const lines = head.map(formatRecapLine);
163
+ if (head.length > 0 && tailDeduped.length > 0 && !windowsOverlap) {
164
+ lines.push('[… earlier messages omitted …]');
165
+ }
166
+ lines.push(...tailDeduped.map(formatRecapLine));
167
+ return lines;
168
+ }
169
+
170
+ module.exports = {
171
+ PINNED_DECISIONS_MAX_CHARS,
172
+ RECAP_LINE_MAX_CHARS,
173
+ decisionLogTitle,
174
+ hashDecisions,
175
+ decisionFingerprint,
176
+ pickDecisionEntry,
177
+ renderPinnedDecisions,
178
+ isRecapEligible,
179
+ formatRecapLine,
180
+ sampleRecap,
181
+ };
@@ -19,6 +19,7 @@ const GooseAdapter = require('./goose');
19
19
  const CopilotAdapter = require('./copilot');
20
20
  const ClineAdapter = require('./cline');
21
21
  const AmpAdapter = require('./amp');
22
+ const MiniSweAgentAdapter = require('./mini');
22
23
 
23
24
  const ADAPTER_MAP = {
24
25
  openclaw: OpenClawAdapter,
@@ -35,11 +36,12 @@ const ADAPTER_MAP = {
35
36
  copilot: CopilotAdapter,
36
37
  cline: ClineAdapter,
37
38
  amp: AmpAdapter,
39
+ 'mini-swe-agent': MiniSweAgentAdapter,
38
40
  };
39
41
 
40
42
  /**
41
43
  * Create an adapter instance for the given agent type.
42
- * @param {string} type - Agent type (openclaw, claude, codex, opencode, nanoclaw, cursor, hermes, gemini, kimi, aider, goose, copilot, cline, amp)
44
+ * @param {string} type - Agent type (openclaw, claude, codex, opencode, nanoclaw, cursor, hermes, gemini, kimi, aider, goose, copilot, cline, amp, mini-swe-agent)
43
45
  * @param {object} opts - Adapter constructor options
44
46
  * @returns {BaseAdapter}
45
47
  */
@@ -67,6 +69,7 @@ module.exports = {
67
69
  CopilotAdapter,
68
70
  ClineAdapter,
69
71
  AmpAdapter,
72
+ MiniSweAgentAdapter,
70
73
  createAdapter,
71
74
  ADAPTER_MAP,
72
75
  };