@cortexkit/aft-pi 0.45.1 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -9697,6 +9697,16 @@ function isEmptyParam(value) {
9697
9697
  return Object.keys(value).length === 0;
9698
9698
  return false;
9699
9699
  }
9700
+ // ../aft-bridge/dist/config-keys.js
9701
+ var OPENCODE_ONLY_KEYS = ["hoist_builtin_tools", "auto_update"];
9702
+ function stripHarnessSpecificConfigKeys(value, keys) {
9703
+ if (!value || typeof value !== "object" || Array.isArray(value))
9704
+ return value;
9705
+ const stripped = { ...value };
9706
+ for (const key of keys)
9707
+ delete stripped[key];
9708
+ return stripped;
9709
+ }
9700
9710
  // ../aft-bridge/dist/config-tiers.js
9701
9711
  import { existsSync, readFileSync as readFileSync2 } from "node:fs";
9702
9712
  import { resolve as resolve2 } from "node:path";
@@ -10017,6 +10027,117 @@ async function fetchLatestTag() {
10017
10027
  clearTimeout(timeout);
10018
10028
  }
10019
10029
  }
10030
+ // ../aft-bridge/dist/durable-log.js
10031
+ import { appendFile, mkdir, rename, rm, stat } from "node:fs/promises";
10032
+ import { homedir as homedir5 } from "node:os";
10033
+ import { dirname, join as join4 } from "node:path";
10034
+ var DEFAULT_LOG_BYTES = 20 * 1024 * 1024;
10035
+ var DEFAULT_LOG_GENERATIONS = 5;
10036
+ function homeDir() {
10037
+ if (process.platform === "win32")
10038
+ return process.env.USERPROFILE || process.env.HOME || homedir5();
10039
+ return process.env.HOME || homedir5();
10040
+ }
10041
+ function dataHome() {
10042
+ if (process.env.XDG_DATA_HOME)
10043
+ return process.env.XDG_DATA_HOME;
10044
+ if (process.platform === "win32") {
10045
+ return process.env.LOCALAPPDATA || process.env.APPDATA || join4(homeDir(), "AppData", "Local");
10046
+ }
10047
+ return join4(homeDir(), ".local", "share");
10048
+ }
10049
+ function resolveAftStorageRoot(configuredRoot) {
10050
+ if (configuredRoot)
10051
+ return configuredRoot;
10052
+ if (process.env.AFT_CACHE_DIR)
10053
+ return join4(process.env.AFT_CACHE_DIR, "aft");
10054
+ return join4(dataHome(), "cortexkit", "aft");
10055
+ }
10056
+ function resolveAftLogPath(filename, configuredRoot) {
10057
+ return join4(resolveAftStorageRoot(configuredRoot), "logs", filename);
10058
+ }
10059
+
10060
+ class RotatingLogSink {
10061
+ path;
10062
+ maxBytes;
10063
+ generations;
10064
+ estimatedBytes = null;
10065
+ queue = Promise.resolve();
10066
+ disabled = false;
10067
+ failureReported = false;
10068
+ constructor(path2, options = {}) {
10069
+ this.path = path2;
10070
+ this.maxBytes = options.maxBytes ?? DEFAULT_LOG_BYTES;
10071
+ this.generations = options.generations ?? DEFAULT_LOG_GENERATIONS;
10072
+ }
10073
+ append(data) {
10074
+ if (this.disabled || data.length === 0)
10075
+ return;
10076
+ this.queue = this.queue.then(() => this.write(data)).catch((error2) => {
10077
+ this.disabled = true;
10078
+ if (!this.failureReported) {
10079
+ this.failureReported = true;
10080
+ try {
10081
+ process.stderr.write(`[aft-plugin] durable log disabled for ${this.path}: ${error2 instanceof Error ? error2.message : String(error2)}
10082
+ `);
10083
+ } catch {}
10084
+ }
10085
+ });
10086
+ }
10087
+ async drain() {
10088
+ await this.queue;
10089
+ }
10090
+ async write(data) {
10091
+ await mkdir(dirname(this.path), { recursive: true });
10092
+ if (this.estimatedBytes === null) {
10093
+ try {
10094
+ this.estimatedBytes = (await stat(this.path)).size;
10095
+ } catch (error2) {
10096
+ if (!isMissing(error2))
10097
+ throw error2;
10098
+ this.estimatedBytes = 0;
10099
+ }
10100
+ }
10101
+ const bytes = Buffer.byteLength(data);
10102
+ if (this.estimatedBytes > 0 && this.estimatedBytes + bytes > this.maxBytes) {
10103
+ await this.rotate();
10104
+ }
10105
+ await appendFile(this.path, data, "utf8");
10106
+ this.estimatedBytes = (this.estimatedBytes ?? 0) + bytes;
10107
+ }
10108
+ async rotate() {
10109
+ if (this.generations <= 0) {
10110
+ await removeIfPresent(this.path);
10111
+ this.estimatedBytes = 0;
10112
+ return;
10113
+ }
10114
+ await removeIfPresent(`${this.path}.${this.generations}`);
10115
+ for (let generation = this.generations - 1;generation >= 1; generation -= 1) {
10116
+ await renameIfPresent(`${this.path}.${generation}`, `${this.path}.${generation + 1}`);
10117
+ }
10118
+ await renameIfPresent(this.path, `${this.path}.1`);
10119
+ this.estimatedBytes = 0;
10120
+ }
10121
+ }
10122
+ function isMissing(error2) {
10123
+ return typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "ENOENT";
10124
+ }
10125
+ async function removeIfPresent(path2) {
10126
+ try {
10127
+ await rm(path2, { force: true });
10128
+ } catch (error2) {
10129
+ if (!isMissing(error2))
10130
+ throw error2;
10131
+ }
10132
+ }
10133
+ async function renameIfPresent(from, to) {
10134
+ try {
10135
+ await rename(from, to);
10136
+ } catch (error2) {
10137
+ if (!isMissing(error2))
10138
+ throw error2;
10139
+ }
10140
+ }
10020
10141
  // ../aft-bridge/dist/edit-summary.js
