@cortexkit/aft-opencode 0.54.0 → 0.55.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
@@ -8842,6 +8842,21 @@ class BinaryBridge {
8842
8842
  }
8843
8843
  return this.sendWithVersionMismatchRetry(command, dispatchParams, options, true);
8844
8844
  }
8845
+ listenForRequestAbort(requestId, signal) {
8846
+ if (!signal)
8847
+ return () => {};
8848
+ let fired = false;
8849
+ const onAbort = () => {
8850
+ if (fired)
8851
+ return;
8852
+ fired = true;
8853
+ this.send("cancel_request", { id: requestId }, { keepBridgeOnTimeout: true }).catch(() => {});
8854
+ };
8855
+ signal.addEventListener("abort", onAbort, { once: true });
8856
+ if (signal.aborted)
8857
+ onAbort();
8858
+ return () => signal.removeEventListener("abort", onAbort);
8859
+ }
8845
8860
  async toolCall(sessionId, name, rawArgs = {}, options) {
8846
8861
  const params = { name, arguments: rawArgs };
8847
8862
  if (sessionId)
@@ -8864,7 +8879,7 @@ class BinaryBridge {
8864
8879
  if (this._shuttingDown) {
8865
8880
  throw new BridgeTransportUnavailableError(`${this.errorPrefix} Bridge is shutting down, cannot send "${command}"`);
8866
8881
  }
8867
- if (Object.hasOwn(params, "id")) {
8882
+ if (Object.hasOwn(params, "id") && command !== "cancel_request") {
8868
8883
  throw new Error("params cannot contain reserved key 'id'");
8869
8884
  }
8870
8885
  const requestSessionId = typeof params.session_id === "string" && params.session_id.length > 0 ? params.session_id : undefined;
@@ -8918,7 +8933,7 @@ class BinaryBridge {
8918
8933
  }
8919
8934
  const id = String(this.nextId++);
8920
8935
  let request;
8921
- if (Object.hasOwn(params, "command") || Object.hasOwn(params, "method")) {
8936
+ if (command === "cancel_request" || Object.hasOwn(params, "command") || Object.hasOwn(params, "method")) {
8922
8937
  const nested = { ...params };
8923
8938
  const reserved = {};
8924
8939
  for (const key of ["session_id", "lsp_hints"]) {
@@ -8938,11 +8953,12 @@ class BinaryBridge {
8938
8953
  const child = this.process;
8939
8954
  const response = await new Promise((resolve3, reject) => {
8940
8955
  const timer = setTimeout(() => {
8941
- const entry = this.pending.get(id);
8942
- if (!entry)
8956
+ const entry2 = this.pending.get(id);
8957
+ if (!entry2)
8943
8958
  return;
8944
8959
  this.pending.delete(id);
8945
- clearTimeout(entry.timer);
8960
+ clearTimeout(entry2.timer);
8961
+ entry2.onSettled?.();
8946
8962
  if (keepBridgeOnTimeout) {
8947
8963
  const timeoutMsg2 = `Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`;
8948
8964
  if (requestSessionId) {
@@ -8950,7 +8966,7 @@ class BinaryBridge {
8950
8966
  } else {
8951
8967
  this.warnVia(timeoutMsg2);
8952
8968
  }
8953
- entry.reject(new BridgeTransportTimeoutError(command, effectiveTimeoutMs, `${this.errorPrefix} Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
8969
+ entry2.reject(new BridgeTransportTimeoutError(command, effectiveTimeoutMs, `${this.errorPrefix} Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
8954
8970
  return;
8955
8971
  }
8956
8972
  const childActiveSinceRequest = this.lastChildActivityAt > requestSentAt;
@@ -8965,13 +8981,20 @@ class BinaryBridge {
8965
8981
  this.warnVia(timeoutMsg);
8966
8982
  }
8967
8983
  if (keepWarm) {
8968
- entry.reject(new Error(`${this.errorPrefix} request "${command}" timed out after ${effectiveTimeoutMs}ms (bridge busy/under load); bridge kept warm — retry`));
8984
+ entry2.reject(new Error(`${this.errorPrefix} request "${command}" timed out after ${effectiveTimeoutMs}ms (bridge busy/under load); bridge kept warm — retry`));
8969
8985
  return;
8970
8986
  }
8971
- entry.reject(new Error(`${this.errorPrefix} Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
8987
+ entry2.reject(new Error(`${this.errorPrefix} Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
8972
8988
  this.handleTimeout(requestSessionId);
8973
8989
  }, effectiveTimeoutMs);
8974
- this.pending.set(id, { resolve: resolve3, reject, timer, onProgress: options?.onProgress, command });
8990
+ const entry = {
8991
+ resolve: resolve3,
8992
+ reject,
8993
+ timer,
8994
+ onProgress: options?.onProgress,
8995
+ command
8996
+ };
8997
+ this.pending.set(id, entry);
8975
8998
  if (!child?.stdin?.writable) {
8976
8999
  this.pending.delete(id);
8977
9000
  clearTimeout(timer);
@@ -8985,11 +9008,12 @@ class BinaryBridge {
8985
9008
  const handleWriteFailure = (cause) => {
8986
9009
  const writeError = cause instanceof Error ? cause : new Error(String(cause));
8987
9010
  const error2 = new BridgeTransportUnknownOutcomeError(`${this.errorPrefix} Failed to write to stdin: ${writeError.message}`, { cause: writeError });
8988
- const entry = this.pending.get(id);
8989
- if (entry) {
9011
+ const entry2 = this.pending.get(id);
9012
+ if (entry2) {
8990
9013
  this.pending.delete(id);
8991
- clearTimeout(entry.timer);
8992
- entry.reject(error2);
9014
+ clearTimeout(entry2.timer);
9015
+ entry2.onSettled?.();
9016
+ entry2.reject(error2);
8993
9017
  }
8994
9018
  if (this.process === child)
8995
9019
  this.invalidateTransportProcess(error2);
@@ -9002,6 +9026,9 @@ class BinaryBridge {
9002
9026
  } catch (err) {
9003
9027
  handleWriteFailure(err);
9004
9028
  }
9029
+ if (this.pending.has(id)) {
9030
+ entry.onSettled = this.listenForRequestAbort(id, options?.abortSignal);
9031
+ }
9005
9032
  });
9006
9033
  if (command === "configure" && response.success === true && options?.markConfiguredOnSuccess !== false) {
9007
9034
  this.configured = true;
@@ -9370,6 +9397,7 @@ class BinaryBridge {
9370
9397
  if (requestId && entry) {
9371
9398
  this.pending.delete(requestId);
9372
9399
  clearTimeout(entry.timer);
9400
+ entry.onSettled?.();
9373
9401
  entry.resolve({
9374
9402
  success: false,
9375
9403
  code: "permission_required",
@@ -9411,6 +9439,7 @@ class BinaryBridge {
9411
9439
  return;
9412
9440
  this.pending.delete(id);
9413
9441
  clearTimeout(entry.timer);
9442
+ entry.onSettled?.();
9414
9443
  this.consecutiveRequestTimeouts = 0;
9415
9444
  this.scheduleRestartCountReset();
9416
9445
  this.accountForBashTaskResponse(entry.command, response);
@@ -9501,6 +9530,7 @@ class BinaryBridge {
9501
9530
  rejectAllPending(error2) {
9502
9531
  for (const [_id, entry] of this.pending) {
9503
9532
  clearTimeout(entry.timer);
9533
+ entry.onSettled?.();
9504
9534
  entry.reject(error2);
9505
9535
  }
9506
9536
  this.pending.clear();
@@ -9525,6 +9555,12 @@ import { join as join5 } from "node:path";
9525
9555
  function homeDir2(env) {
9526
9556
  return (process.platform === "win32" ? env.USERPROFILE || env.HOME : env.HOME) || homedir4();
9527
9557
  }
9558
+ function getOpenCodeCacheRoot(env = process.env, home = homedir4()) {
9559
+ return join5(env.XDG_CACHE_HOME || join5(home, ".cache"), "opencode");
9560
+ }
9561
+ function getOpenCodeConfigRoot(env = process.env, home = homedir4()) {
9562
+ return join5(env.XDG_CONFIG_HOME || join5(home, ".config"), "opencode");
9563
+ }
9528
9564
  function getAftCacheRoot(env = process.env) {
9529
9565
  if (env.AFT_CACHE_DIR)
9530
9566
  return env.AFT_CACHE_DIR;
@@ -14284,6 +14320,7 @@ async function ensureStorageMigrated(opts) {
14284
14320
  throw new Error(`AFT storage migration failed (${detail}). ` + `Harness: ${opts.harness}. Legacy: ${legacyRoot}. Target: ${newRoot}. ` + `See log: ${logPath}. ` + `Plugin load aborted to prevent legacy/new state divergence.` + (stderrTail ? ` Stderr tail: ${stderrTail}` : "") + (stdoutTail ? ` Stdout tail: ${stdoutTail}` : ""));
14285
14321
  }
14286
14322
  // ../aft-bridge/dist/npm-resolver.js
14323
+ import { spawn as spawn3, spawnSync as spawnSync3 } from "node:child_process";
14287
14324
  import { readdirSync as readdirSync2, statSync as statSync5 } from "node:fs";
14288
14325
  import { homedir as homedir8 } from "node:os";
14289
14326
  import { delimiter as delimiter2, dirname as dirname4, isAbsolute as isAbsolute3, join as join10 } from "node:path";
@@ -14390,6 +14427,189 @@ function resolveNpm(deps = defaultDeps()) {
14390
14427
  }
14391
14428
  return null;
14392
14429
  }
14430
+ function quoteCmdArgument(value) {
14431
+ if (/[\0\r\n"%]/.test(value)) {
14432
+ throw new Error(`npm argument cannot be represented safely for cmd.exe: ${JSON.stringify(value)}`);
14433
+ }
14434
+ return `"${value}"`;
14435
+ }
14436
+ function npmInvocation(resolved, npmArgs, platform = process.platform, env = process.env) {
14437
+ if (platform !== "win32" || !/\.(?:cmd|bat)$/i.test(resolved.command)) {
14438
+ return { command: resolved.command, args: [...npmArgs] };
14439
+ }
14440
+ if (/[\0\r\n"]/.test(resolved.command)) {
14441
+ throw new Error(`npm command cannot be represented safely for cmd.exe: ${JSON.stringify(resolved.command)}`);
14442
+ }
14443
+ const commandEnvName = "AFT_NPM_COMMAND";
14444
+ const quotedArgs = npmArgs.map(quoteCmdArgument);
14445
+ const commandLine = `""%${commandEnvName}%"${quotedArgs.length > 0 ? ` ${quotedArgs.join(" ")}` : ""}"`;
14446
+ return {
14447
+ command: env.ComSpec ?? env.COMSPEC ?? "cmd.exe",
14448
+ args: ["/d", "/s", "/v:off", "/c", commandLine],
14449
+ env: { [commandEnvName]: resolved.command },
14450
+ windowsVerbatimArguments: true,
14451
+ windowsCmdShim: true
14452
+ };
14453
+ }
14454
+
14455
+ class NpmTerminationUnknownError extends Error {
14456
+ code = "npm_termination_unknown";
14457
+ constructor(detail) {
14458
+ super(`npm process-tree termination could not be confirmed: ${detail}`);
14459
+ this.name = "NpmTerminationUnknownError";
14460
+ }
14461
+ }
14462
+ function terminateDirectNpmChild(child, gracePeriodMs) {
14463
+ if (child.exitCode !== null || child.signalCode !== null)
14464
+ return Promise.resolve();
14465
+ return new Promise((resolve7, reject) => {
14466
+ let settled = false;
14467
+ let forceTimer = null;
14468
+ let confirmationTimer = null;
14469
+ let signalFailure = null;
14470
+ const cleanup = () => {
14471
+ if (forceTimer)
14472
+ clearTimeout(forceTimer);
14473
+ if (confirmationTimer)
14474
+ clearTimeout(confirmationTimer);
14475
+ child.removeListener("exit", finish);
14476
+ child.removeListener("error", onChildError);
14477
+ };
14478
+ const finish = () => {
14479
+ if (settled)
14480
+ return;
14481
+ settled = true;
14482
+ cleanup();
14483
+ resolve7();
14484
+ };
14485
+ const fail = () => {
14486
+ if (settled)
14487
+ return;
14488
+ if (child.exitCode !== null || child.signalCode !== null) {
14489
+ finish();
14490
+ return;
14491
+ }
14492
+ settled = true;
14493
+ cleanup();
14494
+ reject(new NpmTerminationUnknownError(signalFailure ?? `direct npm child did not exit after SIGKILL within ${gracePeriodMs}ms`));
14495
+ };
14496
+ const onChildError = (error2) => {
14497
+ signalFailure = `direct npm child error during termination: ${String(error2)}`;
14498
+ };
14499
+ const signal = (value) => {
14500
+ try {
14501
+ if (!child.kill(value)) {
14502
+ signalFailure = `direct npm child rejected ${value ?? "SIGTERM"}`;
14503
+ }
14504
+ } catch (error2) {
14505
+ signalFailure = `direct npm child ${value ?? "SIGTERM"} failed: ${String(error2)}`;
14506
+ }
14507
+ };
14508
+ child.once("exit", finish);
14509
+ child.on("error", onChildError);
14510
+ forceTimer = setTimeout(() => {
14511
+ signal("SIGKILL");
14512
+ confirmationTimer = setTimeout(fail, gracePeriodMs);
14513
+ }, gracePeriodMs);
14514
+ signal();
14515
+ if (child.exitCode !== null || child.signalCode !== null)
14516
+ finish();
14517
+ });
14518
+ }
14519
+ function terminateNpmProcessTree(child, invocation, env = process.env, gracePeriodMs = 5000) {
14520
+ if (!invocation.windowsCmdShim)
14521
+ return terminateDirectNpmChild(child, gracePeriodMs);
14522
+ const exitedSuccessfully = () => child.exitCode === 0 && child.signalCode === null;
14523
+ if (exitedSuccessfully())
14524
+ return Promise.resolve();
14525
+ if (child.pid === undefined) {
14526
+ return Promise.reject(new NpmTerminationUnknownError("cmd.exe child has no process ID"));
14527
+ }
14528
+ return new Promise((resolve7, reject) => {
14529
+ let settled = false;
14530
+ let childExited = child.exitCode !== null || child.signalCode !== null;
14531
+ let treeKillConfirmed = false;
14532
+ let treeKillFailure = null;
14533
+ let killer = null;
14534
+ const timeout = setTimeout(() => {
14535
+ try {
14536
+ killer?.kill();
14537
+ } catch {}
14538
+ if (treeKillConfirmed || exitedSuccessfully()) {
14539
+ settled = true;
14540
+ cleanup();
14541
+ resolve7();
14542
+ return;
14543
+ }
14544
+ fail(treeKillFailure ?? `taskkill.exe did not finish within ${gracePeriodMs}ms`);
14545
+ }, gracePeriodMs);
14546
+ const cleanup = () => {
14547
+ clearTimeout(timeout);
14548
+ child.removeListener("exit", onChildExit);
14549
+ };
14550
+ const succeedIfConfirmed = () => {
14551
+ if (settled || !childExited || !treeKillConfirmed)
14552
+ return;
14553
+ settled = true;
14554
+ cleanup();
14555
+ resolve7();
14556
+ };
14557
+ const fail = (detail) => {
14558
+ if (settled)
14559
+ return;
14560
+ settled = true;
14561
+ cleanup();
14562
+ reject(new NpmTerminationUnknownError(detail));
14563
+ };
14564
+ function onChildExit() {
14565
+ childExited = true;
14566
+ if (treeKillFailure !== null) {
14567
+ if (exitedSuccessfully()) {
14568
+ settled = true;
14569
+ cleanup();
14570
+ resolve7();
14571
+ } else {
14572
+ fail(treeKillFailure);
14573
+ }
14574
+ return;
14575
+ }
14576
+ succeedIfConfirmed();
14577
+ }
14578
+ const recordTreeKillFailure = (detail) => {
14579
+ if (settled)
14580
+ return;
14581
+ if (exitedSuccessfully()) {
14582
+ settled = true;
14583
+ cleanup();
14584
+ resolve7();
14585
+ return;
14586
+ }
14587
+ treeKillFailure = detail;
14588
+ if (childExited)
14589
+ fail(detail);
14590
+ };
14591
+ child.once("exit", onChildExit);
14592
+ const systemRoot = env.SystemRoot ?? env.SYSTEMROOT;
14593
+ const taskkill = systemRoot ? join10(systemRoot, "System32", "taskkill.exe") : "taskkill.exe";
14594
+ try {
14595
+ killer = spawn3(taskkill, ["/pid", String(child.pid), "/t", "/f"], {
14596
+ stdio: "ignore",
14597
+ windowsHide: true
14598
+ });
14599
+ killer.once("error", (error2) => recordTreeKillFailure(`taskkill.exe failed to start: ${String(error2)}`));
14600
+ killer.once("exit", (code) => {
14601
+ if (code !== 0) {
14602
+ recordTreeKillFailure(`taskkill.exe exited with code ${code ?? "unknown"}`);
14603
+ return;
14604
+ }
14605
+ treeKillConfirmed = true;
14606
+ succeedIfConfirmed();
14607
+ });
14608
+ } catch (error2) {
14609
+ recordTreeKillFailure(`taskkill.exe failed to start: ${String(error2)}`);
14610
+ }
14611
+ });
14612
+ }
14393
14613
  function npmSpawnEnv(resolved, baseEnv = process.env) {
14394
14614
  if (!resolved.binDir)
14395
14615
  return { ...baseEnv };
@@ -15416,10 +15636,13 @@ function isEditSentinelItem(item) {
15416
15636
  if (record.oldString === null && ["startLine", "endLine"].some((key) => hasOwn(record, key) && record[key] !== null)) {
15417
15637
  return false;
15418
15638
  }
15419
- const newStringEmpty = !hasOwn(record, "newString") || isNullOrEmptyString(record.newString);
15420
- if (!newStringEmpty)
15421
- return false;
15422
- return !hasOwn(record, "content") || isNullOrEmptyString(record.content);
15639
+ return (!hasOwn(record, "newString") || isNullOrEmptyString(record.newString)) && (!hasOwn(record, "content") || isNullOrEmptyString(record.content)) && (!hasOwn(record, "replaceAll") || record.replaceAll === null || record.replaceAll === false) && isDefaultOccurrence(record.occurrence);
15640
+ }
15641
+ function hasMeaningfulFindPayload(item) {
15642
+ return isNonEmptyString(item.oldString);
15643
+ }
15644
+ function isDefaultOccurrence(value) {
15645
+ return value === undefined || value === null || value === 1;
15423
15646
  }
15424
15647
  function normalizeEditArraySentinels(record) {
15425
15648
  const value = record.edits;
@@ -15472,8 +15695,8 @@ function parseEditArray(value) {
15472
15695
  }
15473
15696
  return value;
15474
15697
  }
15475
- function stripLineRangeSentinels(item) {
15476
- const hasRangeField = ["startLine", "endLine", "content"].some((key) => hasOwn(item, key));
15698
+ function normalizeEditItemSentinels(item) {
15699
+ const hadRangeFields = ["startLine", "endLine", "content"].some((key) => hasOwn(item, key));
15477
15700
  for (const key of [
15478
15701
  "oldString",
15479
15702
  "newString",
@@ -15486,7 +15709,19 @@ function stripLineRangeSentinels(item) {
15486
15709
  if (item[key] === null)
15487
15710
  delete item[key];
15488
15711
  }
15489
- if (!hasRangeField)
15712
+ const contentIsEmpty = item.content === "";
15713
+ if (hasMeaningfulFindPayload(item) && (!hasOwn(item, "content") || contentIsEmpty)) {
15714
+ for (const key of ["startLine", "endLine", "content"])
15715
+ delete item[key];
15716
+ if (hadRangeFields) {
15717
+ if (item.replaceAll === false)
15718
+ delete item.replaceAll;
15719
+ if (hasOwn(item, "occurrence") && isDefaultOccurrence(item.occurrence))
15720
+ delete item.occurrence;
15721
+ }
15722
+ return;
15723
+ }
15724
+ if (!isNonEmptyString(item.content))
15490
15725
  return;
15491
15726
  if (item.oldString === "")
15492
15727
  delete item.oldString;
@@ -15494,7 +15729,7 @@ function stripLineRangeSentinels(item) {
15494
15729
  delete item.newString;
15495
15730
  if (item.replaceAll === false)
15496
15731
  delete item.replaceAll;
15497
- if (item.occurrence === 1)
15732
+ if (hasOwn(item, "occurrence") && isDefaultOccurrence(item.occurrence))
15498
15733
  delete item.occurrence;
15499
15734
  }
15500
15735
  function normalizeEditItem(value, index) {
@@ -15505,7 +15740,7 @@ function normalizeEditItem(value, index) {
15505
15740
  const item = copyOwnProperties(source);
15506
15741
  normalizeItemAlias(item, "oldString", "oldText");
15507
15742
  normalizeItemAlias(item, "newString", "newText");
15508
- stripLineRangeSentinels(item);
15743
+ normalizeEditItemSentinels(item);
15509
15744
  const hasFindField = ["oldString", "newString", "replaceAll", "occurrence"].some((key) => hasOwn(item, key));
15510
15745
  const hasRangeField = ["startLine", "endLine", "content"].some((key) => hasOwn(item, key));
15511
15746
  if (hasFindField && hasRangeField) {
@@ -30707,6 +30942,9 @@ var GhShimConfigSchema = exports_external.object({
30707
30942
  enabled: exports_external.boolean().optional(),
30708
30943
  binary_path: exports_external.string().trim().refine(isAbsolute6, "gh_shim.binary_path must be absolute").optional()
30709
30944
  });
30945
+ var GhReadConfigSchema = exports_external.object({
30946
+ enabled: exports_external.boolean().optional()
30947
+ });
30710
30948
  var GitCoAuthorSchema = exports_external.string().trim().refine((value) => value === "off" || value === "auto" || /^[^<>\r\n]+\s+<[^<>\s@]+@[^<>\s@]+>$/.test(value), "git.co_author must be 'off', 'auto', or an explicit 'Name <email>' identity");
30711
30949
  var GitConfigSchema = exports_external.object({
30712
30950
  co_author: GitCoAuthorSchema.optional()
@@ -30768,6 +31006,7 @@ var AftConfigFieldsSchema = exports_external.object({
30768
31006
  bridge: BridgeConfigSchema.optional(),
30769
31007
  subc: SubcConfigSchema.optional(),
30770
31008
  gh_shim: GhShimConfigSchema.optional(),
31009
+ gh_read: GhReadConfigSchema.optional(),
30771
31010
  git: GitConfigSchema.optional()
30772
31011
  });
30773
31012
  var HarnessOverrideSchema = exports_external.preprocess((value) => {
@@ -31246,6 +31485,8 @@ function getStrippedTopLevelKeys(override) {
31246
31485
  stripped.push("subc");
31247
31486
  if (override.gh_shim !== undefined)
31248
31487
  stripped.push("gh_shim");
31488
+ if (override.gh_read !== undefined)
31489
+ stripped.push("gh_read");
31249
31490
  if (override.disabled_tools?.includes("aft_safety"))
31250
31491
  stripped.push("disabled_tools.aft_safety");
31251
31492
  return stripped;
@@ -31463,6 +31704,8 @@ var UNKNOWN_COMPLETION_CAP = 32;
31463
31704
  var DEFAULT_SESSION_ID2 = "__default__";
31464
31705
  var LOG_PREFIX = "[aft-plugin] bg-notifications:";
31465
31706
  var SUBC_NUDGE_LOG_INTERVAL_MS = 60000;
31707
+ var DEFAULT_BG_HOP_TIMEOUT_MS = 15000;
31708
+ var bgHopTimeoutMs = DEFAULT_BG_HOP_TIMEOUT_MS;
31466
31709
  var subcNudgesInFlight = new Map;
31467
31710
  var subcNudgeLogState = new Map;
31468
31711
  function consumeBgCompletion(sessionID, taskId) {
@@ -31690,11 +31933,20 @@ function handleSubcBgEventsNudge(drainContext) {
31690
31933
  }
31691
31934
  async function handleSubcBgEventsNudgeOnce(drainContext) {
31692
31935
  bridgeForDrain(drainContext);
31693
- stateFor(drainContext.sessionID).wakeDeferredTaskIds.clear();
31936
+ const state = stateFor(drainContext.sessionID);
31937
+ state.wakeDeferredTaskIds.clear();
31938
+ rearmHardStoppedWake(state, drainContext);
31694
31939
  await triggerWakeIfPending(drainContext, false, true, true);
31695
31940
  }
31696
31941
  function logSubcNudgeLifecycle(drainContext, kind, message) {
31697
- const key = `${kind}\x00${drainContext.directory}\x00${drainContext.sessionID}`;
31942
+ logDeliveryHop(drainContext, kind, message, {
31943
+ event: "subc_bg_nudge_delivery",
31944
+ cause: kind
31945
+ });
31946
+ }
31947
+ function logDeliveryHop(drainContext, kind, message, data, level = "info") {
31948
+ const taskKey = typeof data.task_id === "string" ? data.task_id : Array.isArray(data.task_ids) ? data.task_ids.join(",") : "";
31949
+ const key = `${kind}\x00${taskKey}\x00${drainContext.directory}\x00${drainContext.sessionID}`;
31698
31950
  const now = Date.now();
31699
31951
  const state = subcNudgeLogState.get(key);
31700
31952
  if (state && now - state.lastEmittedAt < SUBC_NUDGE_LOG_INTERVAL_MS) {
@@ -31703,13 +31955,59 @@ function logSubcNudgeLifecycle(drainContext, kind, message) {
31703
31955
  }
31704
31956
  const suppressed = state?.suppressed ?? 0;
31705
31957
  subcNudgeLogState.set(key, { lastEmittedAt: now, suppressed: 0 });
31706
- sessionLog(drainContext.sessionID, `${LOG_PREFIX} ${message}`, {
31707
- event: "subc_bg_nudge_delivery",
31708
- cause: kind,
31958
+ const payload = {
31959
+ ...data,
31709
31960
  canonical_root: drainContext.directory,
31710
31961
  suppressed
31962
+ };
31963
+ if (level === "warn") {
31964
+ sessionWarn(drainContext.sessionID, `${LOG_PREFIX} ${message}`, payload);
31965
+ } else {
31966
+ sessionLog(drainContext.sessionID, `${LOG_PREFIX} ${message}`, payload);
31967
+ }
31968
+ }
31969
+ function logPerTaskDeliveryHop(drainContext, kind, message, taskIDs, data, level = "info") {
31970
+ for (const taskID of taskIDs) {
31971
+ logDeliveryHop(drainContext, kind, message, { ...data, task_id: taskID, task_ids: taskIDs }, level);
31972
+ }
31973
+ }
31974
+ function rearmHardStoppedWake(state, drainContext) {
31975
+ if (!state.wakeHardStopped)
31976
+ return;
31977
+ state.wakeHardStopped = false;
31978
+ state.wakeRetryAttempts = 0;
31979
+ state.retryDelayMs = null;
31980
+ logDeliveryHop(drainContext, "inject-rearmed", "delivery retry re-armed", {
31981
+ event: "bash_completion_inject_rearmed",
31982
+ task_ids: state.pendingCompletions.map((completion) => completion.task_id)
31711
31983
  });
31712
31984
  }
31985
+ async function withBgHopTimeout(operation, label) {
31986
+ let timer;
31987
+ try {
31988
+ return await Promise.race([
31989
+ operation,
31990
+ new Promise((_, reject) => {
31991
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${bgHopTimeoutMs}ms`)), bgHopTimeoutMs);
31992
+ timer.unref?.();
31993
+ })
31994
+ ]);
31995
+ } finally {
31996
+ if (timer)
31997
+ clearTimeout(timer);
31998
+ }
31999
+ }
32000
+ function promptAsyncFailure(response) {
32001
+ if (!response || typeof response !== "object" || Array.isArray(response))
32002
+ return null;
32003
+ const record2 = response;
32004
+ if (record2.error != null) {
32005
+ return typeof record2.error === "string" ? record2.error : JSON.stringify(record2.error);
32006
+ }
32007
+ if (record2.success === false)
32008
+ return String(record2.message ?? "promptAsync returned success=false");
32009
+ return null;
32010
+ }
31713
32011
  async function triggerWakeIfPending(drainContext, skipDrain, includeDeferredCompletions = true, forceDrain = false) {
31714
32012
  const state = stateFor(drainContext.sessionID);
31715
32013
  if (!skipDrain && (forceDrain || state.outstandingTaskIds.size > 0 || !state.forcedDrainCompleted)) {
@@ -31732,11 +32030,12 @@ async function triggerWakeIfPending(drainContext, skipDrain, includeDeferredComp
31732
32030
  }
31733
32031
  return drainContext.client;
31734
32032
  };
31735
- const sendPrompt = async (client2) => {
31736
- if (typeof client2.session?.promptAsync !== "function") {
32033
+ const sendPrompt = async (client) => {
32034
+ const promptAsync = client.session?.promptAsync;
32035
+ if (typeof promptAsync !== "function") {
31737
32036
  throw new Error("wake client.session.promptAsync is unavailable");
31738
32037
  }
31739
- const promptContext = await resolvePromptContext(client2, drainContext.sessionID);
32038
+ const promptContext = await resolvePromptContext(client, drainContext.sessionID);
31740
32039
  const body = {
31741
32040
  noReply: false,
31742
32041
  parts: [{ type: "text", text: reminder, synthetic: true }]
@@ -31751,52 +32050,73 @@ async function triggerWakeIfPending(drainContext, skipDrain, includeDeferredComp
31751
32050
  }
31752
32051
  if (promptContext?.variant)
31753
32052
  body.variant = promptContext.variant;
31754
- const deliveryID2 = `aftdel_${randomUUID2()}`;
31755
- const wakeMeta = {
31756
- delivery_id: deliveryID2,
31757
- attempt: state.wakeRetryAttempts + 1,
31758
- task_ids: taskIDs,
31759
- directory: drainContext.directory,
31760
- reminder_sha256: hashReminder(reminder),
31761
- reminder_chars: reminder.length,
31762
- prompt_context: promptContext ? {
31763
- agent: promptContext.agent,
31764
- model: promptContext.model ? {
31765
- providerID: promptContext.model.providerID,
31766
- modelID: promptContext.model.modelID
31767
- } : null,
31768
- variant: promptContext.variant ?? null
31769
- } : null
31770
- };
31771
- sessionLog(drainContext.sessionID, `${LOG_PREFIX} wake promptAsync start`, {
31772
- event: "bash_completion_wake_prompt_async_start",
31773
- ...wakeMeta
31774
- });
31775
- try {
31776
- await client2.session.promptAsync({
31777
- path: { id: drainContext.sessionID },
31778
- body
31779
- });
31780
- } catch (err) {
31781
- sessionWarn(drainContext.sessionID, `${LOG_PREFIX} wake promptAsync error`, {
31782
- event: "bash_completion_wake_prompt_async_error",
32053
+ const attemptPrompt = async (attemptBody, mode) => {
32054
+ const deliveryID2 = `aftdel_${randomUUID2()}`;
32055
+ const wakeMeta = {
31783
32056
  delivery_id: deliveryID2,
31784
32057
  attempt: state.wakeRetryAttempts + 1,
31785
32058
  task_ids: taskIDs,
31786
- error: err instanceof Error ? err.message : String(err)
32059
+ directory: drainContext.directory,
32060
+ reminder_sha256: hashReminder(reminder),
32061
+ reminder_chars: reminder.length,
32062
+ injection_mode: mode,
32063
+ prompt_context: promptContext ? {
32064
+ agent: promptContext.agent,
32065
+ model: promptContext.model ? {
32066
+ providerID: promptContext.model.providerID,
32067
+ modelID: promptContext.model.modelID
32068
+ } : null,
32069
+ variant: promptContext.variant ?? null
32070
+ } : null
32071
+ };
32072
+ logPerTaskDeliveryHop(drainContext, `inject-start-${mode}`, "session inject start", taskIDs, { event: "bash_completion_wake_prompt_async_start", ...wakeMeta });
32073
+ try {
32074
+ const response = await withBgHopTimeout(Promise.resolve(promptAsync({
32075
+ path: { id: drainContext.sessionID },
32076
+ body: attemptBody
32077
+ })), "session promptAsync injection");
32078
+ const failure = promptAsyncFailure(response);
32079
+ if (failure)
32080
+ throw new Error(failure);
32081
+ } catch (err) {
32082
+ logPerTaskDeliveryHop(drainContext, "inject-error", "session inject failed", taskIDs, {
32083
+ event: "bash_completion_wake_prompt_async_error",
32084
+ delivery_id: deliveryID2,
32085
+ attempt: state.wakeRetryAttempts + 1,
32086
+ injection_mode: mode,
32087
+ cause: err instanceof Error ? err.message : String(err)
32088
+ }, "warn");
32089
+ throw err;
32090
+ }
32091
+ logPerTaskDeliveryHop(drainContext, `inject-ok-${mode}`, "session inject ok", taskIDs, {
32092
+ event: "bash_completion_wake_prompt_async_ok",
32093
+ delivery_id: deliveryID2,
32094
+ attempt: state.wakeRetryAttempts + 1,
32095
+ injection_mode: mode
31787
32096
  });
31788
- throw err;
32097
+ return deliveryID2;
32098
+ };
32099
+ try {
32100
+ return await attemptPrompt(body, "mirrored-context");
32101
+ } catch (err) {
32102
+ if (!promptContext?.model)
32103
+ throw err;
32104
+ const fallbackBody = { ...body };
32105
+ delete fallbackBody.model;
32106
+ delete fallbackBody.variant;
32107
+ return attemptPrompt(fallbackBody, "model-free-fallback");
31789
32108
  }
31790
- sessionLog(drainContext.sessionID, `${LOG_PREFIX} wake promptAsync ok`, {
31791
- event: "bash_completion_wake_prompt_async_ok",
31792
- delivery_id: deliveryID2,
31793
- attempt: state.wakeRetryAttempts + 1,
31794
- task_ids: taskIDs
31795
- });
31796
- return deliveryID2;
31797
32109
  };
31798
- const client = getInProcessClient();
31799
- const deliveryID = await sendPrompt(client);
32110
+ let deliveryID;
32111
+ try {
32112
+ deliveryID = await sendPrompt(getInProcessClient());
32113
+ } catch (err) {
32114
+ logPerTaskDeliveryHop(drainContext, "inject-error", "session inject failed", taskIDs, {
32115
+ event: "bash_completion_wake_prompt_async_error",
32116
+ cause: err instanceof Error ? err.message : String(err)
32117
+ }, "warn");
32118
+ throw err;
32119
+ }
31800
32120
  markDeliveredAwaitingAck(state, taskIDs);
31801
32121
  const acked = await ackCompletions(drainContext, deliveredCompletions, deliveryID);
31802
32122
  if (acked)
@@ -31812,7 +32132,12 @@ async function triggerWakeIfPending(drainContext, skipDrain, includeDeferredComp
31812
32132
  });
31813
32133
  return;
31814
32134
  }
31815
- sessionWarn(drainContext.sessionID, hardStopped ? `${LOG_PREFIX} wake send failed ${MAX_WAKE_SEND_ATTEMPTS} times; stopping retries: ${err instanceof Error ? err.message : String(err)}` : `${LOG_PREFIX} wake send failed: ${err instanceof Error ? err.message : String(err)}`);
32135
+ if (hardStopped) {
32136
+ logDeliveryHop(drainContext, "inject-hard-stopped", "delivery retries paused", {
32137
+ event: "bash_completion_inject_hard_stopped",
32138
+ cause: err instanceof Error ? err.message : String(err)
32139
+ });
32140
+ }
31816
32141
  }, drainContext.sessionID, includeDeferredCompletions);
31817
32142
  }
31818
32143
  function formatSystemReminder(completions) {
@@ -31899,14 +32224,21 @@ async function drainCompletions(drainContext) {
31899
32224
  try {
31900
32225
  const bridge = bridgeForDrain(drainContext);
31901
32226
  const state = stateFor(drainContext.sessionID);
31902
- const response = await bridge.send("bash_drain_completions", {
31903
- session_id: drainContext.sessionID
31904
- });
32227
+ const response = await withBgHopTimeout(bridge.send("bash_drain_completions", { session_id: drainContext.sessionID }, { timeoutMs: bgHopTimeoutMs }), "bash_drain_completions");
31905
32228
  if (response.success === false) {
31906
- sessionWarn(drainContext.sessionID, `${LOG_PREFIX} drain failed: ${String(response.message ?? "unknown error")}`);
32229
+ logDeliveryHop(drainContext, "drain-error", "drain failed", {
32230
+ event: "bash_completion_drain_error",
32231
+ cause: String(response.message ?? "unknown error")
32232
+ }, "warn");
31907
32233
  return;
31908
32234
  }
31909
32235
  state.forcedDrainCompleted = true;
32236
+ const drainedTaskIds = Array.isArray(response.bg_completions) ? response.bg_completions.filter(isBgCompletion).map((completion) => completion.task_id) : [];
32237
+ logDeliveryHop(drainContext, "drain-ok", "drain ok", {
32238
+ event: "bash_completion_drain_ok",
32239
+ count: drainedTaskIds.length,
32240
+ task_ids: drainedTaskIds
32241
+ });
31910
32242
  const reackTaskIds = reconcileAwaitingAck(state, response.bg_completions);
31911
32243
  ingestDrainedBgCompletions(drainContext.sessionID, response.bg_completions);
31912
32244
  if (reackTaskIds.length > 0) {
@@ -31922,7 +32254,10 @@ async function drainCompletions(drainContext) {
31922
32254
  } catch (err) {
31923
32255
  if (drainContext.nudgeRef && isTerminalNudgeError(err))
31924
32256
  throw err;
31925
- sessionWarn(drainContext.sessionID, `${LOG_PREFIX} drain failed: ${err instanceof Error ? err.message : String(err)}`);
32257
+ logDeliveryHop(drainContext, "drain-error", "drain failed", {
32258
+ event: "bash_completion_drain_error",
32259
+ cause: err instanceof Error ? err.message : String(err)
32260
+ }, "warn");
31926
32261
  }
31927
32262
  }
31928
32263
  async function ackCompletions(drainContext, completions, deliveryID) {
@@ -31931,24 +32266,26 @@ async function ackCompletions(drainContext, completions, deliveryID) {
31931
32266
  return true;
31932
32267
  try {
31933
32268
  const bridge = bridgeForDrain(drainContext);
31934
- const response = await bridge.send("bash_ack_completions", {
31935
- session_id: drainContext.sessionID,
31936
- task_ids: taskIds
31937
- });
32269
+ const response = await withBgHopTimeout(bridge.send("bash_ack_completions", { session_id: drainContext.sessionID, task_ids: taskIds }, { timeoutMs: bgHopTimeoutMs }), "bash_ack_completions");
31938
32270
  if (response.success === false) {
31939
- sessionWarn(drainContext.sessionID, `${LOG_PREFIX} ack failed: ${String(response.message ?? "unknown error")}`);
32271
+ logPerTaskDeliveryHop(drainContext, "ack-error", "ack failed", taskIds, {
32272
+ event: "bash_completion_ack_error",
32273
+ cause: String(response.message ?? "unknown error")
32274
+ }, "warn");
31940
32275
  return false;
31941
32276
  }
31942
- sessionLog(drainContext.sessionID, `${LOG_PREFIX} ack ok`, {
32277
+ logPerTaskDeliveryHop(drainContext, "ack-ok", "ack ok", taskIds, {
31943
32278
  event: "bash_completion_ack_ok",
31944
- delivery_id: deliveryID ?? null,
31945
- task_ids: taskIds
32279
+ delivery_id: deliveryID ?? null
31946
32280
  });
31947
32281
  return true;
31948
32282
  } catch (err) {
31949
32283
  if (drainContext.nudgeRef && isTerminalNudgeError(err))
31950
32284
  throw err;
31951
- sessionWarn(drainContext.sessionID, `${LOG_PREFIX} ack failed: ${err instanceof Error ? err.message : String(err)}`);
32285
+ logPerTaskDeliveryHop(drainContext, "ack-error", "ack failed", taskIds, {
32286
+ event: "bash_completion_ack_error",
32287
+ cause: err instanceof Error ? err.message : String(err)
32288
+ }, "warn");
31952
32289
  return false;
31953
32290
  }
31954
32291
  }
@@ -32540,6 +32877,15 @@ async function callBashBridge(ctx, runtime, command, params = {}, options) {
32540
32877
  // src/bash-wait-detach.ts
32541
32878
  var BASH_WAIT_DETACH_MAGIC_KEYWORD = "&detach";
32542
32879
  var EMPTY_DETACH_MESSAGE = "(requested background detach)";
32880
+ var STANDALONE_DETACH_KEYWORD_SOURCE = `(^|[^\\p{L}\\p{N}_])${BASH_WAIT_DETACH_MAGIC_KEYWORD}(?![\\p{L}\\p{N}_])`;
32881
+ var STANDALONE_DETACH_KEYWORD_PATTERN = new RegExp(STANDALONE_DETACH_KEYWORD_SOURCE, "u");
32882
+ var STANDALONE_DETACH_KEYWORDS_PATTERN = new RegExp(STANDALONE_DETACH_KEYWORD_SOURCE, "gu");
32883
+ function containsStandaloneDetachKeyword(messageText) {
32884
+ return STANDALONE_DETACH_KEYWORD_PATTERN.test(messageText);
32885
+ }
32886
+ function stripStandaloneDetachKeywords(messageText) {
32887
+ return messageText.replace(STANDALONE_DETACH_KEYWORDS_PATTERN, "$1");
32888
+ }
32543
32889
  function userTextParts(output) {
32544
32890
  if (!output || typeof output !== "object")
32545
32891
  return [];
@@ -32561,9 +32907,9 @@ function stripUserMessageDetachKeyword(output) {
32561
32907
  const parts = userTextParts(output);
32562
32908
  const original = parts.map((part) => part.text).join(`
32563
32909
  `);
32564
- if (!original.includes(BASH_WAIT_DETACH_MAGIC_KEYWORD))
32910
+ if (!containsStandaloneDetachKeyword(original))
32565
32911
  return original;
32566
- const stripped = parts.map((part) => part.text.replaceAll(BASH_WAIT_DETACH_MAGIC_KEYWORD, "")).map((text) => text.replace(/[ \t]{2,}/g, " "));
32912
+ const stripped = parts.map((part) => stripStandaloneDetachKeywords(part.text)).map((text) => text.replace(/[ \t]{2,}/g, " "));
32567
32913
  if (stripped.join(`
32568
32914
  `).trim() === "") {
32569
32915
  if (parts[0])
@@ -32579,7 +32925,7 @@ function stripUserMessageDetachKeyword(output) {
32579
32925
  `);
32580
32926
  }
32581
32927
  function shouldDetachBashWaitOnUserMessage(config2, messageText) {
32582
- return resolveBashConfig(config2).detach_on_user_message || messageText.includes(BASH_WAIT_DETACH_MAGIC_KEYWORD);
32928
+ return resolveBashConfig(config2).detach_on_user_message || containsStandaloneDetachKeyword(messageText);
32583
32929
  }
32584
32930
  async function sendBashWaitDetach(bridge, sessionID) {
32585
32931
  const response = await bridge.send("bash_wait_detach", { session_id: sessionID }, { keepBridgeOnTimeout: true, transportTimeoutMs: BASH_TRANSPORT_TIMEOUT_MS });
@@ -33169,40 +33515,32 @@ import {
33169
33515
  import { dirname as dirname8 } from "node:path";
33170
33516
 
33171
33517
  // src/hooks/auto-update-checker/cache.ts
33172
- import { spawn as spawn3 } from "node:child_process";
33518
+ import { spawn as spawn4 } from "node:child_process";
33173
33519
  import { cpSync, existsSync as existsSync14, mkdtempSync, readFileSync as readFileSync10, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
33174
33520
  import { tmpdir as tmpdir2 } from "node:os";
33175
33521
  import { basename as basename3, dirname as dirname7, join as join16 } from "node:path";
33176
33522
  var import_comment_json3 = __toESM(require_src2(), 1);
33177
33523
 
33178
33524
  // src/hooks/auto-update-checker/checker.ts
33179
- var import_comment_json2 = __toESM(require_src2(), 1);
33180
33525
  import { existsSync as existsSync13, readFileSync as readFileSync9, statSync as statSync7, writeFileSync as writeFileSync5 } from "node:fs";
33181
- import { homedir as homedir14 } from "node:os";
33182
33526
  import { dirname as dirname6, isAbsolute as isAbsolute8, join as join15, resolve as resolve9 } from "node:path";
33183
33527
  import { fileURLToPath } from "node:url";
33528
+ var import_comment_json2 = __toESM(require_src2(), 1);
33184
33529
 
33185
33530
  // src/hooks/auto-update-checker/constants.ts
33186
- import { homedir as homedir13, platform as platform2 } from "node:os";
33187
33531
  import { join as join14 } from "node:path";
33188
33532
  var PACKAGE_NAME = "@cortexkit/aft-opencode";
33189
33533
  var NPM_REGISTRY_URL = "https://registry.npmjs.org";
33190
33534
  var NPM_FETCH_TIMEOUT = 1e4;
33191
- function getOpenCodeCacheRoot() {
33192
- if (platform2() === "win32") {
33193
- return join14(process.env.LOCALAPPDATA ?? homedir13(), "opencode");
33194
- }
33195
- return join14(homedir13(), ".cache", "opencode");
33535
+ function cacheDir() {
33536
+ return join14(getOpenCodeCacheRoot(), "packages");
33196
33537
  }
33197
- function getOpenCodeConfigRoot() {
33198
- if (platform2() === "win32") {
33199
- return join14(process.env.APPDATA ?? join14(homedir13(), "AppData", "Roaming"), "opencode");
33200
- }
33201
- return join14(process.env.XDG_CONFIG_HOME ?? join14(homedir13(), ".config"), "opencode");
33538
+ function userOpenCodeConfig() {
33539
+ return join14(getOpenCodeConfigRoot(), "opencode.json");
33540
+ }
33541
+ function userOpenCodeConfigJsonc() {
33542
+ return join14(getOpenCodeConfigRoot(), "opencode.jsonc");
33202
33543
  }
33203
- var CACHE_DIR = join14(getOpenCodeCacheRoot(), "packages");
33204
- var USER_OPENCODE_CONFIG = join14(getOpenCodeConfigRoot(), "opencode.json");
33205
- var USER_OPENCODE_CONFIG_JSONC = join14(getOpenCodeConfigRoot(), "opencode.jsonc");
33206
33544
 
33207
33545
  // src/hooks/auto-update-checker/types.ts
33208
33546
  var NpmPackageEnvelopeSchema = exports_external.object({
@@ -33262,8 +33600,8 @@ function getConfigPaths(directory) {
33262
33600
  return [
33263
33601
  join15(directory, ".opencode", "opencode.json"),
33264
33602
  join15(directory, ".opencode", "opencode.jsonc"),
33265
- USER_OPENCODE_CONFIG,
33266
- USER_OPENCODE_CONFIG_JSONC
33603
+ userOpenCodeConfig(),
33604
+ userOpenCodeConfigJsonc()
33267
33605
  ];
33268
33606
  }
33269
33607
  function resolvePathPluginSpec(spec, configPath) {
@@ -33368,7 +33706,7 @@ function findPluginEntry(directory) {
33368
33706
  }
33369
33707
  var cachedPackageVersion = null;
33370
33708
  function getSpecCachePackageJsonPath(spec) {
33371
- return join15(CACHE_DIR, spec, "node_modules", PACKAGE_NAME, "package.json");
33709
+ return join15(cacheDir(), spec, "node_modules", PACKAGE_NAME, "package.json");
33372
33710
  }
33373
33711
  function getCachedVersion(spec) {
33374
33712
  if (!spec && cachedPackageVersion)
@@ -33377,7 +33715,7 @@ function getCachedVersion(spec) {
33377
33715
  getCurrentRuntimePackageJsonPath(),
33378
33716
  spec ? getSpecCachePackageJsonPath(spec) : null,
33379
33717
  getSpecCachePackageJsonPath(`${PACKAGE_NAME}@latest`),
33380
- join15(homedir14(), ".cache", "opencode", "node_modules", PACKAGE_NAME, "package.json")
33718
+ join15(getOpenCodeCacheRoot(), "node_modules", PACKAGE_NAME, "package.json")
33381
33719
  ].filter(isString);
33382
33720
  for (const packageJsonPath of candidates) {
33383
33721
  try {
@@ -33424,6 +33762,7 @@ async function getLatestVersion(channel = "latest", options = {}) {
33424
33762
 
33425
33763
  // src/hooks/auto-update-checker/cache.ts
33426
33764
  var pendingSnapshots = new Map;
33765
+ var quarantinedInstallDirs = new Set;
33427
33766
  function createAutoUpdateSnapshot(installDir, packageJsonPath, packageName) {
33428
33767
  const packageDir = join16(installDir, "node_modules", packageName);
33429
33768
  const lockfilePath = join16(installDir, "package-lock.json");
@@ -33517,6 +33856,13 @@ function ensureDependencyVersion(packageJsonPath, packageName, version2) {
33517
33856
  return false;
33518
33857
  }
33519
33858
  }
33859
+ function restorePendingSnapshot(installDir) {
33860
+ const snapshot = pendingSnapshots.get(installDir);
33861
+ if (!snapshot)
33862
+ return;
33863
+ pendingSnapshots.delete(installDir);
33864
+ restoreAutoUpdateSnapshot(snapshot);
33865
+ }
33520
33866
  function removeInstalledPackage(installDir, packageName) {
33521
33867
  const packageDir = join16(installDir, "node_modules", packageName);
33522
33868
  if (!existsSync14(packageDir))
@@ -33537,9 +33883,10 @@ function resolveInstallContext(runtimePackageJsonPath = getCurrentRuntimePackage
33537
33883
  }
33538
33884
  return null;
33539
33885
  }
33540
- const legacyPackageJsonPath = join16(dirname7(CACHE_DIR), "package.json");
33886
+ const cacheRoot = dirname7(cacheDir());
33887
+ const legacyPackageJsonPath = join16(cacheRoot, "package.json");
33541
33888
  if (existsSync14(legacyPackageJsonPath)) {
33542
- return { installDir: dirname7(CACHE_DIR), packageJsonPath: legacyPackageJsonPath };
33889
+ return { installDir: cacheRoot, packageJsonPath: legacyPackageJsonPath };
33543
33890
  }
33544
33891
  return null;
33545
33892
  }
@@ -33550,6 +33897,12 @@ function preparePackageUpdate(version2, packageName = PACKAGE_NAME, runtimePacka
33550
33897
  warn2("[auto-update-checker] No install context found for auto-update");
33551
33898
  return null;
33552
33899
  }
33900
+ if (quarantinedInstallDirs.has(installContext.installDir)) {
33901
+ const recoverySnapshot = pendingSnapshots.get(installContext.installDir);
33902
+ const recoveryDetail = recoverySnapshot ? ` Recovery snapshot: ${recoverySnapshot.tempDir}` : "";
33903
+ warn2(`[auto-update-checker] Auto-update blocked after unconfirmed npm termination; ` + `restart OpenCode to retry.${recoveryDetail}`);
33904
+ return null;
33905
+ }
33553
33906
  const snapshot = createAutoUpdateSnapshot(installContext.installDir, installContext.packageJsonPath, packageName);
33554
33907
  pendingSnapshots.set(installContext.installDir, snapshot);
33555
33908
  if (!ensureDependencyVersion(installContext.packageJsonPath, packageName, version2)) {
@@ -33573,18 +33926,29 @@ async function runNpmInstallSafe(installDir, options = {}) {
33573
33926
  let timeout = null;
33574
33927
  let stderrTail = "";
33575
33928
  try {
33576
- if (options.signal?.aborted)
33929
+ if (options.signal?.aborted) {
33930
+ restorePendingSnapshot(installDir);
33577
33931
  return { ok: false, reason: "aborted" };
33932
+ }
33578
33933
  const npm = resolveNpm();
33579
33934
  if (!npm) {
33935
+ restorePendingSnapshot(installDir);
33580
33936
  const reason = "npm not found on PATH or in known version-manager locations";
33581
33937
  warnNpmInstallFailure(reason, stderrTail);
33582
33938
  return { ok: false, reason };
33583
33939
  }
33584
- const proc = spawn3(npm.command, ["install", "--no-audit", "--no-fund", "--no-progress", "--ignore-scripts"], {
33940
+ const invocation = npmInvocation(npm, [
33941
+ "install",
33942
+ "--no-audit",
33943
+ "--no-fund",
33944
+ "--no-progress",
33945
+ "--ignore-scripts"
33946
+ ]);
33947
+ const proc = spawn4(invocation.command, invocation.args, {
33585
33948
  cwd: installDir,
33586
33949
  stdio: ["ignore", "pipe", "pipe"],
33587
- env: npmSpawnEnv(npm)
33950
+ env: { ...npmSpawnEnv(npm), ...invocation.env },
33951
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments
33588
33952
  });
33589
33953
  proc.stderr?.on("data", (chunk) => {
33590
33954
  stderrTail += chunk.toString("utf8");
@@ -33593,12 +33957,11 @@ async function runNpmInstallSafe(installDir, options = {}) {
33593
33957
  }
33594
33958
  });
33595
33959
  proc.stdout?.on("data", () => {});
33960
+ let terminationPromise = null;
33596
33961
  const abortProcess = () => {
33597
- try {
33598
- proc.kill();
33599
- } catch {}
33962
+ terminationPromise ??= terminateNpmProcessTree(proc, invocation);
33963
+ return terminationPromise;
33600
33964
  };
33601
- options.signal?.addEventListener("abort", abortProcess, { once: true });
33602
33965
  const exitPromise = new Promise((resolveExit) => {
33603
33966
  proc.on("error", (err) => resolveExit({ ok: false, reason: `spawn error: ${String(err)}` }));
33604
33967
  proc.on("exit", (code) => resolveExit(code === 0 ? { ok: true } : { ok: false, reason: `npm install exited with code ${code ?? "signal/unknown"}` }));
@@ -33606,16 +33969,29 @@ async function runNpmInstallSafe(installDir, options = {}) {
33606
33969
  const timeoutPromise = new Promise((resolveTimeout) => {
33607
33970
  timeout = setTimeout(() => resolveTimeout("timeout"), options.timeoutMs ?? 60000);
33608
33971
  });
33609
- const result = await Promise.race([exitPromise, timeoutPromise]);
33610
- options.signal?.removeEventListener("abort", abortProcess);
33611
- if (result === "timeout" || options.signal?.aborted) {
33612
- abortProcess();
33613
- const snapshot2 = pendingSnapshots.get(installDir);
33614
- if (snapshot2) {
33615
- pendingSnapshots.delete(installDir);
33616
- restoreAutoUpdateSnapshot(snapshot2);
33617
- }
33618
- const reason = options.signal?.aborted ? "aborted" : "timeout";
33972
+ let resolveAbort;
33973
+ const abortPromise = new Promise((resolve10) => {
33974
+ resolveAbort = resolve10;
33975
+ });
33976
+ const onAbort = () => resolveAbort("abort");
33977
+ options.signal?.addEventListener("abort", onAbort, { once: true });
33978
+ if (options.signal?.aborted)
33979
+ onAbort();
33980
+ const result = await Promise.race([exitPromise, timeoutPromise, abortPromise]);
33981
+ options.signal?.removeEventListener("abort", onAbort);
33982
+ if (result === "timeout" || result === "abort") {
33983
+ try {
33984
+ await abortProcess();
33985
+ } catch (error53) {
33986
+ quarantinedInstallDirs.add(installDir);
33987
+ const recoverySnapshot = pendingSnapshots.get(installDir);
33988
+ const recoveryDetail = recoverySnapshot ? `; auto-update quarantined for this session; recovery snapshot: ${recoverySnapshot.tempDir}` : "; auto-update quarantined for this session";
33989
+ const reason2 = `termination outcome unknown: ${String(error53)}${recoveryDetail}`;
33990
+ warnNpmInstallFailure(reason2, stderrTail);
33991
+ return { ok: false, reason: reason2, stderrTail: stderrTail || undefined };
33992
+ }
33993
+ restorePendingSnapshot(installDir);
33994
+ const reason = result === "abort" ? "aborted" : "timeout";
33619
33995
  warnNpmInstallFailure(reason, stderrTail);
33620
33996
  return { ok: false, reason, stderrTail: stderrTail || undefined };
33621
33997
  }
@@ -33631,11 +34007,7 @@ async function runNpmInstallSafe(installDir, options = {}) {
33631
34007
  }
33632
34008
  return { ...result, stderrTail: stderrTail || undefined };
33633
34009
  } catch (err) {
33634
- const snapshot = pendingSnapshots.get(installDir);
33635
- if (snapshot) {
33636
- pendingSnapshots.delete(installDir);
33637
- restoreAutoUpdateSnapshot(snapshot);
33638
- }
34010
+ restorePendingSnapshot(installDir);
33639
34011
  const reason = `exception: ${String(err)}`;
33640
34012
  warnNpmInstallFailure(reason, stderrTail);
33641
34013
  return { ok: false, reason, stderrTail: stderrTail || undefined };
@@ -33910,7 +34282,7 @@ function showToast(ctx, title, message, variant = "info", duration3 = 3000) {
33910
34282
  }
33911
34283
 
33912
34284
  // src/lsp-auto-install.ts
33913
- import { spawn as spawn4 } from "node:child_process";
34285
+ import { spawn as spawn5 } from "node:child_process";
33914
34286
  import { createHash as createHash6 } from "node:crypto";
33915
34287
  import {
33916
34288
  createReadStream,
@@ -33925,6 +34297,7 @@ import {
33925
34297
  import { join as join19 } from "node:path";
33926
34298
 
33927
34299
  // src/lsp-cache.ts
34300
+ import { randomUUID as randomUUID3 } from "node:crypto";
33928
34301
  import {
33929
34302
  closeSync as closeSync6,
33930
34303
  mkdirSync as mkdirSync7,
@@ -34006,27 +34379,46 @@ var STALE_LOCK_MS2 = 30 * 60 * 1000;
34006
34379
  function acquireInstallLock(lockKey) {
34007
34380
  mkdirSync7(lspPackageDir(lockKey), { recursive: true });
34008
34381
  const lock = lockPath(lockKey);
34382
+ const token = randomUUID3();
34009
34383
  const tryClaim = () => {
34010
34384
  try {
34011
34385
  const fd = openSync6(lock, "wx");
34012
34386
  try {
34013
34387
  writeFileSync8(fd, `${process.pid}
34014
34388
  ${new Date().toISOString()}
34389
+ ${token}
34015
34390
  `);
34016
34391
  } finally {
34017
34392
  closeSync6(fd);
34018
34393
  }
34019
- return true;
34394
+ let retained = false;
34395
+ let released = false;
34396
+ return {
34397
+ retain() {
34398
+ retained = true;
34399
+ },
34400
+ release() {
34401
+ if (released)
34402
+ return;
34403
+ released = true;
34404
+ if (retained) {
34405
+ warn2(`[lsp] retaining install lock for ${lockKey} after unconfirmed npm termination; ` + `cross-process retries remain blocked until stale-lock recovery`);
34406
+ return;
34407
+ }
34408
+ releaseInstallLock(lockKey, token);
34409
+ }
34410
+ };
34020
34411
  } catch (err) {
34021
34412
  const code = err.code;
34022
34413
  if (code === "EEXIST")
34023
- return false;
34414
+ return null;
34024
34415
  warn2(`[lsp] unexpected error acquiring install lock for ${lockKey}: ${err}`);
34025
- return false;
34416
+ return null;
34026
34417
  }
34027
34418
  };
34028
- if (tryClaim())
34029
- return true;
34419
+ const claimed = tryClaim();
34420
+ if (claimed)
34421
+ return claimed;
34030
34422
  let owningPid = null;
34031
34423
  let lockMtimeMs = 0;
34032
34424
  try {
@@ -34044,7 +34436,7 @@ ${new Date().toISOString()}
34044
34436
  const skipLiveness = process.platform === "win32";
34045
34437
  const ownerAlive = !skipLiveness && owningPid !== null && isProcessAlive3(owningPid);
34046
34438
  if (skipLiveness ? ageWithinFresh : ownerAlive && ageWithinFresh) {
34047
- return false;
34439
+ return null;
34048
34440
  }
34049
34441
  log2(`[lsp] reclaiming install lock for ${lockKey} (owner_pid=${owningPid ?? "unknown"}, alive=${ownerAlive}, age_ms=${age})`);
34050
34442
  try {
@@ -34063,16 +34455,18 @@ function isProcessAlive3(pid) {
34063
34455
  return true;
34064
34456
  }
34065
34457
  }
34066
- function releaseInstallLock(lockKey) {
34458
+ function releaseInstallLock(lockKey, acquisitionToken) {
34067
34459
  const lock = lockPath(lockKey);
34068
34460
  try {
34069
34461
  let owningPid = null;
34462
+ let owningToken = "";
34070
34463
  try {
34071
34464
  const raw = readFileSync12(lock, "utf8");
34072
- const firstLine = raw.split(/\r?\n/, 1)[0]?.trim() ?? "";
34073
- const parsed = Number.parseInt(firstLine, 10);
34465
+ const lines = raw.split(/\r?\n/);
34466
+ const parsed = Number.parseInt(lines[0]?.trim() ?? "", 10);
34074
34467
  if (Number.isFinite(parsed) && parsed > 0)
34075
34468
  owningPid = parsed;
34469
+ owningToken = lines[2]?.trim() ?? "";
34076
34470
  } catch (readErr) {
34077
34471
  const code = readErr.code;
34078
34472
  if (code === "ENOENT")
@@ -34080,8 +34474,8 @@ function releaseInstallLock(lockKey) {
34080
34474
  warn2(`[lsp] could not read install lock for ${lockKey} during release: ${readErr}`);
34081
34475
  return;
34082
34476
  }
34083
- if (owningPid !== process.pid) {
34084
- log2(`[lsp] not releasing install lock for ${lockKey}: owned by pid ${owningPid ?? "unknown"} (we are ${process.pid})`);
34477
+ if (owningPid !== process.pid || owningToken !== acquisitionToken) {
34478
+ log2(`[lsp] not releasing install lock for ${lockKey}: acquisition ownership changed`);
34085
34479
  return;
34086
34480
  }
34087
34481
  try {
@@ -34097,12 +34491,13 @@ function releaseInstallLock(lockKey) {
34097
34491
  }
34098
34492
  }
34099
34493
  async function withInstallLock(lockKey, task) {
34100
- if (!acquireInstallLock(lockKey))
34494
+ const lease = acquireInstallLock(lockKey);
34495
+ if (!lease)
34101
34496
  return null;
34102
34497
  try {
34103
- return await task();
34498
+ return await task(lease);
34104
34499
  } finally {
34105
- releaseInstallLock(lockKey);
34500
+ lease.release();
34106
34501
  }
34107
34502
  }
34108
34503
  var VERSION_CHECK_FILE = ".aft-version-check";
@@ -34447,6 +34842,7 @@ function buildExtensionMap(specs) {
34447
34842
  return byExt;
34448
34843
  }
34449
34844
  var inFlightAutoInstalls = new Set;
34845
+ var quarantinedNpmInstalls = new Set;
34450
34846
  function trackInFlightAutoInstall(controller, promise2) {
34451
34847
  const entry = { controller, promise: promise2 };
34452
34848
  inFlightAutoInstalls.add(entry);
@@ -34494,7 +34890,7 @@ function ensureInstallAnchor(cwd) {
34494
34890
  warn2(`[lsp] could not write package.json stub in ${cwd}: ${err}`);
34495
34891
  }
34496
34892
  }
34497
- function runInstall(spec, version2, cwd, signal) {
34893
+ function runInstall(spec, version2, cwd, installLock, signal) {
34498
34894
  return new Promise((resolve10) => {
34499
34895
  const target = `${spec.npm}@${version2}`;
34500
34896
  log2(`[lsp] installing ${target} to ${cwd}`);
@@ -34510,19 +34906,25 @@ function runInstall(spec, version2, cwd, signal) {
34510
34906
  return;
34511
34907
  }
34512
34908
  ensureInstallAnchor(cwd);
34513
- const child = spawn4(npm.command, ["install", "--no-save", "--ignore-scripts", "--silent", target], {
34909
+ const invocation = npmInvocation(npm, [
34910
+ "install",
34911
+ "--no-save",
34912
+ "--ignore-scripts",
34913
+ "--silent",
34914
+ target
34915
+ ]);
34916
+ const child = spawn5(invocation.command, invocation.args, {
34514
34917
  stdio: ["ignore", "pipe", "pipe"],
34515
34918
  cwd,
34516
- env: npmSpawnEnv(npm)
34919
+ env: { ...npmSpawnEnv(npm), ...invocation.env },
34920
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments
34517
34921
  });
34518
34922
  child.unref();
34519
34923
  let stderrBuf = "";
34520
34924
  let settled = false;
34521
- let killTimer = null;
34925
+ let terminationPromise = null;
34522
34926
  const cleanup = () => {
34523
34927
  signal?.removeEventListener("abort", onAbort);
34524
- if (killTimer)
34525
- clearTimeout(killTimer);
34526
34928
  };
34527
34929
  const finish = (ok) => {
34528
34930
  if (settled)
@@ -34531,14 +34933,31 @@ function runInstall(spec, version2, cwd, signal) {
34531
34933
  cleanup();
34532
34934
  resolve10(ok);
34533
34935
  };
34936
+ const childCompletedSuccessfully = () => child.exitCode === 0 && child.signalCode === null;
34937
+ const finishAfterConfirmedTermination = () => {
34938
+ const completedSuccessfully = childCompletedSuccessfully();
34939
+ if (completedSuccessfully)
34940
+ log2(`[lsp] installed ${target}`);
34941
+ finish(completedSuccessfully);
34942
+ };
34534
34943
  const onAbort = () => {
34944
+ if (childCompletedSuccessfully()) {
34945
+ finishAfterConfirmedTermination();
34946
+ return;
34947
+ }
34535
34948
  warn2(`[lsp] install ${target} aborted during shutdown`);
34536
- child.kill("SIGTERM");
34537
- killTimer = setTimeout(() => {
34538
- if (!settled)
34539
- child.kill("SIGKILL");
34540
- }, 5000);
34541
- killTimer.unref?.();
34949
+ terminationPromise ??= terminateNpmProcessTree(child, invocation);
34950
+ terminationPromise.then(finishAfterConfirmedTermination, (terminationError) => {
34951
+ if (child.pid === undefined) {
34952
+ error52(`[lsp] install ${target} failed to spawn during shutdown: ${terminationError}`);
34953
+ finish(false);
34954
+ return;
34955
+ }
34956
+ quarantinedNpmInstalls.add(spec.npm);
34957
+ installLock.retain();
34958
+ error52(`[lsp] install ${target} termination outcome unknown; quarantining retries for this session: ${String(terminationError)}`);
34959
+ finish(false);
34960
+ });
34542
34961
  };
34543
34962
  signal?.addEventListener("abort", onAbort, { once: true });
34544
34963
  if (signal?.aborted)
@@ -34552,10 +34971,16 @@ function runInstall(spec, version2, cwd, signal) {
34552
34971
  }
34553
34972
  });
34554
34973
  child.on("error", (err) => {
34555
- error52(`[lsp] install ${target} failed to spawn: ${err}`);
34974
+ if (settled)
34975
+ return;
34976
+ error52(`[lsp] install ${target} child-process error: ${err}`);
34977
+ if (terminationPromise && child.pid !== undefined)
34978
+ return;
34556
34979
  finish(false);
34557
34980
  });
34558
34981
  child.on("exit", (code) => {
34982
+ if (settled || terminationPromise)
34983
+ return;
34559
34984
  if (code === 0) {
34560
34985
  log2(`[lsp] installed ${target}`);
34561
34986
  finish(true);
@@ -34568,7 +34993,13 @@ ${stderrBuf.trim()}`);
34568
34993
  });
34569
34994
  }
34570
34995
  async function ensureServerInstalled(spec, config2, fetchImpl, signal) {
34571
- const outcome = await withInstallLock(spec.npm, async () => {
34996
+ if (quarantinedNpmInstalls.has(spec.npm)) {
34997
+ return {
34998
+ started: false,
34999
+ reason: "install blocked after unconfirmed npm termination; restart the host to retry"
35000
+ };
35001
+ }
35002
+ const outcome = await withInstallLock(spec.npm, async (installLock) => {
34572
35003
  const { version: version2, probe } = await resolveTargetVersion(spec, config2, fetchImpl);
34573
35004
  if (!version2) {
34574
35005
  const installed = isInstalled(spec.npm, spec.binary);
@@ -34604,7 +35035,7 @@ async function ensureServerInstalled(spec, config2, fetchImpl, signal) {
34604
35035
  log2(`[lsp] reinstalling ${spec.npm}@${version2}: no installed-version metadata recorded`);
34605
35036
  }
34606
35037
  }
34607
- const ok = await runInstall(spec, version2, cachedPackageDir(spec.npm), signal).catch((err) => {
35038
+ const ok = await runInstall(spec, version2, cachedPackageDir(spec.npm), installLock, signal).catch((err) => {
34608
35039
  error52(`[lsp] background install ${spec.npm} crashed: ${err}`);
34609
35040
  return false;
34610
35041
  });
@@ -34787,69 +35218,69 @@ import { Readable as Readable3 } from "node:stream";
34787
35218
  import { pipeline as pipeline3 } from "node:stream/promises";
34788
35219
 
34789
35220
  // src/lsp-github-table.ts
34790
- function exe(platform3, name) {
34791
- return platform3 === "win32" ? `${name}.exe` : name;
35221
+ function exe(platform2, name) {
35222
+ return platform2 === "win32" ? `${name}.exe` : name;
34792
35223
  }
34793
35224
  var CLANGD = {
34794
35225
  id: "clangd",
34795
35226
  githubRepo: "clangd/clangd",
34796
35227
  binary: "clangd",
34797
- resolveAsset: (platform3, _arch, version2) => {
34798
- const platformName = platform3 === "darwin" ? "mac" : platform3 === "linux" ? "linux" : "windows";
35228
+ resolveAsset: (platform2, _arch, version2) => {
35229
+ const platformName = platform2 === "darwin" ? "mac" : platform2 === "linux" ? "linux" : "windows";
34799
35230
  return { name: `clangd-${platformName}-${version2}.zip`, archive: "zip" };
34800
35231
  },
34801
- binaryPathInArchive: (platform3, _arch, version2) => `clangd_${version2}/bin/${exe(platform3, "clangd")}`
35232
+ binaryPathInArchive: (platform2, _arch, version2) => `clangd_${version2}/bin/${exe(platform2, "clangd")}`
34802
35233
  };
34803
35234
  var LUA_LS = {
34804
35235
  id: "lua-ls",
34805
35236
  githubRepo: "LuaLS/lua-language-server",
34806
35237
  binary: "lua-language-server",
34807
- resolveAsset: (platform3, arch, version2) => {
34808
- const ext = platform3 === "win32" ? "zip" : "tar.gz";
34809
- const platformName = platform3 === "darwin" ? "darwin" : platform3 === "linux" ? "linux" : "win32";
35238
+ resolveAsset: (platform2, arch, version2) => {
35239
+ const ext = platform2 === "win32" ? "zip" : "tar.gz";
35240
+ const platformName = platform2 === "darwin" ? "darwin" : platform2 === "linux" ? "linux" : "win32";
34810
35241
  const archName = arch === "arm64" ? "arm64" : "x64";
34811
35242
  return {
34812
35243
  name: `lua-language-server-${version2}-${platformName}-${archName}.${ext}`,
34813
35244
  archive: ext
34814
35245
  };
34815
35246
  },
34816
- binaryPathInArchive: (platform3, _arch, _version) => `bin/${exe(platform3, "lua-language-server")}`
35247
+ binaryPathInArchive: (platform2, _arch, _version) => `bin/${exe(platform2, "lua-language-server")}`
34817
35248
  };
34818
35249
  var ZLS = {
34819
35250
  id: "zls",
34820
35251
  githubRepo: "zigtools/zls",
34821
35252
  binary: "zls",
34822
- resolveAsset: (platform3, arch, _version) => {
34823
- const ext = platform3 === "win32" ? "zip" : "tar.xz";
35253
+ resolveAsset: (platform2, arch, _version) => {
35254
+ const ext = platform2 === "win32" ? "zip" : "tar.xz";
34824
35255
  const archName = arch === "arm64" ? "aarch64" : "x86_64";
34825
- const platformName = platform3 === "darwin" ? "macos" : platform3 === "linux" ? "linux" : "windows";
35256
+ const platformName = platform2 === "darwin" ? "macos" : platform2 === "linux" ? "linux" : "windows";
34826
35257
  return { name: `zls-${archName}-${platformName}.${ext}`, archive: ext };
34827
35258
  },
34828
- binaryPathInArchive: (platform3, _arch, _version) => exe(platform3, "zls")
35259
+ binaryPathInArchive: (platform2, _arch, _version) => exe(platform2, "zls")
34829
35260
  };
34830
35261
  var TINYMIST = {
34831
35262
  id: "tinymist",
34832
35263
  githubRepo: "Myriad-Dreamin/tinymist",
34833
35264
  binary: "tinymist",
34834
- resolveAsset: (platform3, arch, _version) => {
35265
+ resolveAsset: (platform2, arch, _version) => {
34835
35266
  const archName = arch === "arm64" ? "aarch64" : "x86_64";
34836
- const triple = platform3 === "darwin" ? "apple-darwin" : platform3 === "linux" ? "unknown-linux-gnu" : "pc-windows-msvc";
34837
- const ext = platform3 === "win32" ? "zip" : "tar.gz";
35267
+ const triple = platform2 === "darwin" ? "apple-darwin" : platform2 === "linux" ? "unknown-linux-gnu" : "pc-windows-msvc";
35268
+ const ext = platform2 === "win32" ? "zip" : "tar.gz";
34838
35269
  return { name: `tinymist-${archName}-${triple}.${ext}`, archive: ext };
34839
35270
  },
34840
- binaryPathInArchive: (platform3, _arch, _version) => exe(platform3, "tinymist")
35271
+ binaryPathInArchive: (platform2, _arch, _version) => exe(platform2, "tinymist")
34841
35272
  };
34842
35273
  var TEXLAB = {
34843
35274
  id: "texlab",
34844
35275
  githubRepo: "latex-lsp/texlab",
34845
35276
  binary: "texlab",
34846
- resolveAsset: (platform3, arch, _version) => {
35277
+ resolveAsset: (platform2, arch, _version) => {
34847
35278
  const archName = arch === "arm64" ? "aarch64" : "x86_64";
34848
- const platformName = platform3 === "darwin" ? "macos" : platform3 === "linux" ? "linux" : "windows";
34849
- const ext = platform3 === "win32" ? "zip" : "tar.gz";
35279
+ const platformName = platform2 === "darwin" ? "macos" : platform2 === "linux" ? "linux" : "windows";
35280
+ const ext = platform2 === "win32" ? "zip" : "tar.gz";
34850
35281
  return { name: `texlab-${archName}-${platformName}.${ext}`, archive: ext };
34851
35282
  },
34852
- binaryPathInArchive: (platform3, _arch, _version) => exe(platform3, "texlab")
35283
+ binaryPathInArchive: (platform2, _arch, _version) => exe(platform2, "texlab")
34853
35284
  };
34854
35285
  var GITHUB_LSP_TABLE = [
34855
35286
  CLANGD,
@@ -34859,14 +35290,14 @@ var GITHUB_LSP_TABLE = [
34859
35290
  TEXLAB
34860
35291
  ];
34861
35292
  function detectHostPlatform() {
34862
- const platform3 = process.platform;
34863
- if (platform3 !== "darwin" && platform3 !== "linux" && platform3 !== "win32")
35293
+ const platform2 = process.platform;
35294
+ if (platform2 !== "darwin" && platform2 !== "linux" && platform2 !== "win32")
34864
35295
  return null;
34865
35296
  const arch = process.arch;
34866
35297
  if (arch === "x64")
34867
- return { platform: platform3, arch: "x64" };
35298
+ return { platform: platform2, arch: "x64" };
34868
35299
  if (arch === "arm64")
34869
- return { platform: platform3, arch: "arm64" };
35300
+ return { platform: platform2, arch: "arm64" };
34870
35301
  return null;
34871
35302
  }
34872
35303
 
@@ -34884,12 +35315,12 @@ function ghExtractDir(spec) {
34884
35315
  return join20(ghPackageDir(spec), "extracted");
34885
35316
  }
34886
35317
  var INSTALLED_META_FILE2 = ".aft-installed";
34887
- function ghBinaryPath(spec, platform3) {
34888
- const ext = platform3 === "win32" ? ".exe" : "";
35318
+ function ghBinaryPath(spec, platform2) {
35319
+ const ext = platform2 === "win32" ? ".exe" : "";
34889
35320
  return join20(ghBinDir(spec), `${spec.binary}${ext}`);
34890
35321
  }
34891
- function isGithubInstalled(spec, platform3) {
34892
- for (const candidate of ghBinaryCandidates(spec, platform3)) {
35322
+ function isGithubInstalled(spec, platform2) {
35323
+ for (const candidate of ghBinaryCandidates(spec, platform2)) {
34893
35324
  try {
34894
35325
  if (statSync11(join20(ghBinDir(spec), candidate)).isFile())
34895
35326
  return true;
@@ -34897,8 +35328,8 @@ function isGithubInstalled(spec, platform3) {
34897
35328
  }
34898
35329
  return false;
34899
35330
  }
34900
- function ghBinaryCandidates(spec, platform3) {
34901
- if (platform3 !== "win32")
35331
+ function ghBinaryCandidates(spec, platform2) {
35332
+ if (platform2 !== "win32")
34902
35333
  return [spec.binary];
34903
35334
  return [spec.binary, `${spec.binary}.cmd`, `${spec.binary}.exe`, `${spec.binary}.bat`];
34904
35335
  }
@@ -35278,10 +35709,10 @@ function quarantineCachedGithubInstall(spec, reason) {
35278
35709
  warn2(`[lsp] tofu_mismatch ${spec.id}: failed to quarantine cache entry: ${err}`);
35279
35710
  }
35280
35711
  }
35281
- function validateCachedGithubInstall(spec, platform3) {
35712
+ function validateCachedGithubInstall(spec, platform2) {
35282
35713
  const packageDir = ghPackageDir(spec);
35283
35714
  const meta3 = readGithubInstalledMetaIn(packageDir);
35284
- const binaryPath = ghBinaryCandidates(spec, platform3).map((candidate) => join20(ghBinDir(spec), candidate)).find((candidate) => {
35715
+ const binaryPath = ghBinaryCandidates(spec, platform2).map((candidate) => join20(ghBinDir(spec), candidate)).find((candidate) => {
35285
35716
  try {
35286
35717
  return statSync11(candidate).isFile();
35287
35718
  } catch {
@@ -35345,11 +35776,11 @@ function runPlatformExtractor(archivePath, destDir, archiveType) {
35345
35776
  }
35346
35777
  throw new Error(`unsupported archive type: ${archiveType}`);
35347
35778
  }
35348
- async function downloadAndInstall(spec, tag, assets, platform3, arch, fetchImpl, signal) {
35779
+ async function downloadAndInstall(spec, tag, assets, platform2, arch, fetchImpl, signal) {
35349
35780
  const version2 = stripTagV(tag);
35350
- const expected = spec.resolveAsset(platform3, arch, version2);
35781
+ const expected = spec.resolveAsset(platform2, arch, version2);
35351
35782
  if (!expected) {
35352
- warn2(`[lsp] ${spec.id}: unsupported platform/arch combo ${platform3}/${arch}`);
35783
+ warn2(`[lsp] ${spec.id}: unsupported platform/arch combo ${platform2}/${arch}`);
35353
35784
  return null;
35354
35785
  }
35355
35786
  const matchingAsset = assets.find((a) => a.name === expected.name);
@@ -35399,16 +35830,16 @@ async function downloadAndInstall(spec, tag, assets, platform3, arch, fetchImpl,
35399
35830
  unlinkSync7(archivePath);
35400
35831
  } catch {}
35401
35832
  }
35402
- const innerBinaryPath = join20(extractDir, spec.binaryPathInArchive(platform3, arch, version2));
35833
+ const innerBinaryPath = join20(extractDir, spec.binaryPathInArchive(platform2, arch, version2));
35403
35834
  if (!existsSync18(innerBinaryPath)) {
35404
35835
  error52(`[lsp] ${spec.id}: extracted binary not found at ${innerBinaryPath}`);
35405
35836
  return null;
35406
35837
  }
35407
- const targetBinary = ghBinaryPath(spec, platform3);
35838
+ const targetBinary = ghBinaryPath(spec, platform2);
35408
35839
  mkdirSync9(dirname9(targetBinary), { recursive: true });
35409
35840
  try {
35410
35841
  copyFileSync4(innerBinaryPath, targetBinary);
35411
- if (platform3 !== "win32") {
35842
+ if (platform2 !== "win32") {
35412
35843
  const { chmodSync: chmodSync4 } = await import("node:fs");
35413
35844
  chmodSync4(targetBinary, 493);
35414
35845
  }
@@ -35421,11 +35852,11 @@ async function downloadAndInstall(spec, tag, assets, platform3, arch, fetchImpl,
35421
35852
  log2(`[lsp] ${spec.id} ${tag} binary_sha256=${binarySha256}`);
35422
35853
  return { archiveSha256, binarySha256 };
35423
35854
  }
35424
- async function ensureGithubInstalled(spec, config2, fetchImpl, platform3, arch, signal) {
35855
+ async function ensureGithubInstalled(spec, config2, fetchImpl, platform2, arch, signal) {
35425
35856
  const outcome = await withInstallLock(spec.githubRepo, async () => {
35426
35857
  const { tag, assets, blockedByGrace, reason } = await resolveTargetTag(spec, config2, fetchImpl, signal);
35427
35858
  if (!tag) {
35428
- const installed = isGithubInstalled(spec, platform3);
35859
+ const installed = isGithubInstalled(spec, platform2);
35429
35860
  if (installed) {
35430
35861
  warn2(`[lsp] no eligible release of ${spec.githubRepo} (grace=${config2.graceDays}d); keeping existing install`);
35431
35862
  return { started: false, reason: "kept existing install" };
@@ -35434,7 +35865,7 @@ async function ensureGithubInstalled(spec, config2, fetchImpl, platform3, arch,
35434
35865
  warn2(`[lsp] skipping ${spec.id}: ${fallbackReason}`);
35435
35866
  return { started: false, reason: fallbackReason };
35436
35867
  }
35437
- if (isGithubInstalled(spec, platform3)) {
35868
+ if (isGithubInstalled(spec, platform2)) {
35438
35869
  const installedMeta = readInstalledMetaIn(ghPackageDir(spec));
35439
35870
  if (installedMeta && installedMeta.version === tag) {
35440
35871
  return { started: false, reason: "already installed" };
@@ -35445,7 +35876,7 @@ async function ensureGithubInstalled(spec, config2, fetchImpl, platform3, arch,
35445
35876
  log2(`[lsp] reinstalling ${spec.id}@${tag}: no installed-version metadata recorded`);
35446
35877
  }
35447
35878
  }
35448
- const hashes = await downloadAndInstall(spec, tag, assets, platform3, arch, fetchImpl, signal).catch((err) => {
35879
+ const hashes = await downloadAndInstall(spec, tag, assets, platform2, arch, fetchImpl, signal).catch((err) => {
35449
35880
  error52(`[lsp] github install ${spec.id} crashed: ${err}`);
35450
35881
  return null;
35451
35882
  });
@@ -37825,6 +38256,48 @@ function relativeToWorktree(fp, worktree) {
37825
38256
  function readAttachments(data) {
37826
38257
  return Array.isArray(data.attachments) ? data.attachments : [];
37827
38258
  }
38259
+ var ISSUE_AND_PR_READ_DESCRIPTION = "GitHub issues and pull requests can be read with `issue://NUMBER` and `pr://NUMBER` (or `issue://OWNER/REPO/NUMBER` and `pr://OWNER/REPO/NUMBER`).";
38260
+ function asRecord2(value) {
38261
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
38262
+ }
38263
+ function visionCapabilityForOpenCodeModel(model) {
38264
+ const entry = model;
38265
+ if (Array.isArray(entry?.modalities?.input)) {
38266
+ return entry.modalities.input.includes("image");
38267
+ }
38268
+ return typeof entry?.attachment === "boolean" ? entry.attachment : undefined;
38269
+ }
38270
+ function modelFromOpenCodeProvider(provider, modelID) {
38271
+ const models = provider.models;
38272
+ if (Array.isArray(models)) {
38273
+ return models.find((model) => asRecord2(model)?.id === modelID);
38274
+ }
38275
+ return asRecord2(models)?.[modelID];
38276
+ }
38277
+ async function currentSessionVisionCapability(client, sessionID) {
38278
+ if (!sessionID)
38279
+ return;
38280
+ const promptContext = await resolvePromptContext(client, sessionID);
38281
+ const currentModel = promptContext?.model;
38282
+ if (!currentModel)
38283
+ return;
38284
+ const providerApi = client.provider;
38285
+ if (typeof providerApi?.list !== "function")
38286
+ return;
38287
+ let listed;
38288
+ try {
38289
+ listed = await providerApi.list();
38290
+ } catch {
38291
+ return;
38292
+ }
38293
+ const result = asRecord2(listed);
38294
+ const catalog = asRecord2(result?.data) ?? result;
38295
+ const providers = Array.isArray(catalog?.all) ? catalog.all : Array.isArray(catalog?.providers) ? catalog.providers : [];
38296
+ const provider = providers.map(asRecord2).find((entry) => entry?.id === currentModel.providerID);
38297
+ if (!provider)
38298
+ return;
38299
+ return visionCapabilityForOpenCodeModel(modelFromOpenCodeProvider(provider, currentModel.modelID));
38300
+ }
37828
38301
  function persistFilePathAlias(args, context) {
37829
38302
  if (typeof args.path === "string" && !Object.hasOwn(args, "filePath")) {
37830
38303
  args.filePath = args.path;
@@ -38012,10 +38485,19 @@ Examples:
38012
38485
  Read 30 lines from line 200: { "path": "src/app.ts", "offset": 200, "limit": 30 }
38013
38486
  List directory: { "path": "src/" }
38014
38487
  `;
38488
+ function readDescription(ghReadEnabled) {
38489
+ if (!ghReadEnabled)
38490
+ return READ_DESCRIPTION;
38491
+ return READ_DESCRIPTION.replace(`
38492
+ Examples:`, `
38493
+ ${ISSUE_AND_PR_READ_DESCRIPTION}
38494
+
38495
+ Examples:`);
38496
+ }
38015
38497
  function createReadTool(ctx) {
38016
38498
  return prepareToolMap({
38017
38499
  read: {
38018
- description: READ_DESCRIPTION,
38500
+ description: readDescription(ctx.config.gh_read?.enabled === true),
38019
38501
  args: {
38020
38502
  filePath: z8.string().describe("Path to file or directory (absolute or relative to project root)"),
38021
38503
  startLine: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based line to start reading from"),
@@ -38066,6 +38548,9 @@ function createReadTool(ctx) {
38066
38548
  rawArgs.endLine = endLine;
38067
38549
  if (rawLimit !== undefined && rawOffset === undefined)
38068
38550
  rawArgs.limit = rawLimit;
38551
+ const visionCapability = await currentSessionVisionCapability(ctx.client, context.sessionID);
38552
+ if (visionCapability !== undefined)
38553
+ rawArgs.vision_capability = visionCapability;
38069
38554
  const response = await callToolCall(ctx, context, "read", rawArgs);
38070
38555
  if (response.success === false) {
38071
38556
  throw new Error(response.message || "read failed");
@@ -38700,7 +39185,7 @@ async function resolveAndGateScope(ctx, context, scope) {
38700
39185
  }
38701
39186
  return { scope: Array.isArray(scope) ? resolved : resolved[0] };
38702
39187
  }
38703
- function asRecord2(value) {
39188
+ function asRecord3(value) {
38704
39189
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
38705
39190
  }
38706
39191
  function asString(value) {
@@ -38708,7 +39193,7 @@ function asString(value) {
38708
39193
  }
38709
39194
  function firstRecord(...values) {
38710
39195
  for (const value of values) {
38711
- const record2 = asRecord2(value);
39196
+ const record2 = asRecord3(value);
38712
39197
  if (record2)
38713
39198
  return record2;
38714
39199
  }
@@ -38743,7 +39228,7 @@ function parseInspectPhaseEntries(value) {
38743
39228
  if (!Array.isArray(value))
38744
39229
  return [];
38745
39230
  return value.flatMap((candidate) => {
38746
- const entry = asRecord2(candidate);
39231
+ const entry = asRecord3(candidate);
38747
39232
  const id = asString(entry?.id);
38748
39233
  if (!id)
38749
39234
  return [];
@@ -38759,7 +39244,7 @@ function parseInspectPhaseEntries(value) {
38759
39244
  });
38760
39245
  }
38761
39246
  function parseInspectTerminal(payload) {
38762
- const response = asRecord2(payload);
39247
+ const response = asRecord3(payload);
38763
39248
  if (!response)
38764
39249
  return;
38765
39250
  const kind = terminalKind(response);
@@ -38774,7 +39259,7 @@ function parseInspectTerminal(payload) {
38774
39259
  waitStampText: kind === "FRESH" ? firstString(waitStamp?.text, waitStamp?.human_text) : undefined
38775
39260
  };
38776
39261
  }
38777
- const failedPhaseRecord = asRecord2(response.failed_phase);
39262
+ const failedPhaseRecord = asRecord3(response.failed_phase);
38778
39263
  const failedPhaseId = firstString(failedPhaseRecord?.id, response.failed_phase);
38779
39264
  const failedPhase = failedPhaseId ? parseInspectPhaseEntries([
38780
39265
  {
@@ -39724,7 +40209,9 @@ function semanticTools(ctx) {
39724
40209
  rawArgs.includeTests = args.includeTests;
39725
40210
  if (pathArg)
39726
40211
  rawArgs.path = pathArg;
39727
- const response = await callToolCall(ctx, context, "search", rawArgs);
40212
+ const response = await callToolCall(ctx, context, "search", rawArgs, {
40213
+ abortSignal: context.abort
40214
+ });
39728
40215
  if (response.success === false) {
39729
40216
  const message = typeof response.text === "string" && response.text.length > 0 ? response.text : typeof response.message === "string" && response.message.length > 0 ? response.message : "semantic_search failed";
39730
40217
  throw new Error(message);
@@ -39903,7 +40390,7 @@ var PLUGIN_VERSION = (() => {
39903
40390
  return "0.0.0";
39904
40391
  }
39905
40392
  })();
39906
- var ANNOUNCEMENT_VERSION = "0.54.0";
40393
+ var ANNOUNCEMENT_VERSION = "0.55.0";
39907
40394
  var ANNOUNCEMENT_FEATURES = [
39908
40395
  "Callgraph stays consistent under load: edits arriving mid-rebuild can no longer be silently lost from navigation results.",
39909
40396
  "aft_zoom handles real-world .jsonc: files with comment banners above the opening brace now resolve, and not-found errors point at the segment that actually failed (thanks @iceteaSA).",