oh-my-opencode 4.3.0 → 4.4.0

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/dist/index.js CHANGED
@@ -7285,7 +7285,6 @@ var init_model_versions = __esm(() => {
7285
7285
  "anthropic/claude-opus-4-5": "anthropic/claude-opus-4-7",
7286
7286
  "anthropic/claude-opus-4-6": "anthropic/claude-opus-4-7",
7287
7287
  "anthropic/claude-sonnet-4-5": "anthropic/claude-sonnet-4-6",
7288
- "openai/gpt-5.3-codex": "openai/gpt-5.4",
7289
7288
  "openai/gpt-5.4": "openai/gpt-5.5"
7290
7289
  };
7291
7290
  });
@@ -8570,6 +8569,12 @@ function migrateConfigFile(configPath, rawConfig) {
8570
8569
  delete copy.omo_agent;
8571
8570
  needsWrite = true;
8572
8571
  }
8572
+ if (copy.lsp !== undefined) {
8573
+ const droppedServers = copy.lsp && typeof copy.lsp === "object" ? Object.keys(copy.lsp) : [];
8574
+ log("Removed obsolete 'lsp' config key from oh-my-opencode config. Custom LSP servers are now configured in .opencode/lsp.json at the project root (consumed by the 'lsp' MCP server). Move any server definitions there to restore them.", { configPath, droppedServers });
8575
+ delete copy.lsp;
8576
+ needsWrite = true;
8577
+ }
8573
8578
  if (copy.experimental && typeof copy.experimental === "object") {
8574
8579
  const experimental = copy.experimental;
8575
8580
  if ("hashline_edit" in experimental) {
@@ -9157,8 +9162,14 @@ var init_external_plugin_detector = __esm(() => {
9157
9162
  });
9158
9163
 
9159
9164
  // src/shared/bun-spawn-shim.ts
9160
- import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from "child_process";
9165
+ import {
9166
+ spawn as nodeSpawn,
9167
+ spawnSync as nodeSpawnSync
9168
+ } from "child_process";
9161
9169
  import { Readable, Writable } from "stream";
9170
+ function getBunRuntime() {
9171
+ return typeof Bun === "undefined" ? undefined : runtime.Bun;
9172
+ }
9162
9173
  function emptyReadableStream() {
9163
9174
  return new ReadableStream({
9164
9175
  start(controller) {
@@ -9191,6 +9202,38 @@ function resolveStdio(options) {
9191
9202
  return options.stdio;
9192
9203
  return [options.stdin ?? "ignore", options.stdout ?? "pipe", options.stderr ?? "inherit"];
9193
9204
  }
9205
+ function createNodeSpawnOptions(options, platform = process.platform) {
9206
+ const nodeOptions = {
9207
+ stdio: resolveStdio(options),
9208
+ shell: false
9209
+ };
9210
+ if (options.cwd !== undefined)
9211
+ nodeOptions.cwd = options.cwd;
9212
+ if (options.env !== undefined)
9213
+ nodeOptions.env = options.env;
9214
+ if (options.detached !== undefined)
9215
+ nodeOptions.detached = options.detached;
9216
+ if (options.signal !== undefined)
9217
+ nodeOptions.signal = options.signal;
9218
+ if (platform === "win32") {
9219
+ nodeOptions.windowsHide = true;
9220
+ }
9221
+ return nodeOptions;
9222
+ }
9223
+ function createNodeSpawnSyncOptions(options, platform = process.platform) {
9224
+ const nodeOptions = {
9225
+ stdio: resolveStdio(options),
9226
+ shell: false
9227
+ };
9228
+ if (options.cwd !== undefined)
9229
+ nodeOptions.cwd = options.cwd;
9230
+ if (options.env !== undefined)
9231
+ nodeOptions.env = options.env;
9232
+ if (platform === "win32") {
9233
+ nodeOptions.windowsHide = true;
9234
+ }
9235
+ return nodeOptions;
9236
+ }
9194
9237
  function wrapNodeProcess(proc) {
9195
9238
  let exitCode = null;
9196
9239
  const exited = new Promise((resolve5, reject) => {
@@ -9232,42 +9275,45 @@ function wrapNodeProcess(proc) {
9232
9275
  }
9233
9276
  };
9234
9277
  }
9278
+ function toSpawnSyncBuffer(output) {
9279
+ if (output === null) {
9280
+ return;
9281
+ }
9282
+ return Buffer.isBuffer(output) ? output : Buffer.from(output, "utf8");
9283
+ }
9235
9284
  function spawn2(cmdOrOpts, opts) {
9236
- if (IS_BUN)
9237
- return runtime.Bun.spawn(cmdOrOpts, opts);
9238
9285
  const { cmd, opts: options } = resolveCommand(cmdOrOpts, opts);
9286
+ const bun = getBunRuntime();
9287
+ if (bun)
9288
+ return bun.spawn(cmd, options);
9239
9289
  const [bin, ...args] = cmd;
9240
- const proc = nodeSpawn(bin, args, {
9241
- cwd: options.cwd,
9242
- env: options.env,
9243
- stdio: resolveStdio(options),
9244
- detached: options.detached,
9245
- signal: options.signal
9246
- });
9290
+ if (bin === undefined) {
9291
+ throw new Error("Cannot spawn an empty command");
9292
+ }
9293
+ const proc = nodeSpawn(bin, args, createNodeSpawnOptions(options));
9247
9294
  return wrapNodeProcess(proc);
9248
9295
  }
9249
9296
  function spawnSync(cmdOrOpts, opts) {
9250
- if (IS_BUN)
9251
- return runtime.Bun.spawnSync(cmdOrOpts, opts);
9252
9297
  const { cmd, opts: options } = resolveCommand(cmdOrOpts, opts);
9298
+ const bun = getBunRuntime();
9299
+ if (bun)
9300
+ return bun.spawnSync(cmd, options);
9253
9301
  const [bin, ...args] = cmd;
9254
- const result = nodeSpawnSync(bin, args, {
9255
- cwd: options.cwd,
9256
- env: options.env,
9257
- stdio: resolveStdio(options)
9258
- });
9302
+ if (bin === undefined) {
9303
+ throw new Error("Cannot spawnSync an empty command");
9304
+ }
9305
+ const result = nodeSpawnSync(bin, args, createNodeSpawnSyncOptions(options));
9259
9306
  return {
9260
9307
  exitCode: result.status ?? 1,
9261
- stdout: result.stdout ?? undefined,
9262
- stderr: result.stderr ?? undefined,
9308
+ stdout: toSpawnSyncBuffer(result.stdout),
9309
+ stderr: toSpawnSyncBuffer(result.stderr),
9263
9310
  success: (result.status ?? 1) === 0,
9264
9311
  pid: result.pid ?? -1
9265
9312
  };
9266
9313
  }
9267
- var runtime, IS_BUN;
9314
+ var runtime;
9268
9315
  var init_bun_spawn_shim = __esm(() => {
9269
9316
  runtime = globalThis;
9270
- IS_BUN = typeof runtime.Bun !== "undefined";
9271
9317
  });
9272
9318
 
9273
9319
  // src/shared/archive-entry-validator.ts
@@ -9326,6 +9372,52 @@ function validateArchiveEntries(entries, destDir) {
9326
9372
  }
9327
9373
  var init_archive_entry_validator = () => {};
9328
9374
 
9375
+ // src/shared/process-stream-reader.ts
9376
+ function bufferFromChunk(chunk) {
9377
+ if (Buffer.isBuffer(chunk)) {
9378
+ return chunk;
9379
+ }
9380
+ if (chunk instanceof Uint8Array) {
9381
+ return Buffer.from(chunk);
9382
+ }
9383
+ if (typeof chunk === "string") {
9384
+ return Buffer.from(chunk, "utf8");
9385
+ }
9386
+ throw new TypeError(`Unsupported process stream chunk type: ${typeof chunk}`);
9387
+ }
9388
+ async function readWebStream(stream) {
9389
+ const reader = stream.getReader();
9390
+ const chunks = [];
9391
+ try {
9392
+ while (true) {
9393
+ const result = await reader.read();
9394
+ if (result.done) {
9395
+ return chunks;
9396
+ }
9397
+ chunks.push(Buffer.from(result.value));
9398
+ }
9399
+ } finally {
9400
+ reader.releaseLock();
9401
+ }
9402
+ }
9403
+ async function readNodeStream(stream) {
9404
+ const chunks = [];
9405
+ for await (const chunk of stream) {
9406
+ chunks.push(bufferFromChunk(chunk));
9407
+ }
9408
+ return chunks;
9409
+ }
9410
+ function isWebReadableStream(stream) {
9411
+ return typeof ReadableStream !== "undefined" && stream instanceof ReadableStream;
9412
+ }
9413
+ async function readProcessStream(stream) {
9414
+ if (!stream) {
9415
+ return "";
9416
+ }
9417
+ const chunks = isWebReadableStream(stream) ? await readWebStream(stream) : await readNodeStream(stream);
9418
+ return Buffer.concat(chunks).toString("utf8");
9419
+ }
9420
+
9329
9421
  // src/shared/zip-entry-listing/python-zip-entry-listing.ts
9330
9422
  function isPythonZipListingAvailable() {
9331
9423
  const proc = spawnSync(["python3", "--version"], {
@@ -9363,8 +9455,8 @@ async function listZipEntriesWithPython(archivePath) {
9363
9455
  });
9364
9456
  const [exitCode, stdout, stderr] = await Promise.all([
9365
9457
  proc.exited,
9366
- new Response(proc.stdout).text(),
9367
- new Response(proc.stderr).text()
9458
+ readProcessStream(proc.stdout),
9459
+ readProcessStream(proc.stderr)
9368
9460
  ]);
9369
9461
  if (exitCode !== 0) {
9370
9462
  throw new Error(`zip entry listing failed (exit ${exitCode}): ${stderr}`);
@@ -9431,8 +9523,8 @@ async function listZipEntriesWithPowerShell(archivePath, escapePowerShellPath, e
9431
9523
  });
9432
9524
  const [exitCode, stdout, stderr] = await Promise.all([
9433
9525
  proc.exited,
9434
- new Response(proc.stdout).text(),
9435
- new Response(proc.stderr).text()
9526
+ readProcessStream(proc.stdout),
9527
+ readProcessStream(proc.stderr)
9436
9528
  ]);
9437
9529
  if (exitCode !== 0) {
9438
9530
  throw new Error(`zip entry listing failed (exit ${exitCode}): ${stderr}`);
@@ -9495,8 +9587,8 @@ async function listZipEntriesWithTar(archivePath) {
9495
9587
  });
9496
9588
  const [exitCode, stdout, stderr] = await Promise.all([
9497
9589
  proc.exited,
9498
- new Response(proc.stdout).text(),
9499
- new Response(proc.stderr).text()
9590
+ readProcessStream(proc.stdout),
9591
+ readProcessStream(proc.stderr)
9500
9592
  ]);
9501
9593
  if (exitCode !== 0) {
9502
9594
  throw new Error(`zip entry listing failed (exit ${exitCode}): ${stderr}`);
@@ -9516,8 +9608,8 @@ async function readZipSymlinkTarget(archivePath, entryPath) {
9516
9608
  });
9517
9609
  const [exitCode, stdout, stderr] = await Promise.all([
9518
9610
  proc.exited,
9519
- new Response(proc.stdout).text(),
9520
- new Response(proc.stderr).text()
9611
+ readProcessStream(proc.stdout),
9612
+ readProcessStream(proc.stderr)
9521
9613
  ]);
9522
9614
  if (exitCode !== 0) {
9523
9615
  throw new Error(`zip symlink target read failed (exit ${exitCode}): ${stderr}`);
@@ -9560,8 +9652,8 @@ async function listZipEntriesWithZipInfo(archivePath) {
9560
9652
  });
9561
9653
  const [exitCode, stdout, stderr] = await Promise.all([
9562
9654
  proc.exited,
9563
- new Response(proc.stdout).text(),
9564
- new Response(proc.stderr).text()
9655
+ readProcessStream(proc.stdout),
9656
+ readProcessStream(proc.stderr)
9565
9657
  ]);
9566
9658
  if (exitCode !== 0) {
9567
9659
  throw new Error(`zip entry listing failed (exit ${exitCode}): ${stderr}`);
@@ -9657,7 +9749,7 @@ async function extractZip(archivePath, destDir) {
9657
9749
  }
9658
9750
  const exitCode = await proc.exited;
9659
9751
  if (exitCode !== 0) {
9660
- const stderr = await new Response(proc.stderr).text();
9752
+ const stderr = await readProcessStream(proc.stderr);
9661
9753
  throw new Error(`zip extraction failed (exit ${exitCode}): ${stderr}`);
9662
9754
  }
9663
9755
  }
@@ -9716,20 +9808,20 @@ function createNodeFile(path6) {
9716
9808
  };
9717
9809
  }
9718
9810
  function bunFile(path6) {
9719
- if (IS_BUN2)
9811
+ if (IS_BUN)
9720
9812
  return runtime2.Bun.file(path6);
9721
9813
  return createNodeFile(path6);
9722
9814
  }
9723
9815
  async function bunWrite(path6, data) {
9724
- if (IS_BUN2)
9816
+ if (IS_BUN)
9725
9817
  return runtime2.Bun.write(path6, data);
9726
9818
  await writeFile(path6, toWritableData(data));
9727
9819
  return byteLength(data);
9728
9820
  }
9729
- var runtime2, IS_BUN2;
9821
+ var runtime2, IS_BUN;
9730
9822
  var init_bun_file_shim = __esm(() => {
9731
9823
  runtime2 = globalThis;
9732
- IS_BUN2 = typeof runtime2.Bun !== "undefined";
9824
+ IS_BUN = typeof runtime2.Bun !== "undefined";
9733
9825
  });
9734
9826
 
9735
9827
  // src/shared/binary-downloader.ts
@@ -9766,7 +9858,7 @@ async function extractTarGz(archivePath, destDir, options) {
9766
9858
  });
9767
9859
  const exitCode = await proc.exited;
9768
9860
  if (exitCode !== 0) {
9769
- const stderr = await new Response(proc.stderr).text();
9861
+ const stderr = await readProcessStream(proc.stderr);
9770
9862
  if (isTarTraversalErrorOutput(stderr)) {
9771
9863
  throw new Error(`Unsafe archive entry: path contains path traversal (${archivePath})`);
9772
9864
  }
@@ -9816,8 +9908,8 @@ async function listTarEntries(archivePath, cwd) {
9816
9908
  });
9817
9909
  const [exitCode, stdout, stderr] = await Promise.all([
9818
9910
  proc.exited,
9819
- new Response(proc.stdout).text(),
9820
- new Response(proc.stderr).text()
9911
+ readProcessStream(proc.stdout),
9912
+ readProcessStream(proc.stderr)
9821
9913
  ]);
9822
9914
  if (isTarTraversalErrorOutput(stderr)) {
9823
9915
  throw new Error(`Unsafe archive entry: path contains path traversal (${archivePath})`);
@@ -69279,6 +69371,8 @@ async function buildReadyNotificationContent(ctx, input) {
69279
69371
 
69280
69372
  // src/hooks/session-notification-sender.ts
69281
69373
  init_shared();
69374
+ import { execFile } from "child_process";
69375
+ import { promisify as promisify2 } from "util";
69282
69376
  import { platform } from "os";
69283
69377
 
69284
69378
  // src/hooks/session-notification-utils.ts
@@ -69288,7 +69382,7 @@ init_logger();
69288
69382
  import { accessSync as accessSync2, constants as constants4 } from "fs";
69289
69383
  import { delimiter, join as join29 } from "path";
69290
69384
  var runtime3 = globalThis;
69291
- var IS_BUN3 = typeof runtime3.Bun !== "undefined";
69385
+ var IS_BUN2 = typeof runtime3.Bun !== "undefined";
69292
69386
  function isUnsafeCommandName(commandName) {
69293
69387
  if (commandName.includes("/") || commandName.includes("\\"))
69294
69388
  return true;
@@ -69323,7 +69417,7 @@ function bunWhich(commandName) {
69323
69417
  return null;
69324
69418
  if (isUnsafeCommandName(commandName))
69325
69419
  return null;
69326
- if (IS_BUN3)
69420
+ if (IS_BUN2)
69327
69421
  return runtime3.Bun?.which(commandName) ?? null;
69328
69422
  const pathValue = resolvePathValue();
69329
69423
  if (!pathValue)
@@ -69459,14 +69553,33 @@ function getDefaultSoundPath(platform2) {
69459
69553
  }
69460
69554
  }
69461
69555
  var hasLoggedUnavailableShellHelper = false;
69462
- function canRunNotificationCommand(ctx) {
69463
- if (typeof ctx?.$ === "function")
69464
- return true;
69556
+ function getShellRunner(ctx) {
69557
+ if (typeof ctx.$ === "function")
69558
+ return ctx.$;
69465
69559
  if (!hasLoggedUnavailableShellHelper) {
69466
69560
  hasLoggedUnavailableShellHelper = true;
69467
- log("[session-notification] ctx.$ unavailable; skipping notification command execution");
69561
+ log("[session-notification] ctx.$ unavailable; falling back to child_process.execFile");
69468
69562
  }
69469
- return false;
69563
+ return;
69564
+ }
69565
+ function logCommandFailure(commandName, error) {
69566
+ log("[session-notification] notification command failed", {
69567
+ commandName,
69568
+ error: typeof error === "string" ? error : error.message
69569
+ });
69570
+ }
69571
+ function logOperationFailure(operation, error) {
69572
+ log("[session-notification] notification operation failed", {
69573
+ operation,
69574
+ error: typeof error === "string" ? error : error.message
69575
+ });
69576
+ }
69577
+ async function runQuiet(command) {
69578
+ if (typeof command.quiet === "function") {
69579
+ await command.quiet();
69580
+ return;
69581
+ }
69582
+ await command;
69470
69583
  }
69471
69584
  async function runQuietNothrow(command) {
69472
69585
  const safeCommand = typeof command.nothrow === "function" ? command.nothrow() : command;
@@ -69476,85 +69589,124 @@ async function runQuietNothrow(command) {
69476
69589
  }
69477
69590
  await safeCommand;
69478
69591
  }
69479
- async function sendSessionNotification(ctx, platform2, title, message) {
69480
- if (!canRunNotificationCommand(ctx))
69592
+ async function runExecFile(commandPath, args) {
69593
+ const execFileAsync = promisify2(execFile);
69594
+ await execFileAsync(commandPath, [...args], { windowsHide: true });
69595
+ }
69596
+ async function runNotificationCommand(ctx, commandPath, args, shellCommand, shellFailureMode = "nothrow") {
69597
+ const shell = getShellRunner(ctx);
69598
+ if (shell) {
69599
+ if (shellFailureMode === "throw") {
69600
+ await runQuiet(shellCommand(shell));
69601
+ return;
69602
+ }
69603
+ await runQuietNothrow(shellCommand(shell));
69481
69604
  return;
69482
- switch (platform2) {
69483
- case "darwin": {
69484
- const cmuxPath = await getCmuxPath();
69485
- if (cmuxPath) {
69486
- try {
69487
- await ctx.$`${cmuxPath} notify --title ${title} --body ${message}`.quiet();
69488
- break;
69489
- } catch {}
69490
- }
69491
- const terminalNotifierPath = await getTerminalNotifierPath();
69492
- if (terminalNotifierPath) {
69493
- const bundleId = process.env.__CFBundleIdentifier;
69494
- try {
69495
- if (bundleId) {
69496
- await ctx.$`${terminalNotifierPath} -title ${title} -message ${message} -activate ${bundleId}`.quiet();
69497
- } else {
69498
- await ctx.$`${terminalNotifierPath} -title ${title} -message ${message}`.quiet();
69605
+ }
69606
+ await runExecFile(commandPath, args);
69607
+ }
69608
+ async function sendSessionNotification(ctx, platform2, title, message) {
69609
+ try {
69610
+ switch (platform2) {
69611
+ case "darwin": {
69612
+ const cmuxPath = await getCmuxPath();
69613
+ if (cmuxPath) {
69614
+ try {
69615
+ await runNotificationCommand(ctx, cmuxPath, ["notify", "--title", title, "--body", message], (shell) => shell`${cmuxPath} notify --title ${title} --body ${message}`, "throw");
69616
+ break;
69617
+ } catch (error) {
69618
+ if (error instanceof Error) {
69619
+ logCommandFailure("cmux", error);
69620
+ } else {
69621
+ logCommandFailure("cmux", String(error));
69622
+ }
69499
69623
  }
69500
- break;
69501
- } catch {}
69624
+ }
69625
+ const terminalNotifierPath = await getTerminalNotifierPath();
69626
+ if (terminalNotifierPath) {
69627
+ const bundleId = process.env.__CFBundleIdentifier;
69628
+ const args = bundleId ? ["-title", title, "-message", message, "-activate", bundleId] : ["-title", title, "-message", message];
69629
+ try {
69630
+ await runNotificationCommand(ctx, terminalNotifierPath, args, (shell) => bundleId ? shell`${terminalNotifierPath} -title ${title} -message ${message} -activate ${bundleId}` : shell`${terminalNotifierPath} -title ${title} -message ${message}`, "throw");
69631
+ break;
69632
+ } catch (error) {
69633
+ if (error instanceof Error) {
69634
+ logCommandFailure("terminal-notifier", error);
69635
+ } else {
69636
+ logCommandFailure("terminal-notifier", String(error));
69637
+ }
69638
+ }
69639
+ }
69640
+ const osascriptPath = await getOsascriptPath();
69641
+ if (!osascriptPath)
69642
+ return;
69643
+ const escapedTitle = escapeAppleScriptText(title);
69644
+ const escapedMessage = escapeAppleScriptText(message);
69645
+ const appleScript = 'display notification "' + escapedMessage + '" with title "' + escapedTitle + '"';
69646
+ await runNotificationCommand(ctx, osascriptPath, ["-e", appleScript], (shell) => shell`${osascriptPath} -e ${appleScript}`);
69647
+ break;
69648
+ }
69649
+ case "linux": {
69650
+ const notifySendPath = await getNotifySendPath();
69651
+ if (!notifySendPath)
69652
+ return;
69653
+ await runNotificationCommand(ctx, notifySendPath, [title, message], (shell) => shell`${notifySendPath} ${title} ${message} 2>/dev/null`);
69654
+ break;
69655
+ }
69656
+ case "win32": {
69657
+ const powershellPath = await getPowershellPath();
69658
+ if (!powershellPath)
69659
+ return;
69660
+ const toastScript = buildWindowsToastScript(title, message);
69661
+ await runNotificationCommand(ctx, powershellPath, ["-Command", toastScript], (shell) => shell`${powershellPath} -Command ${toastScript}`);
69662
+ break;
69502
69663
  }
69503
- const osascriptPath = await getOsascriptPath();
69504
- if (!osascriptPath)
69505
- return;
69506
- const escapedTitle = escapeAppleScriptText(title);
69507
- const escapedMessage = escapeAppleScriptText(message);
69508
- await runQuietNothrow(ctx.$`${osascriptPath} -e ${'display notification "' + escapedMessage + '" with title "' + escapedTitle + '"'}`);
69509
- break;
69510
- }
69511
- case "linux": {
69512
- const notifySendPath = await getNotifySendPath();
69513
- if (!notifySendPath)
69514
- return;
69515
- await runQuietNothrow(ctx.$`${notifySendPath} ${title} ${message} 2>/dev/null`);
69516
- break;
69517
69664
  }
69518
- case "win32": {
69519
- const powershellPath = await getPowershellPath();
69520
- if (!powershellPath)
69521
- return;
69522
- const toastScript = buildWindowsToastScript(title, message);
69523
- await runQuietNothrow(ctx.$`${powershellPath} -Command ${toastScript}`);
69524
- break;
69665
+ } catch (error) {
69666
+ if (error instanceof Error) {
69667
+ logOperationFailure("send", error);
69668
+ } else {
69669
+ logOperationFailure("send", String(error));
69525
69670
  }
69526
69671
  }
69527
69672
  }
69528
69673
  async function playSessionNotificationSound(ctx, platform2, soundPath) {
69529
- if (!canRunNotificationCommand(ctx))
69530
- return;
69531
- switch (platform2) {
69532
- case "darwin": {
69533
- const afplayPath = await getAfplayPath();
69534
- if (!afplayPath)
69535
- return;
69536
- await runQuietNothrow(ctx.$`${afplayPath} ${soundPath}`);
69537
- break;
69538
- }
69539
- case "linux": {
69540
- const paplayPath = await getPaplayPath();
69541
- if (paplayPath) {
69542
- await runQuietNothrow(ctx.$`${paplayPath} ${soundPath} 2>/dev/null`);
69543
- } else {
69544
- const aplayPath = await getAplayPath();
69545
- if (aplayPath) {
69546
- await runQuietNothrow(ctx.$`${aplayPath} ${soundPath} 2>/dev/null`);
69674
+ try {
69675
+ switch (platform2) {
69676
+ case "darwin": {
69677
+ const afplayPath = await getAfplayPath();
69678
+ if (!afplayPath)
69679
+ return;
69680
+ await runNotificationCommand(ctx, afplayPath, [soundPath], (shell) => shell`${afplayPath} ${soundPath}`);
69681
+ break;
69682
+ }
69683
+ case "linux": {
69684
+ const paplayPath = await getPaplayPath();
69685
+ if (paplayPath) {
69686
+ await runNotificationCommand(ctx, paplayPath, [soundPath], (shell) => shell`${paplayPath} ${soundPath} 2>/dev/null`);
69687
+ } else {
69688
+ const aplayPath = await getAplayPath();
69689
+ if (aplayPath) {
69690
+ await runNotificationCommand(ctx, aplayPath, [soundPath], (shell) => shell`${aplayPath} ${soundPath} 2>/dev/null`);
69691
+ }
69547
69692
  }
69693
+ break;
69694
+ }
69695
+ case "win32": {
69696
+ const powershellPath = await getPowershellPath();
69697
+ if (!powershellPath)
69698
+ return;
69699
+ const escaped = escapePowerShellSingleQuotedText(soundPath);
69700
+ const soundScript = "(New-Object Media.SoundPlayer '" + escaped + "').PlaySync()";
69701
+ await runNotificationCommand(ctx, powershellPath, ["-Command", soundScript], (shell) => shell`${powershellPath} -Command ${soundScript}`);
69702
+ break;
69548
69703
  }
69549
- break;
69550
69704
  }
69551
- case "win32": {
69552
- const powershellPath = await getPowershellPath();
69553
- if (!powershellPath)
69554
- return;
69555
- const escaped = escapePowerShellSingleQuotedText(soundPath);
69556
- await runQuietNothrow(ctx.$`${powershellPath} -Command ${"(New-Object Media.SoundPlayer '" + escaped + "').PlaySync()"}`);
69557
- break;
69705
+ } catch (error) {
69706
+ if (error instanceof Error) {
69707
+ logOperationFailure("sound", error);
69708
+ } else {
69709
+ logOperationFailure("sound", String(error));
69558
69710
  }
69559
69711
  }
69560
69712
  }
@@ -71625,6 +71777,27 @@ async function runCommentChecker2(input, cliPath, customPrompt) {
71625
71777
  // src/hooks/comment-checker/cli-runner.ts
71626
71778
  var cliPathPromise = null;
71627
71779
  var isRunning = false;
71780
+ var sessionLastWarning = new Map;
71781
+ var DEDUP_WINDOW_MS = 30000;
71782
+ function hasCommentSyntax(text) {
71783
+ if (!text)
71784
+ return false;
71785
+ return /^\s*(\/\/|\/\*|#|--|<!--|:\s*)[\s\S]*$/m.test(text) || /<!--[\s\S]*-->/.test(text);
71786
+ }
71787
+ function hasNewCommentsOnly(oldText, newText) {
71788
+ if (!hasCommentSyntax(newText))
71789
+ return false;
71790
+ if (!hasCommentSyntax(oldText))
71791
+ return true;
71792
+ const oldLines = new Set((oldText ?? "").split(`
71793
+ `).map((l) => l.trim()));
71794
+ const newLines = (newText ?? "").split(`
71795
+ `);
71796
+ return newLines.some((l) => {
71797
+ const trimmed = l.trim();
71798
+ return trimmed && hasCommentSyntax(trimmed) && !oldLines.has(trimmed);
71799
+ });
71800
+ }
71628
71801
  async function withCommentCheckerLock(fn, fallback, debugLog3) {
71629
71802
  if (isRunning) {
71630
71803
  debugLog3("comment-checker already running, skipping");
@@ -71666,6 +71839,17 @@ async function processWithCli(input, pendingCall, output, cliPath, customPrompt,
71666
71839
  edits: pendingCall.edits
71667
71840
  }
71668
71841
  };
71842
+ if (!hasNewCommentsOnly(pendingCall.oldString, pendingCall.newString)) {
71843
+ debugLog3("skipping: no net-new comments in edit (oldString/newString)");
71844
+ return;
71845
+ }
71846
+ const lastWarned = sessionLastWarning.get(pendingCall.sessionID) ?? 0;
71847
+ const now = Date.now();
71848
+ if (now - lastWarned < DEDUP_WINDOW_MS) {
71849
+ debugLog3("dedup: skipping comment warning within dedup window for session", pendingCall.sessionID);
71850
+ return;
71851
+ }
71852
+ sessionLastWarning.set(pendingCall.sessionID, now);
71669
71853
  const result = await (deps.runCommentChecker ?? runCommentChecker2)(hookInput, cliPath, customPrompt);
71670
71854
  if (result.hasComments && result.message) {
71671
71855
  debugLog3("CLI detected comments, appending message");
@@ -80498,6 +80682,7 @@ function createTeamModeStatusInjector(config, keywordDetectorConfig) {
80498
80682
  // src/hooks/tool-pair-validator/hook.ts
80499
80683
  init_logger();
80500
80684
  var TOOL_RESULT_PLACEHOLDER = "Tool output unavailable (context compacted)";
80685
+ var TOOL_RESULT_RECOVERY_CONTINUATION = "Recovered missing tool results. Continue from the repaired tool output.";
80501
80686
  function getToolUseID(part) {
80502
80687
  const candidate = part;
80503
80688
  if (candidate.type === "tool_use" && typeof candidate.id === "string" && candidate.id.length > 0) {
@@ -80575,7 +80760,14 @@ function createSyntheticUserMessage(assistantMessage, missingToolUseIDs) {
80575
80760
  role: "user",
80576
80761
  ...sessionID ? { sessionID } : {}
80577
80762
  },
80578
- parts: missingToolUseIDs.map((toolUseID) => createToolResultPart(toolUseID))
80763
+ parts: [
80764
+ ...missingToolUseIDs.map((toolUseID) => createToolResultPart(toolUseID)),
80765
+ {
80766
+ type: "text",
80767
+ text: TOOL_RESULT_RECOVERY_CONTINUATION,
80768
+ synthetic: true
80769
+ }
80770
+ ]
80579
80771
  };
80580
80772
  }
80581
80773
  function getMessageID(message) {
@@ -91844,6 +92036,10 @@ async function handleAtlasSessionIdle(input) {
91844
92036
  }
91845
92037
  const { boulderState, progress, appendedSession } = activeBoulderSession;
91846
92038
  if (progress.isComplete) {
92039
+ if (sessionState.pendingRetryTimer) {
92040
+ clearTimeout(sessionState.pendingRetryTimer);
92041
+ sessionState.pendingRetryTimer = undefined;
92042
+ }
91847
92043
  const work = getWorkForSession(ctx.directory, sessionID);
91848
92044
  if (work) {
91849
92045
  completeBoulder(ctx.directory, work.work_id);
@@ -91854,6 +92050,10 @@ async function handleAtlasSessionIdle(input) {
91854
92050
  log(`[${HOOK_NAME7}] Boulder complete`, { sessionID, plan: boulderState.plan_name });
91855
92051
  return;
91856
92052
  }
92053
+ if (options?.isContinuationStopped?.(sessionID)) {
92054
+ log(`[${HOOK_NAME7}] Boulder completion nudge skipped because continuation stopped`, { sessionID, plan: boulderState.plan_name });
92055
+ return;
92056
+ }
91857
92057
  if (sessionState.boulderCompletionNudgedAt?.[work.work_id]) {
91858
92058
  log(`[${HOOK_NAME7}] Boulder complete`, { sessionID, plan: boulderState.plan_name });
91859
92059
  return;
@@ -100037,11 +100237,17 @@ var cachedCli = null;
100037
100237
  var autoInstallAttempted = false;
100038
100238
  function findExecutable(name) {
100039
100239
  const isWindows2 = process.platform === "win32";
100040
- const cmd = isWindows2 ? "where" : "which";
100240
+ const cmd = isWindows2 ? "where.exe" : "which";
100041
100241
  try {
100042
- const result = spawnSync2(cmd, [name], { encoding: "utf-8", timeout: 5000 });
100043
- if (result.status === 0 && result.stdout.trim()) {
100044
- return result.stdout.trim().split(`
100242
+ const result = spawnSync2(cmd, [name], {
100243
+ encoding: "utf-8",
100244
+ timeout: 5000,
100245
+ windowsHide: isWindows2,
100246
+ shell: false
100247
+ });
100248
+ const stdout = result.stdout;
100249
+ if (result.status === 0 && stdout.trim()) {
100250
+ return stdout.trim().split(`
100045
100251
  `)[0];
100046
100252
  }
100047
100253
  } catch {
@@ -100055,6 +100261,7 @@ function getOpenCodeBundledRg() {
100055
100261
  const isWindows2 = process.platform === "win32";
100056
100262
  const rgName = isWindows2 ? "rg.exe" : "rg";
100057
100263
  const candidates = [
100264
+ join81(getOpenCodeCacheDir(), "bin", rgName),
100058
100265
  join81(getDataDir(), "opencode", "bin", rgName),
100059
100266
  join81(execDir, rgName),
100060
100267
  join81(execDir, "bin", rgName),
@@ -100163,6 +100370,29 @@ class Semaphore {
100163
100370
  }
100164
100371
  var rgSemaphore = new Semaphore(2);
100165
100372
 
100373
+ // src/tools/shared/search-process-output.ts
100374
+ function getErrorMessage5(error) {
100375
+ return error instanceof Error ? error.message : String(error);
100376
+ }
100377
+ function createProcessTimeout(proc, timeoutMs, timeoutMessage) {
100378
+ return new Promise((_, reject) => {
100379
+ const id = setTimeout(() => {
100380
+ proc.kill();
100381
+ reject(new Error(timeoutMessage));
100382
+ }, timeoutMs);
100383
+ proc.exited.then(() => clearTimeout(id), () => clearTimeout(id));
100384
+ });
100385
+ }
100386
+ async function collectSearchProcessOutput(proc, timeoutMs, timeoutMessage) {
100387
+ const stderrPromise = readProcessStream(proc.stderr).catch(getErrorMessage5);
100388
+ const stdout = await Promise.race([
100389
+ readProcessStream(proc.stdout),
100390
+ createProcessTimeout(proc, timeoutMs, timeoutMessage)
100391
+ ]);
100392
+ const [exitCode, stderr] = await Promise.all([proc.exited, stderrPromise]);
100393
+ return { stdout, stderr, exitCode };
100394
+ }
100395
+
100166
100396
  // src/tools/grep/cli.ts
100167
100397
  function buildRgArgs(options) {
100168
100398
  const args = [
@@ -100286,15 +100516,15 @@ function parseCountOutput(output) {
100286
100516
  }
100287
100517
  return results;
100288
100518
  }
100289
- async function runRg(options, resolvedCli) {
100519
+ async function runRg(options, resolvedCli, processSpawner = spawn2) {
100290
100520
  await rgSemaphore.acquire();
100291
100521
  try {
100292
- return await runRgInternal(options, resolvedCli);
100522
+ return await runRgInternal(options, resolvedCli, processSpawner);
100293
100523
  } finally {
100294
100524
  rgSemaphore.release();
100295
100525
  }
100296
100526
  }
100297
- async function runRgInternal(options, resolvedCli) {
100527
+ async function runRgInternal(options, resolvedCli, processSpawner = spawn2) {
100298
100528
  const cli = resolvedCli ?? resolveGrepCli();
100299
100529
  const args = buildArgs(options, cli.backend);
100300
100530
  const timeout = Math.min(options.timeout ?? DEFAULT_TIMEOUT_MS2, DEFAULT_TIMEOUT_MS2);
@@ -100305,21 +100535,12 @@ async function runRgInternal(options, resolvedCli) {
100305
100535
  }
100306
100536
  const paths = options.paths?.length ? options.paths : ["."];
100307
100537
  args.push(...paths);
100308
- const proc = spawn2([cli.path, ...args], {
100309
- stdout: "pipe",
100310
- stderr: "pipe"
100311
- });
100312
- const timeoutPromise = new Promise((_, reject) => {
100313
- const id = setTimeout(() => {
100314
- proc.kill();
100315
- reject(new Error(`Search timeout after ${timeout}ms`));
100316
- }, timeout);
100317
- proc.exited.then(() => clearTimeout(id));
100318
- });
100319
100538
  try {
100320
- const stdout = await Promise.race([new Response(proc.stdout).text(), timeoutPromise]);
100321
- const stderr = await new Response(proc.stderr).text();
100322
- const exitCode = await proc.exited;
100539
+ const proc = processSpawner([cli.path, ...args], {
100540
+ stdout: "pipe",
100541
+ stderr: "pipe"
100542
+ });
100543
+ const { stdout, stderr, exitCode } = await collectSearchProcessOutput(proc, timeout, `Search timeout after ${timeout}ms`);
100323
100544
  const truncated = stdout.length >= DEFAULT_MAX_OUTPUT_BYTES;
100324
100545
  const outputToProcess = truncated ? stdout.substring(0, DEFAULT_MAX_OUTPUT_BYTES) : stdout;
100325
100546
  if (exitCode > 1 && stderr.trim()) {
@@ -100350,15 +100571,15 @@ async function runRgInternal(options, resolvedCli) {
100350
100571
  };
100351
100572
  }
100352
100573
  }
100353
- async function runRgCount(options, resolvedCli) {
100574
+ async function runRgCount(options, resolvedCli, processSpawner = spawn2) {
100354
100575
  await rgSemaphore.acquire();
100355
100576
  try {
100356
- return await runRgCountInternal(options, resolvedCli);
100577
+ return await runRgCountInternal(options, resolvedCli, processSpawner);
100357
100578
  } finally {
100358
100579
  rgSemaphore.release();
100359
100580
  }
100360
100581
  }
100361
- async function runRgCountInternal(options, resolvedCli) {
100582
+ async function runRgCountInternal(options, resolvedCli, processSpawner = spawn2) {
100362
100583
  const cli = resolvedCli ?? resolveGrepCli();
100363
100584
  const args = buildArgs({ ...options, context: 0 }, cli.backend);
100364
100585
  if (cli.backend === "rg") {
@@ -100369,19 +100590,15 @@ async function runRgCountInternal(options, resolvedCli) {
100369
100590
  const paths = options.paths?.length ? options.paths : ["."];
100370
100591
  args.push(...paths);
100371
100592
  const timeout = Math.min(options.timeout ?? DEFAULT_TIMEOUT_MS2, DEFAULT_TIMEOUT_MS2);
100372
- const proc = spawn2([cli.path, ...args], {
100373
- stdout: "pipe",
100374
- stderr: "pipe"
100375
- });
100376
- const timeoutPromise = new Promise((_, reject) => {
100377
- const id = setTimeout(() => {
100378
- proc.kill();
100379
- reject(new Error(`Search timeout after ${timeout}ms`));
100380
- }, timeout);
100381
- proc.exited.then(() => clearTimeout(id));
100382
- });
100383
100593
  try {
100384
- const stdout = await Promise.race([new Response(proc.stdout).text(), timeoutPromise]);
100594
+ const proc = processSpawner([cli.path, ...args], {
100595
+ stdout: "pipe",
100596
+ stderr: "pipe"
100597
+ });
100598
+ const { stdout, stderr, exitCode } = await collectSearchProcessOutput(proc, timeout, `Search timeout after ${timeout}ms`);
100599
+ if (exitCode > 1 && stderr.trim()) {
100600
+ throw new Error(stderr.trim());
100601
+ }
100385
100602
  return parseCountOutput(stdout);
100386
100603
  } catch (e) {
100387
100604
  throw new Error(`Count search failed: ${e instanceof Error ? e.message : String(e)}`);
@@ -100542,12 +100759,12 @@ function buildPowerShellCommand(options) {
100542
100759
  const searchPath = paths[0] || ".";
100543
100760
  const escapedPath = searchPath.replace(/'/g, "''");
100544
100761
  const escapedPattern = options.pattern.replace(/'/g, "''");
100545
- let psCommand = `Get-ChildItem -Path '${escapedPath}' -File -Recurse -Depth ${maxDepth - 1} -Filter '${escapedPattern}'`;
100762
+ let psCommand = `Get-ChildItem -LiteralPath '${escapedPath}' -File -Recurse -Depth ${maxDepth - 1} -Filter '${escapedPattern}'`;
100546
100763
  if (options.hidden !== false) {
100547
100764
  psCommand += " -Force";
100548
100765
  }
100549
100766
  psCommand += " -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName";
100550
- return ["powershell", "-NoProfile", "-Command", psCommand];
100767
+ return ["powershell.exe", "-NoProfile", "-Command", psCommand];
100551
100768
  }
100552
100769
  async function getFileMtime(filePath) {
100553
100770
  try {
@@ -100557,15 +100774,15 @@ async function getFileMtime(filePath) {
100557
100774
  return 0;
100558
100775
  }
100559
100776
  }
100560
- async function runRgFiles(options, resolvedCli) {
100777
+ async function runRgFiles(options, resolvedCli, processSpawner = spawn2) {
100561
100778
  await rgSemaphore.acquire();
100562
100779
  try {
100563
- return await runRgFilesInternal(options, resolvedCli);
100780
+ return await runRgFilesInternal(options, resolvedCli, processSpawner);
100564
100781
  } finally {
100565
100782
  rgSemaphore.release();
100566
100783
  }
100567
100784
  }
100568
- async function runRgFilesInternal(options, resolvedCli) {
100785
+ async function runRgFilesInternal(options, resolvedCli, processSpawner = spawn2) {
100569
100786
  const cli = resolvedCli ?? resolveGrepCli();
100570
100787
  const timeout = Math.min(options.timeout ?? DEFAULT_TIMEOUT_MS3, DEFAULT_TIMEOUT_MS3);
100571
100788
  const limit = Math.min(options.limit ?? DEFAULT_LIMIT, DEFAULT_LIMIT);
@@ -100587,22 +100804,13 @@ async function runRgFilesInternal(options, resolvedCli) {
100587
100804
  cwd = paths[0] || ".";
100588
100805
  command = [cli.path, ...args];
100589
100806
  }
100590
- const proc = spawn2(command, {
100591
- stdout: "pipe",
100592
- stderr: "pipe",
100593
- cwd
100594
- });
100595
- const timeoutPromise = new Promise((_, reject) => {
100596
- const id = setTimeout(() => {
100597
- proc.kill();
100598
- reject(new Error(`Glob search timeout after ${timeout}ms`));
100599
- }, timeout);
100600
- proc.exited.then(() => clearTimeout(id));
100601
- });
100602
100807
  try {
100603
- const stdout = await Promise.race([new Response(proc.stdout).text(), timeoutPromise]);
100604
- const stderr = await new Response(proc.stderr).text();
100605
- const exitCode = await proc.exited;
100808
+ const proc = processSpawner(command, {
100809
+ stdout: "pipe",
100810
+ stderr: "pipe",
100811
+ cwd
100812
+ });
100813
+ const { stdout, stderr, exitCode } = await collectSearchProcessOutput(proc, timeout, `Glob search timeout after ${timeout}ms`);
100606
100814
  if (exitCode > 1 && stderr.trim()) {
100607
100815
  return {
100608
100816
  files: [],
@@ -102373,7 +102581,7 @@ init_opencode_message_dir();
102373
102581
  init_logger();
102374
102582
 
102375
102583
  // src/tools/background-task/session-messages.ts
102376
- function getErrorMessage5(value) {
102584
+ function getErrorMessage6(value) {
102377
102585
  if (Array.isArray(value))
102378
102586
  return null;
102379
102587
  if (value.error === undefined || value.error === null)
@@ -102556,7 +102764,7 @@ async function formatFullSession(task, client, options) {
102556
102764
  } catch (error) {
102557
102765
  return `Error fetching messages: ${error instanceof Error ? error.message : String(error)}`;
102558
102766
  }
102559
- const errorMessage = getErrorMessage5(messagesResult);
102767
+ const errorMessage = getErrorMessage6(messagesResult);
102560
102768
  if (errorMessage) {
102561
102769
  return `Error fetching messages: ${errorMessage}`;
102562
102770
  }
@@ -102762,7 +102970,7 @@ async function formatTaskResult(task, client) {
102762
102970
  } catch (error) {
102763
102971
  return `Error fetching messages: ${error instanceof Error ? error.message : String(error)}`;
102764
102972
  }
102765
- const errorMessage = getErrorMessage5(messagesResult);
102973
+ const errorMessage = getErrorMessage6(messagesResult);
102766
102974
  if (errorMessage) {
102767
102975
  return `Error fetching messages: ${errorMessage}`;
102768
102976
  }
@@ -104640,10 +104848,10 @@ async function waitForLookAtSessionResult(client, sessionID, options) {
104640
104848
  const isActive = supportedButNeverSeen || statusType !== null && !isTerminal;
104641
104849
  const { messages, error: messagesError } = await getSessionMessages(client, sessionID);
104642
104850
  const outcome = extractLatestAssistantOutcome(messages);
104643
- if (outcome.text && !isActive) {
104851
+ if (outcome.text && (!isActive || supportedButNeverSeen)) {
104644
104852
  return { messages, outcome, statusType };
104645
104853
  }
104646
- if (outcome.errorName && !isActive) {
104854
+ if (outcome.errorName && (!isActive || supportedButNeverSeen)) {
104647
104855
  return { messages, outcome, statusType };
104648
104856
  }
104649
104857
  if (isActive) {
@@ -104719,7 +104927,7 @@ Original error: ${createResult.error}`;
104719
104927
  const sessionID = createResult.data.id;
104720
104928
  log(`[look_at] Created session: ${sessionID}`);
104721
104929
  log(`[look_at] Sending prompt with ${isBase64Input ? "base64 image" : "file"} to session ${sessionID}`);
104722
- let promptFailed = false;
104930
+ let shouldWaitForStatus = true;
104723
104931
  try {
104724
104932
  await promptSyncWithModelSuggestionRetry(ctx.client, {
104725
104933
  path: { id: sessionID },
@@ -104742,15 +104950,14 @@ Original error: ${createResult.error}`;
104742
104950
  queueBehavior: "defer"
104743
104951
  });
104744
104952
  } catch (promptError) {
104745
- promptFailed = true;
104746
- log("[look_at] Prompt error (ignored, will still fetch messages):", promptError);
104953
+ log("[look_at] Prompt dispatch failed; checking child session evidence:", promptError);
104954
+ shouldWaitForStatus = isAmbiguousPromptDispatchFailure(promptError);
104747
104955
  }
104748
104956
  let observedMessages;
104749
104957
  let observedText;
104750
- if (typeof ctx.client.session.status === "function") {
104958
+ if (shouldWaitForStatus && typeof ctx.client.session.status === "function") {
104751
104959
  const waitResult = await waitForLookAtSessionResult(ctx.client, sessionID, {
104752
- allowStableIdleWithoutActivity: true,
104753
- allowEmptyStableIdleWithoutActivity: promptFailed
104960
+ allowStableIdleWithoutActivity: true
104754
104961
  });
104755
104962
  observedText = waitResult.outcome.text ?? undefined;
104756
104963
  if (observedText) {
@@ -106157,6 +106364,9 @@ async function executeBackgroundTask(args, ctx, executorCtx, parentContext, agen
106157
106364
 
106158
106365
  Task ID: ${task.id}`;
106159
106366
  }
106367
+ if (!sessionId && updatedTask?.sessionId) {
106368
+ sessionId = updatedTask.sessionId;
106369
+ }
106160
106370
  if (sessionId) {
106161
106371
  registerBackgroundSessionContext({
106162
106372
  sessionId,
@@ -110502,6 +110712,42 @@ function detectRepetitiveToolUse(window) {
110502
110712
 
110503
110713
  // src/features/background-agent/parent-wake-notifier.ts
110504
110714
  init_shared();
110715
+
110716
+ // src/features/background-agent/parent-wake-dedupe.ts
110717
+ function resolveParentWakePromptContext(promptContext) {
110718
+ const resolvedAgent = resolveRegisteredAgentName(promptContext.agent);
110719
+ return {
110720
+ ...promptContext,
110721
+ ...resolvedAgent ? { agent: resolvedAgent } : {},
110722
+ ...promptContext.model ? { model: { ...promptContext.model } } : {},
110723
+ ...promptContext.tools ? { tools: { ...promptContext.tools } } : {}
110724
+ };
110725
+ }
110726
+ function cloneParentWake(wake) {
110727
+ const promptContext = resolveParentWakePromptContext(wake.promptContext);
110728
+ return {
110729
+ promptContext,
110730
+ notifications: [...wake.notifications],
110731
+ shouldReply: wake.shouldReply,
110732
+ ...wake.dispatchedAt !== undefined ? { dispatchedAt: wake.dispatchedAt } : {},
110733
+ ...wake.toolCallDeferralStartedAt !== undefined ? { toolCallDeferralStartedAt: wake.toolCallDeferralStartedAt } : {}
110734
+ };
110735
+ }
110736
+ function isRedundantParentWake(latestWake, dispatchedWake) {
110737
+ return parentWakePromptContextMatches(latestWake, dispatchedWake) && parentWakeReplyModeIsCovered(latestWake, dispatchedWake) && parentWakeNotificationsAreCovered(latestWake, dispatchedWake);
110738
+ }
110739
+ function parentWakePromptContextMatches(left, right) {
110740
+ return JSON.stringify(left.promptContext) === JSON.stringify(right.promptContext);
110741
+ }
110742
+ function parentWakeReplyModeIsCovered(latestWake, dispatchedWake) {
110743
+ return !latestWake.shouldReply || dispatchedWake.shouldReply;
110744
+ }
110745
+ function parentWakeNotificationsAreCovered(latestWake, dispatchedWake) {
110746
+ const dispatchedNotifications = new Set(dispatchedWake.notifications);
110747
+ return latestWake.notifications.every((notification2) => dispatchedNotifications.has(notification2));
110748
+ }
110749
+
110750
+ // src/features/background-agent/parent-wake-notifier.ts
110505
110751
  function unrefTimerHandle(handle) {
110506
110752
  const maybeUnref = handle.unref;
110507
110753
  if (typeof maybeUnref === "function") {
@@ -110539,7 +110785,7 @@ class ParentWakeNotifier {
110539
110785
  this.recentParentSessionActivity.set(sessionID, Date.now());
110540
110786
  }
110541
110787
  queuePendingParentWake(sessionID, notification2, promptContext, shouldReply, delayMs) {
110542
- const resolvedPromptContext = this.resolveParentWakePromptContext(promptContext);
110788
+ const resolvedPromptContext = resolveParentWakePromptContext(promptContext);
110543
110789
  const pendingWake = this.pendingParentWakes.get(sessionID);
110544
110790
  if (pendingWake) {
110545
110791
  pendingWake.notifications.push(notification2);
@@ -110592,6 +110838,12 @@ class ParentWakeNotifier {
110592
110838
  });
110593
110839
  return;
110594
110840
  }
110841
+ const dispatchedWake = this.dispatchedParentWakes.get(sessionID);
110842
+ if (dispatchedWake && isRedundantParentWake(latestWake, dispatchedWake)) {
110843
+ this.pendingParentWakes.delete(sessionID);
110844
+ log("[background-agent] Suppressed duplicate parent wake already dispatched:", { sessionID });
110845
+ return;
110846
+ }
110595
110847
  this.pendingParentWakes.delete(sessionID);
110596
110848
  const notificationContent = latestWake.notifications.join(`
110597
110849
 
@@ -110619,9 +110871,9 @@ class ParentWakeNotifier {
110619
110871
  });
110620
110872
  if (promptResult.status === "failed") {
110621
110873
  if (isAmbiguousPostDispatchPromptFailure(promptResult)) {
110622
- const dispatchedWake = this.cloneParentWake(latestWake);
110623
- dispatchedWake.dispatchedAt = dispatchStartedAt;
110624
- if (await this.hasAcceptedMessageAfterDispatchedParentWake(sessionID, dispatchedWake)) {
110874
+ const dispatchedWake2 = cloneParentWake(latestWake);
110875
+ dispatchedWake2.dispatchedAt = dispatchStartedAt;
110876
+ if (await this.hasAcceptedMessageAfterDispatchedParentWake(sessionID, dispatchedWake2)) {
110625
110877
  this.trackDispatchedParentWake(sessionID, latestWake, dispatchStartedAt);
110626
110878
  log("[background-agent] Treated failed parent wake prompt as accepted after observing session history:", {
110627
110879
  sessionID,
@@ -110633,6 +110885,11 @@ class ParentWakeNotifier {
110633
110885
  throw promptResult.error;
110634
110886
  }
110635
110887
  if (promptResult.status === "reserved" && promptResult.reservedBy === "background-agent-parent-wake") {
110888
+ const dispatchedWake2 = this.dispatchedParentWakes.get(sessionID);
110889
+ if (dispatchedWake2 && isRedundantParentWake(latestWake, dispatchedWake2)) {
110890
+ log("[background-agent] Suppressed duplicate parent wake during promptAsync gate hold:", { sessionID });
110891
+ return;
110892
+ }
110636
110893
  this.requeueWake(sessionID, latestWake);
110637
110894
  this.schedulePendingParentWakeFlush(sessionID, 2000);
110638
110895
  log("[background-agent] Requeued parent wake flush reserved by promptAsync gate hold:", { sessionID });
@@ -110738,28 +110995,9 @@ class ParentWakeNotifier {
110738
110995
  this.recentParentSessionActivity.delete(sessionID);
110739
110996
  return false;
110740
110997
  }
110741
- resolveParentWakePromptContext(promptContext) {
110742
- const resolvedAgent = resolveRegisteredAgentName(promptContext.agent);
110743
- return {
110744
- ...promptContext,
110745
- ...resolvedAgent ? { agent: resolvedAgent } : {},
110746
- ...promptContext.model ? { model: { ...promptContext.model } } : {},
110747
- ...promptContext.tools ? { tools: { ...promptContext.tools } } : {}
110748
- };
110749
- }
110750
- cloneParentWake(wake) {
110751
- const promptContext = this.resolveParentWakePromptContext(wake.promptContext);
110752
- return {
110753
- promptContext,
110754
- notifications: [...wake.notifications],
110755
- shouldReply: wake.shouldReply,
110756
- ...wake.dispatchedAt !== undefined ? { dispatchedAt: wake.dispatchedAt } : {},
110757
- ...wake.toolCallDeferralStartedAt !== undefined ? { toolCallDeferralStartedAt: wake.toolCallDeferralStartedAt } : {}
110758
- };
110759
- }
110760
110998
  trackDispatchedParentWake(sessionID, wake, dispatchedAt) {
110761
110999
  this.clearDispatchedParentWake(sessionID);
110762
- const dispatchedWake = this.cloneParentWake(wake);
111000
+ const dispatchedWake = cloneParentWake(wake);
110763
111001
  dispatchedWake.dispatchedAt = dispatchedAt;
110764
111002
  this.dispatchedParentWakes.set(sessionID, dispatchedWake);
110765
111003
  const timer = setTimeout(() => {
@@ -110937,7 +111175,7 @@ class ParentWakeNotifier {
110937
111175
  pendingWake.toolCallDeferralStartedAt ??= latestWake.toolCallDeferralStartedAt;
110938
111176
  return;
110939
111177
  }
110940
- this.pendingParentWakes.set(sessionID, this.cloneParentWake(latestWake));
111178
+ this.pendingParentWakes.set(sessionID, cloneParentWake(latestWake));
110941
111179
  }
110942
111180
  }
110943
111181
 
@@ -110993,9 +111231,13 @@ function describeProcessCleanupError(error) {
110993
111231
  return { raw: String(error) };
110994
111232
  }
110995
111233
  function registerErrorEvent(signal) {
111234
+ let logging = false;
110996
111235
  const listener = (error) => {
110997
- process.off(signal, listener);
111236
+ if (logging)
111237
+ return;
111238
+ logging = true;
110998
111239
  log(`[background-agent] ${signal} observed; keeping host alive and skipping cleanup (signal handlers run on real shutdown)`, describeProcessCleanupError(error));
111240
+ logging = false;
110999
111241
  };
111000
111242
  process.on(signal, listener);
111001
111243
  return listener;
@@ -132156,13 +132398,16 @@ function maybeCreateAtlasConfig(input) {
132156
132398
  return;
132157
132399
  const orchestratorOverride = agentOverrides["atlas"];
132158
132400
  const atlasRequirement = AGENT_MODEL_REQUIREMENTS["atlas"];
132159
- const atlasResolution = applyModelResolution({
132401
+ let atlasResolution = applyModelResolution({
132160
132402
  uiSelectedModel: orchestratorOverride?.model !== undefined ? undefined : uiSelectedModel,
132161
132403
  userModel: orchestratorOverride?.model,
132162
132404
  requirement: atlasRequirement,
132163
132405
  availableModels,
132164
132406
  systemDefaultModel
132165
132407
  });
132408
+ if (!atlasResolution && orchestratorOverride?.model) {
132409
+ atlasResolution = { model: orchestratorOverride.model, provenance: "override" };
132410
+ }
132166
132411
  if (!atlasResolution)
132167
132412
  return;
132168
132413
  const { model: atlasModel, variant: atlasResolvedVariant } = atlasResolution;
@@ -135833,6 +136078,14 @@ function supportsImageInput(modelConfig) {
135833
136078
  }
135834
136079
  return modelConfig?.capabilities?.input?.image === true;
135835
136080
  }
136081
+ function parseTrustedModel(modelString) {
136082
+ const [providerID, ...modelIDParts] = modelString.split("/");
136083
+ const modelID = modelIDParts.join("/");
136084
+ if (!providerID || modelID.length === 0) {
136085
+ return;
136086
+ }
136087
+ return { providerID, modelID };
136088
+ }
135836
136089
  function applyProviderConfig(params) {
135837
136090
  const providers = params.config.provider;
135838
136091
  const modelContextLimitsCache = params.modelCacheState.modelContextLimitsCache;
@@ -135843,22 +136096,31 @@ function applyProviderConfig(params) {
135843
136096
  params.modelCacheState.visionCapableModelsCache = visionCapableModelsCache2;
135844
136097
  visionCapableModelsCache2.clear();
135845
136098
  setVisionCapableModelsCache(visionCapableModelsCache2);
135846
- if (!providers)
135847
- return;
135848
- for (const [providerID, providerConfig] of Object.entries(providers)) {
135849
- const models = providerConfig?.models;
135850
- if (!models)
135851
- continue;
135852
- for (const [modelID, modelConfig] of Object.entries(models)) {
135853
- if (supportsImageInput(modelConfig)) {
135854
- visionCapableModelsCache2.set(`${providerID}/${modelID}`, { providerID, modelID });
135855
- }
135856
- const contextLimit = modelConfig?.limit?.context;
135857
- if (!contextLimit)
136099
+ if (providers) {
136100
+ for (const [providerID, providerConfig] of Object.entries(providers)) {
136101
+ const models = providerConfig?.models;
136102
+ if (!models)
135858
136103
  continue;
135859
- modelContextLimitsCache.set(`${providerID}/${modelID}`, contextLimit);
136104
+ for (const [modelID, modelConfig] of Object.entries(models)) {
136105
+ if (supportsImageInput(modelConfig)) {
136106
+ visionCapableModelsCache2.set(`${providerID}/${modelID}`, { providerID, modelID });
136107
+ }
136108
+ const contextLimit = modelConfig?.limit?.context;
136109
+ if (!contextLimit)
136110
+ continue;
136111
+ modelContextLimitsCache.set(`${providerID}/${modelID}`, contextLimit);
136112
+ }
135860
136113
  }
135861
136114
  }
136115
+ for (const trustedModelString of params.trustedVisionCapableModels ?? []) {
136116
+ const trustedModel = parseTrustedModel(trustedModelString);
136117
+ if (!trustedModel)
136118
+ continue;
136119
+ const key = `${trustedModel.providerID}/${trustedModel.modelID}`;
136120
+ if (visionCapableModelsCache2.has(key))
136121
+ continue;
136122
+ visionCapableModelsCache2.set(key, trustedModel);
136123
+ }
135862
136124
  }
135863
136125
 
135864
136126
  // src/plugin-handlers/plugin-components-loader.ts
@@ -136020,12 +136282,25 @@ function applyToolConfig(params) {
136020
136282
  }
136021
136283
 
136022
136284
  // src/plugin-handlers/config-handler.ts
136285
+ function collectTrustedVisionCapableModels(pluginConfig) {
136286
+ const trusted = [];
136287
+ const multimodalLookerOverride = pluginConfig.agents?.["multimodal-looker"];
136288
+ const configuredModel = multimodalLookerOverride?.model;
136289
+ if (typeof configuredModel === "string" && configuredModel.includes("/")) {
136290
+ trusted.push(configuredModel);
136291
+ }
136292
+ return trusted;
136293
+ }
136023
136294
  function createConfigHandler(deps) {
136024
136295
  const { ctx, pluginConfig, modelCacheState } = deps;
136025
136296
  return async (config) => {
136026
136297
  const formatterConfig = config.formatter;
136027
136298
  setAdditionalAllowedMcpEnvVars(pluginConfig.mcp_env_allowlist ?? []);
136028
- applyProviderConfig({ config, modelCacheState });
136299
+ applyProviderConfig({
136300
+ config,
136301
+ modelCacheState,
136302
+ trustedVisionCapableModels: collectTrustedVisionCapableModels(pluginConfig)
136303
+ });
136029
136304
  clearFormatterCache();
136030
136305
  const pluginComponents = await loadPluginComponents({ pluginConfig });
136031
136306
  applyHookConfig({ pluginComponents, ctx });
@@ -139870,7 +140145,7 @@ function createEventHandler2(args) {
139870
140145
  const recentSyntheticIdles = new Map;
139871
140146
  const recentRealIdles = new Map;
139872
140147
  const recentAnyIdles = new Map;
139873
- const DEDUP_WINDOW_MS = 500;
140148
+ const DEDUP_WINDOW_MS2 = 500;
139874
140149
  const teamModeConfig = pluginConfig.team_mode?.enabled ? pluginConfig.team_mode : undefined;
139875
140150
  const teamLeadOrphanHandler = teamModeConfig ? createTeamLeadOrphanHandler(teamModeConfig, managers.tmuxSessionManager, managers.backgroundManager) : undefined;
139876
140151
  const teamMemberErrorHandler = teamModeConfig ? createTeamMemberErrorHandler(teamModeConfig, { client: pluginContext.client }) : undefined;
@@ -139896,7 +140171,7 @@ function createEventHandler2(args) {
139896
140171
  };
139897
140172
  const shouldDispatchIdleEvent = (sessionID, now) => {
139898
140173
  const lastDispatchedAt = recentAnyIdles.get(sessionID);
139899
- if (lastDispatchedAt !== undefined && now - lastDispatchedAt < DEDUP_WINDOW_MS) {
140174
+ if (lastDispatchedAt !== undefined && now - lastDispatchedAt < DEDUP_WINDOW_MS2) {
139900
140175
  return false;
139901
140176
  }
139902
140177
  recentAnyIdles.set(sessionID, now);
@@ -140059,7 +140334,7 @@ function createEventHandler2(args) {
140059
140334
  recentRealIdles,
140060
140335
  recentAnyIdles,
140061
140336
  now: Date.now(),
140062
- dedupWindowMs: DEDUP_WINDOW_MS
140337
+ dedupWindowMs: DEDUP_WINDOW_MS2
140063
140338
  });
140064
140339
  const syntheticIdle = normalizeSessionStatusToIdle(input);
140065
140340
  if (input.event.type === "session.idle") {
@@ -140067,7 +140342,7 @@ function createEventHandler2(args) {
140067
140342
  if (sessionID) {
140068
140343
  const now = Date.now();
140069
140344
  const emittedAt = recentSyntheticIdles.get(sessionID);
140070
- if (emittedAt !== undefined && now - emittedAt < DEDUP_WINDOW_MS) {
140345
+ if (emittedAt !== undefined && now - emittedAt < DEDUP_WINDOW_MS2) {
140071
140346
  recentSyntheticIdles.delete(sessionID);
140072
140347
  const lastAnyIdleAt = recentAnyIdles.get(sessionID);
140073
140348
  if (lastAnyIdleAt === emittedAt) {
@@ -140097,7 +140372,7 @@ function createEventHandler2(args) {
140097
140372
  const sessionID = syntheticIdle.event.properties?.sessionID;
140098
140373
  const now = Date.now();
140099
140374
  const emittedAt = recentRealIdles.get(sessionID);
140100
- if (emittedAt !== undefined && now - emittedAt < DEDUP_WINDOW_MS) {
140375
+ if (emittedAt !== undefined && now - emittedAt < DEDUP_WINDOW_MS2) {
140101
140376
  recentRealIdles.delete(sessionID);
140102
140377
  return;
140103
140378
  }