@cortexkit/aft-pi 0.46.0 → 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.4/node_modules/@cortexkit/subc-client/dist/client.js
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.4/node_modules/@cortexkit/subc-client/dist/auth.js
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.4/node_modules/@cortexkit/subc-client/dist/connection-file.js
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");
@@ -11991,128 +12489,60 @@ async function readConnectionFile(path2) {
11991
12489
  key: toBytes(parsed.key, "key"),
11992
12490
  daemonId: toBytes(parsed.daemon_id, "daemon_id"),
11993
12491
  pid: parsed.pid,
11994
- daemonVer: parsed.daemon_ver ?? ""
11995
- };
11996
- validate(info);
11997
- return info;
11998
- }
11999
-
12000
- // ../../node_modules/.bun/@cortexkit+subc-client@0.3.4/node_modules/@cortexkit/subc-client/dist/envelope.js
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
- (function(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 || (FrameType = {}));
12020
- var FRAME_TYPE_MAX = FrameType.Goodbye;
12021
- function isPureHeader(ty) {
12022
- return ty === FrameType.Cancel || ty === FrameType.Ping || ty === FrameType.Pong || ty === FrameType.Goodbye;
12023
- }
12024
- var Priority;
12025
- (function(Priority2) {
12026
- Priority2[Priority2["Passive"] = 0] = "Passive";
12027
- Priority2[Priority2["Interactive"] = 1] = "Interactive";
12028
- Priority2[Priority2["Background"] = 2] = "Background";
12029
- })(Priority || (Priority = {}));
12030
- var FLAG_BINARY = 1;
12031
- var FLAG_PRIORITY_MASK = 6;
12032
- var FLAG_PRIORITY_SHIFT = 1;
12033
- var FLAG_LAST = 8;
12034
- var FLAG_RESERVED_MASK = 240;
12035
- function buildFlags(binary, priority, last) {
12036
- let b = 0;
12037
- if (binary)
12038
- b |= FLAG_BINARY;
12039
- b |= priority << FLAG_PRIORITY_SHIFT;
12040
- if (last)
12041
- b |= FLAG_LAST;
12042
- return b;
12043
- }
12044
- function encodeHeader(h) {
12045
- const buf = new Uint8Array(HEADER_LEN);
12046
- const view = new DataView(buf.buffer);
12047
- view.setUint32(0, h.len, true);
12048
- buf[4] = h.ver;
12049
- buf[5] = h.ty;
12050
- buf[6] = h.flags;
12051
- view.setUint16(7, h.channel, true);
12052
- view.setBigUint64(9, h.corr, true);
12053
- return buf;
12492
+ daemonVer: parsed.daemon_ver ?? ""
12493
+ };
12494
+ validate(info);
12495
+ return info;
12054
12496
  }
12055
12497
 
12056
- class DecodeError extends Error {
12057
- }
12058
- function decodeHeader(bytes) {
12059
- if (bytes.length < FROZEN_PREFIX_LEN) {
12060
- throw new DecodeError(`header shorter than frozen prefix: have ${bytes.length} bytes`);
12061
- }
12062
- const ver = bytes[4];
12063
- if (ver !== PROTOCOL_VERSION) {
12064
- throw new DecodeError(`unsupported envelope version ${ver}`);
12065
- }
12066
- if (bytes.length < HEADER_LEN) {
12067
- throw new DecodeError(`header too short for version: have ${bytes.length} bytes, need ${HEADER_LEN}`);
12068
- }
12069
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
12070
- const len = view.getUint32(0, true);
12071
- const tyByte = bytes[5];
12072
- if (tyByte > FRAME_TYPE_MAX) {
12073
- throw new DecodeError(`unknown frame type byte ${tyByte}`);
12074
- }
12075
- const ty = tyByte;
12076
- const flags = bytes[6];
12077
- if ((flags & FLAG_RESERVED_MASK) !== 0) {
12078
- throw new DecodeError(`reserved flag bits set in flags 0b${flags.toString(2)}`);
12079
- }
12080
- if ((flags & FLAG_PRIORITY_MASK) >> FLAG_PRIORITY_SHIFT === 3) {
12081
- throw new DecodeError(`reserved priority bits set in flags 0b${flags.toString(2)}`);
12498
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/route-handle.js
12499
+ var connectionToken = new WeakMap;
12500
+
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);
12082
12515
  }
12083
- if (isPureHeader(ty) && len !== 0) {
12084
- 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);
12085
12518
  }
12086
- const channel = view.getUint16(7, true);
12087
- const corr = view.getBigUint64(9, true);
12088
- return { len, ver, ty, flags, channel, corr };
12089
12519
  }
12090
- function buildFrame(ty, flags, channel, corr, body) {
12091
- 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);
12092
12523
  }
12093
- function buildFrameWithVersion(ver, ty, flags, channel, corr, body) {
12094
- if (body.length > MAX_FRAME_BODY_LEN) {
12095
- throw new DecodeError(`frame body ${body.length} exceeds max ${MAX_FRAME_BODY_LEN}`);
12096
- }
12097
- if (isPureHeader(ty) && body.length !== 0) {
12098
- 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";
12099
12532
  }
12100
- return {
12101
- header: { len: body.length, ver, ty, flags, channel, corr },
12102
- body
12103
- };
12104
12533
  }
12105
- function encodeFrame(frame) {
12106
- const header = encodeHeader(frame.header);
12107
- const out = new Uint8Array(header.length + frame.body.length);
12108
- out.set(header, 0);
12109
- out.set(frame.body, header.length);
12110
- 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;
12111
12542
  }
12112
12543
 
12113
- // ../../node_modules/.bun/@cortexkit+subc-client@0.3.4/node_modules/@cortexkit/subc-client/dist/socket.js
12544
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/socket.js
12114
12545
  import net from "node:net";
12115
-
12116
12546
  class SocketClosedError extends Error {
12117
12547
  }
12118
12548
 
@@ -12181,6 +12611,24 @@ class SubcSocket {
12181
12611
  });
12182
12612
  });
12183
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
+ }
12184
12632
  readExact(n, deadlineMs) {
12185
12633
  if (this.waiter) {
12186
12634
  return Promise.reject(new Error("concurrent readExact is not supported"));
@@ -12302,7 +12750,7 @@ class SubcSocket {
12302
12750
  }
12303
12751
  }
12304
12752
 
12305
- // ../../node_modules/.bun/@cortexkit+subc-client@0.3.4/node_modules/@cortexkit/subc-client/dist/client.js
12753
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/client.js
12306
12754
  var debug = debuglog("subc-client");
12307
12755
  var DEFAULT_HANDSHAKE_TIMEOUT_MS = 1e4;
12308
12756
  var DEFAULT_REQUEST_TIMEOUT_MS = 30000;
@@ -12348,7 +12796,11 @@ class SubcClient {
12348
12796
  opts;
12349
12797
  nextCorr = 1n;
12350
12798
  pending = new Map;
12799
+ lateResponses = new Map;
12351
12800
  routes = new Map;
12801
+ liveRoutes = new Map;
12802
+ connectionToken = newConnectionToken();
12803
+ ingressEpochDropCount = 0;
12352
12804
  closedErr = null;
12353
12805
  closeStarted = false;
12354
12806
  reconnecting = null;
@@ -12376,40 +12828,71 @@ class SubcClient {
12376
12828
  }
12377
12829
  async routeOpen(target, identity, opts = {}) {
12378
12830
  const consumerIdentity = routeOpenConsumerIdentity(opts);
12831
+ const consumerCapabilities = opts.consumerCapabilities;
12379
12832
  const body = this.encode({
12380
12833
  op: "route.open",
12381
12834
  target,
12382
12835
  identity,
12383
- ...consumerIdentity ? { consumer_identity: consumerIdentity } : {}
12836
+ ...consumerIdentity ? { consumer_identity: consumerIdentity } : {},
12837
+ ...consumerCapabilities !== undefined ? { consumer_capabilities: consumerCapabilities } : {}
12384
12838
  });
12385
- const reply = await this.controlRpc(body);
12386
- const parsed = this.parseJson(reply);
12387
- if (typeof parsed.route_channel !== "number") {
12388
- throw new SubcError(`route.open returned no route_channel: ${JSON.stringify(parsed)}`);
12389
- }
12390
- 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;
12391
12869
  }
12392
- async request(routeChannel, body, opts = {}) {
12870
+ async request(handle, body, opts = {}) {
12871
+ this.assertLiveHandle(handle);
12393
12872
  const bytes = body instanceof Uint8Array ? body : this.encode(body);
12394
12873
  const priority = opts.priority ?? Priority.Interactive;
12395
- const reply = await this.send(routeChannel, bytes, priority, opts.timeoutMs, opts.onProgress);
12874
+ const admission = opts.admissionClass ?? AdmissionClass.Normal;
12875
+ const reply = await this.send(handle, bytes, priority, admission, opts.timeoutMs, opts.onProgress);
12396
12876
  return this.parseJson(reply);
12397
12877
  }
12398
12878
  async call(moduleId, method, params, opts = {}) {
12399
12879
  const body = params === undefined ? { method } : { method, params };
12400
12880
  let retriedUnknownChannel = false;
12401
12881
  for (;; ) {
12402
- const routeChannel = await this.cachedRouteChannel(moduleId, opts);
12882
+ const routeHandle = await this.cachedRouteHandle(moduleId, opts);
12403
12883
  try {
12404
- return await this.managedRequest(routeChannel, body, opts);
12884
+ return await this.managedRequest(routeHandle, body, opts);
12405
12885
  } catch (err) {
12406
12886
  if (!(err instanceof SubcCallError))
12407
12887
  throw this.terminalCallError("managed call failed", err);
12408
12888
  if (err.code === "unknown_channel" && !retriedUnknownChannel && !this.closeStarted) {
12409
12889
  retriedUnknownChannel = true;
12410
- this.evictRouteChannel(routeChannel);
12890
+ this.evictRouteHandle(routeHandle);
12411
12891
  continue;
12412
12892
  }
12893
+ if (err.code === "unknown_channel" && retriedUnknownChannel) {
12894
+ this.evictRouteHandle(routeHandle);
12895
+ }
12413
12896
  if (err.kind === "not_sent") {
12414
12897
  try {
12415
12898
  await this.reconnectAfterDrop(err);
@@ -12425,29 +12908,31 @@ class SubcClient {
12425
12908
  }
12426
12909
  }
12427
12910
  }
12428
- subscribe(routeChannel, body, onEvent, opts = {}) {
12911
+ subscribe(handle, body, onEvent, opts = {}) {
12912
+ this.assertLiveHandle(handle);
12429
12913
  const bytes = body instanceof Uint8Array ? body : this.encode(body);
12430
12914
  const priority = opts.priority ?? Priority.Interactive;
12431
- const corr = this.nextCorr++;
12432
- const key = `${routeChannel}:${corr}`;
12915
+ const admission = opts.admissionClass ?? AdmissionClass.Normal;
12916
+ const corr = this.allocateCorr();
12917
+ const key = pendingKey(handle, corr);
12433
12918
  const closed = new Promise((resolve7, reject) => {
12434
12919
  if (this.closedErr) {
12435
12920
  reject(this.closedErr);
12436
12921
  return;
12437
12922
  }
12438
12923
  this.pending.set(key, {
12439
- channel: routeChannel,
12924
+ handle,
12440
12925
  resolve: () => resolve7(),
12441
12926
  reject,
12442
12927
  onProgress: onEvent,
12443
12928
  timer: null,
12444
12929
  subscription: true
12445
12930
  });
12446
- const frame = buildFrame(FrameType.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);
12447
12932
  this.sock.write(encodeFrame(frame), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS).catch((err) => {
12448
- const p = this.pending.get(key);
12449
- if (p)
12450
- 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)));
12451
12936
  });
12452
12937
  });
12453
12938
  let cancelled = false;
@@ -12455,45 +12940,77 @@ class SubcClient {
12455
12940
  if (cancelled)
12456
12941
  return;
12457
12942
  cancelled = true;
12458
- const cancel = buildFrame(FrameType.Cancel, buildFlags(false, priority, false), routeChannel, corr, EMPTY_BODY);
12459
- this.sock.write(encodeFrame(cancel), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS).catch(() => {});
12943
+ this.cancel(handle, corr, priority);
12460
12944
  };
12461
12945
  return { unsubscribe, closed };
12462
12946
  }
12463
- 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 = {}) {
12464
12987
  const key = routeCacheKey(target, identity, routeOpenConsumerIdentity(opts));
12465
12988
  const cached = this.routes.get(key);
12466
12989
  if (!cached)
12467
12990
  return;
12468
12991
  cached.closed = true;
12469
12992
  this.routes.delete(key);
12470
- const channel = cached.channel;
12471
- cached.channel = null;
12472
- if (channel !== null)
12473
- await this.closeRouteChannel(channel, opts);
12993
+ const handle = cached.handle;
12994
+ cached.handle = null;
12995
+ if (handle)
12996
+ await this.closeRoute(handle, opts);
12474
12997
  }
12475
- async closeRouteChannel(channel, opts = {}) {
12476
- if (channel === 0)
12477
- return;
12478
- if (opts.drain) {
12479
- await this.drainUnaryOnChannel(channel);
12480
- }
12481
- this.failChannel(channel, new SubcError("route closed by closeRoute", "route_closed"));
12482
- this.sendRouteGoodbye(channel);
12998
+ async closeRouteChannel(handle, opts = {}) {
12999
+ await this.closeRoute(handle, opts);
12483
13000
  }
12484
13001
  close() {
12485
13002
  this.closeStarted = true;
12486
13003
  this.fail(new SubcError("client closed"));
12487
13004
  this.sock.close();
12488
13005
  }
12489
- drainUnaryOnChannel(channel) {
13006
+ drainUnaryOnHandle(handle) {
12490
13007
  const waiters = [];
12491
13008
  for (const pending of this.pending.values()) {
12492
- if (pending.channel === channel && !pending.subscription) {
13009
+ if (pending.handle === handle && !pending.subscription) {
12493
13010
  waiters.push(new Promise((resolve7) => {
12494
- const prev = pending.onSettle;
13011
+ const previous = pending.onSettle;
12495
13012
  pending.onSettle = () => {
12496
- prev?.();
13013
+ previous?.();
12497
13014
  resolve7();
12498
13015
  };
12499
13016
  }));
@@ -12503,11 +13020,21 @@ class SubcClient {
12503
13020
  return;
12504
13021
  });
12505
13022
  }
12506
- sendRouteGoodbye(channel) {
12507
- if (this.closedErr)
13023
+ sendRouteGoodbye(handle, closeOnQueueFailure = false) {
13024
+ this.assertLiveConnection(handle);
13025
+ if (this.closedErr) {
13026
+ if (closeOnQueueFailure)
13027
+ this.closeConnectionAfterCleanupFailure();
12508
13028
  return;
12509
- const goodbye = buildFrame(FrameType.Goodbye, buildFlags(false, Priority.Interactive, false), channel, 0n, EMPTY_BODY);
12510
- 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
+ });
12511
13038
  }
12512
13039
  static async openConnection(opts) {
12513
13040
  const conn = await readConnectionFile(opts.connectionFile);
@@ -12522,37 +13049,48 @@ class SubcClient {
12522
13049
  }
12523
13050
  return { sock, conn };
12524
13051
  }
12525
- async controlRpc(body) {
12526
- return this.send(0, body, Priority.Interactive, undefined, undefined);
13052
+ async controlRpc(body, acceptFrame, onLateResponse) {
13053
+ return this.send(null, body, Priority.Interactive, AdmissionClass.Normal, undefined, undefined, acceptFrame, onLateResponse);
12527
13054
  }
12528
- send(channel, body, priority, timeoutMs, onProgress) {
13055
+ send(handle, body, priority, admission, timeoutMs, onProgress, acceptFrame, onLateResponse) {
13056
+ if (handle)
13057
+ this.assertLiveHandle(handle);
12529
13058
  if (this.closedErr)
12530
13059
  return Promise.reject(this.closedErr);
12531
- const corr = this.nextCorr++;
12532
- const key = `${channel}:${corr}`;
12533
- const frame = buildFrame(FrameType.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);
12534
13070
  return new Promise((resolve7, reject) => {
12535
13071
  const ms = timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
12536
13072
  const pending = {
12537
- channel,
13073
+ handle,
12538
13074
  resolve: resolve7,
12539
13075
  reject,
12540
13076
  onProgress,
12541
- timer: null
13077
+ timer: null,
13078
+ acceptFrame,
13079
+ onLateResponse
12542
13080
  };
12543
- pending.timer = setTimeout(() => {
12544
- this.arbitrateTimeout(key, pending, channel, corr, ms);
12545
- }, ms);
13081
+ pending.timer = setTimeout(() => this.arbitrateTimeout(key, pending, channel, corr, ms), ms);
12546
13082
  this.pending.set(key, pending);
12547
- this.sock.write(encodeFrame(frame), Date.now() + ms).catch((err) => {
12548
- const p = this.pending.get(key);
12549
- if (p)
12550
- 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)));
12551
13087
  });
12552
13088
  });
12553
13089
  }
12554
13090
  arbitrateTimeout(key, pending, channel, corr, ms) {
12555
13091
  const settleAsTimeout = () => {
13092
+ if (pending.onLateResponse)
13093
+ this.lateResponses.set(key, pending.onLateResponse);
12556
13094
  this.rejectPending(key, pending, new SubcError(this.timeoutMessage(channel, corr, ms), REQUEST_DEADLINE_MARKER));
12557
13095
  };
12558
13096
  const graceDeadline = Date.now() + this.opts.timeoutArbitrationGraceMs;
@@ -12568,59 +13106,67 @@ class SubcClient {
12568
13106
  };
12569
13107
  setImmediate(arbitrate);
12570
13108
  }
12571
- async managedRequest(routeChannel, body, opts) {
13109
+ async managedRequest(handle, body, opts) {
12572
13110
  const bytes = body instanceof Uint8Array ? body : this.encode(body);
12573
13111
  const priority = opts.priority ?? Priority.Interactive;
13112
+ const admission = opts.admissionClass ?? AdmissionClass.Normal;
12574
13113
  try {
12575
- 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);
12576
13115
  return this.parseJson(reply);
12577
- } catch (err) {
12578
- if (err instanceof SubcCallError)
12579
- throw err;
12580
- 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);
12581
13120
  }
12582
13121
  }
12583
- 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
+ }
12584
13128
  if (this.closedErr) {
12585
13129
  return Promise.reject(this.notSentCallError("request was not sent because the subc connection was already closed", this.closedErr));
12586
13130
  }
12587
- const corr = this.nextCorr++;
12588
- const key = `${channel}:${corr}`;
12589
- const frame = buildFrame(FrameType.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);
12590
13139
  let handedToSocket = false;
12591
- const classifyFailure = (err) => {
12592
- if (!handedToSocket) {
12593
- return this.notSentCallError("request bytes were not queued to the subc socket", err);
12594
- }
12595
- if (err instanceof SubcError && err.code === REQUEST_DEADLINE_MARKER) {
12596
- 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);
12597
13145
  }
12598
- 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);
12599
13147
  };
12600
13148
  return new Promise((resolve7, reject) => {
12601
13149
  const ms = timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
12602
13150
  const pending = {
12603
- channel,
13151
+ handle,
12604
13152
  resolve: resolve7,
12605
13153
  reject,
12606
13154
  onProgress,
12607
13155
  timer: null,
12608
13156
  classifyFailure
12609
13157
  };
12610
- pending.timer = setTimeout(() => {
12611
- this.arbitrateTimeout(key, pending, channel, corr, ms);
12612
- }, ms);
13158
+ pending.timer = setTimeout(() => this.arbitrateTimeout(key, pending, handle.channel, corr, ms), ms);
12613
13159
  this.pending.set(key, pending);
12614
13160
  const write = this.sock.writeTracked(encodeFrame(frame), Date.now() + ms);
12615
13161
  handedToSocket = write.queued;
12616
- write.completed.catch((err) => {
12617
- const p = this.pending.get(key);
12618
- if (p)
12619
- 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)));
12620
13166
  });
12621
13167
  });