10021
10142
  function formatEditSummary(data) {
10022
10143
  if (data.rolled_back === true) {
@@ -10087,32 +10208,32 @@ function stripJsoncSymbols(value) {
10087
10208
  // ../aft-bridge/dist/migration.js
10088
10209
  import { spawnSync as spawnSync2 } from "node:child_process";
10089
10210
  import { closeSync as closeSync3, existsSync as existsSync5, mkdirSync as mkdirSync4, openSync as openSync3, readFileSync as readFileSync5, renameSync as renameSync4, rmSync as rmSync2, statSync as statSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync2 } from "node:fs";
10090
- import { homedir as homedir7, tmpdir } from "node:os";
10091
- import { basename, dirname as dirname2, join as join6, resolve as resolve4 } from "node:path";
10211
+ import { homedir as homedir8, tmpdir } from "node:os";
10212
+ import { basename, dirname as dirname3, join as join7, resolve as resolve4 } from "node:path";
10092
10213
 
10093
10214
  // ../aft-bridge/dist/paths.js
10094
10215
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, renameSync as renameSync2, writeFileSync } from "node:fs";
10095
- import { homedir as homedir5 } from "node:os";
10096
- import { dirname, isAbsolute as isAbsolute2, join as join4, resolve as resolve3 } from "node:path";
10097
- function homeDir() {
10216
+ import { homedir as homedir6 } from "node:os";
10217
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join5, resolve as resolve3 } from "node:path";
10218
+ function homeDir2() {
10098
10219
  if (process.platform === "win32")
10099
- return process.env.USERPROFILE || process.env.HOME || homedir5();
10100
- return process.env.HOME || homedir5();
10220
+ return process.env.USERPROFILE || process.env.HOME || homedir6();
10221
+ return process.env.HOME || homedir6();
10101
10222
  }
10102
10223
  function configHome() {
10103
10224
  const xdg = process.env.XDG_CONFIG_HOME;
10104
10225
  if (xdg && isAbsolute2(xdg))
10105
10226
  return xdg;
10106
- return join4(homeDir(), ".config");
10227
+ return join5(homeDir2(), ".config");
10107
10228
  }
10108
10229
  function legacyOpenCodeConfigDir() {
10109
10230
  const envDir = process.env.OPENCODE_CONFIG_DIR?.trim();
10110
10231
  if (envDir)
10111
10232
  return resolve3(envDir);
10112
- return join4(configHome(), "opencode");
10233
+ return join5(configHome(), "opencode");
10113
10234
  }
10114
10235
  function legacyPiAgentDir() {
10115
- return join4(homeDir(), ".pi", "agent");
10236
+ return join5(homeDir2(), ".pi", "agent");
10116
10237
  }
10117
10238
  function legacySources(basePath, label, harness) {
10118
10239
  return [
@@ -10121,10 +10242,10 @@ function legacySources(basePath, label, harness) {
10121
10242
  ];
10122
10243
  }
10123
10244
  function resolveCortexKitUserConfigPath() {
10124
- return join4(configHome(), "cortexkit", "aft.jsonc");
10245
+ return join5(configHome(), "cortexkit", "aft.jsonc");
10125
10246
  }
10126
10247
  function resolveCortexKitProjectConfigPath(projectDirectory) {
10127
- return join4(projectDirectory, ".cortexkit", "aft.jsonc");
10248
+ return join5(projectDirectory, ".cortexkit", "aft.jsonc");
10128
10249
  }
10129
10250
  function resolveCortexKitConfigPaths(projectDirectory) {
10130
10251
  return {
@@ -10135,25 +10256,25 @@ function resolveCortexKitConfigPaths(projectDirectory) {
10135
10256
  function resolveLegacyAftConfigSources(projectDirectory) {
10136
10257
  return {
10137
10258
  user: [
10138
- ...legacySources(join4(legacyOpenCodeConfigDir(), "aft"), "OpenCode user", "opencode"),
10139
- ...legacySources(join4(legacyPiAgentDir(), "aft"), "Pi user", "pi")
10259
+ ...legacySources(join5(legacyOpenCodeConfigDir(), "aft"), "OpenCode user", "opencode"),
10260
+ ...legacySources(join5(legacyPiAgentDir(), "aft"), "Pi user", "pi")
10140
10261
  ],
10141
10262
  project: [
10142
- ...legacySources(join4(projectDirectory, ".opencode", "aft"), "OpenCode project", "opencode"),
10143
- ...legacySources(join4(projectDirectory, ".pi", "aft"), "Pi project", "pi")
10263
+ ...legacySources(join5(projectDirectory, ".opencode", "aft"), "OpenCode project", "opencode"),
10264
+ ...legacySources(join5(projectDirectory, ".pi", "aft"), "Pi project", "pi")
10144
10265
  ]
10145
10266
  };
10146
10267
  }
10147
10268
  function resolveHarnessStoragePath(storageRoot, harness, ...segments) {
10148
- return join4(storageRoot, harness, ...segments);
10269
+ return join5(storageRoot, harness, ...segments);
10149
10270
  }
10150
10271
  function repairRootScopedStorageFile(storageRoot, harness, fileName) {
10151
10272
  const harnessPath = resolveHarnessStoragePath(storageRoot, harness, fileName);
10152
- const rootPath = join4(storageRoot, fileName);
10273
+ const rootPath = join5(storageRoot, fileName);
10153
10274
  if (existsSync3(harnessPath) || !existsSync3(rootPath))
10154
10275
  return harnessPath;
10155
10276
  try {
10156
- mkdirSync2(dirname(harnessPath), { recursive: true });
10277
+ mkdirSync2(dirname2(harnessPath), { recursive: true });
10157
10278
  renameSync2(rootPath, harnessPath);
10158
10279
  } catch {}
10159
10280
  return harnessPath;
@@ -10174,7 +10295,7 @@ function shouldShowAnnouncement(storageRoot, harness, currentVersion) {
10174
10295
  return false;
10175
10296
  if (!lastVersion) {
10176
10297
  try {
10177
- mkdirSync2(dirname(versionFile), { recursive: true });
10298
+ mkdirSync2(dirname2(versionFile), { recursive: true });
10178
10299
  writeFileSync(versionFile, currentVersion);
10179
10300
  } catch {}
10180
10301
  return false;
@@ -10186,7 +10307,7 @@ function markAnnouncementSeen(storageRoot, harness, currentVersion) {
10186
10307
  return;
10187
10308
  const versionFile = repairRootScopedStorageFile(storageRoot, harness, "last_announced_version");
10188
10309
  try {
10189
- mkdirSync2(dirname(versionFile), { recursive: true });
10310
+ mkdirSync2(dirname2(versionFile), { recursive: true });
10190
10311
  writeFileSync(versionFile, currentVersion);
10191
10312
  } catch {}
10192
10313
  }
@@ -10195,8 +10316,8 @@ function markAnnouncementSeen(storageRoot, harness, currentVersion) {
10195
10316
  import { execSync } from "node:child_process";
10196
10317
  import { chmodSync as chmodSync2, closeSync as closeSync2, copyFileSync as copyFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, openSync as openSync2, readSync, renameSync as renameSync3, unlinkSync as unlinkSync2 } from "node:fs";
10197
10318
  import { createRequire as createRequire2 } from "node:module";
10198
- import { homedir as homedir6 } from "node:os";
10199
- import { join as join5 } from "node:path";
10319
+ import { homedir as homedir7 } from "node:os";
10320
+ import { join as join6 } from "node:path";
10200
10321
  var ensureBinaryForResolver = ensureBinary;
10201
10322
  function copyToVersionedCache(npmBinaryPath, knownVersion) {
10202
10323
  try {
@@ -10205,9 +10326,9 @@ function copyToVersionedCache(npmBinaryPath, knownVersion) {
10205
10326
  return null;
10206
10327
  const tag = version.startsWith("v") ? version : `v${version}`;
10207
10328
  const cacheDir = getCacheDir();
10208
- const versionedDir = join5(cacheDir, tag);
10329
+ const versionedDir = join6(cacheDir, tag);
10209
10330
  const ext = process.platform === "win32" ? ".exe" : "";
10210
- const cachedPath = join5(versionedDir, `aft${ext}`);
10331
+ const cachedPath = join6(versionedDir, `aft${ext}`);
10211
10332
  if (existsSync4(cachedPath)) {
10212
10333
  const cachedVersion = readBinaryVersion(cachedPath);
10213
10334
  if (cachedVersion === version)
@@ -10237,18 +10358,18 @@ function normalizeBareVersion(version) {
10237
10358
  return version.startsWith("v") ? version.slice(1) : version;
10238
10359
  }
10239
10360
  function homeDirFromEnv(env) {
10240
- return (process.platform === "win32" ? env.USERPROFILE || env.HOME : env.HOME) || homedir6();
10361
+ return (process.platform === "win32" ? env.USERPROFILE || env.HOME : env.HOME) || homedir7();
10241
10362
  }
10242
10363
  function cacheDirFromEnv(env) {
10243
10364
  if (process.platform === "win32") {
10244
- const base2 = env.LOCALAPPDATA || env.APPDATA || join5(homeDirFromEnv(env), "AppData", "Local");
10245
- return join5(base2, "aft", "bin");
10365
+ const base2 = env.LOCALAPPDATA || env.APPDATA || join6(homeDirFromEnv(env), "AppData", "Local");
10366
+ return join6(base2, "aft", "bin");
10246
10367
  }
10247
- const base = env.XDG_CACHE_HOME || join5(homeDirFromEnv(env), ".cache");
10248
- return join5(base, "aft", "bin");
10368
+ const base = env.XDG_CACHE_HOME || join6(homeDirFromEnv(env), ".cache");
10369
+ return join6(base, "aft", "bin");
10249
10370
  }
10250
10371
  function cachedBinaryPathFromEnv(version, env, ext) {
10251
- const binaryPath = join5(cacheDirFromEnv(env), version, `aft${ext}`);
10372
+ const binaryPath = join6(cacheDirFromEnv(env), version, `aft${ext}`);
10252
10373
  return existsSync4(binaryPath) ? binaryPath : null;
10253
10374
  }
10254
10375
  function isExpectedCachedBinary2(binaryPath, expectedVersion) {
@@ -10367,7 +10488,7 @@ function findBinarySync(expectedVersion) {
10367
10488
  return usable;
10368
10489
  }
10369
10490
  } catch {}
10370
- const cargoPath = join5(homeDirFromEnv(env), ".cargo", "bin", `aft${ext}`);
10491
+ const cargoPath = join6(homeDirFromEnv(env), ".cargo", "bin", `aft${ext}`);
10371
10492
  if (existsSync4(cargoPath)) {
10372
10493
  const usable = probeBinaryCandidate(cargoPath, "cargo", expectedVersion);
10373
10494
  if (usable)
@@ -10409,26 +10530,26 @@ async function findBinary(expectedVersion) {
10409
10530
  var spawnSyncForMigration = spawnSync2;
10410
10531
  var TARGET_MARKER = ".migrated_from_legacy";
10411
10532
  var DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;
10412
- function dataHome() {
10533
+ function dataHome2() {
10413
10534
  if (process.env.XDG_DATA_HOME)
10414
10535
  return process.env.XDG_DATA_HOME;
10415
10536
  if (process.platform === "win32") {
10416
- return process.env.LOCALAPPDATA || process.env.APPDATA || join6(homeDir2(), "AppData", "Local");
10537
+ return process.env.LOCALAPPDATA || process.env.APPDATA || join7(homeDir3(), "AppData", "Local");
10417
10538
  }
10418
- return join6(homeDir2(), ".local", "share");
10539
+ return join7(homeDir3(), ".local", "share");
10419
10540
  }
10420
- function homeDir2() {
10541
+ function homeDir3() {
10421
10542
  if (process.platform === "win32")
10422
- return process.env.USERPROFILE || process.env.HOME || homedir7();
10423
- return process.env.HOME || homedir7();
10543
+ return process.env.USERPROFILE || process.env.HOME || homedir8();
10544
+ return process.env.HOME || homedir8();
10424
10545
  }
10425
10546
  function resolveLegacyStorageRoot(harness) {
10426
10547
  if (harness === "pi")
10427
- return join6(homeDir2(), ".pi", "agent", "aft");
10428
- return join6(dataHome(), "opencode", "storage", "plugin", "aft");
10548
+ return join7(homeDir3(), ".pi", "agent", "aft");
10549
+ return join7(dataHome2(), "opencode", "storage", "plugin", "aft");
10429
10550
  }
10430
10551
  function resolveCortexKitStorageRoot() {
10431
- return join6(dataHome(), "cortexkit", "aft");
10552
+ return join7(dataHome2(), "cortexkit", "aft");
10432
10553
  }
10433
10554
  function stripJsoncForParse(input) {
10434
10555
  let out = "";
@@ -10557,8 +10678,8 @@ function acquireConfigMigrationLock(lockDir) {
10557
10678
  }
10558
10679
  }
10559
10680
  function atomicCopyConfigFile(sourcePath, targetPath) {
10560
- mkdirSync4(dirname2(targetPath), { recursive: true });
10561
- const tmpPath = join6(dirname2(targetPath), `.${basename(targetPath)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
10681
+ mkdirSync4(dirname3(targetPath), { recursive: true });
10682
+ const tmpPath = join7(dirname3(targetPath), `.${basename(targetPath)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
10562
10683
  let fd = null;
10563
10684
  try {
10564
10685
  fd = openSync3(tmpPath, "wx", 384);
@@ -10579,8 +10700,8 @@ function atomicCopyConfigFile(sourcePath, targetPath) {
10579
10700
  }
10580
10701
  }
10581
10702
  function atomicWriteConfigFile(targetPath, content) {
10582
- mkdirSync4(dirname2(targetPath), { recursive: true });
10583
- const tmpPath = join6(dirname2(targetPath), `.${basename(targetPath)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
10703
+ mkdirSync4(dirname3(targetPath), { recursive: true });
10704
+ const tmpPath = join7(dirname3(targetPath), `.${basename(targetPath)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
10584
10705
  let fd = null;
10585
10706
  try {
10586
10707
  fd = openSync3(tmpPath, "wx", 384);
@@ -10705,7 +10826,7 @@ function migrateAftConfigFile(opts) {
10705
10826
  if (existingSources.length === 0) {
10706
10827
  return { migrated: false, conflict: false, targetPath: opts.targetPath, warnings };
10707
10828
  }
10708
- mkdirSync4(dirname2(opts.targetPath), { recursive: true });
10829
+ mkdirSync4(dirname3(opts.targetPath), { recursive: true });
10709
10830
  const release = acquireConfigMigrationLock(`${opts.targetPath}.lock`);
10710
10831
  try {
10711
10832
  const sources = existingSources.map((source) => ({
@@ -10762,13 +10883,13 @@ function spawnErrorLabel(error2) {
10762
10883
  return [code, error2.message].filter(Boolean).join(": ");
10763
10884
  }
10764
10885
  function migrationLogPath(newRoot, harness, logger) {
10765
- const desired = join6(newRoot, "logs", "migration", `${harness}-${Date.now()}.jsonl`);
10886
+ const desired = join7(newRoot, "logs", "migration", `${harness}-${Date.now()}.jsonl`);
10766
10887
  try {
10767
- mkdirSync4(dirname2(desired), { recursive: true });
10888
+ mkdirSync4(dirname3(desired), { recursive: true });
10768
10889
  return desired;
10769
10890
  } catch (err) {
10770
- const fallback = join6(tmpdir(), `aft-migration-${harness}-${Date.now()}.jsonl`);
10771
- logger?.warn?.(`Failed to create AFT migration log directory ${dirname2(desired)}: ${err instanceof Error ? err.message : String(err)}. ` + `Using fallback log path ${fallback}.`);
10891
+ const fallback = join7(tmpdir(), `aft-migration-${harness}-${Date.now()}.jsonl`);
10892
+ logger?.warn?.(`Failed to create AFT migration log directory ${dirname3(desired)}: ${err instanceof Error ? err.message : String(err)}. ` + `Using fallback log path ${fallback}.`);
10772
10893
  return fallback;
10773
10894
  }
10774
10895
  }
@@ -10830,13 +10951,13 @@ async function ensureStorageMigrated(opts) {
10830
10951
  }
10831
10952
  // ../aft-bridge/dist/npm-resolver.js
10832
10953
  import { readdirSync, statSync as statSync3 } from "node:fs";
10833
- import { homedir as homedir8 } from "node:os";
10834
- import { delimiter, dirname as dirname3, isAbsolute as isAbsolute3, join as join7 } from "node:path";
10954
+ import { homedir as homedir9 } from "node:os";
10955
+ import { delimiter, dirname as dirname4, isAbsolute as isAbsolute3, join as join8 } from "node:path";
10835
10956
  function defaultDeps() {
10836
10957
  return {
10837
10958
  platform: process.platform,
10838
10959
  env: process.env,
10839
- home: homedir8(),
10960
+ home: homedir9(),
10840
10961
  execPath: process.execPath
10841
10962
  };
10842
10963
  }
@@ -10857,14 +10978,14 @@ function npmFromPath(deps) {
10857
10978
  const dir = entry.trim().replace(/^"|"$/g, "");
10858
10979
  if (!dir || !isAbsolute3(dir))
10859
10980
  continue;
10860
- if (isFile(join7(dir, name)))
10981
+ if (isFile(join8(dir, name)))
10861
10982
  return dir;
10862
10983
  }
10863
10984
  return null;
10864
10985
  }
10865
10986
  function npmAdjacentToNode(deps) {
10866
- const dir = dirname3(deps.execPath);
10867
- return isFile(join7(dir, npmBinaryName(deps.platform))) ? dir : null;
10987
+ const dir = dirname4(deps.execPath);
10988
+ return isFile(join8(dir, npmBinaryName(deps.platform))) ? dir : null;
10868
10989
  }
10869
10990
  function highestVersionedNodeBin(installsDir, name) {
10870
10991
  let entries;
@@ -10873,8 +10994,8 @@ function highestVersionedNodeBin(installsDir, name) {
10873
10994
  } catch {
10874
10995
  return null;
10875
10996
  }
10876
- const candidates = entries.filter((v) => isFile(join7(installsDir, v, "bin", name))).sort((a, b) => compareVersionsDesc(a, b));
10877
- return candidates.length > 0 ? join7(installsDir, candidates[0], "bin") : null;
10997
+ const candidates = entries.filter((v) => isFile(join8(installsDir, v, "bin", name))).sort((a, b) => compareVersionsDesc(a, b));
10998
+ return candidates.length > 0 ? join8(installsDir, candidates[0], "bin") : null;
10878
10999
  }
10879
11000
  function compareVersionsDesc(a, b) {
10880
11001
  const pa = a.replace(/^v/, "").split(".").map((n) => Number.parseInt(n, 10));
@@ -10899,22 +11020,22 @@ function wellKnownNpmDirs(deps) {
10899
11020
  const programFiles = env.ProgramFiles || "C:\\Program Files";
10900
11021
  const appData = env.APPDATA;
10901
11022
  const localAppData = env.LOCALAPPDATA;
10902
- push(join7(programFiles, "nodejs"));
11023
+ push(join8(programFiles, "nodejs"));
10903
11024
  if (appData)
10904
- push(join7(appData, "npm"));
11025
+ push(join8(appData, "npm"));
10905
11026
  if (localAppData)
10906
- push(join7(localAppData, "Volta", "bin"));
11027
+ push(join8(localAppData, "Volta", "bin"));
10907
11028
  if (env.NVM_SYMLINK)
10908
11029
  push(env.NVM_SYMLINK);
10909
11030
  } else {
10910
11031
  if (env.NVM_BIN)
10911
11032
  push(env.NVM_BIN);
10912
- push(highestVersionedNodeBin(join7(home, ".nvm", "versions", "node"), name));
10913
- push(highestVersionedNodeBin(join7(home, ".local", "share", "mise", "installs", "node"), name));
10914
- push(highestVersionedNodeBin(join7(home, ".asdf", "installs", "nodejs"), name));
10915
- push(join7(home, ".volta", "bin"));
10916
- push(join7(home, ".asdf", "shims"));
10917
- const systemDirs = deps.systemNpmDirs ?? (platform === "darwin" ? ["/opt/homebrew/bin", "/usr/local/bin"] : ["/usr/local/bin", "/usr/bin", join7(home, ".local", "bin")]);
11033
+ push(highestVersionedNodeBin(join8(home, ".nvm", "versions", "node"), name));
11034
+ push(highestVersionedNodeBin(join8(home, ".local", "share", "mise", "installs", "node"), name));
11035
+ push(highestVersionedNodeBin(join8(home, ".asdf", "installs", "nodejs"), name));
11036
+ push(join8(home, ".volta", "bin"));
11037
+ push(join8(home, ".asdf", "shims"));
11038
+ const systemDirs = deps.systemNpmDirs ?? (platform === "darwin" ? ["/opt/homebrew/bin", "/usr/local/bin"] : ["/usr/local/bin", "/usr/bin", join8(home, ".local", "bin")]);
10918
11039
  for (const dir of systemDirs)
10919
11040
  push(dir);
10920
11041
  }
@@ -10924,12 +11045,12 @@ function resolveNpm(deps = defaultDeps()) {
10924
11045
  const name = npmBinaryName(deps.platform);
10925
11046
  const onPath = npmFromPath(deps);
10926
11047
  if (onPath)
10927
- return { command: join7(onPath, name), binDir: onPath };
11048
+ return { command: join8(onPath, name), binDir: onPath };
10928
11049
  const adjacent = npmAdjacentToNode(deps);
10929
11050
  if (adjacent)
10930
- return { command: join7(adjacent, name), binDir: adjacent };
11051
+ return { command: join8(adjacent, name), binDir: adjacent };
10931
11052
  for (const dir of wellKnownNpmDirs(deps)) {
10932
- const candidate = join7(dir, name);
11053
+ const candidate = join8(dir, name);
10933
11054
  if (isFile(candidate))
10934
11055
  return { command: candidate, binDir: dir };
10935
11056
  }
@@ -10946,7 +11067,7 @@ function npmSpawnEnv(resolved, baseEnv = process.env) {
10946
11067
  import { execFileSync } from "node:child_process";
10947
11068
  import { createHash as createHash3 } from "node:crypto";
10948
11069
  import { chmodSync as chmodSync3, closeSync as closeSync4, copyFileSync as copyFileSync3, createWriteStream as createWriteStream2, existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync5, openSync as openSync4, readdirSync as readdirSync2, readFileSync as readFileSync6, readlinkSync, realpathSync, rmSync as rmSync3, statSync as statSync4, symlinkSync, unlinkSync as unlinkSync4, writeFileSync as writeFileSync3 } from "node:fs";
10949
- import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute4, join as join8, relative as relative2, resolve as resolve5, win32 } from "node:path";
11070
+ import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute4, join as join9, relative as relative2, resolve as resolve5, win32 } from "node:path";
10950
11071
  import { Readable as Readable2 } from "node:stream";
10951
11072
  import { pipeline as pipeline2 } from "node:stream/promises";
10952
11073
  var ORT_VERSION = "1.24.4";
@@ -11009,10 +11130,10 @@ function getManualInstallHint() {
11009
11130
  }
11010
11131
  async function ensureOnnxRuntime(storageDir) {
11011
11132
  const info = getPlatformInfo();
11012
- const ortVersionDir = join8(storageDir, "onnxruntime", ORT_VERSION);
11133
+ const ortVersionDir = join9(storageDir, "onnxruntime", ORT_VERSION);
11013
11134
  const libName = info?.libName ?? "libonnxruntime.dylib";
11014
11135
  const resolvedOrtDir = resolveCachedOnnxRuntimeDir(ortVersionDir, libName);
11015
- const libPath = join8(resolvedOrtDir, libName);
11136
+ const libPath = join9(resolvedOrtDir, libName);
11016
11137
  if (existsSync6(libPath)) {
11017
11138
  const meta = readOnnxInstalledMeta(ortVersionDir);
11018
11139
  if (meta?.sha256) {
@@ -11042,9 +11163,9 @@ async function ensureOnnxRuntime(storageDir) {
11042
11163
  warn(`ONNX Runtime auto-download not available for ${process.platform}/${process.arch}. Install manually: ${getManualInstallHint()}`);
11043
11164
  return null;
11044
11165
  }
11045
- const onnxBaseDir = join8(storageDir, "onnxruntime");
11166
+ const onnxBaseDir = join9(storageDir, "onnxruntime");
11046
11167
  mkdirSync5(onnxBaseDir, { recursive: true });
11047
- const lockPath = join8(onnxBaseDir, ONNX_LOCK_FILE);
11168
+ const lockPath = join9(onnxBaseDir, ONNX_LOCK_FILE);
11048
11169
  cleanupAbandonedStagingDirs(onnxBaseDir);
11049
11170
  if (!acquireLock(lockPath)) {
11050
11171
  warn(`ONNX Runtime install already in progress in another process (lock: ${lockPath}). Skipping.`);
@@ -11063,7 +11184,7 @@ function cleanupAbandonedStagingDirs(onnxBaseDir) {
11063
11184
  for (const entry of entries) {
11064
11185
  if (!entry.startsWith(`${ORT_VERSION}.tmp.`))
11065
11186
  continue;
11066
- const stagingDir = join8(onnxBaseDir, entry);
11187
+ const stagingDir = join9(onnxBaseDir, entry);
11067
11188
  const parts = entry.split(".");
11068
11189
  const pidStr = parts[parts.length - 2];
11069
11190
  const pid = pidStr ? Number.parseInt(pidStr, 10) : NaN;
@@ -11100,7 +11221,7 @@ function cleanupAbandonedStagingDirs(onnxBaseDir) {
11100
11221
  }
11101
11222
  function cleanupIncompleteTargetIfUnowned(ortDir) {
11102
11223
  try {
11103
- if (existsSync6(ortDir) && !existsSync6(join8(ortDir, ONNX_INSTALLED_META_FILE))) {
11224
+ if (existsSync6(ortDir) && !existsSync6(join9(ortDir, ONNX_INSTALLED_META_FILE))) {
11104
11225
  log(`[onnx] removing half-populated install dir ${ortDir} (no meta file)`);
11105
11226
  rmSync3(ortDir, { recursive: true, force: true });
11106
11227
  }
@@ -11144,7 +11265,7 @@ function detectOnnxVersion(libDir, libName) {
11144
11265
  if (version)
11145
11266
  return version;
11146
11267
  }
11147
- const base = join8(libDir, libName);
11268
+ const base = join9(libDir, libName);
11148
11269
  if (existsSync6(base)) {
11149
11270
  try {
11150
11271
  const real = realpathSync(base);
@@ -11183,6 +11304,17 @@ function pathEntriesForPlatform() {
11183
11304
  return isAbsolute4(entry) || win32.isAbsolute(entry);
11184
11305
  });
11185
11306
  }
11307
+ function isWindowsSystem32Directory(dir) {
11308
+ if (process.platform !== "win32")
11309
+ return false;
11310
+ const normalizedDir = win32.resolve(dir).replace(/[\\/]+$/, "").toLowerCase();
11311
+ const windowsRoots = [process.env.SystemRoot, process.env.windir, "C:\\Windows"];
11312
+ return windowsRoots.some((root) => {
11313
+ if (!root)
11314
+ return false;
11315
+ return normalizedDir === win32.resolve(root, "System32").replace(/[\\/]+$/, "").toLowerCase();
11316
+ });
11317
+ }
11186
11318
  function directoryContainsLibrary(dir, libName) {
11187
11319
  try {
11188
11320
  const entries = readdirSync2(dir);
@@ -11196,10 +11328,10 @@ function directoryContainsLibrary(dir, libName) {
11196
11328
  }
11197
11329
  }
11198
11330
  function resolveCachedOnnxRuntimeDir(ortVersionDir, libName) {
11199
- if (existsSync6(join8(ortVersionDir, libName)))
11331
+ if (existsSync6(join9(ortVersionDir, libName)))
11200
11332
  return ortVersionDir;
11201
- const libSubdir = join8(ortVersionDir, "lib");
11202
- if (existsSync6(join8(libSubdir, libName)))
11333
+ const libSubdir = join9(ortVersionDir, "lib");
11334
+ if (existsSync6(join9(libSubdir, libName)))
11203
11335
  return libSubdir;
11204
11336
  return ortVersionDir;
11205
11337
  }
@@ -11214,12 +11346,12 @@ function findSystemOnnxRuntime(libName) {
11214
11346
  } else if (process.platform === "win32") {
11215
11347
  const programFiles = process.env.ProgramFiles ?? "C:\\Program Files";
11216
11348
  const programFilesX86 = process.env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
11217
- searchPaths.push(join8(programFiles, "onnxruntime", "lib"), join8(programFiles, "Microsoft ONNX Runtime", "lib"), join8(programFiles, "Microsoft Machine Learning", "lib"), join8(programFilesX86, "onnxruntime", "lib"), ...(() => {
11349
+ searchPaths.push(join9(programFiles, "onnxruntime", "lib"), join9(programFiles, "Microsoft ONNX Runtime", "lib"), join9(programFiles, "Microsoft Machine Learning", "lib"), join9(programFilesX86, "onnxruntime", "lib"), ...(() => {
11218
11350
  const nugetPaths = [];
11219
11351
  const userProfile = process.env.USERPROFILE ?? "";
11220
11352
  if (!userProfile)
11221
11353
  return nugetPaths;
11222
- const nugetPackageDir = join8(userProfile, ".nuget", "packages", "microsoft.ml.onnxruntime");
11354
+ const nugetPackageDir = join9(userProfile, ".nuget", "packages", "microsoft.ml.onnxruntime");
11223
11355
  if (!existsSync6(nugetPackageDir))
11224
11356
  return nugetPaths;
11225
11357
  try {
@@ -11228,7 +11360,7 @@ function findSystemOnnxRuntime(libName) {
11228
11360
  continue;
11229
11361
  if (entry.name === "__globalPackagesFolder" || entry.name.startsWith("."))
11230
11362
  continue;
11231
- nugetPaths.push(join8(nugetPackageDir, entry.name, "runtimes", "win-x64", "native"), join8(nugetPackageDir, entry.name, "runtimes", "win-arm64", "native"));
11363
+ nugetPaths.push(join9(nugetPackageDir, entry.name, "runtimes", "win-x64", "native"), join9(nugetPackageDir, entry.name, "runtimes", "win-arm64", "native"));
11232
11364
  }
11233
11365
  } catch (err) {
11234
11366
  warn(`Failed to scan NuGet ONNX Runtime cache ${nugetPackageDir}: ${err instanceof Error ? err.message : String(err)}`);
@@ -11250,7 +11382,7 @@ function findSystemOnnxRuntime(libName) {
11250
11382
  });
11251
11383
  const unknownVersionPaths = [];
11252
11384
  for (const dir of uniquePaths) {
11253
- const libPath = join8(dir, libName);
11385
+ const libPath = join9(dir, libName);
11254
11386
  if (process.platform === "win32") {
11255
11387
  if (!directoryContainsLibrary(dir, libName))
11256
11388
  continue;
@@ -11259,6 +11391,10 @@ function findSystemOnnxRuntime(libName) {
11259
11391
  }
11260
11392
  const version = detectOnnxVersion(dir, libName);
11261
11393
  if (!version) {
11394
+ if (isWindowsSystem32Directory(dir)) {
11395
+ warn(`Skipping unversioned Windows system ONNX Runtime at ${dir}; falling through to AFT-managed download.`);
11396
+ continue;
11397
+ }
11262
11398
  unknownVersionPaths.push(dir);
11263
11399
  continue;
11264
11400
  }
@@ -11286,7 +11422,7 @@ async function downloadFileWithCap(url, destPath) {
11286
11422
  if (Number.isFinite(advertised) && advertised > MAX_DOWNLOAD_BYTES2) {
11287
11423
  throw new Error(`Content-Length ${advertised} exceeds max ${MAX_DOWNLOAD_BYTES2}`);
11288
11424
  }
11289
- mkdirSync5(dirname4(destPath), { recursive: true });
11425
+ mkdirSync5(dirname5(destPath), { recursive: true });
11290
11426
  let bytesWritten = 0;
11291
11427
  const guard = new TransformStream({
11292
11428
  transform(chunk, transformController) {
@@ -11316,11 +11452,11 @@ function validateExtractedTree(stagingRoot) {
11316
11452
  const walk = (dir) => {
11317
11453
  const entries = readdirSync2(dir);
11318
11454
  for (const entry of entries) {
11319
- const fullPath = join8(dir, entry);
11455
+ const fullPath = join9(dir, entry);
11320
11456
  const lst = lstatSync(fullPath);
11321
11457
  if (lst.isSymbolicLink()) {
11322
11458
  const linkTarget = readlinkSync(fullPath);
11323
- const resolvedTarget = resolve5(dirname4(fullPath), linkTarget);
11459
+ const resolvedTarget = resolve5(dirname5(fullPath), linkTarget);
11324
11460
  const rel2 = relative2(realRoot, resolvedTarget);
11325
11461
  if (rel2.startsWith("..") || isAbsolute4(rel2) || win32.isAbsolute(rel2)) {
11326
11462
  throw new Error(`extracted symlink ${fullPath} points outside staging root: ${linkTarget}`);
@@ -11351,7 +11487,7 @@ async function downloadOnnxRuntime(info, targetDir) {
11351
11487
  const tmpDir = `${targetDir}.tmp.${process.pid}.${Date.now().toString(36)}`;
11352
11488
  try {
11353
11489
  mkdirSync5(tmpDir, { recursive: true });
11354
- const archivePath = join8(tmpDir, `onnxruntime.${info.archiveType}`);
11490
+ const archivePath = join9(tmpDir, `onnxruntime.${info.archiveType}`);
11355
11491
  await downloadFileWithCap(url, archivePath);
11356
11492
  const archiveSha256 = sha256File(archivePath);
11357
11493
  log(`ONNX Runtime archive sha256=${archiveSha256}`);
@@ -11367,7 +11503,7 @@ async function downloadOnnxRuntime(info, targetDir) {
11367
11503
  unlinkSync4(archivePath);
11368
11504
  } catch {}
11369
11505
  validateExtractedTree(tmpDir);
11370
- const extractedDir = join8(tmpDir, info.assetName, "lib");
11506
+ const extractedDir = join9(tmpDir, info.assetName, "lib");
11371
11507
  if (!existsSync6(extractedDir)) {
11372
11508
  throw new Error(`Expected directory not found: ${extractedDir}`);
11373
11509
  }
@@ -11376,11 +11512,11 @@ async function downloadOnnxRuntime(info, targetDir) {
11376
11512
  const realFiles = [];
11377
11513
  const symlinks = [];
11378
11514
  for (const libFile of libFiles) {
11379
- const src = join8(extractedDir, libFile);
11515
+ const src = join9(extractedDir, libFile);
11380
11516
  try {
11381
- const stat = lstatSync(src);
11382
- log(`ORT extract: ${libFile} — isSymlink=${stat.isSymbolicLink()}, isFile=${stat.isFile()}, size=${stat.size}`);
11383
- if (stat.isSymbolicLink()) {
11517
+ const stat2 = lstatSync(src);
11518
+ log(`ORT extract: ${libFile} — isSymlink=${stat2.isSymbolicLink()}, isFile=${stat2.isFile()}, size=${stat2.size}`);
11519
+ if (stat2.isSymbolicLink()) {
11384
11520
  symlinks.push({ name: libFile, target: readlinkSync(src) });
11385
11521
  } else {
11386
11522
  realFiles.push(libFile);
@@ -11391,7 +11527,7 @@ async function downloadOnnxRuntime(info, targetDir) {
11391
11527
  }
11392
11528
  }
11393
11529
  copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks);
11394
- const libPath = join8(targetDir, info.libName);
11530
+ const libPath = join9(targetDir, info.libName);
11395
11531
  let libHash = null;
11396
11532
  try {
11397
11533
  libHash = sha256File(libPath);
@@ -11416,8 +11552,8 @@ async function downloadOnnxRuntime(info, targetDir) {
11416
11552
  function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, copyFile = copyFileSync3) {
11417
11553
  const requiredLibs = new Set([info.libName]);
11418
11554
  for (const libFile of realFiles) {
11419
- const src = join8(extractedDir, libFile);
11420
- const dst = join8(targetDir, libFile);
11555
+ const src = join9(extractedDir, libFile);
11556
+ const dst = join9(targetDir, libFile);
11421
11557
  try {
11422
11558
  copyFile(src, dst);
11423
11559
  if (process.platform !== "win32") {
@@ -11433,12 +11569,12 @@ function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, c
11433
11569
  }
11434
11570
  const targetRoot = realpathSync(targetDir);
11435
11571
  for (const link of symlinks) {
11436
- const dst = join8(targetDir, link.name);
11572
+ const dst = join9(targetDir, link.name);
11437
11573
  try {
11438
11574
  unlinkSync4(dst);
11439
11575
  } catch {}
11440
- const dstForContainment = join8(targetRoot, link.name);
11441
- const resolvedTarget = resolve5(dirname4(dstForContainment), link.target);
11576
+ const dstForContainment = join9(targetRoot, link.name);
11577
+ const resolvedTarget = resolve5(dirname5(dstForContainment), link.target);
11442
11578
  if (!isPathInsideRoot(targetRoot, resolvedTarget)) {
11443
11579
  const message = `ONNX Runtime symlink ${link.name} points outside install dir: ${link.target}`;
11444
11580
  if (requiredLibs.has(link.name)) {
@@ -11458,7 +11594,7 @@ function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, c
11458
11594
  log(`ORT extract: failed to symlink optional ${link.name}: ${symlinkErr}`);
11459
11595
  }
11460
11596
  }
11461
- const requiredPath = join8(targetDir, info.libName);
11597
+ const requiredPath = join9(targetDir, info.libName);
11462
11598
  if (!existsSync6(requiredPath)) {
11463
11599
  rmSync3(targetDir, { recursive: true, force: true });
11464
11600
  throw new Error(`Required ONNX Runtime library missing after install: ${requiredPath}`);
@@ -11485,13 +11621,13 @@ function writeOnnxInstalledMeta(installDir, version, sha256, archiveSha256) {
11485
11621
  ...sha256 ? { sha256 } : {},
11486
11622
  archiveSha256
11487
11623
  };
11488
- writeFileSync3(join8(installDir, ONNX_INSTALLED_META_FILE), JSON.stringify(meta), "utf8");
11624
+ writeFileSync3(join9(installDir, ONNX_INSTALLED_META_FILE), JSON.stringify(meta), "utf8");
11489
11625
  } catch (err) {
11490
11626
  log(`[onnx] failed to write installed-meta in ${installDir}: ${err}`);
11491
11627
  }
11492
11628
  }
11493
11629
  function readOnnxInstalledMeta(installDir) {
11494
- const path2 = join8(installDir, ONNX_INSTALLED_META_FILE);
11630
+ const path2 = join9(installDir, ONNX_INSTALLED_META_FILE);
11495
11631
  try {
11496
11632
  if (!statSync4(path2).isFile())
11497
11633
  return null;
@@ -11628,7 +11764,7 @@ function isProcessAlive(pid) {
11628
11764
  }
11629
11765
  }
11630
11766
  // ../aft-bridge/dist/pool.js
11631
- import { homedir as homedir9 } from "node:os";
11767
+ import { homedir as homedir10 } from "node:os";
11632
11768
 
11633
11769
  // ../aft-bridge/dist/project-identity.js
11634
11770
  import { realpathSync as realpathSync2 } from "node:fs";
@@ -11667,7 +11803,7 @@ var DEFAULT_MAX_POOL_SIZE = 8;
11667
11803
  var CLEANUP_INTERVAL_MS = 60 * 1000;
11668
11804
  function canonicalHomeDir() {
11669
11805
  try {
11670
- const home = homedir9();
11806
+ const home = homedir10();
11671
11807
  if (!home)
11672
11808
  return null;
11673
11809
  return canonicalizeProjectRoot(home);
@@ -11693,6 +11829,7 @@ class BridgePool {
11693
11829
  projectConfigLoader;
11694
11830
  logger;
11695
11831
  cleanupTimer = null;
11832
+ shutdownCalled = false;
11696
11833
  constructor(binaryPath, options = {}, configOverrides = {}) {
11697
11834
  this.binaryPath = binaryPath;
11698
11835
  this.maxPoolSize = options.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE;
@@ -11714,12 +11851,11 @@ class BridgePool {
11714
11851
  childEnv: options.childEnv
11715
11852
  };
11716
11853
  this.configOverrides = configOverrides;
11717
- if (Number.isFinite(this.idleTimeoutMs)) {
11718
- this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL_MS);
11719
- this.cleanupTimer.unref();
11720
- }
11854
+ this.startCleanupTimer();
11721
11855
  }
11722
11856
  getActiveBridgeForRoot(projectRoot) {
11857
+ if (this.shutdownCalled)
11858
+ return null;
11723
11859
  const key = normalizeKey(projectRoot);
11724
11860
  const entry = this.bridges.get(key);
11725
11861
  if (!entry?.bridge.isAlive())
@@ -11727,7 +11863,21 @@ class BridgePool {
11727
11863
  entry.lastUsed = Date.now();
11728
11864
  return entry.bridge;
11729
11865
  }
11866
+ activeBridges() {
11867
+ if (this.shutdownCalled)
11868
+ return [];
11869
+ const alive = [];
11870
+ for (const entry of this.bridges.values()) {
11871
+ if (entry.bridge.isAlive())
11872
+ alive.push(entry.bridge);
11873
+ }
11874
+ return alive;
11875
+ }
11730
11876
  getBridge(projectRoot) {
11877
+ if (this.shutdownCalled) {
11878
+ this.shutdownCalled = false;
11879
+ this.startCleanupTimer();
11880
+ }
11731
11881
  const key = normalizeKey(projectRoot);
11732
11882
  const existing = this.bridges.get(key);
11733
11883
  if (existing) {
@@ -11737,15 +11887,7 @@ class BridgePool {
11737
11887
  if (this.bridges.size >= this.maxPoolSize) {
11738
11888
  this.evictLRU();
11739
11889
  }
11740
- let projectOverrides = {};
11741
- if (this.projectConfigLoader) {
11742
- try {
11743
- projectOverrides = this.projectConfigLoader(key) ?? {};
11744
- } catch (err) {
11745
- const message = err instanceof Error ? err.message : String(err);
11746
- this.error(`projectConfigLoader failed; using global overrides only: ${message}`);
11747
- }
11748
- }
11890
+ const projectOverrides = this.loadProjectOverrides(key);
11749
11891
  const mergedOverrides = { ...this.configOverrides, ...projectOverrides };
11750
11892
  const bridge = new BinaryBridge(this.binaryPath, key, this.bridgeOptions, mergedOverrides);
11751
11893
  this.bridges.set(key, { bridge, lastUsed: Date.now() });
@@ -11795,6 +11937,7 @@ class BridgePool {
11795
11937
  }
11796
11938
  async closeSession(_projectRoot, _session) {}
11797
11939
  async shutdown() {
11940
+ this.shutdownCalled = true;
11798
11941
  if (this.cleanupTimer) {
11799
11942
  clearInterval(this.cleanupTimer);
11800
11943
  this.cleanupTimer = null;
@@ -11807,6 +11950,9 @@ class BridgePool {
11807
11950
  this.staleBridges.clear();
11808
11951
  await Promise.allSettled(shutdowns);
11809
11952
  }
11953
+ isShutdown() {
11954
+ return this.shutdownCalled;
11955
+ }
11810
11956
  async replaceBinary(newPath) {
11811
11957
  this.binaryPath = newPath;
11812
11958
  for (const entry of this.bridges.values()) {
@@ -11816,6 +11962,12 @@ class BridgePool {
11816
11962
  this.log(`Binary path updated to ${newPath}. Active bridges marked stale — next calls will use the new binary.`);
11817
11963
  return newPath;
11818
11964
  }
11965
+ startCleanupTimer() {
11966
+ if (!Number.isFinite(this.idleTimeoutMs) || this.cleanupTimer)
11967
+ return;
11968
+ this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL_MS);
11969
+ this.cleanupTimer.unref();
11970
+ }
11819
11971
  log(message, meta) {
11820
11972
  const logger = this.logger ?? getActiveLogger();
11821
11973
  if (logger) {
@@ -11847,6 +11999,36 @@ class BridgePool {
11847
11999
  this.configOverrides[key] = value;
11848
12000
  }
11849
12001
  }
12002
+ async reconfigure(projectRoot, overrides) {
12003
+ const key = normalizeKey(projectRoot);
12004
+ for (const [name, value] of Object.entries(overrides)) {
12005
+ this.setConfigureOverride(name, value);
12006
+ }
12007
+ const bridge = this.getActiveBridgeForRoot(key);
12008
+ if (!bridge)
12009
+ return;
12010
+ const projectOverrides = this.loadProjectOverrides(key);
12011
+ const response = await bridge.send("configure", {
12012
+ project_root: key,
12013
+ ...this.configOverrides,
12014
+ ...projectOverrides,
12015
+ ...overrides
12016
+ });
12017
+ if (response.success === false) {
12018
+ throw new Error(`configure refresh failed for ${key}: ${String(response.message ?? "unknown error")}`);
12019
+ }
12020
+ }
12021
+ loadProjectOverrides(key) {
12022
+ if (!this.projectConfigLoader)
12023
+ return {};
12024
+ try {
12025
+ return this.projectConfigLoader(key) ?? {};
12026
+ } catch (err) {
12027
+ const message = err instanceof Error ? err.message : String(err);
12028
+ this.error(`projectConfigLoader failed; using global overrides only: ${message}`);
12029
+ return {};
12030
+ }
12031
+ }
11850
12032
  get size() {
11851
12033
  return this.bridges.size;
11852
12034
  }
@@ -11860,11 +12042,185 @@ class BridgePool {
11860
12042
  function normalizeKey(projectRoot) {
11861
12043
  return canonicalizeProjectRoot(projectRoot);
11862
12044
  }
11863
- // ../../node_modules/.bun/@cortexkit+subc-client@0.3.0/node_modules/@cortexkit/subc-client/src/client.ts
12045
+ // ../aft-bridge/dist/revivable-transport.js
12046
+ class RevivableTransportPool {
12047
+ createPool;
12048
+ onBinaryReplaced;
12049
+ activePool;
12050
+ revival = null;
12051
+ transports = new Map;
12052
+ configureOverrides = new Map;
12053
+ constructor(initialPool, createPool, onBinaryReplaced) {
12054
+ this.createPool = createPool;
12055
+ this.onBinaryReplaced = onBinaryReplaced;
12056
+ this.activePool = initialPool;
12057
+ }
12058
+ getBridge(projectRoot) {
12059
+ const key = canonicalizeProjectRoot(projectRoot);
12060
+ let transport = this.transports.get(key);
12061
+ if (!transport) {
12062
+ transport = new RevivableProjectTransport(this, key);
12063
+ this.transports.set(key, transport);
12064
+ }
12065
+ return transport;
12066
+ }
12067
+ activeBridges() {
12068
+ return this.activePool.activeBridges();
12069
+ }
12070
+ getActiveBridgeForRoot(projectRoot) {
12071
+ const key = canonicalizeProjectRoot(projectRoot);
12072
+ const bridge = this.currentBridge(key);
12073
+ if (!bridge)
12074
+ return null;
12075
+ const transport = this.getBridge(key);
12076
+ transport.refreshStatusSubscription(bridge);
12077
+ return transport;
12078
+ }
12079
+ async toolCall(projectRoot, runtime, name, rawArgs = {}, options) {
12080
+ return this.getBridge(projectRoot).toolCall(runtime.sessionID, name, rawArgs, options);
12081
+ }
12082
+ setConfigureOverride(key, value) {
12083
+ if (value === undefined)
12084
+ this.configureOverrides.delete(key);
12085
+ else
12086
+ this.configureOverrides.set(key, value);
12087
+ this.activePool.setConfigureOverride(key, value);
12088
+ }
12089
+ async reconfigure(projectRoot, overrides) {
12090
+ for (const [key, value] of Object.entries(overrides)) {
12091
+ if (value === undefined)
12092
+ this.configureOverrides.delete(key);
12093
+ else
12094
+ this.configureOverrides.set(key, value);
12095
+ }
12096
+ const pool = await this.ensureActivePool();
12097
+ await pool.reconfigure(projectRoot, overrides);
12098
+ }
12099
+ async replaceBinary(path2) {
12100
+ const replaced = await this.activePool.replaceBinary(path2);
12101
+ this.onBinaryReplaced?.(replaced);
12102
+ return replaced;
12103
+ }
12104
+ closeSession(projectRoot, session) {
12105
+ return this.activePool.closeSession(projectRoot, session);
12106
+ }
12107
+ async shutdown() {
12108
+ const revival = this.revival;
12109
+ if (revival) {
12110
+ await Promise.allSettled([revival]);
12111
+ }
12112
+ await this.activePool.shutdown();
12113
+ for (const transport of this.transports.values()) {
12114
+ transport.refreshStatusSubscription(null);
12115
+ }
12116
+ }
12117
+ isShutdown() {
12118
+ return this.activePool.isShutdown();
12119
+ }
12120
+ async ensureActivePool() {
12121
+ if (!this.activePool.isShutdown())
12122
+ return this.activePool;
12123
+ if (this.revival)
12124
+ return this.revival;
12125
+ warn("transport was shut down but new demand arrived — reviving (host quit hook fired without process exit?)");
12126
+ const revival = this.createPool().then((pool) => {
12127
+ for (const [key, value] of this.configureOverrides) {
12128
+ pool.setConfigureOverride(key, value);
12129
+ }
12130
+ this.activePool = pool;
12131
+ for (const [root, transport] of this.transports) {
12132
+ transport.refreshStatusSubscription(pool.getActiveBridgeForRoot(root));
12133
+ }
12134
+ return pool;
12135
+ });
12136
+ this.revival = revival;
12137
+ revival.then(() => {
12138
+ if (this.revival === revival)
12139
+ this.revival = null;
12140
+ }, () => {
12141
+ if (this.revival === revival)
12142
+ this.revival = null;
12143
+ });
12144
+ return revival;
12145
+ }
12146
+ currentBridge(projectRoot) {
12147
+ return this.activePool.getActiveBridgeForRoot(projectRoot);
12148
+ }
12149
+ async send(projectRoot, command, params, options) {
12150
+ const pool = await this.ensureActivePool();
12151
+ const bridge = pool.getBridge(projectRoot);
12152
+ this.getBridge(projectRoot).refreshStatusSubscription(bridge);
12153
+ return bridge.send(command, params, options);
12154
+ }
12155
+ async toolCallOnProject(projectRoot, sessionId, name, rawArgs, options) {
12156
+ const pool = await this.ensureActivePool();
12157
+ const bridge = pool.getBridge(projectRoot);
12158
+ this.getBridge(projectRoot).refreshStatusSubscription(bridge);
12159
+ return bridge.toolCall(sessionId, name, rawArgs, options);
12160
+ }
12161
+ }
12162
+
12163
+ class RevivableProjectTransport {
12164
+ owner;
12165
+ projectRoot;
12166
+ statusListeners = new Map;
12167
+ statusBridges = new Map;
12168
+ constructor(owner, projectRoot) {
12169
+ this.owner = owner;
12170
+ this.projectRoot = projectRoot;
12171
+ }
12172
+ getCwd() {
12173
+ return this.projectRoot;
12174
+ }
12175
+ getStatusBar() {
12176
+ return this.owner.currentBridge(this.projectRoot)?.getStatusBar();
12177
+ }
12178
+ getCachedStatus() {
12179
+ return this.owner.currentBridge(this.projectRoot)?.getCachedStatus() ?? null;
12180
+ }
12181
+ cacheStatusSnapshot(snapshot) {
12182
+ this.owner.currentBridge(this.projectRoot)?.cacheStatusSnapshot(snapshot);
12183
+ }
12184
+ send(command, params, options) {
12185
+ return this.owner.send(this.projectRoot, command, params, options);
12186
+ }
12187
+ toolCall(sessionId, name, rawArgs, options) {
12188
+ return this.owner.toolCallOnProject(this.projectRoot, sessionId, name, rawArgs, options);
12189
+ }
12190
+ subscribeStatus(listener) {
12191
+ if (this.statusListeners.has(listener))
12192
+ return () => this.removeStatusListener(listener);
12193
+ this.statusListeners.set(listener, () => {});
12194
+ this.bindStatusListener(listener, this.owner.currentBridge(this.projectRoot));
12195
+ return () => this.removeStatusListener(listener);
12196
+ }
12197
+ refreshStatusSubscription(bridge) {
12198
+ for (const listener of this.statusListeners.keys()) {
12199
+ this.bindStatusListener(listener, bridge);
12200
+ }
12201
+ }
12202
+ bindStatusListener(listener, bridge) {
12203
+ if (this.statusBridges.get(listener) === bridge)
12204
+ return;
12205
+ const previousUnsubscribe = this.statusListeners.get(listener);
12206
+ previousUnsubscribe?.();
12207
+ const maybe = bridge;
12208
+ const unsubscribe = maybe && typeof maybe.subscribeStatus === "function" ? maybe.subscribeStatus(listener) : () => {};
12209
+ this.statusListeners.set(listener, unsubscribe);
12210
+ this.statusBridges.set(listener, bridge);
12211
+ }
12212
+ removeStatusListener(listener) {
12213
+ const unsubscribe = this.statusListeners.get(listener);
12214
+ this.statusListeners.delete(listener);
12215
+ this.statusBridges.delete(listener);
12216
+ unsubscribe?.();
12217
+ }
12218
+ }
12219
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/client.js
11864
12220
  import { promises as fs2 } from "node:fs";
11865
12221
  import { debuglog } from "node:util";
11866
12222
 
11867
- // ../../node_modules/.bun/@cortexkit+subc-client@0.3.0/node_modules/@cortexkit/subc-client/src/auth.ts
12223
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/auth.js
11868
12224
  import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
11869
12225
  var NONCE_LEN = 32;
11870
12226
  var MAX_AUTH_MESSAGE_LEN = 4096;
@@ -11928,8 +12284,146 @@ async function authenticateClient(sock, conn, deadlineMs) {
11928
12284
  await writeMessage(sock, { client_auth: Array.from(clientAuth) }, deadlineMs);
11929
12285
  }
11930
12286
 
11931
- // ../../node_modules/.bun/@cortexkit+subc-client@0.3.0/node_modules/@cortexkit/subc-client/src/connection-file.ts
12287
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/connection-file.js
11932
12288
  import { promises as fs } from "node:fs";
12289
+
12290
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/envelope.js
12291
+ var PROTOCOL_VERSION = 2;
12292
+ var HEADER_LEN = 21;
12293
+ var FROZEN_PREFIX_LEN = 5;
12294
+ var MAX_FRAME_BODY_LEN = 64 * 1024 * 1024;
12295
+ var FrameType;
12296
+ (function(FrameType2) {
12297
+ FrameType2[FrameType2["Request"] = 0] = "Request";
12298
+ FrameType2[FrameType2["Response"] = 1] = "Response";
12299
+ FrameType2[FrameType2["Push"] = 2] = "Push";
12300
+ FrameType2[FrameType2["StreamData"] = 3] = "StreamData";
12301
+ FrameType2[FrameType2["StreamEnd"] = 4] = "StreamEnd";
12302
+ FrameType2[FrameType2["Error"] = 5] = "Error";
12303
+ FrameType2[FrameType2["Cancel"] = 6] = "Cancel";
12304
+ FrameType2[FrameType2["Ping"] = 7] = "Ping";
12305
+ FrameType2[FrameType2["Pong"] = 8] = "Pong";
12306
+ FrameType2[FrameType2["Hello"] = 9] = "Hello";
12307
+ FrameType2[FrameType2["HelloAck"] = 10] = "HelloAck";
12308
+ FrameType2[FrameType2["Goodbye"] = 11] = "Goodbye";
12309
+ })(FrameType || (FrameType = {}));
12310
+ var FRAME_TYPE_MAX = FrameType.Goodbye;
12311
+ function isPureHeader(ty) {
12312
+ return ty === FrameType.Cancel || ty === FrameType.Ping || ty === FrameType.Pong || ty === FrameType.Goodbye;
12313
+ }
12314
+ var Priority;
12315
+ (function(Priority2) {
12316
+ Priority2[Priority2["Passive"] = 0] = "Passive";
12317
+ Priority2[Priority2["Interactive"] = 1] = "Interactive";
12318
+ Priority2[Priority2["Background"] = 2] = "Background";
12319
+ })(Priority || (Priority = {}));
12320
+ var AdmissionClass;
12321
+ (function(AdmissionClass2) {
12322
+ AdmissionClass2[AdmissionClass2["Normal"] = 0] = "Normal";
12323
+ AdmissionClass2[AdmissionClass2["Expedite"] = 1] = "Expedite";
12324
+ AdmissionClass2[AdmissionClass2["Sheddable"] = 2] = "Sheddable";
12325
+ })(AdmissionClass || (AdmissionClass = {}));
12326
+ var FLAG_BINARY = 1;
12327
+ var FLAG_PRIORITY_MASK = 6;
12328
+ var FLAG_PRIORITY_SHIFT = 1;
12329
+ var FLAG_LAST = 8;
12330
+ var FLAG_ADMISSION_MASK = 48;
12331
+ var FLAG_ADMISSION_SHIFT = 4;
12332
+ var FLAG_RESERVED_MASK = 192;
12333
+ function buildFlags(binary, priority, last, admissionClass = AdmissionClass.Normal) {
12334
+ let flags = 0;
12335
+ if (binary)
12336
+ flags |= FLAG_BINARY;
12337
+ flags |= priority << FLAG_PRIORITY_SHIFT;
12338
+ if (last)
12339
+ flags |= FLAG_LAST;
12340
+ flags |= admissionClass << FLAG_ADMISSION_SHIFT;
12341
+ return flags;
12342
+ }
12343
+ function encodeHeader(header) {
12344
+ const buffer = new Uint8Array(HEADER_LEN);
12345
+ const view = new DataView(buffer.buffer);
12346
+ view.setUint32(0, header.len, true);
12347
+ buffer[4] = header.ver;
12348
+ buffer[5] = header.ty;
12349
+ buffer[6] = header.flags;
12350
+ view.setUint16(7, header.channel, true);
12351
+ view.setUint32(9, header.epoch, true);
12352
+ view.setBigUint64(13, header.corr, true);
12353
+ return buffer;
12354
+ }
12355
+
12356
+ class DecodeError extends Error {
12357
+ code;
12358
+ constructor(message, code) {
12359
+ super(message);
12360
+ this.code = code;
12361
+ this.name = "DecodeError";
12362
+ }
12363
+ }
12364
+ function decodeHeader(bytes) {
12365
+ if (bytes.length < FROZEN_PREFIX_LEN) {
12366
+ throw new DecodeError(`header shorter than frozen prefix: have ${bytes.length} bytes`, "too_short_for_prefix");
12367
+ }
12368
+ const ver = bytes[4];
12369
+ if (ver !== PROTOCOL_VERSION)
12370
+ throw new DecodeError(`unsupported envelope version ${ver}`, "unsupported_version");
12371
+ if (bytes.length < HEADER_LEN) {
12372
+ throw new DecodeError(`header too short for version: have ${bytes.length} bytes, need ${HEADER_LEN}`, "too_short_for_header");
12373
+ }
12374
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
12375
+ const len = view.getUint32(0, true);
12376
+ const typeByte = bytes[5];
12377
+ if (typeByte > FRAME_TYPE_MAX)
12378
+ throw new DecodeError(`unknown frame type byte ${typeByte}`, "unknown_frame_type");
12379
+ const ty = typeByte;
12380
+ const flags = bytes[6];
12381
+ if ((flags & FLAG_RESERVED_MASK) !== 0) {
12382
+ throw new DecodeError(`reserved flag bits set in flags 0b${flags.toString(2).padStart(8, "0")}`, "reserved_flag_bits");
12383
+ }
12384
+ if ((flags & FLAG_PRIORITY_MASK) >> FLAG_PRIORITY_SHIFT === 3) {
12385
+ throw new DecodeError(`reserved priority bits set in flags 0b${flags.toString(2).padStart(8, "0")}`, "reserved_priority_bits");
12386
+ }
12387
+ const admission = (flags & FLAG_ADMISSION_MASK) >> FLAG_ADMISSION_SHIFT;
12388
+ if (admission === 3) {
12389
+ throw new DecodeError(`reserved admission class set in flags 0b${flags.toString(2).padStart(8, "0")}`, "reserved_admission_class");
12390
+ }
12391
+ if (admission === AdmissionClass.Sheddable && ty !== FrameType.Push && ty !== FrameType.StreamData) {
12392
+ throw new DecodeError(`SHEDDABLE admission class is illegal on ${FrameType[ty]} in flags 0b${flags.toString(2).padStart(8, "0")}`, "sheddable_illegal_frame_type");
12393
+ }
12394
+ const channel = view.getUint16(7, true);
12395
+ const epoch = view.getUint32(9, true);
12396
+ if (channel === 0 && epoch !== 0) {
12397
+ throw new DecodeError(`control channel carried nonzero epoch ${epoch}`, "nonzero_epoch_on_control_channel");
12398
+ }
12399
+ if (isPureHeader(ty) && len !== 0) {
12400
+ throw new DecodeError(`pure-header frame ${FrameType[ty]} declared non-zero body length ${len}`, "pure_header_frame_with_body");
12401
+ }
12402
+ return { len, ver, ty, flags, channel, epoch, corr: view.getBigUint64(13, true) };
12403
+ }
12404
+ function buildFrame(ty, flags, channel, epoch, corr, body) {
12405
+ return buildFrameWithVersion(PROTOCOL_VERSION, ty, flags, channel, epoch, corr, body);
12406
+ }
12407
+ function buildFrameWithVersion(ver, ty, flags, channel, epoch, corr, body) {
12408
+ if (body.length > MAX_FRAME_BODY_LEN) {
12409
+ throw new DecodeError(`frame body ${body.length} exceeds max ${MAX_FRAME_BODY_LEN}`, "frame_body_too_large");
12410
+ }
12411
+ const header = { len: body.length, ver, ty, flags, channel, epoch, corr };
12412
+ decodeHeader(encodeHeader(header));
12413
+ return { header, body };
12414
+ }
12415
+ function encodeFrame(frame) {
12416
+ if (frame.header.len !== frame.body.length) {
12417
+ throw new DecodeError(`frame header length ${frame.header.len} does not match body length ${frame.body.length}`, "frame_length_mismatch");
12418
+ }
12419
+ const header = encodeHeader(frame.header);
12420
+ const output = new Uint8Array(header.length + frame.body.length);
12421
+ output.set(header, 0);
12422
+ output.set(frame.body, header.length);
12423
+ return output;
12424
+ }
12425
+
12426
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/connection-file.js
11933
12427
  var SCHEMA_VERSION = 1;
11934
12428
  var MIN_KEY_LEN = 32;
11935
12429
  var DAEMON_ID_LEN = 16;
@@ -11959,8 +12453,8 @@ function validate(info) {
11959
12453
  async function verifyOwnerOnly(path2) {
11960
12454
  if (process.platform === "win32")
11961
12455
  return;
11962
- const stat = await fs.stat(path2);
11963
- const mode = stat.mode & 511;
12456
+ const stat2 = await fs.stat(path2);
12457
+ const mode = stat2.mode & 511;
11964
12458
  if ((mode & 63) !== 0) {
11965
12459
  throw new ConnectionFileError(`connection file ${path2} has insecure permissions 0o${mode.toString(8)}; expected owner-only 0600`);
11966
12460
  }
@@ -11974,6 +12468,10 @@ async function readConnectionFile(path2) {
11974
12468
  } catch (err) {
11975
12469
  throw new ConnectionFileError(`connection file JSON read failed for ${path2}: ${String(err)}`);
11976
12470
  }
12471
+ const wireVersion = parsed.wire_version;
12472
+ if (wireVersion !== undefined && wireVersion !== PROTOCOL_VERSION) {
12473
+ throw new ConnectionFileError(`connection file wire_version ${String(wireVersion)} but this client speaks ${PROTOCOL_VERSION}; the client library must be upgraded`);
12474
+ }
11977
12475
  const endpointsRaw = parsed.endpoints;
11978
12476
  if (!Array.isArray(endpointsRaw)) {
11979
12477
  throw new ConnectionFileError("connection file 'endpoints' must be an array");
@@ -11997,116 +12495,54 @@ async function readConnectionFile(path2) {
11997
12495
  return info;
11998
12496
  }
11999
12497
 
12000
- // ../../node_modules/.bun/@cortexkit+subc-client@0.3.0/node_modules/@cortexkit/subc-client/src/envelope.ts
12001
- var PROTOCOL_VERSION = 1;
12002
- var HEADER_LEN = 17;
12003
- var FROZEN_PREFIX_LEN = 5;
12004
- var MAX_FRAME_BODY_LEN = 64 * 1024 * 1024;
12005
- var FrameType;
12006
- ((FrameType2) => {
12007
- FrameType2[FrameType2["Request"] = 0] = "Request";
12008
- FrameType2[FrameType2["Response"] = 1] = "Response";
12009
- FrameType2[FrameType2["Push"] = 2] = "Push";
12010
- FrameType2[FrameType2["StreamData"] = 3] = "StreamData";
12011
- FrameType2[FrameType2["StreamEnd"] = 4] = "StreamEnd";
12012
- FrameType2[FrameType2["Error"] = 5] = "Error";
12013
- FrameType2[FrameType2["Cancel"] = 6] = "Cancel";
12014
- FrameType2[FrameType2["Ping"] = 7] = "Ping";
12015
- FrameType2[FrameType2["Pong"] = 8] = "Pong";
12016
- FrameType2[FrameType2["Hello"] = 9] = "Hello";
12017
- FrameType2[FrameType2["HelloAck"] = 10] = "HelloAck";
12018
- FrameType2[FrameType2["Goodbye"] = 11] = "Goodbye";
12019
- })(FrameType ||= {});
12020
- var FRAME_TYPE_MAX = 11 /* Goodbye */;
12021
- function isPureHeader(ty) {
12022
- return ty === 6 /* Cancel */ || ty === 7 /* Ping */ || ty === 8 /* Pong */ || ty === 11 /* Goodbye */;
12023
- }
12024
- var FLAG_BINARY = 1;
12025
- var FLAG_PRIORITY_MASK = 6;
12026
- var FLAG_PRIORITY_SHIFT = 1;
12027
- var FLAG_LAST = 8;
12028
- var FLAG_RESERVED_MASK = 240;
12029
- function buildFlags(binary, priority, last) {
12030
- let b = 0;
12031
- if (binary)
12032
- b |= FLAG_BINARY;
12033
- b |= priority << FLAG_PRIORITY_SHIFT;
12034
- if (last)
12035
- b |= FLAG_LAST;
12036
- return b;
12037
- }
12038
- function encodeHeader(h) {
12039
- const buf = new Uint8Array(HEADER_LEN);
12040
- const view = new DataView(buf.buffer);
12041
- view.setUint32(0, h.len, true);
12042
- buf[4] = h.ver;
12043
- buf[5] = h.ty;
12044
- buf[6] = h.flags;
12045
- view.setUint16(7, h.channel, true);
12046
- view.setBigUint64(9, h.corr, true);
12047
- return buf;
12048
- }
12498
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/route-handle.js
12499
+ var connectionToken = new WeakMap;
12049
12500
 
12050
- class DecodeError extends Error {
12051
- }
12052
- function decodeHeader(bytes) {
12053
- if (bytes.length < FROZEN_PREFIX_LEN) {
12054
- throw new DecodeError(`header shorter than frozen prefix: have ${bytes.length} bytes`);
12055
- }
12056
- const ver = bytes[4];
12057
- if (ver !== PROTOCOL_VERSION) {
12058
- throw new DecodeError(`unsupported envelope version ${ver}`);
12059
- }
12060
- if (bytes.length < HEADER_LEN) {
12061
- throw new DecodeError(`header too short for version: have ${bytes.length} bytes, need ${HEADER_LEN}`);
12062
- }
12063
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
12064
- const len = view.getUint32(0, true);
12065
- const tyByte = bytes[5];
12066
- if (tyByte > FRAME_TYPE_MAX) {
12067
- throw new DecodeError(`unknown frame type byte ${tyByte}`);
12068
- }
12069
- const ty = tyByte;
12070
- const flags = bytes[6];
12071
- if ((flags & FLAG_RESERVED_MASK) !== 0) {
12072
- throw new DecodeError(`reserved flag bits set in flags 0b${flags.toString(2)}`);
12073
- }
12074
- if ((flags & FLAG_PRIORITY_MASK) >> FLAG_PRIORITY_SHIFT === 3) {
12075
- throw new DecodeError(`reserved priority bits set in flags 0b${flags.toString(2)}`);
12501
+ class RouteHandle {
12502
+ channel;
12503
+ epoch;
12504
+ constructor(channel, epoch, token) {
12505
+ if (!Number.isInteger(channel) || channel <= 0 || channel > 65535) {
12506
+ throw new RangeError(`route channel must be an integer in 1..65535, got ${channel}`);
12507
+ }
12508
+ if (!Number.isInteger(epoch) || epoch <= 0 || epoch > 4294967295) {
12509
+ throw new RangeError(`route epoch must be an integer in 1..4294967295, got ${epoch}`);
12510
+ }
12511
+ this.channel = channel;
12512
+ this.epoch = epoch;
12513
+ connectionToken.set(this, token);
12514
+ Object.freeze(this);
12076
12515
  }
12077
- if (isPureHeader(ty) && len !== 0) {
12078
- throw new DecodeError(`pure-header frame ${FrameType[ty]} declared non-zero body length ${len}`);
12516
+ static create(channel, epoch, token) {
12517
+ return new RouteHandle(channel, epoch, token);
12079
12518
  }
12080
- const channel = view.getUint16(7, true);
12081
- const corr = view.getBigUint64(9, true);
12082
- return { len, ver, ty, flags, channel, corr };
12083
12519
  }
12084
- function buildFrame(ty, flags, channel, corr, body) {
12085
- return buildFrameWithVersion(PROTOCOL_VERSION, ty, flags, channel, corr, body);
12520
+ function createRouteHandle(channel, epoch, token) {
12521
+ const factory = RouteHandle;
12522
+ return factory.create(channel, epoch, token);
12086
12523
  }
12087
- function buildFrameWithVersion(ver, ty, flags, channel, corr, body) {
12088
- if (body.length > MAX_FRAME_BODY_LEN) {
12089
- throw new DecodeError(`frame body ${body.length} exceeds max ${MAX_FRAME_BODY_LEN}`);
12090
- }
12091
- if (isPureHeader(ty) && body.length !== 0) {
12092
- throw new DecodeError(`pure-header frame ${FrameType[ty]} cannot carry a body`);
12524
+
12525
+ class StaleRouteHandleError extends Error {
12526
+ handle;
12527
+ code = "stale_route_handle";
12528
+ constructor(handle) {
12529
+ super(`route handle (${handle.channel}, ${handle.epoch}) is not live on the current connection`);
12530
+ this.handle = handle;
12531
+ this.name = "StaleRouteHandleError";
12093
12532
  }
12094
- return {
12095
- header: { len: body.length, ver, ty, flags, channel, corr },
12096
- body
12097
- };
12098
12533
  }
12099
- function encodeFrame(frame) {
12100
- const header = encodeHeader(frame.header);
12101
- const out = new Uint8Array(header.length + frame.body.length);
12102
- out.set(header, 0);
12103
- out.set(frame.body, header.length);
12104
- return out;
12534
+ function newConnectionToken() {
12535
+ return Object.freeze({});
12536
+ }
12537
+ function belongsToConnection(handle, token) {
12538
+ return connectionToken.get(handle) === token;
12539
+ }
12540
+ function sameRouteHandle(left, right) {
12541
+ return left === right;
12105
12542
  }
12106
12543
 
12107
- // ../../node_modules/.bun/@cortexkit+subc-client@0.3.0/node_modules/@cortexkit/subc-client/src/socket.ts
12544
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/socket.js
12108
12545
  import net from "node:net";
12109
-
12110
12546
  class SocketClosedError extends Error {
12111
12547
  }
12112
12548
 
@@ -12175,6 +12611,24 @@ class SubcSocket {
12175
12611
  });
12176
12612
  });
12177
12613
  }
12614
+ async readFrame(headerDeadlineMs, bodyDeadline, onHeader) {
12615
+ const prefix = await this.readExact(FROZEN_PREFIX_LEN, headerDeadlineMs);
12616
+ const version = prefix[4];
12617
+ if (version !== PROTOCOL_VERSION)
12618
+ throw new DecodeError(`unsupported envelope version ${version}`, "unsupported_version");
12619
+ const remainder = await this.readExact(HEADER_LEN - FROZEN_PREFIX_LEN, headerDeadlineMs);
12620
+ const headerBytes = new Uint8Array(HEADER_LEN);
12621
+ headerBytes.set(prefix);
12622
+ headerBytes.set(remainder, FROZEN_PREFIX_LEN);
12623
+ const header = decodeHeader(headerBytes);
12624
+ if (header.len > MAX_FRAME_BODY_LEN) {
12625
+ throw new DecodeError(`frame body ${header.len} exceeds max ${MAX_FRAME_BODY_LEN}`, "frame_body_too_large");
12626
+ }
12627
+ onHeader?.();
12628
+ const bodyDeadlineMs = typeof bodyDeadline === "number" ? bodyDeadline : Date.now() + bodyDeadline.afterHeaderMs;
12629
+ const body = header.len === 0 ? new Uint8Array(0) : await this.readExact(header.len, bodyDeadlineMs);
12630
+ return { header, body };
12631
+ }
12178
12632
  readExact(n, deadlineMs) {
12179
12633
  if (this.waiter) {
12180
12634
  return Promise.reject(new Error("concurrent readExact is not supported"));
@@ -12296,14 +12750,14 @@ class SubcSocket {
12296
12750
  }
12297
12751
  }
12298
12752
 
12299
- // ../../node_modules/.bun/@cortexkit+subc-client@0.3.0/node_modules/@cortexkit/subc-client/src/client.ts
12753
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/client.js
12300
12754
  var debug = debuglog("subc-client");
12301
12755
  var DEFAULT_HANDSHAKE_TIMEOUT_MS = 1e4;
12302
12756
  var DEFAULT_REQUEST_TIMEOUT_MS = 30000;
12303
12757
  var TIMEOUT_ARBITRATION_GRACE_MS = 50;
12304
12758
  var REQUEST_DEADLINE_MARKER = "request_deadline";
12305
12759
  var DEADLINE_NO_DROP_CODE = "deadline_exceeded_no_drop_observed";
12306
- var ROUTE_OPEN_RETRY_DEADLINE_MS = 1e4;
12760
+ var ROUTE_OPEN_RETRY_DEADLINE_MS = 30000;
12307
12761
  var BODY_READ_TIMEOUT_MS = 30000;
12308
12762
  var EMPTY_BODY = new Uint8Array(0);
12309
12763
  var DEFAULT_MANAGED_TARGET_KIND = "management_surface";
@@ -12342,7 +12796,11 @@ class SubcClient {
12342
12796
  opts;
12343
12797
  nextCorr = 1n;
12344
12798
  pending = new Map;
12799
+ lateResponses = new Map;
12345
12800
  routes = new Map;
12801
+ liveRoutes = new Map;
12802
+ connectionToken = newConnectionToken();
12803
+ ingressEpochDropCount = 0;
12346
12804
  closedErr = null;
12347
12805
  closeStarted = false;
12348
12806
  reconnecting = null;
@@ -12370,34 +12828,71 @@ class SubcClient {
12370
12828
  }
12371
12829
  async routeOpen(target, identity, opts = {}) {
12372
12830
  const consumerIdentity = routeOpenConsumerIdentity(opts);
12831
+ const consumerCapabilities = opts.consumerCapabilities;
12373
12832
  const body = this.encode({
12374
12833
  op: "route.open",
12375
12834
  target,
12376
12835
  identity,
12377
- ...consumerIdentity ? { consumer_identity: consumerIdentity } : {}
12836
+ ...consumerIdentity ? { consumer_identity: consumerIdentity } : {},
12837
+ ...consumerCapabilities !== undefined ? { consumer_capabilities: consumerCapabilities } : {}
12378
12838
  });
12379
- const reply = await this.controlRpc(body);
12380
- const parsed = this.parseJson(reply);
12381
- if (typeof parsed.route_channel !== "number") {
12382
- throw new SubcError(`route.open returned no route_channel: ${JSON.stringify(parsed)}`);
12383
- }
12384
- return parsed.route_channel;
12839
+ let installed = null;
12840
+ const install = (frame) => {
12841
+ if (frame.header.ty !== FrameType.Response)
12842
+ return true;
12843
+ const parsed = this.parseJson(frame);
12844
+ if (typeof parsed.route_channel !== "number" || typeof parsed.route_epoch !== "number") {
12845
+ throw new SubcError(`route.open returned no route handle: ${JSON.stringify(parsed)}`);
12846
+ }
12847
+ installed = this.installRoute(parsed.route_channel, parsed.route_epoch);
12848
+ return true;
12849
+ };
12850
+ const closeLateRoute = (frame) => {
12851
+ if (frame.header.ty !== FrameType.Response)
12852
+ return;
12853
+ try {
12854
+ const parsed = this.parseJson(frame);
12855
+ if (typeof parsed.route_channel !== "number" || typeof parsed.route_epoch !== "number")
12856
+ return;
12857
+ const lateHandle = this.installRoute(parsed.route_channel, parsed.route_epoch);
12858
+ this.failHandle(lateHandle, new SubcError("late route.open was closed", "route_closed"));
12859
+ this.liveRoutes.delete(lateHandle.channel);
12860
+ this.sendRouteGoodbye(lateHandle, true);
12861
+ } catch {
12862
+ this.closeConnectionAfterCleanupFailure();
12863
+ }
12864
+ };
12865
+ await this.controlRpc(body, install, closeLateRoute);
12866
+ if (!installed)
12867
+ throw new SubcError("route.open response was not installed");
12868
+ return installed;
12385
12869
  }
12386
- async request(routeChannel, body, opts = {}) {
12870
+ async request(handle, body, opts = {}) {
12871
+ this.assertLiveHandle(handle);
12387
12872
  const bytes = body instanceof Uint8Array ? body : this.encode(body);
12388
- const priority = opts.priority ?? 1 /* Interactive */;
12389
- const reply = await this.send(routeChannel, bytes, priority, opts.timeoutMs, opts.onProgress);
12873
+ const priority = opts.priority ?? Priority.Interactive;
12874
+ const admission = opts.admissionClass ?? AdmissionClass.Normal;
12875
+ const reply = await this.send(handle, bytes, priority, admission, opts.timeoutMs, opts.onProgress);
12390
12876
  return this.parseJson(reply);
12391
12877
  }
12392
12878
  async call(moduleId, method, params, opts = {}) {
12393
12879
  const body = params === undefined ? { method } : { method, params };
12880
+ let retriedUnknownChannel = false;
12394
12881
  for (;; ) {
12395
- const routeChannel = await this.cachedRouteChannel(moduleId, opts);
12882
+ const routeHandle = await this.cachedRouteHandle(moduleId, opts);
12396
12883
  try {
12397
- return await this.managedRequest(routeChannel, body, opts);
12884
+ return await this.managedRequest(routeHandle, body, opts);
12398
12885
  } catch (err) {
12399
12886
  if (!(err instanceof SubcCallError))
12400
12887
  throw this.terminalCallError("managed call failed", err);
12888
+ if (err.code === "unknown_channel" && !retriedUnknownChannel && !this.closeStarted) {
12889
+ retriedUnknownChannel = true;
12890
+ this.evictRouteHandle(routeHandle);
12891
+ continue;
12892
+ }
12893
+ if (err.code === "unknown_channel" && retriedUnknownChannel) {
12894
+ this.evictRouteHandle(routeHandle);
12895
+ }
12401
12896
  if (err.kind === "not_sent") {
12402
12897
  try {
12403
12898
  await this.reconnectAfterDrop(err);
@@ -12413,29 +12908,31 @@ class SubcClient {
12413
12908
  }
12414
12909
  }
12415
12910
  }
12416
- subscribe(routeChannel, body, onEvent, opts = {}) {
12911
+ subscribe(handle, body, onEvent, opts = {}) {
12912
+ this.assertLiveHandle(handle);
12417
12913
  const bytes = body instanceof Uint8Array ? body : this.encode(body);
12418
- const priority = opts.priority ?? 1 /* Interactive */;
12419
- const corr = this.nextCorr++;
12420
- const key = `${routeChannel}:${corr}`;
12914
+ const priority = opts.priority ?? Priority.Interactive;
12915
+ const admission = opts.admissionClass ?? AdmissionClass.Normal;
12916
+ const corr = this.allocateCorr();
12917
+ const key = pendingKey(handle, corr);
12421
12918
  const closed = new Promise((resolve7, reject) => {
12422
12919
  if (this.closedErr) {
12423
12920
  reject(this.closedErr);
12424
12921
  return;
12425
12922
  }
12426
12923
  this.pending.set(key, {
12427
- channel: routeChannel,
12924
+ handle,
12428
12925
  resolve: () => resolve7(),
12429
12926
  reject,
12430
12927
  onProgress: onEvent,
12431
12928
  timer: null,
12432
12929
  subscription: true
12433
12930
  });
12434
- const frame = buildFrame(0 /* Request */, buildFlags(false, priority, false), routeChannel, corr, bytes);
12931
+ const frame = buildFrame(FrameType.Request, buildFlags(false, priority, false, admission), handle.channel, handle.epoch, corr, bytes);
12435
12932
  this.sock.write(encodeFrame(frame), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS).catch((err) => {
12436
- const p = this.pending.get(key);
12437
- if (p)
12438
- this.rejectPending(key, p, err instanceof Error ? err : new SubcError(String(err)));
12933
+ const pending = this.pending.get(key);
12934
+ if (pending)
12935
+ this.rejectPending(key, pending, err instanceof Error ? err : new SubcError(String(err)));
12439
12936
  });
12440
12937
  });
12441
12938
  let cancelled = false;
@@ -12443,45 +12940,77 @@ class SubcClient {
12443
12940
  if (cancelled)
12444
12941
  return;
12445
12942
  cancelled = true;
12446
- const cancel = buildFrame(6 /* Cancel */, buildFlags(false, priority, false), routeChannel, corr, EMPTY_BODY);
12447
- this.sock.write(encodeFrame(cancel), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS).catch(() => {});
12943
+ this.cancel(handle, corr, priority);
12448
12944
  };
12449
12945
  return { unsubscribe, closed };
12450
12946
  }
12451
- async closeRoute(target, identity, opts = {}) {
12947
+ cancel(handle, corr, priority = Priority.Interactive) {
12948
+ this.assertLiveHandle(handle);
12949
+ const cancel = buildFrame(FrameType.Cancel, buildFlags(false, priority, false), handle.channel, handle.epoch, corr, EMPTY_BODY);
12950
+ this.sock.write(encodeFrame(cancel), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS).catch(() => {
12951
+ return;
12952
+ });
12953
+ }
12954
+ async routePoll(handle, kind) {
12955
+ this.assertLiveHandle(handle);
12956
+ const body = this.encode({
12957
+ op: "route.poll",
12958
+ route_channel: handle.channel,
12959
+ route_epoch: handle.epoch,
12960
+ kind
12961
+ });
12962
+ const reply = await this.controlRpc(body, (frame) => {
12963
+ if (frame.header.ty !== FrameType.Response)
12964
+ return true;
12965
+ const parsed = this.parseJson(frame);
12966
+ return parsed.route_channel === handle.channel && parsed.route_epoch === handle.epoch;
12967
+ });
12968
+ return this.parseJson(reply);
12969
+ }
12970
+ async closeRoute(handle, opts = {}) {
12971
+ this.assertLiveHandle(handle);
12972
+ for (const [key, cached] of this.routes) {
12973
+ if (cached.handle && sameRouteHandle(cached.handle, handle)) {
12974
+ cached.closed = true;
12975
+ cached.handle = null;
12976
+ this.routes.delete(key);
12977
+ }
12978
+ }
12979
+ if (opts.drain)
12980
+ await this.drainUnaryOnHandle(handle);
12981
+ this.failHandle(handle, new SubcError("route closed by closeRoute", "route_closed"));
12982
+ if (this.liveRoutes.get(handle.channel) === handle)
12983
+ this.liveRoutes.delete(handle.channel);
12984
+ this.sendRouteGoodbye(handle);
12985
+ }
12986
+ async closeManagedRoute(target, identity, opts = {}) {
12452
12987
  const key = routeCacheKey(target, identity, routeOpenConsumerIdentity(opts));
12453
12988
  const cached = this.routes.get(key);
12454
12989
  if (!cached)
12455
12990
  return;
12456
12991
  cached.closed = true;
12457
12992
  this.routes.delete(key);
12458
- const channel = cached.channel;
12459
- cached.channel = null;
12460
- if (channel !== null)
12461
- await this.closeRouteChannel(channel, opts);
12993
+ const handle = cached.handle;
12994
+ cached.handle = null;
12995
+ if (handle)
12996
+ await this.closeRoute(handle, opts);
12462
12997
  }
12463
- async closeRouteChannel(channel, opts = {}) {
12464
- if (channel === 0)
12465
- return;
12466
- if (opts.drain) {
12467
- await this.drainUnaryOnChannel(channel);
12468
- }
12469
- this.failChannel(channel, new SubcError("route closed by closeRoute", "route_closed"));
12470
- this.sendRouteGoodbye(channel);
12998
+ async closeRouteChannel(handle, opts = {}) {
12999
+ await this.closeRoute(handle, opts);
12471
13000
  }
12472
13001
  close() {
12473
13002
  this.closeStarted = true;
12474
13003
  this.fail(new SubcError("client closed"));
12475
13004
  this.sock.close();
12476
13005
  }
12477
- drainUnaryOnChannel(channel) {
13006
+ drainUnaryOnHandle(handle) {
12478
13007
  const waiters = [];
12479
13008
  for (const pending of this.pending.values()) {
12480
- if (pending.channel === channel && !pending.subscription) {
13009
+ if (pending.handle === handle && !pending.subscription) {
12481
13010
  waiters.push(new Promise((resolve7) => {
12482
- const prev = pending.onSettle;
13011
+ const previous = pending.onSettle;
12483
13012
  pending.onSettle = () => {
12484
- prev?.();
13013
+ previous?.();
12485
13014
  resolve7();
12486
13015
  };
12487
13016
  }));
@@ -12491,11 +13020,21 @@ class SubcClient {
12491
13020
  return;
12492
13021
  });
12493
13022
  }
12494
- sendRouteGoodbye(channel) {
12495
- if (this.closedErr)
13023
+ sendRouteGoodbye(handle, closeOnQueueFailure = false) {
13024
+ this.assertLiveConnection(handle);
13025
+ if (this.closedErr) {
13026
+ if (closeOnQueueFailure)
13027
+ this.closeConnectionAfterCleanupFailure();
12496
13028
  return;
12497
- const goodbye = buildFrame(11 /* Goodbye */, buildFlags(false, 1 /* Interactive */, false), channel, 0n, EMPTY_BODY);
12498
- this.sock.write(encodeFrame(goodbye), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS).catch(() => {});
13029
+ }
13030
+ const goodbye = buildFrame(FrameType.Goodbye, buildFlags(false, Priority.Interactive, false), handle.channel, handle.epoch, 0n, EMPTY_BODY);
13031
+ const write = this.sock.writeTracked(encodeFrame(goodbye), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS);
13032
+ if (!write.queued && closeOnQueueFailure)
13033
+ this.closeConnectionAfterCleanupFailure();
13034
+ write.completed.catch(() => {
13035
+ if (closeOnQueueFailure && !write.queued)
13036
+ this.closeConnectionAfterCleanupFailure();
13037
+ });
12499
13038
  }
12500
13039
  static async openConnection(opts) {
12501
13040
  const conn = await readConnectionFile(opts.connectionFile);
@@ -12510,37 +13049,48 @@ class SubcClient {
12510
13049
  }
12511
13050
  return { sock, conn };
12512
13051
  }
12513
- async controlRpc(body) {
12514
- return this.send(0, body, 1 /* Interactive */, undefined, undefined);
13052
+ async controlRpc(body, acceptFrame, onLateResponse) {
13053
+ return this.send(null, body, Priority.Interactive, AdmissionClass.Normal, undefined, undefined, acceptFrame, onLateResponse);
12515
13054
  }
12516
- send(channel, body, priority, timeoutMs, onProgress) {
13055
+ send(handle, body, priority, admission, timeoutMs, onProgress, acceptFrame, onLateResponse) {
13056
+ if (handle)
13057
+ this.assertLiveHandle(handle);
12517
13058
  if (this.closedErr)
12518
13059
  return Promise.reject(this.closedErr);
12519
- const corr = this.nextCorr++;
12520
- const key = `${channel}:${corr}`;
12521
- const frame = buildFrame(0 /* Request */, buildFlags(false, priority, false), channel, corr, body);
13060
+ let corr;
13061
+ try {
13062
+ corr = this.allocateCorr();
13063
+ } catch (error2) {
13064
+ return Promise.reject(error2);
13065
+ }
13066
+ const key = pendingKey(handle, corr);
13067
+ const channel = handle?.channel ?? 0;
13068
+ const epoch = handle?.epoch ?? 0;
13069
+ const frame = buildFrame(FrameType.Request, buildFlags(false, priority, false, admission), channel, epoch, corr, body);
12522
13070
  return new Promise((resolve7, reject) => {
12523
13071
  const ms = timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
12524
13072
  const pending = {
12525
- channel,
13073
+ handle,
12526
13074
  resolve: resolve7,
12527
13075
  reject,
12528
13076
  onProgress,
12529
- timer: null
13077
+ timer: null,
13078
+ acceptFrame,
13079
+ onLateResponse
12530
13080
  };
12531
- pending.timer = setTimeout(() => {
12532
- this.arbitrateTimeout(key, pending, channel, corr, ms);
12533
- }, ms);
13081
+ pending.timer = setTimeout(() => this.arbitrateTimeout(key, pending, channel, corr, ms), ms);
12534
13082
  this.pending.set(key, pending);
12535
- this.sock.write(encodeFrame(frame), Date.now() + ms).catch((err) => {
12536
- const p = this.pending.get(key);
12537
- if (p)
12538
- this.rejectPending(key, p, err instanceof Error ? err : new SubcError(String(err)));
13083
+ this.sock.write(encodeFrame(frame), Date.now() + ms).catch((error2) => {
13084
+ const current = this.pending.get(key);
13085
+ if (current)
13086
+ this.rejectPending(key, current, error2 instanceof Error ? error2 : new SubcError(String(error2)));
12539
13087
  });
12540
13088
  });
12541
13089
  }
12542
13090
  arbitrateTimeout(key, pending, channel, corr, ms) {
12543
13091
  const settleAsTimeout = () => {
13092
+ if (pending.onLateResponse)
13093
+ this.lateResponses.set(key, pending.onLateResponse);
12544
13094
  this.rejectPending(key, pending, new SubcError(this.timeoutMessage(channel, corr, ms), REQUEST_DEADLINE_MARKER));
12545
13095
  };
12546
13096
  const graceDeadline = Date.now() + this.opts.timeoutArbitrationGraceMs;
@@ -12556,59 +13106,67 @@ class SubcClient {
12556
13106
  };
12557
13107
  setImmediate(arbitrate);
12558
13108
  }
12559
- async managedRequest(routeChannel, body, opts) {
13109
+ async managedRequest(handle, body, opts) {
12560
13110
  const bytes = body instanceof Uint8Array ? body : this.encode(body);
12561
- const priority = opts.priority ?? 1 /* Interactive */;
13111
+ const priority = opts.priority ?? Priority.Interactive;
13112
+ const admission = opts.admissionClass ?? AdmissionClass.Normal;
12562
13113
  try {
12563
- const reply = await this.sendManaged(routeChannel, bytes, priority, opts.timeoutMs, opts.onProgress);
13114
+ const reply = await this.sendManaged(handle, bytes, priority, admission, opts.timeoutMs, opts.onProgress);
12564
13115
  return this.parseJson(reply);
12565
- } catch (err) {
12566
- if (err instanceof SubcCallError)
12567
- throw err;
12568
- throw this.terminalCallError("managed call failed", err);
13116
+ } catch (error2) {
13117
+ if (error2 instanceof SubcCallError)
13118
+ throw error2;
13119
+ throw this.terminalCallError("managed call failed", error2);
12569
13120
  }
12570
13121
  }
12571
- sendManaged(channel, body, priority, timeoutMs, onProgress) {
13122
+ sendManaged(handle, body, priority, admission, timeoutMs, onProgress) {
13123
+ try {
13124
+ this.assertLiveHandle(handle);
13125
+ } catch (error2) {
13126
+ return Promise.reject(this.notSentCallError("request used a stale route handle", error2));
13127
+ }
12572
13128
  if (this.closedErr) {
12573
13129
  return Promise.reject(this.notSentCallError("request was not sent because the subc connection was already closed", this.closedErr));
12574
13130
  }
12575
- const corr = this.nextCorr++;
12576
- const key = `${channel}:${corr}`;
12577
- const frame = buildFrame(0 /* Request */, buildFlags(false, priority, false), channel, corr, body);
13131
+ let corr;
13132
+ try {
13133
+ corr = this.allocateCorr();
13134
+ } catch (error2) {
13135
+ return Promise.reject(this.notSentCallError("request correlation allocator was exhausted", error2));
13136
+ }
13137
+ const key = pendingKey(handle, corr);
13138
+ const frame = buildFrame(FrameType.Request, buildFlags(false, priority, false, admission), handle.channel, handle.epoch, corr, body);
12578
13139
  let handedToSocket = false;
12579
- const classifyFailure = (err) => {
12580
- if (!handedToSocket) {
12581
- return this.notSentCallError("request bytes were not queued to the subc socket", err);
12582
- }
12583
- if (err instanceof SubcError && err.code === REQUEST_DEADLINE_MARKER) {
12584
- return new SubcCallError("outcome_unknown", `managed call deadline exceeded after request bytes were queued to the local socket; no terminal response was observed; outcome unknown${causeMessage(err)}`, DEADLINE_NO_DROP_CODE, err);
13140
+ const classifyFailure = (error2) => {
13141
+ if (!handedToSocket)
13142
+ return this.notSentCallError("request bytes were not queued to the subc socket", error2);
13143
+ if (error2 instanceof SubcError && error2.code === REQUEST_DEADLINE_MARKER) {
13144
+ return new SubcCallError("outcome_unknown", `managed call deadline exceeded after request bytes were queued to the local socket; no terminal response was observed; outcome unknown${causeMessage(error2)}`, DEADLINE_NO_DROP_CODE, error2);
12585
13145
  }
12586
- return this.outcomeUnknownCallError("connection dropped before the managed call returned a response", err);
13146
+ return this.outcomeUnknownCallError("connection dropped before the managed call returned a response", error2);
12587
13147
  };
12588
13148
  return new Promise((resolve7, reject) => {
12589
13149
  const ms = timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
12590
13150
  const pending = {
12591
- channel,
13151
+ handle,
12592
13152
  resolve: resolve7,
12593
13153
  reject,
12594
13154
  onProgress,
12595
13155
  timer: null,
12596
13156
  classifyFailure
12597
13157
  };
12598
- pending.timer = setTimeout(() => {
12599
- this.arbitrateTimeout(key, pending, channel, corr, ms);
12600
- }, ms);
13158
+ pending.timer = setTimeout(() => this.arbitrateTimeout(key, pending, handle.channel, corr, ms), ms);
12601
13159
  this.pending.set(key, pending);
12602
13160
  const write = this.sock.writeTracked(encodeFrame(frame), Date.now() + ms);
12603
13161
  handedToSocket = write.queued;
12604
- write.completed.catch((err) => {
12605
- const p = this.pending.get(key);
12606
- if (p)
12607
- this.rejectPending(key, p, err instanceof Error ? err : new SubcError(String(err)));
13162
+ write.completed.catch((error2) => {
13163
+ const current = this.pending.get(key);
13164
+ if (current)
13165
+ this.rejectPending(key, current, error2 instanceof Error ? error2 : new SubcError(String(error2)));
12608
13166
  });
12609
13167
  });
12610
13168
  }
12611
- async cachedRouteChannel(moduleId, opts) {
13169
+ async cachedRouteHandle(moduleId, opts) {
12612
13170
  const identity = opts.identity ?? this.opts.identity;
12613
13171
  if (!identity) {
12614
13172
  throw new SubcCallError("terminal", "managed call requires a BindIdentity in SubcClient.connect({ identity }) or call(..., { identity })", "missing_identity");
@@ -12624,15 +13182,13 @@ class SubcClient {
12624
13182
  target,
12625
13183
  identity,
12626
13184
  consumerIdentity,
12627
- channel: null,
12628
- generation: 0,
13185
+ handle: null,
12629
13186
  opening: null
12630
13187
  };
12631
13188
  this.routes.set(key, cached);
12632
13189
  }
12633
- if (cached.channel !== null && cached.generation === this.generation && !this.closedErr) {
12634
- return cached.channel;
12635
- }
13190
+ if (cached.handle && this.isLiveHandle(cached.handle))
13191
+ return cached.handle;
12636
13192
  if (!cached.opening) {
12637
13193
  cached.opening = this.openCachedRoute(cached).finally(() => {
12638
13194
  cached.opening = null;
@@ -12649,44 +13205,43 @@ class SubcClient {
12649
13205
  throw this.routeClosedDuringOpen();
12650
13206
  try {
12651
13207
  await this.ensureConnectedForManaged();
12652
- } catch (err) {
12653
- throw this.notSentRecoveryError("route.open could not run because reconnect failed", err);
12654
- }
12655
- if (cached.channel !== null && cached.generation === this.generation && !this.closedErr) {
12656
- return cached.channel;
13208
+ } catch (error2) {
13209
+ throw this.notSentRecoveryError("route.open could not run because reconnect failed", error2);
12657
13210
  }
13211
+ if (cached.handle && this.isLiveHandle(cached.handle))
13212
+ return cached.handle;
12658
13213
  try {
12659
- const channel = await this.routeOpen(cached.target, cached.identity, {
13214
+ const handle = await this.routeOpen(cached.target, cached.identity, {
12660
13215
  consumerIdentity: cached.consumerIdentity ?? null
12661
13216
  });
12662
13217
  if (cached.closed) {
12663
- this.sendRouteGoodbye(channel);
13218
+ this.liveRoutes.delete(handle.channel);
13219
+ this.sendRouteGoodbye(handle);
12664
13220
  throw this.routeClosedDuringOpen();
12665
13221
  }
12666
- cached.channel = channel;
12667
- cached.generation = this.generation;
12668
- return channel;
12669
- } catch (err) {
12670
- if (err instanceof SubcCallError && err.code === "route_closed")
12671
- throw err;
12672
- if (!this.closeStarted && isConsumerReconnectTransient(err)) {
13222
+ cached.handle = handle;
13223
+ return handle;
13224
+ } catch (error2) {
13225
+ if (error2 instanceof SubcCallError && error2.code === "route_closed")
13226
+ throw error2;
13227
+ if (!this.closeStarted && isConsumerReconnectTransient(error2)) {
12673
13228
  try {
12674
- await this.reconnectAfterDrop(err);
12675
- } catch (reconnectErr) {
12676
- throw this.notSentRecoveryError("route.open was not sent and reconnect failed", reconnectErr);
13229
+ await this.reconnectAfterDrop(error2);
13230
+ } catch (reconnectError) {
13231
+ throw this.notSentRecoveryError("route.open was not sent and reconnect failed", reconnectError);
12677
13232
  }
12678
13233
  continue;
12679
13234
  }
12680
- if (!this.closeStarted && err instanceof SubcError && isRetryableRouteOpenCode(err.code)) {
13235
+ if (!this.closeStarted && error2 instanceof SubcError && isRetryableRouteOpenCode(error2.code)) {
12681
13236
  routeRetryAttempt += 1;
12682
13237
  if (routeRetryAttempt < this.opts.reconnectBackoff.maxAttempts && Date.now() < routeRetryDeadline) {
12683
13238
  await this.opts.sleep(routeRetryDelay);
12684
13239
  routeRetryDelay = Math.min(routeRetryDelay * 2, this.opts.reconnectBackoff.capMs);
12685
13240
  continue;
12686
13241
  }
12687
- throw this.notSentCallError(`route.open failed for module ${cached.moduleId}: ${err.code} (retry budget exhausted)`, err);
13242
+ throw this.notSentCallError(`route.open failed for module ${cached.moduleId}: ${error2.code} (retry budget exhausted)`, error2);
12688
13243
  }
12689
- throw this.terminalCallError(`route.open failed for module ${cached.moduleId}`, err);
13244
+ throw this.terminalCallError(`route.open failed for module ${cached.moduleId}`, error2);
12690
13245
  }
12691
13246
  }
12692
13247
  }
@@ -12733,6 +13288,9 @@ class SubcClient {
12733
13288
  return;
12734
13289
  } catch (err) {
12735
13290
  if (!isConsumerReconnectTransient(err) || attempt >= this.opts.reconnectBackoff.maxAttempts) {
13291
+ if (err instanceof AuthError && attempt > 1) {
13292
+ throw new AuthError(`reconnect gave up after ${attempt} attempts: ${err.message} — the connection file and the daemon's key disagree persistently ` + `(daemon restarting in a loop, split connection-file paths, or a genuinely foreign daemon on this port); ` + `check the daemon, then restart this host app`);
13293
+ }
12736
13294
  throw err;
12737
13295
  }
12738
13296
  await this.opts.sleep(delay);
@@ -12746,25 +13304,27 @@ class SubcClient {
12746
13304
  this.currentConn = opened.conn;
12747
13305
  this.closedErr = null;
12748
13306
  this.generation += 1;
13307
+ this.connectionToken = newConnectionToken();
13308
+ this.liveRoutes.clear();
13309
+ this.lateResponses.clear();
13310
+ this.nextCorr = 1n;
12749
13311
  this.readLoop(opened.sock, this.generation);
12750
13312
  }
12751
13313
  async reopenCachedRoutes() {
12752
- for (const cached of this.routes.values()) {
12753
- cached.channel = null;
12754
- cached.generation = 0;
12755
- }
13314
+ for (const cached of this.routes.values())
13315
+ cached.handle = null;
12756
13316
  for (const cached of this.routes.values()) {
12757
13317
  if (cached.closed)
12758
13318
  continue;
12759
- const channel = await this.routeOpen(cached.target, cached.identity, {
13319
+ const handle = await this.routeOpen(cached.target, cached.identity, {
12760
13320
  consumerIdentity: cached.consumerIdentity ?? null
12761
13321
  });
12762
13322
  if (cached.closed) {
12763
- this.sendRouteGoodbye(channel);
13323
+ this.liveRoutes.delete(handle.channel);
13324
+ this.sendRouteGoodbye(handle);
12764
13325
  continue;
12765
13326
  }
12766
- cached.channel = channel;
12767
- cached.generation = this.generation;
13327
+ cached.handle = handle;
12768
13328
  }
12769
13329
  }
12770
13330
  timeoutMessage(channel, corr, ms) {
@@ -12778,52 +13338,69 @@ class SubcClient {
12778
13338
  async readLoop(sock, generation) {
12779
13339
  try {
12780
13340
  for (;; ) {
12781
- const headerBytes = await sock.readExact(HEADER_LEN, Number.POSITIVE_INFINITY);
12782
- this.readerActive = true;
13341
+ this.readerActive = false;
13342
+ const frame = await sock.readFrame(Number.POSITIVE_INFINITY, { afterHeaderMs: BODY_READ_TIMEOUT_MS }, () => {
13343
+ this.readerActive = true;
13344
+ });
12783
13345
  try {
12784
- const header = decodeHeader(headerBytes);
12785
- const body = header.len === 0 ? new Uint8Array(0) : await sock.readExact(header.len, Date.now() + BODY_READ_TIMEOUT_MS);
12786
- if (this.sock === sock && this.generation === generation) {
12787
- this.dispatch({ header, body });
12788
- }
13346
+ if (this.sock === sock && this.generation === generation)
13347
+ this.dispatch(frame);
12789
13348
  } finally {
12790
13349
  this.readerActive = false;
12791
13350
  }
12792
13351
  }
12793
- } catch (err) {
13352
+ } catch (error2) {
12794
13353
  if (this.sock === sock && this.generation === generation) {
12795
- this.fail(err instanceof Error ? err : new SubcError(String(err)));
13354
+ this.fail(error2 instanceof Error ? error2 : new SubcError(String(error2)));
12796
13355
  }
12797
13356
  }
12798
13357
  }
12799
13358
  dispatch(frame) {
12800
- const key = `${frame.header.channel}:${frame.header.corr}`;
13359
+ let handle = null;
13360
+ if (frame.header.channel !== 0) {
13361
+ handle = this.liveRoutes.get(frame.header.channel) ?? null;
13362
+ if (!handle || handle.epoch !== frame.header.epoch) {
13363
+ this.ingressEpochDropCount += 1;
13364
+ return;
13365
+ }
13366
+ }
13367
+ const key = pendingKey(handle, frame.header.corr);
12801
13368
  const pending = this.pending.get(key);
12802
13369
  if (pending) {
13370
+ if (pending.acceptFrame && !pending.acceptFrame(frame))
13371
+ return;
12803
13372
  switch (frame.header.ty) {
12804
- case 2 /* Push */:
12805
- case 3 /* StreamData */:
13373
+ case FrameType.Push:
13374
+ case FrameType.StreamData:
12806
13375
  pending.onProgress?.(frame.body);
12807
13376
  return;
12808
- case 1 /* Response */:
12809
- case 4 /* StreamEnd */:
13377
+ case FrameType.Response:
13378
+ case FrameType.StreamEnd:
12810
13379
  this.settle(key, pending, () => pending.resolve(frame));
12811
13380
  return;
12812
- case 5 /* Error */:
13381
+ case FrameType.Error:
12813
13382
  this.settle(key, pending, () => pending.reject(this.errorFromFrame(frame)));
12814
13383
  return;
12815
13384
  default:
12816
13385
  return;
12817
13386
  }
12818
13387
  }
12819
- if (frame.header.ty === 11 /* Goodbye */) {
12820
- this.failChannel(frame.header.channel, new SubcError("route closed by subc (GOODBYE)"));
13388
+ const late = this.lateResponses.get(key);
13389
+ if (late && (frame.header.ty === FrameType.Response || frame.header.ty === FrameType.Error)) {
13390
+ this.lateResponses.delete(key);
13391
+ late(frame);
12821
13392
  return;
12822
13393
  }
12823
- if (frame.header.ty === 1 /* Response */ || frame.header.ty === 5 /* Error */ || frame.header.ty === 4 /* StreamEnd */) {
12824
- debug("dropped terminal frame with no waiter: type=%d channel=%d corr=%s port=%s", frame.header.ty, frame.header.channel, frame.header.corr, this.sock.localPort() ?? "?");
13394
+ if (frame.header.ty === FrameType.Goodbye && handle) {
13395
+ this.failHandle(handle, new SubcError("route closed by subc (GOODBYE)"));
13396
+ if (this.liveRoutes.get(handle.channel) === handle)
13397
+ this.liveRoutes.delete(handle.channel);
13398
+ this.evictRouteHandle(handle);
12825
13399
  return;
12826
13400
  }
13401
+ if (frame.header.ty === FrameType.Response || frame.header.ty === FrameType.Error || frame.header.ty === FrameType.StreamEnd) {
13402
+ debug("dropped terminal frame with no waiter: type=%d channel=%d epoch=%d corr=%s port=%s", frame.header.ty, frame.header.channel, frame.header.epoch, frame.header.corr, this.sock.localPort() ?? "?");
13403
+ }
12827
13404
  }
12828
13405
  settle(key, pending, run) {
12829
13406
  if (this.pending.get(key) !== pending)
@@ -12846,11 +13423,16 @@ class SubcClient {
12846
13423
  return new SubcError(Buffer.from(frame.body).toString("utf8") || "subc error");
12847
13424
  }
12848
13425
  }
12849
- failChannel(channel, err) {
13426
+ evictRouteHandle(handle) {
13427
+ for (const cached of this.routes.values()) {
13428
+ if (cached.handle && sameRouteHandle(cached.handle, handle))
13429
+ cached.handle = null;
13430
+ }
13431
+ }
13432
+ failHandle(handle, error2) {
12850
13433
  for (const [key, pending] of this.pending) {
12851
- if (pending.channel === channel) {
12852
- this.rejectPending(key, pending, err);
12853
- }
13434
+ if (pending.handle && sameRouteHandle(pending.handle, handle))
13435
+ this.rejectPending(key, pending, error2);
12854
13436
  }
12855
13437
  }
12856
13438
  fail(err) {
@@ -12878,6 +13460,44 @@ class SubcClient {
12878
13460
  return this.notSentCallError(message, cause);
12879
13461
  return this.terminalCallError(message, cause);
12880
13462
  }
13463
+ get droppedIngressFrames() {
13464
+ return this.ingressEpochDropCount;
13465
+ }
13466
+ installRoute(channel, epoch) {
13467
+ const handle = createRouteHandle(channel, epoch, this.connectionToken);
13468
+ this.liveRoutes.set(channel, handle);
13469
+ return handle;
13470
+ }
13471
+ isLiveHandle(handle) {
13472
+ return belongsToConnection(handle, this.connectionToken) && this.liveRoutes.get(handle.channel) === handle;
13473
+ }
13474
+ assertLiveConnection(handle) {
13475
+ if (!belongsToConnection(handle, this.connectionToken))
13476
+ throw new StaleRouteHandleError(handle);
13477
+ }
13478
+ assertLiveHandle(handle) {
13479
+ if (!this.isLiveHandle(handle))
13480
+ throw new StaleRouteHandleError(handle);
13481
+ }
13482
+ allocateCorr() {
13483
+ const maximum = 0xffffffffffffffffn;
13484
+ if (this.nextCorr > maximum) {
13485
+ const error2 = new SubcError("channel-0 correlation id allocator exhausted", "corr_exhausted");
13486
+ this.fail(error2);
13487
+ this.sock.close();
13488
+ this.scheduleReconnectAfterDrop(error2);
13489
+ throw error2;
13490
+ }
13491
+ const corr = this.nextCorr;
13492
+ this.nextCorr += 1n;
13493
+ return corr;
13494
+ }
13495
+ closeConnectionAfterCleanupFailure() {
13496
+ const error2 = new SubcError("late route cleanup could not be queued", "late_route_cleanup_failed");
13497
+ this.fail(error2);
13498
+ this.sock.close();
13499
+ this.scheduleReconnectAfterDrop(error2);
13500
+ }
12881
13501
  encode(value) {
12882
13502
  return new Uint8Array(Buffer.from(JSON.stringify(value), "utf8"));
12883
13503
  }
@@ -12892,7 +13512,9 @@ function isConsumerReconnectTransient(err) {
12892
13512
  return true;
12893
13513
  if (err instanceof SubcCallError)
12894
13514
  return err.kind === "not_sent" || err.kind === "outcome_unknown";
12895
- if (err instanceof SubcError || err instanceof ConnectionFileError || err instanceof AuthError)
13515
+ if (err instanceof AuthError)
13516
+ return true;
13517
+ if (err instanceof SubcError || err instanceof ConnectionFileError)
12896
13518
  return false;
12897
13519
  const code = errorCode(err);
12898
13520
  return code === "ECONNREFUSED" || code === "ECONNRESET" || code === "EPIPE" || code === "ETIMEDOUT" || code === "ENOENT";
@@ -12945,14 +13567,54 @@ function causeMessage(cause) {
12945
13567
  return "";
12946
13568
  return `: ${cause instanceof Error ? cause.message : String(cause)}`;
12947
13569
  }
12948
- // ../../node_modules/.bun/@cortexkit+subc-client@0.3.0/node_modules/@cortexkit/subc-client/src/provider.ts
13570
+ function pendingKey(handle, corr) {
13571
+ return handle ? `${handle.channel}:${handle.epoch}:${corr}` : `0:0:${corr}`;
13572
+ }
13573
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/provider.js
12949
13574
  import { Buffer as Buffer2 } from "node:buffer";
12950
13575
  var DEFAULT_HANDSHAKE_TIMEOUT_MS2 = 1e4;
12951
13576
  var BODY_READ_TIMEOUT_MS2 = 30000;
12952
13577
  var WRITE_TIMEOUT_MS = 30000;
12953
13578
  var DEFAULT_RESTORED_DEBOUNCE_MS = 250;
13579
+ var DEFAULT_PROVIDER_HANDLER_CAPACITY = 64;
13580
+ var HEALTH_CHECK_OP = "health.check";
12954
13581
  var HELLO_CORR = 1n;
12955
13582
 
13583
+ class AsyncPermitPool {
13584
+ available;
13585
+ waiters = [];
13586
+ constructor(capacity) {
13587
+ if (!Number.isInteger(capacity) || capacity <= 0) {
13588
+ throw new SubcProviderError("provider handler capacity must be a positive integer", "invalid_handler_capacity");
13589
+ }
13590
+ this.available = capacity;
13591
+ }
13592
+ async acquire() {
13593
+ if (this.available > 0) {
13594
+ this.available -= 1;
13595
+ return this.releaseOnce();
13596
+ }
13597
+ await new Promise((resolve7) => {
13598
+ this.waiters.push(resolve7);
13599
+ });
13600
+ return this.releaseOnce();
13601
+ }
13602
+ releaseOnce() {
13603
+ let released = false;
13604
+ return () => {
13605
+ if (released)
13606
+ return;
13607
+ released = true;
13608
+ const next = this.waiters.shift();
13609
+ if (next) {
13610
+ next();
13611
+ } else {
13612
+ this.available += 1;
13613
+ }
13614
+ };
13615
+ }
13616
+ }
13617
+
12956
13618
  class SubcProviderError extends Error {
12957
13619
  code;
12958
13620
  constructor(message, code) {
@@ -12971,6 +13633,12 @@ class SubcProvider {
12971
13633
  closeStarted = false;
12972
13634
  closedErr = null;
12973
13635
  inflight = new Map;
13636
+ pending = new Map;
13637
+ liveRoutes = new Map;
13638
+ connectionToken = newConnectionToken();
13639
+ nextCorr = 1n;
13640
+ ingressEpochDropCount = 0;
13641
+ requestGate = new AsyncPermitPool(DEFAULT_PROVIDER_HANDLER_CAPACITY);
12974
13642
  reconnecting = null;
12975
13643
  generation = 1;
12976
13644
  connectionEpoch = 1;
@@ -12989,12 +13657,55 @@ class SubcProvider {
12989
13657
  this.readLoop(sock, this.generation);
12990
13658
  this.enqueueConnectionState({ state: "connected", epoch: this.connectionEpoch });
12991
13659
  }
13660
+ get droppedIngressFrames() {
13661
+ return this.ingressEpochDropCount;
13662
+ }
12992
13663
  get conn() {
12993
13664
  return this.currentConn;
12994
13665
  }
12995
13666
  currentEpoch() {
12996
13667
  return this.connectionEpoch;
12997
13668
  }
13669
+ async request(handle, body, opts = {}) {
13670
+ this.assertLiveHandle(handle);
13671
+ const corr = this.allocateCorr();
13672
+ const key = routeKey(handle, corr);
13673
+ const timeoutMs = opts.timeoutMs ?? WRITE_TIMEOUT_MS;
13674
+ const frame = buildFrame(FrameType.Request, buildFlags(false, opts.priority ?? Priority.Interactive, false, opts.admissionClass ?? AdmissionClass.Normal), handle.channel, handle.epoch, corr, body);
13675
+ return await new Promise((resolve7, reject) => {
13676
+ const timer = setTimeout(() => {
13677
+ if (this.pending.delete(key))
13678
+ reject(new SubcProviderError("reverse request timed out", "request_timeout"));
13679
+ }, timeoutMs);
13680
+ this.pending.set(key, {
13681
+ resolve: (response) => resolve7(response.body),
13682
+ reject,
13683
+ timer
13684
+ });
13685
+ this.sendOn(this.sock, this.generation, frame).catch((error2) => {
13686
+ const pending = this.pending.get(key);
13687
+ if (!pending)
13688
+ return;
13689
+ this.pending.delete(key);
13690
+ clearTimeout(pending.timer);
13691
+ reject(error2 instanceof Error ? error2 : new SubcProviderError(String(error2)));
13692
+ });
13693
+ });
13694
+ }
13695
+ async push(handle, body, opts = {}) {
13696
+ this.assertLiveHandle(handle);
13697
+ await this.sendOn(this.sock, this.generation, buildFrame(FrameType.Push, buildFlags(false, opts.priority ?? Priority.Interactive, false, opts.admissionClass ?? AdmissionClass.Normal), handle.channel, handle.epoch, 0n, body));
13698
+ }
13699
+ cancel(handle, corr) {
13700
+ this.assertLiveHandle(handle);
13701
+ this.sendOn(this.sock, this.generation, buildFrame(FrameType.Cancel, controlFlags(), handle.channel, handle.epoch, corr, new Uint8Array(0)));
13702
+ }
13703
+ closeRoute(handle) {
13704
+ this.assertLiveHandle(handle);
13705
+ this.liveRoutes.delete(handle.channel);
13706
+ this.abortHandle(handle);
13707
+ this.sendOn(this.sock, this.generation, buildFrame(FrameType.Goodbye, controlFlags(), handle.channel, handle.epoch, 0n, new Uint8Array(0)));
13708
+ }
12998
13709
  static async connect(opts) {
12999
13710
  if (opts.manifest.protocol_ver !== PROTOCOL_VERSION) {
13000
13711
  throw new SubcProviderError(`manifest protocol_ver ${opts.manifest.protocol_ver} does not match client protocol ${PROTOCOL_VERSION}`, "invalid_manifest");
@@ -13009,7 +13720,7 @@ class SubcProvider {
13009
13720
  this.cancelRestoredDebounce();
13010
13721
  const sock = this.sock;
13011
13722
  try {
13012
- await sendFrame(sock, buildFrame(11 /* Goodbye */, controlFlags(), 0, 0n, new Uint8Array(0)));
13723
+ await sendFrame(sock, buildFrame(FrameType.Goodbye, controlFlags(), 0, 0, 0n, new Uint8Array(0)));
13013
13724
  } catch {} finally {
13014
13725
  sock.close();
13015
13726
  this.finishClosed();
@@ -13017,12 +13728,13 @@ class SubcProvider {
13017
13728
  }
13018
13729
  await this.closed;
13019
13730
  }
13020
- static async openConnection(opts) {
13731
+ static async openConnection(opts, onSocket) {
13021
13732
  const conn = await readConnectionFile(opts.connectionFile);
13022
13733
  const deadline = Date.now() + (opts.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS2);
13023
13734
  const endpoint = conn.endpoints[0];
13024
13735
  const sock = await SubcSocket.connect(endpoint.host, endpoint.port, deadline);
13025
13736
  try {
13737
+ onSocket?.(sock);
13026
13738
  await authenticateClient(sock, conn, deadline);
13027
13739
  await sendFrame(sock, buildHelloFrame(opts));
13028
13740
  const ack = await expectHelloAck(sock, deadline);
@@ -13035,19 +13747,17 @@ class SubcProvider {
13035
13747
  async readLoop(sock, generation) {
13036
13748
  try {
13037
13749
  for (;; ) {
13038
- const headerBytes = await sock.readExact(HEADER_LEN, Number.POSITIVE_INFINITY);
13039
- const header = decodeHeader(headerBytes);
13040
- const body = header.len === 0 ? new Uint8Array(0) : await sock.readExact(header.len, Date.now() + BODY_READ_TIMEOUT_MS2);
13041
- const keepGoing = await this.dispatch({ header, body }, sock, generation);
13750
+ const frame = await sock.readFrame(Number.POSITIVE_INFINITY, { afterHeaderMs: BODY_READ_TIMEOUT_MS2 });
13751
+ const keepGoing = await this.dispatch(frame, sock, generation);
13042
13752
  if (!keepGoing) {
13043
13753
  if (this.sock === sock && this.generation === generation)
13044
13754
  this.closeStarted = true;
13045
13755
  break;
13046
13756
  }
13047
13757
  }
13048
- } catch (err) {
13758
+ } catch (error2) {
13049
13759
  if (this.sock === sock && this.generation === generation && !this.closeStarted) {
13050
- this.handleUnexpectedDrop(sock, generation, err instanceof Error ? err : new SubcProviderError(String(err)));
13760
+ this.handleUnexpectedDrop(sock, generation, error2 instanceof Error ? error2 : new SubcProviderError(String(error2)));
13051
13761
  return;
13052
13762
  }
13053
13763
  } finally {
@@ -13059,28 +13769,58 @@ class SubcProvider {
13059
13769
  }
13060
13770
  }
13061
13771
  async dispatch(frame, sock, generation) {
13772
+ let handle = null;
13773
+ if (frame.header.channel !== 0) {
13774
+ handle = this.liveRoutes.get(frame.header.channel) ?? null;
13775
+ if (!handle || handle.epoch !== frame.header.epoch) {
13776
+ this.ingressEpochDropCount += 1;
13777
+ return true;
13778
+ }
13779
+ }
13780
+ if (handle) {
13781
+ const pendingKey2 = routeKey(handle, frame.header.corr);
13782
+ const pending = this.pending.get(pendingKey2);
13783
+ if (pending) {
13784
+ if (frame.header.ty === FrameType.Push || frame.header.ty === FrameType.StreamData)
13785
+ return true;
13786
+ if (frame.header.ty === FrameType.Response || frame.header.ty === FrameType.StreamEnd) {
13787
+ this.pending.delete(pendingKey2);
13788
+ clearTimeout(pending.timer);
13789
+ pending.resolve(frame);
13790
+ return true;
13791
+ }
13792
+ if (frame.header.ty === FrameType.Error) {
13793
+ this.pending.delete(pendingKey2);
13794
+ clearTimeout(pending.timer);
13795
+ pending.reject(providerErrorFromFrame(frame));
13796
+ return true;
13797
+ }
13798
+ }
13799
+ }
13062
13800
  switch (frame.header.ty) {
13063
- case 7 /* Ping */:
13801
+ case FrameType.Ping:
13064
13802
  if (frame.header.channel === 0) {
13065
- await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, 8 /* Pong */, frame.header.flags, 0, frame.header.corr, new Uint8Array(0)));
13803
+ await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, FrameType.Pong, frame.header.flags, 0, 0, frame.header.corr, new Uint8Array(0)));
13066
13804
  }
13067
13805
  return true;
13068
- case 11 /* Goodbye */:
13069
- if (frame.header.channel === 0)
13806
+ case FrameType.Goodbye:
13807
+ if (!handle)
13070
13808
  return false;
13071
- this.abortChannel(generation, frame.header.channel);
13072
- await this.opts.onRouteGone?.(frame.header.channel);
13809
+ this.liveRoutes.delete(handle.channel);
13810
+ this.abortHandle(handle);
13811
+ await this.opts.onRouteGone?.(handle);
13073
13812
  return true;
13074
- case 6 /* Cancel */:
13075
- this.inflight.get(routeKey(generation, frame.header.channel, frame.header.corr))?.abort();
13813
+ case FrameType.Cancel:
13814
+ if (handle)
13815
+ this.inflight.get(routeKey(handle, frame.header.corr))?.abort();
13076
13816
  return true;
13077
- case 0 /* Request */:
13817
+ case FrameType.Request:
13078
13818
  if (frame.header.channel === 0) {
13079
13819
  await this.handleControlRequest(frame, sock, generation);
13080
- } else {
13081
- this.handleDataRequest(frame, sock, generation).catch((err) => {
13820
+ } else if (handle) {
13821
+ this.handleDataRequest(frame, handle, sock, generation).catch((error2) => {
13082
13822
  if (!this.closeStarted && this.sock === sock && this.generation === generation) {
13083
- console.warn("SubcProvider handler failed after its request was dispatched", err);
13823
+ console.warn("SubcProvider handler failed after its request was dispatched", error2);
13084
13824
  }
13085
13825
  });
13086
13826
  }
@@ -13089,19 +13829,28 @@ class SubcProvider {
13089
13829
  return true;
13090
13830
  }
13091
13831
  }
13092
- abortChannel(generation, channel) {
13093
- const prefix = `${generation}:${channel}:`;
13832
+ abortHandle(handle) {
13833
+ const prefix = `${handle.channel}:${handle.epoch}:`;
13094
13834
  for (const [key, controller] of this.inflight) {
13095
13835
  if (key.startsWith(prefix))
13096
13836
  controller.abort();
13097
13837
  }
13838
+ for (const [key, pending] of this.pending) {
13839
+ if (!key.startsWith(prefix))
13840
+ continue;
13841
+ this.pending.delete(key);
13842
+ clearTimeout(pending.timer);
13843
+ pending.reject(new StaleRouteHandleError(handle));
13844
+ }
13098
13845
  }
13099
- abortGeneration(generation) {
13100
- const prefix = `${generation}:`;
13101
- for (const [key, controller] of this.inflight) {
13102
- if (key.startsWith(prefix))
13103
- controller.abort();
13846
+ abortGeneration(_generation) {
13847
+ this.abortAllInflight();
13848
+ for (const [key, pending] of this.pending) {
13849
+ this.pending.delete(key);
13850
+ clearTimeout(pending.timer);
13851
+ pending.reject(new SubcProviderError("provider connection dropped", "connection_dropped"));
13104
13852
  }
13853
+ this.liveRoutes.clear();
13105
13854
  }
13106
13855
  abortAllInflight() {
13107
13856
  for (const controller of this.inflight.values())
@@ -13109,56 +13858,136 @@ class SubcProvider {
13109
13858
  }
13110
13859
  async handleControlRequest(frame, sock, generation) {
13111
13860
  const request = parseJson(frame.body);
13861
+ if (request.op === HEALTH_CHECK_OP) {
13862
+ this.handleHealthRequest(frame, sock, generation).catch((error2) => {
13863
+ if (!this.closeStarted && this.sock === sock && this.generation === generation) {
13864
+ console.warn("SubcProvider health handler failed after its request was dispatched", error2);
13865
+ }
13866
+ });
13867
+ return;
13868
+ }
13112
13869
  if (request.op !== "route.bind") {
13113
13870
  throw new SubcProviderError(`unsupported module control request ${request.op ?? "<missing op>"}`);
13114
13871
  }
13872
+ const boundChannel = numberField(request.route_channel, "route_channel");
13873
+ const boundEpoch = numberField(request.epoch, "epoch");
13874
+ const stale = this.liveRoutes.get(boundChannel);
13875
+ if (stale) {
13876
+ if (boundEpoch <= stale.epoch) {
13877
+ await this.sendError(frame, "route_rejected", `route.bind epoch ${boundEpoch} does not supersede installed epoch ${stale.epoch} on channel ${boundChannel}`, controlFlags(), sock, generation);
13878
+ return;
13879
+ }
13880
+ this.liveRoutes.delete(stale.channel);
13881
+ this.abortHandle(stale);
13882
+ await this.opts.onRouteGone?.(stale);
13883
+ }
13884
+ const tentative = createRouteHandle(boundChannel, boundEpoch, this.connectionToken);
13115
13885
  const bindRequest = {
13116
- route_channel: numberField(request.route_channel, "route_channel"),
13886
+ handle: tentative,
13117
13887
  target: request.target,
13118
13888
  identity: request.identity,
13119
- principal: request.principal
13889
+ principal: request.principal,
13890
+ consumer_capabilities: request.consumer_capabilities
13120
13891
  };
13121
- const decision = await this.opts.onBind?.(bindRequest);
13892
+ let decision;
13893
+ try {
13894
+ decision = await this.opts.onBind?.(bindRequest);
13895
+ } catch (error2) {
13896
+ try {
13897
+ await this.sendError(frame, "route_rejected", error2 instanceof Error ? error2.message : String(error2), controlFlags(), sock, generation);
13898
+ } finally {
13899
+ await this.opts.onRouteGone?.(tentative);
13900
+ }
13901
+ return;
13902
+ }
13122
13903
  const rejection = bindRejection(decision);
13123
13904
  if (rejection) {
13124
- await this.sendError(frame, rejection.code, rejection.message, controlFlags(), sock, generation);
13905
+ try {
13906
+ await this.sendError(frame, rejection.code, rejection.message, controlFlags(), sock, generation);
13907
+ } finally {
13908
+ await this.opts.onRouteGone?.(tentative);
13909
+ }
13910
+ return;
13911
+ }
13912
+ try {
13913
+ await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, FrameType.Response, controlFlags(), 0, 0, frame.header.corr, encodeJson({ op: "route.bind" })));
13914
+ } catch (error2) {
13915
+ await this.opts.onRouteGone?.(tentative);
13916
+ throw error2;
13917
+ }
13918
+ if (this.sock !== sock || this.generation !== generation || this.closeStarted || this.closedErr) {
13919
+ await this.opts.onRouteGone?.(tentative);
13125
13920
  return;
13126
13921
  }
13127
- await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, 1 /* Response */, controlFlags(), 0, frame.header.corr, encodeJson({ op: "route.bind" })));
13922
+ this.liveRoutes.set(tentative.channel, tentative);
13923
+ await this.opts.onBound?.(tentative);
13128
13924
  }
13129
- async handleDataRequest(frame, sock, generation) {
13130
- const { channel, corr, ver } = frame.header;
13131
- const key = routeKey(generation, channel, corr);
13925
+ async handleDataRequest(frame, handle, sock, generation) {
13926
+ const { corr, ver } = frame.header;
13927
+ const key = routeKey(handle, corr);
13132
13928
  const controller = new AbortController;
13133
13929
  this.inflight.set(key, controller);
13134
- const dataFlags = buildFlags(false, 1 /* Interactive */, false);
13135
- const ctx = {
13930
+ const context = {
13931
+ handle,
13136
13932
  signal: controller.signal,
13137
13933
  currentEpoch: () => this.connectionEpoch,
13138
- emit: async (eventBody) => {
13934
+ emit: async (eventBody, options = {}) => {
13935
+ this.assertLiveHandle(handle);
13139
13936
  if (controller.signal.aborted)
13140
13937
  return;
13141
- await this.sendOn(sock, generation, buildFrameWithVersion(ver, 3 /* StreamData */, dataFlags, channel, corr, eventBody));
13938
+ await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.StreamData, buildFlags(false, options.priority ?? Priority.Interactive, false, options.admissionClass ?? AdmissionClass.Normal), handle.channel, handle.epoch, corr, eventBody));
13142
13939
  }
13143
13940
  };
13941
+ const releasePermit = await (this.requestGate ?? new AsyncPermitPool(DEFAULT_PROVIDER_HANDLER_CAPACITY)).acquire();
13942
+ const dataFlags = buildFlags(false, Priority.Interactive, false);
13144
13943
  try {
13145
- const body = await this.opts.handler(channel, frame.body, ctx);
13944
+ const body = await this.opts.handler(handle, frame.body, context);
13945
+ if (controller.signal.aborted)
13946
+ return;
13947
+ this.assertLiveHandle(handle);
13146
13948
  if (body === undefined) {
13147
- await this.sendOn(sock, generation, buildFrameWithVersion(ver, 4 /* StreamEnd */, dataFlags, channel, corr, new Uint8Array(0)));
13949
+ await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.StreamEnd, dataFlags, handle.channel, handle.epoch, corr, new Uint8Array(0)));
13148
13950
  } else if (body instanceof Uint8Array) {
13149
- await this.sendOn(sock, generation, buildFrameWithVersion(ver, 1 /* Response */, dataFlags, channel, corr, body));
13951
+ await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.Response, dataFlags, handle.channel, handle.epoch, corr, body));
13150
13952
  } else {
13151
13953
  throw new SubcProviderError("provider handler must return a Uint8Array or void", "invalid_handler_response");
13152
13954
  }
13153
- } catch (err) {
13154
- await this.sendError(frame, err instanceof SubcProviderError && err.code ? err.code : "handler_error", err instanceof Error ? err.message : String(err), dataFlags, sock, generation);
13955
+ } catch (error2) {
13956
+ if (error2 instanceof StaleRouteHandleError || controller.signal.aborted)
13957
+ return;
13958
+ await this.sendError(frame, error2 instanceof SubcProviderError && error2.code ? error2.code : "handler_error", error2 instanceof Error ? error2.message : String(error2), dataFlags, sock, generation);
13959
+ } finally {
13960
+ releasePermit();
13961
+ if (this.inflight.get(key) === controller)
13962
+ this.inflight.delete(key);
13963
+ }
13964
+ }
13965
+ async handleHealthRequest(frame, sock, generation) {
13966
+ const { corr, ver } = frame.header;
13967
+ const key = `control:${generation}:${corr}`;
13968
+ const controller = new AbortController;
13969
+ this.inflight.set(key, controller);
13970
+ const releasePermit = await (this.requestGate ?? new AsyncPermitPool(DEFAULT_PROVIDER_HANDLER_CAPACITY)).acquire();
13971
+ try {
13972
+ if (controller.signal.aborted)
13973
+ return;
13974
+ const report = await this.opts.health();
13975
+ await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.Response, controlFlags(), 0, 0, corr, encodeJson({
13976
+ op: HEALTH_CHECK_OP,
13977
+ status: report.status,
13978
+ ...report.detail === undefined ? {} : { detail: report.detail },
13979
+ ...report.metrics === undefined ? {} : { metrics: report.metrics }
13980
+ })));
13981
+ } catch (error2) {
13982
+ await this.sendError(frame, error2 instanceof SubcProviderError && error2.code ? error2.code : "health_error", error2 instanceof Error ? error2.message : String(error2), controlFlags(), sock, generation);
13155
13983
  } finally {
13984
+ releasePermit();
13156
13985
  if (this.inflight.get(key) === controller)
13157
13986
  this.inflight.delete(key);
13158
13987
  }
13159
13988
  }
13160
13989
  async sendError(frame, code, message, flags, sock, generation) {
13161
- await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, 5 /* Error */, flags, frame.header.channel, frame.header.corr, encodeJson({ code, message })));
13990
+ await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, FrameType.Error, flags, frame.header.channel, frame.header.epoch, frame.header.corr, encodeJson({ code, message })));
13162
13991
  }
13163
13992
  async sendOn(sock, generation, frame) {
13164
13993
  if (this.sock !== sock || this.generation !== generation || this.closeStarted || this.closedErr)
@@ -13166,42 +13995,79 @@ class SubcProvider {
13166
13995
  await sendFrame(sock, frame);
13167
13996
  }
13168
13997
  handleUnexpectedDrop(sock, generation, cause) {
13998
+ if (this.closeStarted || this.sock !== sock || this.generation !== generation)
13999
+ return;
13169
14000
  this.cancelRestoredDebounce();
13170
14001
  this.abortGeneration(generation);
13171
14002
  this.generation += 1;
13172
14003
  sock.close();
13173
- this.enqueueConnectionState({ state: "down", cause });
13174
- this.scheduleReconnectAfterDrop(cause);
14004
+ this.scheduleReconnectAfterDrop(cause, this.generation, sock);
13175
14005
  }
13176
- scheduleReconnectAfterDrop(trigger) {
13177
- if (this.closeStarted || this.reconnecting)
14006
+ scheduleReconnectAfterDrop(cause, generation, droppedSocket) {
14007
+ if (this.closeStarted)
13178
14008
  return;
13179
- const promise = this.reconnectWithRetry(trigger).catch((err) => {
13180
- if (!this.closeStarted)
14009
+ const previous = this.reconnecting;
14010
+ if (previous) {
14011
+ if (!this.shouldSupersedeReconnect(previous, generation, droppedSocket))
14012
+ return;
14013
+ previous.superseded = true;
14014
+ previous.socket?.close();
14015
+ }
14016
+ const cycle = {
14017
+ generation,
14018
+ socket: null,
14019
+ socketDied: false,
14020
+ superseded: false
14021
+ };
14022
+ this.reconnecting = cycle;
14023
+ this.enqueueConnectionState({ state: "down", cause });
14024
+ this.reconnectWithRetry(cycle).catch((err) => {
14025
+ if (this.isCurrentReconnect(cycle) && !this.closeStarted) {
13181
14026
  this.failFatal(err instanceof Error ? err : new SubcProviderError(String(err)));
14027
+ }
13182
14028
  }).finally(() => {
13183
- if (this.reconnecting === promise)
14029
+ if (this.isCurrentReconnect(cycle))
13184
14030
  this.reconnecting = null;
13185
14031
  });
13186
- this.reconnecting = promise;
13187
14032
  }
13188
- async reconnectWithRetry(_trigger) {
14033
+ async reconnectWithRetry(cycle) {
13189
14034
  let attempt = 0;
13190
14035
  let delay = this.opts.reconnectBackoff.baseMs;
13191
14036
  for (;; ) {
14037
+ if (!this.isCurrentReconnect(cycle))
14038
+ return;
13192
14039
  if (this.closeStarted)
13193
14040
  throw new SubcProviderError("provider closed");
14041
+ cycle.socket = null;
14042
+ cycle.socketDied = false;
13194
14043
  attempt += 1;
13195
- this.enqueueConnectionState({ state: "reconnecting", attempt });
14044
+ this.enqueueReconnectState(cycle, attempt);
13196
14045
  try {
13197
- const opened = await SubcProvider.openConnection(this.opts);
13198
- if (this.closeStarted) {
14046
+ const opened = await SubcProvider.openConnection(this.opts, (sock) => {
14047
+ if (!this.isCurrentReconnect(cycle)) {
14048
+ sock.close();
14049
+ return;
14050
+ }
14051
+ cycle.socket = sock;
14052
+ });
14053
+ if (!this.isCurrentReconnect(cycle) || this.closeStarted) {
13199
14054
  opened.sock.close();
13200
- throw new SubcProviderError("provider closed");
14055
+ if (this.closeStarted)
14056
+ throw new SubcProviderError("provider closed");
14057
+ return;
13201
14058
  }
13202
- this.replaceConnection(opened);
14059
+ const epoch = this.replaceConnection(opened, cycle.generation);
14060
+ this.reconnecting = null;
14061
+ if (this.reconnecting !== null) {
14062
+ throw new SubcProviderError("reconnect state must clear before restored", "reconnect_state");
14063
+ }
14064
+ this.scheduleRestored(cycle.generation, epoch);
13203
14065
  return;
13204
14066
  } catch (err) {
14067
+ if (!this.isCurrentReconnect(cycle))
14068
+ return;
14069
+ if (cycle.socket)
14070
+ cycle.socketDied = true;
13205
14071
  if (this.closeStarted)
13206
14072
  throw err;
13207
14073
  if (!isProviderReconnectTransient(err))
@@ -13211,16 +14077,29 @@ class SubcProvider {
13211
14077
  }
13212
14078
  }
13213
14079
  }
13214
- replaceConnection(opened) {
14080
+ isCurrentReconnect(cycle) {
14081
+ return !cycle.superseded && this.reconnecting === cycle && this.generation === cycle.generation;
14082
+ }
14083
+ shouldSupersedeReconnect(cycle, generation, droppedSocket) {
14084
+ return cycle.generation < generation || cycle.socketDied || cycle.socket === droppedSocket;
14085
+ }
14086
+ enqueueReconnectState(cycle, attempt) {
14087
+ if (!this.isCurrentReconnect(cycle))
14088
+ return;
14089
+ this.enqueueConnectionState({ state: "reconnecting", attempt }, cycle.generation, cycle);
14090
+ }
14091
+ replaceConnection(opened, generation) {
13215
14092
  this.sock.close();
13216
14093
  this.sock = opened.sock;
13217
14094
  this.currentConn = opened.conn;
13218
14095
  this.storage = opened.ack.storage;
13219
14096
  this.closedErr = null;
13220
14097
  this.connectionEpoch += 1;
13221
- const generation = this.generation;
14098
+ this.connectionToken = newConnectionToken();
14099
+ this.liveRoutes.clear();
14100
+ this.nextCorr = 1n;
13222
14101
  this.readLoop(opened.sock, generation);
13223
- this.scheduleRestored(generation, this.connectionEpoch);
14102
+ return this.connectionEpoch;
13224
14103
  }
13225
14104
  scheduleRestored(generation, epoch) {
13226
14105
  if (!this.opts.onConnectionState)
@@ -13228,7 +14107,7 @@ class SubcProvider {
13228
14107
  const token = ++this.restoredDebounceToken;
13229
14108
  this.opts.sleep(this.opts.restoredDebounceMs).then(() => {
13230
14109
  if (token === this.restoredDebounceToken && !this.closeStarted && this.sock && this.generation === generation && this.connectionEpoch === epoch) {
13231
- this.enqueueConnectionState({ state: "restored", epoch });
14110
+ this.enqueueConnectionState({ state: "restored", epoch }, generation);
13232
14111
  }
13233
14112
  }).catch((err) => {
13234
14113
  if (token === this.restoredDebounceToken && !this.closeStarted) {
@@ -13239,10 +14118,10 @@ class SubcProvider {
13239
14118
  cancelRestoredDebounce() {
13240
14119
  this.restoredDebounceToken += 1;
13241
14120
  }
13242
- enqueueConnectionState(event) {
14121
+ enqueueConnectionState(event, generation, reconnect) {
13243
14122
  if (!this.opts.onConnectionState)
13244
14123
  return;
13245
- this.stateQueue.push(event);
14124
+ this.stateQueue.push({ event, generation, reconnect });
13246
14125
  if (!this.drainingStateQueue)
13247
14126
  this.drainConnectionStateQueue();
13248
14127
  }
@@ -13252,7 +14131,12 @@ class SubcProvider {
13252
14131
  this.drainingStateQueue = true;
13253
14132
  try {
13254
14133
  while (this.stateQueue.length > 0) {
13255
- const event = this.stateQueue[0];
14134
+ const queued = this.stateQueue[0];
14135
+ if (this.closeStarted || queued.generation !== undefined && queued.generation !== this.generation || queued.reconnect?.superseded) {
14136
+ this.stateQueue.shift();
14137
+ continue;
14138
+ }
14139
+ const { event } = queued;
13256
14140
  try {
13257
14141
  await this.opts.onConnectionState?.(event);
13258
14142
  this.stateQueue.shift();
@@ -13272,6 +14156,24 @@ class SubcProvider {
13272
14156
  this.drainConnectionStateQueue();
13273
14157
  }
13274
14158
  }
14159
+ isLiveHandle(handle) {
14160
+ return belongsToConnection(handle, this.connectionToken) && this.liveRoutes.get(handle.channel) === handle;
14161
+ }
14162
+ assertLiveHandle(handle) {
14163
+ if (!this.isLiveHandle(handle))
14164
+ throw new StaleRouteHandleError(handle);
14165
+ }
14166
+ allocateCorr() {
14167
+ const maximum = 0xffffffffffffffffn;
14168
+ if (this.nextCorr > maximum) {
14169
+ const error2 = new SubcProviderError("channel-0 correlation id allocator exhausted", "corr_exhausted");
14170
+ this.handleUnexpectedDrop(this.sock, this.generation, error2);
14171
+ throw error2;
14172
+ }
14173
+ const corr = this.nextCorr;
14174
+ this.nextCorr += 1n;
14175
+ return corr;
14176
+ }
13275
14177
  failFatal(err) {
13276
14178
  if (!this.closedErr)
13277
14179
  this.closedErr = err;
@@ -13285,8 +14187,16 @@ class SubcProvider {
13285
14187
  this.resolveClosed();
13286
14188
  }
13287
14189
  }
13288
- function routeKey(generation, channel, corr) {
13289
- return `${generation}:${channel}:${corr}`;
14190
+ function routeKey(handle, corr) {
14191
+ return `${handle.channel}:${handle.epoch}:${corr}`;
14192
+ }
14193
+ function providerErrorFromFrame(frame) {
14194
+ try {
14195
+ const body = parseJson(frame.body);
14196
+ return new SubcProviderError(body.message ?? "subc error", body.code);
14197
+ } catch {
14198
+ return new SubcProviderError(Buffer2.from(frame.body).toString("utf8") || "subc error");
14199
+ }
13290
14200
  }
13291
14201
  function launchNonce(opts) {
13292
14202
  const nonce = opts.launchNonce ?? process.env[SUBC_LAUNCH_NONCE_ENV2];
@@ -13298,9 +14208,11 @@ function normalizeProviderConnectOptions(opts) {
13298
14208
  connectionFile: opts.connectionFile,
13299
14209
  manifest: opts.manifest,
13300
14210
  handler: opts.handler,
14211
+ health: opts.health ?? (() => ({ status: "ok" })),
13301
14212
  handshakeTimeoutMs: opts.handshakeTimeoutMs,
13302
14213
  controlOps: opts.controlOps,
13303
14214
  onBind: opts.onBind,
14215
+ onBound: opts.onBound,
13304
14216
  onRouteGone: opts.onRouteGone,
13305
14217
  reconnectBackoff: opts.reconnectBackoff ?? DEFAULT_RECONNECT_BACKOFF,
13306
14218
  sleep: opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms))),
@@ -13309,12 +14221,19 @@ function normalizeProviderConnectOptions(opts) {
13309
14221
  launchNonce: opts.launchNonce
13310
14222
  };
13311
14223
  }
14224
+ function normalizedControlOps(controlOps) {
14225
+ if (controlOps === null)
14226
+ return null;
14227
+ const merged = new Set(controlOps ?? []);
14228
+ merged.add(HEALTH_CHECK_OP);
14229
+ return [...merged];
14230
+ }
13312
14231
  function buildHelloFrame(opts) {
13313
14232
  const nonce = launchNonce(opts);
13314
- return buildFrame(9 /* Hello */, controlFlags(), 0, HELLO_CORR, encodeJson({
14233
+ return buildFrame(FrameType.Hello, controlFlags(), 0, 0, HELLO_CORR, encodeJson({
13315
14234
  manifest: normalizeManifest(opts.manifest),
13316
14235
  protocol_ver: PROTOCOL_VERSION,
13317
- control_ops: opts.controlOps === undefined ? null : opts.controlOps,
14236
+ control_ops: normalizedControlOps(opts.controlOps),
13318
14237
  ...nonce ? { launch_nonce: nonce } : {}
13319
14238
  }));
13320
14239
  }
@@ -13325,7 +14244,9 @@ function isProviderReconnectTransient(err) {
13325
14244
  return true;
13326
14245
  if (err instanceof SocketWriteNotQueuedError || err instanceof SocketWriteQueuedError)
13327
14246
  return true;
13328
- if (err instanceof ConnectionFileError || err instanceof AuthError)
14247
+ if (err instanceof AuthError)
14248
+ return true;
14249
+ if (err instanceof ConnectionFileError)
13329
14250
  return false;
13330
14251
  const code = errorCode2(err);
13331
14252
  return code === "ECONNREFUSED" || code === "ECONNRESET" || code === "EPIPE" || code === "ETIMEDOUT" || code === "ENOENT";
@@ -13342,20 +14263,23 @@ async function pauseBeforeStateRetry() {
13342
14263
  await new Promise((resolve7) => setTimeout(resolve7, 0));
13343
14264
  }
13344
14265
  function controlFlags() {
13345
- return buildFlags(false, 0 /* Passive */, false);
14266
+ return buildFlags(false, Priority.Passive, false);
13346
14267
  }
13347
14268
  async function sendFrame(sock, frame) {
13348
14269
  await sock.write(encodeFrame(frame), Date.now() + WRITE_TIMEOUT_MS);
13349
14270
  }
13350
14271
  async function expectHelloAck(sock, deadline) {
13351
- const header = decodeHeader(await sock.readExact(HEADER_LEN, deadline));
13352
- const body = header.len === 0 ? new Uint8Array(0) : await sock.readExact(header.len, deadline);
13353
- const frame = { header, body };
13354
- switch (header.ty) {
13355
- case 10 /* HelloAck */:
13356
- return parseJson(body);
13357
- case 5 /* Error */: {
13358
- const error2 = parseJson(body);
14272
+ const frame = await sock.readFrame(deadline, deadline);
14273
+ switch (frame.header.ty) {
14274
+ case FrameType.HelloAck: {
14275
+ const ack = parseJson(frame.body);
14276
+ if (ack.negotiated_ver !== PROTOCOL_VERSION) {
14277
+ throw new SubcProviderError(`subc negotiated protocol ${ack.negotiated_ver}; expected exactly ${PROTOCOL_VERSION}`, "unsupported_version");
14278
+ }
14279
+ return ack;
14280
+ }
14281
+ case FrameType.Error: {
14282
+ const error2 = parseJson(frame.body);
13359
14283
  throw new SubcProviderError(`subc rejected HELLO: ${error2.code ?? "unknown"} — ${error2.message ?? "subc error"}`, error2.code);
13360
14284
  }
13361
14285
  default:
@@ -13420,6 +14344,7 @@ function normalizeProviderRole(role) {
13420
14344
  role: "tool_provider",
13421
14345
  tools: role.tools.map((tool) => ({
13422
14346
  name: tool.name,
14347
+ ...tool.description === undefined ? {} : { description: tool.description },
13423
14348
  execution_mode: tool.execution_mode,
13424
14349
  schema: tool.schema
13425
14350
  })),
@@ -13509,15 +14434,17 @@ class BgSubscription {
13509
14434
  identity;
13510
14435
  acquireClient;
13511
14436
  dropClient;
14437
+ consumerIdentity;
13512
14438
  onNudge;
13513
14439
  sleep;
13514
14440
  stopped = false;
13515
14441
  current = null;
13516
14442
  loop;
13517
- constructor(identity, acquireClient, dropClient, onNudge, sleep2) {
14443
+ constructor(identity, acquireClient, dropClient, consumerIdentity, onNudge, sleep2) {
13518
14444
  this.identity = identity;
13519
14445
  this.acquireClient = acquireClient;
13520
14446
  this.dropClient = dropClient;
14447
+ this.consumerIdentity = consumerIdentity;
13521
14448
  this.onNudge = onNudge;
13522
14449
  this.sleep = sleep2;
13523
14450
  this.loop = this.run();
@@ -13546,9 +14473,9 @@ class BgSubscription {
13546
14473
  }
13547
14474
  if (this.stopped)
13548
14475
  return;
13549
- let channel;
14476
+ let route;
13550
14477
  try {
13551
- channel = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, this.identity);
14478
+ route = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, this.identity, { consumerIdentity: this.consumerIdentity });
13552
14479
  } catch (err) {
13553
14480
  if (isConsumerReconnectTransient(err))
13554
14481
  this.dropClient(client);
@@ -13556,12 +14483,12 @@ class BgSubscription {
13556
14483
  continue;
13557
14484
  }
13558
14485
  if (this.stopped) {
13559
- safeCloseRoute(client, channel);
14486
+ safeCloseRoute(client, route);
13560
14487
  return;
13561
14488
  }
13562
14489
  const subscribedAt = Date.now();
13563
14490
  try {
13564
- const sub = client.subscribe(channel, { op: "bg_events" }, () => {
14491
+ const sub = client.subscribe(route, { op: "bg_events" }, () => {
13565
14492
  if (!this.stopped)
13566
14493
  this.onNudge();
13567
14494
  });
@@ -13581,7 +14508,7 @@ class BgSubscription {
13581
14508
  attempt = 0;
13582
14509
  } finally {
13583
14510
  this.current = null;
13584
- safeCloseRoute(client, channel);
14511
+ safeCloseRoute(client, route);
13585
14512
  }
13586
14513
  await this.backoff(attempt++);
13587
14514
  }
@@ -13594,9 +14521,12 @@ class BgSubscription {
13594
14521
  function isRecord(value) {
13595
14522
  return typeof value === "object" && value !== null && !Array.isArray(value);
13596
14523
  }
13597
- function safeCloseRoute(client, channel) {
14524
+ function isRouteProvenAbsentError(err) {
14525
+ return err instanceof SubcError && err.code === "unknown_channel" || err instanceof StaleRouteHandleError;
14526
+ }
14527
+ function safeCloseRoute(client, route) {
13598
14528
  try {
13599
- client.closeRouteChannel(channel).catch(() => {
14529
+ client.closeRouteChannel(route).catch(() => {
13600
14530
  return;
13601
14531
  });
13602
14532
  } catch {}
@@ -13673,7 +14603,7 @@ class SubcTransport {
13673
14603
  if (!options)
13674
14604
  return {};
13675
14605
  const preview = options.preview;
13676
- const timeoutMs = options.timeoutMs;
14606
+ const timeoutMs = options.transportTimeoutMs ?? options.timeoutMs;
13677
14607
  const onProgress = options.onProgress;
13678
14608
  return { preview, timeoutMs, onProgress };
13679
14609
  }
@@ -13683,6 +14613,7 @@ class SubcTransportPool {
13683
14613
  harness;
13684
14614
  connectionFile;
13685
14615
  handshakeTimeoutMs;
14616
+ consumerIdentity;
13686
14617
  connectFn;
13687
14618
  onBgEventsNudge;
13688
14619
  bgBackoffSleep;
@@ -13696,6 +14627,7 @@ class SubcTransportPool {
13696
14627
  this.connectionFile = options.connectionFile;
13697
14628
  this.harness = options.harness;
13698
14629
  this.handshakeTimeoutMs = options.handshakeTimeoutMs;
14630
+ this.consumerIdentity = options.consumerIdentity;
13699
14631
  this.connectFn = options.connect ?? ((opts) => SubcClient.connect(opts));
13700
14632
  this.onBgEventsNudge = options.onBgEventsNudge;
13701
14633
  this.bgBackoffSleep = options.bgBackoffSleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
@@ -13718,6 +14650,11 @@ class SubcTransportPool {
13718
14650
  return null;
13719
14651
  return this.transports.get(key) ?? null;
13720
14652
  }
14653
+ activeBridges() {
14654
+ if (!this.client || this.shuttingDown)
14655
+ return [];
14656
+ return [...this.transports.values()];
14657
+ }
13721
14658
  async toolCall(projectRoot, runtime, name, rawArgs = {}, options) {
13722
14659
  return this.getBridge(projectRoot).toolCall(runtime.sessionID, name, rawArgs, options);
13723
14660
  }
@@ -13743,45 +14680,66 @@ class SubcTransportPool {
13743
14680
  record.inflight += 1;
13744
14681
  try {
13745
14682
  const client = await this.ensureClient();
13746
- if (!this.isCurrentSession(key, record)) {
13747
- throw new RouteTornDownError("subc session closed");
13748
- }
13749
- let channel;
13750
- let entry;
13751
- try {
13752
- ({ channel, entry } = await this.routeChannel(client, identity, record));
14683
+ const openRoute = async () => {
13753
14684
  if (!this.isCurrentSession(key, record)) {
13754
14685
  throw new RouteTornDownError("subc session closed");
13755
14686
  }
13756
- } catch (err) {
13757
- if (err instanceof RouteTornDownError)
14687
+ try {
14688
+ const opened = await this.routeHandle(client, identity, record);
14689
+ if (!this.isCurrentSession(key, record)) {
14690
+ throw new RouteTornDownError("subc session closed");
14691
+ }
14692
+ return opened;
14693
+ } catch (err) {
14694
+ if (err instanceof RouteTornDownError)
14695
+ throw err;
14696
+ if (isConsumerReconnectTransient(err) && this.isCurrentSession(key, record) && this.client === client) {
14697
+ this.dropClient(client);
14698
+ }
13758
14699
  throw err;
13759
- if (isConsumerReconnectTransient(err) && this.isCurrentSession(key, record) && this.client === client) {
13760
- this.dropClient(client);
13761
- }
13762
- throw err;
13763
- }
13764
- try {
13765
- const reply = await client.request(channel, body, { timeoutMs, onProgress });
13766
- if (this.isCurrentSession(key, record) && this.client === client) {
13767
- this.transportFailures = 0;
13768
14700
  }
13769
- this.ensureBgSubscription(identity, record);
13770
- return reply;
13771
- } catch (err) {
13772
- if (record.routeEntry === entry) {
13773
- entry.closed = true;
14701
+ };
14702
+ const clearRouteEntry = (entry2) => {
14703
+ if (record.routeEntry === entry2) {
14704
+ entry2.closed = true;
13774
14705
  record.routeEntry = null;
13775
14706
  }
14707
+ };
14708
+ const handleRequestFailure = (err, entry2) => {
14709
+ clearRouteEntry(entry2);
13776
14710
  if (this.isCurrentSession(key, record) && this.client === client) {
13777
14711
  if (isConsumerReconnectTransient(err)) {
13778
14712
  this.transportFailures = 0;
13779
14713
  this.dropClient(client);
13780
- } else if (++this.transportFailures >= MAX_CONSECUTIVE_TRANSPORT_FAILURES) {
14714
+ } else if (!isRouteProvenAbsentError(err) && ++this.transportFailures >= MAX_CONSECUTIVE_TRANSPORT_FAILURES) {
13781
14715
  this.transportFailures = 0;
13782
14716
  this.dropClient(client);
13783
14717
  }
13784
14718
  }
14719
+ };
14720
+ const requestOnRoute = async (route2) => {
14721
+ const reply = await client.request(route2, body, { timeoutMs, onProgress });
14722
+ if (this.isCurrentSession(key, record) && this.client === client) {
14723
+ this.transportFailures = 0;
14724
+ }
14725
+ this.ensureBgSubscription(identity, record);
14726
+ return reply;
14727
+ };
14728
+ let { route, entry } = await openRoute();
14729
+ try {
14730
+ return await requestOnRoute(route);
14731
+ } catch (err) {
14732
+ if (isRouteProvenAbsentError(err) && this.isCurrentSession(key, record) && this.client === client) {
14733
+ clearRouteEntry(entry);
14734
+ ({ route, entry } = await openRoute());
14735
+ try {
14736
+ return await requestOnRoute(route);
14737
+ } catch (retryErr) {
14738
+ handleRequestFailure(retryErr, entry);
14739
+ throw retryErr;
14740
+ }
14741
+ }
14742
+ handleRequestFailure(err, entry);
13785
14743
  throw err;
13786
14744
  }
13787
14745
  } finally {
@@ -13817,24 +14775,26 @@ class SubcTransportPool {
13817
14775
  });
13818
14776
  return this.connecting;
13819
14777
  }
13820
- async routeChannel(client, identity, record) {
14778
+ async routeHandle(client, identity, record) {
13821
14779
  const key = identityKey(identity);
13822
14780
  const existing = record.routeEntry;
13823
- if (existing?.channel != null)
13824
- return { channel: existing.channel, entry: existing };
14781
+ if (existing?.handle != null)
14782
+ return { route: existing.handle, entry: existing };
13825
14783
  if (existing?.opening)
13826
- return { channel: await existing.opening, entry: existing };
13827
- const entry = { client, opening: null, channel: null, closed: false };
13828
- const opening = client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity).then((channel) => {
14784
+ return { route: await existing.opening, entry: existing };
14785
+ const entry = { client, opening: null, handle: null, closed: false };
14786
+ const opening = client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, {
14787
+ consumerIdentity: this.consumerIdentity
14788
+ }).then((route) => {
13829
14789
  if (!this.isCurrentSession(key, record) || record.routeEntry !== entry || entry.closed || this.client !== client) {
13830
- safeCloseRoute(client, channel);
14790
+ safeCloseRoute(client, route);
13831
14791
  if (record.routeEntry === entry)
13832
14792
  record.routeEntry = null;
13833
14793
  throw new RouteTornDownError("subc route opened after teardown");
13834
14794
  }
13835
- entry.channel = channel;
14795
+ entry.handle = route;
13836
14796
  entry.opening = null;
13837
- return channel;
14797
+ return route;
13838
14798
  }).catch((err) => {
13839
14799
  const current = this.isCurrentSession(key, record);
13840
14800
  if (record.routeEntry === entry) {
@@ -13848,7 +14808,7 @@ class SubcTransportPool {
13848
14808
  });
13849
14809
  entry.opening = opening;
13850
14810
  record.routeEntry = entry;
13851
- return { channel: await opening, entry };
14811
+ return { route: await opening, entry };
13852
14812
  }
13853
14813
  ensureBgSubscription(identity, record) {
13854
14814
  if (this.shuttingDown || !this.onBgEventsNudge)
@@ -13859,7 +14819,7 @@ class SubcTransportPool {
13859
14819
  if (record.bgSub)
13860
14820
  return;
13861
14821
  const onNudge = () => this.onBgEventsNudge?.(identity.project_root, identity.session);
13862
- const sub = new BgSubscription(identity, () => this.ensureClient(), (client) => this.dropClient(client), onNudge, this.bgBackoffSleep);
14822
+ const sub = new BgSubscription(identity, () => this.ensureClient(), (client) => this.dropClient(client), this.consumerIdentity, onNudge, this.bgBackoffSleep);
13863
14823
  record.bgSub = sub;
13864
14824
  }
13865
14825
  dropClient(client) {
@@ -13880,9 +14840,13 @@ class SubcTransportPool {
13880
14840
  }
13881
14841
  }
13882
14842
  setConfigureOverride(_key, _value) {}
14843
+ async reconfigure(_projectRoot, _overrides) {}
13883
14844
  async replaceBinary(path2) {
13884
14845
  return path2;
13885
14846
  }
14847
+ isShutdown() {
14848
+ return this.shuttingDown;
14849
+ }
13886
14850
  async shutdown() {
13887
14851
  this.shuttingDown = true;
13888
14852
  const subs = [];
@@ -13906,10 +14870,10 @@ class SubcTransportPool {
13906
14870
  this.transports.clear();
13907
14871
  await Promise.allSettled(subs.map((sub) => sub.stop()));
13908
14872
  await Promise.allSettled(entries.map(async (entry) => {
13909
- if (entry.channel == null)
14873
+ if (entry.handle == null)
13910
14874
  return;
13911
14875
  try {
13912
- await entry.client.closeRouteChannel(entry.channel);
14876
+ await entry.client.closeRouteChannel(entry.handle);
13913
14877
  } catch {}
13914
14878
  }));
13915
14879
  if (client) {
@@ -13938,9 +14902,9 @@ class SubcTransportPool {
13938
14902
  entry.closed = true;
13939
14903
  if (sub)
13940
14904
  await sub.stop();
13941
- if (entry?.channel != null) {
14905
+ if (entry?.handle != null) {
13942
14906
  try {
13943
- await entry.client.closeRouteChannel(entry.channel);
14907
+ await entry.client.closeRouteChannel(entry.handle);
13944
14908
  } catch {}
13945
14909
  }
13946
14910
  }
@@ -14017,18 +14981,25 @@ function collectStructuredExtras(response) {
14017
14981
  return stringifyData(extras);
14018
14982
  }
14019
14983
  // ../aft-bridge/dist/transport-factory.js
14020
- import { homedir as homedir10 } from "node:os";
14021
- import { isAbsolute as isAbsolute5, join as join9 } from "node:path";
14984
+ import { homedir as homedir11 } from "node:os";
14985
+ import { isAbsolute as isAbsolute5, join as join10 } from "node:path";
14022
14986
  function resolveConnectionFilePath(raw) {
14023
14987
  const trimmed = raw.trim();
14024
14988
  if (trimmed.startsWith("~")) {
14025
- return join9(homedir10(), trimmed.slice(1).replace(/^[/\\]/, ""));
14989
+ return join10(homedir11(), trimmed.slice(1).replace(/^[/\\]/, ""));
14026
14990
  }
14027
14991
  if (isAbsolute5(trimmed))
14028
14992
  return trimmed;
14029
- return join9(homedir10(), trimmed);
14993
+ return join10(homedir11(), trimmed);
14030
14994
  }
14031
14995
  async function createAftTransportPool(opts) {
14996
+ let binaryPath = opts.binaryPath;
14997
+ const createPool = () => createConcreteAftTransportPool({ ...opts, binaryPath });
14998
+ return new RevivableTransportPool(await createPool(), createPool, (path2) => {
14999
+ binaryPath = path2;
15000
+ });
15001
+ }
15002
+ async function createConcreteAftTransportPool(opts) {
14032
15003
  const raw = opts.subcConnectionFile?.trim();
14033
15004
  if (raw && raw.length > 0) {
14034
15005
  const connectionFile = resolveConnectionFilePath(raw);
@@ -14039,18 +15010,17 @@ async function createAftTransportPool(opts) {
14039
15010
  return new SubcTransportPool({
14040
15011
  connectionFile,
14041
15012
  harness: opts.harness,
15013
+ consumerIdentity: opts.subcConsumerIdentity,
14042
15014
  onBgEventsNudge: opts.onBgEventsNudge
14043
15015
  });
14044
15016
  }
14045
15017
  return new BridgePool(opts.binaryPath, opts.poolOptions, opts.configOverrides);
14046
15018
  }
14047
15019
  // src/logger.ts
14048
- import * as fs3 from "node:fs";
14049
- import * as os2 from "node:os";
14050
- import * as path2 from "node:path";
14051
15020
  var TAG = "[aft-pi]";
14052
15021
  var isTestEnv = process.env.BUN_TEST === "1" || false;
14053
- var logFile = path2.join(os2.tmpdir(), isTestEnv ? "aft-pi-test.log" : "aft-pi.log");
15022
+ var logFile = resolveAftLogPath(isTestEnv ? "aft-plugin-test.log" : "aft-plugin.log");
15023
+ var fileSink = new RotatingLogSink(logFile);
14054
15024
  var useStderr = process.env.AFT_LOG_STDERR === "1";
14055
15025
  var buffer = [];
14056
15026
  var flushTimer = null;
@@ -14065,7 +15035,7 @@ function flush() {
14065
15035
  if (useStderr) {
14066
15036
  process.stderr.write(data);
14067
15037
  } else {
14068
- fs3.appendFileSync(logFile, data);
15038
+ fileSink.append(data);
14069
15039
  }
14070
15040
  } catch {}
14071
15041
  }
@@ -14778,7 +15748,7 @@ import {
14778
15748
  // package.json
14779
15749
  var package_default = {
14780
15750
  name: "@cortexkit/aft-pi",
14781
- version: "0.45.1",
15751
+ version: "0.47.0",
14782
15752
  type: "module",
14783
15753
  description: "Pi coding agent extension for Agent File Tools (AFT) — tree-sitter and LSP-powered code analysis",
14784
15754
  main: "dist/index.js",
@@ -14801,19 +15771,19 @@ var package_default = {
14801
15771
  "test:unit": "bun test src/__tests__ --path-ignore-patterns 'src/__tests__/e2e/**'"
14802
15772
  },
14803
15773
  dependencies: {
14804
- "@cortexkit/aft-bridge": "0.45.1",
15774
+ "@cortexkit/aft-bridge": "0.47.0",
14805
15775
  "comment-json": "^5.0.0",
14806
15776
  diff: "^8.0.4",
14807
15777
  typebox: "1.1.38",
14808
15778
  zod: "^4.4.3"
14809
15779
  },
14810
15780
  optionalDependencies: {
14811
- "@cortexkit/aft-darwin-arm64": "0.45.1",
14812
- "@cortexkit/aft-darwin-x64": "0.45.1",
14813
- "@cortexkit/aft-linux-arm64": "0.45.1",
14814
- "@cortexkit/aft-linux-x64": "0.45.1",
14815
- "@cortexkit/aft-win32-arm64": "0.45.1",
14816
- "@cortexkit/aft-win32-x64": "0.45.1"
15781
+ "@cortexkit/aft-darwin-arm64": "0.47.0",
15782
+ "@cortexkit/aft-darwin-x64": "0.47.0",
15783
+ "@cortexkit/aft-linux-arm64": "0.47.0",
15784
+ "@cortexkit/aft-linux-x64": "0.47.0",
15785
+ "@cortexkit/aft-win32-arm64": "0.47.0",
15786
+ "@cortexkit/aft-win32-x64": "0.47.0"
14817
15787
  },
14818
15788
  devDependencies: {
14819
15789
  "@earendil-works/pi-coding-agent": "^0.80.2",
@@ -15044,9 +16014,15 @@ function formatStatusDialogMessage(status) {
15044
16014
  }
15045
16015
 
15046
16016
  // src/tools/_shared.ts
16017
+ import { existsSync as existsSync7 } from "node:fs";
15047
16018
  import { Type } from "typebox";
15048
- var optionalInt = (_min, _max) => Type.Optional(Type.Any({ description: "(integer)" }));
16019
+ var optionalInt = (min, max, description = "(integer)") => Type.Optional(Type.Union([Type.Integer({ minimum: min, maximum: max }), Type.String()], {
16020
+ description
16021
+ }));
15049
16022
  function bridgeFor(ctx, cwd) {
16023
+ if (!existsSync7(cwd)) {
16024
+ throw new Error(`project directory no longer exists: ${cwd} (stale restored session?)`);
16025
+ }
15050
16026
  return ctx.pool.getBridge(cwd);
15051
16027
  }
15052
16028
  function resolveSessionId(extCtx) {
@@ -15402,7 +16378,7 @@ function registerStatusCommand(pi, ctx) {
15402
16378
  }
15403
16379
 
15404
16380
  // src/config.ts
15405
- import { existsSync as existsSync7, readFileSync as readFileSync7, renameSync as renameSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync4 } from "node:fs";
16381
+ import { existsSync as existsSync8, readFileSync as readFileSync7, renameSync as renameSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync4 } from "node:fs";
15406
16382
  var import_comment_json = __toESM(require_src2(), 1);
15407
16383
 
15408
16384
  // ../../node_modules/.bun/zod@4.4.3/node_modules/zod/v4/classic/external.js
@@ -16169,10 +17145,10 @@ function mergeDefs(...defs) {
16169
17145
  function cloneDef(schema) {
16170
17146
  return mergeDefs(schema._zod.def);
16171
17147
  }
16172
- function getElementAtPath(obj, path3) {
16173
- if (!path3)
17148
+ function getElementAtPath(obj, path2) {
17149
+ if (!path2)
16174
17150
  return obj;
16175
- return path3.reduce((acc, key) => acc?.[key], obj);
17151
+ return path2.reduce((acc, key) => acc?.[key], obj);
16176
17152
  }
16177
17153
  function promiseAllObject(promisesObj) {
16178
17154
  const keys = Object.keys(promisesObj);
@@ -16580,11 +17556,11 @@ function explicitlyAborted(x, startIndex = 0) {
16580
17556
  }
16581
17557
  return false;
16582
17558
  }
16583
- function prefixIssues(path3, issues) {
17559
+ function prefixIssues(path2, issues) {
16584
17560
  return issues.map((iss) => {
16585
17561
  var _a2;
16586
17562
  (_a2 = iss).path ?? (_a2.path = []);
16587
- iss.path.unshift(path3);
17563
+ iss.path.unshift(path2);
16588
17564
  return iss;
16589
17565
  });
16590
17566
  }
@@ -16731,16 +17707,16 @@ function flattenError(error3, mapper = (issue2) => issue2.message) {
16731
17707
  }
16732
17708
  function formatError(error3, mapper = (issue2) => issue2.message) {
16733
17709
  const fieldErrors = { _errors: [] };
16734
- const processError = (error4, path3 = []) => {
17710
+ const processError = (error4, path2 = []) => {
16735
17711
  for (const issue2 of error4.issues) {
16736
17712
  if (issue2.code === "invalid_union" && issue2.errors.length) {
16737
- issue2.errors.map((issues) => processError({ issues }, [...path3, ...issue2.path]));
17713
+ issue2.errors.map((issues) => processError({ issues }, [...path2, ...issue2.path]));
16738
17714
  } else if (issue2.code === "invalid_key") {
16739
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
17715
+ processError({ issues: issue2.issues }, [...path2, ...issue2.path]);
16740
17716
  } else if (issue2.code === "invalid_element") {
16741
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
17717
+ processError({ issues: issue2.issues }, [...path2, ...issue2.path]);
16742
17718
  } else {
16743
- const fullpath = [...path3, ...issue2.path];
17719
+ const fullpath = [...path2, ...issue2.path];
16744
17720
  if (fullpath.length === 0) {
16745
17721
  fieldErrors._errors.push(mapper(issue2));
16746
17722
  } else {
@@ -16767,17 +17743,17 @@ function formatError(error3, mapper = (issue2) => issue2.message) {
16767
17743
  }
16768
17744
  function treeifyError(error3, mapper = (issue2) => issue2.message) {
16769
17745
  const result = { errors: [] };
16770
- const processError = (error4, path3 = []) => {
17746
+ const processError = (error4, path2 = []) => {
16771
17747
  var _a2, _b;
16772
17748
  for (const issue2 of error4.issues) {
16773
17749
  if (issue2.code === "invalid_union" && issue2.errors.length) {
16774
- issue2.errors.map((issues) => processError({ issues }, [...path3, ...issue2.path]));
17750
+ issue2.errors.map((issues) => processError({ issues }, [...path2, ...issue2.path]));
16775
17751
  } else if (issue2.code === "invalid_key") {
16776
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
17752
+ processError({ issues: issue2.issues }, [...path2, ...issue2.path]);
16777
17753
  } else if (issue2.code === "invalid_element") {
16778
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
17754
+ processError({ issues: issue2.issues }, [...path2, ...issue2.path]);
16779
17755
  } else {
16780
- const fullpath = [...path3, ...issue2.path];
17756
+ const fullpath = [...path2, ...issue2.path];
16781
17757
  if (fullpath.length === 0) {
16782
17758
  result.errors.push(mapper(issue2));
16783
17759
  continue;
@@ -16809,8 +17785,8 @@ function treeifyError(error3, mapper = (issue2) => issue2.message) {
16809
17785
  }
16810
17786
  function toDotPath(_path) {
16811
17787
  const segs = [];
16812
- const path3 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
16813
- for (const seg of path3) {
17788
+ const path2 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
17789
+ for (const seg of path2) {
16814
17790
  if (typeof seg === "number")
16815
17791
  segs.push(`[${seg}]`);
16816
17792
  else if (typeof seg === "symbol")
@@ -29269,13 +30245,13 @@ function resolveRef(ref, ctx) {
29269
30245
  if (!ref.startsWith("#")) {
29270
30246
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
29271
30247
  }
29272
- const path3 = ref.slice(1).split("/").filter(Boolean);
29273
- if (path3.length === 0) {
30248
+ const path2 = ref.slice(1).split("/").filter(Boolean);
30249
+ if (path2.length === 0) {
29274
30250
  return ctx.rootSchema;
29275
30251
  }
29276
30252
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
29277
- if (path3[0] === defsKey) {
29278
- const key = path3[1];
30253
+ if (path2[0] === defsKey) {
30254
+ const key = path2[1];
29279
30255
  if (!key || !ctx.defs[key]) {
29280
30256
  throw new Error(`Reference not found: ${ref}`);
29281
30257
  }
@@ -29761,6 +30737,7 @@ var SemanticConfigSchema = exports_external.object({
29761
30737
  base_url: exports_external.string().trim().min(1).optional(),
29762
30738
  api_key_env: exports_external.string().trim().min(1).optional(),
29763
30739
  timeout_ms: exports_external.number().int().positive().optional(),
30740
+ query_timeout_ms: exports_external.number().int().positive().optional(),
29764
30741
  max_batch_size: exports_external.number().int().positive().optional(),
29765
30742
  max_files: exports_external.number().int().positive().optional()
29766
30743
  });
@@ -29818,16 +30795,7 @@ var InspectConfigSchema = exports_external.object({
29818
30795
  tier2_soft_deadline_ms: exports_external.number().int().positive().optional(),
29819
30796
  max_drill_down_items: exports_external.number().int().positive().max(100).optional(),
29820
30797
  duplicates: exports_external.object({
29821
- lower_bound: exports_external.number().int().positive().optional(),
29822
- discard_cost: exports_external.number().int().min(0).optional(),
29823
- expected_mirrors: exports_external.array(exports_external.tuple([exports_external.string().trim().min(1), exports_external.string().trim().min(1)])).optional(),
29824
- anonymize: exports_external.object({
29825
- variables: exports_external.boolean().optional(),
29826
- fields: exports_external.boolean().optional(),
29827
- methods: exports_external.boolean().optional(),
29828
- types: exports_external.boolean().optional(),
29829
- literals: exports_external.boolean().optional()
29830
- }).optional()
30798
+ expected_mirrors: exports_external.array(exports_external.tuple([exports_external.string().trim().min(1), exports_external.string().trim().min(1)])).optional()
29831
30799
  }).optional()
29832
30800
  });
29833
30801
  var BackupConfigSchema = exports_external.object({
@@ -29835,7 +30803,7 @@ var BackupConfigSchema = exports_external.object({
29835
30803
  max_depth: exports_external.number().int().positive().optional(),
29836
30804
  max_file_size: exports_external.number().int().positive().optional()
29837
30805
  });
29838
- var AftConfigSchema = exports_external.object({
30806
+ var AftConfigSchema = exports_external.preprocess((value) => stripHarnessSpecificConfigKeys(value, OPENCODE_ONLY_KEYS), exports_external.object({
29839
30807
  $schema: exports_external.string().optional(),
29840
30808
  enabled: exports_external.boolean().optional(),
29841
30809
  format_on_edit: exports_external.boolean().optional(),
@@ -29860,7 +30828,7 @@ var AftConfigSchema = exports_external.object({
29860
30828
  semantic: SemanticConfigSchema.optional(),
29861
30829
  bridge: BridgeConfigSchema.optional(),
29862
30830
  subc: SubcConfigSchema.optional()
29863
- }).strict();
30831
+ }).strict());
29864
30832
  var CONFIG_MIGRATIONS = [
29865
30833
  { oldKey: "experimental_search_index", newPath: ["search_index"] },
29866
30834
  { oldKey: "experimental_semantic_search", newPath: ["semantic_search"] },
@@ -29885,9 +30853,9 @@ function extractCommentsForPreservation(content) {
29885
30853
  }
29886
30854
  return comments;
29887
30855
  }
29888
- function ensureRecordAtPath(root, path3) {
30856
+ function ensureRecordAtPath(root, path2) {
29889
30857
  let current = root;
29890
- for (const segment of path3) {
30858
+ for (const segment of path2) {
29891
30859
  const existing = current[segment];
29892
30860
  if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
29893
30861
  current[segment] = {};
@@ -29896,9 +30864,9 @@ function ensureRecordAtPath(root, path3) {
29896
30864
  }
29897
30865
  return current;
29898
30866
  }
29899
- function hasPath(root, path3) {
30867
+ function hasPath(root, path2) {
29900
30868
  let current = root;
29901
- for (const segment of path3) {
30869
+ for (const segment of path2) {
29902
30870
  if (!current || typeof current !== "object" || Array.isArray(current))
29903
30871
  return false;
29904
30872
  const record2 = current;
@@ -29908,9 +30876,9 @@ function hasPath(root, path3) {
29908
30876
  }
29909
30877
  return true;
29910
30878
  }
29911
- function setPath(root, path3, value) {
29912
- const parent = ensureRecordAtPath(root, path3.slice(0, -1));
29913
- parent[path3[path3.length - 1]] = value;
30879
+ function setPath(root, path2, value) {
30880
+ const parent = ensureRecordAtPath(root, path2.slice(0, -1));
30881
+ parent[path2[path2.length - 1]] = value;
29914
30882
  }
29915
30883
  function migrateRawConfig(rawConfig, configPath, logger) {
29916
30884
  const oldKeys = [];
@@ -29971,7 +30939,7 @@ function migrateExperimentalBashBlock(rawConfig, configPath, logger) {
29971
30939
  return movedKeys;
29972
30940
  }
29973
30941
  function migrateAftConfigFile2(configPath, logger = { log: log2, warn: warn2 }) {
29974
- if (!existsSync7(configPath)) {
30942
+ if (!existsSync8(configPath)) {
29975
30943
  return { migrated: false, oldKeys: [] };
29976
30944
  }
29977
30945
  let tmpPath = null;
@@ -30024,7 +30992,7 @@ function recordConfigParseFailure(configPath, errorMessage) {
30024
30992
  }
30025
30993
  function loadConfigFromPath(configPath) {
30026
30994
  try {
30027
- if (!existsSync7(configPath))
30995
+ if (!existsSync8(configPath))
30028
30996
  return null;
30029
30997
  const content = readFileSync7(configPath, "utf-8");
30030
30998
  const rawConfig = import_comment_json.parse(content);
@@ -30109,16 +31077,9 @@ function mergeInspectConfig(baseInspect, overrideInspect) {
30109
31077
  ...overrideInspect,
30110
31078
  duplicates: baseInspect?.duplicates || overrideInspect?.duplicates ? {
30111
31079
  ...baseInspect?.duplicates,
30112
- ...overrideInspect?.duplicates,
30113
- anonymize: baseInspect?.duplicates?.anonymize || overrideInspect?.duplicates?.anonymize ? {
30114
- ...baseInspect?.duplicates?.anonymize,
30115
- ...overrideInspect?.duplicates?.anonymize
30116
- } : undefined
31080
+ ...overrideInspect?.duplicates
30117
31081
  } : undefined
30118
31082
  };
30119
- if (inspect.duplicates && inspect.duplicates.anonymize === undefined) {
30120
- delete inspect.duplicates.anonymize;
30121
- }
30122
31083
  if (Object.values(inspect).every((value) => value === undefined)) {
30123
31084
  return;
30124
31085
  }
@@ -30312,7 +31273,7 @@ import { spawn as spawn2 } from "node:child_process";
30312
31273
  import { createHash as createHash4 } from "node:crypto";
30313
31274
  import {
30314
31275
  createReadStream,
30315
- existsSync as existsSync9,
31276
+ existsSync as existsSync10,
30316
31277
  mkdirSync as mkdirSync7,
30317
31278
  readFileSync as readFileSync10,
30318
31279
  renameSync as renameSync6,
@@ -30332,7 +31293,7 @@ import {
30332
31293
  unlinkSync as unlinkSync6,
30333
31294
  writeFileSync as writeFileSync5
30334
31295
  } from "node:fs";
30335
- import { homedir as homedir11 } from "node:os";
31296
+ import { homedir as homedir12 } from "node:os";
30336
31297
  import { join as join11 } from "node:path";
30337
31298
  function aftCacheBase() {
30338
31299
  const override = process.env.AFT_CACHE_DIR;
@@ -30340,10 +31301,10 @@ function aftCacheBase() {
30340
31301
  return override;
30341
31302
  if (process.platform === "win32") {
30342
31303
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
30343
- const base2 = localAppData || join11(homedir11(), "AppData", "Local");
31304
+ const base2 = localAppData || join11(homedir12(), "AppData", "Local");
30344
31305
  return join11(base2, "aft");
30345
31306
  }
30346
- const base = process.env.XDG_CACHE_HOME || join11(homedir11(), ".cache");
31307
+ const base = process.env.XDG_CACHE_HOME || join11(homedir12(), ".cache");
30347
31308
  return join11(base, "aft");
30348
31309
  }
30349
31310
  function lspCacheRoot() {
@@ -30387,11 +31348,11 @@ function writeInstalledMetaIn(installDir, version2, sha256) {
30387
31348
  }
30388
31349
  }
30389
31350
  function readInstalledMetaIn(installDir) {
30390
- const path3 = join11(installDir, INSTALLED_META_FILE);
31351
+ const path2 = join11(installDir, INSTALLED_META_FILE);
30391
31352
  try {
30392
- if (!statSync5(path3).isFile())
31353
+ if (!statSync5(path2).isFile())
30393
31354
  return null;
30394
- const raw = readFileSync8(path3, "utf8");
31355
+ const raw = readFileSync8(path2, "utf8");
30395
31356
  const parsed = JSON.parse(raw);
30396
31357
  if (typeof parsed.version !== "string" || parsed.version.length === 0)
30397
31358
  return null;
@@ -30697,7 +31658,7 @@ var NPM_LSP_TABLE = [
30697
31658
  ];
30698
31659
 
30699
31660
  // src/lsp-project-relevance.ts
30700
- import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "node:fs";
31661
+ import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "node:fs";
30701
31662
  import { join as join12 } from "node:path";
30702
31663
  var MAX_WALK_DIRS = 200;
30703
31664
  var MAX_WALK_DEPTH = 4;
@@ -30715,7 +31676,7 @@ function hasRootMarker(projectRoot, rootMarkers) {
30715
31676
  if (!rootMarkers)
30716
31677
  return false;
30717
31678
  for (const marker of rootMarkers) {
30718
- if (existsSync8(join12(projectRoot, marker)))
31679
+ if (existsSync9(join12(projectRoot, marker)))
30719
31680
  return true;
30720
31681
  }
30721
31682
  return false;
@@ -30897,7 +31858,7 @@ async function resolveTargetVersion(spec, config2, fetchImpl = fetch) {
30897
31858
  function ensureInstallAnchor(cwd) {
30898
31859
  try {
30899
31860
  const stub = join13(cwd, "package.json");
30900
- if (!existsSync9(stub)) {
31861
+ if (!existsSync10(stub)) {
30901
31862
  writeFileSync6(stub, `${JSON.stringify({ name: "aft-lsp-cache", version: "0.0.0", private: true })}
30902
31863
  `);
30903
31864
  }
@@ -31083,8 +32044,8 @@ function installedBinaryPath(spec) {
31083
32044
  }
31084
32045
  return null;
31085
32046
  }
31086
- function sha256OfFileSync(path3) {
31087
- return createHash4("sha256").update(readFileSync10(path3)).digest("hex");
32047
+ function sha256OfFileSync(path2) {
32048
+ return createHash4("sha256").update(readFileSync10(path2)).digest("hex");
31088
32049
  }
31089
32050
  function quarantineCachedNpmInstall(spec, reason) {
31090
32051
  const packageDir = lspPackageDir(spec.npm);
@@ -31162,9 +32123,15 @@ function runAutoInstall(projectRoot, config2, fetchImpl = fetch) {
31162
32123
  return installsStarted;
31163
32124
  },
31164
32125
  skipped,
31165
- installsComplete: Promise.all(installPromises).then(() => {})
32126
+ installsComplete: Promise.all(installPromises).then(() => {}),
32127
+ getCachedBinDirs: () => NPM_LSP_TABLE.filter((spec) => isInstalled(spec.npm, spec.binary) && validateCachedNpmInstall(spec)).map((spec) => lspBinDir(spec.npm))
31166
32128
  };
31167
32129
  }
32130
+ async function pushLspPathsAfterAutoInstall(pool, projectRoot, cachedBinDirs) {
32131
+ const paths = [...new Set(cachedBinDirs)];
32132
+ pool.setConfigureOverride("lsp_paths_extra", paths);
32133
+ await pool.reconfigure(projectRoot, { lsp_paths_extra: paths });
32134
+ }
31168
32135
 
31169
32136
  // src/lsp-github-install.ts
31170
32137
  import { execFileSync as execFileSync2 } from "node:child_process";
@@ -31173,7 +32140,7 @@ import {
31173
32140
  copyFileSync as copyFileSync4,
31174
32141
  createReadStream as createReadStream2,
31175
32142
  createWriteStream as createWriteStream3,
31176
- existsSync as existsSync10,
32143
+ existsSync as existsSync11,
31177
32144
  lstatSync as lstatSync2,
31178
32145
  mkdirSync as mkdirSync8,
31179
32146
  readdirSync as readdirSync4,
@@ -31186,7 +32153,7 @@ import {
31186
32153
  unlinkSync as unlinkSync7,
31187
32154
  writeFileSync as writeFileSync7
31188
32155
  } from "node:fs";
31189
- import { dirname as dirname5, join as join14, relative as relative3, resolve as resolve7 } from "node:path";
32156
+ import { dirname as dirname6, join as join14, relative as relative3, resolve as resolve7 } from "node:path";
31190
32157
  import { Readable as Readable3 } from "node:stream";
31191
32158
  import { pipeline as pipeline3 } from "node:stream/promises";
31192
32159
 
@@ -31308,10 +32275,10 @@ function ghBinaryCandidates(spec, platform) {
31308
32275
  }
31309
32276
  function readGithubInstalledMetaIn(installDir) {
31310
32277
  try {
31311
- const path3 = join14(installDir, INSTALLED_META_FILE2);
31312
- if (!statSync7(path3).isFile())
32278
+ const path2 = join14(installDir, INSTALLED_META_FILE2);
32279
+ if (!statSync7(path2).isFile())
31313
32280
  return null;
31314
- const parsed = JSON.parse(readFileSync11(path3, "utf8"));
32281
+ const parsed = JSON.parse(readFileSync11(path2, "utf8"));
31315
32282
  if (typeof parsed.version !== "string" || parsed.version.length === 0)
31316
32283
  return null;
31317
32284
  return {
@@ -31342,17 +32309,17 @@ function writeGithubInstalledMetaIn(installDir, version2, binarySha256, archiveS
31342
32309
  }
31343
32310
  var MAX_DOWNLOAD_BYTES3 = 256 * 1024 * 1024;
31344
32311
  var MAX_EXTRACT_BYTES2 = 1024 * 1024 * 1024;
31345
- function sha256OfFile(path3) {
32312
+ function sha256OfFile(path2) {
31346
32313
  return new Promise((resolve8, reject) => {
31347
32314
  const hash2 = createHash5("sha256");
31348
- const stream = createReadStream2(path3);
32315
+ const stream = createReadStream2(path2);
31349
32316
  stream.on("error", reject);
31350
32317
  stream.on("data", (chunk) => hash2.update(chunk));
31351
32318
  stream.on("end", () => resolve8(hash2.digest("hex")));
31352
32319
  });
31353
32320
  }
31354
- function sha256OfFileSync2(path3) {
31355
- return createHash5("sha256").update(readFileSync11(path3)).digest("hex");
32321
+ function sha256OfFileSync2(path2) {
32322
+ return createHash5("sha256").update(readFileSync11(path2)).digest("hex");
31356
32323
  }
31357
32324
  async function fetchReleaseByTag(githubRepo, tag, fetchImpl, signal) {
31358
32325
  const candidates = [];
@@ -31528,7 +32495,7 @@ async function downloadFile(url2, destPath, fetchImpl, assetSize, signal) {
31528
32495
  if (Number.isFinite(advertised) && advertised > MAX_DOWNLOAD_BYTES3) {
31529
32496
  throw new Error(`Content-Length ${advertised} exceeds max ${MAX_DOWNLOAD_BYTES3}`);
31530
32497
  }
31531
- mkdirSync8(dirname5(destPath), { recursive: true });
32498
+ mkdirSync8(dirname6(destPath), { recursive: true });
31532
32499
  let bytesWritten = 0;
31533
32500
  const guard = new TransformStream({
31534
32501
  transform(chunk, controller) {
@@ -31675,7 +32642,7 @@ function quarantineCachedGithubInstall(spec, reason) {
31675
32642
  const dest = join14(ghCacheRoot(), ".quarantine", encodeURIComponent(spec.id), `${Date.now()}`);
31676
32643
  warn2(`[lsp] tofu_mismatch ${spec.id}: ${reason}; quarantining ${packageDir} -> ${dest}`);
31677
32644
  try {
31678
- mkdirSync8(dirname5(dest), { recursive: true });
32645
+ mkdirSync8(dirname6(dest), { recursive: true });
31679
32646
  rmSync5(dest, { recursive: true, force: true });
31680
32647
  renameSync7(packageDir, dest);
31681
32648
  } catch (err) {
@@ -31804,12 +32771,12 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
31804
32771
  } catch {}
31805
32772
  }
31806
32773
  const innerBinaryPath = join14(extractDir, spec.binaryPathInArchive(platform, arch, version2));
31807
- if (!existsSync10(innerBinaryPath)) {
32774
+ if (!existsSync11(innerBinaryPath)) {
31808
32775
  error2(`[lsp] ${spec.id}: extracted binary not found at ${innerBinaryPath}`);
31809
32776
  return null;
31810
32777
  }
31811
32778
  const targetBinary = ghBinaryPath(spec, platform);
31812
- mkdirSync8(dirname5(targetBinary), { recursive: true });
32779
+ mkdirSync8(dirname6(targetBinary), { recursive: true });
31813
32780
  try {
31814
32781
  copyFileSync4(innerBinaryPath, targetBinary);
31815
32782
  if (platform !== "win32") {
@@ -31888,7 +32855,7 @@ function runGithubAutoInstall(relevantServers, config2, fetchImpl = fetch) {
31888
32855
  if (!host) {
31889
32856
  for (const spec of GITHUB_LSP_TABLE) {
31890
32857
  try {
31891
- if (existsSync10(ghBinDir(spec))) {
32858
+ if (existsSync11(ghBinDir(spec))) {
31892
32859
  cachedBinDirs.push(ghBinDir(spec));
31893
32860
  }
31894
32861
  } catch {}
@@ -31898,7 +32865,8 @@ function runGithubAutoInstall(relevantServers, config2, fetchImpl = fetch) {
31898
32865
  installsStarted: 0,
31899
32866
  installingBinaries: [],
31900
32867
  skipped,
31901
- installsComplete: Promise.resolve()
32868
+ installsComplete: Promise.resolve(),
32869
+ getCachedBinDirs: () => [...cachedBinDirs]
31902
32870
  };
31903
32871
  }
31904
32872
  for (const spec of GITHUB_LSP_TABLE) {
@@ -31940,7 +32908,13 @@ function runGithubAutoInstall(relevantServers, config2, fetchImpl = fetch) {
31940
32908
  return installsStarted;
31941
32909
  },
31942
32910
  skipped,
31943
- installsComplete: Promise.all(installPromises).then(() => {})
32911
+ installsComplete: Promise.all(installPromises).then(() => {}),
32912
+ getCachedBinDirs: () => {
32913
+ const currentHost = detectHostPlatform();
32914
+ if (!currentHost)
32915
+ return [];
32916
+ return GITHUB_LSP_TABLE.filter((spec) => isGithubInstalled(spec, currentHost.platform) && validateCachedGithubInstall(spec, currentHost.platform)).map((spec) => ghBinDir(spec));
32917
+ }
31944
32918
  };
31945
32919
  }
31946
32920
  function discoverRelevantGithubServers(projectRoot) {
@@ -32126,6 +33100,27 @@ function statusBarBlockForSession(sessionID, counts, force = false) {
32126
33100
  return shouldEmitStatusBar(state, counts) ? formatStatusBar(counts) : undefined;
32127
33101
  }
32128
33102
 
33103
+ // src/bash-wait-detach.ts
33104
+ var BASH_TRANSPORT_TIMEOUT_MS = 30000;
33105
+ async function sendBashWaitDetach(bridge, sessionID) {
33106
+ const response = await bridge.send("bash_wait_detach", { session_id: sessionID }, { keepBridgeOnTimeout: true, transportTimeoutMs: BASH_TRANSPORT_TIMEOUT_MS });
33107
+ if (response.success === false) {
33108
+ throw new Error(String(response.message ?? "bash_wait_detach failed"));
33109
+ }
33110
+ }
33111
+ async function signalBashWaitDetachForProject(pool, projectRoot, sessionID) {
33112
+ if (!sessionID)
33113
+ return;
33114
+ const bridge = pool.getActiveBridgeForRoot(projectRoot);
33115
+ if (!bridge)
33116
+ return;
33117
+ try {
33118
+ await sendBashWaitDetach(bridge, sessionID);
33119
+ } catch (err) {
33120
+ warn2(`[bash_wait_detach] failed for session ${sessionID}: ${err instanceof Error ? err.message : String(err)}`);
33121
+ }
33122
+ }
33123
+
32129
33124
  // src/shutdown-hooks.ts
32130
33125
  var GLOBAL_KEY = "__aftPiShutdownHooks__";
32131
33126
  function getState() {
@@ -32222,8 +33217,8 @@ import { StringEnum } from "@earendil-works/pi-ai";
32222
33217
  import { Type as Type3 } from "typebox";
32223
33218
 
32224
33219
  // src/tools/hoisted.ts
32225
- import { stat } from "node:fs/promises";
32226
- import { homedir as homedir12 } from "node:os";
33220
+ import { stat as stat2 } from "node:fs/promises";
33221
+ import { homedir as homedir13 } from "node:os";
32227
33222
  import { isAbsolute as isAbsolute6, relative as relative4, resolve as resolve8, sep } from "node:path";
32228
33223
  import {
32229
33224
  renderDiff
@@ -32367,15 +33362,15 @@ function containsPath(parent, child) {
32367
33362
  const rel = relative4(parent, child);
32368
33363
  return rel === "" || !rel.startsWith("..") && !isAbsolute6(rel);
32369
33364
  }
32370
- function expandTilde2(path3) {
32371
- if (!path3 || !path3.startsWith("~"))
32372
- return path3;
32373
- if (path3 === "~")
32374
- return homedir12();
32375
- if (path3.startsWith(`~${sep}`) || path3.startsWith("~/")) {
32376
- return resolve8(homedir12(), path3.slice(2));
33365
+ function expandTilde2(path2) {
33366
+ if (!path2 || !path2.startsWith("~"))
33367
+ return path2;
33368
+ if (path2 === "~")
33369
+ return homedir13();
33370
+ if (path2.startsWith(`~${sep}`) || path2.startsWith("~/")) {
33371
+ return resolve8(homedir13(), path2.slice(2));
32377
33372
  }
32378
- return path3;
33373
+ return path2;
32379
33374
  }
32380
33375
  function absoluteSearchPath(cwd, target) {
32381
33376
  const expanded = expandTilde2(target);
@@ -32383,7 +33378,7 @@ function absoluteSearchPath(cwd, target) {
32383
33378
  }
32384
33379
  async function searchPathExists(cwd, target) {
32385
33380
  try {
32386
- await stat(absoluteSearchPath(cwd, target));
33381
+ await stat2(absoluteSearchPath(cwd, target));
32387
33382
  return true;
32388
33383
  } catch {
32389
33384
  return false;
@@ -32440,6 +33435,8 @@ async function assertExternalDirectoryPermission(extCtx, target, options = {}) {
32440
33435
  return;
32441
33436
  if (options.restrictToProjectRoot === false)
32442
33437
  return;
33438
+ if (options.serverValidatedRead === true)
33439
+ return;
32443
33440
  throw new Error(`Blocked: '${absoluteTarget}' is outside the project root and restrict_to_project_root is ` + "enabled (AFT full isolation). Not overridable per-call; set restrict_to_project_root: false " + "in aft.jsonc to allow external paths.");
32444
33441
  }
32445
33442
  var ReadParams = Type2.Object({
@@ -32449,8 +33446,8 @@ var ReadParams = Type2.Object({
32449
33446
  filePath: Type2.Optional(Type2.String({
32450
33447
  description: "Alias for `path` — provide one of the two."
32451
33448
  })),
32452
- offset: optionalInt(1, Number.MAX_SAFE_INTEGER),
32453
- limit: optionalInt(1, Number.MAX_SAFE_INTEGER)
33449
+ offset: optionalInt(1, Number.MAX_SAFE_INTEGER, "1-based line number to start reading from (use with limit)"),
33450
+ limit: optionalInt(1, Number.MAX_SAFE_INTEGER, "Maximum number of lines to return")
32454
33451
  });
32455
33452
  var WriteParams = Type2.Object({
32456
33453
  filePath: Type2.Optional(Type2.String({
@@ -32461,6 +33458,17 @@ var WriteParams = Type2.Object({
32461
33458
  })),
32462
33459
  content: Type2.String({ description: "Full file contents to write" })
32463
33460
  });
33461
+ var BatchEditParams = Type2.Object({
33462
+ oldString: Type2.Optional(Type2.String({ description: "Text to find for a batch find/replace edit" })),
33463
+ newString: Type2.Optional(Type2.String({ description: "Replacement text for a batch find/replace edit" })),
33464
+ replaceAll: Type2.Optional(Type2.Boolean({ description: "Replace every occurrence for this batch item" })),
33465
+ occurrence: optionalInt(0, Number.MAX_SAFE_INTEGER, "0-based occurrence for this batch item"),
33466
+ startLine: optionalInt(1, Number.MAX_SAFE_INTEGER, "1-based start line for a batch line-range edit"),
33467
+ endLine: optionalInt(1, Number.MAX_SAFE_INTEGER, "1-based end line for a batch line-range edit"),
33468
+ content: Type2.Optional(Type2.String({
33469
+ description: "Replacement text for a batch line-range edit (empty string deletes the lines)"
33470
+ }))
33471
+ });
32464
33472
  var EditParams = Type2.Object({
32465
33473
  filePath: Type2.Optional(Type2.String({
32466
33474
  description: "Path to the file to edit (absolute or relative to project root)"
@@ -32471,9 +33479,12 @@ var EditParams = Type2.Object({
32471
33479
  oldString: Type2.Optional(Type2.String({ description: "Text to find (exact match, fuzzy fallback)" })),
32472
33480
  newString: Type2.Optional(Type2.String({ description: "Replacement text (omit to delete match)" })),
32473
33481
  replaceAll: Type2.Optional(Type2.Boolean({ description: "Replace every occurrence" })),
32474
- occurrence: optionalInt(0, Number.MAX_SAFE_INTEGER),
33482
+ occurrence: optionalInt(0, Number.MAX_SAFE_INTEGER, "0-based occurrence to replace when multiple matches exist"),
32475
33483
  appendContent: Type2.Optional(Type2.String({
32476
- description: "Append text to the end of the file (creates the file if missing, parent dirs auto-created). When set, oldString/newString are ignored."
33484
+ description: "Append text to the end of the file (creates the file if missing, parent dirs auto-created). When set, edits/oldString/newString are ignored."
33485
+ })),
33486
+ edits: Type2.Optional(Type2.Array(BatchEditParams, {
33487
+ description: "Batch edits — array of { oldString, newString }, { oldString, newString, replaceAll: true }, or { startLine, endLine, content } objects applied atomically."
32477
33488
  }))
32478
33489
  });
32479
33490
  var GrepParams = Type2.Object({
@@ -32490,6 +33501,47 @@ function readPathArg(args) {
32490
33501
  function mutationFilePathArg(args) {
32491
33502
  return coerceAliasedStringParam(args.filePath, args.path);
32492
33503
  }
33504
+ function hasOwn(record2, key) {
33505
+ return Object.hasOwn(record2, key);
33506
+ }
33507
+ function validateBatchEdit(edit, index) {
33508
+ if (!edit || typeof edit !== "object" || Array.isArray(edit)) {
33509
+ throw new Error(`batch: edit[${index}] must be an object`);
33510
+ }
33511
+ const record2 = edit;
33512
+ if (typeof record2.oldString === "string") {
33513
+ return;
33514
+ }
33515
+ if (hasOwn(record2, "startLine")) {
33516
+ if (typeof record2.startLine !== "number" || !Number.isInteger(record2.startLine) || record2.startLine < 0) {
33517
+ throw new Error(`batch: edit[${index}] 'startLine' must be a positive integer (1-based)`);
33518
+ }
33519
+ if (record2.startLine === 0) {
33520
+ throw new Error(`batch: edit[${index}] 'startLine' must be >= 1 (1-based)`);
33521
+ }
33522
+ if (typeof record2.endLine !== "number" || !Number.isInteger(record2.endLine) || record2.endLine < 0) {
33523
+ throw new Error(`batch: edit[${index}] 'endLine' must be a positive integer (1-based)`);
33524
+ }
33525
+ if (record2.endLine === 0) {
33526
+ throw new Error(`batch: edit[${index}] 'endLine' must be >= 1 (1-based)`);
33527
+ }
33528
+ return;
33529
+ }
33530
+ throw new Error(`batch: edit[${index}] must have either 'oldString' or 'startLine'/'endLine'`);
33531
+ }
33532
+ function validateBatchEdits(edits) {
33533
+ if (edits === undefined)
33534
+ return;
33535
+ if (!Array.isArray(edits)) {
33536
+ throw new Error("batch: missing required param 'edits' (expected array)");
33537
+ }
33538
+ if (edits.length === 0) {
33539
+ throw new Error("batch: 'edits' array must not be empty");
33540
+ }
33541
+ edits.forEach((edit, index) => {
33542
+ validateBatchEdit(edit, index);
33543
+ });
33544
+ }
32493
33545
  function renderReadCall(args, theme, context) {
32494
33546
  const text = reuseText(context.lastComponent);
32495
33547
  const filePath = args ? readPathArg(args) : undefined;
@@ -32516,7 +33568,8 @@ function registerHoistedTools(pi, ctx, surface) {
32516
33568
  const limit = coerceOptionalInt(params.limit, "limit", 1, Number.MAX_SAFE_INTEGER);
32517
33569
  const filePath = await resolvePathArg(extCtx.cwd, pathArg);
32518
33570
  await assertExternalDirectoryPermission(extCtx, filePath, {
32519
- restrictToProjectRoot: surface.restrictToProjectRoot
33571
+ restrictToProjectRoot: surface.restrictToProjectRoot,
33572
+ serverValidatedRead: true
32520
33573
  });
32521
33574
  const rawArgs = { filePath: pathArg };
32522
33575
  if (offset !== undefined)
@@ -32597,19 +33650,26 @@ PDFs aren't supported on the Pi harness yet.`, response);
32597
33650
  pi.registerTool({
32598
33651
  name: "edit",
32599
33652
  label: "edit",
32600
- description: "Find-and-replace edit with progressive fuzzy matching (handles whitespace and Unicode drift). Uses `filePath`, `oldString`, `newString`. Errors on multiple matches use `occurrence` to pick one, or `replaceAll: true`.",
32601
- promptSnippet: "Targeted find-and-replace (uses filePath/oldString/newString; occurrence or replaceAll for disambiguation; fuzzy whitespace matching). Pass appendContent to append to a file (creates if missing).",
33653
+ description: "Edit part of a file via `appendContent`, batch `edits[]`, or `oldString`/`newString` find-and-replace. Batch `{ oldString, newString, replaceAll: true }` replaces every match. Mode priority: appendContent > edits > oldString.",
33654
+ promptSnippet: "Partial file edits via appendContent, edits[], or oldString/newString (mode priority: appendContent > edits > oldString).",
32602
33655
  promptGuidelines: [
32603
33656
  "Prefer edit over write when changing part of an existing file.",
32604
- "Include enough surrounding context in oldString to make the match unique, or set replaceAll/occurrence explicitly.",
32605
- "Use appendContent (instead of read+write) when adding text to the end of a file."
33657
+ "Use appendContent when adding text to the end of a file.",
33658
+ "Use edits[] for multiple atomic changes in one file.",
33659
+ "Include enough surrounding context in oldString to make the match unique, or set replaceAll/occurrence explicitly."
32606
33660
  ],
32607
33661
  parameters: EditParams,
32608
33662
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
33663
+ const argsRecord = params;
33664
+ if (argsRecord.startLine !== undefined || argsRecord.endLine !== undefined) {
33665
+ throw new Error("edit: 'startLine'/'endLine' are not top-level parameters. " + "For line-range edits, nest them inside the `edits` array: " + '`edits: [{ startLine: N, endLine: M, content: "..." }]`. ' + "For find/replace, use `oldString`/`newString` instead.");
33666
+ }
32609
33667
  const filePathArg = mutationFilePathArg(params);
32610
33668
  if (typeof filePathArg !== "string") {
32611
33669
  throw new Error("edit: missing required parameter `filePath`");
32612
33670
  }
33671
+ if (params.appendContent === undefined)
33672
+ validateBatchEdits(params.edits);
32613
33673
  const filePath = await resolvePathArg(extCtx.cwd, filePathArg);
32614
33674
  await assertExternalDirectoryPermission(extCtx, filePath, {
32615
33675
  restrictToProjectRoot: surface.restrictToProjectRoot
@@ -32617,8 +33677,18 @@ PDFs aren't supported on the Pi harness yet.`, response);
32617
33677
  const bridge = bridgeFor(ctx, extCtx.cwd);
32618
33678
  const rawArgs = { filePath: filePathArg };
32619
33679
  for (const key of ["appendContent", "oldString", "newString"]) {
32620
- if (params[key] !== undefined)
32621
- rawArgs[key] = params[key];
33680
+ if (argsRecord[key] !== undefined)
33681
+ rawArgs[key] = argsRecord[key];
33682
+ }
33683
+ if (Array.isArray(argsRecord.edits)) {
33684
+ rawArgs.edits = argsRecord.edits.map((item) => {
33685
+ if (!item || typeof item !== "object" || Array.isArray(item))
33686
+ return item;
33687
+ const batchItem = item;
33688
+ return batchItem.replaceAll === undefined ? batchItem : { ...batchItem, replaceAll: coerceBoolean(batchItem.replaceAll) };
33689
+ });
33690
+ } else if (argsRecord.edits !== undefined) {
33691
+ rawArgs.edits = argsRecord.edits;
32622
33692
  }
32623
33693
  if (params.replaceAll !== undefined)
32624
33694
  rawArgs.replaceAll = coerceBoolean(params.replaceAll);
@@ -32682,6 +33752,7 @@ function buildMutationResult(response) {
32682
33752
  const additions = diffObj?.additions ?? 0;
32683
33753
  const deletions = diffObj?.deletions ?? 0;
32684
33754
  const replacements = response.replacements;
33755
+ const editsApplied = response.edits_applied;
32685
33756
  const diagnostics = response.lsp_diagnostics;
32686
33757
  const truncated = diffObj?.truncated === true;
32687
33758
  const noOp = response.no_op === true;
@@ -32728,6 +33799,7 @@ ${formatDiagnosticsText(diagnostics)}`;
32728
33799
  additions,
32729
33800
  deletions,
32730
33801
  replacements,
33802
+ editsApplied,
32731
33803
  diagnostics,
32732
33804
  truncated: truncated || undefined,
32733
33805
  formatted,
@@ -32802,7 +33874,8 @@ ${theme.fg("error", errorText || "edit failed")}`);
32802
33874
  const additions = details?.additions ?? 0;
32803
33875
  const deletions = details?.deletions ?? 0;
32804
33876
  const text = reuseText(context.lastComponent);
32805
- const summary = theme.fg("success", `+${additions}/-${deletions}`);
33877
+ const countDetail = typeof details?.editsApplied === "number" && details.editsApplied > 1 ? `, ${details.editsApplied} edits` : typeof details?.replacements === "number" && details.replacements > 1 ? `, ${details.replacements} replacements` : "";
33878
+ const summary = theme.fg("success", `+${additions}/-${deletions}${countDetail}`);
32806
33879
  let suffix = "";
32807
33880
  if (details?.truncated) {
32808
33881
  suffix = ` ${theme.fg("muted", "(diff truncated)")}`;
@@ -32819,17 +33892,17 @@ ${summary}${suffix}`);
32819
33892
  container.addChild(new Text(renderDiff(diff), 1, 0));
32820
33893
  return container;
32821
33894
  }
32822
- function shortenPath2(path3) {
32823
- const home = homedir12();
32824
- if (path3.startsWith(home))
32825
- return `~${path3.slice(home.length)}`;
32826
- return path3;
33895
+ function shortenPath2(path2) {
33896
+ const home = homedir13();
33897
+ if (path2.startsWith(home))
33898
+ return `~${path2.slice(home.length)}`;
33899
+ return path2;
32827
33900
  }
32828
- async function resolvePathArg(cwd, path3) {
32829
- const expanded = expandTilde2(path3);
32830
- const abs = absoluteSearchPath(cwd, path3);
33901
+ async function resolvePathArg(cwd, path2) {
33902
+ const expanded = expandTilde2(path2);
33903
+ const abs = absoluteSearchPath(cwd, path2);
32831
33904
  try {
32832
- await stat(abs);
33905
+ await stat2(abs);
32833
33906
  return abs;
32834
33907
  } catch {
32835
33908
  return expanded;
@@ -32837,7 +33910,7 @@ async function resolvePathArg(cwd, path3) {
32837
33910
  }
32838
33911
 
32839
33912
  // src/tools/render-helpers.ts
32840
- import { homedir as homedir13 } from "node:os";
33913
+ import { homedir as homedir14 } from "node:os";
32841
33914
  import { renderDiff as renderDiff2 } from "@earendil-works/pi-coding-agent";
32842
33915
  import { Container as Container2, Spacer as Spacer2, Text as Text2 } from "@earendil-works/pi-tui";
32843
33916
  function reuseText2(last) {
@@ -32846,11 +33919,11 @@ function reuseText2(last) {
32846
33919
  function reuseContainer2(last) {
32847
33920
  return last instanceof Container2 ? last : new Container2;
32848
33921
  }
32849
- function shortenPath3(path3) {
32850
- const home = homedir13();
32851
- if (path3.startsWith(home))
32852
- return `~${path3.slice(home.length)}`;
32853
- return path3;
33922
+ function shortenPath3(path2) {
33923
+ const home = homedir14();
33924
+ if (path2.startsWith(home))
33925
+ return `~${path2.slice(home.length)}`;
33926
+ return path2;
32854
33927
  }
32855
33928
  function renderToolCall(toolName, summary, theme, context) {
32856
33929
  const text = reuseText2(context.lastComponent);
@@ -32858,10 +33931,10 @@ function renderToolCall(toolName, summary, theme, context) {
32858
33931
  text.setText(`${theme.fg("toolTitle", theme.bold(toolName))}${suffix}`);
32859
33932
  return text;
32860
33933
  }
32861
- function accentPath(theme, path3) {
32862
- if (!path3)
33934
+ function accentPath(theme, path2) {
33935
+ if (!path2)
32863
33936
  return theme.fg("toolOutput", "...");
32864
- return theme.fg("accent", shortenPath3(path3));
33937
+ return theme.fg("accent", shortenPath3(path2));
32865
33938
  }
32866
33939
  function collectTextContent(result) {
32867
33940
  return result.content.filter((part) => part.type === "text").map((part) => part.text ?? "").join(`
@@ -33046,17 +34119,17 @@ ${warningStrings.map((w) => ` ${w}`).join(`
33046
34119
  async function resolveAstPaths(extCtx, paths) {
33047
34120
  if (isEmptyParam(paths) || !Array.isArray(paths))
33048
34121
  return;
33049
- return Promise.all(paths.filter((path3) => typeof path3 === "string").map((path3) => resolvePathArg(extCtx.cwd, path3)));
34122
+ return Promise.all(paths.filter((path2) => typeof path2 === "string").map((path2) => resolvePathArg(extCtx.cwd, path2)));
33050
34123
  }
33051
34124
  async function assertAstPathsPermission(extCtx, paths, restrictToProjectRoot) {
33052
34125
  if (paths === undefined || paths.length === 0)
33053
34126
  return;
33054
34127
  const checked = new Set;
33055
- for (const path3 of paths) {
33056
- if (checked.has(path3))
34128
+ for (const path2 of paths) {
34129
+ if (checked.has(path2))
33057
34130
  continue;
33058
- checked.add(path3);
33059
- await assertExternalDirectoryPermission(extCtx, path3, { restrictToProjectRoot });
34131
+ checked.add(path2);
34132
+ await assertExternalDirectoryPermission(extCtx, path2, { restrictToProjectRoot });
33060
34133
  }
33061
34134
  }
33062
34135
  function buildAstSearchSections(payload, theme) {
@@ -33238,7 +34311,7 @@ function registerAstTools(pi, ctx, surface) {
33238
34311
  }
33239
34312
 
33240
34313
  // src/tools/bash.ts
33241
- import * as fs4 from "node:fs/promises";
34314
+ import * as fs3 from "node:fs/promises";
33242
34315
  import { Container as Container3, Spacer as Spacer3, Text as Text3 } from "@earendil-works/pi-tui";
33243
34316
  import { Type as Type4 } from "typebox";
33244
34317
  var BASH_WAIT_POLL_INTERVAL_MS = 100;
@@ -33254,7 +34327,7 @@ function resolveForegroundWaitMs(configured) {
33254
34327
  }
33255
34328
  return configured;
33256
34329
  }
33257
- var BASH_TRANSPORT_TIMEOUT_MS = 30000;
34330
+ var BASH_TRANSPORT_TIMEOUT_MS2 = 30000;
33258
34331
  var DEFAULT_HARD_TIMEOUT_MS = 30 * 60 * 1000;
33259
34332
  var BASH_TRANSPORT_MARGIN_MS = 1e4;
33260
34333
  function orchestratedTransportTimeoutMs(blockToCompletion, wait, effectiveTimeout, foregroundWaitMs) {
@@ -33265,7 +34338,7 @@ var BashBaseParams = {
33265
34338
  command: Type4.String({
33266
34339
  description: "Shell command to execute. Supports pipes, redirections, and shell syntax."
33267
34340
  }),
33268
- timeout: optionalInt(1, Number.MAX_SAFE_INTEGER),
34341
+ timeout: optionalInt(1, Number.MAX_SAFE_INTEGER, "Hard kill timeout in milliseconds"),
33269
34342
  workdir: Type4.Optional(Type4.String({
33270
34343
  description: "Working directory for command execution. Relative paths resolve against the project root. Defaults to the current session's working directory."
33271
34344
  })),
@@ -33273,7 +34346,7 @@ var BashBaseParams = {
33273
34346
  description: "Human-readable description shown in UI logs. Helps users understand what the command does without reading shell syntax."
33274
34347
  })),
33275
34348
  wait: Type4.Optional(Type4.Boolean({
33276
- description: "When true, run in the foreground without auto-promoting and wait until the command finishes or reaches its timeout. Use only when you know the result is required before doing anything else."
34349
+ description: "When true, run in the foreground without auto-promoting and wait until the command finishes or reaches its timeout; if you send a new message, the wait detaches to background. Use only when you know the result is required before doing anything else."
33277
34350
  }))
33278
34351
  };
33279
34352
  var BashBackgroundFlagParam = {
@@ -33290,8 +34363,8 @@ var BashPtyParams = {
33290
34363
  pty: Type4.Optional(Type4.Boolean({
33291
34364
  description: 'Spawn the command in a real PTY for interactive programs. Implies background: true automatically. Inspect with bash_status({ task_id, output_mode: "screen" }) and send input with bash_write.'
33292
34365
  })),
33293
- ptyRows: optionalInt(1, 60),
33294
- ptyCols: optionalInt(1, 140)
34366
+ ptyRows: optionalInt(1, 60, "PTY terminal height in rows (minimum 1, maximum 60)"),
34367
+ ptyCols: optionalInt(1, 140, "PTY terminal width in columns (minimum 1, maximum 140)")
33295
34368
  };
33296
34369
  var BashParams = Type4.Object({
33297
34370
  ...BashBaseParams,
@@ -33325,7 +34398,7 @@ var BashWatchParams = Type4.Object({
33325
34398
  }),
33326
34399
  pattern: Type4.Optional(Type4.Union([Type4.String(), Type4.Object({ regex: Type4.String() })])),
33327
34400
  background: Type4.Optional(Type4.Boolean()),
33328
- timeout_ms: optionalInt(1, MAX_BASH_STATUS_WAIT_TIMEOUT_MS),
34401
+ timeout_ms: optionalInt(1, MAX_BASH_STATUS_WAIT_TIMEOUT_MS, "Maximum time to wait in milliseconds"),
33329
34402
  once: Type4.Optional(Type4.Boolean())
33330
34403
  });
33331
34404
  var BashWriteParams = Type4.Object({
@@ -33351,7 +34424,7 @@ function unavailableSnapshot() {
33351
34424
  }
33352
34425
  async function callBashBridge(bridge, command, params = {}, extCtx, options) {
33353
34426
  return await callBridge(bridge, command, params, extCtx, {
33354
- transportTimeoutMs: BASH_TRANSPORT_TIMEOUT_MS,
34427
+ transportTimeoutMs: BASH_TRANSPORT_TIMEOUT_MS2,
33355
34428
  ...options,
33356
34429
  keepBridgeOnTimeout: true
33357
34430
  });
@@ -33382,7 +34455,7 @@ function registerBashTool(pi, ctx, aftSearchRegistered = false) {
33382
34455
  const searchSteer = aftSearchRegistered ? "use `aft_search` (concepts, identifiers, regex, literals), `read`, `aft_outline`, or `aft_zoom` instead" : "use the `grep` tool, `read`, `aft_outline`, or `aft_zoom` instead";
33383
34456
  const bashCfg = resolveBashConfig(ctx.config);
33384
34457
  const compressionSentence = bashCfg.compress ? " Output is compressed by default; pass `compressed: false` for raw output. Piped commands run verbatim and show the pipeline's output; for AFT's test/build summary, run the runner without `| head`, `| tail`, or `| grep`." : "";
33385
- const tasksSentence = bashCfg.background ? ' Commands run in the foreground and return inline; `wait: true` blocks until a long command finishes instead of auto-promoting — use it when you need the result before doing anything else; keep it off otherwise so auto-promote can remind you while you work. Use `background: true` yourself ONLY when you have other useful work to do while it runs; then `bash_watch` waits on the task (sync blocks until exit/pattern, async notifies) and `bash_status` peeks at it — never background a command and immediately `bash_watch` it (that wastes a turn for what foreground returns in one), and never loop `bash_status` to wait. `pty: true` runs interactive programs (REPLs, TUIs), implies background, and is driven with `bash_status({ output_mode: "screen" })` plus `bash_write`.' : " Commands run in the foreground to completion; `timeout` is the hard kill cap (default 30 minutes).";
34458
+ const tasksSentence = bashCfg.background ? ' Commands run in the foreground and return inline; `wait: true` blocks until a long command finishes instead of auto-promoting, but detaches to background if you send a new message — use it when you need the result before doing anything else; keep it off otherwise so auto-promote can remind you while you work. Use `background: true` yourself ONLY when you have other useful work to do while it runs; then `bash_watch` waits on the task (sync blocks until exit/pattern, async notifies) and `bash_status` peeks at it — never background a command and immediately `bash_watch` it (that wastes a turn for what foreground returns in one), and never loop `bash_status` to wait. `pty: true` runs interactive programs (REPLs, TUIs), implies background, and is driven with `bash_status({ output_mode: "screen" })` plus `bash_write`.' : " Commands run in the foreground to completion; `timeout` is the hard kill cap (default 30 minutes).";
33386
34459
  pi.registerTool({
33387
34460
  name: "bash",
33388
34461
  label: "bash",
@@ -33656,7 +34729,7 @@ async function waitForBashStatus(ctx, bridge, extCtx, taskId, outputMode, waitFo
33656
34729
  let scanBaseOffset = 0;
33657
34730
  const bridgeOptions = {
33658
34731
  keepBridgeOnTimeout: true,
33659
- transportTimeoutMs: BASH_TRANSPORT_TIMEOUT_MS
34732
+ transportTimeoutMs: BASH_TRANSPORT_TIMEOUT_MS2
33660
34733
  };
33661
34734
  if (waitFor?.kind === "regex") {
33662
34735
  await validateWaitRegex(bridge, extCtx, waitFor);
@@ -33767,7 +34840,7 @@ async function readNewTaskOutput(data, cursor) {
33767
34840
  };
33768
34841
  }
33769
34842
  async function readFileBytesFrom(outputPath, cursor) {
33770
- const handle = await fs4.open(outputPath, "r");
34843
+ const handle = await fs3.open(outputPath, "r");
33771
34844
  try {
33772
34845
  const chunks = [];
33773
34846
  let offset = cursor;
@@ -33896,7 +34969,7 @@ async function formatPtyStatus(_extCtx, taskId, details, requestedOutputMode) {
33896
34969
  return `
33897
34970
  [PTY output path unavailable]`;
33898
34971
  const outputMode = requestedOutputMode ?? "screen";
33899
- const raw = outputMode === "raw" || outputMode === "both" ? await fs4.readFile(details.output_path) : undefined;
34972
+ const raw = outputMode === "raw" || outputMode === "both" ? await fs3.readFile(details.output_path) : undefined;
33900
34973
  let suffix = "";
33901
34974
  if (outputMode === "raw") {
33902
34975
  suffix = raw && raw.length > 0 ? `
@@ -34017,12 +35090,12 @@ function registerConflictsTool(pi, ctx) {
34017
35090
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
34018
35091
  const bridge = bridgeFor(ctx, extCtx.cwd);
34019
35092
  const reqParams = {};
34020
- const path3 = params?.path;
34021
- if (typeof path3 === "string" && path3.trim() !== "") {
34022
- await assertExternalDirectoryPermission(extCtx, path3, {
35093
+ const path2 = params?.path;
35094
+ if (typeof path2 === "string" && path2.trim() !== "") {
35095
+ await assertExternalDirectoryPermission(extCtx, path2, {
34023
35096
  restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
34024
35097
  });
34025
- reqParams.path = await resolvePathArg(extCtx.cwd, path3);
35098
+ reqParams.path = await resolvePathArg(extCtx.cwd, path2);
34026
35099
  }
34027
35100
  const response = await callToolCall(bridge, "conflicts", reqParams, extCtx);
34028
35101
  if (response.success === false) {
@@ -34434,8 +35507,8 @@ function tier2SummaryPart(summary, key, label) {
34434
35507
  return `${label} ${status ?? "unavailable"}`;
34435
35508
  }
34436
35509
  function shortDupOccurrence(entry) {
34437
- const [path3] = entry.split(":");
34438
- return path3?.split("/").pop() ?? entry;
35510
+ const [path2] = entry.split(":");
35511
+ return path2?.split("/").pop() ?? entry;
34439
35512
  }
34440
35513
  function tier2TopPreview(summary, theme) {
34441
35514
  const lines = [];
@@ -34619,7 +35692,7 @@ function navigateParamsSchema() {
34619
35692
  description: "Source file containing the symbol (absolute or relative to project root)"
34620
35693
  }),
34621
35694
  symbol: Type9.String({ description: "Name of the symbol to analyze" }),
34622
- depth: optionalInt(1, Number.MAX_SAFE_INTEGER),
35695
+ depth: optionalInt(1, Number.MAX_SAFE_INTEGER, "Maximum call-graph depth to traverse"),
34623
35696
  expression: Type9.Optional(Type9.String({ description: "Expression to track (required for trace_data)" })),
34624
35697
  toSymbol: Type9.Optional(Type9.String({
34625
35698
  description: "Target symbol for trace_to_symbol; the returned path ends here"
@@ -35056,9 +36129,9 @@ var RefactorParams = Type11.Object({
35056
36129
  destination: Type11.Optional(Type11.String({ description: "Target file (for move)" })),
35057
36130
  scope: Type11.Optional(Type11.String({ description: "Disambiguation scope for move op" })),
35058
36131
  name: Type11.Optional(Type11.String({ description: "New function name (for extract)" })),
35059
- startLine: optionalInt(1, Number.MAX_SAFE_INTEGER),
35060
- endLine: optionalInt(1, Number.MAX_SAFE_INTEGER),
35061
- callSiteLine: optionalInt(1, Number.MAX_SAFE_INTEGER)
36132
+ startLine: optionalInt(1, Number.MAX_SAFE_INTEGER, "1-based start line for extract"),
36133
+ endLine: optionalInt(1, Number.MAX_SAFE_INTEGER, "1-based end line (inclusive) for extract"),
36134
+ callSiteLine: optionalInt(1, Number.MAX_SAFE_INTEGER, "1-based call site line for inline")
35062
36135
  });
35063
36136
  function buildRefactorSections(args, payload, theme) {
35064
36137
  const response = asRecord3(payload);
@@ -35177,7 +36250,7 @@ function registerRefactorTool(pi, ctx) {
35177
36250
  import { StringEnum as StringEnum5 } from "@earendil-works/pi-ai";
35178
36251
  import { Type as Type12 } from "typebox";
35179
36252
  function responsePaths(response) {
35180
- return Array.isArray(response.paths) ? response.paths.filter((path3) => typeof path3 === "string" && path3.length > 0) : [];
36253
+ return Array.isArray(response.paths) ? response.paths.filter((path2) => typeof path2 === "string" && path2.length > 0) : [];
35181
36254
  }
35182
36255
  var SafetyParams = Type12.Object({
35183
36256
  op: StringEnum5(["undo", "history", "checkpoint", "restore", "list"], {
@@ -35377,6 +36450,9 @@ var SearchParams2 = Type13.Object({
35377
36450
  includeTests: Type13.Optional(Type13.Boolean({
35378
36451
  default: false,
35379
36452
  description: "Include test files (*.test.*, *_test.rs, __tests__/, …) plus test-support, fixture, mock, snapshot, and corpus files. Defaults to false."
36453
+ })),
36454
+ path: Type13.Optional(Type13.String({
36455
+ description: "Search a different project root (absolute or ~ path). Requires that project to have been indexed by AFT."
35380
36456
  }))
35381
36457
  });
35382
36458
  function buildSemanticSections(args, payload, theme) {
@@ -35484,6 +36560,8 @@ function registerSemanticTool(pi, ctx) {
35484
36560
  req.hint = params.hint;
35485
36561
  if (params.includeTests !== undefined)
35486
36562
  req.includeTests = params.includeTests;
36563
+ if (params.path !== undefined)
36564
+ req.path = params.path;
35487
36565
  const response = await callToolCall(bridge, "search", req, extCtx);
35488
36566
  if (response.success === false) {
35489
36567
  throw new Error(response.text || response.message || "search failed");
@@ -35554,7 +36632,7 @@ function buildWorkflowHints(opts) {
35554
36632
  }
35555
36633
  if (hasBgBash) {
35556
36634
  sections.push([
35557
- `**Long-running commands** (builds, installs, full test suites): run them in the FOREGROUND — use \`${bashName}({ command, wait: true })\` when you know it is long and need the result before anything else; otherwise omit \`wait\` so auto-promote can hand you a reminder while you work.`,
36635
+ `**Long-running commands** (builds, installs, full test suites): run them in the FOREGROUND — use \`${bashName}({ command, wait: true })\` when you know it is long and need the result before anything else; if you send a new message, the wait detaches to background; otherwise omit \`wait\` so auto-promote can hand you a reminder while you work.`,
35558
36636
  "- `background: true` is ONLY for when you have OTHER useful work to do while it runs: start it, do the other work, and the completion reminder delivers the result (or spawn a subagent for the side work). Do NOT background a command and then immediately `bash_watch` it — that spends a whole extra turn waiting for something foreground returns in one.",
35559
36637
  "- `bash_watch` is for blocking on an ALREADY-backgrounded task once you've run out of parallel work (sync — the user can interrupt), or reacting to a specific early output line (async: background:true + pattern). Never loop `bash_status` to wait — it's a one-shot inspector."
35560
36638
  ].join(`
@@ -35625,18 +36703,18 @@ function createVersionMismatchHandler(getPool, ensureCompatibleBinary = ensureBi
35625
36703
  const upgradePromise = (async () => {
35626
36704
  warn2(`WARNING: aft binary v${binaryVersion} is older than plugin v${minVersion}. ` + "Some features may not work. Attempting to download a compatible binary...");
35627
36705
  try {
35628
- const path3 = await ensureCompatibleBinary(`v${minVersion}`);
35629
- if (!path3) {
36706
+ const path2 = await ensureCompatibleBinary(`v${minVersion}`);
36707
+ if (!path2) {
35630
36708
  warn2(`Could not find or download v${minVersion}. Continuing with v${binaryVersion}.`);
35631
36709
  return null;
35632
36710
  }
35633
36711
  const pool = getPool();
35634
36712
  if (!pool) {
35635
- warn2(`Found/downloaded compatible binary at ${path3}, but bridge pool is not ready.`);
36713
+ warn2(`Found/downloaded compatible binary at ${path2}, but bridge pool is not ready.`);
35636
36714
  return null;
35637
36715
  }
35638
- log2(`Found/downloaded compatible binary at ${path3}. Replacing running bridges...`);
35639
- const replaced = await pool.replaceBinary(path3);
36716
+ log2(`Found/downloaded compatible binary at ${path2}. Replacing running bridges...`);
36717
+ const replaced = await pool.replaceBinary(path2);
35640
36718
  log2("Binary replaced successfully. New bridges will use the updated binary.");
35641
36719
  return replaced;
35642
36720
  } catch (err) {
@@ -35658,13 +36736,14 @@ var PLUGIN_VERSION = (() => {
35658
36736
  return "0.0.0";
35659
36737
  }
35660
36738
  })();
35661
- var ANNOUNCEMENT_VERSION = "0.45.0";
36739
+ var ANNOUNCEMENT_VERSION = "0.47.0";
35662
36740
  var ANNOUNCEMENT_FEATURES = [
35663
- "Code Health verified against 12 real repositories: dead code is only reported for languages we can actually analyze (Java/Kotlin now say 'not analyzed' instead of guessing), and generated code (gen/ trees, *_pb.ts, DO NOT EDIT banners) moves out of the headline counts into its own bucket.",
35664
- "Config files (playwright/eslint/tsup/vitest *.config.ts), Storybook stories, Mocha hooks, and SvelteKit hooks/$lib imports are no longer flagged as dead code.",
35665
- "Rust module aliases, inline-module calls, and cross-crate pub use re-exports now resolve; Go interface methods (sort.Interface, Bubble Tea) are no longer flagged dead.",
35666
- "Fixed npx @cortexkit/aft doctor --fix and setup failing to download the binary on fresh installs.",
35667
- "Bridges now detect an updated aft binary on disk and respawn onto it automatically."
36741
+ "Big background CPU cuts: semantic re-embeds coalesce behind a 15s quiet window instead of firing per edit, file-change processing is sliced and budgeted, and a callgraph resolver bug that could pin a core at 100% on cyclic barrel re-exports is fixed.",
36742
+ "Faster search: indexed grep and glob skip a per-query filesystem walk (2-4x faster), and cross-project search reuses cached artifacts (~40x faster).",
36743
+ "bash with wait:true now detaches to the background the moment you send a new message instead of blocking the conversation.",
36744
+ "apply_patch failures show the nearest-miss file lines so a failed patch is fixable in one shot; batch edits support replaceAll per item.",
36745
+ "Groovy, Gradle, and Jenkinsfile support in outline, zoom, search, and AST tools.",
36746
+ "Fixed: LSP memory growth during repeated scoped diagnostics (#160), fresh LSP auto-installs picked up without restart (#153), and truthful delete results for symlinked directories and failed batches."
35668
36747
  ];
35669
36748
  var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/DSa65w8wuf";
35670
36749
  var ALL_ONLY_TOOLS = new Set(["aft_callgraph", "aft_delete", "aft_move", "aft_refactor"]);
@@ -35861,6 +36940,7 @@ async function src_default(pi) {
35861
36940
  const configOverrides = buildConfigTierConfigureParams(projectRoot, {
35862
36941
  storage_dir: storageDir
35863
36942
  });
36943
+ let lspInstallCompletion = null;
35864
36944
  try {
35865
36945
  const lspAutoInstall = config2.lsp?.auto_install ?? true;
35866
36946
  const lspGraceDays = config2.lsp?.grace_days ?? 7;
@@ -35890,10 +36970,24 @@ async function src_default(pi) {
35890
36970
  if (lspInflightInstalls.length > 0) {
35891
36971
  configOverrides.lsp_inflight_installs = lspInflightInstalls;
35892
36972
  }
35893
- if (npmResult.installsStarted > 0 || ghResult.installsStarted > 0) {
36973
+ const installsWereStarted = npmResult.installsStarted > 0 || ghResult.installsStarted > 0;
36974
+ if (installsWereStarted) {
35894
36975
  log2(`[lsp] auto-install: ${npmResult.installsStarted} npm + ${ghResult.installsStarted} github install(s) running in background`);
35895
36976
  }
35896
- Promise.all([npmResult.installsComplete, ghResult.installsComplete]).then(() => {
36977
+ const installCompletion = Promise.all([npmResult.installsComplete, ghResult.installsComplete]).then(() => {
36978
+ if (installsWereStarted) {
36979
+ const updatedPaths = [
36980
+ ...new Set([...npmResult.getCachedBinDirs(), ...ghResult.getCachedBinDirs()])
36981
+ ];
36982
+ if (updatedPaths.length > 0) {
36983
+ configOverrides.lsp_paths_extra = updatedPaths;
36984
+ } else {
36985
+ delete configOverrides.lsp_paths_extra;
36986
+ }
36987
+ return updatedPaths;
36988
+ }
36989
+ return null;
36990
+ }).then((updatedPaths) => {
35897
36991
  const actionable = [...npmResult.skipped, ...ghResult.skipped].filter((s) => {
35898
36992
  const r = s.reason.toLowerCase();
35899
36993
  if (r === "auto_install: false")
@@ -35908,16 +37002,20 @@ async function src_default(pi) {
35908
37002
  return false;
35909
37003
  return true;
35910
37004
  });
35911
- if (actionable.length === 0)
35912
- return;
35913
- const lines = actionable.map((s) => ` • ${s.id}: ${s.reason}`).join(`
37005
+ if (actionable.length > 0) {
37006
+ const lines = actionable.map((s) => ` • ${s.id}: ${s.reason}`).join(`
35914
37007
  `);
35915
- warn2(`[lsp] skipped or failed to install ${actionable.length} server(s):
37008
+ warn2(`[lsp] skipped or failed to install ${actionable.length} server(s):
35916
37009
  ${lines}
35917
37010
  ` + 'Pin a working version with `lsp.versions: { "<package>": "<version>" }` if grace is blocking, ' + "or set `lsp.auto_install: false` to suppress.");
37011
+ }
37012
+ return updatedPaths;
35918
37013
  }).catch((err) => {
35919
37014
  warn2(`[lsp] install-summary aggregation failed: ${err}`);
37015
+ return null;
35920
37016
  });
37017
+ if (installsWereStarted)
37018
+ lspInstallCompletion = installCompletion;
35921
37019
  } catch (err) {
35922
37020
  warn2(`[lsp] auto-install setup failed: ${err instanceof Error ? err.message : String(err)}`);
35923
37021
  }
@@ -35988,6 +37086,17 @@ ${lines}
35988
37086
  });
35989
37087
  }
35990
37088
  });
37089
+ if (lspInstallCompletion) {
37090
+ lspInstallCompletion.then((updatedPaths) => {
37091
+ if (!updatedPaths)
37092
+ return;
37093
+ pushLspPathsAfterAutoInstall(pool, projectRoot, updatedPaths).then(() => {
37094
+ log2(`[lsp] lsp_paths_extra updated after auto-install: ${updatedPaths.length} dirs pushed to live bridges`);
37095
+ }).catch((err) => {
37096
+ warn2(`[lsp] live bridge lsp_paths_extra update failed: ${err}`);
37097
+ });
37098
+ });
37099
+ }
35991
37100
  pool.setConfigureOverride("harness", "pi");
35992
37101
  pool.setConfigureOverride("aft_search_registered", resolveToolSurface(config2).semantic);
35993
37102
  const ctx = { pool, config: config2, storageDir };
@@ -36100,7 +37209,9 @@ ${lines}
36100
37209
  }
36101
37210
  });
36102
37211
  pi.on("input", (_event, extCtx) => {
36103
- signalSyncWatchAbort(resolveSessionId(extCtx));
37212
+ const sessionId = resolveSessionId(extCtx);
37213
+ signalSyncWatchAbort(sessionId);
37214
+ signalBashWaitDetachForProject(pool, extCtx.cwd, sessionId);
36104
37215
  });
36105
37216
  const unregisterShutdownCleanup = registerShutdownCleanup(async () => {
36106
37217
  try {