@openagents-org/agent-launcher 0.2.180 → 0.2.182

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.
@@ -35,7 +35,11 @@
35
35
  const fs = require('fs');
36
36
  const os = require('os');
37
37
  const path = require('path');
38
- const { execSync, spawn } = require('child_process');
38
+ const { execSync } = require('child_process');
39
+ // spawn() here is the WSL bridge from ../wsl: same signature as
40
+ // child_process.spawn, and a straight pass-through unless the resolved CLI
41
+ // lives on the other side of the Windows/WSL boundary.
42
+ const { spawn, resolveWslBinary } = require('../wsl');
39
43
 
40
44
  const BaseAdapter = require('./base');
41
45
  const { whichBinary, getEnhancedEnv, aiderBinDirs } = require('../paths');
@@ -262,7 +266,7 @@ class AiderAdapter extends BaseAdapter {
262
266
  // PATH (nvm/fnm/volta/homebrew + the Aider install dirs added to paths.js)
263
267
  // — covers the GUI/daemon "not on PATH" case for uv-tool / pipx / pip-user
264
268
  // installs.
265
- const resolved = whichBinary('aider');
269
+ const resolved = whichBinary('aider', { allowWsl: false });
266
270
  if (resolved) return resolved;
267
271
 
268
272
  // Explicit fallback over every real install dir (XDG bin, XDG_DATA_HOME/../
@@ -274,7 +278,10 @@ class AiderAdapter extends BaseAdapter {
274
278
  if (fs.existsSync(c)) return c;
275
279
  }
276
280
  }
277
- return null;
281
+ // Nothing native anywhere. Last of all, look inside WSL: a CLI the user
282
+ // installed in their distro is a real install, and the marked path it comes
283
+ // back as is what the spawn bridge in ../wsl turns into a wsl.exe run.
284
+ return resolveWslBinary('aider');
278
285
  }
279
286
 
280
287
  // ------------------------------------------------------------------
@@ -25,7 +25,11 @@
25
25
  const fs = require('fs');
26
26
  const os = require('os');
27
27
  const path = require('path');
28
- const { execSync, spawn } = require('child_process');
28
+ const { execSync } = require('child_process');
29
+ // spawn() here is the WSL bridge from ../wsl: same signature as
30
+ // child_process.spawn, and a straight pass-through unless the resolved CLI
31
+ // lives on the other side of the Windows/WSL boundary.
32
+ const { spawn, resolveWslBinary } = require('../wsl');
29
33
 
30
34
  const BaseAdapter = require('./base');
31
35
  const { buildOpenclawSystemPrompt } = require('./workspace-prompt');
@@ -123,7 +127,7 @@ class AmpAdapter extends BaseAdapter {
123
127
  // the Amp installer's ~/.amp/bin. This is what makes detection work inside
124
128
  // a GUI- or daemon-spawned process that did NOT inherit the user's
125
129
  // interactive shell PATH (the common "installed but not found" case).
126
- const resolved = whichBinary('amp');
130
+ const resolved = whichBinary('amp', { allowWsl: false });
127
131
  if (resolved) return resolved;
128
132
 
129
133
  // Tier 2: explicit known install dirs as a last resort. The official
@@ -145,7 +149,10 @@ class AmpAdapter extends BaseAdapter {
145
149
  if (c && fs.existsSync(c)) return c;
146
150
  }
147
151
 
148
- return null;
152
+ // Nothing native anywhere. Last of all, look inside WSL: a CLI the user
153
+ // installed in their distro is a real install, and the marked path it comes
154
+ // back as is what the spawn bridge in ../wsl turns into a wsl.exe run.
155
+ return resolveWslBinary('amp');
149
156
  }
150
157
 