12622
13168
  }
12623
- async cachedRouteChannel(moduleId, opts) {
13169
+ async cachedRouteHandle(moduleId, opts) {
12624
13170
  const identity = opts.identity ?? this.opts.identity;
12625
13171
  if (!identity) {
12626
13172
  throw new SubcCallError("terminal", "managed call requires a BindIdentity in SubcClient.connect({ identity }) or call(..., { identity })", "missing_identity");
@@ -12636,15 +13182,13 @@ class SubcClient {
12636
13182
  target,
12637
13183
  identity,
12638
13184
  consumerIdentity,
12639
- channel: null,
12640
- generation: 0,
13185
+ handle: null,
12641
13186
  opening: null
12642
13187
  };
12643
13188
  this.routes.set(key, cached);
12644
13189
  }
12645
- if (cached.channel !== null && cached.generation === this.generation && !this.closedErr) {
12646
- return cached.channel;
12647
- }
13190
+ if (cached.handle && this.isLiveHandle(cached.handle))
13191
+ return cached.handle;
12648
13192
  if (!cached.opening) {
12649
13193
  cached.opening = this.openCachedRoute(cached).finally(() => {
12650
13194
  cached.opening = null;
@@ -12661,44 +13205,43 @@ class SubcClient {
12661
13205
  throw this.routeClosedDuringOpen();
12662
13206
  try {
12663
13207
  await this.ensureConnectedForManaged();
12664
- } catch (err) {
12665
- throw this.notSentRecoveryError("route.open could not run because reconnect failed", err);
12666
- }
12667
- if (cached.channel !== null && cached.generation === this.generation && !this.closedErr) {
12668
- return cached.channel;
13208
+ } catch (error2) {
13209
+ throw this.notSentRecoveryError("route.open could not run because reconnect failed", error2);
12669
13210
  }
13211
+ if (cached.handle && this.isLiveHandle(cached.handle))
13212
+ return cached.handle;
12670
13213
  try {
12671
- const channel = await this.routeOpen(cached.target, cached.identity, {
13214
+ const handle = await this.routeOpen(cached.target, cached.identity, {
12672
13215
  consumerIdentity: cached.consumerIdentity ?? null
12673
13216
  });
12674
13217
  if (cached.closed) {
12675
- this.sendRouteGoodbye(channel);
13218
+ this.liveRoutes.delete(handle.channel);
13219
+ this.sendRouteGoodbye(handle);
12676
13220
  throw this.routeClosedDuringOpen();
12677
13221
  }
12678
- cached.channel = channel;
12679
- cached.generation = this.generation;
12680
- return channel;
12681
- } catch (err) {
12682
- if (err instanceof SubcCallError && err.code === "route_closed")
12683
- throw err;
12684
- 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)) {
12685
13228
  try {
12686
- await this.reconnectAfterDrop(err);
12687
- } catch (reconnectErr) {
12688
- 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);
12689
13232
  }
12690
13233
  continue;
12691
13234
  }
12692
- if (!this.closeStarted && err instanceof SubcError && isRetryableRouteOpenCode(err.code)) {
13235
+ if (!this.closeStarted && error2 instanceof SubcError && isRetryableRouteOpenCode(error2.code)) {
12693
13236
  routeRetryAttempt += 1;
12694
13237
  if (routeRetryAttempt < this.opts.reconnectBackoff.maxAttempts && Date.now() < routeRetryDeadline) {
12695
13238
  await this.opts.sleep(routeRetryDelay);
12696
13239
  routeRetryDelay = Math.min(routeRetryDelay * 2, this.opts.reconnectBackoff.capMs);
12697
13240
  continue;
12698
13241
  }
12699
- 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);
12700
13243
  }
12701
- throw this.terminalCallError(`route.open failed for module ${cached.moduleId}`, err);
13244
+ throw this.terminalCallError(`route.open failed for module ${cached.moduleId}`, error2);
12702
13245
  }
12703
13246
  }
12704
13247
  }
@@ -12761,25 +13304,27 @@ class SubcClient {
12761
13304
  this.currentConn = opened.conn;
12762
13305
  this.closedErr = null;
12763
13306
  this.generation += 1;
13307
+ this.connectionToken = newConnectionToken();
13308
+ this.liveRoutes.clear();
13309
+ this.lateResponses.clear();
13310
+ this.nextCorr = 1n;
12764
13311
  this.readLoop(opened.sock, this.generation);
12765
13312
  }
12766
13313
  async reopenCachedRoutes() {
12767
- for (const cached of this.routes.values()) {
12768
- cached.channel = null;
12769
- cached.generation = 0;
12770
- }
13314
+ for (const cached of this.routes.values())
13315
+ cached.handle = null;
12771
13316
  for (const cached of this.routes.values()) {
12772
13317
  if (cached.closed)
12773
13318
  continue;
12774
- const channel = await this.routeOpen(cached.target, cached.identity, {
13319
+ const handle = await this.routeOpen(cached.target, cached.identity, {
12775
13320
  consumerIdentity: cached.consumerIdentity ?? null
12776
13321
  });
12777
13322
  if (cached.closed) {
12778
- this.sendRouteGoodbye(channel);
13323
+ this.liveRoutes.delete(handle.channel);
13324
+ this.sendRouteGoodbye(handle);
12779
13325
  continue;
12780
13326
  }
12781
- cached.channel = channel;
12782
- cached.generation = this.generation;
13327
+ cached.handle = handle;
12783
13328
  }
12784
13329
  }
12785
13330
  timeoutMessage(channel, corr, ms) {
@@ -12793,28 +13338,37 @@ class SubcClient {
12793
13338
  async readLoop(sock, generation) {
12794
13339
  try {
12795
13340
  for (;; ) {
12796
- const headerBytes = await sock.readExact(HEADER_LEN, Number.POSITIVE_INFINITY);
12797
- 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
+ });
12798
13345
  try {
12799
- const header = decodeHeader(headerBytes);
12800
- const body = header.len === 0 ? new Uint8Array(0) : await sock.readExact(header.len, Date.now() + BODY_READ_TIMEOUT_MS);
12801
- if (this.sock === sock && this.generation === generation) {
12802
- this.dispatch({ header, body });
12803
- }
13346
+ if (this.sock === sock && this.generation === generation)
13347
+ this.dispatch(frame);
12804
13348
  } finally {
12805
13349
  this.readerActive = false;
12806
13350
  }
12807
13351
  }
12808
- } catch (err) {
13352
+ } catch (error2) {
12809
13353
  if (this.sock === sock && this.generation === generation) {
12810
- this.fail(err instanceof Error ? err : new SubcError(String(err)));
13354
+ this.fail(error2 instanceof Error ? error2 : new SubcError(String(error2)));
12811
13355
  }
12812
13356
  }
12813
13357
  }
12814
13358
  dispatch(frame) {
12815
- 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);
12816
13368
  const pending = this.pending.get(key);
12817
13369
  if (pending) {
13370
+ if (pending.acceptFrame && !pending.acceptFrame(frame))
13371
+ return;
12818
13372
  switch (frame.header.ty) {
12819
13373
  case FrameType.Push:
12820
13374
  case FrameType.StreamData:
@@ -12831,15 +13385,22 @@ class SubcClient {
12831
13385
  return;
12832
13386
  }
12833
13387
  }
12834
- if (frame.header.ty === FrameType.Goodbye) {
12835
- this.failChannel(frame.header.channel, new SubcError("route closed by subc (GOODBYE)"));
12836
- this.evictRouteChannel(frame.header.channel);
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);
12837
13392
  return;
12838
13393
  }
12839
- if (frame.header.ty === FrameType.Response || frame.header.ty === FrameType.Error || frame.header.ty === FrameType.StreamEnd) {
12840
- 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);
12841
13399
  return;
12842
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
+ }
12843
13404
  }
12844
13405
  settle(key, pending, run) {
12845
13406
  if (this.pending.get(key) !== pending)
@@ -12862,18 +13423,16 @@ class SubcClient {
12862
13423
  return new SubcError(Buffer.from(frame.body).toString("utf8") || "subc error");
12863
13424
  }
12864
13425
  }
12865
- evictRouteChannel(channel) {
13426
+ evictRouteHandle(handle) {
12866
13427
  for (const cached of this.routes.values()) {
12867
- if (cached.channel === channel && cached.generation === this.generation) {
12868
- cached.channel = null;
12869
- }
13428
+ if (cached.handle && sameRouteHandle(cached.handle, handle))
13429
+ cached.handle = null;
12870
13430
  }
12871
13431
  }
12872
- failChannel(channel, err) {
13432
+ failHandle(handle, error2) {
12873
13433
  for (const [key, pending] of this.pending) {
12874
- if (pending.channel === channel) {
12875
- this.rejectPending(key, pending, err);
12876
- }
13434
+ if (pending.handle && sameRouteHandle(pending.handle, handle))
13435
+ this.rejectPending(key, pending, error2);
12877
13436
  }
12878
13437
  }
12879
13438
  fail(err) {
@@ -12901,6 +13460,44 @@ class SubcClient {
12901
13460
  return this.notSentCallError(message, cause);
12902
13461
  return this.terminalCallError(message, cause);
12903
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
+ }
12904
13501
  encode(value) {
12905
13502
  return new Uint8Array(Buffer.from(JSON.stringify(value), "utf8"));
12906
13503
  }
@@ -12970,7 +13567,10 @@ function causeMessage(cause) {
12970
13567
  return "";
12971
13568
  return `: ${cause instanceof Error ? cause.message : String(cause)}`;
12972
13569
  }
12973
- // ../../node_modules/.bun/@cortexkit+subc-client@0.3.4/node_modules/@cortexkit/subc-client/dist/provider.js
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
12974
13574
  import { Buffer as Buffer2 } from "node:buffer";
12975
13575
  var DEFAULT_HANDSHAKE_TIMEOUT_MS2 = 1e4;
12976
13576
  var BODY_READ_TIMEOUT_MS2 = 30000;
@@ -13033,6 +13633,11 @@ class SubcProvider {
13033
13633
  closeStarted = false;
13034
13634
  closedErr = null;
13035
13635
  inflight = new Map;
13636
+ pending = new Map;
13637
+ liveRoutes = new Map;
13638
+ connectionToken = newConnectionToken();
13639
+ nextCorr = 1n;
13640
+ ingressEpochDropCount = 0;
13036
13641
  requestGate = new AsyncPermitPool(DEFAULT_PROVIDER_HANDLER_CAPACITY);
13037
13642
  reconnecting = null;
13038
13643
  generation = 1;
@@ -13052,12 +13657,55 @@ class SubcProvider {
13052
13657
  this.readLoop(sock, this.generation);
13053
13658
  this.enqueueConnectionState({ state: "connected", epoch: this.connectionEpoch });
13054
13659
  }
13660
+ get droppedIngressFrames() {
13661
+ return this.ingressEpochDropCount;
13662
+ }
13055
13663
  get conn() {
13056
13664
  return this.currentConn;
13057
13665
  }
13058
13666
  currentEpoch() {
13059
13667
  return this.connectionEpoch;
13060
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
+ }
13061
13709
  static async connect(opts) {
13062
13710
  if (opts.manifest.protocol_ver !== PROTOCOL_VERSION) {
13063
13711
  throw new SubcProviderError(`manifest protocol_ver ${opts.manifest.protocol_ver} does not match client protocol ${PROTOCOL_VERSION}`, "invalid_manifest");
@@ -13072,7 +13720,7 @@ class SubcProvider {
13072
13720
  this.cancelRestoredDebounce();
13073
13721
  const sock = this.sock;
13074
13722
  try {
13075
- await sendFrame(sock, buildFrame(FrameType.Goodbye, controlFlags(), 0, 0n, new Uint8Array(0)));
13723
+ await sendFrame(sock, buildFrame(FrameType.Goodbye, controlFlags(), 0, 0, 0n, new Uint8Array(0)));
13076
13724
  } catch {} finally {
13077
13725
  sock.close();
13078
13726
  this.finishClosed();
@@ -13080,12 +13728,13 @@ class SubcProvider {
13080
13728
  }
13081
13729
  await this.closed;
13082
13730
  }
13083
- static async openConnection(opts) {
13731
+ static async openConnection(opts, onSocket) {
13084
13732
  const conn = await readConnectionFile(opts.connectionFile);
13085
13733
  const deadline = Date.now() + (opts.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS2);
13086
13734
  const endpoint = conn.endpoints[0];
13087
13735
  const sock = await SubcSocket.connect(endpoint.host, endpoint.port, deadline);
13088
13736
  try {
13737
+ onSocket?.(sock);
13089
13738
  await authenticateClient(sock, conn, deadline);
13090
13739
  await sendFrame(sock, buildHelloFrame(opts));
13091
13740
  const ack = await expectHelloAck(sock, deadline);
@@ -13098,19 +13747,17 @@ class SubcProvider {
13098
13747
  async readLoop(sock, generation) {
13099
13748
  try {
13100
13749
  for (;; ) {
13101
- const headerBytes = await sock.readExact(HEADER_LEN, Number.POSITIVE_INFINITY);
13102
- const header = decodeHeader(headerBytes);
13103
- const body = header.len === 0 ? new Uint8Array(0) : await sock.readExact(header.len, Date.now() + BODY_READ_TIMEOUT_MS2);
13104
- 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);
13105
13752
  if (!keepGoing) {
13106
13753
  if (this.sock === sock && this.generation === generation)
13107
13754
  this.closeStarted = true;
13108
13755
  break;
13109
13756
  }
13110
13757
  }
13111
- } catch (err) {
13758
+ } catch (error2) {
13112
13759
  if (this.sock === sock && this.generation === generation && !this.closeStarted) {
13113
- 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)));
13114
13761
  return;
13115
13762
  }
13116
13763
  } finally {
@@ -13122,28 +13769,58 @@ class SubcProvider {
13122
13769
  }
13123
13770
  }
13124
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
+ }
13125
13800
  switch (frame.header.ty) {
13126
13801
  case FrameType.Ping:
13127
13802
  if (frame.header.channel === 0) {
13128
- await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, FrameType.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)));
13129
13804
  }
13130
13805
  return true;
13131
13806
  case FrameType.Goodbye:
13132
- if (frame.header.channel === 0)
13807
+ if (!handle)
13133
13808
  return false;
13134
- this.abortChannel(generation, frame.header.channel);
13135
- await this.opts.onRouteGone?.(frame.header.channel);
13809
+ this.liveRoutes.delete(handle.channel);
13810
+ this.abortHandle(handle);
13811
+ await this.opts.onRouteGone?.(handle);
13136
13812
  return true;
13137
13813
  case FrameType.Cancel:
13138
- this.inflight.get(routeKey(generation, frame.header.channel, frame.header.corr))?.abort();
13814
+ if (handle)
13815
+ this.inflight.get(routeKey(handle, frame.header.corr))?.abort();
13139
13816
  return true;
13140
13817
  case FrameType.Request:
13141
13818
  if (frame.header.channel === 0) {
13142
13819
  await this.handleControlRequest(frame, sock, generation);
13143
- } else {
13144
- this.handleDataRequest(frame, sock, generation).catch((err) => {
13820
+ } else if (handle) {
13821
+ this.handleDataRequest(frame, handle, sock, generation).catch((error2) => {
13145
13822
  if (!this.closeStarted && this.sock === sock && this.generation === generation) {
13146
- console.warn("SubcProvider handler failed after its request was dispatched", err);
13823
+ console.warn("SubcProvider handler failed after its request was dispatched", error2);
13147
13824
  }
13148
13825
  });
13149
13826
  }
@@ -13152,19 +13829,28 @@ class SubcProvider {
13152
13829
  return true;
13153
13830
  }
13154
13831
  }
13155
- abortChannel(generation, channel) {
13156
- const prefix = `${generation}:${channel}:`;
13832
+ abortHandle(handle) {
13833
+ const prefix = `${handle.channel}:${handle.epoch}:`;
13157
13834
  for (const [key, controller] of this.inflight) {
13158
13835
  if (key.startsWith(prefix))
13159
13836
  controller.abort();
13160
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
+ }
13161
13845
  }
13162
- abortGeneration(generation) {
13163
- const prefix = `${generation}:`;
13164
- for (const [key, controller] of this.inflight) {
13165
- if (key.startsWith(prefix))
13166
- 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"));
13167
13852
  }
13853
+ this.liveRoutes.clear();
13168
13854
  }
13169
13855
  abortAllInflight() {
13170
13856
  for (const controller of this.inflight.values())
@@ -13173,9 +13859,9 @@ class SubcProvider {
13173
13859
  async handleControlRequest(frame, sock, generation) {
13174
13860
  const request = parseJson(frame.body);
13175
13861
  if (request.op === HEALTH_CHECK_OP) {
13176
- this.handleHealthRequest(frame, sock, generation).catch((err) => {
13862
+ this.handleHealthRequest(frame, sock, generation).catch((error2) => {
13177
13863
  if (!this.closeStarted && this.sock === sock && this.generation === generation) {
13178
- console.warn("SubcProvider health handler failed after its request was dispatched", err);
13864
+ console.warn("SubcProvider health handler failed after its request was dispatched", error2);
13179
13865
  }
13180
13866
  });
13181
13867
  return;
@@ -13183,47 +13869,93 @@ class SubcProvider {
13183
13869
  if (request.op !== "route.bind") {
13184
13870
  throw new SubcProviderError(`unsupported module control request ${request.op ?? "<missing op>"}`);
13185
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);
13186
13885
  const bindRequest = {
13187
- route_channel: numberField(request.route_channel, "route_channel"),
13886
+ handle: tentative,
13188
13887
  target: request.target,
13189
13888
  identity: request.identity,
13190
- principal: request.principal
13889
+ principal: request.principal,
13890
+ consumer_capabilities: request.consumer_capabilities
13191
13891
  };
13192
- 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
+ }
13193
13903
  const rejection = bindRejection(decision);
13194
13904
  if (rejection) {
13195
- 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);
13196
13920
  return;
13197
13921
  }
13198
- await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, FrameType.Response, controlFlags(), 0, frame.header.corr, encodeJson({ op: "route.bind" })));
13922
+ this.liveRoutes.set(tentative.channel, tentative);
13923
+ await this.opts.onBound?.(tentative);
13199
13924
  }
13200
- async handleDataRequest(frame, sock, generation) {
13201
- const { channel, corr, ver } = frame.header;
13202
- 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);
13203
13928
  const controller = new AbortController;
13204
13929
  this.inflight.set(key, controller);
13205
- const dataFlags = buildFlags(false, Priority.Interactive, false);
13206
- const ctx = {
13930
+ const context = {
13931
+ handle,
13207
13932
  signal: controller.signal,
13208
13933
  currentEpoch: () => this.connectionEpoch,
13209
- emit: async (eventBody) => {
13934
+ emit: async (eventBody, options = {}) => {
13935
+ this.assertLiveHandle(handle);
13210
13936
  if (controller.signal.aborted)
13211
13937
  return;
13212
- await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.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));
13213
13939
  }
13214
13940
  };
13215
13941
  const releasePermit = await (this.requestGate ?? new AsyncPermitPool(DEFAULT_PROVIDER_HANDLER_CAPACITY)).acquire();
13942
+ const dataFlags = buildFlags(false, Priority.Interactive, false);
13216
13943
  try {
13217
- 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);
13218
13948
  if (body === undefined) {
13219
- await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.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)));
13220
13950
  } else if (body instanceof Uint8Array) {
13221
- await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.Response, dataFlags, channel, corr, body));
13951
+ await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.Response, dataFlags, handle.channel, handle.epoch, corr, body));
13222
13952
  } else {
13223
13953
  throw new SubcProviderError("provider handler must return a Uint8Array or void", "invalid_handler_response");
13224
13954
  }
13225
- } catch (err) {
13226
- 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);
13227
13959
  } finally {
13228
13960
  releasePermit();
13229
13961
  if (this.inflight.get(key) === controller)
@@ -13231,8 +13963,8 @@ class SubcProvider {
13231
13963
  }
13232
13964
  }
13233
13965
  async handleHealthRequest(frame, sock, generation) {
13234
- const { channel, corr, ver } = frame.header;
13235
- const key = routeKey(generation, channel, corr);
13966
+ const { corr, ver } = frame.header;
13967
+ const key = `control:${generation}:${corr}`;
13236
13968
  const controller = new AbortController;
13237
13969
  this.inflight.set(key, controller);
13238
13970
  const releasePermit = await (this.requestGate ?? new AsyncPermitPool(DEFAULT_PROVIDER_HANDLER_CAPACITY)).acquire();
@@ -13240,14 +13972,14 @@ class SubcProvider {
13240
13972
  if (controller.signal.aborted)
13241
13973
  return;
13242
13974
  const report = await this.opts.health();
13243
- await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.Response, controlFlags(), channel, corr, encodeJson({
13975
+ await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.Response, controlFlags(), 0, 0, corr, encodeJson({
13244
13976
  op: HEALTH_CHECK_OP,
13245
13977
  status: report.status,
13246
13978
  ...report.detail === undefined ? {} : { detail: report.detail },
13247
13979
  ...report.metrics === undefined ? {} : { metrics: report.metrics }
13248
13980
  })));
13249
- } catch (err) {
13250
- await this.sendError(frame, err instanceof SubcProviderError && err.code ? err.code : "health_error", err instanceof Error ? err.message : String(err), controlFlags(), sock, generation);
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);
13251
13983
  } finally {
13252
13984
  releasePermit();
13253
13985
  if (this.inflight.get(key) === controller)
@@ -13255,7 +13987,7 @@ class SubcProvider {
13255
13987
  }
13256
13988
  }
13257
13989
  async sendError(frame, code, message, flags, sock, generation) {
13258
- await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, FrameType.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 })));
13259
13991
  }
