@cortexkit/aft-opencode 0.26.0 → 0.26.2

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
@@ -7803,7 +7803,7 @@ var require_src2 = __commonJS((exports, module) => {
7803
7803
  });
7804
7804
 
7805
7805
  // src/index.ts
7806
- import { existsSync as existsSync14, mkdirSync as mkdirSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync11 } from "fs";
7806
+ import { existsSync as existsSync14, mkdirSync as mkdirSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync12 } from "fs";
7807
7807
  import { createRequire as createRequire2 } from "module";
7808
7808
  import { homedir as homedir11 } from "os";
7809
7809
  import { join as join21 } from "path";
@@ -7820,12 +7820,22 @@ function getActiveLogger() {
7820
7820
  return loggerGlobal()[ACTIVE_LOGGER_SYMBOL];
7821
7821
  }
7822
7822
  function getLogFilePath() {
7823
- return getActiveLogger()?.getLogFilePath?.();
7823
+ try {
7824
+ return getActiveLogger()?.getLogFilePath?.();
7825
+ } catch (err) {
7826
+ console.error(`[aft-bridge] ERROR: active logger getLogFilePath threw: ${err instanceof Error ? err.message : String(err)}`);
7827
+ return;
7828
+ }
7824
7829
  }
7825
7830
  function log(message, meta) {
7826
7831
  const active = getActiveLogger();
7827
7832
  if (active) {
7828
- active.log(message, meta);
7833
+ try {
7834
+ active.log(message, meta);
7835
+ } catch (err) {
7836
+ console.error(`[aft-bridge] ERROR: active logger log threw: ${err instanceof Error ? err.message : String(err)}`);
7837
+ console.error(`[aft-bridge] ${message}`);
7838
+ }
7829
7839
  } else {
7830
7840
  console.error(`[aft-bridge] ${message}`);
7831
7841
  }
@@ -7833,7 +7843,12 @@ function log(message, meta) {
7833
7843
  function warn(message, meta) {
7834
7844
  const active = getActiveLogger();
7835
7845
  if (active) {
7836
- active.warn(message, meta);
7846
+ try {
7847
+ active.warn(message, meta);
7848
+ } catch (err) {
7849
+ console.error(`[aft-bridge] ERROR: active logger warn threw: ${err instanceof Error ? err.message : String(err)}`);
7850
+ console.error(`[aft-bridge] WARN: ${message}`);
7851
+ }
7837
7852
  } else {
7838
7853
  console.error(`[aft-bridge] WARN: ${message}`);
7839
7854
  }
@@ -7841,20 +7856,16 @@ function warn(message, meta) {
7841
7856
  function error(message, meta) {
7842
7857
  const active = getActiveLogger();
7843
7858
  if (active) {
7844
- active.error(message, meta);
7859
+ try {
7860
+ active.error(message, meta);
7861
+ } catch (err) {
7862
+ console.error(`[aft-bridge] ERROR: active logger error threw: ${err instanceof Error ? err.message : String(err)}`);
7863
+ console.error(`[aft-bridge] ERROR: ${message}`);
7864
+ }
7845
7865
  } else {
7846
7866
  console.error(`[aft-bridge] ERROR: ${message}`);
7847
7867
  }
7848
7868
  }
7849
- function sessionLog(sessionId, message) {
7850
- log(message, sessionId ? { sessionId } : undefined);
7851
- }
7852
- function sessionWarn(sessionId, message) {
7853
- warn(message, sessionId ? { sessionId } : undefined);
7854
- }
7855
- function sessionError(sessionId, message) {
7856
- error(message, sessionId ? { sessionId } : undefined);
7857
- }
7858
7869
  // ../aft-bridge/dist/bridge.js
7859
7870
  import { spawn } from "child_process";
7860
7871
  import { homedir } from "os";
@@ -7949,6 +7960,7 @@ class BinaryBridge {
7949
7960
  pending = new Map;
7950
7961
  nextId = 1;
7951
7962
  stdoutBuffer = "";
7963
+ stderrBuffer = "";
7952
7964
  stderrTail = [];
7953
7965
  _restartCount = 0;
7954
7966
  _shuttingDown = false;
@@ -7984,24 +7996,62 @@ class BinaryBridge {
7984
7996
  }
7985
7997
  logVia(message, meta) {
7986
7998
  const logger = this.logger ?? getActiveLogger();
7987
- if (logger)
7988
- logger.log(message, meta);
7989
- else
7999
+ if (logger) {
8000
+ try {
8001
+ logger.log(message, meta);
8002
+ } catch (err) {
8003
+ console.error(`[aft-bridge] ERROR: logger log threw: ${err instanceof Error ? err.message : String(err)}`);
8004
+ console.error(`[aft-bridge] ${message}`);
8005
+ }
8006
+ } else {
7990
8007
  log(message, meta);
8008
+ }
7991
8009
  }
7992
8010
  warnVia(message, meta) {
7993
8011
  const logger = this.logger ?? getActiveLogger();
7994
- if (logger)
7995
- logger.warn(message, meta);
7996
- else
8012
+ if (logger) {
8013
+ try {
8014
+ logger.warn(message, meta);
8015
+ } catch (err) {
8016
+ console.error(`[aft-bridge] ERROR: logger warn threw: ${err instanceof Error ? err.message : String(err)}`);
8017
+ console.error(`[aft-bridge] WARN: ${message}`);
8018
+ }
8019
+ } else {
7997
8020
  warn(message, meta);
8021
+ }
7998
8022
  }
7999
8023
  errorVia(message, meta) {
8000
8024
  const logger = this.logger ?? getActiveLogger();
8001
- if (logger)
8002
- logger.error(message, meta);
8003
- else
8025
+ if (logger) {
8026
+ try {
8027
+ logger.error(message, meta);
8028
+ } catch (err) {
8029
+ console.error(`[aft-bridge] ERROR: logger error threw: ${err instanceof Error ? err.message : String(err)}`);
8030
+ console.error(`[aft-bridge] ERROR: ${message}`);
8031
+ }
8032
+ } else {
8004
8033
  error(message, meta);
8034
+ }
8035
+ }
8036
+ getLogFilePathVia() {
8037
+ if (this.logger?.getLogFilePath) {
8038
+ try {
8039
+ return this.logger.getLogFilePath();
8040
+ } catch (err) {
8041
+ console.error(`[aft-bridge] ERROR: logger getLogFilePath threw: ${err instanceof Error ? err.message : String(err)}`);
8042
+ return;
8043
+ }
8044
+ }
8045
+ return getLogFilePath();
8046
+ }
8047
+ sessionLogVia(sessionId, message) {
8048
+ this.logVia(message, sessionId ? { sessionId } : undefined);
8049
+ }
8050
+ sessionWarnVia(sessionId, message) {
8051
+ this.warnVia(message, sessionId ? { sessionId } : undefined);
8052
+ }
8053
+ sessionErrorVia(sessionId, message) {
8054
+ this.errorVia(message, sessionId ? { sessionId } : undefined);
8005
8055
  }
8006
8056
  get restartCount() {
8007
8057
  return this._restartCount;
@@ -8060,7 +8110,7 @@ class BinaryBridge {
8060
8110
  await this.deliverConfigureWarnings(configResult, params, options);
8061
8111
  await this.checkVersion();
8062
8112
  if (!this.isAlive()) {
8063
- throw new Error(`${this.errorPrefix} Bridge died during version check. Check logs: ${getLogFilePath()}`);
8113
+ throw new Error(`${this.errorPrefix} Bridge died during version check. Check logs: ${this.getLogFilePathVia()}`);
8064
8114
  }
8065
8115
  this.configured = true;
8066
8116
  } finally {
@@ -8096,9 +8146,9 @@ class BinaryBridge {
8096
8146
  const restartSuffix = keepBridgeOnTimeout ? "" : " \u2014 restarting bridge";
8097
8147
  const timeoutMsg = `Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms${restartSuffix}`;
8098
8148
  if (requestSessionId) {
8099
- sessionWarn(requestSessionId, timeoutMsg);
8149
+ this.sessionWarnVia(requestSessionId, timeoutMsg);
8100
8150
  } else {
8101
- warn(timeoutMsg);
8151
+ this.warnVia(timeoutMsg);
8102
8152
  }
8103
8153
  reject(new Error(`${this.errorPrefix} Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
8104
8154
  if (!keepBridgeOnTimeout) {
@@ -8125,7 +8175,7 @@ class BinaryBridge {
8125
8175
  });
8126
8176
  } catch (err) {
8127
8177
  if (err instanceof BridgeReplacedDuringVersionCheck && canRetryAfterVersionSwap && command !== "configure" && command !== "version") {
8128
- log(`Retrying request "${command}" once after coordinated binary replacement: ${err.newBinaryPath}`);
8178
+ this.logVia(`Retrying request "${command}" once after coordinated binary replacement: ${err.newBinaryPath}`);
8129
8179
  return this.sendWithVersionMismatchRetry(command, params, options, false);
8130
8180
  }
8131
8181
  throw err;
@@ -8145,7 +8195,7 @@ class BinaryBridge {
8145
8195
  warnings: configResult.warnings
8146
8196
  });
8147
8197
  } catch (err) {
8148
- warn(`configure warning delivery failed: ${err instanceof Error ? err.message : String(err)}`);
8198
+ this.warnVia(`configure warning delivery failed: ${err instanceof Error ? err.message : String(err)}`);
8149
8199
  } finally {
8150
8200
  if (sessionId) {
8151
8201
  this.configureWarningClients.delete(sessionId);
@@ -8179,7 +8229,7 @@ class BinaryBridge {
8179
8229
  if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot))
8180
8230
  return;
8181
8231
  this.cachedStatus = snapshot;
8182
- log("Received status_changed push frame; cached AFT status snapshot");
8232
+ this.logVia("Received status_changed push frame; cached AFT status snapshot");
8183
8233
  for (const listener of this.statusListeners) {
8184
8234
  this.deliverStatusSnapshot(listener, this.cachedStatus);
8185
8235
  }
@@ -8188,7 +8238,7 @@ class BinaryBridge {
8188
8238
  try {
8189
8239
  listener(snapshot);
8190
8240
  } catch (err) {
8191
- warn(`status listener threw: ${err instanceof Error ? err.message : String(err)}`);
8241
+ this.warnVia(`status listener threw: ${err instanceof Error ? err.message : String(err)}`);
8192
8242
  }
8193
8243
  }
8194
8244
  async shutdown() {
@@ -8206,7 +8256,7 @@ class BinaryBridge {
8206
8256
  }, 5000);
8207
8257
  proc.once("exit", () => {
8208
8258
  clearTimeout(forceKillTimer);
8209
- log("Process exited during shutdown");
8259
+ this.logVia("Process exited during shutdown");
8210
8260
  resolve();
8211
8261
  });
8212
8262
  proc.kill("SIGTERM");
@@ -8225,9 +8275,9 @@ class BinaryBridge {
8225
8275
  if (typeof binaryVersion !== "string") {
8226
8276
  throw new Error(`Binary did not report a version \u2014 likely too old (minVersion: ${this.minVersion})`);
8227
8277
  }
8228
- log(`Binary version: ${binaryVersion}`);
8278
+ this.logVia(`Binary version: ${binaryVersion}`);
8229
8279
  if (compareSemver(binaryVersion, this.minVersion) < 0) {
8230
- warn(`Binary version ${binaryVersion} is older than required ${this.minVersion}`);
8280
+ this.warnVia(`Binary version ${binaryVersion} is older than required ${this.minVersion}`);
8231
8281
  const replacementPath = await this.onVersionMismatch?.(binaryVersion, this.minVersion);
8232
8282
  if (replacementPath === undefined) {
8233
8283
  return;
@@ -8239,7 +8289,7 @@ class BinaryBridge {
8239
8289
  throw new BridgeReplacedDuringVersionCheck(replacementPath);
8240
8290
  }
8241
8291
  } catch (err) {
8242
- warn(`Version check failed: ${err.message}`);
8292
+ this.warnVia(`Version check failed: ${err.message}`);
8243
8293
  throw err;
8244
8294
  }
8245
8295
  }
@@ -8259,7 +8309,7 @@ class BinaryBridge {
8259
8309
  }, 5000);
8260
8310
  proc.once("exit", () => {
8261
8311
  clearTimeout(forceKillTimer);
8262
- log("Process exited during coordinated binary replacement");
8312
+ this.logVia("Process exited during coordinated binary replacement");
8263
8313
  resolve();
8264
8314
  });
8265
8315
  proc.kill("SIGTERM");
@@ -8272,9 +8322,9 @@ class BinaryBridge {
8272
8322
  }
8273
8323
  spawnProcess(triggeringSessionId) {
8274
8324
  if (triggeringSessionId) {
8275
- sessionLog(triggeringSessionId, `Spawning binary: ${this.binaryPath} (cwd: ${this.cwd})`);
8325
+ this.sessionLogVia(triggeringSessionId, `Spawning binary: ${this.binaryPath} (cwd: ${this.cwd})`);
8276
8326
  } else {
8277
- log(`Spawning binary: ${this.binaryPath} (cwd: ${this.cwd})`);
8327
+ this.logVia(`Spawning binary: ${this.binaryPath} (cwd: ${this.cwd})`);
8278
8328
  }
8279
8329
  const semantic = this.configOverrides.semantic;
8280
8330
  const semanticBackend = (() => {
@@ -8321,11 +8371,12 @@ class BinaryBridge {
8321
8371
  const remaining = stderrDecoder.end();
8322
8372
  if (remaining)
8323
8373
  this.onStderrData(remaining);
8374
+ this.flushStderrBuffer();
8324
8375
  });
8325
8376
  child.on("error", (err) => {
8326
8377
  if (this.process !== currentChild)
8327
8378
  return;
8328
- error(`Process error: ${err.message}${this.formatStderrTail()}`);
8379
+ this.errorVia(`Process error: ${err.message}${this.formatStderrTail()}`);
8329
8380
  this.handleCrash();
8330
8381
  });
8331
8382
  child.on("exit", (code, signal) => {
@@ -8333,7 +8384,7 @@ class BinaryBridge {
8333
8384
  return;
8334
8385
  if (this._shuttingDown)
8335
8386
  return;
8336
- log(`Process exited: code=${code}, signal=${signal}`);
8387
+ this.logVia(`Process exited: code=${code}, signal=${signal}`);
8337
8388
  if (signal === "SIGTERM" || signal === "SIGKILL" || signal === "SIGHUP" || signal === "SIGINT") {
8338
8389
  this.process = null;
8339
8390
  this.configured = false;
@@ -8345,6 +8396,7 @@ class BinaryBridge {
8345
8396
  });
8346
8397
  this.process = child;
8347
8398
  this.stdoutBuffer = "";
8399
+ this.stderrBuffer = "";
8348
8400
  this.stderrTail = [];
8349
8401
  }
8350
8402
  pushStderrLine(line) {
@@ -8354,16 +8406,28 @@ class BinaryBridge {
8354
8406
  }
8355
8407
  }
8356
8408
  onStderrData(data) {
8357
- const lines = data.trimEnd().split(`
8358
- `);
8359
- for (const line of lines) {
8409
+ this.stderrBuffer += data;
8410
+ let newlineIdx;
8411
+ while ((newlineIdx = this.stderrBuffer.indexOf(`
8412
+ `)) !== -1) {
8413
+ const line = this.stderrBuffer.slice(0, newlineIdx).replace(/\r$/, "");
8414
+ this.stderrBuffer = this.stderrBuffer.slice(newlineIdx + 1);
8360
8415
  if (!line)
8361
8416
  continue;
8362
8417
  const tagged = tagStderrLine(line);
8363
- log(tagged);
8418
+ this.logVia(tagged);
8364
8419
  this.pushStderrLine(tagged);
8365
8420
  }
8366
8421
  }
8422
+ flushStderrBuffer() {
8423
+ const line = this.stderrBuffer.replace(/\r$/, "");
8424
+ this.stderrBuffer = "";
8425
+ if (!line)
8426
+ return;
8427
+ const tagged = tagStderrLine(line);
8428
+ this.logVia(tagged);
8429
+ this.pushStderrLine(tagged);
8430
+ }
8367
8431
  formatStderrTail() {
8368
8432
  if (this.stderrTail.length === 0)
8369
8433
  return "";
@@ -8421,7 +8485,7 @@ class BinaryBridge {
8421
8485
  }
8422
8486
  if (response.type === "configure_warnings") {
8423
8487
  this.handleConfigureWarningsFrame(response).catch((err) => {
8424
- warn(`configure warning delivery failed: ${err instanceof Error ? err.message : String(err)}`);
8488
+ this.warnVia(`configure warning delivery failed: ${err instanceof Error ? err.message : String(err)}`);
8425
8489
  });
8426
8490
  continue;
8427
8491
  }
@@ -8439,10 +8503,10 @@ class BinaryBridge {
8439
8503
  this.scheduleRestartCountReset();
8440
8504
  entry.resolve(response);
8441
8505
  } else if (typeof response.type === "string") {
8442
- log(`Ignoring unknown stdout push frame type: ${response.type}`);
8506
+ this.logVia(`Ignoring unknown stdout push frame type: ${response.type}`);
8443
8507
  }
8444
8508
  } catch (_err) {
8445
- warn(`Failed to parse stdout line: ${line}`);
8509
+ this.warnVia(`Failed to parse stdout line: ${line}`);
8446
8510
  }
8447
8511
  }
8448
8512
  }
@@ -8456,17 +8520,17 @@ class BinaryBridge {
8456
8520
  this.configured = false;
8457
8521
  const tail = this.formatStderrTail();
8458
8522
  this.stderrTail = [];
8459
- const killedMsg = tail ? `Bridge killed after timeout.${tail}` : `Bridge killed after timeout (see ${getLogFilePath()})`;
8523
+ const killedMsg = tail ? `Bridge killed after timeout.${tail}` : `Bridge killed after timeout (see ${this.getLogFilePathVia()})`;
8460
8524
  if (tail) {
8461
8525
  if (triggeringSessionId) {
8462
- sessionError(triggeringSessionId, killedMsg);
8526
+ this.sessionErrorVia(triggeringSessionId, killedMsg);
8463
8527
  } else {
8464
- error(killedMsg);
8528
+ this.errorVia(killedMsg);
8465
8529
  }
8466
8530
  } else if (triggeringSessionId) {
8467
- sessionWarn(triggeringSessionId, killedMsg);
8531
+ this.sessionWarnVia(triggeringSessionId, killedMsg);
8468
8532
  } else {
8469
- warn(killedMsg);
8533
+ this.warnVia(killedMsg);
8470
8534
  }
8471
8535
  }
8472
8536
  handleCrash(cause) {
@@ -8479,25 +8543,25 @@ class BinaryBridge {
8479
8543
  this.configured = false;
8480
8544
  const tail = this.formatStderrTail();
8481
8545
  if (tail) {
8482
- error(`Binary crashed (restarts: ${this._restartCount})${cause ? `: ${cause.message}` : ""}.${tail}`);
8546
+ this.errorVia(`Binary crashed (restarts: ${this._restartCount})${cause ? `: ${cause.message}` : ""}.${tail}`);
8483
8547
  }
8484
- this.rejectAllPending(new Error(`${this.errorPrefix} Binary crashed (restarts: ${this._restartCount})${cause ? `: ${cause.message}` : ""} (see ${getLogFilePath()})`));
8548
+ this.rejectAllPending(new Error(`${this.errorPrefix} Binary crashed (restarts: ${this._restartCount})${cause ? `: ${cause.message}` : ""} (see ${this.getLogFilePathVia()})`));
8485
8549
  if (this._restartCount < this.maxRestarts) {
8486
8550
  const delay = 100 * 2 ** this._restartCount;
8487
8551
  this._restartCount++;
8488
- log(`Auto-restart #${this._restartCount} in ${delay}ms`);
8552
+ this.logVia(`Auto-restart #${this._restartCount} in ${delay}ms`);
8489
8553
  setTimeout(() => {
8490
8554
  if (!this._shuttingDown && !this.isAlive()) {
8491
8555
  try {
8492
8556
  this.spawnProcess();
8493
8557
  } catch (err) {
8494
- error(`Failed to restart: ${err.message}`);
8558
+ this.errorVia(`Failed to restart: ${err.message}`);
8495
8559
  }
8496
8560
  }
8497
8561
  }, delay);
8498
8562
  this.scheduleRestartCountReset();
8499
8563
  } else {
8500
- error(`Max restarts (${this.maxRestarts}) reached, giving up. Logs: ${getLogFilePath()}${tail}`);
8564
+ this.errorVia(`Max restarts (${this.maxRestarts}) reached, giving up. Logs: ${this.getLogFilePathVia()}${tail}`);
8501
8565
  this.scheduleRestartCountReset();
8502
8566
  }
8503
8567
  }
@@ -8523,9 +8587,12 @@ class BinaryBridge {
8523
8587
  }
8524
8588
  }
8525
8589
  // ../aft-bridge/dist/downloader.js
8526
- import { chmodSync, existsSync, mkdirSync, unlinkSync } from "fs";
8590
+ import { createHash } from "crypto";
8591
+ import { chmodSync, closeSync, createWriteStream, existsSync, mkdirSync, openSync, renameSync, rmSync, statSync, unlinkSync } from "fs";
8527
8592
  import { homedir as homedir2 } from "os";
8528
8593
  import { join as join2 } from "path";
8594
+ import { Readable } from "stream";
8595
+ import { pipeline } from "stream/promises";
8529
8596
 
8530
8597
  // ../aft-bridge/dist/platform.js
8531
8598
  var PLATFORM_ARCH_MAP = {
@@ -8543,6 +8610,11 @@ var PLATFORM_ASSET_MAP = {
8543
8610
 
8544
8611
  // ../aft-bridge/dist/downloader.js
8545
8612
  var REPO = "cortexkit/aft";
8613
+ var DOWNLOAD_TIMEOUT_MS = 300000;
8614
+ var LATEST_TAG_TIMEOUT_MS = 30000;
8615
+ var MAX_DOWNLOAD_BYTES = 200 * 1024 * 1024;
8616
+ var DOWNLOAD_LOCK_TIMEOUT_MS = 120000;
8617
+ var DOWNLOAD_LOCK_STALE_MS = 10 * 60000;
8546
8618
  function getCacheDir() {
8547
8619
  if (process.platform === "win32") {
8548
8620
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
@@ -8584,54 +8656,101 @@ async function downloadBinary(version) {
8584
8656
  const downloadUrl = `https://github.com/${REPO}/releases/download/${tag}/${assetName}`;
8585
8657
  const checksumUrl = `https://github.com/${REPO}/releases/download/${tag}/checksums.sha256`;
8586
8658
  log(`Downloading AFT binary (${tag}) for ${platformKey}...`);
8659
+ const lockPath = join2(versionedCacheDir, ".download.lock");
8660
+ let releaseLock = null;
8661
+ let binaryController = null;
8662
+ let checksumController = null;
8663
+ let binaryTimeout = null;
8664
+ let checksumTimeout = null;
8665
+ const tmpPath = `${binaryPath}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
8587
8666
  try {
8588
8667
  if (!existsSync(versionedCacheDir)) {
8589
8668
  mkdirSync(versionedCacheDir, { recursive: true });
8590
8669
  }
8670
+ releaseLock = await acquireDownloadLock(lockPath);
8671
+ if (existsSync(binaryPath)) {
8672
+ return binaryPath;
8673
+ }
8674
+ binaryController = new AbortController;
8675
+ checksumController = new AbortController;
8676
+ const activeBinaryController = binaryController;
8677
+ const activeChecksumController = checksumController;
8678
+ binaryTimeout = setTimeout(() => activeBinaryController.abort(), DOWNLOAD_TIMEOUT_MS);
8679
+ checksumTimeout = setTimeout(() => activeChecksumController.abort(), DOWNLOAD_TIMEOUT_MS);
8591
8680
  const [binaryResponse, checksumResponse] = await Promise.all([
8592
- fetch(downloadUrl, { redirect: "follow" }),
8593
- fetch(checksumUrl, { redirect: "follow" })
8681
+ fetch(downloadUrl, { redirect: "follow", signal: activeBinaryController.signal }),
8682
+ fetch(checksumUrl, { redirect: "follow", signal: activeChecksumController.signal })
8594
8683
  ]);
8595
8684
  if (!binaryResponse.ok) {
8596
8685
  throw new Error(`HTTP ${binaryResponse.status}: ${binaryResponse.statusText} (${downloadUrl})`);
8597
8686
  }
8598
- const arrayBuffer = await binaryResponse.arrayBuffer();
8687
+ if (!binaryResponse.body) {
8688
+ throw new Error(`Download response for ${assetName} had no body`);
8689
+ }
8690
+ const advertised = Number.parseInt(binaryResponse.headers.get("content-length") ?? "", 10);
8691
+ if (Number.isFinite(advertised) && advertised > MAX_DOWNLOAD_BYTES) {
8692
+ throw new Error(`Content-Length ${advertised} exceeds max ${MAX_DOWNLOAD_BYTES}`);
8693
+ }
8599
8694
  if (!checksumResponse.ok) {
8600
8695
  warn(`Checksum verification failed: no checksums.sha256 found for ${tag}. ` + "Binary download aborted for security reasons.");
8601
8696
  return null;
8602
8697
  }
8603
8698
  const checksumText = await checksumResponse.text();
8699
+ clearTimeout(checksumTimeout);
8700
+ checksumTimeout = null;
8604
8701
  const expectedHash = parseChecksumForAsset(checksumText, assetName);
8605
8702
  if (!expectedHash) {
8606
8703
  warn(`Checksum verification failed: checksums.sha256 found but no entry for ${assetName}. ` + "Binary download aborted for security reasons.");
8607
8704
  return null;
8608
8705
  }
8609
- const { createHash } = await import("crypto");
8610
- const actualHash = createHash("sha256").update(Buffer.from(arrayBuffer)).digest("hex");
8706
+ const hash = createHash("sha256");
8707
+ let bytesWritten = 0;
8708
+ const guard = new TransformStream({
8709
+ transform(chunk, controller) {
8710
+ bytesWritten += chunk.byteLength;
8711
+ if (bytesWritten > MAX_DOWNLOAD_BYTES) {
8712
+ controller.error(new Error(`download exceeded ${MAX_DOWNLOAD_BYTES} bytes after streaming (server lied about size or sent unbounded body)`));
8713
+ return;
8714
+ }
8715
+ hash.update(chunk);
8716
+ controller.enqueue(chunk);
8717
+ }
8718
+ });
8719
+ const guarded = binaryResponse.body.pipeThrough(guard);
8720
+ const nodeStream = Readable.fromWeb(guarded);
8721
+ await pipeline(nodeStream, createWriteStream(tmpPath), { signal: binaryController.signal });
8722
+ clearTimeout(binaryTimeout);
8723
+ binaryTimeout = null;
8724
+ const actualHash = hash.digest("hex");
8611
8725
  if (actualHash !== expectedHash) {
8612
- throw new Error(`Checksum mismatch for ${assetName}: expected ${expectedHash}, got ${actualHash}. The binary may have been tampered with.`);
8726
+ throw new Error(`Checksum mismatch for ${assetName}: expected ${expectedHash}, got ${actualHash}. ` + "The binary may have been tampered with.");
8613
8727
  }
8614
8728
  log(`Checksum verified (SHA-256: ${actualHash.slice(0, 16)}...)`);
8615
- const tmpPath = `${binaryPath}.tmp`;
8616
- const { writeFileSync } = await import("fs");
8617
- writeFileSync(tmpPath, Buffer.from(arrayBuffer));
8618
8729
  if (process.platform !== "win32") {
8619
8730
  chmodSync(tmpPath, 493);
8620
8731
  }
8621
- const { renameSync } = await import("fs");
8622
8732
  renameSync(tmpPath, binaryPath);
8623
8733
  log(`AFT binary ready at ${binaryPath}`);
8624
8734
  return binaryPath;
8625
8735
  } catch (err) {
8626
8736
  const msg = err instanceof Error ? err.message : String(err);
8627
8737
  error(`Failed to download AFT binary: ${msg}`);
8628
- const tmpPath = `${binaryPath}.tmp`;
8629
8738
  if (existsSync(tmpPath)) {
8630
8739
  try {
8631
8740
  unlinkSync(tmpPath);
8632
8741
  } catch {}
8633
8742
  }
8634
8743
  return null;
8744
+ } finally {
8745
+ if (binaryTimeout) {
8746
+ binaryController?.abort();
8747
+ clearTimeout(binaryTimeout);
8748
+ }
8749
+ if (checksumTimeout) {
8750
+ checksumController?.abort();
8751
+ clearTimeout(checksumTimeout);
8752
+ }
8753
+ releaseLock?.();
8635
8754
  }
8636
8755
  }
8637
8756
  async function ensureBinary(version) {
@@ -8648,6 +8767,39 @@ async function ensureBinary(version) {
8648
8767
  log("No cached binary found, downloading latest...");
8649
8768
  return downloadBinary();
8650
8769
  }
8770
+ async function acquireDownloadLock(lockPath) {
8771
+ const startedAt = Date.now();
8772
+ while (true) {
8773
+ try {
8774
+ const fd = openSync(lockPath, "wx");
8775
+ return () => {
8776
+ try {
8777
+ closeSync(fd);
8778
+ } catch {}
8779
+ try {
8780
+ rmSync(lockPath, { force: true });
8781
+ } catch {}
8782
+ };
8783
+ } catch (err) {
8784
+ const code = err.code;
8785
+ if (code !== "EEXIST")
8786
+ throw err;
8787
+ try {
8788
+ const ageMs = Date.now() - statSync(lockPath).mtimeMs;
8789
+ if (ageMs > DOWNLOAD_LOCK_STALE_MS) {
8790
+ rmSync(lockPath, { force: true });
8791
+ continue;
8792
+ }
8793
+ } catch {
8794
+ continue;
8795
+ }
8796
+ if (Date.now() - startedAt > DOWNLOAD_LOCK_TIMEOUT_MS) {
8797
+ throw new Error(`Timed out waiting for download lock: ${lockPath}`);
8798
+ }
8799
+ await new Promise((resolve) => setTimeout(resolve, 100));
8800
+ }
8801
+ }
8802
+ }
8651
8803
  function parseChecksumForAsset(checksumText, assetName) {
8652
8804
  for (const line of checksumText.split(`
8653
8805
  `)) {
@@ -8662,9 +8814,12 @@ function parseChecksumForAsset(checksumText, assetName) {
8662
8814
  return null;
8663
8815
  }
8664
8816
  async function fetchLatestTag() {
8817
+ const controller = new AbortController;
8818
+ const timeout = setTimeout(() => controller.abort(), LATEST_TAG_TIMEOUT_MS);
8665
8819
  try {
8666
8820
  const response = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`, {
8667
- headers: { Accept: "application/vnd.github.v3+json" }
8821
+ headers: { Accept: "application/vnd.github.v3+json" },
8822
+ signal: controller.signal
8668
8823
  });
8669
8824
  if (!response.ok)
8670
8825
  return null;
@@ -8672,18 +8827,20 @@ async function fetchLatestTag() {
8672
8827
  return data.tag_name ?? null;
8673
8828
  } catch {
8674
8829
  return null;
8830
+ } finally {
8831
+ clearTimeout(timeout);
8675
8832
  }
8676
8833
  }
8677
8834
  // ../aft-bridge/dist/onnx-runtime.js
8678
8835
  import { execFileSync } from "child_process";
8679
- import { createHash } from "crypto";
8680
- import { chmodSync as chmodSync2, closeSync, copyFileSync, createWriteStream, existsSync as existsSync2, lstatSync, mkdirSync as mkdirSync2, openSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, unlinkSync as unlinkSync2, writeFileSync } from "fs";
8836
+ import { createHash as createHash2 } from "crypto";
8837
+ import { chmodSync as chmodSync2, closeSync as closeSync2, copyFileSync, createWriteStream as createWriteStream2, existsSync as existsSync2, lstatSync, mkdirSync as mkdirSync2, openSync as openSync2, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync as rmSync2, statSync as statSync2, symlinkSync, unlinkSync as unlinkSync2, writeFileSync } from "fs";
8681
8838
  import { dirname, join as join3, relative, resolve } from "path";
8682
- import { Readable } from "stream";
8683
- import { pipeline } from "stream/promises";
8839
+ import { Readable as Readable2 } from "stream";
8840
+ import { pipeline as pipeline2 } from "stream/promises";
8684
8841
  var ORT_VERSION = "1.24.4";
8685
8842
  var ORT_REPO = "microsoft/onnxruntime";
8686
- var MAX_DOWNLOAD_BYTES = 256 * 1024 * 1024;
8843
+ var MAX_DOWNLOAD_BYTES2 = 256 * 1024 * 1024;
8687
8844
  var MAX_EXTRACT_BYTES = 1 * 1024 * 1024 * 1024;
8688
8845
  var ONNX_LOCK_FILE = ".aft-onnx-installing";
8689
8846
  var ONNX_INSTALLED_META_FILE = ".aft-onnx-installed";
@@ -8804,7 +8961,7 @@ function cleanupAbandonedStagingDirs(onnxBaseDir) {
8804
8961
  if (Number.isFinite(pid) && pid > 0) {
8805
8962
  if (process.platform === "win32") {
8806
8963
  try {
8807
- const ageMs = Date.now() - statSync(stagingDir).mtimeMs;
8964
+ const ageMs = Date.now() - statSync2(stagingDir).mtimeMs;
8808
8965
  abandoned = ageMs > STALE_LOCK_MS;
8809
8966
  } catch {
8810
8967
  abandoned = true;
@@ -8818,7 +8975,7 @@ function cleanupAbandonedStagingDirs(onnxBaseDir) {
8818
8975
  if (abandoned) {
8819
8976
  log(`[onnx] removing abandoned staging dir ${stagingDir}`);
8820
8977
  try {
8821
- rmSync(stagingDir, { recursive: true, force: true });
8978
+ rmSync2(stagingDir, { recursive: true, force: true });
8822
8979
  } catch (err) {
8823
8980
  warn(`[onnx] failed to remove ${stagingDir}: ${err}`);
8824
8981
  }
@@ -8830,7 +8987,7 @@ function cleanupIncompleteTargetIfUnowned(ortDir) {
8830
8987
  try {
8831
8988
  if (existsSync2(ortDir) && !existsSync2(join3(ortDir, ONNX_INSTALLED_META_FILE))) {
8832
8989
  log(`[onnx] removing half-populated install dir ${ortDir} (no meta file)`);
8833
- rmSync(ortDir, { recursive: true, force: true });
8990
+ rmSync2(ortDir, { recursive: true, force: true });
8834
8991
  }
8835
8992
  } catch (err) {
8836
8993
  warn(`[onnx] failed to sweep ${ortDir}: ${err}`);
@@ -8910,24 +9067,24 @@ async function downloadFileWithCap(url, destPath) {
8910
9067
  throw new Error(`download failed (HTTP ${res.status})`);
8911
9068
  }
8912
9069
  const advertised = Number.parseInt(res.headers.get("content-length") ?? "", 10);
8913
- if (Number.isFinite(advertised) && advertised > MAX_DOWNLOAD_BYTES) {
8914
- throw new Error(`Content-Length ${advertised} exceeds max ${MAX_DOWNLOAD_BYTES}`);
9070
+ if (Number.isFinite(advertised) && advertised > MAX_DOWNLOAD_BYTES2) {
9071
+ throw new Error(`Content-Length ${advertised} exceeds max ${MAX_DOWNLOAD_BYTES2}`);
8915
9072
  }
8916
9073
  mkdirSync2(dirname(destPath), { recursive: true });
8917
9074
  let bytesWritten = 0;
8918
9075
  const guard = new TransformStream({
8919
9076
  transform(chunk, transformController) {
8920
9077
  bytesWritten += chunk.byteLength;
8921
- if (bytesWritten > MAX_DOWNLOAD_BYTES) {
8922
- transformController.error(new Error(`download exceeded ${MAX_DOWNLOAD_BYTES} bytes after streaming (server lied about size or sent unbounded body)`));
9078
+ if (bytesWritten > MAX_DOWNLOAD_BYTES2) {
9079
+ transformController.error(new Error(`download exceeded ${MAX_DOWNLOAD_BYTES2} bytes after streaming (server lied about size or sent unbounded body)`));
8923
9080
  return;
8924
9081
  }
8925
9082
  transformController.enqueue(chunk);
8926
9083
  }
8927
9084
  });
8928
9085
  const guarded = res.body.pipeThrough(guard);
8929
- const nodeStream = Readable.fromWeb(guarded);
8930
- await pipeline(nodeStream, createWriteStream(destPath), { signal: controller.signal });
9086
+ const nodeStream = Readable2.fromWeb(guarded);
9087
+ await pipeline2(nodeStream, createWriteStream2(destPath), { signal: controller.signal });
8931
9088
  } catch (err) {
8932
9089
  try {
8933
9090
  unlinkSync2(destPath);
@@ -9026,16 +9183,16 @@ async function downloadOnnxRuntime(info, targetDir) {
9026
9183
  warn(`Could not hash newly-installed ONNX library at ${libPath}: ${err}`);
9027
9184
  }
9028
9185
  writeOnnxInstalledMeta(targetDir, ORT_VERSION, libHash, archiveSha256);
9029
- rmSync(tmpDir, { recursive: true, force: true });
9186
+ rmSync2(tmpDir, { recursive: true, force: true });
9030
9187
  log(`ONNX Runtime v${ORT_VERSION} installed to ${targetDir}`);
9031
9188
  return targetDir;
9032
9189
  } catch (err) {
9033
9190
  error(`Failed to download ONNX Runtime: ${err}`);
9034
9191
  try {
9035
- rmSync(tmpDir, { recursive: true, force: true });
9192
+ rmSync2(tmpDir, { recursive: true, force: true });
9036
9193
  } catch {}
9037
9194
  try {
9038
- rmSync(targetDir, { recursive: true, force: true });
9195
+ rmSync2(targetDir, { recursive: true, force: true });
9039
9196
  } catch {}
9040
9197
  return null;
9041
9198
  }
@@ -9052,7 +9209,7 @@ function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, c
9052
9209
  }
9053
9210
  } catch (copyErr) {
9054
9211
  if (requiredLibs.has(libFile)) {
9055
- rmSync(targetDir, { recursive: true, force: true });
9212
+ rmSync2(targetDir, { recursive: true, force: true });
9056
9213
  throw copyErr;
9057
9214
  }
9058
9215
  log(`ORT extract: failed to copy optional ${libFile}: ${copyErr}`);
@@ -9067,7 +9224,7 @@ function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, c
9067
9224
  symlinkSync(link.target, dst);
9068
9225
  } catch (symlinkErr) {
9069
9226
  if (requiredLibs.has(link.name)) {
9070
- rmSync(targetDir, { recursive: true, force: true });
9227
+ rmSync2(targetDir, { recursive: true, force: true });
9071
9228
  throw symlinkErr;
9072
9229
  }
9073
9230
  log(`ORT extract: failed to symlink optional ${link.name}: ${symlinkErr}`);
@@ -9075,7 +9232,7 @@ function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, c
9075
9232
  }
9076
9233
  const requiredPath = join3(targetDir, info.libName);
9077
9234
  if (!existsSync2(requiredPath)) {
9078
- rmSync(targetDir, { recursive: true, force: true });
9235
+ rmSync2(targetDir, { recursive: true, force: true });
9079
9236
  throw new Error(`Required ONNX Runtime library missing after install: ${requiredPath}`);
9080
9237
  }
9081
9238
  }
@@ -9108,7 +9265,7 @@ function writeOnnxInstalledMeta(installDir, version, sha256, archiveSha256) {
9108
9265
  function readOnnxInstalledMeta(installDir) {
9109
9266
  const path = join3(installDir, ONNX_INSTALLED_META_FILE);
9110
9267
  try {
9111
- if (!statSync(path).isFile())
9268
+ if (!statSync2(path).isFile())
9112
9269
  return null;
9113
9270
  const raw = readFileSync(path, "utf8");
9114
9271
  const parsed = JSON.parse(raw);
@@ -9125,20 +9282,20 @@ function readOnnxInstalledMeta(installDir) {
9125
9282
  }
9126
9283
  }
9127
9284
  function sha256File(path) {
9128
- const hash = createHash("sha256");
9285
+ const hash = createHash2("sha256");
9129
9286
  hash.update(readFileSync(path));
9130
9287
  return hash.digest("hex");
9131
9288
  }
9132
9289
  function acquireLock(lockPath) {
9133
9290
  const tryClaim = () => {
9134
9291
  try {
9135
- const fd = openSync(lockPath, "wx");
9292
+ const fd = openSync2(lockPath, "wx");
9136
9293
  try {
9137
9294
  writeFileSync(fd, `${process.pid}
9138
9295
  ${new Date().toISOString()}
9139
9296
  `);
9140
9297
  } finally {
9141
- closeSync(fd);
9298
+ closeSync2(fd);
9142
9299
  }
9143
9300
  return true;
9144
9301
  } catch (err) {
@@ -9159,7 +9316,7 @@ ${new Date().toISOString()}
9159
9316
  const parsed = Number.parseInt(firstLine, 10);
9160
9317
  if (Number.isFinite(parsed) && parsed > 0)
9161
9318
  owningPid = parsed;
9162
- lockMtimeMs = statSync(lockPath).mtimeMs;
9319
+ lockMtimeMs = statSync2(lockPath).mtimeMs;
9163
9320
  } catch {
9164
9321
  return tryClaim();
9165
9322
  }
@@ -9225,15 +9382,6 @@ import { homedir as homedir3 } from "os";
9225
9382
  var DEFAULT_IDLE_TIMEOUT_MS = Infinity;
9226
9383
  var DEFAULT_MAX_POOL_SIZE = 8;
9227
9384
  var CLEANUP_INTERVAL_MS = 60 * 1000;
9228
-
9229
- class HomeProjectRootError extends Error {
9230
- projectRoot;
9231
- constructor(projectRoot) {
9232
- super(`aft refuses to spawn a bridge with project_root=${projectRoot} (user home directory). ` + `Open OpenCode/Pi from a project subdirectory instead, or set the session's ` + `directory to a real project root.`);
9233
- this.projectRoot = projectRoot;
9234
- this.name = "HomeProjectRootError";
9235
- }
9236
- }
9237
9385
  function canonicalHomeDir() {
9238
9386
  try {
9239
9387
  const home = homedir3();
@@ -9276,7 +9424,9 @@ class BridgePool {
9276
9424
  onVersionMismatch: options.onVersionMismatch,
9277
9425
  onConfigureWarnings: options.onConfigureWarnings,
9278
9426
  onBashCompletion: options.onBashCompletion,
9279
- onBashLongRunning: options.onBashLongRunning
9427
+ onBashLongRunning: options.onBashLongRunning,
9428
+ errorPrefix: options.errorPrefix,
9429
+ logger: options.logger
9280
9430
  };
9281
9431
  this.configOverrides = configOverrides;
9282
9432
  if (Number.isFinite(this.idleTimeoutMs)) {
@@ -9294,9 +9444,6 @@ class BridgePool {
9294
9444
  }
9295
9445
  getBridge(projectRoot) {
9296
9446
  const key = normalizeKey(projectRoot);
9297
- if (isHomeDirectoryRoot(key)) {
9298
- throw new HomeProjectRootError(key);
9299
- }
9300
9447
  const existing = this.bridges.get(key);
9301
9448
  if (existing) {
9302
9449
  existing.lastUsed = Date.now();
@@ -9348,24 +9495,32 @@ class BridgePool {
9348
9495
  }
9349
9496
  async replaceBinary(newPath) {
9350
9497
  this.binaryPath = newPath;
9351
- const shutdowns = Array.from(this.bridges.values()).map((entry) => entry.bridge.shutdown());
9352
9498
  this.bridges.clear();
9353
- await Promise.allSettled(shutdowns);
9354
9499
  this.log(`Binary path updated to ${newPath}. All bridges cleared \u2014 next calls will use the new binary.`);
9355
9500
  return newPath;
9356
9501
  }
9357
9502
  log(message, meta) {
9358
9503
  const logger = this.logger ?? getActiveLogger();
9359
- if (logger)
9360
- logger.log(message, meta);
9361
- else
9504
+ if (logger) {
9505
+ try {
9506
+ logger.log(message, meta);
9507
+ } catch (err) {
9508
+ console.error(`[aft-bridge] ERROR: pool logger log threw: ${err instanceof Error ? err.message : String(err)}`);
9509
+ console.error(`[aft-bridge] ${message}`);
9510
+ }
9511
+ } else
9362
9512
  log(message, meta);
9363
9513
  }
9364
9514
  error(message, meta) {
9365
9515
  const logger = this.logger ?? getActiveLogger();
9366
- if (logger)
9367
- logger.error(message, meta);
9368
- else
9516
+ if (logger) {
9517
+ try {
9518
+ logger.error(message, meta);
9519
+ } catch (err) {
9520
+ console.error(`[aft-bridge] ERROR: pool logger error threw: ${err instanceof Error ? err.message : String(err)}`);
9521
+ console.error(`[aft-bridge] ERROR: ${message}`);
9522
+ }
9523
+ } else
9369
9524
  error(message, meta);
9370
9525
  }
9371
9526
  setConfigureOverride(key, value) {
@@ -9392,7 +9547,7 @@ function normalizeKey(projectRoot) {
9392
9547
  }
9393
9548
  // ../aft-bridge/dist/resolver.js
9394
9549
  import { execSync, spawnSync } from "child_process";
9395
- import { chmodSync as chmodSync3, copyFileSync as copyFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync3, renameSync } from "fs";
9550
+ import { chmodSync as chmodSync3, copyFileSync as copyFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync3, renameSync as renameSync2 } from "fs";
9396
9551
  import { createRequire } from "module";
9397
9552
  import { homedir as homedir4 } from "os";
9398
9553
  import { join as join4 } from "path";
@@ -9423,15 +9578,19 @@ function copyToVersionedCache(npmBinaryPath, knownVersion) {
9423
9578
  const versionedDir = join4(cacheDir, tag);
9424
9579
  const ext = process.platform === "win32" ? ".exe" : "";
9425
9580
  const cachedPath = join4(versionedDir, `aft${ext}`);
9426
- if (existsSync3(cachedPath))
9427
- return cachedPath;
9581
+ if (existsSync3(cachedPath)) {
9582
+ const cachedVersion = readBinaryVersion(cachedPath);
9583
+ if (cachedVersion === version)
9584
+ return cachedPath;
9585
+ warn(`Cached binary at ${cachedPath} reports ${cachedVersion ?? "no version"}, expected ${version}; refreshing from npm package`);
9586
+ }
9428
9587
  mkdirSync3(versionedDir, { recursive: true });
9429
- const tmpPath = `${cachedPath}.tmp`;
9588
+ const tmpPath = `${cachedPath}.${process.pid}.${Date.now()}.tmp`;
9430
9589
  copyFileSync2(npmBinaryPath, tmpPath);
9431
9590
  if (process.platform !== "win32") {
9432
9591
  chmodSync3(tmpPath, 493);
9433
9592
  }
9434
- renameSync(tmpPath, cachedPath);
9593
+ renameSync2(tmpPath, cachedPath);
9435
9594
  log(`Copied npm binary to versioned cache: ${cachedPath}`);
9436
9595
  return cachedPath;
9437
9596
  } catch (err) {
@@ -9439,6 +9598,32 @@ function copyToVersionedCache(npmBinaryPath, knownVersion) {
9439
9598
  return null;
9440
9599
  }
9441
9600
  }
9601
+ function normalizeBareVersion(version) {
9602
+ return version.startsWith("v") ? version.slice(1) : version;
9603
+ }
9604
+ function homeDirFromEnv(env) {
9605
+ return (process.platform === "win32" ? env.USERPROFILE || env.HOME : env.HOME) || homedir4();
9606
+ }
9607
+ function cacheDirFromEnv(env) {
9608
+ if (process.platform === "win32") {
9609
+ const base2 = env.LOCALAPPDATA || env.APPDATA || join4(homeDirFromEnv(env), "AppData", "Local");
9610
+ return join4(base2, "aft", "bin");
9611
+ }
9612
+ const base = env.XDG_CACHE_HOME || join4(homeDirFromEnv(env), ".cache");
9613
+ return join4(base, "aft", "bin");
9614
+ }
9615
+ function cachedBinaryPathFromEnv(version, env, ext) {
9616
+ const binaryPath = join4(cacheDirFromEnv(env), version, `aft${ext}`);
9617
+ return existsSync3(binaryPath) ? binaryPath : null;
9618
+ }
9619
+ function isExpectedCachedBinary(binaryPath, expectedVersion) {
9620
+ const expected = normalizeBareVersion(expectedVersion);
9621
+ const actual = readBinaryVersion(binaryPath);
9622
+ if (actual === expected)
9623
+ return true;
9624
+ warn(`Cached binary at ${binaryPath} reports ${actual ?? "no version"}, expected ${expected}; skipping cache candidate`);
9625
+ return false;
9626
+ }
9442
9627
  function platformKey(platform = process.platform, arch = process.arch) {
9443
9628
  const archMap = PLATFORM_ARCH_MAP[platform];
9444
9629
  if (!archMap) {
@@ -9452,6 +9637,7 @@ function platformKey(platform = process.platform, arch = process.arch) {
9452
9637
  }
9453
9638
  function findBinarySync(expectedVersion) {
9454
9639
  const ext = process.platform === "win32" ? ".exe" : "";
9640
+ const env = { ...process.env };
9455
9641
  const pluginVersion = expectedVersion ?? (() => {
9456
9642
  try {
9457
9643
  const req = createRequire(import.meta.url);
@@ -9462,8 +9648,8 @@ function findBinarySync(expectedVersion) {
9462
9648
  })();
9463
9649
  if (pluginVersion) {
9464
9650
  const tag = pluginVersion.startsWith("v") ? pluginVersion : `v${pluginVersion}`;
9465
- const versionCached = getCachedBinaryPath(tag);
9466
- if (versionCached)
9651
+ const versionCached = cachedBinaryPathFromEnv(tag, env, ext);
9652
+ if (versionCached && isExpectedCachedBinary(versionCached, pluginVersion))
9467
9653
  return versionCached;
9468
9654
  }
9469
9655
  try {
@@ -9473,10 +9659,12 @@ function findBinarySync(expectedVersion) {
9473
9659
  const resolved = req.resolve(packageBin);
9474
9660
  if (existsSync3(resolved)) {
9475
9661
  const npmVersion = readBinaryVersion(resolved);
9476
- if (pluginVersion && npmVersion && npmVersion !== pluginVersion) {
9662
+ if (npmVersion === null) {
9663
+ warn(`npm platform package binary at ${resolved} did not report a version; skipping (continuing to PATH lookup)`);
9664
+ } else if (pluginVersion && npmVersion !== normalizeBareVersion(pluginVersion)) {
9477
9665
  warn(`npm platform package binary v${npmVersion} does not match plugin v${pluginVersion}; skipping (continuing to PATH lookup)`);
9478
9666
  } else {
9479
- const copied = copyToVersionedCache(resolved, npmVersion ?? undefined);
9667
+ const copied = copyToVersionedCache(resolved, npmVersion);
9480
9668
  return copied ?? resolved;
9481
9669
  }
9482
9670
  }
@@ -9485,12 +9673,13 @@ function findBinarySync(expectedVersion) {
9485
9673
  const whichCmd = process.platform === "win32" ? "where aft" : "which aft";
9486
9674
  const result = execSync(whichCmd, {
9487
9675
  encoding: "utf-8",
9676
+ env,
9488
9677
  stdio: ["pipe", "pipe", "pipe"]
9489
9678
  }).trim();
9490
9679
  if (result)
9491
9680
  return result;
9492
9681
  } catch {}
9493
- const cargoPath = join4(homedir4(), ".cargo", "bin", `aft${ext}`);
9682
+ const cargoPath = join4(homeDirFromEnv(env), ".cargo", "bin", `aft${ext}`);
9494
9683
  if (existsSync3(cargoPath))
9495
9684
  return cargoPath;
9496
9685
  return null;
@@ -9525,7 +9714,7 @@ async function findBinary(expectedVersion) {
9525
9714
  `));
9526
9715
  }
9527
9716
  // ../aft-bridge/dist/url-fetch.js
9528
- import { createHash as createHash2 } from "crypto";
9717
+ import { createHash as createHash3 } from "crypto";
9529
9718
  import { lookup } from "dns/promises";
9530
9719
  import { existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2 } from "fs";
9531
9720
  import { isIP } from "net";
@@ -9539,7 +9728,7 @@ function cacheDir(storageDir) {
9539
9728
  return join5(storageDir, "url_cache");
9540
9729
  }
9541
9730
  function hashUrl(url) {
9542
- return createHash2("sha256").update(url).digest("hex").slice(0, 16);
9731
+ return createHash3("sha256").update(url).digest("hex").slice(0, 16);
9543
9732
  }
9544
9733
  function metaPath(storageDir, hash) {
9545
9734
  return join5(cacheDir(storageDir), `${hash}.meta.json`);
@@ -9797,8 +9986,8 @@ async function fetchUrlToTempFile(url, storageDir, options = {}) {
9797
9986
  const contentFile = contentPath(storageDir, hash, extension);
9798
9987
  const tmpContent = `${contentFile}.tmp-${process.pid}`;
9799
9988
  writeFileSync2(tmpContent, body);
9800
- const { renameSync: renameSync2 } = await import("fs");
9801
- renameSync2(tmpContent, contentFile);
9989
+ const { renameSync: renameSync3 } = await import("fs");
9990
+ renameSync3(tmpContent, contentFile);
9802
9991
  const meta = {
9803
9992
  url,
9804
9993
  contentType,
@@ -9807,7 +9996,7 @@ async function fetchUrlToTempFile(url, storageDir, options = {}) {
9807
9996
  };
9808
9997
  const tmpMeta = `${metaFile}.tmp-${process.pid}`;
9809
9998
  writeFileSync2(tmpMeta, JSON.stringify(meta));
9810
- renameSync2(tmpMeta, metaFile);
9999
+ renameSync3(tmpMeta, metaFile);
9811
10000
  log(`URL cached (${total} bytes): ${url}`);
9812
10001
  return contentFile;
9813
10002
  }
@@ -9961,24 +10150,24 @@ function warn2(message, data) {
9961
10150
  function error2(message, data) {
9962
10151
  write("ERROR", message, data);
9963
10152
  }
9964
- function sessionLog2(sessionId, message, data) {
10153
+ function sessionLog(sessionId, message, data) {
9965
10154
  write("INFO", message, data, sessionId);
9966
10155
  }
9967
- function sessionWarn2(sessionId, message, data) {
10156
+ function sessionWarn(sessionId, message, data) {
9968
10157
  write("WARN", message, data, sessionId);
9969
10158
  }
9970
- function sessionError2(sessionId, message, data) {
10159
+ function sessionError(sessionId, message, data) {
9971
10160
  write("ERROR", message, data, sessionId);
9972
10161
  }
9973
10162
  var bridgeLogger = {
9974
10163
  log(message, meta) {
9975
- sessionLog2(meta?.sessionId, message);
10164
+ sessionLog(meta?.sessionId, message);
9976
10165
  },
9977
10166
  warn(message, meta) {
9978
- sessionWarn2(meta?.sessionId, message);
10167
+ sessionWarn(meta?.sessionId, message);
9979
10168
  },
9980
10169
  error(message, meta) {
9981
- sessionError2(meta?.sessionId, message);
10170
+ sessionError(meta?.sessionId, message);
9982
10171
  },
9983
10172
  getLogFilePath: () => logFile
9984
10173
  };
@@ -10073,6 +10262,7 @@ var sessionBgStates = new Map;
10073
10262
  var SESSION_BG_STATE_IDLE_TTL_MS = 60 * 60 * 1000;
10074
10263
  var DEBOUNCE_STEP_MS = 200;
10075
10264
  var DEBOUNCE_CAP_MS = 1000;
10265
+ var MAX_WAKE_SEND_ATTEMPTS = 5;
10076
10266
  var UNKNOWN_COMPLETION_TTL_MS = 5000;
10077
10267
  var UNKNOWN_COMPLETION_CAP = 32;
10078
10268
  var DEFAULT_SESSION_ID = "__default__";
@@ -10125,17 +10315,24 @@ async function appendInTurnBgCompletions(drainContext, output) {
10125
10315
  return;
10126
10316
  const state = stateFor(drainContext.sessionID);
10127
10317
  if (state.outstandingTaskIds.size === 0 && state.pendingCompletions.length === 0 && state.pendingLongRunning.length === 0) {
10128
- return;
10318
+ await drainCompletions(drainContext);
10319
+ if (state.outstandingTaskIds.size === 0 && state.pendingCompletions.length === 0 && state.pendingLongRunning.length === 0) {
10320
+ return;
10321
+ }
10129
10322
  }
10130
- if (state.outstandingTaskIds.size > 0) {
10323
+ if (state.outstandingTaskIds.size > 0 || !state.forcedDrainCompleted) {
10131
10324
  await drainCompletions(drainContext);
10132
10325
  }
10133
10326
  if (state.pendingCompletions.length === 0 && state.pendingLongRunning.length === 0)
10134
10327
  return;
10328
+ const deliveredCompletions = [...state.pendingCompletions];
10135
10329
  const reminder = formatCombinedSystemReminder(state.pendingCompletions, state.pendingLongRunning);
10136
10330
  output.output = appendReminder(output.output ?? "", reminder);
10137
10331
  state.pendingCompletions = [];
10138
10332
  state.pendingLongRunning = [];
10333
+ state.wakeRetryAttempts = 0;
10334
+ state.wakeHardStopped = false;
10335
+ await ackCompletions(drainContext, deliveredCompletions);
10139
10336
  if (state.debounceTimer) {
10140
10337
  clearTimeout(state.debounceTimer);
10141
10338
  state.debounceTimer = null;
@@ -10149,12 +10346,12 @@ async function handleIdleBgCompletions(drainContext) {
10149
10346
  }
10150
10347
  async function triggerWakeIfPending(drainContext, skipDrain) {
10151
10348
  const state = stateFor(drainContext.sessionID);
10152
- if (!skipDrain && state.outstandingTaskIds.size > 0) {
10349
+ if (!skipDrain && (state.outstandingTaskIds.size > 0 || !state.forcedDrainCompleted)) {
10153
10350
  await drainCompletions(drainContext);
10154
10351
  }
10155
10352
  if (state.pendingCompletions.length === 0 && state.pendingLongRunning.length === 0)
10156
10353
  return;
10157
- scheduleWake(state, async (reminder) => {
10354
+ scheduleWake(state, async (reminder, deliveredCompletions) => {
10158
10355
  const client = drainContext.client;
10159
10356
  if (typeof client.session?.promptAsync !== "function") {
10160
10357
  throw new Error("client.session.promptAsync is unavailable");
@@ -10178,8 +10375,9 @@ async function triggerWakeIfPending(drainContext, skipDrain) {
10178
10375
  path: { id: drainContext.sessionID },
10179
10376
  body
10180
10377
  });
10181
- }, (err) => {
10182
- sessionWarn2(drainContext.sessionID, `${LOG_PREFIX} wake send failed: ${err instanceof Error ? err.message : String(err)}`);
10378
+ await ackCompletions(drainContext, deliveredCompletions);
10379
+ }, (err, hardStopped) => {
10380
+ 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)}`);
10183
10381
  });
10184
10382
  }
10185
10383
  function formatSystemReminder(completions) {
@@ -10230,19 +10428,40 @@ function extractSessionID(value) {
10230
10428
  return;
10231
10429
  }
10232
10430
  async function drainCompletions({ ctx, directory, sessionID }) {
10431
+ const state = stateFor(sessionID);
10233
10432
  try {
10234
10433
  const bridge = ctx.pool.getActiveBridgeForRoot(directory) ?? ctx.pool.getBridge(directory);
10235
10434
  const response = await bridge.send("bash_drain_completions", { session_id: sessionID });
10236
10435
  if (response.success === false) {
10237
- sessionWarn2(sessionID, `${LOG_PREFIX} drain failed: ${String(response.message ?? "unknown error")}`);
10436
+ sessionWarn(sessionID, `${LOG_PREFIX} drain failed: ${String(response.message ?? "unknown error")}`);
10238
10437
  return;
10239
10438
  }
10240
- ingestBgCompletions(sessionID, response.bg_completions);
10439
+ state.forcedDrainCompleted = true;
10440
+ ingestDrainedBgCompletions(sessionID, response.bg_completions);
10241
10441
  } catch (err) {
10242
- sessionWarn2(sessionID, `${LOG_PREFIX} drain failed: ${err instanceof Error ? err.message : String(err)}`);
10442
+ sessionWarn(sessionID, `${LOG_PREFIX} drain failed: ${err instanceof Error ? err.message : String(err)}`);
10443
+ }
10444
+ }
10445
+ async function ackCompletions({ ctx, directory, sessionID }, completions) {
10446
+ const taskIds = [...new Set(completions.map((completion) => completion.task_id))];
10447
+ if (taskIds.length === 0)
10448
+ return;
10449
+ try {
10450
+ const bridge = ctx.pool.getActiveBridgeForRoot(directory) ?? ctx.pool.getBridge(directory);
10451
+ const response = await bridge.send("bash_ack_completions", {
10452
+ session_id: sessionID,
10453
+ task_ids: taskIds
10454
+ });
10455
+ if (response.success === false) {
10456
+ sessionWarn(sessionID, `${LOG_PREFIX} ack failed: ${String(response.message ?? "unknown error")}`);
10457
+ }
10458
+ } catch (err) {
10459
+ sessionWarn(sessionID, `${LOG_PREFIX} ack failed: ${err instanceof Error ? err.message : String(err)}`);
10243
10460
  }
10244
10461
  }
10245
10462
  function scheduleWake(state, sendWake, onSendFailure) {
10463
+ if (state.wakeHardStopped)
10464
+ return;
10246
10465
  const now = Date.now();
10247
10466
  const pendingCount = state.pendingCompletions.length + state.pendingLongRunning.length;
10248
10467
  if (state.debounceTimer && pendingCount <= state.scheduledCompletionCount) {
@@ -10271,13 +10490,22 @@ function scheduleWake(state, sendWake, onSendFailure) {
10271
10490
  const reminder = formatCombinedSystemReminder(pending, pendingLongRunning);
10272
10491
  state.pendingCompletions = [];
10273
10492
  state.pendingLongRunning = [];
10274
- sendWake(reminder).then(() => {
10493
+ sendWake(reminder, pending).then(() => {
10275
10494
  state.retryDelayMs = null;
10495
+ state.wakeRetryAttempts = 0;
10496
+ state.wakeHardStopped = false;
10276
10497
  }).catch((err) => {
10277
10498
  state.pendingCompletions = [...pending, ...state.pendingCompletions];
10278
10499
  state.pendingLongRunning = [...pendingLongRunning, ...state.pendingLongRunning];
10500
+ state.wakeRetryAttempts += 1;
10501
+ if (state.wakeRetryAttempts >= MAX_WAKE_SEND_ATTEMPTS) {
10502
+ state.retryDelayMs = null;
10503
+ state.wakeHardStopped = true;
10504
+ onSendFailure(err, true);
10505
+ return;
10506
+ }
10279
10507
  state.retryDelayMs = Math.min((delay || DEBOUNCE_STEP_MS) * 2, DEBOUNCE_CAP_MS);
10280
- onSendFailure(err);
10508
+ onSendFailure(err, false);
10281
10509
  scheduleWake(state, sendWake, onSendFailure);
10282
10510
  });
10283
10511
  }, delay);
@@ -10298,6 +10526,9 @@ function stateFor(sessionID) {
10298
10526
  scheduledFireAt: null,
10299
10527
  scheduledCompletionCount: 0,
10300
10528
  retryDelayMs: null,
10529
+ wakeRetryAttempts: 0,
10530
+ wakeHardStopped: false,
10531
+ forcedDrainCompleted: false,
10301
10532
  unknownCompletions: [],
10302
10533
  lastSeenAt: now
10303
10534
  };
@@ -10307,6 +10538,22 @@ function stateFor(sessionID) {
10307
10538
  }
10308
10539
  return state;
10309
10540
  }
10541
+ function ingestDrainedBgCompletions(sessionID, completions) {
10542
+ if (!Array.isArray(completions) || completions.length === 0)
10543
+ return [];
10544
+ const state = stateFor(sessionID);
10545
+ const accepted = [];
10546
+ for (const completion of completions) {
10547
+ if (!isBgCompletion(completion))
10548
+ continue;
10549
+ state.outstandingTaskIds.delete(completion.task_id);
10550
+ if (!state.pendingCompletions.some((pending) => pending.task_id === completion.task_id) && !accepted.some((pending) => pending.task_id === completion.task_id)) {
10551
+ accepted.push(completion);
10552
+ }
10553
+ }
10554
+ state.pendingCompletions.push(...accepted);
10555
+ return accepted;
10556
+ }
10310
10557
  function cleanupIdleSessionStates(now) {
10311
10558
  const cutoff = now - SESSION_BG_STATE_IDLE_TTL_MS;
10312
10559
  for (const [sessionID, state] of sessionBgStates) {
@@ -10397,7 +10644,7 @@ function formatDuration(completion) {
10397
10644
 
10398
10645
  // src/config.ts
10399
10646
  var import_comment_json = __toESM(require_src2(), 1);
10400
- import { existsSync as existsSync5, readFileSync as readFileSync3, renameSync as renameSync2, unlinkSync as unlinkSync4, writeFileSync as writeFileSync3 } from "fs";
10647
+ import { existsSync as existsSync5, readFileSync as readFileSync3, renameSync as renameSync3, unlinkSync as unlinkSync4, writeFileSync as writeFileSync3 } from "fs";
10401
10648
  import { homedir as homedir5 } from "os";
10402
10649
  import { join as join7 } from "path";
10403
10650
 
@@ -24176,7 +24423,7 @@ function migrateAftConfigFile(configPath, logger = { log: log2, warn: warn2 }) {
24176
24423
  ${serialized}` : serialized;
24177
24424
  tmpPath = `${configPath}.tmp.${process.pid}`;
24178
24425
  writeFileSync3(tmpPath, nextContent, "utf-8");
24179
- renameSync2(tmpPath, configPath);
24426
+ renameSync3(tmpPath, configPath);
24180
24427
  logger.log(`Migrated config at ${configPath}: removed ${oldKeys.join(", ")}`);
24181
24428
  return { migrated: true, oldKeys };
24182
24429
  } catch (err) {
@@ -24411,19 +24658,19 @@ function loadAftConfig(projectDirectory) {
24411
24658
  }
24412
24659
 
24413
24660
  // src/hooks/auto-update-checker/index.ts
24414
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync6, renameSync as renameSync3, writeFileSync as writeFileSync6 } from "fs";
24661
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync6, renameSync as renameSync4, writeFileSync as writeFileSync6 } from "fs";
24415
24662
  import { dirname as dirname4, join as join11 } from "path";
24416
24663
 
24417
24664
  // src/hooks/auto-update-checker/cache.ts
24418
24665
  var import_comment_json3 = __toESM(require_src2(), 1);
24419
24666
  import { spawn as spawn2 } from "child_process";
24420
- import { cpSync, existsSync as existsSync7, mkdtempSync, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
24667
+ import { cpSync, existsSync as existsSync7, mkdtempSync, readFileSync as readFileSync5, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
24421
24668
  import { tmpdir as tmpdir2 } from "os";
24422
24669
  import { basename, dirname as dirname3, join as join10 } from "path";
24423
24670
 
24424
24671
  // src/hooks/auto-update-checker/checker.ts
24425
24672
  var import_comment_json2 = __toESM(require_src2(), 1);
24426
- import { existsSync as existsSync6, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
24673
+ import { existsSync as existsSync6, readFileSync as readFileSync4, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
24427
24674
  import { homedir as homedir7 } from "os";
24428
24675
  import { dirname as dirname2, isAbsolute, join as join9, resolve as resolve2 } from "path";
24429
24676
  import { fileURLToPath } from "url";
@@ -24550,7 +24797,7 @@ function getLocalDevPath(directory) {
24550
24797
  }
24551
24798
  function findPackageJsonUp(startPath) {
24552
24799
  try {
24553
- const stat = statSync2(startPath);
24800
+ const stat = statSync3(startPath);
24554
24801
  let dir = stat.isDirectory() ? startPath : dirname2(startPath);
24555
24802
  for (let i = 0;i < 10; i++) {
24556
24803
  const pkgPath = join9(dir, "package.json");
@@ -24690,19 +24937,19 @@ function createAutoUpdateSnapshot(installDir, packageJsonPath, packageName) {
24690
24937
  function restoreAutoUpdateSnapshot(snapshot) {
24691
24938
  try {
24692
24939
  if (snapshot.packageJson === null)
24693
- rmSync2(snapshot.packageJsonPath, { force: true });
24940
+ rmSync3(snapshot.packageJsonPath, { force: true });
24694
24941
  else
24695
24942
  writeFileSync5(snapshot.packageJsonPath, snapshot.packageJson);
24696
24943
  if (snapshot.lockfile === null)
24697
- rmSync2(snapshot.lockfilePath, { force: true });
24944
+ rmSync3(snapshot.lockfilePath, { force: true });
24698
24945
  else
24699
24946
  writeFileSync5(snapshot.lockfilePath, snapshot.lockfile);
24700
- rmSync2(snapshot.packageDir, { recursive: true, force: true });
24947
+ rmSync3(snapshot.packageDir, { recursive: true, force: true });
24701
24948
  if (snapshot.stagedPackageDir) {
24702
24949
  cpSync(snapshot.stagedPackageDir, snapshot.packageDir, { recursive: true });
24703
24950
  }
24704
24951
  } finally {
24705
- rmSync2(snapshot.tempDir, { recursive: true, force: true });
24952
+ rmSync3(snapshot.tempDir, { recursive: true, force: true });
24706
24953
  }
24707
24954
  }
24708
24955
  function stripPackageNameFromPath(pathValue, packageName) {
@@ -24767,7 +25014,7 @@ function removeInstalledPackage(installDir, packageName) {
24767
25014
  const packageDir = join10(installDir, "node_modules", packageName);
24768
25015
  if (!existsSync7(packageDir))
24769
25016
  return false;
24770
- rmSync2(packageDir, { recursive: true, force: true });
25017
+ rmSync3(packageDir, { recursive: true, force: true });
24771
25018
  log2(`[auto-update-checker] Package removed: ${packageDir}`);
24772
25019
  return true;
24773
25020
  }
@@ -24852,7 +25099,7 @@ async function runNpmInstallSafe(installDir, options = {}) {
24852
25099
  if (!result && snapshot) {
24853
25100
  restoreAutoUpdateSnapshot(snapshot);
24854
25101
  } else if (snapshot) {
24855
- rmSync2(snapshot.tempDir, { recursive: true, force: true });
25102
+ rmSync3(snapshot.tempDir, { recursive: true, force: true });
24856
25103
  }
24857
25104
  return result;
24858
25105
  } catch (err) {
@@ -24936,7 +25183,7 @@ function claimCheckSlot(storageDir, intervalMs) {
24936
25183
  mkdirSync5(dirname4(file2), { recursive: true });
24937
25184
  const tmp = `${file2}.tmp.${process.pid}`;
24938
25185
  writeFileSync6(tmp, JSON.stringify({ lastCheckedMs: Date.now() }), "utf-8");
24939
- renameSync3(tmp, file2);
25186
+ renameSync4(tmp, file2);
24940
25187
  return true;
24941
25188
  } catch (err) {
24942
25189
  warn2(`[auto-update-checker] Could not coordinate via timestamp file: ${String(err)}`);
@@ -25023,17 +25270,17 @@ function showToast(ctx, title, message, variant = "info", duration3 = 3000) {
25023
25270
 
25024
25271
  // src/lsp-auto-install.ts
25025
25272
  import { spawn as spawn3 } from "child_process";
25026
- import { createHash as createHash3 } from "crypto";
25027
- import { createReadStream, mkdirSync as mkdirSync7, readFileSync as readFileSync8, renameSync as renameSync4, rmSync as rmSync3, statSync as statSync4 } from "fs";
25273
+ import { createHash as createHash4 } from "crypto";
25274
+ import { createReadStream, mkdirSync as mkdirSync7, readFileSync as readFileSync8, renameSync as renameSync5, rmSync as rmSync4, statSync as statSync5 } from "fs";
25028
25275
  import { join as join14 } from "path";
25029
25276
 
25030
25277
  // src/lsp-cache.ts
25031
25278
  import {
25032
- closeSync as closeSync2,
25279
+ closeSync as closeSync3,
25033
25280
  mkdirSync as mkdirSync6,
25034
- openSync as openSync2,
25281
+ openSync as openSync3,
25035
25282
  readFileSync as readFileSync7,
25036
- statSync as statSync3,
25283
+ statSync as statSync4,
25037
25284
  unlinkSync as unlinkSync5,
25038
25285
  writeFileSync as writeFileSync7
25039
25286
  } from "fs";
@@ -25066,7 +25313,7 @@ function lspBinDir(npmPackage) {
25066
25313
  function isInstalled(npmPackage, binary) {
25067
25314
  for (const candidate of lspBinaryCandidates(binary)) {
25068
25315
  try {
25069
- if (statSync3(join12(lspBinDir(npmPackage), candidate)).isFile())
25316
+ if (statSync4(join12(lspBinDir(npmPackage), candidate)).isFile())
25070
25317
  return true;
25071
25318
  } catch {}
25072
25319
  }
@@ -25094,7 +25341,7 @@ function writeInstalledMetaIn(installDir, version2, sha256) {
25094
25341
  function readInstalledMetaIn(installDir) {
25095
25342
  const path2 = join12(installDir, INSTALLED_META_FILE);
25096
25343
  try {
25097
- if (!statSync3(path2).isFile())
25344
+ if (!statSync4(path2).isFile())
25098
25345
  return null;
25099
25346
  const raw = readFileSync7(path2, "utf8");
25100
25347
  const parsed = JSON.parse(raw);
@@ -25124,13 +25371,13 @@ function acquireInstallLock(lockKey) {
25124
25371
  const lock = lockPath(lockKey);
25125
25372
  const tryClaim = () => {
25126
25373
  try {
25127
- const fd = openSync2(lock, "wx");
25374
+ const fd = openSync3(lock, "wx");
25128
25375
  try {
25129
25376
  writeFileSync7(fd, `${process.pid}
25130
25377
  ${new Date().toISOString()}
25131
25378
  `);
25132
25379
  } finally {
25133
- closeSync2(fd);
25380
+ closeSync3(fd);
25134
25381
  }
25135
25382
  return true;
25136
25383
  } catch (err) {
@@ -25151,7 +25398,7 @@ ${new Date().toISOString()}
25151
25398
  const parsed = Number.parseInt(firstLine, 10);
25152
25399
  if (Number.isFinite(parsed) && parsed > 0)
25153
25400
  owningPid = parsed;
25154
- lockMtimeMs = statSync3(lock).mtimeMs;
25401
+ lockMtimeMs = statSync4(lock).mtimeMs;
25155
25402
  } catch {
25156
25403
  return tryClaim();
25157
25404
  }
@@ -25692,7 +25939,7 @@ function hashInstalledBinary(spec) {
25692
25939
  let pathToHash = null;
25693
25940
  for (const p of candidates) {
25694
25941
  try {
25695
- if (statSync4(p).isFile()) {
25942
+ if (statSync5(p).isFile()) {
25696
25943
  pathToHash = p;
25697
25944
  break;
25698
25945
  }
@@ -25702,7 +25949,7 @@ function hashInstalledBinary(spec) {
25702
25949
  reject(new Error(`installed binary not found at any of: ${candidates.join(", ")}`));
25703
25950
  return;
25704
25951
  }
25705
- const hash2 = createHash3("sha256");
25952
+ const hash2 = createHash4("sha256");
25706
25953
  const stream = createReadStream(pathToHash);
25707
25954
  stream.on("error", reject);
25708
25955
  stream.on("data", (chunk) => hash2.update(chunk));
@@ -25718,14 +25965,14 @@ function installedBinaryPath(spec) {
25718
25965
  ] : [lspBinaryPath(spec.npm, spec.binary)];
25719
25966
  for (const candidate of candidates) {
25720
25967
  try {
25721
- if (statSync4(candidate).isFile())
25968
+ if (statSync5(candidate).isFile())
25722
25969
  return candidate;
25723
25970
  } catch {}
25724
25971
  }
25725
25972
  return null;
25726
25973
  }
25727
25974
  function sha256OfFileSync(path2) {
25728
- return createHash3("sha256").update(readFileSync8(path2)).digest("hex");
25975
+ return createHash4("sha256").update(readFileSync8(path2)).digest("hex");
25729
25976
  }
25730
25977
  function quarantineCachedNpmInstall(spec, reason) {
25731
25978
  const packageDir = lspPackageDir(spec.npm);
@@ -25733,8 +25980,8 @@ function quarantineCachedNpmInstall(spec, reason) {
25733
25980
  warn2(`[lsp] tofu_mismatch ${spec.npm}: ${reason}; quarantining ${packageDir} -> ${dest}`);
25734
25981
  try {
25735
25982
  mkdirSync7(join14(dest, ".."), { recursive: true });
25736
- rmSync3(dest, { recursive: true, force: true });
25737
- renameSync4(packageDir, dest);
25983
+ rmSync4(dest, { recursive: true, force: true });
25984
+ renameSync5(packageDir, dest);
25738
25985
  } catch (err) {
25739
25986
  warn2(`[lsp] tofu_mismatch ${spec.npm}: failed to quarantine cache entry: ${err}`);
25740
25987
  }
@@ -25810,11 +26057,11 @@ function runAutoInstall(projectRoot, config2, fetchImpl = fetch) {
25810
26057
 
25811
26058
  // src/lsp-github-install.ts
25812
26059
  import { execFileSync as execFileSync2 } from "child_process";
25813
- import { createHash as createHash4, randomBytes } from "crypto";
26060
+ import { createHash as createHash5, randomBytes } from "crypto";
25814
26061
  import {
25815
26062
  copyFileSync as copyFileSync3,
25816
26063
  createReadStream as createReadStream2,
25817
- createWriteStream as createWriteStream2,
26064
+ createWriteStream as createWriteStream3,
25818
26065
  existsSync as existsSync10,
25819
26066
  lstatSync as lstatSync2,
25820
26067
  mkdirSync as mkdirSync8,
@@ -25822,14 +26069,15 @@ import {
25822
26069
  readFileSync as readFileSync9,
25823
26070
  readlinkSync as readlinkSync2,
25824
26071
  realpathSync as realpathSync3,
25825
- renameSync as renameSync5,
25826
- rmSync as rmSync4,
25827
- statSync as statSync5,
25828
- unlinkSync as unlinkSync6
26072
+ renameSync as renameSync6,
26073
+ rmSync as rmSync5,
26074
+ statSync as statSync6,
26075
+ unlinkSync as unlinkSync6,
26076
+ writeFileSync as writeFileSync8
25829
26077
  } from "fs";
25830
26078
  import { dirname as dirname5, join as join15, relative as relative2, resolve as resolve3 } from "path";
25831
- import { Readable as Readable2 } from "stream";
25832
- import { pipeline as pipeline2 } from "stream/promises";
26079
+ import { Readable as Readable3 } from "stream";
26080
+ import { pipeline as pipeline3 } from "stream/promises";
25833
26081
 
25834
26082
  // src/lsp-github-table.ts
25835
26083
  function exe(platform2, name) {
@@ -25928,6 +26176,7 @@ function ghBinDir(spec) {
25928
26176
  function ghExtractDir(spec) {
25929
26177
  return join15(ghPackageDir(spec), "extracted");
25930
26178
  }
26179
+ var INSTALLED_META_FILE2 = ".aft-installed";
25931
26180
  function ghBinaryPath(spec, platform2) {
25932
26181
  const ext = platform2 === "win32" ? ".exe" : "";
25933
26182
  return join15(ghBinDir(spec), `${spec.binary}${ext}`);
@@ -25935,7 +26184,7 @@ function ghBinaryPath(spec, platform2) {
25935
26184
  function isGithubInstalled(spec, platform2) {
25936
26185
  for (const candidate of ghBinaryCandidates(spec, platform2)) {
25937
26186
  try {
25938
- if (statSync5(join15(ghBinDir(spec), candidate)).isFile())
26187
+ if (statSync6(join15(ghBinDir(spec), candidate)).isFile())
25939
26188
  return true;
25940
26189
  } catch {}
25941
26190
  }
@@ -25946,11 +26195,45 @@ function ghBinaryCandidates(spec, platform2) {
25946
26195
  return [spec.binary];
25947
26196
  return [spec.binary, `${spec.binary}.cmd`, `${spec.binary}.exe`, `${spec.binary}.bat`];
25948
26197
  }
25949
- var MAX_DOWNLOAD_BYTES2 = 256 * 1024 * 1024;
26198
+ function readGithubInstalledMetaIn(installDir) {
26199
+ try {
26200
+ const path2 = join15(installDir, INSTALLED_META_FILE2);
26201
+ if (!statSync6(path2).isFile())
26202
+ return null;
26203
+ const parsed = JSON.parse(readFileSync9(path2, "utf8"));
26204
+ if (typeof parsed.version !== "string" || parsed.version.length === 0)
26205
+ return null;
26206
+ return {
26207
+ version: parsed.version,
26208
+ installedAt: typeof parsed.installedAt === "string" ? parsed.installedAt : "",
26209
+ ...typeof parsed.sha256 === "string" && parsed.sha256.length > 0 ? { sha256: parsed.sha256 } : {},
26210
+ ...typeof parsed.binarySha256 === "string" && parsed.binarySha256.length > 0 ? { binarySha256: parsed.binarySha256 } : {},
26211
+ ...typeof parsed.archiveSha256 === "string" && parsed.archiveSha256.length > 0 ? { archiveSha256: parsed.archiveSha256 } : {}
26212
+ };
26213
+ } catch {
26214
+ return null;
26215
+ }
26216
+ }
26217
+ function writeGithubInstalledMetaIn(installDir, version2, binarySha256, archiveSha256) {
26218
+ try {
26219
+ mkdirSync8(installDir, { recursive: true });
26220
+ const meta3 = {
26221
+ version: version2,
26222
+ installedAt: new Date().toISOString(),
26223
+ sha256: binarySha256,
26224
+ binarySha256,
26225
+ ...archiveSha256 ? { archiveSha256 } : {}
26226
+ };
26227
+ writeFileSync8(join15(installDir, INSTALLED_META_FILE2), JSON.stringify(meta3), "utf8");
26228
+ } catch (err) {
26229
+ warn2(`[lsp] failed to write github installed metadata in ${installDir}: ${err}`);
26230
+ }
26231
+ }
26232
+ var MAX_DOWNLOAD_BYTES3 = 256 * 1024 * 1024;
25950
26233
  var MAX_EXTRACT_BYTES2 = 1024 * 1024 * 1024;
25951
26234
  function sha256OfFile(path2) {
25952
26235
  return new Promise((resolve4, reject) => {
25953
- const hash2 = createHash4("sha256");
26236
+ const hash2 = createHash5("sha256");
25954
26237
  const stream = createReadStream2(path2);
25955
26238
  stream.on("error", reject);
25956
26239
  stream.on("data", (chunk) => hash2.update(chunk));
@@ -25958,7 +26241,7 @@ function sha256OfFile(path2) {
25958
26241
  });
25959
26242
  }
25960
26243
  function sha256OfFileSync2(path2) {
25961
- return createHash4("sha256").update(readFileSync9(path2)).digest("hex");
26244
+ return createHash5("sha256").update(readFileSync9(path2)).digest("hex");
25962
26245
  }
25963
26246
  async function fetchReleaseByTag(githubRepo, tag, fetchImpl, signal) {
25964
26247
  const candidates = [];
@@ -26121,8 +26404,8 @@ function assertAllowedDownloadUrl(rawUrl) {
26121
26404
  return parsed;
26122
26405
  }
26123
26406
  async function downloadFile(url2, destPath, fetchImpl, assetSize, signal) {
26124
- if (assetSize !== undefined && assetSize > MAX_DOWNLOAD_BYTES2) {
26125
- throw new Error(`asset size ${assetSize} exceeds max ${MAX_DOWNLOAD_BYTES2} (set lsp.versions to pin a smaller release if this is wrong)`);
26407
+ if (assetSize !== undefined && assetSize > MAX_DOWNLOAD_BYTES3) {
26408
+ throw new Error(`asset size ${assetSize} exceeds max ${MAX_DOWNLOAD_BYTES3} (set lsp.versions to pin a smaller release if this is wrong)`);
26126
26409
  }
26127
26410
  const timeout = controlledTimeoutSignal(120000, signal);
26128
26411
  try {
@@ -26131,24 +26414,24 @@ async function downloadFile(url2, destPath, fetchImpl, assetSize, signal) {
26131
26414
  throw new Error(`download failed (${res.status})`);
26132
26415
  }
26133
26416
  const advertised = Number.parseInt(res.headers.get("content-length") ?? "", 10);
26134
- if (Number.isFinite(advertised) && advertised > MAX_DOWNLOAD_BYTES2) {
26135
- throw new Error(`Content-Length ${advertised} exceeds max ${MAX_DOWNLOAD_BYTES2}`);
26417
+ if (Number.isFinite(advertised) && advertised > MAX_DOWNLOAD_BYTES3) {
26418
+ throw new Error(`Content-Length ${advertised} exceeds max ${MAX_DOWNLOAD_BYTES3}`);
26136
26419
  }
26137
26420
  mkdirSync8(dirname5(destPath), { recursive: true });
26138
26421
  let bytesWritten = 0;
26139
26422
  const guard = new TransformStream({
26140
26423
  transform(chunk, controller) {
26141
26424
  bytesWritten += chunk.byteLength;
26142
- if (bytesWritten > MAX_DOWNLOAD_BYTES2) {
26143
- controller.error(new Error(`download exceeded ${MAX_DOWNLOAD_BYTES2} bytes after streaming (server lied about size or sent unbounded body)`));
26425
+ if (bytesWritten > MAX_DOWNLOAD_BYTES3) {
26426
+ controller.error(new Error(`download exceeded ${MAX_DOWNLOAD_BYTES3} bytes after streaming (server lied about size or sent unbounded body)`));
26144
26427
  return;
26145
26428
  }
26146
26429
  controller.enqueue(chunk);
26147
26430
  }
26148
26431
  });
26149
26432
  const guarded = res.body.pipeThrough(guard);
26150
- const nodeStream = Readable2.fromWeb(guarded);
26151
- await pipeline2(nodeStream, createWriteStream2(destPath), { signal: timeout.signal });
26433
+ const nodeStream = Readable3.fromWeb(guarded);
26434
+ await pipeline3(nodeStream, createWriteStream3(destPath), { signal: timeout.signal });
26152
26435
  } catch (err) {
26153
26436
  try {
26154
26437
  unlinkSync6(destPath);
@@ -26228,7 +26511,7 @@ function validateExtraction(stagingRoot) {
26228
26511
  };
26229
26512
  walk(realStagingRoot);
26230
26513
  }
26231
- function precheckArchiveSize(archivePath, archiveType) {
26514
+ function precheckArchiveContents(archivePath, archiveType) {
26232
26515
  let totalBytes = 0;
26233
26516
  if (archiveType === "zip") {
26234
26517
  const out = execFileSync2("unzip", ["-l", archivePath], { encoding: "utf8" });
@@ -26239,6 +26522,9 @@ function precheckArchiveSize(archivePath, archiveType) {
26239
26522
  const out = execFileSync2("tar", ["-tvf", archivePath], { encoding: "utf8" });
26240
26523
  for (const line of out.split(`
26241
26524
  `)) {
26525
+ if (line.startsWith("h")) {
26526
+ throw new Error(`archive contains hardlink entry: ${line.trim()}`);
26527
+ }
26242
26528
  const parts = line.trim().split(/\s+/);
26243
26529
  if (parts.length >= 6) {
26244
26530
  const numeric = parts.map((part) => Number.parseInt(part, 10)).filter((value) => Number.isFinite(value) && value >= 0);
@@ -26255,20 +26541,20 @@ function extractArchiveSafely(archivePath, destDir, archiveType) {
26255
26541
  const suffix = randomBytes(8).toString("hex");
26256
26542
  const stagingDir = `${destDir}.staging-${suffix}`;
26257
26543
  try {
26258
- rmSync4(stagingDir, { recursive: true, force: true });
26544
+ rmSync5(stagingDir, { recursive: true, force: true });
26259
26545
  } catch {}
26260
26546
  mkdirSync8(stagingDir, { recursive: true });
26261
26547
  try {
26262
- precheckArchiveSize(archivePath, archiveType);
26548
+ precheckArchiveContents(archivePath, archiveType);
26263
26549
  runPlatformExtractor(archivePath, stagingDir, archiveType);
26264
26550
  validateExtraction(stagingDir);
26265
26551
  try {
26266
- rmSync4(destDir, { recursive: true, force: true });
26552
+ rmSync5(destDir, { recursive: true, force: true });
26267
26553
  } catch {}
26268
- renameSync5(stagingDir, destDir);
26554
+ renameSync6(stagingDir, destDir);
26269
26555
  } catch (err) {
26270
26556
  try {
26271
- rmSync4(stagingDir, { recursive: true, force: true });
26557
+ rmSync5(stagingDir, { recursive: true, force: true });
26272
26558
  } catch {}
26273
26559
  throw err;
26274
26560
  }
@@ -26279,28 +26565,44 @@ function quarantineCachedGithubInstall(spec, reason) {
26279
26565
  warn2(`[lsp] tofu_mismatch ${spec.id}: ${reason}; quarantining ${packageDir} -> ${dest}`);
26280
26566
  try {
26281
26567
  mkdirSync8(dirname5(dest), { recursive: true });
26282
- rmSync4(dest, { recursive: true, force: true });
26283
- renameSync5(packageDir, dest);
26568
+ rmSync5(dest, { recursive: true, force: true });
26569
+ renameSync6(packageDir, dest);
26284
26570
  } catch (err) {
26285
26571
  warn2(`[lsp] tofu_mismatch ${spec.id}: failed to quarantine cache entry: ${err}`);
26286
26572
  }
26287
26573
  }
26288
26574
  function validateCachedGithubInstall(spec, platform2) {
26289
- const meta3 = readInstalledMetaIn(ghPackageDir(spec));
26575
+ const packageDir = ghPackageDir(spec);
26576
+ const meta3 = readGithubInstalledMetaIn(packageDir);
26290
26577
  const binaryPath = ghBinaryCandidates(spec, platform2).map((candidate) => join15(ghBinDir(spec), candidate)).find((candidate) => {
26291
26578
  try {
26292
- return statSync5(candidate).isFile();
26579
+ return statSync6(candidate).isFile();
26293
26580
  } catch {
26294
26581
  return false;
26295
26582
  }
26296
26583
  });
26297
- if (!meta3?.sha256 || !meta3.version || !isSafeVersion(meta3.version) || !binaryPath) {
26584
+ if (!meta3?.version || !isSafeVersion(meta3.version) || !binaryPath) {
26298
26585
  quarantineCachedGithubInstall(spec, "missing/unsafe metadata or binary");
26299
26586
  return false;
26300
26587
  }
26301
26588
  const currentHash = sha256OfFileSync2(binaryPath);
26302
- if (currentHash !== meta3.sha256) {
26303
- quarantineCachedGithubInstall(spec, `recorded ${meta3.sha256}, current ${currentHash}`);
26589
+ const recordedBinaryHash = meta3.binarySha256 ?? meta3.sha256;
26590
+ if (recordedBinaryHash && currentHash === recordedBinaryHash) {
26591
+ if (meta3.sha256 !== currentHash || meta3.binarySha256 !== currentHash) {
26592
+ writeGithubInstalledMetaIn(packageDir, meta3.version, currentHash, meta3.archiveSha256);
26593
+ }
26594
+ return true;
26595
+ }
26596
+ if (meta3.sha256 && !meta3.binarySha256 && !meta3.archiveSha256) {
26597
+ writeGithubInstalledMetaIn(packageDir, meta3.version, currentHash, meta3.sha256);
26598
+ return true;
26599
+ }
26600
+ if (!recordedBinaryHash) {
26601
+ quarantineCachedGithubInstall(spec, "missing binary sha256 metadata");
26602
+ return false;
26603
+ }
26604
+ if (currentHash !== recordedBinaryHash) {
26605
+ quarantineCachedGithubInstall(spec, `recorded ${recordedBinaryHash}, current ${currentHash}`);
26304
26606
  return false;
26305
26607
  }
26306
26608
  return true;
@@ -26369,10 +26671,11 @@ async function downloadAndInstall(spec, tag, assets, platform2, arch, fetchImpl,
26369
26671
  return null;
26370
26672
  }
26371
26673
  log2(`[lsp] ${spec.id} ${tag} sha256=${archiveSha256}`);
26372
- const previousMeta = readInstalledMetaIn(ghPackageDir(spec));
26373
- if (previousMeta && previousMeta.version === tag && previousMeta.sha256) {
26374
- if (previousMeta.sha256 !== archiveSha256) {
26375
- error2(`[lsp] ${spec.id} ${tag}: TOFU sha256 mismatch \u2014 refusing install. ` + `Previously installed sha256=${previousMeta.sha256}, downloaded sha256=${archiveSha256}. ` + `This means the published release for tag ${tag} changed. Investigate before proceeding. ` + `Run \`aft doctor --clear\` to wipe the cache and force a fresh install if you've verified the change.`);
26674
+ const previousMeta = readGithubInstalledMetaIn(ghPackageDir(spec));
26675
+ const previousArchiveSha256 = previousMeta?.archiveSha256 ?? (previousMeta?.binarySha256 ? undefined : previousMeta?.sha256);
26676
+ if (previousMeta && previousMeta.version === tag && previousArchiveSha256) {
26677
+ if (previousArchiveSha256 !== archiveSha256) {
26678
+ error2(`[lsp] ${spec.id} ${tag}: TOFU sha256 mismatch \u2014 refusing install. ` + `Previously installed archive sha256=${previousArchiveSha256}, downloaded sha256=${archiveSha256}. ` + `This means the published release for tag ${tag} changed. Investigate before proceeding. ` + `Run \`aft doctor --clear\` to wipe the cache and force a fresh install if you've verified the change.`);
26376
26679
  try {
26377
26680
  unlinkSync6(archivePath);
26378
26681
  } catch {}
@@ -26407,7 +26710,9 @@ async function downloadAndInstall(spec, tag, assets, platform2, arch, fetchImpl,
26407
26710
  return null;
26408
26711
  }
26409
26712
  log2(`[lsp] installed ${spec.id} ${tag} at ${targetBinary}`);
26410
- return archiveSha256;
26713
+ const binarySha256 = await sha256OfFile(targetBinary);
26714
+ log2(`[lsp] ${spec.id} ${tag} binary_sha256=${binarySha256}`);
26715
+ return { archiveSha256, binarySha256 };
26411
26716
  }
26412
26717
  async function ensureGithubInstalled(spec, config2, fetchImpl, platform2, arch, signal) {
26413
26718
  const outcome = await withInstallLock(spec.githubRepo, async () => {
@@ -26433,14 +26738,14 @@ async function ensureGithubInstalled(spec, config2, fetchImpl, platform2, arch,
26433
26738
  log2(`[lsp] reinstalling ${spec.id}@${tag}: no installed-version metadata recorded`);
26434
26739
  }
26435
26740
  }
26436
- const archiveSha256 = await downloadAndInstall(spec, tag, assets, platform2, arch, fetchImpl, signal).catch((err) => {
26741
+ const hashes = await downloadAndInstall(spec, tag, assets, platform2, arch, fetchImpl, signal).catch((err) => {
26437
26742
  error2(`[lsp] github install ${spec.id} crashed: ${err}`);
26438
26743
  return null;
26439
26744
  });
26440
- if (!archiveSha256) {
26745
+ if (!hashes) {
26441
26746
  return { started: true, reason: "install failed (see plugin log)" };
26442
26747
  }
26443
- writeInstalledMetaIn(ghPackageDir(spec), tag, archiveSha256);
26748
+ writeGithubInstalledMetaIn(ghPackageDir(spec), tag, hashes.binarySha256, hashes.archiveSha256);
26444
26749
  return { started: true };
26445
26750
  });
26446
26751
  if (outcome === null) {
@@ -26635,7 +26940,7 @@ function normalizeToolMap(tools) {
26635
26940
  }
26636
26941
 
26637
26942
  // src/notifications.ts
26638
- import { existsSync as existsSync11, mkdirSync as mkdirSync9, readFileSync as readFileSync10, renameSync as renameSync6, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
26943
+ import { existsSync as existsSync11, mkdirSync as mkdirSync9, readFileSync as readFileSync10, renameSync as renameSync7, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "fs";
26639
26944
  import { homedir as homedir9, platform as platform2 } from "os";
26640
26945
  import { join as join16 } from "path";
26641
26946
  function isTuiMode() {
@@ -26742,7 +27047,7 @@ async function sendIgnoredMessage(client, sessionId, text) {
26742
27047
  return true;
26743
27048
  }
26744
27049
  } catch (err) {
26745
- sessionLog2(sessionId, `[aft-plugin] notification send failed: ${err instanceof Error ? err.message : String(err)}`);
27050
+ sessionLog(sessionId, `[aft-plugin] notification send failed: ${err instanceof Error ? err.message : String(err)}`);
26746
27051
  }
26747
27052
  return false;
26748
27053
  }
@@ -26768,7 +27073,7 @@ async function sendWarning(opts, message) {
26768
27073
  if (!sessionId)
26769
27074
  return;
26770
27075
  const text = `${WARNING_MARKER} ${message}`;
26771
- sessionLog2(sessionId, `[aft-plugin] sending warning to session ${sessionId}`);
27076
+ sessionLog(sessionId, `[aft-plugin] sending warning to session ${sessionId}`);
26772
27077
  await sendIgnoredMessage(opts.client, sessionId, text);
26773
27078
  }
26774
27079
  async function sendFeatureAnnouncement(opts, version2, features, storageDir) {
@@ -26791,13 +27096,13 @@ async function sendFeatureAnnouncement(opts, version2, features, storageDir) {
26791
27096
  return;
26792
27097
  const text = [`${FEATURE_MARKER} v${version2}:`, ...features.map((f) => ` \u2022 ${f}`)].join(`
26793
27098
  `);
26794
- sessionLog2(sessionId, `[aft-plugin] sending feature announcement for v${version2}`);
27099
+ sessionLog(sessionId, `[aft-plugin] sending feature announcement for v${version2}`);
26795
27100
  await sendIgnoredMessage(opts.client, sessionId, text);
26796
27101
  }
26797
27102
  if (storageDir) {
26798
27103
  try {
26799
27104
  mkdirSync9(storageDir, { recursive: true });
26800
- writeFileSync8(join16(storageDir, "last_announced_version"), version2);
27105
+ writeFileSync9(join16(storageDir, "last_announced_version"), version2);
26801
27106
  } catch {}
26802
27107
  }
26803
27108
  }
@@ -26825,9 +27130,9 @@ function writeWarnedTools(storageDir, warned) {
26825
27130
  mkdirSync9(storageDir, { recursive: true });
26826
27131
  const warnedToolsPath = join16(storageDir, WARNED_TOOLS_FILE);
26827
27132
  const tmpPath = join16(storageDir, `${WARNED_TOOLS_FILE}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
26828
- writeFileSync8(tmpPath, `${JSON.stringify(warned, null, 2)}
27133
+ writeFileSync9(tmpPath, `${JSON.stringify(warned, null, 2)}
26829
27134
  `);
26830
- renameSync6(tmpPath, warnedToolsPath);
27135
+ renameSync7(tmpPath, warnedToolsPath);
26831
27136
  } catch {}
26832
27137
  }
26833
27138
  async function withWarnedToolsLock(storageDir, fn) {
@@ -26839,7 +27144,7 @@ async function withWarnedToolsLock(storageDir, fn) {
26839
27144
  try {
26840
27145
  return await fn();
26841
27146
  } finally {
26842
- rmSync5(lockDir, { recursive: true, force: true });
27147
+ rmSync6(lockDir, { recursive: true, force: true });
26843
27148
  }
26844
27149
  } catch (err) {
26845
27150
  const code = err.code;
@@ -26940,7 +27245,7 @@ async function cleanupWarnings(opts) {
26940
27245
  }
26941
27246
  if (warningIds.length === 0)
26942
27247
  return;
26943
- sessionLog2(sessionId, `[aft-plugin] cleaning up ${warningIds.length} stale warning(s)`);
27248
+ sessionLog(sessionId, `[aft-plugin] cleaning up ${warningIds.length} stale warning(s)`);
26944
27249
  for (const id of warningIds) {
26945
27250
  await deleteMessage(effectiveServerUrl, sessionId, id);
26946
27251
  }
@@ -26948,16 +27253,16 @@ async function cleanupWarnings(opts) {
26948
27253
 
26949
27254
  // src/shared/rpc-server.ts
26950
27255
  import { randomBytes as randomBytes2 } from "crypto";
26951
- import { mkdirSync as mkdirSync10, renameSync as renameSync7, unlinkSync as unlinkSync7, writeFileSync as writeFileSync9 } from "fs";
27256
+ import { mkdirSync as mkdirSync10, renameSync as renameSync8, unlinkSync as unlinkSync7, writeFileSync as writeFileSync10 } from "fs";
26952
27257
  import { createServer } from "http";
26953
27258
  import { dirname as dirname6 } from "path";
26954
27259
 
26955
27260
  // src/shared/rpc-utils.ts
26956
- import { createHash as createHash5 } from "crypto";
27261
+ import { createHash as createHash6 } from "crypto";
26957
27262
  import { join as join17 } from "path";
26958
27263
  function projectHash(directory) {
26959
27264
  const normalized = directory.replace(/\/+$/, "");
26960
- return createHash5("sha256").update(normalized).digest("hex").slice(0, 16);
27265
+ return createHash6("sha256").update(normalized).digest("hex").slice(0, 16);
26961
27266
  }
26962
27267
  function rpcPortFilePath(storageDir, directory) {
26963
27268
  const hash2 = projectHash(directory);
@@ -26997,11 +27302,11 @@ class AftRpcServer {
26997
27302
  const dir = dirname6(this.portFilePath);
26998
27303
  mkdirSync10(dir, { recursive: true, mode: 448 });
26999
27304
  const tmpPath = `${this.portFilePath}.tmp`;
27000
- writeFileSync9(tmpPath, JSON.stringify({ port: this.port, token: this.token }), {
27305
+ writeFileSync10(tmpPath, JSON.stringify({ port: this.port, token: this.token }), {
27001
27306
  encoding: "utf-8",
27002
27307
  mode: 384
27003
27308
  });
27004
- renameSync7(tmpPath, this.portFilePath);
27309
+ renameSync8(tmpPath, this.portFilePath);
27005
27310
  log2(`RPC server listening on 127.0.0.1:${this.port}`);
27006
27311
  } catch (err) {
27007
27312
  warn2(`Failed to write RPC port file: ${err}`);
@@ -27104,7 +27409,7 @@ async function getSessionDirectory(client, sessionId, fallbackDirectory) {
27104
27409
  dir = session.directory;
27105
27410
  }
27106
27411
  } catch (err) {
27107
- sessionWarn2(sessionId, `[aft-plugin] session.get lookup failed: ${err instanceof Error ? err.message : String(err)}`);
27412
+ sessionWarn(sessionId, `[aft-plugin] session.get lookup failed: ${err instanceof Error ? err.message : String(err)}`);
27108
27413
  return null;
27109
27414
  }
27110
27415
  setCache(sessionId, dir);
@@ -27190,6 +27495,8 @@ function coerceAftStatus(response) {
27190
27495
  project_root: readNullableString(response.project_root),
27191
27496
  canonical_root: readNullableString(response.canonical_root),
27192
27497
  cache_role: readString(response.cache_role, "not_initialized"),
27498
+ degraded: readBoolean(response.degraded),
27499
+ degraded_reasons: Array.isArray(response.degraded_reasons) ? response.degraded_reasons.filter((r) => typeof r === "string") : [],
27193
27500
  features: {
27194
27501
  format_on_edit: readBoolean(features.format_on_edit),
27195
27502
  validate_on_edit: readString(features.validate_on_edit, "off"),
@@ -27288,7 +27595,7 @@ function formatStatusMarkdown(status) {
27288
27595
 
27289
27596
  // src/shared/tui-config.ts
27290
27597
  var import_comment_json4 = __toESM(require_src2(), 1);
27291
- import { existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync11, writeFileSync as writeFileSync10 } from "fs";
27598
+ import { existsSync as existsSync12, mkdirSync as mkdirSync11, readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "fs";
27292
27599
  import { dirname as dirname7, join as join19 } from "path";
27293
27600
 
27294
27601
  // src/shared/opencode-config-dir.ts
@@ -27345,7 +27652,7 @@ function ensureTuiPluginEntry() {
27345
27652
  plugins.push(PLUGIN_ENTRY);
27346
27653
  config2.plugin = plugins;
27347
27654
  mkdirSync11(dirname7(configPath), { recursive: true });
27348
- writeFileSync10(configPath, `${import_comment_json4.stringify(config2, null, 2)}
27655
+ writeFileSync11(configPath, `${import_comment_json4.stringify(config2, null, 2)}
27349
27656
  `);
27350
27657
  log2(`[aft-plugin] added TUI plugin entry to ${configPath}`);
27351
27658
  return true;
@@ -28224,7 +28531,7 @@ var CACHE_MAX_ENTRIES2 = 200;
28224
28531
  var cache2 = new Map;
28225
28532
  async function resolveIsSubagent(client, sessionId, _fallbackDirectory) {
28226
28533
  if (!sessionId) {
28227
- sessionLog2(undefined, "[subagent-detect] no sessionId provided \u2192 primary");
28534
+ sessionLog(undefined, "[subagent-detect] no sessionId provided \u2192 primary");
28228
28535
  return false;
28229
28536
  }
28230
28537
  const cached2 = cache2.get(sessionId);
@@ -28236,11 +28543,11 @@ async function resolveIsSubagent(client, sessionId, _fallbackDirectory) {
28236
28543
  const c = client;
28237
28544
  const sessionApi = c?.session;
28238
28545
  if (!sessionApi || typeof sessionApi.get !== "function") {
28239
- sessionLog2(sessionId, `[subagent-detect] client.session.get unavailable (client=${typeof client}, session=${typeof sessionApi}, get=${typeof sessionApi?.get}) \u2192 caching as primary`);
28546
+ sessionLog(sessionId, `[subagent-detect] client.session.get unavailable (client=${typeof client}, session=${typeof sessionApi}, get=${typeof sessionApi?.get}) \u2192 caching as primary`);
28240
28547
  setCache2(sessionId, false);
28241
28548
  return false;
28242
28549
  }
28243
- sessionLog2(sessionId, `[subagent-detect] cache miss, calling client.session.get(id=${sessionId})`);
28550
+ sessionLog(sessionId, `[subagent-detect] cache miss, calling client.session.get(id=${sessionId})`);
28244
28551
  let isSubagent = false;
28245
28552
  let parentIdRaw;
28246
28553
  try {
@@ -28250,9 +28557,9 @@ async function resolveIsSubagent(client, sessionId, _fallbackDirectory) {
28250
28557
  const session = result?.data ?? result;
28251
28558
  parentIdRaw = session?.parentID;
28252
28559
  isSubagent = session !== undefined && typeof session.parentID === "string" && session.parentID.length > 0;
28253
- sessionLog2(sessionId, `[subagent-detect] SDK returned session=${session !== undefined ? "present" : "undefined"}, parentID=${JSON.stringify(parentIdRaw)} \u2192 isSubagent=${isSubagent}`);
28560
+ sessionLog(sessionId, `[subagent-detect] SDK returned session=${session !== undefined ? "present" : "undefined"}, parentID=${JSON.stringify(parentIdRaw)} \u2192 isSubagent=${isSubagent}`);
28254
28561
  } catch (err) {
28255
- sessionWarn2(sessionId, `[subagent-detect] SDK lookup failed: ${err instanceof Error ? err.message : String(err)}`);
28562
+ sessionWarn(sessionId, `[subagent-detect] SDK lookup failed: ${err instanceof Error ? err.message : String(err)}`);
28256
28563
  return false;
28257
28564
  }
28258
28565
  setCache2(sessionId, isSubagent);
@@ -28320,7 +28627,7 @@ function createBashTool(ctx) {
28320
28627
  const requestedBackground = args.background === true;
28321
28628
  const effectiveBackground = isSubagent ? false : requestedBackground;
28322
28629
  if (isSubagent && requestedBackground) {
28323
- sessionLog2(context.sessionID, "[bash] subagent + background:true \u2192 converting to foreground (subagent would lose task_id)");
28630
+ sessionLog(context.sessionID, "[bash] subagent + background:true \u2192 converting to foreground (subagent would lose task_id)");
28324
28631
  }
28325
28632
  const shellEnv = await ctx.plugin?.trigger?.("shell.env", { cwd, sessionID: context.sessionID, callID: getCallID(context) }, { env: {} });
28326
28633
  const data = await withPermissionLoop(ctx, context, {
@@ -30098,7 +30405,7 @@ async function queryLspHints(client, symbolName, directory, sessionId) {
30098
30405
  }
30099
30406
  return { symbols: hints };
30100
30407
  } catch (err) {
30101
- sessionWarn2(sessionId, `LSP query failed for "${symbolName}": ${err.message}`);
30408
+ sessionWarn(sessionId, `LSP query failed for "${symbolName}": ${err.message}`);
30102
30409
  return;
30103
30410
  }
30104
30411
  }
@@ -30887,31 +31194,37 @@ ${lines}
30887
31194
  } catch (err) {
30888
31195
  warn2(`[lsp] auto-install setup failed: ${err instanceof Error ? err.message : String(err)}`);
30889
31196
  }
30890
- let versionUpgradeAttempted = null;
31197
+ const versionUpgradePromises = new Map;
30891
31198
  const poolOptions = {
30892
31199
  errorPrefix: "[aft-plugin]",
30893
31200
  minVersion: PLUGIN_VERSION,
30894
31201
  onVersionMismatch: async (binaryVersion, minVersion) => {
30895
- if (versionUpgradeAttempted === binaryVersion) {
30896
- log2(`Version ${binaryVersion} < ${minVersion} but upgrade already attempted \u2014 continuing`);
30897
- return null;
31202
+ const existing = versionUpgradePromises.get(minVersion);
31203
+ if (existing) {
31204
+ log2(`Version ${binaryVersion} < ${minVersion}; awaiting in-flight compatible binary upgrade`);
31205
+ return existing;
30898
31206
  }
30899
- versionUpgradeAttempted = binaryVersion;
30900
- warn2(`WARNING: aft binary v${binaryVersion} is older than plugin v${minVersion}. ` + "Some features may not work. Attempting to download a compatible binary...");
30901
- try {
30902
- const path7 = await ensureBinary(`v${minVersion}`);
30903
- if (!path7) {
30904
- warn2(`Could not find or download v${minVersion}. Continuing with v${binaryVersion}.`);
31207
+ const upgradePromise = (async () => {
31208
+ warn2(`WARNING: aft binary v${binaryVersion} is older than plugin v${minVersion}. ` + "Some features may not work. Attempting to download a compatible binary...");
31209
+ try {
31210
+ const path7 = await ensureBinary(`v${minVersion}`);
31211
+ if (!path7) {
31212
+ warn2(`Could not find or download v${minVersion}. Continuing with v${binaryVersion}.`);
31213
+ return null;
31214
+ }
31215
+ log2(`Found/downloaded compatible binary at ${path7}. Replacing running bridges...`);
31216
+ const replaced = await pool.replaceBinary(path7);
31217
+ log2("Binary replaced successfully. New bridges will use the updated binary.");
31218
+ return replaced;
31219
+ } catch (err) {
31220
+ error2(`Auto-download failed: ${err.message}. Install manually: cargo install agent-file-tools@${minVersion}`);
30905
31221
  return null;
31222
+ } finally {
31223
+ versionUpgradePromises.delete(minVersion);
30906
31224
  }
30907
- log2(`Found/downloaded compatible binary at ${path7}. Replacing running bridges...`);
30908
- const replaced = await pool.replaceBinary(path7);
30909
- log2("Binary replaced successfully. New bridges will use the updated binary.");
30910
- return replaced;
30911
- } catch (err) {
30912
- error2(`Auto-download failed: ${err.message}. Install manually: cargo install agent-file-tools@${minVersion}`);
30913
- return null;
30914
- }
31225
+ })();
31226
+ versionUpgradePromises.set(minVersion, upgradePromise);
31227
+ return upgradePromise;
30915
31228
  },
30916
31229
  onConfigureWarnings: async ({ projectRoot, sessionId, client, warnings }) => {
30917
31230
  const pendingWarnings = sessionId ? drainPendingEagerWarnings(projectRoot) : [];
@@ -31045,7 +31358,7 @@ ${lines}
31045
31358
  if (storageDir && ANNOUNCEMENT_VERSION) {
31046
31359
  try {
31047
31360
  mkdirSync12(storageDir, { recursive: true });
31048
- writeFileSync11(join21(storageDir, "last_announced_version"), ANNOUNCEMENT_VERSION);
31361
+ writeFileSync12(join21(storageDir, "last_announced_version"), ANNOUNCEMENT_VERSION);
31049
31362
  } catch {}
31050
31363
  }
31051
31364
  return { success: true };