151
158
  /**
@@ -181,6 +188,7 @@ class AmpAdapter extends BaseAdapter {
181
188
  endpoint: this.endpoint,
182
189
  token: this.token,
183
190
  mode: this._mode,
191
+ model: this.modelLabel(),
184
192
  disabledModules: this.disabledModules,
185
193
  browserEnabled,
186
194
  ...this.pinnedPromptOpts(channelName),
@@ -17,7 +17,11 @@
17
17
  const fs = require('fs');
18
18
  const os = require('os');
19
19
  const path = require('path');
20
- const { execSync, spawn } = require('child_process');
20
+ const { execSync } = require('child_process');
21
+ // spawn() here is the WSL bridge from ../wsl: same signature as
22
+ // child_process.spawn, and a straight pass-through unless the resolved CLI
23
+ // lives on the other side of the Windows/WSL boundary.
24
+ const { spawn, resolveWslBinary } = require('../wsl');
21
25
 
22
26
  const BaseAdapter = require('./base');
23
27
  const { whereBinary } = require('../paths');
@@ -131,7 +135,10 @@ class AntigravityAdapter extends BaseAdapter {
131
135
  for (const c of candidates) {
132
136
  if (fs.existsSync(c)) return c;
133
137
  }
134
- return null;
138
+ // Nothing native anywhere. Last of all, look inside WSL: a CLI the user
139
+ // installed in their distro is a real install, and the marked path it comes
140
+ // back as is what the spawn bridge in ../wsl turns into a wsl.exe run.
141
+ return resolveWslBinary('agy');
135
142
  }
136
143
 
137
144
  /**
@@ -178,6 +185,7 @@ class AntigravityAdapter extends BaseAdapter {
178
185
  workspaceId: this.workspaceId,
179
186
  channelName,
180
187
  mode: this._mode,
188
+ model: this.modelLabel(),
181
189
  decisionLog: pinned.decisionLog ? { ...pinned.decisionLog, writeAccess: false } : null,
182
190
  glossary: pinned.glossary ? { ...pinned.glossary, writeAccess: false } : null,
183
191
  });
@@ -60,6 +60,18 @@ const CURSOR_RESUME_MAX_AGE_MS = 24 * 60 * 60 * 1000;
60
60
  // heartbeat_failed up to the daemon. A success resets the streak immediately.
61
61
  const HEARTBEAT_ERROR_THRESHOLD = 2;
62
62
 
63
+ /**
64
+ * Model variables whose name does not follow `<TYPE>_MODEL`. Only the ones an
65
+ * adapter really reads — see `modelLabel`, which tries `<TYPE>_MODEL` first
66
+ * and `LLM_MODEL` last, so nothing else needs an entry here.
67
+ */
68
+ const MODEL_ENV_ALIASES = {
69
+ claude: ['ANTHROPIC_MODEL', 'CLAUDE_MODEL'],
70
+ gemini: ['GEMINI_MODEL', 'GOOGLE_GEMINI_MODEL'],
71
+ antigravity: ['ANTIGRAVITY_MODEL', 'AGY_MODEL'],
72
+ codex: ['CODEX_MODEL', 'OPENCLAW_MODEL'],
73
+ };
74
+
63
75
  class BaseAdapter {
64
76
  /**
65
77
  * @param {object} opts
@@ -220,6 +232,40 @@ class BaseAdapter {
220
232
  }
221
233
  }
222
234
 
235
+ /**
236
+ * The model this agent is actually running on, for the workspace prompt.
237
+ *
238
+ * The one fact about itself an agent cannot look up. Asked "what model are
239
+ * you?", a CLI-driven agent answers from what its weights remember about
240
+ * their own training, which is a guess — and a user who had configured
241
+ * deepseek was told, confidently, "I am Claude, by Anthropic". We know the
242
+ * real answer: it is the id we hand the CLI.
243
+ *
244
+ * Resolution order is the same one the adapters spawn with: a model chosen
245
+ * in the workspace wins, then this agent type's own model variable, then the
246
+ * generic LLM_MODEL that the LLM-direct types and the daemon's model.set
247
+ * both write. Adapters that qualify the id further (OpenWorker prefixes it
248
+ * with the provider) override this and return exactly what they send.
249
+ *
250
+ * Null when nothing is configured — the CLI is then running its own default
251
+ * and we have nothing truthful to say about it. Better silent than wrong.
252
+ */
253
+ modelLabel() {
254
+ const fromWorkspace = String(this.workspaceModel || '').trim();
255
+ if (fromWorkspace) return fromWorkspace;
256
+ const type = String(this.agentType || '').toUpperCase().replace(/[^A-Z0-9]+/g, '_');
257
+ const candidates = [
258
+ ...(type ? [`${type}_MODEL`] : []),
259
+ ...(MODEL_ENV_ALIASES[this.agentType] || []),
260
+ 'LLM_MODEL',
261
+ ];
262
+ for (const key of candidates) {
263
+ const value = String((this.agentEnv && this.agentEnv[key]) || '').trim();
264
+ if (value) return value;
265
+ }
266
+ return null;
267
+ }
268
+
223
269
  async run() {
224
270
  this._running = true;
225
271
 
@@ -14,7 +14,11 @@
14
14
  const fs = require('fs');
15
15
  const os = require('os');
16
16
  const path = require('path');
17
- const { execSync, spawn } = require('child_process');
17
+ const { execSync } = require('child_process');
18
+ // spawn() here is the WSL bridge from ../wsl: same signature as
19
+ // child_process.spawn, and a straight pass-through unless the resolved CLI
20
+ // lives on the other side of the Windows/WSL boundary.
21
+ const { spawn } = require('../wsl');
18
22
 
19
23
  const BaseAdapter = require('./base');
20
24
  const { formatAttachmentsForPrompt, SESSION_DEFAULT_RE, generateSessionTitle, redactSecrets } = require('./utils');
@@ -462,6 +466,7 @@ class ClaudeAdapter extends BaseAdapter {
462
466
  workspaceId: this.workspaceId,
463
467
  channelName,
464
468
  mode: this._mode,
469
+ model: this.modelLabel(),
465
470
  browserEnabled,
466
471
  toolMode: this.toolMode,
467
472
  decisionLog,
@@ -26,7 +26,11 @@
26
26
  const fs = require('fs');
27
27
  const os = require('os');
28
28
  const path = require('path');
29
- const { execSync, execFile, spawn } = require('child_process');
29
+ const { execSync, execFile } = require('child_process');
30
+ // spawn() here is the WSL bridge from ../wsl: same signature as
31
+ // child_process.spawn, and a straight pass-through unless the resolved CLI
32
+ // lives on the other side of the Windows/WSL boundary.
33
+ const { spawn } = require('../wsl');
30
34
 
31
35
  const BaseAdapter = require('./base');
32
36
  const { formatAttachmentsForPrompt, SESSION_DEFAULT_RE, generateSessionTitle } = require('./utils');
@@ -49,7 +49,11 @@
49
49
  const fs = require('fs');
50
50
  const os = require('os');
51
51
  const path = require('path');
52
- const { execFileSync, spawn } = require('child_process');
52
+ const { execFileSync } = require('child_process');
53
+ // spawn() here is the WSL bridge from ../wsl: same signature as
54
+ // child_process.spawn, and a straight pass-through unless the resolved CLI
55
+ // lives on the other side of the Windows/WSL boundary.
56
+ const { spawn } = require('../wsl');
53
57
 
54
58
  const BaseAdapter = require('./base');
55
59
  const { formatAttachmentsForPrompt, SESSION_DEFAULT_RE, generateSessionTitle } = require('./utils');
@@ -824,6 +828,7 @@ class CodeBuddyAdapter extends BaseAdapter {
824
828
  workspaceId: this.workspaceId,
825
829
  channelName: channel,
826
830
  mode: this._mode,
831
+ model: this.modelLabel(),
827
832
  browserEnabled: await this.getBrowserEnabled(),
828
833
  toolMode: 'mcp',
829
834
  });
@@ -15,7 +15,11 @@
15
15
  const fs = require('fs');
16
16
  const os = require('os');
17
17
  const path = require('path');
18
- const { execSync, spawn } = require('child_process');
18
+ const { execSync } = require('child_process');
19
+ // spawn() here is the WSL bridge from ../wsl: same signature as
20
+ // child_process.spawn, and a straight pass-through unless the resolved CLI
21
+ // lives on the other side of the Windows/WSL boundary.
22
+ const { spawn, resolveWslBinary } = require('../wsl');
19
23
  const http = require('http');
20
24
  const https = require('https');
21
25
 
@@ -165,7 +169,10 @@ class CodexAdapter extends BaseAdapter {
165
169
  if (fs.existsSync(c)) return c;
166
170
  }
167
171
 
168
- return null;
172
+ // Nothing native anywhere. Last of all, look inside WSL: a CLI the user
173
+ // installed in their distro is a real install, and the marked path it comes
174
+ // back as is what the spawn bridge in ../wsl turns into a wsl.exe run.
175
+ return resolveWslBinary('codex');
169
176
  }
170
177
 
171
178
  _buildSystemContext(channelName) {
@@ -176,6 +183,7 @@ class CodexAdapter extends BaseAdapter {
176
183
  endpoint: this.endpoint,
177
184
  token: this.token,
178
185
  mode: this._mode,
186
+ model: this.modelLabel(),
179
187
  disabledModules: this.disabledModules,
180
188
  ...this.pinnedPromptOpts(channelName),
181
189
  });
@@ -45,7 +45,11 @@
45
45
  const fs = require('fs');
46
46
  const os = require('os');
47
47
  const path = require('path');
48
- const { execFileSync, spawn } = require('child_process');
48
+ const { execFileSync } = require('child_process');
49
+ // spawn() here is the WSL bridge from ../wsl: same signature as
50
+ // child_process.spawn, and a straight pass-through unless the resolved CLI
51
+ // lives on the other side of the Windows/WSL boundary.
52
+ const { spawn } = require('../wsl');
49
53
 
50
54
  const BaseAdapter = require('./base');
51
55
  const { formatAttachmentsForPrompt, SESSION_DEFAULT_RE, generateSessionTitle } = require('./utils');
@@ -168,17 +168,50 @@ function parseLine(line) {
168
168
  * When real samples are available, extend THIS table only.
169
169
  */
170
170
  const EVENT_KIND_BY_TYPE = {
171
- // session lifecycle / identity (kind only mapped when an id is also present)
171
+ // ── CAPTURED from a real authenticated run (CLI v1.0.83) ────────────────
172
+ // Every name in this block was read off a live JSONL stream, not inferred.
173
+ // The payload of each sits under `data` (see the `data` unwrap in
174
+ // classifyEvent) — except `result`, which is flat.
175
+ //
176
+ // The full type list observed in one turn: assistant.turn_start,
177
+ // assistant.reasoning_delta, assistant.reasoning, assistant.message_start,
178
+ // assistant.message_delta, assistant.turn_end, assistant.idle,
179
+ // model.call_start, model.call_finished, session.auto_mode_resolved,
180
+ // session.usage_checkpoint, session.tools_updated, session.skills_loaded,
181
+ // session.mcp_servers_loaded, session.mcp_server_status_changed,
182
+ // session.custom_agents_updated, user.message, result.
183
+ //
184
+ // The ones with no entry here are deliberate: they carry no content the
185
+ // workspace shows (lifecycle bookkeeping, or `user.message`, which is our
186
+ // own prompt echoed back). They degrade to `unknown`, which is logged and
187
+ // ignored — mapping them would put noise in the chat.
188
+ 'assistant.message_delta': 'text_delta', // data.deltaContent — THE answer
189
+ 'assistant.reasoning': 'reasoning', // data.content — ONE whole block
190
+ result: 'done', // flat: sessionId, exitCode, usage
191
+ // NOT mapped on purpose: `assistant.reasoning_delta`. Copilot streams at
192
+ // TOKEN granularity — 81 deltas for one short thought in the captured turn —
193
+ // and every adapter here narrates progress in whole blocks (claude sends
194
+ // `block.text`, codex `item.text`). Mapping the deltas posted one chat
195
+ // message per token: a column of "How" / "can" / "I" / "help" / "?" in the
196
+ // workspace. `assistant.reasoning` carries the same content as a single
197
+ // block afterwards, so that is what gets narrated.
198
+ 'session.usage_checkpoint': 'usage',
199
+ 'session.auto_mode_resolved': 'usage', // data.chosenModel under Auto
200
+ 'model.call_finished': 'usage',
201
+
202
+ // ── INFERRED — no live sample yet ───────────────────────────────────────
203
+ // The captured turn was a plain greeting, so no tool/file/shell/permission
204
+ // event appeared in it. The names below predate that capture and are kept
205
+ // as a net: they cost nothing (the real names above are matched first) and
206
+ // an unrecognized event is logged, never rendered. Replace them the moment
207
+ // a tool-using turn is captured — do not add more guesses.
172
208
  session: 'session', 'session.created': 'session', 'session.started': 'session',
173
209
  session_started: 'session', thread: 'session', 'thread.started': 'session',
174
- // streaming assistant text (qualified names only — no bare 'delta'/'token')
175
210
  'text.delta': 'text_delta', text_delta: 'text_delta',
176
211
  'message.delta': 'text_delta', 'assistant.delta': 'text_delta',
177
212
  content_block_delta: 'text_delta',
178
- // final assistant text (qualified/explicit names only — no bare 'message')
179
213
  text: 'text', 'message.completed': 'text', 'assistant.message': 'text',
180
214
  assistant: 'text', completion: 'text', agent_message: 'text',
181
- // reasoning / status narration
182
215
  reasoning: 'reasoning', thinking: 'reasoning', thought: 'reasoning',
183
216
  status: 'reasoning', progress: 'reasoning',
184
217
  // tool calls
@@ -250,7 +283,14 @@ function classifyEvent(obj) {
250
283
  const itemKind = itemType ? EVENT_KIND_BY_TYPE[itemType.toLowerCase()] : undefined;
251
284
  if (itemKind) kind = itemKind;
252
285
  }
253
- const src = item || obj;
286
+ // Copilot puts every event's payload under `data`, with the envelope
287
+ // (type/id/timestamp/parentId) on the outside — so reading fields off the
288
+ // top level found nothing and the answer came back empty. `result` is the
289
+ // one exception: it is flat, and falls through to `obj` here.
290
+ const data = (obj.data && typeof obj.data === 'object' && !Array.isArray(obj.data))
291
+ ? obj.data
292
+ : null;
293
+ const src = item || data || obj;
254
294
 
255
295
  switch (kind) {
256
296
  case 'session': {
@@ -261,7 +301,9 @@ function classifyEvent(obj) {
261
301
  return { kind: 'session', sessionId };
262
302
  }
263
303
  case 'text_delta': {
264
- const text = _coerceText(src.delta) ?? _coerceText(src.text) ?? _coerceText(src.content) ?? _coerceText(src);
304
+ // `deltaContent` is Copilot's own field name, verified on v1.0.83.
305
+ const text = _coerceText(src.deltaContent) ?? _coerceText(src.delta)
306
+ ?? _coerceText(src.text) ?? _coerceText(src.content) ?? _coerceText(src);
265
307
  return { kind: 'text_delta', text: text || '' };
266
308
  }
267
309
  case 'text': {
@@ -269,7 +311,8 @@ function classifyEvent(obj) {
269
311
  return { kind: 'text', text: text || '' };
270
312
  }
271
313
  case 'reasoning': {
272
- const text = _coerceText(src.text) ?? _coerceText(src.reasoning) ?? _coerceText(src.message)
314
+ const text = _coerceText(src.deltaContent) ?? _coerceText(src.text)
315
+ ?? _coerceText(src.reasoning) ?? _coerceText(src.message)
273
316
  ?? _coerceText(src.content) ?? _firstString(src, ['status', 'label', 'title']) ?? '';
274
317
  return { kind: 'reasoning', text };
275
318
  }
@@ -313,7 +356,14 @@ function classifyEvent(obj) {
313
356
  }
314
357
  case 'done': {
315
358
  const status = _firstString(src, ['status', 'reason', 'result']) || 'completed';
316
- return { kind: 'done', status };
359
+ // The `result` frame is also where the real session id finally shows up
360
+ // — there is no session event earlier in the stream. Carrying it here is
361
+ // what lets the next turn resume instead of starting over. Read from the
362
+ // top level too: `result` is flat, so `src` is already `obj`, but an
363
+ // `item`-wrapped variant would not be.
364
+ const sessionId = _firstString(src, ['sessionId', 'session_id'])
365
+ ?? _firstString(obj, ['sessionId', 'session_id']);
366
+ return sessionId ? { kind: 'done', status, sessionId } : { kind: 'done', status };
317
367
  }
318
368
  case 'error': {
319
369
  const err = (src.error && typeof src.error === 'object') ? src.error : src;
@@ -38,7 +38,11 @@
38
38
  const fs = require('fs');
39
39
  const os = require('os');
40
40
  const path = require('path');
41
- const { execSync, execFileSync, spawn } = require('child_process');
41
+ const { execSync, execFileSync } = require('child_process');
42
+ // spawn() here is the WSL bridge from ../wsl: same signature as
43
+ // child_process.spawn, and a straight pass-through unless the resolved CLI
44
+ // lives on the other side of the Windows/WSL boundary.
45
+ const { spawn } = require('../wsl');
42
46
 
43
47
  const BaseAdapter = require('./base');
44
48
  const { buildOpenclawSystemPrompt } = require('./workspace-prompt');
@@ -326,6 +330,7 @@ class CopilotAdapter extends BaseAdapter {
326
330
  endpoint: this.endpoint,
327
331
  token: this.token,
328
332
  mode: this._mode,
333
+ model: this.modelLabel(),
329
334
  disabledModules: this.disabledModules,
330
335
  ...this.pinnedPromptOpts(channelName),
331
336
  });
@@ -606,10 +611,14 @@ class CopilotAdapter extends BaseAdapter {
606
611
  }
607
612
  break;
608
613
  case 'text_delta':
609
- // Streamed interim text is shown live as `thinking`; the final
610
- // `text` event (if any) is the authoritative answer.
614
+ // Accumulate only — never narrate. These deltas ARE the final
615
+ // answer being streamed a token at a time (Copilot's
616
+ // `assistant.message_delta`), and the answer is posted in full
617
+ // when the turn ends. Sending each one as `thinking` published the
618
+ // reply twice: once as a column of one-word messages, then again
619
+ // whole.
611
620
  if (sawToolSinceText) { finalParts.length = 0; deltaBuf = ''; sawToolSinceText = false; }
612
- if (ev.text) { deltaBuf += ev.text; try { await this.sendThinking(channel, ev.text); } catch {} }
621
+ if (ev.text) deltaBuf += ev.text;
613
622
  break;
614
623
  case 'text':
615
624
  // A complete message supersedes the deltas streamed for this block.
@@ -655,6 +664,13 @@ class CopilotAdapter extends BaseAdapter {
655
664
  errorEvent = errorEvent || { message: ev.message, code: ev.code };
656
665
  break;
657
666
  case 'done':
667
+ // Copilot reports its session id only in the terminal `result`
668
+ // frame — no session event precedes it — so this is the one place
669
+ // a resumable id can be captured.
670
+ if (ev.sessionId && this._channelSessions[channel] !== ev.sessionId) {
671
+ this._channelSessions[channel] = ev.sessionId;
672
+ this._saveSessions();
673
+ }
658
674
  break;
659
675
  case 'unknown':
660
676
  // Preserve a redacted diagnostic; never crash the task.
@@ -12,7 +12,11 @@
12
12
  const fs = require('fs');
13
13
  const os = require('os');
14
14
  const path = require('path');
15
- const { execSync, spawn } = require('child_process');
15
+ const { execSync } = require('child_process');
16
+ // spawn() here is the WSL bridge from ../wsl: same signature as
17
+ // child_process.spawn, and a straight pass-through unless the resolved CLI
18
+ // lives on the other side of the Windows/WSL boundary.
19
+ const { spawn } = require('../wsl');
16
20
 
17
21
  const BaseAdapter = require('./base');
18
22
  const { formatAttachmentsForPrompt, SESSION_DEFAULT_RE, generateSessionTitle } = require('./utils');
@@ -47,7 +47,11 @@
47
47
  const fs = require('fs');
48
48
  const os = require('os');
49
49
  const path = require('path');
50
- const { execFileSync, spawn, execSync } = require('child_process');
50
+ const { execFileSync, execSync } = require('child_process');
51
+ // spawn() here is the WSL bridge from ../wsl: same signature as
52
+ // child_process.spawn, and a straight pass-through unless the resolved CLI
53
+ // lives on the other side of the Windows/WSL boundary.
54
+ const { spawn } = require('../wsl');
51
55
 
52
56
  const BaseAdapter = require('./base');
53
57
  const { whichBinary, getEnhancedEnv } = require('../paths');
@@ -11,7 +11,11 @@
11
11
  const fs = require('fs');
12
12
  const os = require('os');
13
13
  const path = require('path');
14
- const { execSync, spawn } = require('child_process');
14
+ const { execSync } = require('child_process');
15
+ // spawn() here is the WSL bridge from ../wsl: same signature as
16
+ // child_process.spawn, and a straight pass-through unless the resolved CLI
17
+ // lives on the other side of the Windows/WSL boundary.
18
+ const { spawn, resolveWslBinary } = require('../wsl');
15
19
 
16
20
  const BaseAdapter = require('./base');
17
21
  const { whereBinary } = require('../paths');
@@ -183,7 +187,10 @@ class GeminiAdapter extends BaseAdapter {
183
187
  if (fs.existsSync(c)) return c;
184
188
  }
185
189
 
186
- return null;
190
+ // Nothing native anywhere. Last of all, look inside WSL: a CLI the user
191
+ // installed in their distro is a real install, and the marked path it comes
192
+ // back as is what the spawn bridge in ../wsl turns into a wsl.exe run.
193
+ return resolveWslBinary('gemini');
187
194
  }
188
195
 
189
196
  // The Gemini CLI refuses to run non-interactively unless an auth method is
@@ -232,6 +239,7 @@ class GeminiAdapter extends BaseAdapter {
232
239
  workspaceId: this.workspaceId,
233
240
  channelName,
234
241
  mode: this._mode,
242
+ model: this.modelLabel(),
235
243
  decisionLog: pinned.decisionLog ? { ...pinned.decisionLog, writeAccess: false } : null,
236
244
  glossary: pinned.glossary ? { ...pinned.glossary, writeAccess: false } : null,
237
245
  });
@@ -28,7 +28,11 @@ const fs = require('fs');
28
28
  const path = require('path');
29
29
  const os = require('os');
30
30
  const crypto = require('crypto');
31
- const { spawn, execSync, execFileSync } = require('child_process');
31
+ const { execSync, execFileSync } = require('child_process');
32
+ // spawn() here is the WSL bridge from ../wsl: same signature as
33
+ // child_process.spawn, and a straight pass-through unless the resolved CLI
34
+ // lives on the other side of the Windows/WSL boundary.
35
+ const { spawn } = require('../wsl');
32
36
 
33
37
  const BaseAdapter = require('./base');
34
38
  const { GooseStreamParser, classifyGooseError, redactSecrets } = require('./goose-stream');
@@ -249,7 +253,7 @@ class GooseAdapter extends BaseAdapter {
249
253
 
250
254
  _buildSystemPrompt(channelName) {
251
255
  const pinned = this.pinnedPromptOpts(channelName);
252
- let prompt = buildWorkspaceIdentity(this.agentName, this.workspaceId, channelName, this._mode)
256
+ let prompt = buildWorkspaceIdentity(this.agentName, this.workspaceId, channelName, this._mode, 'mcp', this.modelLabel())
253
257
  + buildCollaborationPrompt()
254
258
  + buildModePrompt(this._mode)
255
259
  + buildPinnedSections({
@@ -17,7 +17,11 @@
17
17
  const fs = require('fs');
18
18
  const os = require('os');
19
19
  const path = require('path');
20
- const { execSync, spawn } = require('child_process');
20
+ const { execSync } = require('child_process');
21
+ // spawn() here is the WSL bridge from ../wsl: same signature as
22
+ // child_process.spawn, and a straight pass-through unless the resolved CLI
23
+ // lives on the other side of the Windows/WSL boundary.
24
+ const { spawn, bridgeSpawn } = require('../wsl');
21
25
 
22
26
  const BaseAdapter = require('./base');
23
27
  const { buildOpenclawSystemPrompt } = require('./workspace-prompt');
@@ -71,9 +75,6 @@ class HermesAdapter extends BaseAdapter {
71
75
 
72
76
  _findHermesBinary() {
73
77
  const home = os.homedir();
74
- // Reset each call. When set, this._hermesBin is a path INSIDE WSL and must
75
- // be invoked as `wsl -e <path> …` rather than spawned natively.
76
- this._hermesViaWsl = false;
77
78
 
78
79
  // Tier 1: PATH (enriched env so we see the dirs the launcher adds; a fresh
79
80
  // install updates the user PATH, which the running daemon won't pick up).
@@ -112,40 +113,13 @@ class HermesAdapter extends BaseAdapter {
112
113
  if (fs.existsSync(c)) return c;
113
114
  }
114
115
 
115
- // Tier 3: Deep scan of every known bin dir.
116
+ // Tier 3: Deep scan of every known bin dir — which ends inside WSL when
117
+ // nothing native turned up, so a hermes the user set up in their distro is
118
+ // found here and comes back marked for the ../wsl spawn bridge. Native
119
+ // Windows (Tiers 1-2) is still preferred.
116
120
  const viaWhich = whichBinary('hermes');
117
121
  if (viaWhich) return viaWhich;
118
122
 
119
- // Tier 4 (Windows only): fall back to a WSL install if one exists. Native
120
- // Windows (Tiers 1-3) is preferred, but a user who set hermes up inside
121
- // WSL2 still works — resolve its absolute in-WSL path; _runHermes then
122
- // invokes it as `wsl -e <path> …`.
123
- if (IS_WINDOWS) {
124
- const wslPath = this._resolveWslHermes();
125
- if (wslPath) {
126
- this._hermesViaWsl = true;
127
- return wslPath;
128
- }
129
- }
130
-
131
- return null;
132
- }
133
-
134
- /**
135
- * Resolve hermes's absolute path inside the default WSL distro, or null.
136
- * Uses a login shell (`bash -lc`) so the installer's PATH additions
137
- * (~/.local/bin) are visible. Returns an absolute Linux path like
138
- * /home/<user>/.local/bin/hermes.
139
- */
140
- _resolveWslHermes() {
141
- if (!IS_WINDOWS) return null;
142
- try {
143
- const out = execSync('wsl.exe -e bash -lc "command -v hermes"', {
144
- encoding: 'utf-8', timeout: 8000, windowsHide: true,
145
- }).trim();
146
- const p = out.split(/\r?\n/).map((s) => s.trim()).find(Boolean);
147
- if (p && p.startsWith('/')) return p;
148
- } catch {}
149
123
  return null;
150
124
  }
151
125
 
@@ -240,6 +214,7 @@ class HermesAdapter extends BaseAdapter {
240
214
  endpoint: this.endpoint,
241
215
  token: this.token,
242
216
  mode: this._mode,
217
+ model: this.modelLabel(),
243
218
  disabledModules: this.disabledModules,
244
219
  ...this.pinnedPromptOpts(channelName),
245
220
  }),
@@ -314,23 +289,17 @@ class HermesAdapter extends BaseAdapter {
314
289
 
315
290
  const env = { ...(this.agentEnv || process.env) };
316
291
 
317
- // On Windows hermes lives inside WSL: invoke `wsl -e <wsl-hermes-path> …`.
318
- // `-e` runs the binary directly (no shell), so every arg — including the
319
- // multi-line prompt in `-q` — passes through verbatim with no quoting hazard.
320
- // hermes then uses its own config (~/.hermes inside WSL) for model/keys.
321
- let spawnBin = this._hermesBin;
322
- let spawnArgs = args;
323
- if (this._hermesViaWsl) {
324
- spawnBin = 'wsl.exe';
325
- spawnArgs = ['-e', this._hermesBin, ...args];
326
- }
327
-
328
- const proc = spawn(spawnBin, spawnArgs, {
292
+ // A hermes that lives inside WSL is rewritten to `wsl.exe -e <path> …` by
293
+ // the bridge in ../wsl (which also forces detached off, there being no
294
+ // process group on the far side, and hands the distro the agent's env via
295
+ // WSLENV). hermes then uses its own config — ~/.hermes inside the distro —
296
+ // for model and keys. Nothing to special-case here.
297
+ const proc = spawn(this._hermesBin, args, {
329
298
  env,
330
299
  stdio: ['ignore', 'pipe', 'pipe'],
331
- // No process group on Windows / WSL (can't signal a group); windowsHide
332
- // keeps the wsl.exe console from flashing up.
333
- detached: !IS_WINDOWS && !this._hermesViaWsl,
300
+ // No process group on Windows (can't signal one); windowsHide keeps a
301
+ // console from flashing up.
302
+ detached: !IS_WINDOWS,
334
303
  windowsHide: true,
335
304
  });
336
305
  this._channelProcesses[channelName] = proc;
@@ -469,9 +438,10 @@ class HermesAdapter extends BaseAdapter {
469
438
 
470
439
  const { execFileSync } = require('child_process');
471
440
  const run = (args) => {
472
- const spawnBin = probe._hermesViaWsl ? 'wsl.exe' : bin;
473
- const spawnArgs = probe._hermesViaWsl ? ['-e', bin, ...args] : args;
474
- execFileSync(spawnBin, spawnArgs, {
441
+ // Same rewrite the spawn bridge does, for the one call here that needs to
442
+ // be synchronous: an in-distro hermes is only reachable through wsl.exe.
443
+ const { file, args: argv } = bridgeSpawn(bin, args);
444
+ execFileSync(file, argv, {
475
445
  stdio: 'ignore',
476
446
  timeout: 20000,
477
447
  windowsHide: true,