13260
13992
  async sendOn(sock, generation, frame) {
13261
13993
  if (this.sock !== sock || this.generation !== generation || this.closeStarted || this.closedErr)
@@ -13263,42 +13995,79 @@ class SubcProvider {
13263
13995
  await sendFrame(sock, frame);
13264
13996
  }
13265
13997
  handleUnexpectedDrop(sock, generation, cause) {
13998
+ if (this.closeStarted || this.sock !== sock || this.generation !== generation)
13999
+ return;
13266
14000
  this.cancelRestoredDebounce();
13267
14001
  this.abortGeneration(generation);
13268
14002
  this.generation += 1;
13269
14003
  sock.close();
13270
- this.enqueueConnectionState({ state: "down", cause });
13271
- this.scheduleReconnectAfterDrop(cause);
14004
+ this.scheduleReconnectAfterDrop(cause, this.generation, sock);
13272
14005
  }
13273
- scheduleReconnectAfterDrop(trigger) {
13274
- if (this.closeStarted || this.reconnecting)
14006
+ scheduleReconnectAfterDrop(cause, generation, droppedSocket) {
14007
+ if (this.closeStarted)
13275
14008
  return;
13276
- const promise = this.reconnectWithRetry(trigger).catch((err) => {
13277
- 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) {
13278
14026
  this.failFatal(err instanceof Error ? err : new SubcProviderError(String(err)));
14027
+ }
13279
14028
  }).finally(() => {
13280
- if (this.reconnecting === promise)
14029
+ if (this.isCurrentReconnect(cycle))
13281
14030
  this.reconnecting = null;
13282
14031
  });
13283
- this.reconnecting = promise;
13284
14032
  }
13285
- async reconnectWithRetry(_trigger) {
14033
+ async reconnectWithRetry(cycle) {
13286
14034
  let attempt = 0;
13287
14035
  let delay = this.opts.reconnectBackoff.baseMs;
13288
14036
  for (;; ) {
14037
+ if (!this.isCurrentReconnect(cycle))
14038
+ return;
13289
14039
  if (this.closeStarted)
13290
14040
  throw new SubcProviderError("provider closed");
14041
+ cycle.socket = null;
14042
+ cycle.socketDied = false;
13291
14043
  attempt += 1;
13292
- this.enqueueConnectionState({ state: "reconnecting", attempt });
14044
+ this.enqueueReconnectState(cycle, attempt);
13293
14045
  try {
13294
- const opened = await SubcProvider.openConnection(this.opts);
13295
- 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) {
13296
14054
  opened.sock.close();
13297
- throw new SubcProviderError("provider closed");
14055
+ if (this.closeStarted)
14056
+ throw new SubcProviderError("provider closed");
14057
+ return;
13298
14058
  }
13299
- 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);
13300
14065
  return;
13301
14066
  } catch (err) {
14067
+ if (!this.isCurrentReconnect(cycle))
14068
+ return;
14069
+ if (cycle.socket)
14070
+ cycle.socketDied = true;
13302
14071
  if (this.closeStarted)
13303
14072
  throw err;
13304
14073
  if (!isProviderReconnectTransient(err))
@@ -13308,16 +14077,29 @@ class SubcProvider {
13308
14077
  }
13309
14078
  }
13310
14079
  }
13311
- 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) {
13312
14092
  this.sock.close();
13313
14093
  this.sock = opened.sock;
13314
14094
  this.currentConn = opened.conn;
13315
14095
  this.storage = opened.ack.storage;
13316
14096
  this.closedErr = null;
13317
14097
  this.connectionEpoch += 1;
13318
- const generation = this.generation;
14098
+ this.connectionToken = newConnectionToken();
14099
+ this.liveRoutes.clear();
14100
+ this.nextCorr = 1n;
13319
14101
  this.readLoop(opened.sock, generation);
13320
- this.scheduleRestored(generation, this.connectionEpoch);
14102
+ return this.connectionEpoch;
13321
14103
  }
13322
14104
  scheduleRestored(generation, epoch) {
13323
14105
  if (!this.opts.onConnectionState)
@@ -13325,7 +14107,7 @@ class SubcProvider {
13325
14107
  const token = ++this.restoredDebounceToken;
13326
14108
  this.opts.sleep(this.opts.restoredDebounceMs).then(() => {
13327
14109
  if (token === this.restoredDebounceToken && !this.closeStarted && this.sock && this.generation === generation && this.connectionEpoch === epoch) {
13328
- this.enqueueConnectionState({ state: "restored", epoch });
14110
+ this.enqueueConnectionState({ state: "restored", epoch }, generation);
13329
14111
  }
13330
14112
  }).catch((err) => {
13331
14113
  if (token === this.restoredDebounceToken && !this.closeStarted) {
@@ -13336,10 +14118,10 @@ class SubcProvider {
13336
14118
  cancelRestoredDebounce() {
13337
14119
  this.restoredDebounceToken += 1;
13338
14120
  }
13339
- enqueueConnectionState(event) {
14121
+ enqueueConnectionState(event, generation, reconnect) {
13340
14122
  if (!this.opts.onConnectionState)
13341
14123
  return;
13342
- this.stateQueue.push(event);
14124
+ this.stateQueue.push({ event, generation, reconnect });
13343
14125
  if (!this.drainingStateQueue)
13344
14126
  this.drainConnectionStateQueue();
13345
14127
  }
@@ -13349,7 +14131,12 @@ class SubcProvider {
13349
14131
  this.drainingStateQueue = true;
13350
14132
  try {
13351
14133
  while (this.stateQueue.length > 0) {
13352
- 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;
13353
14140
  try {
13354
14141
  await this.opts.onConnectionState?.(event);
13355
14142
  this.stateQueue.shift();
@@ -13369,6 +14156,24 @@ class SubcProvider {
13369
14156
  this.drainConnectionStateQueue();
13370
14157
  }
13371
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
+ }
13372
14177
  failFatal(err) {
13373
14178
  if (!this.closedErr)
13374
14179
  this.closedErr = err;
@@ -13382,8 +14187,16 @@ class SubcProvider {
13382
14187
  this.resolveClosed();
13383
14188
  }
13384
14189
  }
13385
- function routeKey(generation, channel, corr) {
13386
- 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
+ }
13387
14200
  }
13388
14201
  function launchNonce(opts) {
13389
14202
  const nonce = opts.launchNonce ?? process.env[SUBC_LAUNCH_NONCE_ENV2];
@@ -13399,6 +14212,7 @@ function normalizeProviderConnectOptions(opts) {
13399
14212
  handshakeTimeoutMs: opts.handshakeTimeoutMs,
13400
14213
  controlOps: opts.controlOps,
13401
14214
  onBind: opts.onBind,
14215
+ onBound: opts.onBound,
13402
14216
  onRouteGone: opts.onRouteGone,
13403
14217
  reconnectBackoff: opts.reconnectBackoff ?? DEFAULT_RECONNECT_BACKOFF,
13404
14218
  sleep: opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms))),
@@ -13416,7 +14230,7 @@ function normalizedControlOps(controlOps) {
13416
14230
  }
13417
14231
  function buildHelloFrame(opts) {
13418
14232
  const nonce = launchNonce(opts);
13419
- return buildFrame(FrameType.Hello, controlFlags(), 0, HELLO_CORR, encodeJson({
14233
+ return buildFrame(FrameType.Hello, controlFlags(), 0, 0, HELLO_CORR, encodeJson({
13420
14234
  manifest: normalizeManifest(opts.manifest),
13421
14235
  protocol_ver: PROTOCOL_VERSION,
13422
14236
  control_ops: normalizedControlOps(opts.controlOps),
@@ -13455,14 +14269,17 @@ async function sendFrame(sock, frame) {
13455
14269
  await sock.write(encodeFrame(frame), Date.now() + WRITE_TIMEOUT_MS);
13456
14270
  }
13457
14271
  async function expectHelloAck(sock, deadline) {
13458
- const header = decodeHeader(await sock.readExact(HEADER_LEN, deadline));
13459
- const body = header.len === 0 ? new Uint8Array(0) : await sock.readExact(header.len, deadline);
13460
- const frame = { header, body };
13461
- switch (header.ty) {
13462
- case FrameType.HelloAck:
13463
- return 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
+ }
13464
14281
  case FrameType.Error: {
13465
- const error2 = parseJson(body);
14282
+ const error2 = parseJson(frame.body);
13466
14283
  throw new SubcProviderError(`subc rejected HELLO: ${error2.code ?? "unknown"} — ${error2.message ?? "subc error"}`, error2.code);
13467
14284
  }
13468
14285
  default:
@@ -13617,15 +14434,17 @@ class BgSubscription {
13617
14434
  identity;
13618
14435
  acquireClient;
13619
14436
  dropClient;
14437
+ consumerIdentity;
13620
14438
  onNudge;
13621
14439
  sleep;
13622
14440
  stopped = false;
13623
14441
  current = null;
13624
14442
  loop;
13625
- constructor(identity, acquireClient, dropClient, onNudge, sleep2) {
14443
+ constructor(identity, acquireClient, dropClient, consumerIdentity, onNudge, sleep2) {
13626
14444
  this.identity = identity;
13627
14445
  this.acquireClient = acquireClient;
13628
14446
  this.dropClient = dropClient;
14447
+ this.consumerIdentity = consumerIdentity;
13629
14448
  this.onNudge = onNudge;
13630
14449
  this.sleep = sleep2;
13631
14450
  this.loop = this.run();
@@ -13654,9 +14473,9 @@ class BgSubscription {
13654
14473
  }
13655
14474
  if (this.stopped)
13656
14475
  return;
13657
- let channel;
14476
+ let route;
13658
14477
  try {
13659
- 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 });
13660
14479
  } catch (err) {
13661
14480
  if (isConsumerReconnectTransient(err))
13662
14481
  this.dropClient(client);
@@ -13664,12 +14483,12 @@ class BgSubscription {
13664
14483
  continue;
13665
14484
  }
13666
14485
  if (this.stopped) {
13667
- safeCloseRoute(client, channel);
14486
+ safeCloseRoute(client, route);
13668
14487
  return;
13669
14488
  }
13670
14489
  const subscribedAt = Date.now();
13671
14490
  try {
13672
- const sub = client.subscribe(channel, { op: "bg_events" }, () => {
14491
+ const sub = client.subscribe(route, { op: "bg_events" }, () => {
13673
14492
  if (!this.stopped)
13674
14493
  this.onNudge();
13675
14494
  });
@@ -13689,7 +14508,7 @@ class BgSubscription {
13689
14508
  attempt = 0;
13690
14509
  } finally {
13691
14510
  this.current = null;
13692
- safeCloseRoute(client, channel);
14511
+ safeCloseRoute(client, route);
13693
14512
  }
13694
14513
  await this.backoff(attempt++);
13695
14514
  }
@@ -13702,12 +14521,12 @@ class BgSubscription {
13702
14521
  function isRecord(value) {
13703
14522
  return typeof value === "object" && value !== null && !Array.isArray(value);
13704
14523
  }
13705
- function isUnknownChannelError(err) {
13706
- return err instanceof SubcError && err.code === "unknown_channel";
14524
+ function isRouteProvenAbsentError(err) {
14525
+ return err instanceof SubcError && err.code === "unknown_channel" || err instanceof StaleRouteHandleError;
13707
14526
  }
13708
- function safeCloseRoute(client, channel) {
14527
+ function safeCloseRoute(client, route) {
13709
14528
  try {
13710
- client.closeRouteChannel(channel).catch(() => {
14529
+ client.closeRouteChannel(route).catch(() => {
13711
14530
  return;
13712
14531
  });
13713
14532
  } catch {}
@@ -13794,6 +14613,7 @@ class SubcTransportPool {
13794
14613
  harness;
13795
14614
  connectionFile;
13796
14615
  handshakeTimeoutMs;
14616
+ consumerIdentity;
13797
14617
  connectFn;
13798
14618
  onBgEventsNudge;
13799
14619
  bgBackoffSleep;
@@ -13807,6 +14627,7 @@ class SubcTransportPool {
13807
14627
  this.connectionFile = options.connectionFile;
13808
14628
  this.harness = options.harness;
13809
14629
  this.handshakeTimeoutMs = options.handshakeTimeoutMs;
14630
+ this.consumerIdentity = options.consumerIdentity;
13810
14631
  this.connectFn = options.connect ?? ((opts) => SubcClient.connect(opts));
13811
14632
  this.onBgEventsNudge = options.onBgEventsNudge;
13812
14633
  this.bgBackoffSleep = options.bgBackoffSleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
@@ -13829,6 +14650,11 @@ class SubcTransportPool {
13829
14650
  return null;
13830
14651
  return this.transports.get(key) ?? null;
13831
14652
  }
14653
+ activeBridges() {
14654
+ if (!this.client || this.shuttingDown)
14655
+ return [];
14656
+ return [...this.transports.values()];
14657
+ }
13832
14658
  async toolCall(projectRoot, runtime, name, rawArgs = {}, options) {
13833
14659
  return this.getBridge(projectRoot).toolCall(runtime.sessionID, name, rawArgs, options);
13834
14660
  }
@@ -13859,7 +14685,7 @@ class SubcTransportPool {
13859
14685
  throw new RouteTornDownError("subc session closed");
13860
14686
  }
13861
14687
  try {
13862
- const opened = await this.routeChannel(client, identity, record);
14688
+ const opened = await this.routeHandle(client, identity, record);
13863
14689
  if (!this.isCurrentSession(key, record)) {
13864
14690
  throw new RouteTornDownError("subc session closed");
13865
14691
  }
@@ -13885,29 +14711,29 @@ class SubcTransportPool {
13885
14711
  if (isConsumerReconnectTransient(err)) {
13886
14712
  this.transportFailures = 0;
13887
14713
  this.dropClient(client);
13888
- } else if (!isUnknownChannelError(err) && ++this.transportFailures >= MAX_CONSECUTIVE_TRANSPORT_FAILURES) {
14714
+ } else if (!isRouteProvenAbsentError(err) && ++this.transportFailures >= MAX_CONSECUTIVE_TRANSPORT_FAILURES) {
13889
14715
  this.transportFailures = 0;
13890
14716
  this.dropClient(client);
13891
14717
  }
13892
14718
  }
13893
14719
  };
13894
- const requestOnRoute = async (channel2) => {
13895
- const reply = await client.request(channel2, body, { timeoutMs, onProgress });
14720
+ const requestOnRoute = async (route2) => {
14721
+ const reply = await client.request(route2, body, { timeoutMs, onProgress });
13896
14722
  if (this.isCurrentSession(key, record) && this.client === client) {
13897
14723
  this.transportFailures = 0;
13898
14724
  }
13899
14725
  this.ensureBgSubscription(identity, record);
13900
14726
  return reply;
13901
14727
  };
13902
- let { channel, entry } = await openRoute();
14728
+ let { route, entry } = await openRoute();
13903
14729
  try {
13904
- return await requestOnRoute(channel);
14730
+ return await requestOnRoute(route);
13905
14731
  } catch (err) {
13906
- if (isUnknownChannelError(err) && this.isCurrentSession(key, record) && this.client === client) {
14732
+ if (isRouteProvenAbsentError(err) && this.isCurrentSession(key, record) && this.client === client) {
13907
14733
  clearRouteEntry(entry);
13908
- ({ channel, entry } = await openRoute());
14734
+ ({ route, entry } = await openRoute());
13909
14735
  try {
13910
- return await requestOnRoute(channel);
14736
+ return await requestOnRoute(route);
13911
14737
  } catch (retryErr) {
13912
14738
  handleRequestFailure(retryErr, entry);
13913
14739
  throw retryErr;
@@ -13949,24 +14775,26 @@ class SubcTransportPool {
13949
14775
  });
13950
14776
  return this.connecting;
13951
14777
  }
13952
- async routeChannel(client, identity, record) {
14778
+ async routeHandle(client, identity, record) {
13953
14779
  const key = identityKey(identity);
13954
14780
  const existing = record.routeEntry;
13955
- if (existing?.channel != null)
13956
- return { channel: existing.channel, entry: existing };
14781
+ if (existing?.handle != null)
14782
+ return { route: existing.handle, entry: existing };
13957
14783
  if (existing?.opening)
13958
- return { channel: await existing.opening, entry: existing };
13959
- const entry = { client, opening: null, channel: null, closed: false };
13960
- 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) => {
13961
14789
  if (!this.isCurrentSession(key, record) || record.routeEntry !== entry || entry.closed || this.client !== client) {
13962
- safeCloseRoute(client, channel);
14790
+ safeCloseRoute(client, route);
13963
14791
  if (record.routeEntry === entry)
13964
14792
  record.routeEntry = null;
13965
14793
  throw new RouteTornDownError("subc route opened after teardown");
13966
14794
  }
13967
- entry.channel = channel;
14795
+ entry.handle = route;
13968
14796
  entry.opening = null;
13969
- return channel;
14797
+ return route;
13970
14798
  }).catch((err) => {
13971
14799
  const current = this.isCurrentSession(key, record);
13972
14800
  if (record.routeEntry === entry) {
@@ -13980,7 +14808,7 @@ class SubcTransportPool {
13980
14808
  });
13981
14809
  entry.opening = opening;
13982
14810
  record.routeEntry = entry;
13983
- return { channel: await opening, entry };
14811
+ return { route: await opening, entry };
13984
14812
  }
13985
14813
  ensureBgSubscription(identity, record) {
13986
14814
  if (this.shuttingDown || !this.onBgEventsNudge)
@@ -13991,7 +14819,7 @@ class SubcTransportPool {
13991
14819
  if (record.bgSub)
13992
14820
  return;
13993
14821
  const onNudge = () => this.onBgEventsNudge?.(identity.project_root, identity.session);
13994
- 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);
13995
14823
  record.bgSub = sub;
13996
14824
  }
13997
14825
  dropClient(client) {
@@ -14012,9 +14840,13 @@ class SubcTransportPool {
14012
14840
  }
14013
14841
  }
14014
14842
  setConfigureOverride(_key, _value) {}
14843
+ async reconfigure(_projectRoot, _overrides) {}
14015
14844
  async replaceBinary(path2) {
14016
14845
  return path2;
14017
14846
  }
14847
+ isShutdown() {
14848
+ return this.shuttingDown;
14849
+ }
14018
14850
  async shutdown() {
14019
14851
  this.shuttingDown = true;
14020
14852
  const subs = [];
@@ -14038,10 +14870,10 @@ class SubcTransportPool {
14038
14870
  this.transports.clear();
14039
14871
  await Promise.allSettled(subs.map((sub) => sub.stop()));
14040
14872
  await Promise.allSettled(entries.map(async (entry) => {
14041
- if (entry.channel == null)
14873
+ if (entry.handle == null)
14042
14874
  return;
14043
14875
  try {
14044
- await entry.client.closeRouteChannel(entry.channel);
14876
+ await entry.client.closeRouteChannel(entry.handle);
14045
14877
  } catch {}
14046
14878
  }));
14047
14879
  if (client) {
@@ -14070,9 +14902,9 @@ class SubcTransportPool {
14070
14902
  entry.closed = true;
14071
14903
  if (sub)
14072
14904
  await sub.stop();
14073
- if (entry?.channel != null) {
14905
+ if (entry?.handle != null) {
14074
14906
  try {
14075
- await entry.client.closeRouteChannel(entry.channel);
14907
+ await entry.client.closeRouteChannel(entry.handle);
14076
14908
  } catch {}
14077
14909
  }
14078
14910
  }
@@ -14149,18 +14981,25 @@ function collectStructuredExtras(response) {
14149
14981
  return stringifyData(extras);
14150
14982
  }
14151
14983
  // ../aft-bridge/dist/transport-factory.js
14152
- import { homedir as homedir10 } from "node:os";
14153
- 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";
14154
14986
  function resolveConnectionFilePath(raw) {
14155
14987
  const trimmed = raw.trim();
14156
14988
  if (trimmed.startsWith("~")) {
14157
- return join9(homedir10(), trimmed.slice(1).replace(/^[/\\]/, ""));
14989
+ return join10(homedir11(), trimmed.slice(1).replace(/^[/\\]/, ""));
14158
14990
  }
14159
14991
  if (isAbsolute5(trimmed))
14160
14992
  return trimmed;
14161
- return join9(homedir10(), trimmed);
14993
+ return join10(homedir11(), trimmed);
14162
14994
  }
14163
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) {
14164
15003
  const raw = opts.subcConnectionFile?.trim();
14165
15004
  if (raw && raw.length > 0) {
14166
15005
  const connectionFile = resolveConnectionFilePath(raw);
@@ -14171,18 +15010,17 @@ async function createAftTransportPool(opts) {
14171
15010
  return new SubcTransportPool({
14172
15011
  connectionFile,
14173
15012
  harness: opts.harness,
15013
+ consumerIdentity: opts.subcConsumerIdentity,
14174
15014
  onBgEventsNudge: opts.onBgEventsNudge
14175
15015
  });
14176
15016
  }
14177
15017
  return new BridgePool(opts.binaryPath, opts.poolOptions, opts.configOverrides);
14178
15018
  }
14179
15019
  // src/logger.ts
14180
- import * as fs3 from "node:fs";
14181
- import * as os2 from "node:os";
14182
- import * as path2 from "node:path";
14183
15020
  var TAG = "[aft-pi]";
14184
15021
  var isTestEnv = process.env.BUN_TEST === "1" || false;
14185
- 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);
14186
15024
  var useStderr = process.env.AFT_LOG_STDERR === "1";
14187
15025
  var buffer = [];
14188
15026
  var flushTimer = null;
@@ -14197,7 +15035,7 @@ function flush() {
14197
15035
  if (useStderr) {
14198
15036
  process.stderr.write(data);
14199
15037
  } else {
14200
- fs3.appendFileSync(logFile, data);
15038
+ fileSink.append(data);
14201
15039
  }
14202
15040
  } catch {}
14203
15041
  }
@@ -14910,7 +15748,7 @@ import {
14910
15748
  // package.json
14911
15749
  var package_default = {
14912
15750
  name: "@cortexkit/aft-pi",
14913
- version: "0.46.0",
15751
+ version: "0.47.0",
14914
15752
  type: "module",
14915
15753
  description: "Pi coding agent extension for Agent File Tools (AFT) — tree-sitter and LSP-powered code analysis",
14916
15754
  main: "dist/index.js",
@@ -14933,19 +15771,19 @@ var package_default = {
14933
15771
  "test:unit": "bun test src/__tests__ --path-ignore-patterns 'src/__tests__/e2e/**'"
14934
15772
  },
14935
15773
  dependencies: {
14936
- "@cortexkit/aft-bridge": "0.46.0",
15774
+ "@cortexkit/aft-bridge": "0.47.0",
14937
15775
  "comment-json": "^5.0.0",
14938
15776
  diff: "^8.0.4",
14939
15777
  typebox: "1.1.38",
14940
15778
  zod: "^4.4.3"
14941
15779
  },
14942
15780
  optionalDependencies: {
14943
- "@cortexkit/aft-darwin-arm64": "0.46.0",
14944
- "@cortexkit/aft-darwin-x64": "0.46.0",
14945
- "@cortexkit/aft-linux-arm64": "0.46.0",
14946
- "@cortexkit/aft-linux-x64": "0.46.0",
14947
- "@cortexkit/aft-win32-arm64": "0.46.0",
14948
- "@cortexkit/aft-win32-x64": "0.46.0"
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"
14949
15787
  },
14950
15788
  devDependencies: {
14951
15789
  "@earendil-works/pi-coding-agent": "^0.80.2",
@@ -15176,9 +16014,15 @@ function formatStatusDialogMessage(status) {
15176
16014
  }
15177
16015
 
15178
16016
  // src/tools/_shared.ts
16017
+ import { existsSync as existsSync7 } from "node:fs";
15179
16018
  import { Type } from "typebox";
15180
- 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
+ }));
15181
16022
  function bridgeFor(ctx, cwd) {
16023
+ if (!existsSync7(cwd)) {
16024
+ throw new Error(`project directory no longer exists: ${cwd} (stale restored session?)`);
16025
+ }
15182
16026
  return ctx.pool.getBridge(cwd);
15183
16027
  }
15184
16028
  function resolveSessionId(extCtx) {
@@ -15534,7 +16378,7 @@ function registerStatusCommand(pi, ctx) {
15534
16378
  }
15535
16379
 
15536
16380
  // src/config.ts
15537
- 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";
15538
16382
  var import_comment_json = __toESM(require_src2(), 1);
15539
16383
 
15540
16384
  // ../../node_modules/.bun/zod@4.4.3/node_modules/zod/v4/classic/external.js
@@ -16301,10 +17145,10 @@ function mergeDefs(...defs) {
16301
17145
  function cloneDef(schema) {
16302
17146
  return mergeDefs(schema._zod.def);
16303
17147
  }
16304
- function getElementAtPath(obj, path3) {
16305
- if (!path3)
17148
+ function getElementAtPath(obj, path2) {
17149
+ if (!path2)
16306
17150
  return obj;
16307
- return path3.reduce((acc, key) => acc?.[key], obj);
17151
+ return path2.reduce((acc, key) => acc?.[key], obj);
16308
17152
  }
16309
17153
  function promiseAllObject(promisesObj) {
16310
17154
  const keys = Object.keys(promisesObj);
@@ -16712,11 +17556,11 @@ function explicitlyAborted(x, startIndex = 0) {
16712
17556
  }
16713
17557
  return false;
16714
17558
  }
16715
- function prefixIssues(path3, issues) {
17559
+ function prefixIssues(path2, issues) {
16716
17560
  return issues.map((iss) => {
16717
17561
  var _a2;
16718
17562
  (_a2 = iss).path ?? (_a2.path = []);
16719
- iss.path.unshift(path3);
17563
+ iss.path.unshift(path2);
16720
17564
  return iss;
16721
17565
  });
16722
17566
  }
@@ -16863,16 +17707,16 @@ function flattenError(error3, mapper = (issue2) => issue2.message) {
16863
17707
  }
16864
17708
  function formatError(error3, mapper = (issue2) => issue2.message) {
16865
17709
  const fieldErrors = { _errors: [] };
16866
- const processError = (error4, path3 = []) => {
17710
+ const processError = (error4, path2 = []) => {
16867
17711
  for (const issue2 of error4.issues) {
16868
17712
  if (issue2.code === "invalid_union" && issue2.errors.length) {
16869
- issue2.errors.map((issues) => processError({ issues }, [...path3, ...issue2.path]));
17713
+ issue2.errors.map((issues) => processError({ issues }, [...path2, ...issue2.path]));
16870
17714
  } else if (issue2.code === "invalid_key") {
16871
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
17715
+ processError({ issues: issue2.issues }, [...path2, ...issue2.path]);
16872
17716
  } else if (issue2.code === "invalid_element") {
16873
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
17717
+ processError({ issues: issue2.issues }, [...path2, ...issue2.path]);
16874
17718
  } else {
16875
- const fullpath = [...path3, ...issue2.path];
17719
+ const fullpath = [...path2, ...issue2.path];
16876
17720
  if (fullpath.length === 0) {
16877
17721
  fieldErrors._errors.push(mapper(issue2));
16878
17722
  } else {
@@ -16899,17 +17743,17 @@ function formatError(error3, mapper = (issue2) => issue2.message) {
16899
17743
  }
16900
17744
  function treeifyError(error3, mapper = (issue2) => issue2.message) {
16901
17745
  const result = { errors: [] };
16902
- const processError = (error4, path3 = []) => {
17746
+ const processError = (error4, path2 = []) => {
16903
17747
  var _a2, _b;
16904
17748
  for (const issue2 of error4.issues) {
16905
17749
  if (issue2.code === "invalid_union" && issue2.errors.length) {
16906
- issue2.errors.map((issues) => processError({ issues }, [...path3, ...issue2.path]));
17750
+ issue2.errors.map((issues) => processError({ issues }, [...path2, ...issue2.path]));
16907
17751
  } else if (issue2.code === "invalid_key") {
16908
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
17752
+ processError({ issues: issue2.issues }, [...path2, ...issue2.path]);
16909
17753
  } else if (issue2.code === "invalid_element") {
16910
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
17754
+ processError({ issues: issue2.issues }, [...path2, ...issue2.path]);
16911
17755
  } else {
16912
- const fullpath = [...path3, ...issue2.path];
17756
+ const fullpath = [...path2, ...issue2.path];
16913
17757
  if (fullpath.length === 0) {
16914
17758
  result.errors.push(mapper(issue2));
16915
17759
  continue;
@@ -16941,8 +17785,8 @@ function treeifyError(error3, mapper = (issue2) => issue2.message) {
16941
17785
  }
16942
17786
  function toDotPath(_path) {
16943
17787
  const segs = [];
16944
- const path3 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
16945
- for (const seg of path3) {
17788
+ const path2 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
17789
+ for (const seg of path2) {
16946
17790
  if (typeof seg === "number")
16947
17791
  segs.push(`[${seg}]`);
16948
17792
  else if (typeof seg === "symbol")
@@ -29401,13 +30245,13 @@ function resolveRef(ref, ctx) {
29401
30245
  if (!ref.startsWith("#")) {
29402
30246
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
29403
30247
  }
29404
- const path3 = ref.slice(1).split("/").filter(Boolean);
29405
- if (path3.length === 0) {
30248
+ const path2 = ref.slice(1).split("/").filter(Boolean);
30249
+ if (path2.length === 0) {
29406
30250
  return ctx.rootSchema;
29407
30251
  }
29408
30252
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
29409
- if (path3[0] === defsKey) {
29410
- const key = path3[1];
30253
+ if (path2[0] === defsKey) {
30254
+ const key = path2[1];
29411
30255
  if (!key || !ctx.defs[key]) {
29412
30256
  throw new Error(`Reference not found: ${ref}`);
29413
30257
  }
@@ -29893,6 +30737,7 @@ var SemanticConfigSchema = exports_external.object({
29893
30737
  base_url: exports_external.string().trim().min(1).optional(),
29894
30738
  api_key_env: exports_external.string().trim().min(1).optional(),
29895
30739
  timeout_ms: exports_external.number().int().positive().optional(),
30740
+ query_timeout_ms: exports_external.number().int().positive().optional(),
29896
30741
  max_batch_size: exports_external.number().int().positive().optional(),
29897
30742
  max_files: exports_external.number().int().positive().optional()
29898
30743
  });
@@ -29958,7 +30803,7 @@ var BackupConfigSchema = exports_external.object({
29958
30803
  max_depth: exports_external.number().int().positive().optional(),
29959
30804
  max_file_size: exports_external.number().int().positive().optional()
29960
30805
  });
29961
- var AftConfigSchema = exports_external.object({
30806
+ var AftConfigSchema = exports_external.preprocess((value) => stripHarnessSpecificConfigKeys(value, OPENCODE_ONLY_KEYS), exports_external.object({
29962
30807
  $schema: exports_external.string().optional(),
29963
30808
  enabled: exports_external.boolean().optional(),
29964
30809
  format_on_edit: exports_external.boolean().optional(),
@@ -29983,7 +30828,7 @@ var AftConfigSchema = exports_external.object({
29983
30828
  semantic: SemanticConfigSchema.optional(),
29984
30829
  bridge: BridgeConfigSchema.optional(),
29985
30830
  subc: SubcConfigSchema.optional()
29986
- }).strict();
30831
+ }).strict());
29987
30832
  var CONFIG_MIGRATIONS = [
29988
30833
  { oldKey: "experimental_search_index", newPath: ["search_index"] },
29989
30834
  { oldKey: "experimental_semantic_search", newPath: ["semantic_search"] },
@@ -30008,9 +30853,9 @@ function extractCommentsForPreservation(content) {
30008
30853
  }
30009
30854
  return comments;
30010
30855
  }
30011
- function ensureRecordAtPath(root, path3) {
30856
+ function ensureRecordAtPath(root, path2) {
30012
30857
  let current = root;
30013
- for (const segment of path3) {
30858
+ for (const segment of path2) {
30014
30859
  const existing = current[segment];
30015
30860
  if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
30016
30861
  current[segment] = {};
@@ -30019,9 +30864,9 @@ function ensureRecordAtPath(root, path3) {
30019
30864
  }
30020
30865
  return current;
30021
30866
  }
30022
- function hasPath(root, path3) {
30867
+ function hasPath(root, path2) {
30023
30868
  let current = root;
30024
- for (const segment of path3) {
30869
+ for (const segment of path2) {
30025
30870
  if (!current || typeof current !== "object" || Array.isArray(current))
30026
30871
  return false;
30027
30872
  const record2 = current;
@@ -30031,9 +30876,9 @@ function hasPath(root, path3) {
30031
30876
  }
30032
30877
  return true;
30033
30878
  }
30034
- function setPath(root, path3, value) {
30035
- const parent = ensureRecordAtPath(root, path3.slice(0, -1));
30036
- 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;
30037
30882
  }
30038
30883
  function migrateRawConfig(rawConfig, configPath, logger) {
30039
30884
  const oldKeys = [];
@@ -30094,7 +30939,7 @@ function migrateExperimentalBashBlock(rawConfig, configPath, logger) {
30094
30939
  return movedKeys;
30095
30940
  }
30096
30941
  function migrateAftConfigFile2(configPath, logger = { log: log2, warn: warn2 }) {
30097
- if (!existsSync7(configPath)) {
30942
+ if (!existsSync8(configPath)) {
30098
30943
  return { migrated: false, oldKeys: [] };
30099
30944
  }
30100
30945
  let tmpPath = null;
@@ -30147,7 +30992,7 @@ function recordConfigParseFailure(configPath, errorMessage) {
30147
30992
  }
30148
30993
  function loadConfigFromPath(configPath) {
30149
30994
  try {
30150
- if (!existsSync7(configPath))
30995
+ if (!existsSync8(configPath))
30151
30996
  return null;
30152
30997
  const content = readFileSync7(configPath, "utf-8");
30153
30998
  const rawConfig = import_comment_json.parse(content);
@@ -30428,7 +31273,7 @@ import { spawn as spawn2 } from "node:child_process";
30428
31273
  import { createHash as createHash4 } from "node:crypto";
30429
31274
  import {
30430
31275
  createReadStream,
30431
- existsSync as existsSync9,
31276
+ existsSync as existsSync10,
30432
31277
  mkdirSync as mkdirSync7,
30433
31278
  readFileSync as readFileSync10,
30434
31279
  renameSync as renameSync6,
@@ -30448,7 +31293,7 @@ import {
30448
31293
  unlinkSync as unlinkSync6,
30449
31294
  writeFileSync as writeFileSync5
30450
31295
  } from "node:fs";
30451
- import { homedir as homedir11 } from "node:os";
31296
+ import { homedir as homedir12 } from "node:os";
30452
31297
  import { join as join11 } from "node:path";
30453
31298
  function aftCacheBase() {
30454
31299
  const override = process.env.AFT_CACHE_DIR;
@@ -30456,10 +31301,10 @@ function aftCacheBase() {
30456
31301
  return override;
30457
31302
  if (process.platform === "win32") {
30458
31303
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
30459
- const base2 = localAppData || join11(homedir11(), "AppData", "Local");
31304
+ const base2 = localAppData || join11(homedir12(), "AppData", "Local");
30460
31305
  return join11(base2, "aft");
30461
31306
  }
30462
- const base = process.env.XDG_CACHE_HOME || join11(homedir11(), ".cache");
31307
+ const base = process.env.XDG_CACHE_HOME || join11(homedir12(), ".cache");
30463
31308
  return join11(base, "aft");
30464
31309
  }
30465
31310
  function lspCacheRoot() {
@@ -30503,11 +31348,11 @@ function writeInstalledMetaIn(installDir, version2, sha256) {
30503
31348
  }
30504
31349
  }
30505
31350
  function readInstalledMetaIn(installDir) {
30506
- const path3 = join11(installDir, INSTALLED_META_FILE);
31351
+ const path2 = join11(installDir, INSTALLED_META_FILE);
30507
31352
  try {
30508
- if (!statSync5(path3).isFile())
31353
+ if (!statSync5(path2).isFile())
30509
31354
  return null;
30510
- const raw = readFileSync8(path3, "utf8");
31355
+ const raw = readFileSync8(path2, "utf8");
30511
31356
  const parsed = JSON.parse(raw);
30512
31357
  if (typeof parsed.version !== "string" || parsed.version.length === 0)
30513
31358
  return null;
@@ -30813,7 +31658,7 @@ var NPM_LSP_TABLE = [
30813
31658
  ];
30814
31659
 
30815
31660
  // src/lsp-project-relevance.ts
30816
- 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";
30817
31662
  import { join as join12 } from "node:path";
30818
31663
  var MAX_WALK_DIRS = 200;
30819
31664
  var MAX_WALK_DEPTH = 4;
@@ -30831,7 +31676,7 @@ function hasRootMarker(projectRoot, rootMarkers) {
30831
31676
  if (!rootMarkers)
30832
31677
  return false;
30833
31678
  for (const marker of rootMarkers) {
30834
- if (existsSync8(join12(projectRoot, marker)))
31679
+ if (existsSync9(join12(projectRoot, marker)))
30835
31680
  return true;
30836
31681
  }
30837
31682
  return false;
@@ -31013,7 +31858,7 @@ async function resolveTargetVersion(spec, config2, fetchImpl = fetch) {
31013
31858
  function ensureInstallAnchor(cwd) {
31014
31859
  try {
31015
31860
  const stub = join13(cwd, "package.json");
31016
- if (!existsSync9(stub)) {
31861
+ if (!existsSync10(stub)) {
31017
31862
  writeFileSync6(stub, `${JSON.stringify({ name: "aft-lsp-cache", version: "0.0.0", private: true })}
31018
31863
  `);
31019
31864
  }
@@ -31199,8 +32044,8 @@ function installedBinaryPath(spec) {
31199
32044
  }
31200
32045
  return null;
31201
32046
  }
31202
- function sha256OfFileSync(path3) {
31203
- return createHash4("sha256").update(readFileSync10(path3)).digest("hex");
32047
+ function sha256OfFileSync(path2) {
32048
+ return createHash4("sha256").update(readFileSync10(path2)).digest("hex");
31204
32049
  }
31205
32050
  function quarantineCachedNpmInstall(spec, reason) {
31206
32051
  const packageDir = lspPackageDir(spec.npm);
@@ -31278,9 +32123,15 @@ function runAutoInstall(projectRoot, config2, fetchImpl = fetch) {
31278
32123
  return installsStarted;
31279
32124
  },
31280
32125
  skipped,
31281
- 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))
31282
32128
  };
31283
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
+ }
31284
32135
 
31285
32136
  // src/lsp-github-install.ts
31286
32137
  import { execFileSync as execFileSync2 } from "node:child_process";
@@ -31289,7 +32140,7 @@ import {
31289
32140
  copyFileSync as copyFileSync4,
31290
32141
  createReadStream as createReadStream2,
31291
32142
  createWriteStream as createWriteStream3,
31292
- existsSync as existsSync10,
32143
+ existsSync as existsSync11,
31293
32144
  lstatSync as lstatSync2,
31294
32145
  mkdirSync as mkdirSync8,
31295
32146
  readdirSync as readdirSync4,
@@ -31302,7 +32153,7 @@ import {
31302
32153
  unlinkSync as unlinkSync7,
31303
32154
  writeFileSync as writeFileSync7
31304
32155
  } from "node:fs";
31305
- 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";
31306
32157
  import { Readable as Readable3 } from "node:stream";
31307
32158
  import { pipeline as pipeline3 } from "node:stream/promises";
31308
32159
 
@@ -31424,10 +32275,10 @@ function ghBinaryCandidates(spec, platform) {
31424
32275
  }
31425
32276
  function readGithubInstalledMetaIn(installDir) {
31426
32277
  try {
31427
- const path3 = join14(installDir, INSTALLED_META_FILE2);
31428
- if (!statSync7(path3).isFile())
32278
+ const path2 = join14(installDir, INSTALLED_META_FILE2);
32279
+ if (!statSync7(path2).isFile())
31429
32280
  return null;
31430
- const parsed = JSON.parse(readFileSync11(path3, "utf8"));
32281
+ const parsed = JSON.parse(readFileSync11(path2, "utf8"));
31431
32282
  if (typeof parsed.version !== "string" || parsed.version.length === 0)
31432
32283
  return null;
31433
32284
  return {
@@ -31458,17 +32309,17 @@ function writeGithubInstalledMetaIn(installDir, version2, binarySha256, archiveS
31458
32309
  }
31459
32310
  var MAX_DOWNLOAD_BYTES3 = 256 * 1024 * 1024;
31460
32311
  var MAX_EXTRACT_BYTES2 = 1024 * 1024 * 1024;
31461
- function sha256OfFile(path3) {
32312
+ function sha256OfFile(path2) {
31462
32313
  return new Promise((resolve8, reject) => {
31463
32314
  const hash2 = createHash5("sha256");
31464
- const stream = createReadStream2(path3);
32315
+ const stream = createReadStream2(path2);
31465
32316
  stream.on("error", reject);
31466
32317
  stream.on("data", (chunk) => hash2.update(chunk));
31467
32318
  stream.on("end", () => resolve8(hash2.digest("hex")));
31468
32319
  });
31469
32320
  }
31470
- function sha256OfFileSync2(path3) {
31471
- return createHash5("sha256").update(readFileSync11(path3)).digest("hex");
32321
+ function sha256OfFileSync2(path2) {
32322
+ return createHash5("sha256").update(readFileSync11(path2)).digest("hex");
31472
32323
  }
31473
32324
  async function fetchReleaseByTag(githubRepo, tag, fetchImpl, signal) {
31474
32325
  const candidates = [];
@@ -31644,7 +32495,7 @@ async function downloadFile(url2, destPath, fetchImpl, assetSize, signal) {
31644
32495
  if (Number.isFinite(advertised) && advertised > MAX_DOWNLOAD_BYTES3) {
31645
32496
  throw new Error(`Content-Length ${advertised} exceeds max ${MAX_DOWNLOAD_BYTES3}`);
31646
32497
  }
31647
- mkdirSync8(dirname5(destPath), { recursive: true });
32498
+ mkdirSync8(dirname6(destPath), { recursive: true });
31648
32499
  let bytesWritten = 0;
31649
32500
  const guard = new TransformStream({
31650
32501
  transform(chunk, controller) {
@@ -31791,7 +32642,7 @@ function quarantineCachedGithubInstall(spec, reason) {
31791
32642
  const dest = join14(ghCacheRoot(), ".quarantine", encodeURIComponent(spec.id), `${Date.now()}`);
31792
32643
  warn2(`[lsp] tofu_mismatch ${spec.id}: ${reason}; quarantining ${packageDir} -> ${dest}`);
31793
32644
  try {
31794
- mkdirSync8(dirname5(dest), { recursive: true });
32645
+ mkdirSync8(dirname6(dest), { recursive: true });
31795
32646
  rmSync5(dest, { recursive: true, force: true });
31796
32647
  renameSync7(packageDir, dest);
31797
32648
  } catch (err) {
@@ -31920,12 +32771,12 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
31920
32771
  } catch {}
31921
32772
  }
31922
32773
  const innerBinaryPath = join14(extractDir, spec.binaryPathInArchive(platform, arch, version2));
31923
- if (!existsSync10(innerBinaryPath)) {
32774
+ if (!existsSync11(innerBinaryPath)) {
31924
32775
  error2(`[lsp] ${spec.id}: extracted binary not found at ${innerBinaryPath}`);
31925
32776
  return null;
31926
32777
  }
31927
32778
  const targetBinary = ghBinaryPath(spec, platform);
31928
- mkdirSync8(dirname5(targetBinary), { recursive: true });
32779
+ mkdirSync8(dirname6(targetBinary), { recursive: true });
31929
32780
  try {
31930
32781
  copyFileSync4(innerBinaryPath, targetBinary);
31931
32782
  if (platform !== "win32") {
@@ -32004,7 +32855,7 @@ function runGithubAutoInstall(relevantServers, config2, fetchImpl = fetch) {
32004
32855
  if (!host) {
32005
32856
  for (const spec of GITHUB_LSP_TABLE) {
32006
32857
  try {
32007
- if (existsSync10(ghBinDir(spec))) {
32858
+ if (existsSync11(ghBinDir(spec))) {
32008
32859
  cachedBinDirs.push(ghBinDir(spec));
32009
32860
  }
32010
32861
  } catch {}
@@ -32014,7 +32865,8 @@ function runGithubAutoInstall(relevantServers, config2, fetchImpl = fetch) {
32014
32865
  installsStarted: 0,
32015
32866
  installingBinaries: [],
32016
32867
  skipped,
32017
- installsComplete: Promise.resolve()
32868
+ installsComplete: Promise.resolve(),
32869
+ getCachedBinDirs: () => [...cachedBinDirs]
32018
32870
  };
32019
32871
  }
32020
32872
  for (const spec of GITHUB_LSP_TABLE) {
@@ -32056,7 +32908,13 @@ function runGithubAutoInstall(relevantServers, config2, fetchImpl = fetch) {
32056
32908
  return installsStarted;
32057
32909
  },
32058
32910
  skipped,
32059
- 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
+ }
32060
32918
  };
32061
32919
  }
32062
32920
  function discoverRelevantGithubServers(projectRoot) {
@@ -32242,6 +33100,27 @@ function statusBarBlockForSession(sessionID, counts, force = false) {
32242
33100
  return shouldEmitStatusBar(state, counts) ? formatStatusBar(counts) : undefined;
32243
33101
  }
32244
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
+
32245
33124
  // src/shutdown-hooks.ts
32246
33125
  var GLOBAL_KEY = "__aftPiShutdownHooks__";
32247
33126
  function getState() {
@@ -32338,8 +33217,8 @@ import { StringEnum } from "@earendil-works/pi-ai";
32338
33217
  import { Type as Type3 } from "typebox";
32339
33218
 
32340
33219
  // src/tools/hoisted.ts
32341
- import { stat } from "node:fs/promises";
32342
- import { homedir as homedir12 } from "node:os";
33220
+ import { stat as stat2 } from "node:fs/promises";
33221
+ import { homedir as homedir13 } from "node:os";
32343
33222
  import { isAbsolute as isAbsolute6, relative as relative4, resolve as resolve8, sep } from "node:path";
32344
33223
  import {
32345
33224
  renderDiff
@@ -32483,15 +33362,15 @@ function containsPath(parent, child) {
32483
33362
  const rel = relative4(parent, child);
32484
33363
  return rel === "" || !rel.startsWith("..") && !isAbsolute6(rel);
32485
33364
  }
32486
- function expandTilde2(path3) {
32487
- if (!path3 || !path3.startsWith("~"))
32488
- return path3;
32489
- if (path3 === "~")
32490
- return homedir12();
32491
- if (path3.startsWith(`~${sep}`) || path3.startsWith("~/")) {
32492
- 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));
32493
33372
  }
32494
- return path3;
33373
+ return path2;
32495
33374
  }
32496
33375
  function absoluteSearchPath(cwd, target) {
32497
33376
  const expanded = expandTilde2(target);
@@ -32499,7 +33378,7 @@ function absoluteSearchPath(cwd, target) {
32499
33378
  }
32500
33379
  async function searchPathExists(cwd, target) {
32501
33380
  try {
32502
- await stat(absoluteSearchPath(cwd, target));
33381
+ await stat2(absoluteSearchPath(cwd, target));
32503
33382
  return true;
32504
33383
  } catch {
32505
33384
  return false;
@@ -32556,6 +33435,8 @@ async function assertExternalDirectoryPermission(extCtx, target, options = {}) {
32556
33435
  return;
32557
33436
  if (options.restrictToProjectRoot === false)
32558
33437
  return;
33438
+ if (options.serverValidatedRead === true)
33439
+ return;
32559
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.");
32560
33441
  }
32561
33442
  var ReadParams = Type2.Object({
@@ -32565,8 +33446,8 @@ var ReadParams = Type2.Object({
32565
33446
  filePath: Type2.Optional(Type2.String({
32566
33447
  description: "Alias for `path` — provide one of the two."
32567
33448
  })),
32568
- offset: optionalInt(1, Number.MAX_SAFE_INTEGER),
32569
- 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")
32570
33451
  });
32571
33452
  var WriteParams = Type2.Object({
32572
33453
  filePath: Type2.Optional(Type2.String({
@@ -32580,8 +33461,10 @@ var WriteParams = Type2.Object({
32580
33461
  var BatchEditParams = Type2.Object({
32581
33462
  oldString: Type2.Optional(Type2.String({ description: "Text to find for a batch find/replace edit" })),
32582
33463
  newString: Type2.Optional(Type2.String({ description: "Replacement text for a batch find/replace edit" })),
32583
- startLine: Type2.Optional(Type2.Any({ description: "1-based start line for a batch line-range edit" })),
32584
- endLine: Type2.Optional(Type2.Any({ description: "1-based end line for a batch line-range 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"),
32585
33468
  content: Type2.Optional(Type2.String({
32586
33469
  description: "Replacement text for a batch line-range edit (empty string deletes the lines)"
32587
33470
  }))
@@ -32596,12 +33479,12 @@ var EditParams = Type2.Object({
32596
33479
  oldString: Type2.Optional(Type2.String({ description: "Text to find (exact match, fuzzy fallback)" })),
32597
33480
  newString: Type2.Optional(Type2.String({ description: "Replacement text (omit to delete match)" })),
32598
33481
  replaceAll: Type2.Optional(Type2.Boolean({ description: "Replace every occurrence" })),
32599
- occurrence: optionalInt(0, Number.MAX_SAFE_INTEGER),
33482
+ occurrence: optionalInt(0, Number.MAX_SAFE_INTEGER, "0-based occurrence to replace when multiple matches exist"),
32600
33483
  appendContent: Type2.Optional(Type2.String({
32601
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."
32602
33485
  })),
32603
33486
  edits: Type2.Optional(Type2.Array(BatchEditParams, {
32604
- description: "Batch edits — array of { oldString, newString } or { startLine, endLine, content } objects applied atomically to one file."
33487
+ description: "Batch edits — array of { oldString, newString }, { oldString, newString, replaceAll: true }, or { startLine, endLine, content } objects applied atomically."
32605
33488
  }))
32606
33489
  });
32607
33490
  var GrepParams = Type2.Object({
@@ -32685,7 +33568,8 @@ function registerHoistedTools(pi, ctx, surface) {
32685
33568
  const limit = coerceOptionalInt(params.limit, "limit", 1, Number.MAX_SAFE_INTEGER);
32686
33569
  const filePath = await resolvePathArg(extCtx.cwd, pathArg);
32687
33570
  await assertExternalDirectoryPermission(extCtx, filePath, {
32688
- restrictToProjectRoot: surface.restrictToProjectRoot
33571
+ restrictToProjectRoot: surface.restrictToProjectRoot,
33572
+ serverValidatedRead: true
32689
33573
  });
32690
33574
  const rawArgs = { filePath: pathArg };
32691
33575
  if (offset !== undefined)
@@ -32766,7 +33650,7 @@ PDFs aren't supported on the Pi harness yet.`, response);
32766
33650
  pi.registerTool({
32767
33651
  name: "edit",
32768
33652
  label: "edit",
32769
- description: "Edit part of a file via `appendContent`, batch `edits[]`, or `oldString`/`newString` find-and-replace. Mode priority: appendContent > edits > oldString. Find/replace errors on multiple matches use `occurrence` or `replaceAll: true`.",
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.",
32770
33654
  promptSnippet: "Partial file edits via appendContent, edits[], or oldString/newString (mode priority: appendContent > edits > oldString).",
32771
33655
  promptGuidelines: [
32772
33656
  "Prefer edit over write when changing part of an existing file.",
@@ -32792,10 +33676,20 @@ PDFs aren't supported on the Pi harness yet.`, response);
32792
33676
  });
32793
33677
  const bridge = bridgeFor(ctx, extCtx.cwd);
32794
33678
  const rawArgs = { filePath: filePathArg };
32795
- for (const key of ["appendContent", "edits", "oldString", "newString"]) {
33679
+ for (const key of ["appendContent", "oldString", "newString"]) {
32796
33680
  if (argsRecord[key] !== undefined)
32797
33681
  rawArgs[key] = argsRecord[key];
32798
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;
33692
+ }
32799
33693
  if (params.replaceAll !== undefined)
32800
33694
  rawArgs.replaceAll = coerceBoolean(params.replaceAll);
32801
33695
  const occurrence = coerceOptionalInt(params.occurrence, "occurrence", 0, Number.MAX_SAFE_INTEGER);
@@ -32998,17 +33892,17 @@ ${summary}${suffix}`);
32998
33892
  container.addChild(new Text(renderDiff(diff), 1, 0));
32999
33893
  return container;
33000
33894
  }
33001
- function shortenPath2(path3) {
33002
- const home = homedir12();
33003
- if (path3.startsWith(home))
33004
- return `~${path3.slice(home.length)}`;
33005
- return path3;
33895
+ function shortenPath2(path2) {
33896
+ const home = homedir13();
33897
+ if (path2.startsWith(home))
33898
+ return `~${path2.slice(home.length)}`;
33899
+ return path2;
33006
33900
  }
33007
- async function resolvePathArg(cwd, path3) {
33008
- const expanded = expandTilde2(path3);
33009
- const abs = absoluteSearchPath(cwd, path3);
33901
+ async function resolvePathArg(cwd, path2) {
33902
+ const expanded = expandTilde2(path2);
33903
+ const abs = absoluteSearchPath(cwd, path2);
33010
33904
  try {
33011
- await stat(abs);
33905
+ await stat2(abs);
33012
33906
  return abs;
33013
33907
  } catch {
33014
33908
  return expanded;
@@ -33016,7 +33910,7 @@ async function resolvePathArg(cwd, path3) {
33016
33910
  }
33017
33911
 
33018
33912
  // src/tools/render-helpers.ts
33019
- import { homedir as homedir13 } from "node:os";
33913
+ import { homedir as homedir14 } from "node:os";
33020
33914
  import { renderDiff as renderDiff2 } from "@earendil-works/pi-coding-agent";
33021
33915
  import { Container as Container2, Spacer as Spacer2, Text as Text2 } from "@earendil-works/pi-tui";
33022
33916
  function reuseText2(last) {
@@ -33025,11 +33919,11 @@ function reuseText2(last) {
33025
33919
  function reuseContainer2(last) {
33026
33920
  return last instanceof Container2 ? last : new Container2;
33027
33921
  }
33028
- function shortenPath3(path3) {
33029
- const home = homedir13();
33030
- if (path3.startsWith(home))
33031
- return `~${path3.slice(home.length)}`;
33032
- return path3;
33922
+ function shortenPath3(path2) {
33923
+ const home = homedir14();
33924
+ if (path2.startsWith(home))
33925
+ return `~${path2.slice(home.length)}`;
33926
+ return path2;
33033
33927
  }
33034
33928
  function renderToolCall(toolName, summary, theme, context) {
33035
33929
  const text = reuseText2(context.lastComponent);
@@ -33037,10 +33931,10 @@ function renderToolCall(toolName, summary, theme, context) {
33037
33931
  text.setText(`${theme.fg("toolTitle", theme.bold(toolName))}${suffix}`);
33038
33932
  return text;
33039
33933
  }
33040
- function accentPath(theme, path3) {
33041
- if (!path3)
33934
+ function accentPath(theme, path2) {
33935
+ if (!path2)
33042
33936
  return theme.fg("toolOutput", "...");
33043
- return theme.fg("accent", shortenPath3(path3));
33937
+ return theme.fg("accent", shortenPath3(path2));
33044
33938
  }
33045
33939
  function collectTextContent(result) {
33046
33940
  return result.content.filter((part) => part.type === "text").map((part) => part.text ?? "").join(`
@@ -33225,17 +34119,17 @@ ${warningStrings.map((w) => ` ${w}`).join(`
33225
34119
  async function resolveAstPaths(extCtx, paths) {
33226
34120
  if (isEmptyParam(paths) || !Array.isArray(paths))
33227
34121
  return;
33228
- 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)));
33229
34123
  }
33230
34124
  async function assertAstPathsPermission(extCtx, paths, restrictToProjectRoot) {
33231
34125
  if (paths === undefined || paths.length === 0)
33232
34126
  return;
33233
34127
  const checked = new Set;
33234
- for (const path3 of paths) {
33235
- if (checked.has(path3))
34128
+ for (const path2 of paths) {
34129
+ if (checked.has(path2))
33236
34130
  continue;
33237
- checked.add(path3);
33238
- await assertExternalDirectoryPermission(extCtx, path3, { restrictToProjectRoot });
34131
+ checked.add(path2);
34132
+ await assertExternalDirectoryPermission(extCtx, path2, { restrictToProjectRoot });
33239
34133
  }
33240
34134
  }
33241
34135
  function buildAstSearchSections(payload, theme) {
@@ -33417,7 +34311,7 @@ function registerAstTools(pi, ctx, surface) {
33417
34311
  }
33418
34312
 
33419
34313
  // src/tools/bash.ts
33420
- import * as fs4 from "node:fs/promises";
34314
+ import * as fs3 from "node:fs/promises";
33421
34315
  import { Container as Container3, Spacer as Spacer3, Text as Text3 } from "@earendil-works/pi-tui";
33422
34316
  import { Type as Type4 } from "typebox";
33423
34317
  var BASH_WAIT_POLL_INTERVAL_MS = 100;
@@ -33433,7 +34327,7 @@ function resolveForegroundWaitMs(configured) {
33433
34327
  }
33434
34328
  return configured;
33435
34329
  }
33436
- var BASH_TRANSPORT_TIMEOUT_MS = 30000;
34330
+ var BASH_TRANSPORT_TIMEOUT_MS2 = 30000;
33437
34331
  var DEFAULT_HARD_TIMEOUT_MS = 30 * 60 * 1000;
33438
34332
  var BASH_TRANSPORT_MARGIN_MS = 1e4;
33439
34333
  function orchestratedTransportTimeoutMs(blockToCompletion, wait, effectiveTimeout, foregroundWaitMs) {
@@ -33444,7 +34338,7 @@ var BashBaseParams = {
33444
34338
  command: Type4.String({
33445
34339
  description: "Shell command to execute. Supports pipes, redirections, and shell syntax."
33446
34340
  }),
33447
- timeout: optionalInt(1, Number.MAX_SAFE_INTEGER),
34341
+ timeout: optionalInt(1, Number.MAX_SAFE_INTEGER, "Hard kill timeout in milliseconds"),
33448
34342
  workdir: Type4.Optional(Type4.String({
33449
34343
  description: "Working directory for command execution. Relative paths resolve against the project root. Defaults to the current session's working directory."
33450
34344
  })),
@@ -33452,7 +34346,7 @@ var BashBaseParams = {
33452
34346
  description: "Human-readable description shown in UI logs. Helps users understand what the command does without reading shell syntax."
33453
34347
  })),
33454
34348
  wait: Type4.Optional(Type4.Boolean({
33455
- 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."
33456
34350
  }))
33457
34351
  };
33458
34352
  var BashBackgroundFlagParam = {
@@ -33469,8 +34363,8 @@ var BashPtyParams = {
33469
34363
  pty: Type4.Optional(Type4.Boolean({
33470
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.'
33471
34365
  })),
33472
- ptyRows: optionalInt(1, 60),
33473
- 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)")
33474
34368
  };
33475
34369
  var BashParams = Type4.Object({
33476
34370
  ...BashBaseParams,
@@ -33504,7 +34398,7 @@ var BashWatchParams = Type4.Object({
33504
34398
  }),
33505
34399
  pattern: Type4.Optional(Type4.Union([Type4.String(), Type4.Object({ regex: Type4.String() })])),
33506
34400
  background: Type4.Optional(Type4.Boolean()),
33507
- 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"),
33508
34402
  once: Type4.Optional(Type4.Boolean())
33509
34403
  });
33510
34404
  var BashWriteParams = Type4.Object({
@@ -33530,7 +34424,7 @@ function unavailableSnapshot() {
33530
34424
  }
33531
34425
  async function callBashBridge(bridge, command, params = {}, extCtx, options) {
33532
34426
  return await callBridge(bridge, command, params, extCtx, {
33533
- transportTimeoutMs: BASH_TRANSPORT_TIMEOUT_MS,
34427
+ transportTimeoutMs: BASH_TRANSPORT_TIMEOUT_MS2,
33534
34428
  ...options,
33535
34429
  keepBridgeOnTimeout: true
33536
34430
  });
@@ -33561,7 +34455,7 @@ function registerBashTool(pi, ctx, aftSearchRegistered = false) {
33561
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";
33562
34456
  const bashCfg = resolveBashConfig(ctx.config);
33563
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`." : "";
33564
- 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).";
33565
34459
  pi.registerTool({
33566
34460
  name: "bash",
33567
34461
  label: "bash",
@@ -33835,7 +34729,7 @@ async function waitForBashStatus(ctx, bridge, extCtx, taskId, outputMode, waitFo
33835
34729
  let scanBaseOffset = 0;
33836
34730
  const bridgeOptions = {
33837
34731
  keepBridgeOnTimeout: true,
33838
- transportTimeoutMs: BASH_TRANSPORT_TIMEOUT_MS
34732
+ transportTimeoutMs: BASH_TRANSPORT_TIMEOUT_MS2
33839
34733
  };
33840
34734
  if (waitFor?.kind === "regex") {
33841
34735
  await validateWaitRegex(bridge, extCtx, waitFor);
@@ -33946,7 +34840,7 @@ async function readNewTaskOutput(data, cursor) {
33946
34840
  };
33947
34841
  }
33948
34842
  async function readFileBytesFrom(outputPath, cursor) {
33949
- const handle = await fs4.open(outputPath, "r");
34843
+ const handle = await fs3.open(outputPath, "r");
33950
34844
  try {
33951
34845
  const chunks = [];
33952
34846
  let offset = cursor;
@@ -34075,7 +34969,7 @@ async function formatPtyStatus(_extCtx, taskId, details, requestedOutputMode) {
34075
34969
  return `
34076
34970
  [PTY output path unavailable]`;
34077
34971
  const outputMode = requestedOutputMode ?? "screen";
34078
- 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;
34079
34973
  let suffix = "";
34080
34974
  if (outputMode === "raw") {
34081
34975
  suffix = raw && raw.length > 0 ? `
@@ -34196,12 +35090,12 @@ function registerConflictsTool(pi, ctx) {
34196
35090
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
34197
35091
  const bridge = bridgeFor(ctx, extCtx.cwd);
34198
35092
  const reqParams = {};
34199
- const path3 = params?.path;
34200
- if (typeof path3 === "string" && path3.trim() !== "") {
34201
- await assertExternalDirectoryPermission(extCtx, path3, {
35093
+ const path2 = params?.path;
35094
+ if (typeof path2 === "string" && path2.trim() !== "") {
35095
+ await assertExternalDirectoryPermission(extCtx, path2, {
34202
35096
  restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
34203
35097
  });
34204
- reqParams.path = await resolvePathArg(extCtx.cwd, path3);
35098
+ reqParams.path = await resolvePathArg(extCtx.cwd, path2);
34205
35099
  }
34206
35100
  const response = await callToolCall(bridge, "conflicts", reqParams, extCtx);
34207
35101
  if (response.success === false) {
@@ -34613,8 +35507,8 @@ function tier2SummaryPart(summary, key, label) {
34613
35507
  return `${label} ${status ?? "unavailable"}`;
34614
35508
  }
34615
35509
  function shortDupOccurrence(entry) {
34616
- const [path3] = entry.split(":");
34617
- return path3?.split("/").pop() ?? entry;
35510
+ const [path2] = entry.split(":");
35511
+ return path2?.split("/").pop() ?? entry;
34618
35512
  }
34619
35513
  function tier2TopPreview(summary, theme) {
34620
35514
  const lines = [];
@@ -34798,7 +35692,7 @@ function navigateParamsSchema() {
34798
35692
  description: "Source file containing the symbol (absolute or relative to project root)"
34799
35693
  }),
34800
35694
  symbol: Type9.String({ description: "Name of the symbol to analyze" }),
34801
- depth: optionalInt(1, Number.MAX_SAFE_INTEGER),
35695
+ depth: optionalInt(1, Number.MAX_SAFE_INTEGER, "Maximum call-graph depth to traverse"),
34802
35696
  expression: Type9.Optional(Type9.String({ description: "Expression to track (required for trace_data)" })),
34803
35697
  toSymbol: Type9.Optional(Type9.String({
34804
35698
  description: "Target symbol for trace_to_symbol; the returned path ends here"
@@ -35235,9 +36129,9 @@ var RefactorParams = Type11.Object({
35235
36129
  destination: Type11.Optional(Type11.String({ description: "Target file (for move)" })),
35236
36130
  scope: Type11.Optional(Type11.String({ description: "Disambiguation scope for move op" })),
35237
36131
  name: Type11.Optional(Type11.String({ description: "New function name (for extract)" })),
35238
- startLine: optionalInt(1, Number.MAX_SAFE_INTEGER),
35239
- endLine: optionalInt(1, Number.MAX_SAFE_INTEGER),
35240
- 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")
35241
36135
  });
35242
36136
  function buildRefactorSections(args, payload, theme) {
35243
36137
  const response = asRecord3(payload);
@@ -35356,7 +36250,7 @@ function registerRefactorTool(pi, ctx) {
35356
36250
  import { StringEnum as StringEnum5 } from "@earendil-works/pi-ai";
35357
36251
  import { Type as Type12 } from "typebox";
35358
36252
  function responsePaths(response) {
35359
- 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) : [];
35360
36254
  }
35361
36255
  var SafetyParams = Type12.Object({
35362
36256
  op: StringEnum5(["undo", "history", "checkpoint", "restore", "list"], {
@@ -35738,7 +36632,7 @@ function buildWorkflowHints(opts) {
35738
36632
  }
35739
36633
  if (hasBgBash) {
35740
36634
  sections.push([
35741
- `**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.`,
35742
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.",
35743
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."
35744
36638
  ].join(`
@@ -35809,18 +36703,18 @@ function createVersionMismatchHandler(getPool, ensureCompatibleBinary = ensureBi
35809
36703
  const upgradePromise = (async () => {
35810
36704
  warn2(`WARNING: aft binary v${binaryVersion} is older than plugin v${minVersion}. ` + "Some features may not work. Attempting to download a compatible binary...");
35811
36705
  try {
35812
- const path3 = await ensureCompatibleBinary(`v${minVersion}`);
35813
- if (!path3) {
36706
+ const path2 = await ensureCompatibleBinary(`v${minVersion}`);
36707
+ if (!path2) {
35814
36708
  warn2(`Could not find or download v${minVersion}. Continuing with v${binaryVersion}.`);
35815
36709
  return null;
35816
36710
  }
35817
36711
  const pool = getPool();
35818
36712
  if (!pool) {
35819
- 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.`);
35820
36714
  return null;
35821
36715
  }
35822
- log2(`Found/downloaded compatible binary at ${path3}. Replacing running bridges...`);
35823
- const replaced = await pool.replaceBinary(path3);
36716
+ log2(`Found/downloaded compatible binary at ${path2}. Replacing running bridges...`);
36717
+ const replaced = await pool.replaceBinary(path2);
35824
36718
  log2("Binary replaced successfully. New bridges will use the updated binary.");
35825
36719
  return replaced;
35826
36720
  } catch (err) {
@@ -35842,12 +36736,14 @@ var PLUGIN_VERSION = (() => {
35842
36736
  return "0.0.0";
35843
36737
  }
35844
36738
  })();
35845
- var ANNOUNCEMENT_VERSION = "0.46.0";
36739
+ var ANNOUNCEMENT_VERSION = "0.47.0";
35846
36740
  var ANNOUNCEMENT_FEATURES = [
35847
- "Search index changes persist on clean shutdown, so restarts and cross-worktree searches see current results.",
35848
- "Cross-project search: sessions can search sibling checkouts read-only, serving borrowed indexes with drift warnings instead of failing.",
35849
- "Duplicate detection requires a 10-line minimum span, ending flags on 3-4 line idiomatic blocks.",
35850
- "Pi edit tool supports batch edits[] arrays, matching OpenCode."
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."
35851
36747
  ];
35852
36748
  var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/DSa65w8wuf";
35853
36749
  var ALL_ONLY_TOOLS = new Set(["aft_callgraph", "aft_delete", "aft_move", "aft_refactor"]);
@@ -36044,6 +36940,7 @@ async function src_default(pi) {
36044
36940
  const configOverrides = buildConfigTierConfigureParams(projectRoot, {
36045
36941
  storage_dir: storageDir
36046
36942
  });
36943
+ let lspInstallCompletion = null;
36047
36944
  try {
36048
36945
  const lspAutoInstall = config2.lsp?.auto_install ?? true;
36049
36946
  const lspGraceDays = config2.lsp?.grace_days ?? 7;
@@ -36073,10 +36970,24 @@ async function src_default(pi) {
36073
36970
  if (lspInflightInstalls.length > 0) {
36074
36971
  configOverrides.lsp_inflight_installs = lspInflightInstalls;
36075
36972
  }
36076
- if (npmResult.installsStarted > 0 || ghResult.installsStarted > 0) {
36973
+ const installsWereStarted = npmResult.installsStarted > 0 || ghResult.installsStarted > 0;
36974
+ if (installsWereStarted) {
36077
36975
  log2(`[lsp] auto-install: ${npmResult.installsStarted} npm + ${ghResult.installsStarted} github install(s) running in background`);
36078
36976
  }
36079
- 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) => {
36080
36991
  const actionable = [...npmResult.skipped, ...ghResult.skipped].filter((s) => {
36081
36992
  const r = s.reason.toLowerCase();
36082
36993
  if (r === "auto_install: false")
@@ -36091,16 +37002,20 @@ async function src_default(pi) {
36091
37002
  return false;
36092
37003
  return true;
36093
37004
  });
36094
- if (actionable.length === 0)
36095
- return;
36096
- 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(`
36097
37007
  `);
36098
- warn2(`[lsp] skipped or failed to install ${actionable.length} server(s):
37008
+ warn2(`[lsp] skipped or failed to install ${actionable.length} server(s):
36099
37009
  ${lines}
36100
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;
36101
37013
  }).catch((err) => {
36102
37014
  warn2(`[lsp] install-summary aggregation failed: ${err}`);
37015
+ return null;
36103
37016
  });
37017
+ if (installsWereStarted)
37018
+ lspInstallCompletion = installCompletion;
36104
37019
  } catch (err) {
36105
37020
  warn2(`[lsp] auto-install setup failed: ${err instanceof Error ? err.message : String(err)}`);
36106
37021
  }
@@ -36171,6 +37086,17 @@ ${lines}
36171
37086
  });
36172
37087
  }
36173
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
+ }
36174
37100
  pool.setConfigureOverride("harness", "pi");
36175
37101
  pool.setConfigureOverride("aft_search_registered", resolveToolSurface(config2).semantic);
36176
37102
  const ctx = { pool, config: config2, storageDir };
@@ -36283,7 +37209,9 @@ ${lines}
36283
37209
  }
36284
37210
  });
36285
37211
  pi.on("input", (_event, extCtx) => {
36286
- signalSyncWatchAbort(resolveSessionId(extCtx));
37212
+ const sessionId = resolveSessionId(extCtx);
37213
+ signalSyncWatchAbort(sessionId);
37214
+ signalBashWaitDetachForProject(pool, extCtx.cwd, sessionId);
36287
37215
  });
36288
37216
  const unregisterShutdownCleanup = registerShutdownCleanup(async () => {
36289
37217
  try {