@cortexkit/aft-pi 0.46.0 → 0.47.1

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,17 +10307,60 @@ 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
  }
10314
+ function decodeFileUrl(target) {
10315
+ if (!target.startsWith("file:"))
10316
+ return target;
10317
+ const rest = target.slice("file:".length);
10318
+ let pathPart;
10319
+ if (rest.startsWith("//")) {
10320
+ const after = rest.slice(2);
10321
+ const slash = after.indexOf("/");
10322
+ const authority = slash === -1 ? after : after.slice(0, slash);
10323
+ const p = slash === -1 ? "" : after.slice(slash);
10324
+ if (authority === "" || authority === "localhost")
10325
+ pathPart = p;
10326
+ else if (process.platform === "win32")
10327
+ pathPart = `//${authority}${p}`;
10328
+ else
10329
+ return target;
10330
+ } else if (rest.startsWith("/")) {
10331
+ pathPart = rest;
10332
+ } else {
10333
+ return target;
10334
+ }
10335
+ const bytes = [];
10336
+ for (let i = 0;i < pathPart.length; ) {
10337
+ if (pathPart[i] === "%" && i + 2 < pathPart.length) {
10338
+ const hex = pathPart.slice(i + 1, i + 3);
10339
+ if (/^[0-9a-fA-F]{2}$/.test(hex)) {
10340
+ bytes.push(Number.parseInt(hex, 16));
10341
+ i += 3;
10342
+ continue;
10343
+ }
10344
+ }
10345
+ const cp = pathPart.codePointAt(i);
10346
+ const ch = String.fromCodePoint(cp);
10347
+ for (const b of Buffer.from(ch, "utf8"))
10348
+ bytes.push(b);
10349
+ i += ch.length;
10350
+ }
10351
+ const decoded = Buffer.from(bytes).toString("utf8");
10352
+ if (process.platform === "win32" && /^\/[A-Za-z]:/.test(decoded)) {
10353
+ return decoded.slice(1);
10354
+ }
10355
+ return decoded;
10356
+ }
10193
10357
 
10194
10358
  // ../aft-bridge/dist/resolver.js
10195
10359
  import { execSync } from "node:child_process";
10196
10360
  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
10361
  import { createRequire as createRequire2 } from "node:module";
10198
- import { homedir as homedir6 } from "node:os";
10199
- import { join as join5 } from "node:path";
10362
+ import { homedir as homedir7 } from "node:os";
10363
+ import { join as join6 } from "node:path";
10200
10364
  var ensureBinaryForResolver = ensureBinary;
10201
10365
  function copyToVersionedCache(npmBinaryPath, knownVersion) {
10202
10366
  try {
@@ -10205,9 +10369,9 @@ function copyToVersionedCache(npmBinaryPath, knownVersion) {
10205
10369
  return null;
10206
10370
  const tag = version.startsWith("v") ? version : `v${version}`;
10207
10371
  const cacheDir = getCacheDir();
10208
- const versionedDir = join5(cacheDir, tag);
10372
+ const versionedDir = join6(cacheDir, tag);
10209
10373
  const ext = process.platform === "win32" ? ".exe" : "";
10210
- const cachedPath = join5(versionedDir, `aft${ext}`);
10374
+ const cachedPath = join6(versionedDir, `aft${ext}`);
10211
10375
  if (existsSync4(cachedPath)) {
10212
10376
  const cachedVersion = readBinaryVersion(cachedPath);
10213
10377
  if (cachedVersion === version)
@@ -10237,18 +10401,18 @@ function normalizeBareVersion(version) {
10237
10401
  return version.startsWith("v") ? version.slice(1) : version;
10238
10402
  }
10239
10403
  function homeDirFromEnv(env) {
10240
- return (process.platform === "win32" ? env.USERPROFILE || env.HOME : env.HOME) || homedir6();
10404
+ return (process.platform === "win32" ? env.USERPROFILE || env.HOME : env.HOME) || homedir7();
10241
10405
  }
10242
10406
  function cacheDirFromEnv(env) {
10243
10407
  if (process.platform === "win32") {
10244
- const base2 = env.LOCALAPPDATA || env.APPDATA || join5(homeDirFromEnv(env), "AppData", "Local");
10245
- return join5(base2, "aft", "bin");
10408
+ const base2 = env.LOCALAPPDATA || env.APPDATA || join6(homeDirFromEnv(env), "AppData", "Local");
10409
+ return join6(base2, "aft", "bin");
10246
10410
  }
10247
- const base = env.XDG_CACHE_HOME || join5(homeDirFromEnv(env), ".cache");
10248
- return join5(base, "aft", "bin");
10411
+ const base = env.XDG_CACHE_HOME || join6(homeDirFromEnv(env), ".cache");
10412
+ return join6(base, "aft", "bin");
10249
10413
  }
10250
10414
  function cachedBinaryPathFromEnv(version, env, ext) {
10251
- const binaryPath = join5(cacheDirFromEnv(env), version, `aft${ext}`);
10415
+ const binaryPath = join6(cacheDirFromEnv(env), version, `aft${ext}`);
10252
10416
  return existsSync4(binaryPath) ? binaryPath : null;
10253
10417
  }
10254
10418
  function isExpectedCachedBinary2(binaryPath, expectedVersion) {
@@ -10367,7 +10531,7 @@ function findBinarySync(expectedVersion) {
10367
10531
  return usable;
10368
10532
  }
10369
10533
  } catch {}
10370
- const cargoPath = join5(homeDirFromEnv(env), ".cargo", "bin", `aft${ext}`);
10534
+ const cargoPath = join6(homeDirFromEnv(env), ".cargo", "bin", `aft${ext}`);
10371
10535
  if (existsSync4(cargoPath)) {
10372
10536
  const usable = probeBinaryCandidate(cargoPath, "cargo", expectedVersion);
10373
10537
  if (usable)
@@ -10409,26 +10573,26 @@ async function findBinary(expectedVersion) {
10409
10573
  var spawnSyncForMigration = spawnSync2;
10410
10574
  var TARGET_MARKER = ".migrated_from_legacy";
10411
10575
  var DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;
10412
- function dataHome() {
10576
+ function dataHome2() {
10413
10577
  if (process.env.XDG_DATA_HOME)
10414
10578
  return process.env.XDG_DATA_HOME;
10415
10579
  if (process.platform === "win32") {
10416
- return process.env.LOCALAPPDATA || process.env.APPDATA || join6(homeDir2(), "AppData", "Local");
10580
+ return process.env.LOCALAPPDATA || process.env.APPDATA || join7(homeDir3(), "AppData", "Local");
10417
10581
  }
10418
- return join6(homeDir2(), ".local", "share");
10582
+ return join7(homeDir3(), ".local", "share");
10419
10583
  }
10420
- function homeDir2() {
10584
+ function homeDir3() {
10421
10585
  if (process.platform === "win32")
10422
- return process.env.USERPROFILE || process.env.HOME || homedir7();
10423
- return process.env.HOME || homedir7();
10586
+ return process.env.USERPROFILE || process.env.HOME || homedir8();
10587
+ return process.env.HOME || homedir8();
10424
10588
  }
10425
10589
  function resolveLegacyStorageRoot(harness) {
10426
10590
  if (harness === "pi")
10427
- return join6(homeDir2(), ".pi", "agent", "aft");
10428
- return join6(dataHome(), "opencode", "storage", "plugin", "aft");
10591
+ return join7(homeDir3(), ".pi", "agent", "aft");
10592
+ return join7(dataHome2(), "opencode", "storage", "plugin", "aft");
10429
10593
  }
10430
10594
  function resolveCortexKitStorageRoot() {
10431
- return join6(dataHome(), "cortexkit", "aft");
10595
+ return join7(dataHome2(), "cortexkit", "aft");
10432
10596
  }
10433
10597
  function stripJsoncForParse(input) {
10434
10598
  let out = "";
@@ -10557,8 +10721,8 @@ function acquireConfigMigrationLock(lockDir) {
10557
10721
  }
10558
10722
  }
10559
10723
  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`);
10724
+ mkdirSync4(dirname3(targetPath), { recursive: true });
10725
+ const tmpPath = join7(dirname3(targetPath), `.${basename(targetPath)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
10562
10726
  let fd = null;
10563
10727
  try {
10564
10728
  fd = openSync3(tmpPath, "wx", 384);
@@ -10579,8 +10743,8 @@ function atomicCopyConfigFile(sourcePath, targetPath) {
10579
10743
  }
10580
10744
  }
10581
10745
  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`);
10746
+ mkdirSync4(dirname3(targetPath), { recursive: true });
10747
+ const tmpPath = join7(dirname3(targetPath), `.${basename(targetPath)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
10584
10748
  let fd = null;
10585
10749
  try {
10586
10750
  fd = openSync3(tmpPath, "wx", 384);
@@ -10705,7 +10869,7 @@ function migrateAftConfigFile(opts) {
10705
10869
  if (existingSources.length === 0) {
10706
10870
  return { migrated: false, conflict: false, targetPath: opts.targetPath, warnings };
10707
10871
  }
10708
- mkdirSync4(dirname2(opts.targetPath), { recursive: true });
10872
+ mkdirSync4(dirname3(opts.targetPath), { recursive: true });
10709
10873
  const release = acquireConfigMigrationLock(`${opts.targetPath}.lock`);
10710
10874
  try {
10711
10875
  const sources = existingSources.map((source) => ({
@@ -10762,13 +10926,13 @@ function spawnErrorLabel(error2) {
10762
10926
  return [code, error2.message].filter(Boolean).join(": ");
10763
10927
  }
10764
10928
  function migrationLogPath(newRoot, harness, logger) {
10765
- const desired = join6(newRoot, "logs", "migration", `${harness}-${Date.now()}.jsonl`);
10929
+ const desired = join7(newRoot, "logs", "migration", `${harness}-${Date.now()}.jsonl`);
10766
10930
  try {
10767
- mkdirSync4(dirname2(desired), { recursive: true });
10931
+ mkdirSync4(dirname3(desired), { recursive: true });
10768
10932
  return desired;
10769
10933
  } 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}.`);
10934
+ const fallback = join7(tmpdir(), `aft-migration-${harness}-${Date.now()}.jsonl`);
10935
+ logger?.warn?.(`Failed to create AFT migration log directory ${dirname3(desired)}: ${err instanceof Error ? err.message : String(err)}. ` + `Using fallback log path ${fallback}.`);
10772
10936
  return fallback;
10773
10937
  }
10774
10938
  }
@@ -10830,13 +10994,13 @@ async function ensureStorageMigrated(opts) {
10830
10994
  }
10831
10995
  // ../aft-bridge/dist/npm-resolver.js
10832
10996
  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";
10997
+ import { homedir as homedir9 } from "node:os";
10998
+ import { delimiter, dirname as dirname4, isAbsolute as isAbsolute3, join as join8 } from "node:path";
10835
10999
  function defaultDeps() {
10836
11000
  return {
10837
11001
  platform: process.platform,
10838
11002
  env: process.env,
10839
- home: homedir8(),
11003
+ home: homedir9(),
10840
11004
  execPath: process.execPath
10841
11005
  };
10842
11006
  }
@@ -10857,14 +11021,14 @@ function npmFromPath(deps) {
10857
11021
  const dir = entry.trim().replace(/^"|"$/g, "");
10858
11022
  if (!dir || !isAbsolute3(dir))
10859
11023
  continue;
10860
- if (isFile(join7(dir, name)))
11024
+ if (isFile(join8(dir, name)))
10861
11025
  return dir;
10862
11026
  }
10863
11027
  return null;
10864
11028
  }
10865
11029
  function npmAdjacentToNode(deps) {
10866
- const dir = dirname3(deps.execPath);
10867
- return isFile(join7(dir, npmBinaryName(deps.platform))) ? dir : null;
11030
+ const dir = dirname4(deps.execPath);
11031
+ return isFile(join8(dir, npmBinaryName(deps.platform))) ? dir : null;
10868
11032
  }
10869
11033
  function highestVersionedNodeBin(installsDir, name) {
10870
11034
  let entries;
@@ -10873,8 +11037,8 @@ function highestVersionedNodeBin(installsDir, name) {
10873
11037
  } catch {
10874
11038
  return null;
10875
11039
  }
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;
11040
+ const candidates = entries.filter((v) => isFile(join8(installsDir, v, "bin", name))).sort((a, b) => compareVersionsDesc(a, b));
11041
+ return candidates.length > 0 ? join8(installsDir, candidates[0], "bin") : null;
10878
11042
  }
10879
11043
  function compareVersionsDesc(a, b) {
10880
11044
  const pa = a.replace(/^v/, "").split(".").map((n) => Number.parseInt(n, 10));
@@ -10899,22 +11063,22 @@ function wellKnownNpmDirs(deps) {
10899
11063
  const programFiles = env.ProgramFiles || "C:\\Program Files";
10900
11064
  const appData = env.APPDATA;
10901
11065
  const localAppData = env.LOCALAPPDATA;
10902
- push(join7(programFiles, "nodejs"));
11066
+ push(join8(programFiles, "nodejs"));
10903
11067
  if (appData)
10904
- push(join7(appData, "npm"));
11068
+ push(join8(appData, "npm"));
10905
11069
  if (localAppData)
10906
- push(join7(localAppData, "Volta", "bin"));
11070
+ push(join8(localAppData, "Volta", "bin"));
10907
11071
  if (env.NVM_SYMLINK)
10908
11072
  push(env.NVM_SYMLINK);
10909
11073
  } else {
10910
11074
  if (env.NVM_BIN)
10911
11075
  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")]);
11076
+ push(highestVersionedNodeBin(join8(home, ".nvm", "versions", "node"), name));
11077
+ push(highestVersionedNodeBin(join8(home, ".local", "share", "mise", "installs", "node"), name));
11078
+ push(highestVersionedNodeBin(join8(home, ".asdf", "installs", "nodejs"), name));
11079
+ push(join8(home, ".volta", "bin"));
11080
+ push(join8(home, ".asdf", "shims"));
11081
+ const systemDirs = deps.systemNpmDirs ?? (platform === "darwin" ? ["/opt/homebrew/bin", "/usr/local/bin"] : ["/usr/local/bin", "/usr/bin", join8(home, ".local", "bin")]);
10918
11082
  for (const dir of systemDirs)
10919
11083
  push(dir);
10920
11084
  }
@@ -10924,12 +11088,12 @@ function resolveNpm(deps = defaultDeps()) {
10924
11088
  const name = npmBinaryName(deps.platform);
10925
11089
  const onPath = npmFromPath(deps);
10926
11090
  if (onPath)
10927
- return { command: join7(onPath, name), binDir: onPath };
11091
+ return { command: join8(onPath, name), binDir: onPath };
10928
11092
  const adjacent = npmAdjacentToNode(deps);
10929
11093
  if (adjacent)
10930
- return { command: join7(adjacent, name), binDir: adjacent };
11094
+ return { command: join8(adjacent, name), binDir: adjacent };
10931
11095
  for (const dir of wellKnownNpmDirs(deps)) {
10932
- const candidate = join7(dir, name);
11096
+ const candidate = join8(dir, name);
10933
11097
  if (isFile(candidate))
10934
11098
  return { command: candidate, binDir: dir };
10935
11099
  }
@@ -10946,7 +11110,7 @@ function npmSpawnEnv(resolved, baseEnv = process.env) {
10946
11110
  import { execFileSync } from "node:child_process";
10947
11111
  import { createHash as createHash3 } from "node:crypto";
10948
11112
  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";
11113
+ import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute4, join as join9, relative as relative2, resolve as resolve5, win32 } from "node:path";
10950
11114
  import { Readable as Readable2 } from "node:stream";
10951
11115
  import { pipeline as pipeline2 } from "node:stream/promises";
10952
11116
  var ORT_VERSION = "1.24.4";
@@ -11009,10 +11173,10 @@ function getManualInstallHint() {
11009
11173
  }
11010
11174
  async function ensureOnnxRuntime(storageDir) {
11011
11175
  const info = getPlatformInfo();
11012
- const ortVersionDir = join8(storageDir, "onnxruntime", ORT_VERSION);
11176
+ const ortVersionDir = join9(storageDir, "onnxruntime", ORT_VERSION);
11013
11177
  const libName = info?.libName ?? "libonnxruntime.dylib";
11014
11178
  const resolvedOrtDir = resolveCachedOnnxRuntimeDir(ortVersionDir, libName);
11015
- const libPath = join8(resolvedOrtDir, libName);
11179
+ const libPath = join9(resolvedOrtDir, libName);
11016
11180
  if (existsSync6(libPath)) {
11017
11181
  const meta = readOnnxInstalledMeta(ortVersionDir);
11018
11182
  if (meta?.sha256) {
@@ -11042,9 +11206,9 @@ async function ensureOnnxRuntime(storageDir) {
11042
11206
  warn(`ONNX Runtime auto-download not available for ${process.platform}/${process.arch}. Install manually: ${getManualInstallHint()}`);
11043
11207
  return null;
11044
11208
  }
11045
- const onnxBaseDir = join8(storageDir, "onnxruntime");
11209
+ const onnxBaseDir = join9(storageDir, "onnxruntime");
11046
11210
  mkdirSync5(onnxBaseDir, { recursive: true });
11047
- const lockPath = join8(onnxBaseDir, ONNX_LOCK_FILE);
11211
+ const lockPath = join9(onnxBaseDir, ONNX_LOCK_FILE);
11048
11212
  cleanupAbandonedStagingDirs(onnxBaseDir);
11049
11213
  if (!acquireLock(lockPath)) {
11050
11214
  warn(`ONNX Runtime install already in progress in another process (lock: ${lockPath}). Skipping.`);
@@ -11063,7 +11227,7 @@ function cleanupAbandonedStagingDirs(onnxBaseDir) {
11063
11227
  for (const entry of entries) {
11064
11228
  if (!entry.startsWith(`${ORT_VERSION}.tmp.`))
11065
11229
  continue;
11066
- const stagingDir = join8(onnxBaseDir, entry);
11230
+ const stagingDir = join9(onnxBaseDir, entry);
11067
11231
  const parts = entry.split(".");
11068
11232
  const pidStr = parts[parts.length - 2];
11069
11233
  const pid = pidStr ? Number.parseInt(pidStr, 10) : NaN;
@@ -11100,7 +11264,7 @@ function cleanupAbandonedStagingDirs(onnxBaseDir) {
11100
11264
  }
11101
11265
  function cleanupIncompleteTargetIfUnowned(ortDir) {
11102
11266
  try {
11103
- if (existsSync6(ortDir) && !existsSync6(join8(ortDir, ONNX_INSTALLED_META_FILE))) {
11267
+ if (existsSync6(ortDir) && !existsSync6(join9(ortDir, ONNX_INSTALLED_META_FILE))) {
11104
11268
  log(`[onnx] removing half-populated install dir ${ortDir} (no meta file)`);
11105
11269
  rmSync3(ortDir, { recursive: true, force: true });
11106
11270
  }
@@ -11144,7 +11308,7 @@ function detectOnnxVersion(libDir, libName) {
11144
11308
  if (version)
11145
11309
  return version;
11146
11310
  }
11147
- const base = join8(libDir, libName);
11311
+ const base = join9(libDir, libName);
11148
11312
  if (existsSync6(base)) {
11149
11313
  try {
11150
11314
  const real = realpathSync(base);
@@ -11183,6 +11347,17 @@ function pathEntriesForPlatform() {
11183
11347
  return isAbsolute4(entry) || win32.isAbsolute(entry);
11184
11348
  });
11185
11349
  }
11350
+ function isWindowsSystem32Directory(dir) {
11351
+ if (process.platform !== "win32")
11352
+ return false;
11353
+ const normalizedDir = win32.resolve(dir).replace(/[\\/]+$/, "").toLowerCase();
11354
+ const windowsRoots = [process.env.SystemRoot, process.env.windir, "C:\\Windows"];
11355
+ return windowsRoots.some((root) => {
11356
+ if (!root)
11357
+ return false;
11358
+ return normalizedDir === win32.resolve(root, "System32").replace(/[\\/]+$/, "").toLowerCase();
11359
+ });
11360
+ }
11186
11361
  function directoryContainsLibrary(dir, libName) {
11187
11362
  try {
11188
11363
  const entries = readdirSync2(dir);
@@ -11196,10 +11371,10 @@ function directoryContainsLibrary(dir, libName) {
11196
11371
  }
11197
11372
  }
11198
11373
  function resolveCachedOnnxRuntimeDir(ortVersionDir, libName) {
11199
- if (existsSync6(join8(ortVersionDir, libName)))
11374
+ if (existsSync6(join9(ortVersionDir, libName)))
11200
11375
  return ortVersionDir;
11201
- const libSubdir = join8(ortVersionDir, "lib");
11202
- if (existsSync6(join8(libSubdir, libName)))
11376
+ const libSubdir = join9(ortVersionDir, "lib");
11377
+ if (existsSync6(join9(libSubdir, libName)))
11203
11378
  return libSubdir;
11204
11379
  return ortVersionDir;
11205
11380
  }
@@ -11214,12 +11389,12 @@ function findSystemOnnxRuntime(libName) {
11214
11389
  } else if (process.platform === "win32") {
11215
11390
  const programFiles = process.env.ProgramFiles ?? "C:\\Program Files";
11216
11391
  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"), ...(() => {
11392
+ searchPaths.push(join9(programFiles, "onnxruntime", "lib"), join9(programFiles, "Microsoft ONNX Runtime", "lib"), join9(programFiles, "Microsoft Machine Learning", "lib"), join9(programFilesX86, "onnxruntime", "lib"), ...(() => {
11218
11393
  const nugetPaths = [];
11219
11394
  const userProfile = process.env.USERPROFILE ?? "";
11220
11395
  if (!userProfile)
11221
11396
  return nugetPaths;
11222
- const nugetPackageDir = join8(userProfile, ".nuget", "packages", "microsoft.ml.onnxruntime");
11397
+ const nugetPackageDir = join9(userProfile, ".nuget", "packages", "microsoft.ml.onnxruntime");
11223
11398
  if (!existsSync6(nugetPackageDir))
11224
11399
  return nugetPaths;
11225
11400
  try {
@@ -11228,7 +11403,7 @@ function findSystemOnnxRuntime(libName) {
11228
11403
  continue;
11229
11404
  if (entry.name === "__globalPackagesFolder" || entry.name.startsWith("."))
11230
11405
  continue;
11231
- nugetPaths.push(join8(nugetPackageDir, entry.name, "runtimes", "win-x64", "native"), join8(nugetPackageDir, entry.name, "runtimes", "win-arm64", "native"));
11406
+ nugetPaths.push(join9(nugetPackageDir, entry.name, "runtimes", "win-x64", "native"), join9(nugetPackageDir, entry.name, "runtimes", "win-arm64", "native"));
11232
11407
  }
11233
11408
  } catch (err) {
11234
11409
  warn(`Failed to scan NuGet ONNX Runtime cache ${nugetPackageDir}: ${err instanceof Error ? err.message : String(err)}`);
@@ -11250,7 +11425,7 @@ function findSystemOnnxRuntime(libName) {
11250
11425
  });
11251
11426
  const unknownVersionPaths = [];
11252
11427
  for (const dir of uniquePaths) {
11253
- const libPath = join8(dir, libName);
11428
+ const libPath = join9(dir, libName);
11254
11429
  if (process.platform === "win32") {
11255
11430
  if (!directoryContainsLibrary(dir, libName))
11256
11431
  continue;
@@ -11259,6 +11434,10 @@ function findSystemOnnxRuntime(libName) {
11259
11434
  }
11260
11435
  const version = detectOnnxVersion(dir, libName);
11261
11436
  if (!version) {
11437
+ if (isWindowsSystem32Directory(dir)) {
11438
+ warn(`Skipping unversioned Windows system ONNX Runtime at ${dir}; falling through to AFT-managed download.`);
11439
+ continue;
11440
+ }
11262
11441
  unknownVersionPaths.push(dir);
11263
11442
  continue;
11264
11443
  }
@@ -11286,7 +11465,7 @@ async function downloadFileWithCap(url, destPath) {
11286
11465
  if (Number.isFinite(advertised) && advertised > MAX_DOWNLOAD_BYTES2) {
11287
11466
  throw new Error(`Content-Length ${advertised} exceeds max ${MAX_DOWNLOAD_BYTES2}`);
11288
11467
  }
11289
- mkdirSync5(dirname4(destPath), { recursive: true });
11468
+ mkdirSync5(dirname5(destPath), { recursive: true });
11290
11469
  let bytesWritten = 0;
11291
11470
  const guard = new TransformStream({
11292
11471
  transform(chunk, transformController) {
@@ -11316,11 +11495,11 @@ function validateExtractedTree(stagingRoot) {
11316
11495
  const walk = (dir) => {
11317
11496
  const entries = readdirSync2(dir);
11318
11497
  for (const entry of entries) {
11319
- const fullPath = join8(dir, entry);
11498
+ const fullPath = join9(dir, entry);
11320
11499
  const lst = lstatSync(fullPath);
11321
11500
  if (lst.isSymbolicLink()) {
11322
11501
  const linkTarget = readlinkSync(fullPath);
11323
- const resolvedTarget = resolve5(dirname4(fullPath), linkTarget);
11502
+ const resolvedTarget = resolve5(dirname5(fullPath), linkTarget);
11324
11503
  const rel2 = relative2(realRoot, resolvedTarget);
11325
11504
  if (rel2.startsWith("..") || isAbsolute4(rel2) || win32.isAbsolute(rel2)) {
11326
11505
  throw new Error(`extracted symlink ${fullPath} points outside staging root: ${linkTarget}`);
@@ -11351,7 +11530,7 @@ async function downloadOnnxRuntime(info, targetDir) {
11351
11530
  const tmpDir = `${targetDir}.tmp.${process.pid}.${Date.now().toString(36)}`;
11352
11531
  try {
11353
11532
  mkdirSync5(tmpDir, { recursive: true });
11354
- const archivePath = join8(tmpDir, `onnxruntime.${info.archiveType}`);
11533
+ const archivePath = join9(tmpDir, `onnxruntime.${info.archiveType}`);
11355
11534
  await downloadFileWithCap(url, archivePath);
11356
11535
  const archiveSha256 = sha256File(archivePath);
11357
11536
  log(`ONNX Runtime archive sha256=${archiveSha256}`);
@@ -11367,7 +11546,7 @@ async function downloadOnnxRuntime(info, targetDir) {
11367
11546
  unlinkSync4(archivePath);
11368
11547
  } catch {}
11369
11548
  validateExtractedTree(tmpDir);
11370
- const extractedDir = join8(tmpDir, info.assetName, "lib");
11549
+ const extractedDir = join9(tmpDir, info.assetName, "lib");
11371
11550
  if (!existsSync6(extractedDir)) {
11372
11551
  throw new Error(`Expected directory not found: ${extractedDir}`);
11373
11552
  }
@@ -11376,11 +11555,11 @@ async function downloadOnnxRuntime(info, targetDir) {
11376
11555
  const realFiles = [];
11377
11556
  const symlinks = [];
11378
11557
  for (const libFile of libFiles) {
11379
- const src = join8(extractedDir, libFile);
11558
+ const src = join9(extractedDir, libFile);
11380
11559
  try {
11381
- const stat = lstatSync(src);
11382
- log(`ORT extract: ${libFile} — isSymlink=${stat.isSymbolicLink()}, isFile=${stat.isFile()}, size=${stat.size}`);
11383
- if (stat.isSymbolicLink()) {
11560
+ const stat2 = lstatSync(src);
11561
+ log(`ORT extract: ${libFile} — isSymlink=${stat2.isSymbolicLink()}, isFile=${stat2.isFile()}, size=${stat2.size}`);
11562
+ if (stat2.isSymbolicLink()) {
11384
11563
  symlinks.push({ name: libFile, target: readlinkSync(src) });
11385
11564
  } else {
11386
11565
  realFiles.push(libFile);
@@ -11391,7 +11570,7 @@ async function downloadOnnxRuntime(info, targetDir) {
11391
11570
  }
11392
11571
  }
11393
11572
  copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks);
11394
- const libPath = join8(targetDir, info.libName);
11573
+ const libPath = join9(targetDir, info.libName);
11395
11574
  let libHash = null;
11396
11575
  try {
11397
11576
  libHash = sha256File(libPath);
@@ -11416,8 +11595,8 @@ async function downloadOnnxRuntime(info, targetDir) {
11416
11595
  function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, copyFile = copyFileSync3) {
11417
11596
  const requiredLibs = new Set([info.libName]);
11418
11597
  for (const libFile of realFiles) {
11419
- const src = join8(extractedDir, libFile);
11420
- const dst = join8(targetDir, libFile);
11598
+ const src = join9(extractedDir, libFile);
11599
+ const dst = join9(targetDir, libFile);
11421
11600
  try {
11422
11601
  copyFile(src, dst);
11423
11602
  if (process.platform !== "win32") {
@@ -11433,12 +11612,12 @@ function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, c
11433
11612
  }
11434
11613
  const targetRoot = realpathSync(targetDir);
11435
11614
  for (const link of symlinks) {
11436
- const dst = join8(targetDir, link.name);
11615
+ const dst = join9(targetDir, link.name);
11437
11616
  try {
11438
11617
  unlinkSync4(dst);
11439
11618
  } catch {}
11440
- const dstForContainment = join8(targetRoot, link.name);
11441
- const resolvedTarget = resolve5(dirname4(dstForContainment), link.target);
11619
+ const dstForContainment = join9(targetRoot, link.name);
11620
+ const resolvedTarget = resolve5(dirname5(dstForContainment), link.target);
11442
11621
  if (!isPathInsideRoot(targetRoot, resolvedTarget)) {
11443
11622
  const message = `ONNX Runtime symlink ${link.name} points outside install dir: ${link.target}`;
11444
11623
  if (requiredLibs.has(link.name)) {
@@ -11458,7 +11637,7 @@ function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, c
11458
11637
  log(`ORT extract: failed to symlink optional ${link.name}: ${symlinkErr}`);
11459
11638
  }
11460
11639
  }
11461
- const requiredPath = join8(targetDir, info.libName);
11640
+ const requiredPath = join9(targetDir, info.libName);
11462
11641
  if (!existsSync6(requiredPath)) {
11463
11642
  rmSync3(targetDir, { recursive: true, force: true });
11464
11643
  throw new Error(`Required ONNX Runtime library missing after install: ${requiredPath}`);
@@ -11485,13 +11664,13 @@ function writeOnnxInstalledMeta(installDir, version, sha256, archiveSha256) {
11485
11664
  ...sha256 ? { sha256 } : {},
11486
11665
  archiveSha256
11487
11666
  };
11488
- writeFileSync3(join8(installDir, ONNX_INSTALLED_META_FILE), JSON.stringify(meta), "utf8");
11667
+ writeFileSync3(join9(installDir, ONNX_INSTALLED_META_FILE), JSON.stringify(meta), "utf8");
11489
11668
  } catch (err) {
11490
11669
  log(`[onnx] failed to write installed-meta in ${installDir}: ${err}`);
11491
11670
  }
11492
11671
  }
11493
11672
  function readOnnxInstalledMeta(installDir) {
11494
- const path2 = join8(installDir, ONNX_INSTALLED_META_FILE);
11673
+ const path2 = join9(installDir, ONNX_INSTALLED_META_FILE);
11495
11674
  try {
11496
11675
  if (!statSync4(path2).isFile())
11497
11676
  return null;
@@ -11628,7 +11807,7 @@ function isProcessAlive(pid) {
11628
11807
  }
11629
11808
  }
11630
11809
  // ../aft-bridge/dist/pool.js
11631
- import { homedir as homedir9 } from "node:os";
11810
+ import { homedir as homedir10 } from "node:os";
11632
11811
 
11633
11812
  // ../aft-bridge/dist/project-identity.js
11634
11813
  import { realpathSync as realpathSync2 } from "node:fs";
@@ -11667,7 +11846,7 @@ var DEFAULT_MAX_POOL_SIZE = 8;
11667
11846
  var CLEANUP_INTERVAL_MS = 60 * 1000;
11668
11847
  function canonicalHomeDir() {
11669
11848
  try {
11670
- const home = homedir9();
11849
+ const home = homedir10();
11671
11850
  if (!home)
11672
11851
  return null;
11673
11852
  return canonicalizeProjectRoot(home);
@@ -11693,6 +11872,7 @@ class BridgePool {
11693
11872
  projectConfigLoader;
11694
11873
  logger;
11695
11874
  cleanupTimer = null;
11875
+ shutdownCalled = false;
11696
11876
  constructor(binaryPath, options = {}, configOverrides = {}) {
11697
11877
  this.binaryPath = binaryPath;
11698
11878
  this.maxPoolSize = options.maxPoolSize ?? DEFAULT_MAX_POOL_SIZE;
@@ -11714,12 +11894,11 @@ class BridgePool {
11714
11894
  childEnv: options.childEnv
11715
11895
  };
11716
11896
  this.configOverrides = configOverrides;
11717
- if (Number.isFinite(this.idleTimeoutMs)) {
11718
- this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL_MS);
11719
- this.cleanupTimer.unref();
11720
- }
11897
+ this.startCleanupTimer();
11721
11898
  }
11722
11899
  getActiveBridgeForRoot(projectRoot) {
11900
+ if (this.shutdownCalled)
11901
+ return null;
11723
11902
  const key = normalizeKey(projectRoot);
11724
11903
  const entry = this.bridges.get(key);
11725
11904
  if (!entry?.bridge.isAlive())
@@ -11727,7 +11906,21 @@ class BridgePool {
11727
11906
  entry.lastUsed = Date.now();
11728
11907
  return entry.bridge;
11729
11908
  }
11909
+ activeBridges() {
11910
+ if (this.shutdownCalled)
11911
+ return [];
11912
+ const alive = [];
11913
+ for (const entry of this.bridges.values()) {
11914
+ if (entry.bridge.isAlive())
11915
+ alive.push(entry.bridge);
11916
+ }
11917
+ return alive;
11918
+ }
11730
11919
  getBridge(projectRoot) {
11920
+ if (this.shutdownCalled) {
11921
+ this.shutdownCalled = false;
11922
+ this.startCleanupTimer();
11923
+ }
11731
11924
  const key = normalizeKey(projectRoot);
11732
11925
  const existing = this.bridges.get(key);
11733
11926
  if (existing) {
@@ -11737,15 +11930,7 @@ class BridgePool {
11737
11930
  if (this.bridges.size >= this.maxPoolSize) {
11738
11931
  this.evictLRU();
11739
11932
  }
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
- }
11933
+ const projectOverrides = this.loadProjectOverrides(key);
11749
11934
  const mergedOverrides = { ...this.configOverrides, ...projectOverrides };
11750
11935
  const bridge = new BinaryBridge(this.binaryPath, key, this.bridgeOptions, mergedOverrides);
11751
11936
  this.bridges.set(key, { bridge, lastUsed: Date.now() });
@@ -11795,6 +11980,7 @@ class BridgePool {
11795
11980
  }
11796
11981
  async closeSession(_projectRoot, _session) {}
11797
11982
  async shutdown() {
11983
+ this.shutdownCalled = true;
11798
11984
  if (this.cleanupTimer) {
11799
11985
  clearInterval(this.cleanupTimer);
11800
11986
  this.cleanupTimer = null;
@@ -11807,6 +11993,9 @@ class BridgePool {
11807
11993
  this.staleBridges.clear();
11808
11994
  await Promise.allSettled(shutdowns);
11809
11995
  }
11996
+ isShutdown() {
11997
+ return this.shutdownCalled;
11998
+ }
11810
11999
  async replaceBinary(newPath) {
11811
12000
  this.binaryPath = newPath;
11812
12001
  for (const entry of this.bridges.values()) {
@@ -11816,6 +12005,12 @@ class BridgePool {
11816
12005
  this.log(`Binary path updated to ${newPath}. Active bridges marked stale — next calls will use the new binary.`);
11817
12006
  return newPath;
11818
12007
  }
12008
+ startCleanupTimer() {
12009
+ if (!Number.isFinite(this.idleTimeoutMs) || this.cleanupTimer)
12010
+ return;
12011
+ this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL_MS);
12012
+ this.cleanupTimer.unref();
12013
+ }
11819
12014
  log(message, meta) {
11820
12015
  const logger = this.logger ?? getActiveLogger();
11821
12016
  if (logger) {
@@ -11847,6 +12042,36 @@ class BridgePool {
11847
12042
  this.configOverrides[key] = value;
11848
12043
  }
11849
12044
  }
12045
+ async reconfigure(projectRoot, overrides) {
12046
+ const key = normalizeKey(projectRoot);
12047
+ for (const [name, value] of Object.entries(overrides)) {
12048
+ this.setConfigureOverride(name, value);
12049
+ }
12050
+ const bridge = this.getActiveBridgeForRoot(key);
12051
+ if (!bridge)
12052
+ return;
12053
+ const projectOverrides = this.loadProjectOverrides(key);
12054
+ const response = await bridge.send("configure", {
12055
+ project_root: key,
12056
+ ...this.configOverrides,
12057
+ ...projectOverrides,
12058
+ ...overrides
12059
+ });
12060
+ if (response.success === false) {
12061
+ throw new Error(`configure refresh failed for ${key}: ${String(response.message ?? "unknown error")}`);
12062
+ }
12063
+ }
12064
+ loadProjectOverrides(key) {
12065
+ if (!this.projectConfigLoader)
12066
+ return {};
12067
+ try {
12068
+ return this.projectConfigLoader(key) ?? {};
12069
+ } catch (err) {
12070
+ const message = err instanceof Error ? err.message : String(err);
12071
+ this.error(`projectConfigLoader failed; using global overrides only: ${message}`);
12072
+ return {};
12073
+ }
12074
+ }
11850
12075
  get size() {
11851
12076
  return this.bridges.size;
11852
12077
  }
@@ -11860,11 +12085,185 @@ class BridgePool {
11860
12085
  function normalizeKey(projectRoot) {
11861
12086
  return canonicalizeProjectRoot(projectRoot);
11862
12087
  }
11863
- // ../../node_modules/.bun/@cortexkit+subc-client@0.3.4/node_modules/@cortexkit/subc-client/dist/client.js
12088
+ // ../aft-bridge/dist/revivable-transport.js
12089
+ class RevivableTransportPool {
12090
+ createPool;
12091
+ onBinaryReplaced;
12092
+ activePool;
12093
+ revival = null;
12094
+ transports = new Map;
12095
+ configureOverrides = new Map;
12096
+ constructor(initialPool, createPool, onBinaryReplaced) {
12097
+ this.createPool = createPool;
12098
+ this.onBinaryReplaced = onBinaryReplaced;
12099
+ this.activePool = initialPool;
12100
+ }
12101
+ getBridge(projectRoot) {
12102
+ const key = canonicalizeProjectRoot(projectRoot);
12103
+ let transport = this.transports.get(key);
12104
+ if (!transport) {
12105
+ transport = new RevivableProjectTransport(this, key);
12106
+ this.transports.set(key, transport);
12107
+ }
12108
+ return transport;
12109
+ }
12110
+ activeBridges() {
12111
+ return this.activePool.activeBridges();
12112
+ }
12113
+ getActiveBridgeForRoot(projectRoot) {
12114
+ const key = canonicalizeProjectRoot(projectRoot);
12115
+ const bridge = this.currentBridge(key);
12116
+ if (!bridge)
12117
+ return null;
12118
+ const transport = this.getBridge(key);
12119
+ transport.refreshStatusSubscription(bridge);
12120
+ return transport;
12121
+ }
12122
+ async toolCall(projectRoot, runtime, name, rawArgs = {}, options) {
12123
+ return this.getBridge(projectRoot).toolCall(runtime.sessionID, name, rawArgs, options);
12124
+ }
12125
+ setConfigureOverride(key, value) {
12126
+ if (value === undefined)
12127
+ this.configureOverrides.delete(key);
12128
+ else
12129
+ this.configureOverrides.set(key, value);
12130
+ this.activePool.setConfigureOverride(key, value);
12131
+ }
12132
+ async reconfigure(projectRoot, overrides) {
12133
+ for (const [key, value] of Object.entries(overrides)) {
12134
+ if (value === undefined)
12135
+ this.configureOverrides.delete(key);
12136
+ else
12137
+ this.configureOverrides.set(key, value);
12138
+ }
12139
+ const pool = await this.ensureActivePool();
12140
+ await pool.reconfigure(projectRoot, overrides);
12141
+ }
12142
+ async replaceBinary(path2) {
12143
+ const replaced = await this.activePool.replaceBinary(path2);
12144
+ this.onBinaryReplaced?.(replaced);
12145
+ return replaced;
12146
+ }
12147
+ closeSession(projectRoot, session) {
12148
+ return this.activePool.closeSession(projectRoot, session);
12149
+ }
12150
+ async shutdown() {
12151
+ const revival = this.revival;
12152
+ if (revival) {
12153
+ await Promise.allSettled([revival]);
12154
+ }
12155
+ await this.activePool.shutdown();
12156
+ for (const transport of this.transports.values()) {
12157
+ transport.refreshStatusSubscription(null);
12158
+ }
12159
+ }
12160
+ isShutdown() {
12161
+ return this.activePool.isShutdown();
12162
+ }
12163
+ async ensureActivePool() {
12164
+ if (!this.activePool.isShutdown())
12165
+ return this.activePool;
12166
+ if (this.revival)
12167
+ return this.revival;
12168
+ warn("transport was shut down but new demand arrived — reviving (host quit hook fired without process exit?)");
12169
+ const revival = this.createPool().then((pool) => {
12170
+ for (const [key, value] of this.configureOverrides) {
12171
+ pool.setConfigureOverride(key, value);
12172
+ }
12173
+ this.activePool = pool;
12174
+ for (const [root, transport] of this.transports) {
12175
+ transport.refreshStatusSubscription(pool.getActiveBridgeForRoot(root));
12176
+ }
12177
+ return pool;
12178
+ });
12179
+ this.revival = revival;
12180
+ revival.then(() => {
12181
+ if (this.revival === revival)
12182
+ this.revival = null;
12183
+ }, () => {
12184
+ if (this.revival === revival)
12185
+ this.revival = null;
12186
+ });
12187
+ return revival;
12188
+ }
12189
+ currentBridge(projectRoot) {
12190
+ return this.activePool.getActiveBridgeForRoot(projectRoot);
12191
+ }
12192
+ async send(projectRoot, command, params, options) {
12193
+ const pool = await this.ensureActivePool();
12194
+ const bridge = pool.getBridge(projectRoot);
12195
+ this.getBridge(projectRoot).refreshStatusSubscription(bridge);
12196
+ return bridge.send(command, params, options);
12197
+ }
12198
+ async toolCallOnProject(projectRoot, sessionId, name, rawArgs, options) {
12199
+ const pool = await this.ensureActivePool();
12200
+ const bridge = pool.getBridge(projectRoot);
12201
+ this.getBridge(projectRoot).refreshStatusSubscription(bridge);
12202
+ return bridge.toolCall(sessionId, name, rawArgs, options);
12203
+ }
12204
+ }
12205
+
12206
+ class RevivableProjectTransport {
12207
+ owner;
12208
+ projectRoot;
12209
+ statusListeners = new Map;
12210
+ statusBridges = new Map;
12211
+ constructor(owner, projectRoot) {
12212
+ this.owner = owner;
12213
+ this.projectRoot = projectRoot;
12214
+ }
12215
+ getCwd() {
12216
+ return this.projectRoot;
12217
+ }
12218
+ getStatusBar() {
12219
+ return this.owner.currentBridge(this.projectRoot)?.getStatusBar();
12220
+ }
12221
+ getCachedStatus() {
12222
+ return this.owner.currentBridge(this.projectRoot)?.getCachedStatus() ?? null;
12223
+ }
12224
+ cacheStatusSnapshot(snapshot) {
12225
+ this.owner.currentBridge(this.projectRoot)?.cacheStatusSnapshot(snapshot);
12226
+ }
12227
+ send(command, params, options) {
12228
+ return this.owner.send(this.projectRoot, command, params, options);
12229
+ }
12230
+ toolCall(sessionId, name, rawArgs, options) {
12231
+ return this.owner.toolCallOnProject(this.projectRoot, sessionId, name, rawArgs, options);
12232
+ }
12233
+ subscribeStatus(listener) {
12234
+ if (this.statusListeners.has(listener))
12235
+ return () => this.removeStatusListener(listener);
12236
+ this.statusListeners.set(listener, () => {});
12237
+ this.bindStatusListener(listener, this.owner.currentBridge(this.projectRoot));
12238
+ return () => this.removeStatusListener(listener);
12239
+ }
12240
+ refreshStatusSubscription(bridge) {
12241
+ for (const listener of this.statusListeners.keys()) {
12242
+ this.bindStatusListener(listener, bridge);
12243
+ }
12244
+ }
12245
+ bindStatusListener(listener, bridge) {
12246
+ if (this.statusBridges.get(listener) === bridge)
12247
+ return;
12248
+ const previousUnsubscribe = this.statusListeners.get(listener);
12249
+ previousUnsubscribe?.();
12250
+ const maybe = bridge;
12251
+ const unsubscribe = maybe && typeof maybe.subscribeStatus === "function" ? maybe.subscribeStatus(listener) : () => {};
12252
+ this.statusListeners.set(listener, unsubscribe);
12253
+ this.statusBridges.set(listener, bridge);
12254
+ }
12255
+ removeStatusListener(listener) {
12256
+ const unsubscribe = this.statusListeners.get(listener);
12257
+ this.statusListeners.delete(listener);
12258
+ this.statusBridges.delete(listener);
12259
+ unsubscribe?.();
12260
+ }
12261
+ }
12262
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/client.js
11864
12263
  import { promises as fs2 } from "node:fs";
11865
12264
  import { debuglog } from "node:util";
11866
12265
 
11867
- // ../../node_modules/.bun/@cortexkit+subc-client@0.3.4/node_modules/@cortexkit/subc-client/dist/auth.js
12266
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/auth.js
11868
12267
  import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
11869
12268
  var NONCE_LEN = 32;
11870
12269
  var MAX_AUTH_MESSAGE_LEN = 4096;
@@ -11928,8 +12327,146 @@ async function authenticateClient(sock, conn, deadlineMs) {
11928
12327
  await writeMessage(sock, { client_auth: Array.from(clientAuth) }, deadlineMs);
11929
12328
  }
11930
12329
 
11931
- // ../../node_modules/.bun/@cortexkit+subc-client@0.3.4/node_modules/@cortexkit/subc-client/dist/connection-file.js
12330
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/connection-file.js
11932
12331
  import { promises as fs } from "node:fs";
12332
+
12333
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/envelope.js
12334
+ var PROTOCOL_VERSION = 2;
12335
+ var HEADER_LEN = 21;
12336
+ var FROZEN_PREFIX_LEN = 5;
12337
+ var MAX_FRAME_BODY_LEN = 64 * 1024 * 1024;
12338
+ var FrameType;
12339
+ (function(FrameType2) {
12340
+ FrameType2[FrameType2["Request"] = 0] = "Request";
12341
+ FrameType2[FrameType2["Response"] = 1] = "Response";
12342
+ FrameType2[FrameType2["Push"] = 2] = "Push";
12343
+ FrameType2[FrameType2["StreamData"] = 3] = "StreamData";
12344
+ FrameType2[FrameType2["StreamEnd"] = 4] = "StreamEnd";
12345
+ FrameType2[FrameType2["Error"] = 5] = "Error";
12346
+ FrameType2[FrameType2["Cancel"] = 6] = "Cancel";
12347
+ FrameType2[FrameType2["Ping"] = 7] = "Ping";
12348
+ FrameType2[FrameType2["Pong"] = 8] = "Pong";
12349
+ FrameType2[FrameType2["Hello"] = 9] = "Hello";
12350
+ FrameType2[FrameType2["HelloAck"] = 10] = "HelloAck";
12351
+ FrameType2[FrameType2["Goodbye"] = 11] = "Goodbye";
12352
+ })(FrameType || (FrameType = {}));
12353
+ var FRAME_TYPE_MAX = FrameType.Goodbye;
12354
+ function isPureHeader(ty) {
12355
+ return ty === FrameType.Cancel || ty === FrameType.Ping || ty === FrameType.Pong || ty === FrameType.Goodbye;
12356
+ }
12357
+ var Priority;
12358
+ (function(Priority2) {
12359
+ Priority2[Priority2["Passive"] = 0] = "Passive";
12360
+ Priority2[Priority2["Interactive"] = 1] = "Interactive";
12361
+ Priority2[Priority2["Background"] = 2] = "Background";
12362
+ })(Priority || (Priority = {}));
12363
+ var AdmissionClass;
12364
+ (function(AdmissionClass2) {
12365
+ AdmissionClass2[AdmissionClass2["Normal"] = 0] = "Normal";
12366
+ AdmissionClass2[AdmissionClass2["Expedite"] = 1] = "Expedite";
12367
+ AdmissionClass2[AdmissionClass2["Sheddable"] = 2] = "Sheddable";
12368
+ })(AdmissionClass || (AdmissionClass = {}));
12369
+ var FLAG_BINARY = 1;
12370
+ var FLAG_PRIORITY_MASK = 6;
12371
+ var FLAG_PRIORITY_SHIFT = 1;
12372
+ var FLAG_LAST = 8;
12373
+ var FLAG_ADMISSION_MASK = 48;
12374
+ var FLAG_ADMISSION_SHIFT = 4;
12375
+ var FLAG_RESERVED_MASK = 192;
12376
+ function buildFlags(binary, priority, last, admissionClass = AdmissionClass.Normal) {
12377
+ let flags = 0;
12378
+ if (binary)
12379
+ flags |= FLAG_BINARY;
12380
+ flags |= priority << FLAG_PRIORITY_SHIFT;
12381
+ if (last)
12382
+ flags |= FLAG_LAST;
12383
+ flags |= admissionClass << FLAG_ADMISSION_SHIFT;
12384
+ return flags;
12385
+ }
12386
+ function encodeHeader(header) {
12387
+ const buffer = new Uint8Array(HEADER_LEN);
12388
+ const view = new DataView(buffer.buffer);
12389
+ view.setUint32(0, header.len, true);
12390
+ buffer[4] = header.ver;
12391
+ buffer[5] = header.ty;
12392
+ buffer[6] = header.flags;
12393
+ view.setUint16(7, header.channel, true);
12394
+ view.setUint32(9, header.epoch, true);
12395
+ view.setBigUint64(13, header.corr, true);
12396
+ return buffer;
12397
+ }
12398
+
12399
+ class DecodeError extends Error {
12400
+ code;
12401
+ constructor(message, code) {
12402
+ super(message);
12403
+ this.code = code;
12404
+ this.name = "DecodeError";
12405
+ }
12406
+ }
12407
+ function decodeHeader(bytes) {
12408
+ if (bytes.length < FROZEN_PREFIX_LEN) {
12409
+ throw new DecodeError(`header shorter than frozen prefix: have ${bytes.length} bytes`, "too_short_for_prefix");
12410
+ }
12411
+ const ver = bytes[4];
12412
+ if (ver !== PROTOCOL_VERSION)
12413
+ throw new DecodeError(`unsupported envelope version ${ver}`, "unsupported_version");
12414
+ if (bytes.length < HEADER_LEN) {
12415
+ throw new DecodeError(`header too short for version: have ${bytes.length} bytes, need ${HEADER_LEN}`, "too_short_for_header");
12416
+ }
12417
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
12418
+ const len = view.getUint32(0, true);
12419
+ const typeByte = bytes[5];
12420
+ if (typeByte > FRAME_TYPE_MAX)
12421
+ throw new DecodeError(`unknown frame type byte ${typeByte}`, "unknown_frame_type");
12422
+ const ty = typeByte;
12423
+ const flags = bytes[6];
12424
+ if ((flags & FLAG_RESERVED_MASK) !== 0) {
12425
+ throw new DecodeError(`reserved flag bits set in flags 0b${flags.toString(2).padStart(8, "0")}`, "reserved_flag_bits");
12426
+ }
12427
+ if ((flags & FLAG_PRIORITY_MASK) >> FLAG_PRIORITY_SHIFT === 3) {
12428
+ throw new DecodeError(`reserved priority bits set in flags 0b${flags.toString(2).padStart(8, "0")}`, "reserved_priority_bits");
12429
+ }
12430
+ const admission = (flags & FLAG_ADMISSION_MASK) >> FLAG_ADMISSION_SHIFT;
12431
+ if (admission === 3) {
12432
+ throw new DecodeError(`reserved admission class set in flags 0b${flags.toString(2).padStart(8, "0")}`, "reserved_admission_class");
12433
+ }
12434
+ if (admission === AdmissionClass.Sheddable && ty !== FrameType.Push && ty !== FrameType.StreamData) {
12435
+ throw new DecodeError(`SHEDDABLE admission class is illegal on ${FrameType[ty]} in flags 0b${flags.toString(2).padStart(8, "0")}`, "sheddable_illegal_frame_type");
12436
+ }
12437
+ const channel = view.getUint16(7, true);
12438
+ const epoch = view.getUint32(9, true);
12439
+ if (channel === 0 && epoch !== 0) {
12440
+ throw new DecodeError(`control channel carried nonzero epoch ${epoch}`, "nonzero_epoch_on_control_channel");
12441
+ }
12442
+ if (isPureHeader(ty) && len !== 0) {
12443
+ throw new DecodeError(`pure-header frame ${FrameType[ty]} declared non-zero body length ${len}`, "pure_header_frame_with_body");
12444
+ }
12445
+ return { len, ver, ty, flags, channel, epoch, corr: view.getBigUint64(13, true) };
12446
+ }
12447
+ function buildFrame(ty, flags, channel, epoch, corr, body) {
12448
+ return buildFrameWithVersion(PROTOCOL_VERSION, ty, flags, channel, epoch, corr, body);
12449
+ }
12450
+ function buildFrameWithVersion(ver, ty, flags, channel, epoch, corr, body) {
12451
+ if (body.length > MAX_FRAME_BODY_LEN) {
12452
+ throw new DecodeError(`frame body ${body.length} exceeds max ${MAX_FRAME_BODY_LEN}`, "frame_body_too_large");
12453
+ }
12454
+ const header = { len: body.length, ver, ty, flags, channel, epoch, corr };
12455
+ decodeHeader(encodeHeader(header));
12456
+ return { header, body };
12457
+ }
12458
+ function encodeFrame(frame) {
12459
+ if (frame.header.len !== frame.body.length) {
12460
+ throw new DecodeError(`frame header length ${frame.header.len} does not match body length ${frame.body.length}`, "frame_length_mismatch");
12461
+ }
12462
+ const header = encodeHeader(frame.header);
12463
+ const output = new Uint8Array(header.length + frame.body.length);
12464
+ output.set(header, 0);
12465
+ output.set(frame.body, header.length);
12466
+ return output;
12467
+ }
12468
+
12469
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/connection-file.js
11933
12470
  var SCHEMA_VERSION = 1;
11934
12471
  var MIN_KEY_LEN = 32;
11935
12472
  var DAEMON_ID_LEN = 16;
@@ -11959,8 +12496,8 @@ function validate(info) {
11959
12496
  async function verifyOwnerOnly(path2) {
11960
12497
  if (process.platform === "win32")
11961
12498
  return;
11962
- const stat = await fs.stat(path2);
11963
- const mode = stat.mode & 511;
12499
+ const stat2 = await fs.stat(path2);
12500
+ const mode = stat2.mode & 511;
11964
12501
  if ((mode & 63) !== 0) {
11965
12502
  throw new ConnectionFileError(`connection file ${path2} has insecure permissions 0o${mode.toString(8)}; expected owner-only 0600`);
11966
12503
  }
@@ -11974,6 +12511,10 @@ async function readConnectionFile(path2) {
11974
12511
  } catch (err) {
11975
12512
  throw new ConnectionFileError(`connection file JSON read failed for ${path2}: ${String(err)}`);
11976
12513
  }
12514
+ const wireVersion = parsed.wire_version;
12515
+ if (wireVersion !== undefined && wireVersion !== PROTOCOL_VERSION) {
12516
+ throw new ConnectionFileError(`connection file wire_version ${String(wireVersion)} but this client speaks ${PROTOCOL_VERSION}; the client library must be upgraded`);
12517
+ }
11977
12518
  const endpointsRaw = parsed.endpoints;
11978
12519
  if (!Array.isArray(endpointsRaw)) {
11979
12520
  throw new ConnectionFileError("connection file 'endpoints' must be an array");
@@ -11989,130 +12530,62 @@ async function readConnectionFile(path2) {
11989
12530
  schema: parsed.schema,
11990
12531
  endpoints,
11991
12532
  key: toBytes(parsed.key, "key"),
11992
- daemonId: toBytes(parsed.daemon_id, "daemon_id"),
11993
- 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;
12533
+ daemonId: toBytes(parsed.daemon_id, "daemon_id"),
12534
+ pid: parsed.pid,
12535
+ daemonVer: parsed.daemon_ver ?? ""
12536
+ };
12537
+ validate(info);
12538
+ return info;
12054
12539
  }
12055
12540
 
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)}`);
12541
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/route-handle.js
12542
+ var connectionToken = new WeakMap;
12543
+
12544
+ class RouteHandle {
12545
+ channel;
12546
+ epoch;
12547
+ constructor(channel, epoch, token) {
12548
+ if (!Number.isInteger(channel) || channel <= 0 || channel > 65535) {
12549
+ throw new RangeError(`route channel must be an integer in 1..65535, got ${channel}`);
12550
+ }
12551
+ if (!Number.isInteger(epoch) || epoch <= 0 || epoch > 4294967295) {
12552
+ throw new RangeError(`route epoch must be an integer in 1..4294967295, got ${epoch}`);
12553
+ }
12554
+ this.channel = channel;
12555
+ this.epoch = epoch;
12556
+ connectionToken.set(this, token);
12557
+ Object.freeze(this);
12082
12558
  }
12083
- if (isPureHeader(ty) && len !== 0) {
12084
- throw new DecodeError(`pure-header frame ${FrameType[ty]} declared non-zero body length ${len}`);
12559
+ static create(channel, epoch, token) {
12560
+ return new RouteHandle(channel, epoch, token);
12085
12561
  }
12086
- const channel = view.getUint16(7, true);
12087
- const corr = view.getBigUint64(9, true);
12088
- return { len, ver, ty, flags, channel, corr };
12089
12562
  }
12090
- function buildFrame(ty, flags, channel, corr, body) {
12091
- return buildFrameWithVersion(PROTOCOL_VERSION, ty, flags, channel, corr, body);
12563
+ function createRouteHandle(channel, epoch, token) {
12564
+ const factory = RouteHandle;
12565
+ return factory.create(channel, epoch, token);
12092
12566
  }
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`);
12567
+
12568
+ class StaleRouteHandleError extends Error {
12569
+ handle;
12570
+ code = "stale_route_handle";
12571
+ constructor(handle) {
12572
+ super(`route handle (${handle.channel}, ${handle.epoch}) is not live on the current connection`);
12573
+ this.handle = handle;
12574
+ this.name = "StaleRouteHandleError";
12099
12575
  }
12100
- return {
12101
- header: { len: body.length, ver, ty, flags, channel, corr },
12102
- body
12103
- };
12104
12576
  }
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;
12577
+ function newConnectionToken() {
12578
+ return Object.freeze({});
12579
+ }
12580
+ function belongsToConnection(handle, token) {
12581
+ return connectionToken.get(handle) === token;
12582
+ }
12583
+ function sameRouteHandle(left, right) {
12584
+ return left === right;
12111
12585
  }
12112
12586
 
12113
- // ../../node_modules/.bun/@cortexkit+subc-client@0.3.4/node_modules/@cortexkit/subc-client/dist/socket.js
12587
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/socket.js
12114
12588
  import net from "node:net";
12115
-
12116
12589
  class SocketClosedError extends Error {
12117
12590
  }
12118
12591
 
@@ -12181,6 +12654,24 @@ class SubcSocket {
12181
12654
  });
12182
12655
  });
12183
12656
  }
12657
+ async readFrame(headerDeadlineMs, bodyDeadline, onHeader) {
12658
+ const prefix = await this.readExact(FROZEN_PREFIX_LEN, headerDeadlineMs);
12659
+ const version = prefix[4];
12660
+ if (version !== PROTOCOL_VERSION)
12661
+ throw new DecodeError(`unsupported envelope version ${version}`, "unsupported_version");
12662
+ const remainder = await this.readExact(HEADER_LEN - FROZEN_PREFIX_LEN, headerDeadlineMs);
12663
+ const headerBytes = new Uint8Array(HEADER_LEN);
12664
+ headerBytes.set(prefix);
12665
+ headerBytes.set(remainder, FROZEN_PREFIX_LEN);
12666
+ const header = decodeHeader(headerBytes);
12667
+ if (header.len > MAX_FRAME_BODY_LEN) {
12668
+ throw new DecodeError(`frame body ${header.len} exceeds max ${MAX_FRAME_BODY_LEN}`, "frame_body_too_large");
12669
+ }
12670
+ onHeader?.();
12671
+ const bodyDeadlineMs = typeof bodyDeadline === "number" ? bodyDeadline : Date.now() + bodyDeadline.afterHeaderMs;
12672
+ const body = header.len === 0 ? new Uint8Array(0) : await this.readExact(header.len, bodyDeadlineMs);
12673
+ return { header, body };
12674
+ }
12184
12675
  readExact(n, deadlineMs) {
12185
12676
  if (this.waiter) {
12186
12677
  return Promise.reject(new Error("concurrent readExact is not supported"));
@@ -12302,7 +12793,7 @@ class SubcSocket {
12302
12793
  }
12303
12794
  }
12304
12795
 
12305
- // ../../node_modules/.bun/@cortexkit+subc-client@0.3.4/node_modules/@cortexkit/subc-client/dist/client.js
12796
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/client.js
12306
12797
  var debug = debuglog("subc-client");
12307
12798
  var DEFAULT_HANDSHAKE_TIMEOUT_MS = 1e4;
12308
12799
  var DEFAULT_REQUEST_TIMEOUT_MS = 30000;
@@ -12348,7 +12839,11 @@ class SubcClient {
12348
12839
  opts;
12349
12840
  nextCorr = 1n;
12350
12841
  pending = new Map;
12842
+ lateResponses = new Map;
12351
12843
  routes = new Map;
12844
+ liveRoutes = new Map;
12845
+ connectionToken = newConnectionToken();
12846
+ ingressEpochDropCount = 0;
12352
12847
  closedErr = null;
12353
12848
  closeStarted = false;
12354
12849
  reconnecting = null;
@@ -12376,40 +12871,71 @@ class SubcClient {
12376
12871
  }
12377
12872
  async routeOpen(target, identity, opts = {}) {
12378
12873
  const consumerIdentity = routeOpenConsumerIdentity(opts);
12874
+ const consumerCapabilities = opts.consumerCapabilities;
12379
12875
  const body = this.encode({
12380
12876
  op: "route.open",
12381
12877
  target,
12382
12878
  identity,
12383
- ...consumerIdentity ? { consumer_identity: consumerIdentity } : {}
12879
+ ...consumerIdentity ? { consumer_identity: consumerIdentity } : {},
12880
+ ...consumerCapabilities !== undefined ? { consumer_capabilities: consumerCapabilities } : {}
12384
12881
  });
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;
12882
+ let installed = null;
12883
+ const install = (frame) => {
12884
+ if (frame.header.ty !== FrameType.Response)
12885
+ return true;
12886
+ const parsed = this.parseJson(frame);
12887
+ if (typeof parsed.route_channel !== "number" || typeof parsed.route_epoch !== "number") {
12888
+ throw new SubcError(`route.open returned no route handle: ${JSON.stringify(parsed)}`);
12889
+ }
12890
+ installed = this.installRoute(parsed.route_channel, parsed.route_epoch);
12891
+ return true;
12892
+ };
12893
+ const closeLateRoute = (frame) => {
12894
+ if (frame.header.ty !== FrameType.Response)
12895
+ return;
12896
+ try {
12897
+ const parsed = this.parseJson(frame);
12898
+ if (typeof parsed.route_channel !== "number" || typeof parsed.route_epoch !== "number")
12899
+ return;
12900
+ const lateHandle = this.installRoute(parsed.route_channel, parsed.route_epoch);
12901
+ this.failHandle(lateHandle, new SubcError("late route.open was closed", "route_closed"));
12902
+ this.liveRoutes.delete(lateHandle.channel);
12903
+ this.sendRouteGoodbye(lateHandle, true);
12904
+ } catch {
12905
+ this.closeConnectionAfterCleanupFailure();
12906
+ }
12907
+ };
12908
+ await this.controlRpc(body, install, closeLateRoute);
12909
+ if (!installed)
12910
+ throw new SubcError("route.open response was not installed");
12911
+ return installed;
12391
12912
  }
12392
- async request(routeChannel, body, opts = {}) {
12913
+ async request(handle, body, opts = {}) {
12914
+ this.assertLiveHandle(handle);
12393
12915
  const bytes = body instanceof Uint8Array ? body : this.encode(body);
12394
12916
  const priority = opts.priority ?? Priority.Interactive;
12395
- const reply = await this.send(routeChannel, bytes, priority, opts.timeoutMs, opts.onProgress);
12917
+ const admission = opts.admissionClass ?? AdmissionClass.Normal;
12918
+ const reply = await this.send(handle, bytes, priority, admission, opts.timeoutMs, opts.onProgress);
12396
12919
  return this.parseJson(reply);
12397
12920
  }
12398
12921
  async call(moduleId, method, params, opts = {}) {
12399
12922
  const body = params === undefined ? { method } : { method, params };
12400
12923
  let retriedUnknownChannel = false;
12401
12924
  for (;; ) {
12402
- const routeChannel = await this.cachedRouteChannel(moduleId, opts);
12925
+ const routeHandle = await this.cachedRouteHandle(moduleId, opts);
12403
12926
  try {
12404
- return await this.managedRequest(routeChannel, body, opts);
12927
+ return await this.managedRequest(routeHandle, body, opts);
12405
12928
  } catch (err) {
12406
12929
  if (!(err instanceof SubcCallError))
12407
12930
  throw this.terminalCallError("managed call failed", err);
12408
12931
  if (err.code === "unknown_channel" && !retriedUnknownChannel && !this.closeStarted) {
12409
12932
  retriedUnknownChannel = true;
12410
- this.evictRouteChannel(routeChannel);
12933
+ this.evictRouteHandle(routeHandle);
12411
12934
  continue;
12412
12935
  }
12936
+ if (err.code === "unknown_channel" && retriedUnknownChannel) {
12937
+ this.evictRouteHandle(routeHandle);
12938
+ }
12413
12939
  if (err.kind === "not_sent") {
12414
12940
  try {
12415
12941
  await this.reconnectAfterDrop(err);
@@ -12425,29 +12951,31 @@ class SubcClient {
12425
12951
  }
12426
12952
  }
12427
12953
  }
12428
- subscribe(routeChannel, body, onEvent, opts = {}) {
12954
+ subscribe(handle, body, onEvent, opts = {}) {
12955
+ this.assertLiveHandle(handle);
12429
12956
  const bytes = body instanceof Uint8Array ? body : this.encode(body);
12430
12957
  const priority = opts.priority ?? Priority.Interactive;
12431
- const corr = this.nextCorr++;
12432
- const key = `${routeChannel}:${corr}`;
12958
+ const admission = opts.admissionClass ?? AdmissionClass.Normal;
12959
+ const corr = this.allocateCorr();
12960
+ const key = pendingKey(handle, corr);
12433
12961
  const closed = new Promise((resolve7, reject) => {
12434
12962
  if (this.closedErr) {
12435
12963
  reject(this.closedErr);
12436
12964
  return;
12437
12965
  }
12438
12966
  this.pending.set(key, {
12439
- channel: routeChannel,
12967
+ handle,
12440
12968
  resolve: () => resolve7(),
12441
12969
  reject,
12442
12970
  onProgress: onEvent,
12443
12971
  timer: null,
12444
12972
  subscription: true
12445
12973
  });
12446
- const frame = buildFrame(FrameType.Request, buildFlags(false, priority, false), routeChannel, corr, bytes);
12974
+ const frame = buildFrame(FrameType.Request, buildFlags(false, priority, false, admission), handle.channel, handle.epoch, corr, bytes);
12447
12975
  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)));
12976
+ const pending = this.pending.get(key);
12977
+ if (pending)
12978
+ this.rejectPending(key, pending, err instanceof Error ? err : new SubcError(String(err)));
12451
12979
  });
12452
12980
  });
12453
12981
  let cancelled = false;
@@ -12455,45 +12983,77 @@ class SubcClient {
12455
12983
  if (cancelled)
12456
12984
  return;
12457
12985
  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(() => {});
12986
+ this.cancel(handle, corr, priority);
12460
12987
  };
12461
12988
  return { unsubscribe, closed };
12462
12989
  }
12463
- async closeRoute(target, identity, opts = {}) {
12990
+ cancel(handle, corr, priority = Priority.Interactive) {
12991
+ this.assertLiveHandle(handle);
12992
+ const cancel = buildFrame(FrameType.Cancel, buildFlags(false, priority, false), handle.channel, handle.epoch, corr, EMPTY_BODY);
12993
+ this.sock.write(encodeFrame(cancel), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS).catch(() => {
12994
+ return;
12995
+ });
12996
+ }
12997
+ async routePoll(handle, kind) {
12998
+ this.assertLiveHandle(handle);
12999
+ const body = this.encode({
13000
+ op: "route.poll",
13001
+ route_channel: handle.channel,
13002
+ route_epoch: handle.epoch,
13003
+ kind
13004
+ });
13005
+ const reply = await this.controlRpc(body, (frame) => {
13006
+ if (frame.header.ty !== FrameType.Response)
13007
+ return true;
13008
+ const parsed = this.parseJson(frame);
13009
+ return parsed.route_channel === handle.channel && parsed.route_epoch === handle.epoch;
13010
+ });
13011
+ return this.parseJson(reply);
13012
+ }
13013
+ async closeRoute(handle, opts = {}) {
13014
+ this.assertLiveHandle(handle);
13015
+ for (const [key, cached] of this.routes) {
13016
+ if (cached.handle && sameRouteHandle(cached.handle, handle)) {
13017
+ cached.closed = true;
13018
+ cached.handle = null;
13019
+ this.routes.delete(key);
13020
+ }
13021
+ }
13022
+ if (opts.drain)
13023
+ await this.drainUnaryOnHandle(handle);
13024
+ this.failHandle(handle, new SubcError("route closed by closeRoute", "route_closed"));
13025
+ if (this.liveRoutes.get(handle.channel) === handle)
13026
+ this.liveRoutes.delete(handle.channel);
13027
+ this.sendRouteGoodbye(handle);
13028
+ }
13029
+ async closeManagedRoute(target, identity, opts = {}) {
12464
13030
  const key = routeCacheKey(target, identity, routeOpenConsumerIdentity(opts));
12465
13031
  const cached = this.routes.get(key);
12466
13032
  if (!cached)
12467
13033
  return;
12468
13034
  cached.closed = true;
12469
13035
  this.routes.delete(key);
12470
- const channel = cached.channel;
12471
- cached.channel = null;
12472
- if (channel !== null)
12473
- await this.closeRouteChannel(channel, opts);
13036
+ const handle = cached.handle;
13037
+ cached.handle = null;
13038
+ if (handle)
13039
+ await this.closeRoute(handle, opts);
12474
13040
  }
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);
13041
+ async closeRouteChannel(handle, opts = {}) {
13042
+ await this.closeRoute(handle, opts);
12483
13043
  }
12484
13044
  close() {
12485
13045
  this.closeStarted = true;
12486
13046
  this.fail(new SubcError("client closed"));
12487
13047
  this.sock.close();
12488
13048
  }
12489
- drainUnaryOnChannel(channel) {
13049
+ drainUnaryOnHandle(handle) {
12490
13050
  const waiters = [];
12491
13051
  for (const pending of this.pending.values()) {
12492
- if (pending.channel === channel && !pending.subscription) {
13052
+ if (pending.handle === handle && !pending.subscription) {
12493
13053
  waiters.push(new Promise((resolve7) => {
12494
- const prev = pending.onSettle;
13054
+ const previous = pending.onSettle;
12495
13055
  pending.onSettle = () => {
12496
- prev?.();
13056
+ previous?.();
12497
13057
  resolve7();
12498
13058
  };
12499
13059
  }));
@@ -12503,11 +13063,21 @@ class SubcClient {
12503
13063
  return;
12504
13064
  });
12505
13065
  }
12506
- sendRouteGoodbye(channel) {
12507
- if (this.closedErr)
13066
+ sendRouteGoodbye(handle, closeOnQueueFailure = false) {
13067
+ this.assertLiveConnection(handle);
13068
+ if (this.closedErr) {
13069
+ if (closeOnQueueFailure)
13070
+ this.closeConnectionAfterCleanupFailure();
12508
13071
  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(() => {});
13072
+ }
13073
+ const goodbye = buildFrame(FrameType.Goodbye, buildFlags(false, Priority.Interactive, false), handle.channel, handle.epoch, 0n, EMPTY_BODY);
13074
+ const write = this.sock.writeTracked(encodeFrame(goodbye), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS);
13075
+ if (!write.queued && closeOnQueueFailure)
13076
+ this.closeConnectionAfterCleanupFailure();
13077
+ write.completed.catch(() => {
13078
+ if (closeOnQueueFailure && !write.queued)
13079
+ this.closeConnectionAfterCleanupFailure();
13080
+ });
12511
13081
  }
12512
13082
  static async openConnection(opts) {
12513
13083
  const conn = await readConnectionFile(opts.connectionFile);
@@ -12522,37 +13092,48 @@ class SubcClient {
12522
13092
  }
12523
13093
  return { sock, conn };
12524
13094
  }
12525
- async controlRpc(body) {
12526
- return this.send(0, body, Priority.Interactive, undefined, undefined);
13095
+ async controlRpc(body, acceptFrame, onLateResponse) {
13096
+ return this.send(null, body, Priority.Interactive, AdmissionClass.Normal, undefined, undefined, acceptFrame, onLateResponse);
12527
13097
  }
12528
- send(channel, body, priority, timeoutMs, onProgress) {
13098
+ send(handle, body, priority, admission, timeoutMs, onProgress, acceptFrame, onLateResponse) {
13099
+ if (handle)
13100
+ this.assertLiveHandle(handle);
12529
13101
  if (this.closedErr)
12530
13102
  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);
13103
+ let corr;
13104
+ try {
13105
+ corr = this.allocateCorr();
13106
+ } catch (error2) {
13107
+ return Promise.reject(error2);
13108
+ }
13109
+ const key = pendingKey(handle, corr);
13110
+ const channel = handle?.channel ?? 0;
13111
+ const epoch = handle?.epoch ?? 0;
13112
+ const frame = buildFrame(FrameType.Request, buildFlags(false, priority, false, admission), channel, epoch, corr, body);
12534
13113
  return new Promise((resolve7, reject) => {
12535
13114
  const ms = timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
12536
13115
  const pending = {
12537
- channel,
13116
+ handle,
12538
13117
  resolve: resolve7,
12539
13118
  reject,
12540
13119
  onProgress,
12541
- timer: null
13120
+ timer: null,
13121
+ acceptFrame,
13122
+ onLateResponse
12542
13123
  };
12543
- pending.timer = setTimeout(() => {
12544
- this.arbitrateTimeout(key, pending, channel, corr, ms);
12545
- }, ms);
13124
+ pending.timer = setTimeout(() => this.arbitrateTimeout(key, pending, channel, corr, ms), ms);
12546
13125
  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)));
13126
+ this.sock.write(encodeFrame(frame), Date.now() + ms).catch((error2) => {
13127
+ const current = this.pending.get(key);
13128
+ if (current)
13129
+ this.rejectPending(key, current, error2 instanceof Error ? error2 : new SubcError(String(error2)));
12551
13130
  });
12552
13131
  });
12553
13132
  }
12554
13133
  arbitrateTimeout(key, pending, channel, corr, ms) {
12555
13134
  const settleAsTimeout = () => {
13135
+ if (pending.onLateResponse)
13136
+ this.lateResponses.set(key, pending.onLateResponse);
12556
13137
  this.rejectPending(key, pending, new SubcError(this.timeoutMessage(channel, corr, ms), REQUEST_DEADLINE_MARKER));
12557
13138
  };
12558
13139
  const graceDeadline = Date.now() + this.opts.timeoutArbitrationGraceMs;
@@ -12568,59 +13149,67 @@ class SubcClient {
12568
13149
  };
12569
13150
  setImmediate(arbitrate);
12570
13151
  }
12571
- async managedRequest(routeChannel, body, opts) {
13152
+ async managedRequest(handle, body, opts) {
12572
13153
  const bytes = body instanceof Uint8Array ? body : this.encode(body);
12573
13154
  const priority = opts.priority ?? Priority.Interactive;
13155
+ const admission = opts.admissionClass ?? AdmissionClass.Normal;
12574
13156
  try {
12575
- const reply = await this.sendManaged(routeChannel, bytes, priority, opts.timeoutMs, opts.onProgress);
13157
+ const reply = await this.sendManaged(handle, bytes, priority, admission, opts.timeoutMs, opts.onProgress);
12576
13158
  return this.parseJson(reply);
12577
- } catch (err) {
12578
- if (err instanceof SubcCallError)
12579
- throw err;
12580
- throw this.terminalCallError("managed call failed", err);
13159
+ } catch (error2) {
13160
+ if (error2 instanceof SubcCallError)
13161
+ throw error2;
13162
+ throw this.terminalCallError("managed call failed", error2);
12581
13163
  }
12582
13164
  }
12583
- sendManaged(channel, body, priority, timeoutMs, onProgress) {
13165
+ sendManaged(handle, body, priority, admission, timeoutMs, onProgress) {
13166
+ try {
13167
+ this.assertLiveHandle(handle);
13168
+ } catch (error2) {
13169
+ return Promise.reject(this.notSentCallError("request used a stale route handle", error2));
13170
+ }
12584
13171
  if (this.closedErr) {
12585
13172
  return Promise.reject(this.notSentCallError("request was not sent because the subc connection was already closed", this.closedErr));
12586
13173
  }
12587
- const corr = this.nextCorr++;
12588
- const key = `${channel}:${corr}`;
12589
- const frame = buildFrame(FrameType.Request, buildFlags(false, priority, false), channel, corr, body);
13174
+ let corr;
13175
+ try {
13176
+ corr = this.allocateCorr();
13177
+ } catch (error2) {
13178
+ return Promise.reject(this.notSentCallError("request correlation allocator was exhausted", error2));
13179
+ }
13180
+ const key = pendingKey(handle, corr);
13181
+ const frame = buildFrame(FrameType.Request, buildFlags(false, priority, false, admission), handle.channel, handle.epoch, corr, body);
12590
13182
  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);
13183
+ const classifyFailure = (error2) => {
13184
+ if (!handedToSocket)
13185
+ return this.notSentCallError("request bytes were not queued to the subc socket", error2);
13186
+ if (error2 instanceof SubcError && error2.code === REQUEST_DEADLINE_MARKER) {
13187
+ 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
13188
  }
12598
- return this.outcomeUnknownCallError("connection dropped before the managed call returned a response", err);
13189
+ return this.outcomeUnknownCallError("connection dropped before the managed call returned a response", error2);
12599
13190
  };
12600
13191
  return new Promise((resolve7, reject) => {
12601
13192
  const ms = timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
12602
13193
  const pending = {
12603
- channel,
13194
+ handle,
12604
13195
  resolve: resolve7,
12605
13196
  reject,
12606
13197
  onProgress,
12607
13198
  timer: null,
12608
13199
  classifyFailure
12609
13200
  };
12610
- pending.timer = setTimeout(() => {
12611
- this.arbitrateTimeout(key, pending, channel, corr, ms);
12612
- }, ms);
13201
+ pending.timer = setTimeout(() => this.arbitrateTimeout(key, pending, handle.channel, corr, ms), ms);
12613
13202
  this.pending.set(key, pending);
12614
13203
  const write = this.sock.writeTracked(encodeFrame(frame), Date.now() + ms);
12615
13204
  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)));
13205
+ write.completed.catch((error2) => {
13206
+ const current = this.pending.get(key);
13207
+ if (current)
13208
+ this.rejectPending(key, current, error2 instanceof Error ? error2 : new SubcError(String(error2)));
12620
13209
  });
12621
13210
  });
12622
13211
  }
12623
- async cachedRouteChannel(moduleId, opts) {
13212
+ async cachedRouteHandle(moduleId, opts) {
12624
13213
  const identity = opts.identity ?? this.opts.identity;
12625
13214
  if (!identity) {
12626
13215
  throw new SubcCallError("terminal", "managed call requires a BindIdentity in SubcClient.connect({ identity }) or call(..., { identity })", "missing_identity");
@@ -12636,15 +13225,13 @@ class SubcClient {
12636
13225
  target,
12637
13226
  identity,
12638
13227
  consumerIdentity,
12639
- channel: null,
12640
- generation: 0,
13228
+ handle: null,
12641
13229
  opening: null
12642
13230
  };
12643
13231
  this.routes.set(key, cached);
12644
13232
  }
12645
- if (cached.channel !== null && cached.generation === this.generation && !this.closedErr) {
12646
- return cached.channel;
12647
- }
13233
+ if (cached.handle && this.isLiveHandle(cached.handle))
13234
+ return cached.handle;
12648
13235
  if (!cached.opening) {
12649
13236
  cached.opening = this.openCachedRoute(cached).finally(() => {
12650
13237
  cached.opening = null;
@@ -12661,44 +13248,43 @@ class SubcClient {
12661
13248
  throw this.routeClosedDuringOpen();
12662
13249
  try {
12663
13250
  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;
13251
+ } catch (error2) {
13252
+ throw this.notSentRecoveryError("route.open could not run because reconnect failed", error2);
12669
13253
  }
13254
+ if (cached.handle && this.isLiveHandle(cached.handle))
13255
+ return cached.handle;
12670
13256
  try {
12671
- const channel = await this.routeOpen(cached.target, cached.identity, {
13257
+ const handle = await this.routeOpen(cached.target, cached.identity, {
12672
13258
  consumerIdentity: cached.consumerIdentity ?? null
12673
13259
  });
12674
13260
  if (cached.closed) {
12675
- this.sendRouteGoodbye(channel);
13261
+ this.liveRoutes.delete(handle.channel);
13262
+ this.sendRouteGoodbye(handle);
12676
13263
  throw this.routeClosedDuringOpen();
12677
13264
  }
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)) {
13265
+ cached.handle = handle;
13266
+ return handle;
13267
+ } catch (error2) {
13268
+ if (error2 instanceof SubcCallError && error2.code === "route_closed")
13269
+ throw error2;
13270
+ if (!this.closeStarted && isConsumerReconnectTransient(error2)) {
12685
13271
  try {
12686
- await this.reconnectAfterDrop(err);
12687
- } catch (reconnectErr) {
12688
- throw this.notSentRecoveryError("route.open was not sent and reconnect failed", reconnectErr);
13272
+ await this.reconnectAfterDrop(error2);
13273
+ } catch (reconnectError) {
13274
+ throw this.notSentRecoveryError("route.open was not sent and reconnect failed", reconnectError);
12689
13275
  }
12690
13276
  continue;
12691
13277
  }
12692
- if (!this.closeStarted && err instanceof SubcError && isRetryableRouteOpenCode(err.code)) {
13278
+ if (!this.closeStarted && error2 instanceof SubcError && isRetryableRouteOpenCode(error2.code)) {
12693
13279
  routeRetryAttempt += 1;
12694
13280
  if (routeRetryAttempt < this.opts.reconnectBackoff.maxAttempts && Date.now() < routeRetryDeadline) {
12695
13281
  await this.opts.sleep(routeRetryDelay);
12696
13282
  routeRetryDelay = Math.min(routeRetryDelay * 2, this.opts.reconnectBackoff.capMs);
12697
13283
  continue;
12698
13284
  }
12699
- throw this.notSentCallError(`route.open failed for module ${cached.moduleId}: ${err.code} (retry budget exhausted)`, err);
13285
+ throw this.notSentCallError(`route.open failed for module ${cached.moduleId}: ${error2.code} (retry budget exhausted)`, error2);
12700
13286
  }
12701
- throw this.terminalCallError(`route.open failed for module ${cached.moduleId}`, err);
13287
+ throw this.terminalCallError(`route.open failed for module ${cached.moduleId}`, error2);
12702
13288
  }
12703
13289
  }
12704
13290
  }
@@ -12761,25 +13347,27 @@ class SubcClient {
12761
13347
  this.currentConn = opened.conn;
12762
13348
  this.closedErr = null;
12763
13349
  this.generation += 1;
13350
+ this.connectionToken = newConnectionToken();
13351
+ this.liveRoutes.clear();
13352
+ this.lateResponses.clear();
13353
+ this.nextCorr = 1n;
12764
13354
  this.readLoop(opened.sock, this.generation);
12765
13355
  }
12766
13356
  async reopenCachedRoutes() {
12767
- for (const cached of this.routes.values()) {
12768
- cached.channel = null;
12769
- cached.generation = 0;
12770
- }
13357
+ for (const cached of this.routes.values())
13358
+ cached.handle = null;
12771
13359
  for (const cached of this.routes.values()) {
12772
13360
  if (cached.closed)
12773
13361
  continue;
12774
- const channel = await this.routeOpen(cached.target, cached.identity, {
13362
+ const handle = await this.routeOpen(cached.target, cached.identity, {
12775
13363
  consumerIdentity: cached.consumerIdentity ?? null
12776
13364
  });
12777
13365
  if (cached.closed) {
12778
- this.sendRouteGoodbye(channel);
13366
+ this.liveRoutes.delete(handle.channel);
13367
+ this.sendRouteGoodbye(handle);
12779
13368
  continue;
12780
13369
  }
12781
- cached.channel = channel;
12782
- cached.generation = this.generation;
13370
+ cached.handle = handle;
12783
13371
  }
12784
13372
  }
12785
13373
  timeoutMessage(channel, corr, ms) {
@@ -12793,28 +13381,37 @@ class SubcClient {
12793
13381
  async readLoop(sock, generation) {
12794
13382
  try {
12795
13383
  for (;; ) {
12796
- const headerBytes = await sock.readExact(HEADER_LEN, Number.POSITIVE_INFINITY);
12797
- this.readerActive = true;
13384
+ this.readerActive = false;
13385
+ const frame = await sock.readFrame(Number.POSITIVE_INFINITY, { afterHeaderMs: BODY_READ_TIMEOUT_MS }, () => {
13386
+ this.readerActive = true;
13387
+ });
12798
13388
  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
- }
13389
+ if (this.sock === sock && this.generation === generation)
13390
+ this.dispatch(frame);
12804
13391
  } finally {
12805
13392
  this.readerActive = false;
12806
13393
  }
12807
13394
  }
12808
- } catch (err) {
13395
+ } catch (error2) {
12809
13396
  if (this.sock === sock && this.generation === generation) {
12810
- this.fail(err instanceof Error ? err : new SubcError(String(err)));
13397
+ this.fail(error2 instanceof Error ? error2 : new SubcError(String(error2)));
12811
13398
  }
12812
13399
  }
12813
13400
  }
12814
13401
  dispatch(frame) {
12815
- const key = `${frame.header.channel}:${frame.header.corr}`;
13402
+ let handle = null;
13403
+ if (frame.header.channel !== 0) {
13404
+ handle = this.liveRoutes.get(frame.header.channel) ?? null;
13405
+ if (!handle || handle.epoch !== frame.header.epoch) {
13406
+ this.ingressEpochDropCount += 1;
13407
+ return;
13408
+ }
13409
+ }
13410
+ const key = pendingKey(handle, frame.header.corr);
12816
13411
  const pending = this.pending.get(key);
12817
13412
  if (pending) {
13413
+ if (pending.acceptFrame && !pending.acceptFrame(frame))
13414
+ return;
12818
13415
  switch (frame.header.ty) {
12819
13416
  case FrameType.Push:
12820
13417
  case FrameType.StreamData:
@@ -12831,15 +13428,22 @@ class SubcClient {
12831
13428
  return;
12832
13429
  }
12833
13430
  }
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);
13431
+ const late = this.lateResponses.get(key);
13432
+ if (late && (frame.header.ty === FrameType.Response || frame.header.ty === FrameType.Error)) {
13433
+ this.lateResponses.delete(key);
13434
+ late(frame);
12837
13435
  return;
12838
13436
  }
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() ?? "?");
13437
+ if (frame.header.ty === FrameType.Goodbye && handle) {
13438
+ this.failHandle(handle, new SubcError("route closed by subc (GOODBYE)"));
13439
+ if (this.liveRoutes.get(handle.channel) === handle)
13440
+ this.liveRoutes.delete(handle.channel);
13441
+ this.evictRouteHandle(handle);
12841
13442
  return;
12842
13443
  }
13444
+ if (frame.header.ty === FrameType.Response || frame.header.ty === FrameType.Error || frame.header.ty === FrameType.StreamEnd) {
13445
+ 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() ?? "?");
13446
+ }
12843
13447
  }
12844
13448
  settle(key, pending, run) {
12845
13449
  if (this.pending.get(key) !== pending)
@@ -12862,18 +13466,16 @@ class SubcClient {
12862
13466
  return new SubcError(Buffer.from(frame.body).toString("utf8") || "subc error");
12863
13467
  }
12864
13468
  }
12865
- evictRouteChannel(channel) {
13469
+ evictRouteHandle(handle) {
12866
13470
  for (const cached of this.routes.values()) {
12867
- if (cached.channel === channel && cached.generation === this.generation) {
12868
- cached.channel = null;
12869
- }
13471
+ if (cached.handle && sameRouteHandle(cached.handle, handle))
13472
+ cached.handle = null;
12870
13473
  }
12871
13474
  }
12872
- failChannel(channel, err) {
13475
+ failHandle(handle, error2) {
12873
13476
  for (const [key, pending] of this.pending) {
12874
- if (pending.channel === channel) {
12875
- this.rejectPending(key, pending, err);
12876
- }
13477
+ if (pending.handle && sameRouteHandle(pending.handle, handle))
13478
+ this.rejectPending(key, pending, error2);
12877
13479
  }
12878
13480
  }
12879
13481
  fail(err) {
@@ -12901,6 +13503,44 @@ class SubcClient {
12901
13503
  return this.notSentCallError(message, cause);
12902
13504
  return this.terminalCallError(message, cause);
12903
13505
  }
13506
+ get droppedIngressFrames() {
13507
+ return this.ingressEpochDropCount;
13508
+ }
13509
+ installRoute(channel, epoch) {
13510
+ const handle = createRouteHandle(channel, epoch, this.connectionToken);
13511
+ this.liveRoutes.set(channel, handle);
13512
+ return handle;
13513
+ }
13514
+ isLiveHandle(handle) {
13515
+ return belongsToConnection(handle, this.connectionToken) && this.liveRoutes.get(handle.channel) === handle;
13516
+ }
13517
+ assertLiveConnection(handle) {
13518
+ if (!belongsToConnection(handle, this.connectionToken))
13519
+ throw new StaleRouteHandleError(handle);
13520
+ }
13521
+ assertLiveHandle(handle) {
13522
+ if (!this.isLiveHandle(handle))
13523
+ throw new StaleRouteHandleError(handle);
13524
+ }
13525
+ allocateCorr() {
13526
+ const maximum = 0xffffffffffffffffn;
13527
+ if (this.nextCorr > maximum) {
13528
+ const error2 = new SubcError("channel-0 correlation id allocator exhausted", "corr_exhausted");
13529
+ this.fail(error2);
13530
+ this.sock.close();
13531
+ this.scheduleReconnectAfterDrop(error2);
13532
+ throw error2;
13533
+ }
13534
+ const corr = this.nextCorr;
13535
+ this.nextCorr += 1n;
13536
+ return corr;
13537
+ }
13538
+ closeConnectionAfterCleanupFailure() {
13539
+ const error2 = new SubcError("late route cleanup could not be queued", "late_route_cleanup_failed");
13540
+ this.fail(error2);
13541
+ this.sock.close();
13542
+ this.scheduleReconnectAfterDrop(error2);
13543
+ }
12904
13544
  encode(value) {
12905
13545
  return new Uint8Array(Buffer.from(JSON.stringify(value), "utf8"));
12906
13546
  }
@@ -12970,7 +13610,10 @@ function causeMessage(cause) {
12970
13610
  return "";
12971
13611
  return `: ${cause instanceof Error ? cause.message : String(cause)}`;
12972
13612
  }
12973
- // ../../node_modules/.bun/@cortexkit+subc-client@0.3.4/node_modules/@cortexkit/subc-client/dist/provider.js
13613
+ function pendingKey(handle, corr) {
13614
+ return handle ? `${handle.channel}:${handle.epoch}:${corr}` : `0:0:${corr}`;
13615
+ }
13616
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.5.0/node_modules/@cortexkit/subc-client/dist/provider.js
12974
13617
  import { Buffer as Buffer2 } from "node:buffer";
12975
13618
  var DEFAULT_HANDSHAKE_TIMEOUT_MS2 = 1e4;
12976
13619
  var BODY_READ_TIMEOUT_MS2 = 30000;
@@ -13033,6 +13676,11 @@ class SubcProvider {
13033
13676
  closeStarted = false;
13034
13677
  closedErr = null;
13035
13678
  inflight = new Map;
13679
+ pending = new Map;
13680
+ liveRoutes = new Map;
13681
+ connectionToken = newConnectionToken();
13682
+ nextCorr = 1n;
13683
+ ingressEpochDropCount = 0;
13036
13684
  requestGate = new AsyncPermitPool(DEFAULT_PROVIDER_HANDLER_CAPACITY);
13037
13685
  reconnecting = null;
13038
13686
  generation = 1;
@@ -13052,12 +13700,55 @@ class SubcProvider {
13052
13700
  this.readLoop(sock, this.generation);
13053
13701
  this.enqueueConnectionState({ state: "connected", epoch: this.connectionEpoch });
13054
13702
  }
13703
+ get droppedIngressFrames() {
13704
+ return this.ingressEpochDropCount;
13705
+ }
13055
13706
  get conn() {
13056
13707
  return this.currentConn;
13057
13708
  }
13058
13709
  currentEpoch() {
13059
13710
  return this.connectionEpoch;
13060
13711
  }
13712
+ async request(handle, body, opts = {}) {
13713
+ this.assertLiveHandle(handle);
13714
+ const corr = this.allocateCorr();
13715
+ const key = routeKey(handle, corr);
13716
+ const timeoutMs = opts.timeoutMs ?? WRITE_TIMEOUT_MS;
13717
+ const frame = buildFrame(FrameType.Request, buildFlags(false, opts.priority ?? Priority.Interactive, false, opts.admissionClass ?? AdmissionClass.Normal), handle.channel, handle.epoch, corr, body);
13718
+ return await new Promise((resolve7, reject) => {
13719
+ const timer = setTimeout(() => {
13720
+ if (this.pending.delete(key))
13721
+ reject(new SubcProviderError("reverse request timed out", "request_timeout"));
13722
+ }, timeoutMs);
13723
+ this.pending.set(key, {
13724
+ resolve: (response) => resolve7(response.body),
13725
+ reject,
13726
+ timer
13727
+ });
13728
+ this.sendOn(this.sock, this.generation, frame).catch((error2) => {
13729
+ const pending = this.pending.get(key);
13730
+ if (!pending)
13731
+ return;
13732
+ this.pending.delete(key);
13733
+ clearTimeout(pending.timer);
13734
+ reject(error2 instanceof Error ? error2 : new SubcProviderError(String(error2)));
13735
+ });
13736
+ });
13737
+ }
13738
+ async push(handle, body, opts = {}) {
13739
+ this.assertLiveHandle(handle);
13740
+ 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));
13741
+ }
13742
+ cancel(handle, corr) {
13743
+ this.assertLiveHandle(handle);
13744
+ this.sendOn(this.sock, this.generation, buildFrame(FrameType.Cancel, controlFlags(), handle.channel, handle.epoch, corr, new Uint8Array(0)));
13745
+ }
13746
+ closeRoute(handle) {
13747
+ this.assertLiveHandle(handle);
13748
+ this.liveRoutes.delete(handle.channel);
13749
+ this.abortHandle(handle);
13750
+ this.sendOn(this.sock, this.generation, buildFrame(FrameType.Goodbye, controlFlags(), handle.channel, handle.epoch, 0n, new Uint8Array(0)));
13751
+ }
13061
13752
  static async connect(opts) {
13062
13753
  if (opts.manifest.protocol_ver !== PROTOCOL_VERSION) {
13063
13754
  throw new SubcProviderError(`manifest protocol_ver ${opts.manifest.protocol_ver} does not match client protocol ${PROTOCOL_VERSION}`, "invalid_manifest");
@@ -13072,7 +13763,7 @@ class SubcProvider {
13072
13763
  this.cancelRestoredDebounce();
13073
13764
  const sock = this.sock;
13074
13765
  try {
13075
- await sendFrame(sock, buildFrame(FrameType.Goodbye, controlFlags(), 0, 0n, new Uint8Array(0)));
13766
+ await sendFrame(sock, buildFrame(FrameType.Goodbye, controlFlags(), 0, 0, 0n, new Uint8Array(0)));
13076
13767
  } catch {} finally {
13077
13768
  sock.close();
13078
13769
  this.finishClosed();
@@ -13080,12 +13771,13 @@ class SubcProvider {
13080
13771
  }
13081
13772
  await this.closed;
13082
13773
  }
13083
- static async openConnection(opts) {
13774
+ static async openConnection(opts, onSocket) {
13084
13775
  const conn = await readConnectionFile(opts.connectionFile);
13085
13776
  const deadline = Date.now() + (opts.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS2);
13086
13777
  const endpoint = conn.endpoints[0];
13087
13778
  const sock = await SubcSocket.connect(endpoint.host, endpoint.port, deadline);
13088
13779
  try {
13780
+ onSocket?.(sock);
13089
13781
  await authenticateClient(sock, conn, deadline);
13090
13782
  await sendFrame(sock, buildHelloFrame(opts));
13091
13783
  const ack = await expectHelloAck(sock, deadline);
@@ -13098,19 +13790,17 @@ class SubcProvider {
13098
13790
  async readLoop(sock, generation) {
13099
13791
  try {
13100
13792
  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);
13793
+ const frame = await sock.readFrame(Number.POSITIVE_INFINITY, { afterHeaderMs: BODY_READ_TIMEOUT_MS2 });
13794
+ const keepGoing = await this.dispatch(frame, sock, generation);
13105
13795
  if (!keepGoing) {
13106
13796
  if (this.sock === sock && this.generation === generation)
13107
13797
  this.closeStarted = true;
13108
13798
  break;
13109
13799
  }
13110
13800
  }
13111
- } catch (err) {
13801
+ } catch (error2) {
13112
13802
  if (this.sock === sock && this.generation === generation && !this.closeStarted) {
13113
- this.handleUnexpectedDrop(sock, generation, err instanceof Error ? err : new SubcProviderError(String(err)));
13803
+ this.handleUnexpectedDrop(sock, generation, error2 instanceof Error ? error2 : new SubcProviderError(String(error2)));
13114
13804
  return;
13115
13805
  }
13116
13806
  } finally {
@@ -13122,28 +13812,58 @@ class SubcProvider {
13122
13812
  }
13123
13813
  }
13124
13814
  async dispatch(frame, sock, generation) {
13815
+ let handle = null;
13816
+ if (frame.header.channel !== 0) {
13817
+ handle = this.liveRoutes.get(frame.header.channel) ?? null;
13818
+ if (!handle || handle.epoch !== frame.header.epoch) {
13819
+ this.ingressEpochDropCount += 1;
13820
+ return true;
13821
+ }
13822
+ }
13823
+ if (handle) {
13824
+ const pendingKey2 = routeKey(handle, frame.header.corr);
13825
+ const pending = this.pending.get(pendingKey2);
13826
+ if (pending) {
13827
+ if (frame.header.ty === FrameType.Push || frame.header.ty === FrameType.StreamData)
13828
+ return true;
13829
+ if (frame.header.ty === FrameType.Response || frame.header.ty === FrameType.StreamEnd) {
13830
+ this.pending.delete(pendingKey2);
13831
+ clearTimeout(pending.timer);
13832
+ pending.resolve(frame);
13833
+ return true;
13834
+ }
13835
+ if (frame.header.ty === FrameType.Error) {
13836
+ this.pending.delete(pendingKey2);
13837
+ clearTimeout(pending.timer);
13838
+ pending.reject(providerErrorFromFrame(frame));
13839
+ return true;
13840
+ }
13841
+ }
13842
+ }
13125
13843
  switch (frame.header.ty) {
13126
13844
  case FrameType.Ping:
13127
13845
  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)));
13846
+ await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, FrameType.Pong, frame.header.flags, 0, 0, frame.header.corr, new Uint8Array(0)));
13129
13847
  }
13130
13848
  return true;
13131
13849
  case FrameType.Goodbye:
13132
- if (frame.header.channel === 0)
13850
+ if (!handle)
13133
13851
  return false;
13134
- this.abortChannel(generation, frame.header.channel);
13135
- await this.opts.onRouteGone?.(frame.header.channel);
13852
+ this.liveRoutes.delete(handle.channel);
13853
+ this.abortHandle(handle);
13854
+ await this.opts.onRouteGone?.(handle);
13136
13855
  return true;
13137
13856
  case FrameType.Cancel:
13138
- this.inflight.get(routeKey(generation, frame.header.channel, frame.header.corr))?.abort();
13857
+ if (handle)
13858
+ this.inflight.get(routeKey(handle, frame.header.corr))?.abort();
13139
13859
  return true;
13140
13860
  case FrameType.Request:
13141
13861
  if (frame.header.channel === 0) {
13142
13862
  await this.handleControlRequest(frame, sock, generation);
13143
- } else {
13144
- this.handleDataRequest(frame, sock, generation).catch((err) => {
13863
+ } else if (handle) {
13864
+ this.handleDataRequest(frame, handle, sock, generation).catch((error2) => {
13145
13865
  if (!this.closeStarted && this.sock === sock && this.generation === generation) {
13146
- console.warn("SubcProvider handler failed after its request was dispatched", err);
13866
+ console.warn("SubcProvider handler failed after its request was dispatched", error2);
13147
13867
  }
13148
13868
  });
13149
13869
  }
@@ -13152,19 +13872,28 @@ class SubcProvider {
13152
13872
  return true;
13153
13873
  }
13154
13874
  }
13155
- abortChannel(generation, channel) {
13156
- const prefix = `${generation}:${channel}:`;
13875
+ abortHandle(handle) {
13876
+ const prefix = `${handle.channel}:${handle.epoch}:`;
13157
13877
  for (const [key, controller] of this.inflight) {
13158
13878
  if (key.startsWith(prefix))
13159
13879
  controller.abort();
13160
13880
  }
13881
+ for (const [key, pending] of this.pending) {
13882
+ if (!key.startsWith(prefix))
13883
+ continue;
13884
+ this.pending.delete(key);
13885
+ clearTimeout(pending.timer);
13886
+ pending.reject(new StaleRouteHandleError(handle));
13887
+ }
13161
13888
  }
13162
- abortGeneration(generation) {
13163
- const prefix = `${generation}:`;
13164
- for (const [key, controller] of this.inflight) {
13165
- if (key.startsWith(prefix))
13166
- controller.abort();
13889
+ abortGeneration(_generation) {
13890
+ this.abortAllInflight();
13891
+ for (const [key, pending] of this.pending) {
13892
+ this.pending.delete(key);
13893
+ clearTimeout(pending.timer);
13894
+ pending.reject(new SubcProviderError("provider connection dropped", "connection_dropped"));
13167
13895
  }
13896
+ this.liveRoutes.clear();
13168
13897
  }
13169
13898
  abortAllInflight() {
13170
13899
  for (const controller of this.inflight.values())
@@ -13173,9 +13902,9 @@ class SubcProvider {
13173
13902
  async handleControlRequest(frame, sock, generation) {
13174
13903
  const request = parseJson(frame.body);
13175
13904
  if (request.op === HEALTH_CHECK_OP) {
13176
- this.handleHealthRequest(frame, sock, generation).catch((err) => {
13905
+ this.handleHealthRequest(frame, sock, generation).catch((error2) => {
13177
13906
  if (!this.closeStarted && this.sock === sock && this.generation === generation) {
13178
- console.warn("SubcProvider health handler failed after its request was dispatched", err);
13907
+ console.warn("SubcProvider health handler failed after its request was dispatched", error2);
13179
13908
  }
13180
13909
  });
13181
13910
  return;
@@ -13183,47 +13912,93 @@ class SubcProvider {
13183
13912
  if (request.op !== "route.bind") {
13184
13913
  throw new SubcProviderError(`unsupported module control request ${request.op ?? "<missing op>"}`);
13185
13914
  }
13915
+ const boundChannel = numberField(request.route_channel, "route_channel");
13916
+ const boundEpoch = numberField(request.epoch, "epoch");
13917
+ const stale = this.liveRoutes.get(boundChannel);
13918
+ if (stale) {
13919
+ if (boundEpoch <= stale.epoch) {
13920
+ await this.sendError(frame, "route_rejected", `route.bind epoch ${boundEpoch} does not supersede installed epoch ${stale.epoch} on channel ${boundChannel}`, controlFlags(), sock, generation);
13921
+ return;
13922
+ }
13923
+ this.liveRoutes.delete(stale.channel);
13924
+ this.abortHandle(stale);
13925
+ await this.opts.onRouteGone?.(stale);
13926
+ }
13927
+ const tentative = createRouteHandle(boundChannel, boundEpoch, this.connectionToken);
13186
13928
  const bindRequest = {
13187
- route_channel: numberField(request.route_channel, "route_channel"),
13929
+ handle: tentative,
13188
13930
  target: request.target,
13189
13931
  identity: request.identity,
13190
- principal: request.principal
13932
+ principal: request.principal,
13933
+ consumer_capabilities: request.consumer_capabilities
13191
13934
  };
13192
- const decision = await this.opts.onBind?.(bindRequest);
13935
+ let decision;
13936
+ try {
13937
+ decision = await this.opts.onBind?.(bindRequest);
13938
+ } catch (error2) {
13939
+ try {
13940
+ await this.sendError(frame, "route_rejected", error2 instanceof Error ? error2.message : String(error2), controlFlags(), sock, generation);
13941
+ } finally {
13942
+ await this.opts.onRouteGone?.(tentative);
13943
+ }
13944
+ return;
13945
+ }
13193
13946
  const rejection = bindRejection(decision);
13194
13947
  if (rejection) {
13195
- await this.sendError(frame, rejection.code, rejection.message, controlFlags(), sock, generation);
13948
+ try {
13949
+ await this.sendError(frame, rejection.code, rejection.message, controlFlags(), sock, generation);
13950
+ } finally {
13951
+ await this.opts.onRouteGone?.(tentative);
13952
+ }
13953
+ return;
13954
+ }
13955
+ try {
13956
+ await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, FrameType.Response, controlFlags(), 0, 0, frame.header.corr, encodeJson({ op: "route.bind" })));
13957
+ } catch (error2) {
13958
+ await this.opts.onRouteGone?.(tentative);
13959
+ throw error2;
13960
+ }
13961
+ if (this.sock !== sock || this.generation !== generation || this.closeStarted || this.closedErr) {
13962
+ await this.opts.onRouteGone?.(tentative);
13196
13963
  return;
13197
13964
  }
13198
- await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, FrameType.Response, controlFlags(), 0, frame.header.corr, encodeJson({ op: "route.bind" })));
13965
+ this.liveRoutes.set(tentative.channel, tentative);
13966
+ await this.opts.onBound?.(tentative);
13199
13967
  }
13200
- async handleDataRequest(frame, sock, generation) {
13201
- const { channel, corr, ver } = frame.header;
13202
- const key = routeKey(generation, channel, corr);
13968
+ async handleDataRequest(frame, handle, sock, generation) {
13969
+ const { corr, ver } = frame.header;
13970
+ const key = routeKey(handle, corr);
13203
13971
  const controller = new AbortController;
13204
13972
  this.inflight.set(key, controller);
13205
- const dataFlags = buildFlags(false, Priority.Interactive, false);
13206
- const ctx = {
13973
+ const context = {
13974
+ handle,
13207
13975
  signal: controller.signal,
13208
13976
  currentEpoch: () => this.connectionEpoch,
13209
- emit: async (eventBody) => {
13977
+ emit: async (eventBody, options = {}) => {
13978
+ this.assertLiveHandle(handle);
13210
13979
  if (controller.signal.aborted)
13211
13980
  return;
13212
- await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.StreamData, dataFlags, channel, corr, eventBody));
13981
+ 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
13982
  }
13214
13983
  };
13215
13984
  const releasePermit = await (this.requestGate ?? new AsyncPermitPool(DEFAULT_PROVIDER_HANDLER_CAPACITY)).acquire();
13985
+ const dataFlags = buildFlags(false, Priority.Interactive, false);
13216
13986
  try {
13217
- const body = await this.opts.handler(channel, frame.body, ctx);
13987
+ const body = await this.opts.handler(handle, frame.body, context);
13988
+ if (controller.signal.aborted)
13989
+ return;
13990
+ this.assertLiveHandle(handle);
13218
13991
  if (body === undefined) {
13219
- await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.StreamEnd, dataFlags, channel, corr, new Uint8Array(0)));
13992
+ await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.StreamEnd, dataFlags, handle.channel, handle.epoch, corr, new Uint8Array(0)));
13220
13993
  } else if (body instanceof Uint8Array) {
13221
- await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.Response, dataFlags, channel, corr, body));
13994
+ await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.Response, dataFlags, handle.channel, handle.epoch, corr, body));
13222
13995
  } else {
13223
13996
  throw new SubcProviderError("provider handler must return a Uint8Array or void", "invalid_handler_response");
13224
13997
  }
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);
13998
+ } catch (error2) {
13999
+ if (error2 instanceof StaleRouteHandleError || controller.signal.aborted)
14000
+ return;
14001
+ await this.sendError(frame, error2 instanceof SubcProviderError && error2.code ? error2.code : "handler_error", error2 instanceof Error ? error2.message : String(error2), dataFlags, sock, generation);
13227
14002
  } finally {
13228
14003
  releasePermit();
13229
14004
  if (this.inflight.get(key) === controller)
@@ -13231,8 +14006,8 @@ class SubcProvider {
13231
14006
  }
13232
14007
  }
13233
14008
  async handleHealthRequest(frame, sock, generation) {
13234
- const { channel, corr, ver } = frame.header;
13235
- const key = routeKey(generation, channel, corr);
14009
+ const { corr, ver } = frame.header;
14010
+ const key = `control:${generation}:${corr}`;
13236
14011
  const controller = new AbortController;
13237
14012
  this.inflight.set(key, controller);
13238
14013
  const releasePermit = await (this.requestGate ?? new AsyncPermitPool(DEFAULT_PROVIDER_HANDLER_CAPACITY)).acquire();
@@ -13240,14 +14015,14 @@ class SubcProvider {
13240
14015
  if (controller.signal.aborted)
13241
14016
  return;
13242
14017
  const report = await this.opts.health();
13243
- await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.Response, controlFlags(), channel, corr, encodeJson({
14018
+ await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.Response, controlFlags(), 0, 0, corr, encodeJson({
13244
14019
  op: HEALTH_CHECK_OP,
13245
14020
  status: report.status,
13246
14021
  ...report.detail === undefined ? {} : { detail: report.detail },
13247
14022
  ...report.metrics === undefined ? {} : { metrics: report.metrics }
13248
14023
  })));
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);
14024
+ } catch (error2) {
14025
+ await this.sendError(frame, error2 instanceof SubcProviderError && error2.code ? error2.code : "health_error", error2 instanceof Error ? error2.message : String(error2), controlFlags(), sock, generation);
13251
14026
  } finally {
13252
14027
  releasePermit();
13253
14028
  if (this.inflight.get(key) === controller)
@@ -13255,7 +14030,7 @@ class SubcProvider {
13255
14030
  }
13256
14031
  }
13257
14032
  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 })));
14033
+ 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
14034
  }
13260
14035
  async sendOn(sock, generation, frame) {
13261
14036
  if (this.sock !== sock || this.generation !== generation || this.closeStarted || this.closedErr)
@@ -13263,42 +14038,79 @@ class SubcProvider {
13263
14038
  await sendFrame(sock, frame);
13264
14039
  }
13265
14040
  handleUnexpectedDrop(sock, generation, cause) {
14041
+ if (this.closeStarted || this.sock !== sock || this.generation !== generation)
14042
+ return;
13266
14043
  this.cancelRestoredDebounce();
13267
14044
  this.abortGeneration(generation);
13268
14045
  this.generation += 1;
13269
14046
  sock.close();
13270
- this.enqueueConnectionState({ state: "down", cause });
13271
- this.scheduleReconnectAfterDrop(cause);
14047
+ this.scheduleReconnectAfterDrop(cause, this.generation, sock);
13272
14048
  }
13273
- scheduleReconnectAfterDrop(trigger) {
13274
- if (this.closeStarted || this.reconnecting)
14049
+ scheduleReconnectAfterDrop(cause, generation, droppedSocket) {
14050
+ if (this.closeStarted)
13275
14051
  return;
13276
- const promise = this.reconnectWithRetry(trigger).catch((err) => {
13277
- if (!this.closeStarted)
14052
+ const previous = this.reconnecting;
14053
+ if (previous) {
14054
+ if (!this.shouldSupersedeReconnect(previous, generation, droppedSocket))
14055
+ return;
14056
+ previous.superseded = true;
14057
+ previous.socket?.close();
14058
+ }
14059
+ const cycle = {
14060
+ generation,
14061
+ socket: null,
14062
+ socketDied: false,
14063
+ superseded: false
14064
+ };
14065
+ this.reconnecting = cycle;
14066
+ this.enqueueConnectionState({ state: "down", cause });
14067
+ this.reconnectWithRetry(cycle).catch((err) => {
14068
+ if (this.isCurrentReconnect(cycle) && !this.closeStarted) {
13278
14069
  this.failFatal(err instanceof Error ? err : new SubcProviderError(String(err)));
14070
+ }
13279
14071
  }).finally(() => {
13280
- if (this.reconnecting === promise)
14072
+ if (this.isCurrentReconnect(cycle))
13281
14073
  this.reconnecting = null;
13282
14074
  });
13283
- this.reconnecting = promise;
13284
14075
  }
13285
- async reconnectWithRetry(_trigger) {
14076
+ async reconnectWithRetry(cycle) {
13286
14077
  let attempt = 0;
13287
14078
  let delay = this.opts.reconnectBackoff.baseMs;
13288
14079
  for (;; ) {
14080
+ if (!this.isCurrentReconnect(cycle))
14081
+ return;
13289
14082
  if (this.closeStarted)
13290
14083
  throw new SubcProviderError("provider closed");
14084
+ cycle.socket = null;
14085
+ cycle.socketDied = false;
13291
14086
  attempt += 1;
13292
- this.enqueueConnectionState({ state: "reconnecting", attempt });
14087
+ this.enqueueReconnectState(cycle, attempt);
13293
14088
  try {
13294
- const opened = await SubcProvider.openConnection(this.opts);
13295
- if (this.closeStarted) {
14089
+ const opened = await SubcProvider.openConnection(this.opts, (sock) => {
14090
+ if (!this.isCurrentReconnect(cycle)) {
14091
+ sock.close();
14092
+ return;
14093
+ }
14094
+ cycle.socket = sock;
14095
+ });
14096
+ if (!this.isCurrentReconnect(cycle) || this.closeStarted) {
13296
14097
  opened.sock.close();
13297
- throw new SubcProviderError("provider closed");
14098
+ if (this.closeStarted)
14099
+ throw new SubcProviderError("provider closed");
14100
+ return;
13298
14101
  }
13299
- this.replaceConnection(opened);
14102
+ const epoch = this.replaceConnection(opened, cycle.generation);
14103
+ this.reconnecting = null;
14104
+ if (this.reconnecting !== null) {
14105
+ throw new SubcProviderError("reconnect state must clear before restored", "reconnect_state");
14106
+ }
14107
+ this.scheduleRestored(cycle.generation, epoch);
13300
14108
  return;
13301
14109
  } catch (err) {
14110
+ if (!this.isCurrentReconnect(cycle))
14111
+ return;
14112
+ if (cycle.socket)
14113
+ cycle.socketDied = true;
13302
14114
  if (this.closeStarted)
13303
14115
  throw err;
13304
14116
  if (!isProviderReconnectTransient(err))
@@ -13308,16 +14120,29 @@ class SubcProvider {
13308
14120
  }
13309
14121
  }
13310
14122
  }
13311
- replaceConnection(opened) {
14123
+ isCurrentReconnect(cycle) {
14124
+ return !cycle.superseded && this.reconnecting === cycle && this.generation === cycle.generation;
14125
+ }
14126
+ shouldSupersedeReconnect(cycle, generation, droppedSocket) {
14127
+ return cycle.generation < generation || cycle.socketDied || cycle.socket === droppedSocket;
14128
+ }
14129
+ enqueueReconnectState(cycle, attempt) {
14130
+ if (!this.isCurrentReconnect(cycle))
14131
+ return;
14132
+ this.enqueueConnectionState({ state: "reconnecting", attempt }, cycle.generation, cycle);
14133
+ }
14134
+ replaceConnection(opened, generation) {
13312
14135
  this.sock.close();
13313
14136
  this.sock = opened.sock;
13314
14137
  this.currentConn = opened.conn;
13315
14138
  this.storage = opened.ack.storage;
13316
14139
  this.closedErr = null;
13317
14140
  this.connectionEpoch += 1;
13318
- const generation = this.generation;
14141
+ this.connectionToken = newConnectionToken();
14142
+ this.liveRoutes.clear();
14143
+ this.nextCorr = 1n;
13319
14144
  this.readLoop(opened.sock, generation);
13320
- this.scheduleRestored(generation, this.connectionEpoch);
14145
+ return this.connectionEpoch;
13321
14146
  }
13322
14147
  scheduleRestored(generation, epoch) {
13323
14148
  if (!this.opts.onConnectionState)
@@ -13325,7 +14150,7 @@ class SubcProvider {
13325
14150
  const token = ++this.restoredDebounceToken;
13326
14151
  this.opts.sleep(this.opts.restoredDebounceMs).then(() => {
13327
14152
  if (token === this.restoredDebounceToken && !this.closeStarted && this.sock && this.generation === generation && this.connectionEpoch === epoch) {
13328
- this.enqueueConnectionState({ state: "restored", epoch });
14153
+ this.enqueueConnectionState({ state: "restored", epoch }, generation);
13329
14154
  }
13330
14155
  }).catch((err) => {
13331
14156
  if (token === this.restoredDebounceToken && !this.closeStarted) {
@@ -13336,10 +14161,10 @@ class SubcProvider {
13336
14161
  cancelRestoredDebounce() {
13337
14162
  this.restoredDebounceToken += 1;
13338
14163
  }
13339
- enqueueConnectionState(event) {
14164
+ enqueueConnectionState(event, generation, reconnect) {
13340
14165
  if (!this.opts.onConnectionState)
13341
14166
  return;
13342
- this.stateQueue.push(event);
14167
+ this.stateQueue.push({ event, generation, reconnect });
13343
14168
  if (!this.drainingStateQueue)
13344
14169
  this.drainConnectionStateQueue();
13345
14170
  }
@@ -13349,7 +14174,12 @@ class SubcProvider {
13349
14174
  this.drainingStateQueue = true;
13350
14175
  try {
13351
14176
  while (this.stateQueue.length > 0) {
13352
- const event = this.stateQueue[0];
14177
+ const queued = this.stateQueue[0];
14178
+ if (this.closeStarted || queued.generation !== undefined && queued.generation !== this.generation || queued.reconnect?.superseded) {
14179
+ this.stateQueue.shift();
14180
+ continue;
14181
+ }
14182
+ const { event } = queued;
13353
14183
  try {
13354
14184
  await this.opts.onConnectionState?.(event);
13355
14185
  this.stateQueue.shift();
@@ -13369,6 +14199,24 @@ class SubcProvider {
13369
14199
  this.drainConnectionStateQueue();
13370
14200
  }
13371
14201
  }
14202
+ isLiveHandle(handle) {
14203
+ return belongsToConnection(handle, this.connectionToken) && this.liveRoutes.get(handle.channel) === handle;
14204
+ }
14205
+ assertLiveHandle(handle) {
14206
+ if (!this.isLiveHandle(handle))
14207
+ throw new StaleRouteHandleError(handle);
14208
+ }
14209
+ allocateCorr() {
14210
+ const maximum = 0xffffffffffffffffn;
14211
+ if (this.nextCorr > maximum) {
14212
+ const error2 = new SubcProviderError("channel-0 correlation id allocator exhausted", "corr_exhausted");
14213
+ this.handleUnexpectedDrop(this.sock, this.generation, error2);
14214
+ throw error2;
14215
+ }
14216
+ const corr = this.nextCorr;
14217
+ this.nextCorr += 1n;
14218
+ return corr;
14219
+ }
13372
14220
  failFatal(err) {
13373
14221
  if (!this.closedErr)
13374
14222
  this.closedErr = err;
@@ -13382,8 +14230,16 @@ class SubcProvider {
13382
14230
  this.resolveClosed();
13383
14231
  }
13384
14232
  }
13385
- function routeKey(generation, channel, corr) {
13386
- return `${generation}:${channel}:${corr}`;
14233
+ function routeKey(handle, corr) {
14234
+ return `${handle.channel}:${handle.epoch}:${corr}`;
14235
+ }
14236
+ function providerErrorFromFrame(frame) {
14237
+ try {
14238
+ const body = parseJson(frame.body);
14239
+ return new SubcProviderError(body.message ?? "subc error", body.code);
14240
+ } catch {
14241
+ return new SubcProviderError(Buffer2.from(frame.body).toString("utf8") || "subc error");
14242
+ }
13387
14243
  }
13388
14244
  function launchNonce(opts) {
13389
14245
  const nonce = opts.launchNonce ?? process.env[SUBC_LAUNCH_NONCE_ENV2];
@@ -13399,6 +14255,7 @@ function normalizeProviderConnectOptions(opts) {
13399
14255
  handshakeTimeoutMs: opts.handshakeTimeoutMs,
13400
14256
  controlOps: opts.controlOps,
13401
14257
  onBind: opts.onBind,
14258
+ onBound: opts.onBound,
13402
14259
  onRouteGone: opts.onRouteGone,
13403
14260
  reconnectBackoff: opts.reconnectBackoff ?? DEFAULT_RECONNECT_BACKOFF,
13404
14261
  sleep: opts.sleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms))),
@@ -13416,7 +14273,7 @@ function normalizedControlOps(controlOps) {
13416
14273
  }
13417
14274
  function buildHelloFrame(opts) {
13418
14275
  const nonce = launchNonce(opts);
13419
- return buildFrame(FrameType.Hello, controlFlags(), 0, HELLO_CORR, encodeJson({
14276
+ return buildFrame(FrameType.Hello, controlFlags(), 0, 0, HELLO_CORR, encodeJson({
13420
14277
  manifest: normalizeManifest(opts.manifest),
13421
14278
  protocol_ver: PROTOCOL_VERSION,
13422
14279
  control_ops: normalizedControlOps(opts.controlOps),
@@ -13455,14 +14312,17 @@ async function sendFrame(sock, frame) {
13455
14312
  await sock.write(encodeFrame(frame), Date.now() + WRITE_TIMEOUT_MS);
13456
14313
  }
13457
14314
  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);
14315
+ const frame = await sock.readFrame(deadline, deadline);
14316
+ switch (frame.header.ty) {
14317
+ case FrameType.HelloAck: {
14318
+ const ack = parseJson(frame.body);
14319
+ if (ack.negotiated_ver !== PROTOCOL_VERSION) {
14320
+ throw new SubcProviderError(`subc negotiated protocol ${ack.negotiated_ver}; expected exactly ${PROTOCOL_VERSION}`, "unsupported_version");
14321
+ }
14322
+ return ack;
14323
+ }
13464
14324
  case FrameType.Error: {
13465
- const error2 = parseJson(body);
14325
+ const error2 = parseJson(frame.body);
13466
14326
  throw new SubcProviderError(`subc rejected HELLO: ${error2.code ?? "unknown"} — ${error2.message ?? "subc error"}`, error2.code);
13467
14327
  }
13468
14328
  default:
@@ -13617,15 +14477,17 @@ class BgSubscription {
13617
14477
  identity;
13618
14478
  acquireClient;
13619
14479
  dropClient;
14480
+ consumerIdentity;
13620
14481
  onNudge;
13621
14482
  sleep;
13622
14483
  stopped = false;
13623
14484
  current = null;
13624
14485
  loop;
13625
- constructor(identity, acquireClient, dropClient, onNudge, sleep2) {
14486
+ constructor(identity, acquireClient, dropClient, consumerIdentity, onNudge, sleep2) {
13626
14487
  this.identity = identity;
13627
14488
  this.acquireClient = acquireClient;
13628
14489
  this.dropClient = dropClient;
14490
+ this.consumerIdentity = consumerIdentity;
13629
14491
  this.onNudge = onNudge;
13630
14492
  this.sleep = sleep2;
13631
14493
  this.loop = this.run();
@@ -13654,9 +14516,9 @@ class BgSubscription {
13654
14516
  }
13655
14517
  if (this.stopped)
13656
14518
  return;
13657
- let channel;
14519
+ let route;
13658
14520
  try {
13659
- channel = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, this.identity);
14521
+ route = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, this.identity, { consumerIdentity: this.consumerIdentity });
13660
14522
  } catch (err) {
13661
14523
  if (isConsumerReconnectTransient(err))
13662
14524
  this.dropClient(client);
@@ -13664,12 +14526,12 @@ class BgSubscription {
13664
14526
  continue;
13665
14527
  }
13666
14528
  if (this.stopped) {
13667
- safeCloseRoute(client, channel);
14529
+ safeCloseRoute(client, route);
13668
14530
  return;
13669
14531
  }
13670
14532
  const subscribedAt = Date.now();
13671
14533
  try {
13672
- const sub = client.subscribe(channel, { op: "bg_events" }, () => {
14534
+ const sub = client.subscribe(route, { op: "bg_events" }, () => {
13673
14535
  if (!this.stopped)
13674
14536
  this.onNudge();
13675
14537
  });
@@ -13689,7 +14551,7 @@ class BgSubscription {
13689
14551
  attempt = 0;
13690
14552
  } finally {
13691
14553
  this.current = null;
13692
- safeCloseRoute(client, channel);
14554
+ safeCloseRoute(client, route);
13693
14555
  }
13694
14556
  await this.backoff(attempt++);
13695
14557
  }
@@ -13702,12 +14564,12 @@ class BgSubscription {
13702
14564
  function isRecord(value) {
13703
14565
  return typeof value === "object" && value !== null && !Array.isArray(value);
13704
14566
  }
13705
- function isUnknownChannelError(err) {
13706
- return err instanceof SubcError && err.code === "unknown_channel";
14567
+ function isRouteProvenAbsentError(err) {
14568
+ return err instanceof SubcError && err.code === "unknown_channel" || err instanceof StaleRouteHandleError;
13707
14569
  }
13708
- function safeCloseRoute(client, channel) {
14570
+ function safeCloseRoute(client, route) {
13709
14571
  try {
13710
- client.closeRouteChannel(channel).catch(() => {
14572
+ client.closeRouteChannel(route).catch(() => {
13711
14573
  return;
13712
14574
  });
13713
14575
  } catch {}
@@ -13794,6 +14656,7 @@ class SubcTransportPool {
13794
14656
  harness;
13795
14657
  connectionFile;
13796
14658
  handshakeTimeoutMs;
14659
+ consumerIdentity;
13797
14660
  connectFn;
13798
14661
  onBgEventsNudge;
13799
14662
  bgBackoffSleep;
@@ -13807,6 +14670,7 @@ class SubcTransportPool {
13807
14670
  this.connectionFile = options.connectionFile;
13808
14671
  this.harness = options.harness;
13809
14672
  this.handshakeTimeoutMs = options.handshakeTimeoutMs;
14673
+ this.consumerIdentity = options.consumerIdentity;
13810
14674
  this.connectFn = options.connect ?? ((opts) => SubcClient.connect(opts));
13811
14675
  this.onBgEventsNudge = options.onBgEventsNudge;
13812
14676
  this.bgBackoffSleep = options.bgBackoffSleep ?? ((ms) => new Promise((resolve7) => setTimeout(resolve7, ms)));
@@ -13829,6 +14693,11 @@ class SubcTransportPool {
13829
14693
  return null;
13830
14694
  return this.transports.get(key) ?? null;
13831
14695
  }
14696
+ activeBridges() {
14697
+ if (!this.client || this.shuttingDown)
14698
+ return [];
14699
+ return [...this.transports.values()];
14700
+ }
13832
14701
  async toolCall(projectRoot, runtime, name, rawArgs = {}, options) {
13833
14702
  return this.getBridge(projectRoot).toolCall(runtime.sessionID, name, rawArgs, options);
13834
14703
  }
@@ -13859,7 +14728,7 @@ class SubcTransportPool {
13859
14728
  throw new RouteTornDownError("subc session closed");
13860
14729
  }
13861
14730
  try {
13862
- const opened = await this.routeChannel(client, identity, record);
14731
+ const opened = await this.routeHandle(client, identity, record);
13863
14732
  if (!this.isCurrentSession(key, record)) {
13864
14733
  throw new RouteTornDownError("subc session closed");
13865
14734
  }
@@ -13885,29 +14754,29 @@ class SubcTransportPool {
13885
14754
  if (isConsumerReconnectTransient(err)) {
13886
14755
  this.transportFailures = 0;
13887
14756
  this.dropClient(client);
13888
- } else if (!isUnknownChannelError(err) && ++this.transportFailures >= MAX_CONSECUTIVE_TRANSPORT_FAILURES) {
14757
+ } else if (!isRouteProvenAbsentError(err) && ++this.transportFailures >= MAX_CONSECUTIVE_TRANSPORT_FAILURES) {
13889
14758
  this.transportFailures = 0;
13890
14759
  this.dropClient(client);
13891
14760
  }
13892
14761
  }
13893
14762
  };
13894
- const requestOnRoute = async (channel2) => {
13895
- const reply = await client.request(channel2, body, { timeoutMs, onProgress });
14763
+ const requestOnRoute = async (route2) => {
14764
+ const reply = await client.request(route2, body, { timeoutMs, onProgress });
13896
14765
  if (this.isCurrentSession(key, record) && this.client === client) {
13897
14766
  this.transportFailures = 0;
13898
14767
  }
13899
14768
  this.ensureBgSubscription(identity, record);
13900
14769
  return reply;
13901
14770
  };
13902
- let { channel, entry } = await openRoute();
14771
+ let { route, entry } = await openRoute();
13903
14772
  try {
13904
- return await requestOnRoute(channel);
14773
+ return await requestOnRoute(route);
13905
14774
  } catch (err) {
13906
- if (isUnknownChannelError(err) && this.isCurrentSession(key, record) && this.client === client) {
14775
+ if (isRouteProvenAbsentError(err) && this.isCurrentSession(key, record) && this.client === client) {
13907
14776
  clearRouteEntry(entry);
13908
- ({ channel, entry } = await openRoute());
14777
+ ({ route, entry } = await openRoute());
13909
14778
  try {
13910
- return await requestOnRoute(channel);
14779
+ return await requestOnRoute(route);
13911
14780
  } catch (retryErr) {
13912
14781
  handleRequestFailure(retryErr, entry);
13913
14782
  throw retryErr;
@@ -13949,24 +14818,26 @@ class SubcTransportPool {
13949
14818
  });
13950
14819
  return this.connecting;
13951
14820
  }
13952
- async routeChannel(client, identity, record) {
14821
+ async routeHandle(client, identity, record) {
13953
14822
  const key = identityKey(identity);
13954
14823
  const existing = record.routeEntry;
13955
- if (existing?.channel != null)
13956
- return { channel: existing.channel, entry: existing };
14824
+ if (existing?.handle != null)
14825
+ return { route: existing.handle, entry: existing };
13957
14826
  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) => {
14827
+ return { route: await existing.opening, entry: existing };
14828
+ const entry = { client, opening: null, handle: null, closed: false };
14829
+ const opening = client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, {
14830
+ consumerIdentity: this.consumerIdentity
14831
+ }).then((route) => {
13961
14832
  if (!this.isCurrentSession(key, record) || record.routeEntry !== entry || entry.closed || this.client !== client) {
13962
- safeCloseRoute(client, channel);
14833
+ safeCloseRoute(client, route);
13963
14834
  if (record.routeEntry === entry)
13964
14835
  record.routeEntry = null;
13965
14836
  throw new RouteTornDownError("subc route opened after teardown");
13966
14837
  }
13967
- entry.channel = channel;
14838
+ entry.handle = route;
13968
14839
  entry.opening = null;
13969
- return channel;
14840
+ return route;
13970
14841
  }).catch((err) => {
13971
14842
  const current = this.isCurrentSession(key, record);
13972
14843
  if (record.routeEntry === entry) {
@@ -13980,7 +14851,7 @@ class SubcTransportPool {
13980
14851
  });
13981
14852
  entry.opening = opening;
13982
14853
  record.routeEntry = entry;
13983
- return { channel: await opening, entry };
14854
+ return { route: await opening, entry };
13984
14855
  }
13985
14856
  ensureBgSubscription(identity, record) {
13986
14857
  if (this.shuttingDown || !this.onBgEventsNudge)
@@ -13991,7 +14862,7 @@ class SubcTransportPool {
13991
14862
  if (record.bgSub)
13992
14863
  return;
13993
14864
  const onNudge = () => this.onBgEventsNudge?.(identity.project_root, identity.session);
13994
- const sub = new BgSubscription(identity, () => this.ensureClient(), (client) => this.dropClient(client), onNudge, this.bgBackoffSleep);
14865
+ const sub = new BgSubscription(identity, () => this.ensureClient(), (client) => this.dropClient(client), this.consumerIdentity, onNudge, this.bgBackoffSleep);
13995
14866
  record.bgSub = sub;
13996
14867
  }
13997
14868
  dropClient(client) {
@@ -14012,9 +14883,13 @@ class SubcTransportPool {
14012
14883
  }
14013
14884
  }
14014
14885
  setConfigureOverride(_key, _value) {}
14886
+ async reconfigure(_projectRoot, _overrides) {}
14015
14887
  async replaceBinary(path2) {
14016
14888
  return path2;
14017
14889
  }
14890
+ isShutdown() {
14891
+ return this.shuttingDown;
14892
+ }
14018
14893
  async shutdown() {
14019
14894
  this.shuttingDown = true;
14020
14895
  const subs = [];
@@ -14038,10 +14913,10 @@ class SubcTransportPool {
14038
14913
  this.transports.clear();
14039
14914
  await Promise.allSettled(subs.map((sub) => sub.stop()));
14040
14915
  await Promise.allSettled(entries.map(async (entry) => {
14041
- if (entry.channel == null)
14916
+ if (entry.handle == null)
14042
14917
  return;
14043
14918
  try {
14044
- await entry.client.closeRouteChannel(entry.channel);
14919
+ await entry.client.closeRouteChannel(entry.handle);
14045
14920
  } catch {}
14046
14921
  }));
14047
14922
  if (client) {
@@ -14070,9 +14945,9 @@ class SubcTransportPool {
14070
14945
  entry.closed = true;
14071
14946
  if (sub)
14072
14947
  await sub.stop();
14073
- if (entry?.channel != null) {
14948
+ if (entry?.handle != null) {
14074
14949
  try {
14075
- await entry.client.closeRouteChannel(entry.channel);
14950
+ await entry.client.closeRouteChannel(entry.handle);
14076
14951
  } catch {}
14077
14952
  }
14078
14953
  }
@@ -14149,18 +15024,25 @@ function collectStructuredExtras(response) {
14149
15024
  return stringifyData(extras);
14150
15025
  }
14151
15026
  // ../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";
15027
+ import { homedir as homedir11 } from "node:os";
15028
+ import { isAbsolute as isAbsolute5, join as join10 } from "node:path";
14154
15029
  function resolveConnectionFilePath(raw) {
14155
15030
  const trimmed = raw.trim();
14156
15031
  if (trimmed.startsWith("~")) {
14157
- return join9(homedir10(), trimmed.slice(1).replace(/^[/\\]/, ""));
15032
+ return join10(homedir11(), trimmed.slice(1).replace(/^[/\\]/, ""));
14158
15033
  }
14159
15034
  if (isAbsolute5(trimmed))
14160
15035
  return trimmed;
14161
- return join9(homedir10(), trimmed);
15036
+ return join10(homedir11(), trimmed);
14162
15037
  }
14163
15038
  async function createAftTransportPool(opts) {
15039
+ let binaryPath = opts.binaryPath;
15040
+ const createPool = () => createConcreteAftTransportPool({ ...opts, binaryPath });
15041
+ return new RevivableTransportPool(await createPool(), createPool, (path2) => {
15042
+ binaryPath = path2;
15043
+ });
15044
+ }
15045
+ async function createConcreteAftTransportPool(opts) {
14164
15046
  const raw = opts.subcConnectionFile?.trim();
14165
15047
  if (raw && raw.length > 0) {
14166
15048
  const connectionFile = resolveConnectionFilePath(raw);
@@ -14171,18 +15053,17 @@ async function createAftTransportPool(opts) {
14171
15053
  return new SubcTransportPool({
14172
15054
  connectionFile,
14173
15055
  harness: opts.harness,
15056
+ consumerIdentity: opts.subcConsumerIdentity,
14174
15057
  onBgEventsNudge: opts.onBgEventsNudge
14175
15058
  });
14176
15059
  }
14177
15060
  return new BridgePool(opts.binaryPath, opts.poolOptions, opts.configOverrides);
14178
15061
  }
14179
15062
  // 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
15063
  var TAG = "[aft-pi]";
14184
15064
  var isTestEnv = process.env.BUN_TEST === "1" || false;
14185
- var logFile = path2.join(os2.tmpdir(), isTestEnv ? "aft-pi-test.log" : "aft-pi.log");
15065
+ var logFile = resolveAftLogPath(isTestEnv ? "aft-plugin-test.log" : "aft-plugin.log");
15066
+ var fileSink = new RotatingLogSink(logFile);
14186
15067
  var useStderr = process.env.AFT_LOG_STDERR === "1";
14187
15068
  var buffer = [];
14188
15069
  var flushTimer = null;
@@ -14197,7 +15078,7 @@ function flush() {
14197
15078
  if (useStderr) {
14198
15079
  process.stderr.write(data);
14199
15080
  } else {
14200
- fs3.appendFileSync(logFile, data);
15081
+ fileSink.append(data);
14201
15082
  }
14202
15083
  } catch {}
14203
15084
  }
@@ -14910,7 +15791,7 @@ import {
14910
15791
  // package.json
14911
15792
  var package_default = {
14912
15793
  name: "@cortexkit/aft-pi",
14913
- version: "0.46.0",
15794
+ version: "0.47.1",
14914
15795
  type: "module",
14915
15796
  description: "Pi coding agent extension for Agent File Tools (AFT) — tree-sitter and LSP-powered code analysis",
14916
15797
  main: "dist/index.js",
@@ -14933,19 +15814,19 @@ var package_default = {
14933
15814
  "test:unit": "bun test src/__tests__ --path-ignore-patterns 'src/__tests__/e2e/**'"
14934
15815
  },
14935
15816
  dependencies: {
14936
- "@cortexkit/aft-bridge": "0.46.0",
15817
+ "@cortexkit/aft-bridge": "0.47.1",
14937
15818
  "comment-json": "^5.0.0",
14938
15819
  diff: "^8.0.4",
14939
15820
  typebox: "1.1.38",
14940
15821
  zod: "^4.4.3"
14941
15822
  },
14942
15823
  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"
15824
+ "@cortexkit/aft-darwin-arm64": "0.47.1",
15825
+ "@cortexkit/aft-darwin-x64": "0.47.1",
15826
+ "@cortexkit/aft-linux-arm64": "0.47.1",
15827
+ "@cortexkit/aft-linux-x64": "0.47.1",
15828
+ "@cortexkit/aft-win32-arm64": "0.47.1",
15829
+ "@cortexkit/aft-win32-x64": "0.47.1"
14949
15830
  },
14950
15831
  devDependencies: {
14951
15832
  "@earendil-works/pi-coding-agent": "^0.80.2",
@@ -15176,9 +16057,15 @@ function formatStatusDialogMessage(status) {
15176
16057
  }
15177
16058
 
15178
16059
  // src/tools/_shared.ts
16060
+ import { existsSync as existsSync7 } from "node:fs";
15179
16061
  import { Type } from "typebox";
15180
- var optionalInt = (_min, _max) => Type.Optional(Type.Any({ description: "(integer)" }));
16062
+ var optionalInt = (min, max, description = "(integer)") => Type.Optional(Type.Union([Type.Integer({ minimum: min, maximum: max }), Type.String()], {
16063
+ description
16064
+ }));
15181
16065
  function bridgeFor(ctx, cwd) {
16066
+ if (!existsSync7(cwd)) {
16067
+ throw new Error(`project directory no longer exists: ${cwd} (stale restored session?)`);
16068
+ }
15182
16069
  return ctx.pool.getBridge(cwd);
15183
16070
  }
15184
16071
  function resolveSessionId(extCtx) {
@@ -15534,7 +16421,7 @@ function registerStatusCommand(pi, ctx) {
15534
16421
  }
15535
16422
 
15536
16423
  // src/config.ts
15537
- import { existsSync as existsSync7, readFileSync as readFileSync7, renameSync as renameSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync4 } from "node:fs";
16424
+ import { existsSync as existsSync8, readFileSync as readFileSync7, renameSync as renameSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync4 } from "node:fs";
15538
16425
  var import_comment_json = __toESM(require_src2(), 1);
15539
16426
 
15540
16427
  // ../../node_modules/.bun/zod@4.4.3/node_modules/zod/v4/classic/external.js
@@ -16301,10 +17188,10 @@ function mergeDefs(...defs) {
16301
17188
  function cloneDef(schema) {
16302
17189
  return mergeDefs(schema._zod.def);
16303
17190
  }
16304
- function getElementAtPath(obj, path3) {
16305
- if (!path3)
17191
+ function getElementAtPath(obj, path2) {
17192
+ if (!path2)
16306
17193
  return obj;
16307
- return path3.reduce((acc, key) => acc?.[key], obj);
17194
+ return path2.reduce((acc, key) => acc?.[key], obj);
16308
17195
  }
16309
17196
  function promiseAllObject(promisesObj) {
16310
17197
  const keys = Object.keys(promisesObj);
@@ -16712,11 +17599,11 @@ function explicitlyAborted(x, startIndex = 0) {
16712
17599
  }
16713
17600
  return false;
16714
17601
  }
16715
- function prefixIssues(path3, issues) {
17602
+ function prefixIssues(path2, issues) {
16716
17603
  return issues.map((iss) => {
16717
17604
  var _a2;
16718
17605
  (_a2 = iss).path ?? (_a2.path = []);
16719
- iss.path.unshift(path3);
17606
+ iss.path.unshift(path2);
16720
17607
  return iss;
16721
17608
  });
16722
17609
  }
@@ -16863,16 +17750,16 @@ function flattenError(error3, mapper = (issue2) => issue2.message) {
16863
17750
  }
16864
17751
  function formatError(error3, mapper = (issue2) => issue2.message) {
16865
17752
  const fieldErrors = { _errors: [] };
16866
- const processError = (error4, path3 = []) => {
17753
+ const processError = (error4, path2 = []) => {
16867
17754
  for (const issue2 of error4.issues) {
16868
17755
  if (issue2.code === "invalid_union" && issue2.errors.length) {
16869
- issue2.errors.map((issues) => processError({ issues }, [...path3, ...issue2.path]));
17756
+ issue2.errors.map((issues) => processError({ issues }, [...path2, ...issue2.path]));
16870
17757
  } else if (issue2.code === "invalid_key") {
16871
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
17758
+ processError({ issues: issue2.issues }, [...path2, ...issue2.path]);
16872
17759
  } else if (issue2.code === "invalid_element") {
16873
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
17760
+ processError({ issues: issue2.issues }, [...path2, ...issue2.path]);
16874
17761
  } else {
16875
- const fullpath = [...path3, ...issue2.path];
17762
+ const fullpath = [...path2, ...issue2.path];
16876
17763
  if (fullpath.length === 0) {
16877
17764
  fieldErrors._errors.push(mapper(issue2));
16878
17765
  } else {
@@ -16899,17 +17786,17 @@ function formatError(error3, mapper = (issue2) => issue2.message) {
16899
17786
  }
16900
17787
  function treeifyError(error3, mapper = (issue2) => issue2.message) {
16901
17788
  const result = { errors: [] };
16902
- const processError = (error4, path3 = []) => {
17789
+ const processError = (error4, path2 = []) => {
16903
17790
  var _a2, _b;
16904
17791
  for (const issue2 of error4.issues) {
16905
17792
  if (issue2.code === "invalid_union" && issue2.errors.length) {
16906
- issue2.errors.map((issues) => processError({ issues }, [...path3, ...issue2.path]));
17793
+ issue2.errors.map((issues) => processError({ issues }, [...path2, ...issue2.path]));
16907
17794
  } else if (issue2.code === "invalid_key") {
16908
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
17795
+ processError({ issues: issue2.issues }, [...path2, ...issue2.path]);
16909
17796
  } else if (issue2.code === "invalid_element") {
16910
- processError({ issues: issue2.issues }, [...path3, ...issue2.path]);
17797
+ processError({ issues: issue2.issues }, [...path2, ...issue2.path]);
16911
17798
  } else {
16912
- const fullpath = [...path3, ...issue2.path];
17799
+ const fullpath = [...path2, ...issue2.path];
16913
17800
  if (fullpath.length === 0) {
16914
17801
  result.errors.push(mapper(issue2));
16915
17802
  continue;
@@ -16941,8 +17828,8 @@ function treeifyError(error3, mapper = (issue2) => issue2.message) {
16941
17828
  }
16942
17829
  function toDotPath(_path) {
16943
17830
  const segs = [];
16944
- const path3 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
16945
- for (const seg of path3) {
17831
+ const path2 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
17832
+ for (const seg of path2) {
16946
17833
  if (typeof seg === "number")
16947
17834
  segs.push(`[${seg}]`);
16948
17835
  else if (typeof seg === "symbol")
@@ -29401,13 +30288,13 @@ function resolveRef(ref, ctx) {
29401
30288
  if (!ref.startsWith("#")) {
29402
30289
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
29403
30290
  }
29404
- const path3 = ref.slice(1).split("/").filter(Boolean);
29405
- if (path3.length === 0) {
30291
+ const path2 = ref.slice(1).split("/").filter(Boolean);
30292
+ if (path2.length === 0) {
29406
30293
  return ctx.rootSchema;
29407
30294
  }
29408
30295
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
29409
- if (path3[0] === defsKey) {
29410
- const key = path3[1];
30296
+ if (path2[0] === defsKey) {
30297
+ const key = path2[1];
29411
30298
  if (!key || !ctx.defs[key]) {
29412
30299
  throw new Error(`Reference not found: ${ref}`);
29413
30300
  }
@@ -29893,6 +30780,7 @@ var SemanticConfigSchema = exports_external.object({
29893
30780
  base_url: exports_external.string().trim().min(1).optional(),
29894
30781
  api_key_env: exports_external.string().trim().min(1).optional(),
29895
30782
  timeout_ms: exports_external.number().int().positive().optional(),
30783
+ query_timeout_ms: exports_external.number().int().positive().optional(),
29896
30784
  max_batch_size: exports_external.number().int().positive().optional(),
29897
30785
  max_files: exports_external.number().int().positive().optional()
29898
30786
  });
@@ -29958,7 +30846,7 @@ var BackupConfigSchema = exports_external.object({
29958
30846
  max_depth: exports_external.number().int().positive().optional(),
29959
30847
  max_file_size: exports_external.number().int().positive().optional()
29960
30848
  });
29961
- var AftConfigSchema = exports_external.object({
30849
+ var AftConfigSchema = exports_external.preprocess((value) => stripHarnessSpecificConfigKeys(value, OPENCODE_ONLY_KEYS), exports_external.object({
29962
30850
  $schema: exports_external.string().optional(),
29963
30851
  enabled: exports_external.boolean().optional(),
29964
30852
  format_on_edit: exports_external.boolean().optional(),
@@ -29983,7 +30871,7 @@ var AftConfigSchema = exports_external.object({
29983
30871
  semantic: SemanticConfigSchema.optional(),
29984
30872
  bridge: BridgeConfigSchema.optional(),
29985
30873
  subc: SubcConfigSchema.optional()
29986
- }).strict();
30874
+ }).strict());
29987
30875
  var CONFIG_MIGRATIONS = [
29988
30876
  { oldKey: "experimental_search_index", newPath: ["search_index"] },
29989
30877
  { oldKey: "experimental_semantic_search", newPath: ["semantic_search"] },
@@ -30008,9 +30896,9 @@ function extractCommentsForPreservation(content) {
30008
30896
  }
30009
30897
  return comments;
30010
30898
  }
30011
- function ensureRecordAtPath(root, path3) {
30899
+ function ensureRecordAtPath(root, path2) {
30012
30900
  let current = root;
30013
- for (const segment of path3) {
30901
+ for (const segment of path2) {
30014
30902
  const existing = current[segment];
30015
30903
  if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
30016
30904
  current[segment] = {};
@@ -30019,9 +30907,9 @@ function ensureRecordAtPath(root, path3) {
30019
30907
  }
30020
30908
  return current;
30021
30909
  }
30022
- function hasPath(root, path3) {
30910
+ function hasPath(root, path2) {
30023
30911
  let current = root;
30024
- for (const segment of path3) {
30912
+ for (const segment of path2) {
30025
30913
  if (!current || typeof current !== "object" || Array.isArray(current))
30026
30914
  return false;
30027
30915
  const record2 = current;
@@ -30031,9 +30919,9 @@ function hasPath(root, path3) {
30031
30919
  }
30032
30920
  return true;
30033
30921
  }
30034
- function setPath(root, path3, value) {
30035
- const parent = ensureRecordAtPath(root, path3.slice(0, -1));
30036
- parent[path3[path3.length - 1]] = value;
30922
+ function setPath(root, path2, value) {
30923
+ const parent = ensureRecordAtPath(root, path2.slice(0, -1));
30924
+ parent[path2[path2.length - 1]] = value;
30037
30925
  }
30038
30926
  function migrateRawConfig(rawConfig, configPath, logger) {
30039
30927
  const oldKeys = [];
@@ -30094,7 +30982,7 @@ function migrateExperimentalBashBlock(rawConfig, configPath, logger) {
30094
30982
  return movedKeys;
30095
30983
  }
30096
30984
  function migrateAftConfigFile2(configPath, logger = { log: log2, warn: warn2 }) {
30097
- if (!existsSync7(configPath)) {
30985
+ if (!existsSync8(configPath)) {
30098
30986
  return { migrated: false, oldKeys: [] };
30099
30987
  }
30100
30988
  let tmpPath = null;
@@ -30147,7 +31035,7 @@ function recordConfigParseFailure(configPath, errorMessage) {
30147
31035
  }
30148
31036
  function loadConfigFromPath(configPath) {
30149
31037
  try {
30150
- if (!existsSync7(configPath))
31038
+ if (!existsSync8(configPath))
30151
31039
  return null;
30152
31040
  const content = readFileSync7(configPath, "utf-8");
30153
31041
  const rawConfig = import_comment_json.parse(content);
@@ -30428,7 +31316,7 @@ import { spawn as spawn2 } from "node:child_process";
30428
31316
  import { createHash as createHash4 } from "node:crypto";
30429
31317
  import {
30430
31318
  createReadStream,
30431
- existsSync as existsSync9,
31319
+ existsSync as existsSync10,
30432
31320
  mkdirSync as mkdirSync7,
30433
31321
  readFileSync as readFileSync10,
30434
31322
  renameSync as renameSync6,
@@ -30448,7 +31336,7 @@ import {
30448
31336
  unlinkSync as unlinkSync6,
30449
31337
  writeFileSync as writeFileSync5
30450
31338
  } from "node:fs";
30451
- import { homedir as homedir11 } from "node:os";
31339
+ import { homedir as homedir12 } from "node:os";
30452
31340
  import { join as join11 } from "node:path";
30453
31341
  function aftCacheBase() {
30454
31342
  const override = process.env.AFT_CACHE_DIR;
@@ -30456,10 +31344,10 @@ function aftCacheBase() {
30456
31344
  return override;
30457
31345
  if (process.platform === "win32") {
30458
31346
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
30459
- const base2 = localAppData || join11(homedir11(), "AppData", "Local");
31347
+ const base2 = localAppData || join11(homedir12(), "AppData", "Local");
30460
31348
  return join11(base2, "aft");
30461
31349
  }
30462
- const base = process.env.XDG_CACHE_HOME || join11(homedir11(), ".cache");
31350
+ const base = process.env.XDG_CACHE_HOME || join11(homedir12(), ".cache");
30463
31351
  return join11(base, "aft");
30464
31352
  }
30465
31353
  function lspCacheRoot() {
@@ -30503,11 +31391,11 @@ function writeInstalledMetaIn(installDir, version2, sha256) {
30503
31391
  }
30504
31392
  }
30505
31393
  function readInstalledMetaIn(installDir) {
30506
- const path3 = join11(installDir, INSTALLED_META_FILE);
31394
+ const path2 = join11(installDir, INSTALLED_META_FILE);
30507
31395
  try {
30508
- if (!statSync5(path3).isFile())
31396
+ if (!statSync5(path2).isFile())
30509
31397
  return null;
30510
- const raw = readFileSync8(path3, "utf8");
31398
+ const raw = readFileSync8(path2, "utf8");
30511
31399
  const parsed = JSON.parse(raw);
30512
31400
  if (typeof parsed.version !== "string" || parsed.version.length === 0)
30513
31401
  return null;
@@ -30813,7 +31701,7 @@ var NPM_LSP_TABLE = [
30813
31701
  ];
30814
31702
 
30815
31703
  // src/lsp-project-relevance.ts
30816
- import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "node:fs";
31704
+ import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync9 } from "node:fs";
30817
31705
  import { join as join12 } from "node:path";
30818
31706
  var MAX_WALK_DIRS = 200;
30819
31707
  var MAX_WALK_DEPTH = 4;
@@ -30831,7 +31719,7 @@ function hasRootMarker(projectRoot, rootMarkers) {
30831
31719
  if (!rootMarkers)
30832
31720
  return false;
30833
31721
  for (const marker of rootMarkers) {
30834
- if (existsSync8(join12(projectRoot, marker)))
31722
+ if (existsSync9(join12(projectRoot, marker)))
30835
31723
  return true;
30836
31724
  }
30837
31725
  return false;
@@ -31013,7 +31901,7 @@ async function resolveTargetVersion(spec, config2, fetchImpl = fetch) {
31013
31901
  function ensureInstallAnchor(cwd) {
31014
31902
  try {
31015
31903
  const stub = join13(cwd, "package.json");
31016
- if (!existsSync9(stub)) {
31904
+ if (!existsSync10(stub)) {
31017
31905
  writeFileSync6(stub, `${JSON.stringify({ name: "aft-lsp-cache", version: "0.0.0", private: true })}
31018
31906
  `);
31019
31907
  }
@@ -31199,8 +32087,8 @@ function installedBinaryPath(spec) {
31199
32087
  }
31200
32088
  return null;
31201
32089
  }
31202
- function sha256OfFileSync(path3) {
31203
- return createHash4("sha256").update(readFileSync10(path3)).digest("hex");
32090
+ function sha256OfFileSync(path2) {
32091
+ return createHash4("sha256").update(readFileSync10(path2)).digest("hex");
31204
32092
  }
31205
32093
  function quarantineCachedNpmInstall(spec, reason) {
31206
32094
  const packageDir = lspPackageDir(spec.npm);
@@ -31278,9 +32166,15 @@ function runAutoInstall(projectRoot, config2, fetchImpl = fetch) {
31278
32166
  return installsStarted;
31279
32167
  },
31280
32168
  skipped,
31281
- installsComplete: Promise.all(installPromises).then(() => {})
32169
+ installsComplete: Promise.all(installPromises).then(() => {}),
32170
+ getCachedBinDirs: () => NPM_LSP_TABLE.filter((spec) => isInstalled(spec.npm, spec.binary) && validateCachedNpmInstall(spec)).map((spec) => lspBinDir(spec.npm))
31282
32171
  };
31283
32172
  }
32173
+ async function pushLspPathsAfterAutoInstall(pool, projectRoot, cachedBinDirs) {
32174
+ const paths = [...new Set(cachedBinDirs)];
32175
+ pool.setConfigureOverride("lsp_paths_extra", paths);
32176
+ await pool.reconfigure(projectRoot, { lsp_paths_extra: paths });
32177
+ }
31284
32178
 
31285
32179
  // src/lsp-github-install.ts
31286
32180
  import { execFileSync as execFileSync2 } from "node:child_process";
@@ -31289,7 +32183,7 @@ import {
31289
32183
  copyFileSync as copyFileSync4,
31290
32184
  createReadStream as createReadStream2,
31291
32185
  createWriteStream as createWriteStream3,
31292
- existsSync as existsSync10,
32186
+ existsSync as existsSync11,
31293
32187
  lstatSync as lstatSync2,
31294
32188
  mkdirSync as mkdirSync8,
31295
32189
  readdirSync as readdirSync4,
@@ -31302,7 +32196,7 @@ import {
31302
32196
  unlinkSync as unlinkSync7,
31303
32197
  writeFileSync as writeFileSync7
31304
32198
  } from "node:fs";
31305
- import { dirname as dirname5, join as join14, relative as relative3, resolve as resolve7 } from "node:path";
32199
+ import { dirname as dirname6, join as join14, relative as relative3, resolve as resolve7 } from "node:path";
31306
32200
  import { Readable as Readable3 } from "node:stream";
31307
32201
  import { pipeline as pipeline3 } from "node:stream/promises";
31308
32202
 
@@ -31424,10 +32318,10 @@ function ghBinaryCandidates(spec, platform) {
31424
32318
  }
31425
32319
  function readGithubInstalledMetaIn(installDir) {
31426
32320
  try {
31427
- const path3 = join14(installDir, INSTALLED_META_FILE2);
31428
- if (!statSync7(path3).isFile())
32321
+ const path2 = join14(installDir, INSTALLED_META_FILE2);
32322
+ if (!statSync7(path2).isFile())
31429
32323
  return null;
31430
- const parsed = JSON.parse(readFileSync11(path3, "utf8"));
32324
+ const parsed = JSON.parse(readFileSync11(path2, "utf8"));
31431
32325
  if (typeof parsed.version !== "string" || parsed.version.length === 0)
31432
32326
  return null;
31433
32327
  return {
@@ -31458,17 +32352,17 @@ function writeGithubInstalledMetaIn(installDir, version2, binarySha256, archiveS
31458
32352
  }
31459
32353
  var MAX_DOWNLOAD_BYTES3 = 256 * 1024 * 1024;
31460
32354
  var MAX_EXTRACT_BYTES2 = 1024 * 1024 * 1024;
31461
- function sha256OfFile(path3) {
32355
+ function sha256OfFile(path2) {
31462
32356
  return new Promise((resolve8, reject) => {
31463
32357
  const hash2 = createHash5("sha256");
31464
- const stream = createReadStream2(path3);
32358
+ const stream = createReadStream2(path2);
31465
32359
  stream.on("error", reject);
31466
32360
  stream.on("data", (chunk) => hash2.update(chunk));
31467
32361
  stream.on("end", () => resolve8(hash2.digest("hex")));
31468
32362
  });
31469
32363
  }
31470
- function sha256OfFileSync2(path3) {
31471
- return createHash5("sha256").update(readFileSync11(path3)).digest("hex");
32364
+ function sha256OfFileSync2(path2) {
32365
+ return createHash5("sha256").update(readFileSync11(path2)).digest("hex");
31472
32366
  }
31473
32367
  async function fetchReleaseByTag(githubRepo, tag, fetchImpl, signal) {
31474
32368
  const candidates = [];
@@ -31644,7 +32538,7 @@ async function downloadFile(url2, destPath, fetchImpl, assetSize, signal) {
31644
32538
  if (Number.isFinite(advertised) && advertised > MAX_DOWNLOAD_BYTES3) {
31645
32539
  throw new Error(`Content-Length ${advertised} exceeds max ${MAX_DOWNLOAD_BYTES3}`);
31646
32540
  }
31647
- mkdirSync8(dirname5(destPath), { recursive: true });
32541
+ mkdirSync8(dirname6(destPath), { recursive: true });
31648
32542
  let bytesWritten = 0;
31649
32543
  const guard = new TransformStream({
31650
32544
  transform(chunk, controller) {
@@ -31791,7 +32685,7 @@ function quarantineCachedGithubInstall(spec, reason) {
31791
32685
  const dest = join14(ghCacheRoot(), ".quarantine", encodeURIComponent(spec.id), `${Date.now()}`);
31792
32686
  warn2(`[lsp] tofu_mismatch ${spec.id}: ${reason}; quarantining ${packageDir} -> ${dest}`);
31793
32687
  try {
31794
- mkdirSync8(dirname5(dest), { recursive: true });
32688
+ mkdirSync8(dirname6(dest), { recursive: true });
31795
32689
  rmSync5(dest, { recursive: true, force: true });
31796
32690
  renameSync7(packageDir, dest);
31797
32691
  } catch (err) {
@@ -31920,12 +32814,12 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
31920
32814
  } catch {}
31921
32815
  }
31922
32816
  const innerBinaryPath = join14(extractDir, spec.binaryPathInArchive(platform, arch, version2));
31923
- if (!existsSync10(innerBinaryPath)) {
32817
+ if (!existsSync11(innerBinaryPath)) {
31924
32818
  error2(`[lsp] ${spec.id}: extracted binary not found at ${innerBinaryPath}`);
31925
32819
  return null;
31926
32820
  }
31927
32821
  const targetBinary = ghBinaryPath(spec, platform);
31928
- mkdirSync8(dirname5(targetBinary), { recursive: true });
32822
+ mkdirSync8(dirname6(targetBinary), { recursive: true });
31929
32823
  try {
31930
32824
  copyFileSync4(innerBinaryPath, targetBinary);
31931
32825
  if (platform !== "win32") {
@@ -32004,7 +32898,7 @@ function runGithubAutoInstall(relevantServers, config2, fetchImpl = fetch) {
32004
32898
  if (!host) {
32005
32899
  for (const spec of GITHUB_LSP_TABLE) {
32006
32900
  try {
32007
- if (existsSync10(ghBinDir(spec))) {
32901
+ if (existsSync11(ghBinDir(spec))) {
32008
32902
  cachedBinDirs.push(ghBinDir(spec));
32009
32903
  }
32010
32904
  } catch {}
@@ -32014,7 +32908,8 @@ function runGithubAutoInstall(relevantServers, config2, fetchImpl = fetch) {
32014
32908
  installsStarted: 0,
32015
32909
  installingBinaries: [],
32016
32910
  skipped,
32017
- installsComplete: Promise.resolve()
32911
+ installsComplete: Promise.resolve(),
32912
+ getCachedBinDirs: () => [...cachedBinDirs]
32018
32913
  };
32019
32914
  }
32020
32915
  for (const spec of GITHUB_LSP_TABLE) {
@@ -32056,7 +32951,13 @@ function runGithubAutoInstall(relevantServers, config2, fetchImpl = fetch) {
32056
32951
  return installsStarted;
32057
32952
  },
32058
32953
  skipped,
32059
- installsComplete: Promise.all(installPromises).then(() => {})
32954
+ installsComplete: Promise.all(installPromises).then(() => {}),
32955
+ getCachedBinDirs: () => {
32956
+ const currentHost = detectHostPlatform();
32957
+ if (!currentHost)
32958
+ return [];
32959
+ return GITHUB_LSP_TABLE.filter((spec) => isGithubInstalled(spec, currentHost.platform) && validateCachedGithubInstall(spec, currentHost.platform)).map((spec) => ghBinDir(spec));
32960
+ }
32060
32961
  };
32061
32962
  }
32062
32963
  function discoverRelevantGithubServers(projectRoot) {
@@ -32242,6 +33143,27 @@ function statusBarBlockForSession(sessionID, counts, force = false) {
32242
33143
  return shouldEmitStatusBar(state, counts) ? formatStatusBar(counts) : undefined;
32243
33144
  }
32244
33145
 
33146
+ // src/bash-wait-detach.ts
33147
+ var BASH_TRANSPORT_TIMEOUT_MS = 30000;
33148
+ async function sendBashWaitDetach(bridge, sessionID) {
33149
+ const response = await bridge.send("bash_wait_detach", { session_id: sessionID }, { keepBridgeOnTimeout: true, transportTimeoutMs: BASH_TRANSPORT_TIMEOUT_MS });
33150
+ if (response.success === false) {
33151
+ throw new Error(String(response.message ?? "bash_wait_detach failed"));
33152
+ }
33153
+ }
33154
+ async function signalBashWaitDetachForProject(pool, projectRoot, sessionID) {
33155
+ if (!sessionID)
33156
+ return;
33157
+ const bridge = pool.getActiveBridgeForRoot(projectRoot);
33158
+ if (!bridge)
33159
+ return;
33160
+ try {
33161
+ await sendBashWaitDetach(bridge, sessionID);
33162
+ } catch (err) {
33163
+ warn2(`[bash_wait_detach] failed for session ${sessionID}: ${err instanceof Error ? err.message : String(err)}`);
33164
+ }
33165
+ }
33166
+
32245
33167
  // src/shutdown-hooks.ts
32246
33168
  var GLOBAL_KEY = "__aftPiShutdownHooks__";
32247
33169
  function getState() {
@@ -32338,8 +33260,8 @@ import { StringEnum } from "@earendil-works/pi-ai";
32338
33260
  import { Type as Type3 } from "typebox";
32339
33261
 
32340
33262
  // src/tools/hoisted.ts
32341
- import { stat } from "node:fs/promises";
32342
- import { homedir as homedir12 } from "node:os";
33263
+ import { stat as stat2 } from "node:fs/promises";
33264
+ import { homedir as homedir13 } from "node:os";
32343
33265
  import { isAbsolute as isAbsolute6, relative as relative4, resolve as resolve8, sep } from "node:path";
32344
33266
  import {
32345
33267
  renderDiff
@@ -32483,23 +33405,24 @@ function containsPath(parent, child) {
32483
33405
  const rel = relative4(parent, child);
32484
33406
  return rel === "" || !rel.startsWith("..") && !isAbsolute6(rel);
32485
33407
  }
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));
33408
+ function normalizePathInput(path2) {
33409
+ const decoded = decodeFileUrl(path2);
33410
+ if (!decoded || !decoded.startsWith("~"))
33411
+ return decoded;
33412
+ if (decoded === "~")
33413
+ return homedir13();
33414
+ if (decoded.startsWith(`~${sep}`) || decoded.startsWith("~/")) {
33415
+ return resolve8(homedir13(), decoded.slice(2));
32493
33416
  }
32494
- return path3;
33417
+ return decoded;
32495
33418
  }
32496
33419
  function absoluteSearchPath(cwd, target) {
32497
- const expanded = expandTilde2(target);
33420
+ const expanded = normalizePathInput(target);
32498
33421
  return isAbsolute6(expanded) ? expanded : resolve8(cwd, expanded);
32499
33422
  }
32500
33423
  async function searchPathExists(cwd, target) {
32501
33424
  try {
32502
- await stat(absoluteSearchPath(cwd, target));
33425
+ await stat2(absoluteSearchPath(cwd, target));
32503
33426
  return true;
32504
33427
  } catch {
32505
33428
  return false;
@@ -32550,12 +33473,14 @@ ${note}` : note;
32550
33473
  async function assertExternalDirectoryPermission(extCtx, target, options = {}) {
32551
33474
  if (!target)
32552
33475
  return;
32553
- const expanded = expandTilde2(target);
33476
+ const expanded = normalizePathInput(target);
32554
33477
  const absoluteTarget = isAbsolute6(expanded) ? expanded : resolve8(extCtx.cwd, expanded);
32555
33478
  if (containsPath(extCtx.cwd, absoluteTarget))
32556
33479
  return;
32557
33480
  if (options.restrictToProjectRoot === false)
32558
33481
  return;
33482
+ if (options.serverValidatedRead === true)
33483
+ return;
32559
33484
  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
33485
  }
32561
33486
  var ReadParams = Type2.Object({
@@ -32565,8 +33490,8 @@ var ReadParams = Type2.Object({
32565
33490
  filePath: Type2.Optional(Type2.String({
32566
33491
  description: "Alias for `path` — provide one of the two."
32567
33492
  })),
32568
- offset: optionalInt(1, Number.MAX_SAFE_INTEGER),
32569
- limit: optionalInt(1, Number.MAX_SAFE_INTEGER)
33493
+ offset: optionalInt(1, Number.MAX_SAFE_INTEGER, "1-based line number to start reading from (use with limit)"),
33494
+ limit: optionalInt(1, Number.MAX_SAFE_INTEGER, "Maximum number of lines to return")
32570
33495
  });
32571
33496
  var WriteParams = Type2.Object({
32572
33497
  filePath: Type2.Optional(Type2.String({
@@ -32580,8 +33505,10 @@ var WriteParams = Type2.Object({
32580
33505
  var BatchEditParams = Type2.Object({
32581
33506
  oldString: Type2.Optional(Type2.String({ description: "Text to find for a batch find/replace edit" })),
32582
33507
  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" })),
33508
+ replaceAll: Type2.Optional(Type2.Boolean({ description: "Replace every occurrence for this batch item" })),
33509
+ occurrence: optionalInt(0, Number.MAX_SAFE_INTEGER, "0-based occurrence for this batch item"),
33510
+ startLine: optionalInt(1, Number.MAX_SAFE_INTEGER, "1-based start line for a batch line-range edit"),
33511
+ endLine: optionalInt(1, Number.MAX_SAFE_INTEGER, "1-based end line for a batch line-range edit"),
32585
33512
  content: Type2.Optional(Type2.String({
32586
33513
  description: "Replacement text for a batch line-range edit (empty string deletes the lines)"
32587
33514
  }))
@@ -32596,12 +33523,12 @@ var EditParams = Type2.Object({
32596
33523
  oldString: Type2.Optional(Type2.String({ description: "Text to find (exact match, fuzzy fallback)" })),
32597
33524
  newString: Type2.Optional(Type2.String({ description: "Replacement text (omit to delete match)" })),
32598
33525
  replaceAll: Type2.Optional(Type2.Boolean({ description: "Replace every occurrence" })),
32599
- occurrence: optionalInt(0, Number.MAX_SAFE_INTEGER),
33526
+ occurrence: optionalInt(0, Number.MAX_SAFE_INTEGER, "0-based occurrence to replace when multiple matches exist"),
32600
33527
  appendContent: Type2.Optional(Type2.String({
32601
33528
  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
33529
  })),
32603
33530
  edits: Type2.Optional(Type2.Array(BatchEditParams, {
32604
- description: "Batch edits — array of { oldString, newString } or { startLine, endLine, content } objects applied atomically to one file."
33531
+ description: "Batch edits — array of { oldString, newString }, { oldString, newString, replaceAll: true }, or { startLine, endLine, content } objects applied atomically."
32605
33532
  }))
32606
33533
  });
32607
33534
  var GrepParams = Type2.Object({
@@ -32685,7 +33612,8 @@ function registerHoistedTools(pi, ctx, surface) {
32685
33612
  const limit = coerceOptionalInt(params.limit, "limit", 1, Number.MAX_SAFE_INTEGER);
32686
33613
  const filePath = await resolvePathArg(extCtx.cwd, pathArg);
32687
33614
  await assertExternalDirectoryPermission(extCtx, filePath, {
32688
- restrictToProjectRoot: surface.restrictToProjectRoot
33615
+ restrictToProjectRoot: surface.restrictToProjectRoot,
33616
+ serverValidatedRead: true
32689
33617
  });
32690
33618
  const rawArgs = { filePath: pathArg };
32691
33619
  if (offset !== undefined)
@@ -32766,7 +33694,7 @@ PDFs aren't supported on the Pi harness yet.`, response);
32766
33694
  pi.registerTool({
32767
33695
  name: "edit",
32768
33696
  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`.",
33697
+ 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
33698
  promptSnippet: "Partial file edits via appendContent, edits[], or oldString/newString (mode priority: appendContent > edits > oldString).",
32771
33699
  promptGuidelines: [
32772
33700
  "Prefer edit over write when changing part of an existing file.",
@@ -32792,10 +33720,20 @@ PDFs aren't supported on the Pi harness yet.`, response);
32792
33720
  });
32793
33721
  const bridge = bridgeFor(ctx, extCtx.cwd);
32794
33722
  const rawArgs = { filePath: filePathArg };
32795
- for (const key of ["appendContent", "edits", "oldString", "newString"]) {
33723
+ for (const key of ["appendContent", "oldString", "newString"]) {
32796
33724
  if (argsRecord[key] !== undefined)
32797
33725
  rawArgs[key] = argsRecord[key];
32798
33726
  }
33727
+ if (Array.isArray(argsRecord.edits)) {
33728
+ rawArgs.edits = argsRecord.edits.map((item) => {
33729
+ if (!item || typeof item !== "object" || Array.isArray(item))
33730
+ return item;
33731
+ const batchItem = item;
33732
+ return batchItem.replaceAll === undefined ? batchItem : { ...batchItem, replaceAll: coerceBoolean(batchItem.replaceAll) };
33733
+ });
33734
+ } else if (argsRecord.edits !== undefined) {
33735
+ rawArgs.edits = argsRecord.edits;
33736
+ }
32799
33737
  if (params.replaceAll !== undefined)
32800
33738
  rawArgs.replaceAll = coerceBoolean(params.replaceAll);
32801
33739
  const occurrence = coerceOptionalInt(params.occurrence, "occurrence", 0, Number.MAX_SAFE_INTEGER);
@@ -32998,17 +33936,17 @@ ${summary}${suffix}`);
32998
33936
  container.addChild(new Text(renderDiff(diff), 1, 0));
32999
33937
  return container;
33000
33938
  }
33001
- function shortenPath2(path3) {
33002
- const home = homedir12();
33003
- if (path3.startsWith(home))
33004
- return `~${path3.slice(home.length)}`;
33005
- return path3;
33939
+ function shortenPath2(path2) {
33940
+ const home = homedir13();
33941
+ if (path2.startsWith(home))
33942
+ return `~${path2.slice(home.length)}`;
33943
+ return path2;
33006
33944
  }
33007
- async function resolvePathArg(cwd, path3) {
33008
- const expanded = expandTilde2(path3);
33009
- const abs = absoluteSearchPath(cwd, path3);
33945
+ async function resolvePathArg(cwd, path2) {
33946
+ const expanded = normalizePathInput(path2);
33947
+ const abs = absoluteSearchPath(cwd, path2);
33010
33948
  try {
33011
- await stat(abs);
33949
+ await stat2(abs);
33012
33950
  return abs;
33013
33951
  } catch {
33014
33952
  return expanded;
@@ -33016,7 +33954,7 @@ async function resolvePathArg(cwd, path3) {
33016
33954
  }
33017
33955
 
33018
33956
  // src/tools/render-helpers.ts
33019
- import { homedir as homedir13 } from "node:os";
33957
+ import { homedir as homedir14 } from "node:os";
33020
33958
  import { renderDiff as renderDiff2 } from "@earendil-works/pi-coding-agent";
33021
33959
  import { Container as Container2, Spacer as Spacer2, Text as Text2 } from "@earendil-works/pi-tui";
33022
33960
  function reuseText2(last) {
@@ -33025,11 +33963,11 @@ function reuseText2(last) {
33025
33963
  function reuseContainer2(last) {
33026
33964
  return last instanceof Container2 ? last : new Container2;
33027
33965
  }
33028
- function shortenPath3(path3) {
33029
- const home = homedir13();
33030
- if (path3.startsWith(home))
33031
- return `~${path3.slice(home.length)}`;
33032
- return path3;
33966
+ function shortenPath3(path2) {
33967
+ const home = homedir14();
33968
+ if (path2.startsWith(home))
33969
+ return `~${path2.slice(home.length)}`;
33970
+ return path2;
33033
33971
  }
33034
33972
  function renderToolCall(toolName, summary, theme, context) {
33035
33973
  const text = reuseText2(context.lastComponent);
@@ -33037,10 +33975,10 @@ function renderToolCall(toolName, summary, theme, context) {
33037
33975
  text.setText(`${theme.fg("toolTitle", theme.bold(toolName))}${suffix}`);
33038
33976
  return text;
33039
33977
  }
33040
- function accentPath(theme, path3) {
33041
- if (!path3)
33978
+ function accentPath(theme, path2) {
33979
+ if (!path2)
33042
33980
  return theme.fg("toolOutput", "...");
33043
- return theme.fg("accent", shortenPath3(path3));
33981
+ return theme.fg("accent", shortenPath3(path2));
33044
33982
  }
33045
33983
  function collectTextContent(result) {
33046
33984
  return result.content.filter((part) => part.type === "text").map((part) => part.text ?? "").join(`
@@ -33225,17 +34163,17 @@ ${warningStrings.map((w) => ` ${w}`).join(`
33225
34163
  async function resolveAstPaths(extCtx, paths) {
33226
34164
  if (isEmptyParam(paths) || !Array.isArray(paths))
33227
34165
  return;
33228
- return Promise.all(paths.filter((path3) => typeof path3 === "string").map((path3) => resolvePathArg(extCtx.cwd, path3)));
34166
+ return Promise.all(paths.filter((path2) => typeof path2 === "string").map((path2) => resolvePathArg(extCtx.cwd, path2)));
33229
34167
  }
33230
34168
  async function assertAstPathsPermission(extCtx, paths, restrictToProjectRoot) {
33231
34169
  if (paths === undefined || paths.length === 0)
33232
34170
  return;
33233
34171
  const checked = new Set;
33234
- for (const path3 of paths) {
33235
- if (checked.has(path3))
34172
+ for (const path2 of paths) {
34173
+ if (checked.has(path2))
33236
34174
  continue;
33237
- checked.add(path3);
33238
- await assertExternalDirectoryPermission(extCtx, path3, { restrictToProjectRoot });
34175
+ checked.add(path2);
34176
+ await assertExternalDirectoryPermission(extCtx, path2, { restrictToProjectRoot });
33239
34177
  }
33240
34178
  }
33241
34179
  function buildAstSearchSections(payload, theme) {
@@ -33417,7 +34355,7 @@ function registerAstTools(pi, ctx, surface) {
33417
34355
  }
33418
34356
 
33419
34357
  // src/tools/bash.ts
33420
- import * as fs4 from "node:fs/promises";
34358
+ import * as fs3 from "node:fs/promises";
33421
34359
  import { Container as Container3, Spacer as Spacer3, Text as Text3 } from "@earendil-works/pi-tui";
33422
34360
  import { Type as Type4 } from "typebox";
33423
34361
  var BASH_WAIT_POLL_INTERVAL_MS = 100;
@@ -33433,7 +34371,7 @@ function resolveForegroundWaitMs(configured) {
33433
34371
  }
33434
34372
  return configured;
33435
34373
  }
33436
- var BASH_TRANSPORT_TIMEOUT_MS = 30000;
34374
+ var BASH_TRANSPORT_TIMEOUT_MS2 = 30000;
33437
34375
  var DEFAULT_HARD_TIMEOUT_MS = 30 * 60 * 1000;
33438
34376
  var BASH_TRANSPORT_MARGIN_MS = 1e4;
33439
34377
  function orchestratedTransportTimeoutMs(blockToCompletion, wait, effectiveTimeout, foregroundWaitMs) {
@@ -33444,7 +34382,7 @@ var BashBaseParams = {
33444
34382
  command: Type4.String({
33445
34383
  description: "Shell command to execute. Supports pipes, redirections, and shell syntax."
33446
34384
  }),
33447
- timeout: optionalInt(1, Number.MAX_SAFE_INTEGER),
34385
+ timeout: optionalInt(1, Number.MAX_SAFE_INTEGER, "Hard kill timeout in milliseconds"),
33448
34386
  workdir: Type4.Optional(Type4.String({
33449
34387
  description: "Working directory for command execution. Relative paths resolve against the project root. Defaults to the current session's working directory."
33450
34388
  })),
@@ -33452,7 +34390,7 @@ var BashBaseParams = {
33452
34390
  description: "Human-readable description shown in UI logs. Helps users understand what the command does without reading shell syntax."
33453
34391
  })),
33454
34392
  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."
34393
+ 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
34394
  }))
33457
34395
  };
33458
34396
  var BashBackgroundFlagParam = {
@@ -33469,8 +34407,8 @@ var BashPtyParams = {
33469
34407
  pty: Type4.Optional(Type4.Boolean({
33470
34408
  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
34409
  })),
33472
- ptyRows: optionalInt(1, 60),
33473
- ptyCols: optionalInt(1, 140)
34410
+ ptyRows: optionalInt(1, 60, "PTY terminal height in rows (minimum 1, maximum 60)"),
34411
+ ptyCols: optionalInt(1, 140, "PTY terminal width in columns (minimum 1, maximum 140)")
33474
34412
  };
33475
34413
  var BashParams = Type4.Object({
33476
34414
  ...BashBaseParams,
@@ -33504,7 +34442,7 @@ var BashWatchParams = Type4.Object({
33504
34442
  }),
33505
34443
  pattern: Type4.Optional(Type4.Union([Type4.String(), Type4.Object({ regex: Type4.String() })])),
33506
34444
  background: Type4.Optional(Type4.Boolean()),
33507
- timeout_ms: optionalInt(1, MAX_BASH_STATUS_WAIT_TIMEOUT_MS),
34445
+ timeout_ms: optionalInt(1, MAX_BASH_STATUS_WAIT_TIMEOUT_MS, "Maximum time to wait in milliseconds"),
33508
34446
  once: Type4.Optional(Type4.Boolean())
33509
34447
  });
33510
34448
  var BashWriteParams = Type4.Object({
@@ -33530,7 +34468,7 @@ function unavailableSnapshot() {
33530
34468
  }
33531
34469
  async function callBashBridge(bridge, command, params = {}, extCtx, options) {
33532
34470
  return await callBridge(bridge, command, params, extCtx, {
33533
- transportTimeoutMs: BASH_TRANSPORT_TIMEOUT_MS,
34471
+ transportTimeoutMs: BASH_TRANSPORT_TIMEOUT_MS2,
33534
34472
  ...options,
33535
34473
  keepBridgeOnTimeout: true
33536
34474
  });
@@ -33561,7 +34499,7 @@ function registerBashTool(pi, ctx, aftSearchRegistered = false) {
33561
34499
  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
34500
  const bashCfg = resolveBashConfig(ctx.config);
33563
34501
  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).";
34502
+ 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
34503
  pi.registerTool({
33566
34504
  name: "bash",
33567
34505
  label: "bash",
@@ -33835,7 +34773,7 @@ async function waitForBashStatus(ctx, bridge, extCtx, taskId, outputMode, waitFo
33835
34773
  let scanBaseOffset = 0;
33836
34774
  const bridgeOptions = {
33837
34775
  keepBridgeOnTimeout: true,
33838
- transportTimeoutMs: BASH_TRANSPORT_TIMEOUT_MS
34776
+ transportTimeoutMs: BASH_TRANSPORT_TIMEOUT_MS2
33839
34777
  };
33840
34778
  if (waitFor?.kind === "regex") {
33841
34779
  await validateWaitRegex(bridge, extCtx, waitFor);
@@ -33946,7 +34884,7 @@ async function readNewTaskOutput(data, cursor) {
33946
34884
  };
33947
34885
  }
33948
34886
  async function readFileBytesFrom(outputPath, cursor) {
33949
- const handle = await fs4.open(outputPath, "r");
34887
+ const handle = await fs3.open(outputPath, "r");
33950
34888
  try {
33951
34889
  const chunks = [];
33952
34890
  let offset = cursor;
@@ -34075,7 +35013,7 @@ async function formatPtyStatus(_extCtx, taskId, details, requestedOutputMode) {
34075
35013
  return `
34076
35014
  [PTY output path unavailable]`;
34077
35015
  const outputMode = requestedOutputMode ?? "screen";
34078
- const raw = outputMode === "raw" || outputMode === "both" ? await fs4.readFile(details.output_path) : undefined;
35016
+ const raw = outputMode === "raw" || outputMode === "both" ? await fs3.readFile(details.output_path) : undefined;
34079
35017
  let suffix = "";
34080
35018
  if (outputMode === "raw") {
34081
35019
  suffix = raw && raw.length > 0 ? `
@@ -34196,12 +35134,12 @@ function registerConflictsTool(pi, ctx) {
34196
35134
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
34197
35135
  const bridge = bridgeFor(ctx, extCtx.cwd);
34198
35136
  const reqParams = {};
34199
- const path3 = params?.path;
34200
- if (typeof path3 === "string" && path3.trim() !== "") {
34201
- await assertExternalDirectoryPermission(extCtx, path3, {
35137
+ const path2 = params?.path;
35138
+ if (typeof path2 === "string" && path2.trim() !== "") {
35139
+ await assertExternalDirectoryPermission(extCtx, path2, {
34202
35140
  restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
34203
35141
  });
34204
- reqParams.path = await resolvePathArg(extCtx.cwd, path3);
35142
+ reqParams.path = await resolvePathArg(extCtx.cwd, path2);
34205
35143
  }
34206
35144
  const response = await callToolCall(bridge, "conflicts", reqParams, extCtx);
34207
35145
  if (response.success === false) {
@@ -34613,8 +35551,8 @@ function tier2SummaryPart(summary, key, label) {
34613
35551
  return `${label} ${status ?? "unavailable"}`;
34614
35552
  }
34615
35553
  function shortDupOccurrence(entry) {
34616
- const [path3] = entry.split(":");
34617
- return path3?.split("/").pop() ?? entry;
35554
+ const [path2] = entry.split(":");
35555
+ return path2?.split("/").pop() ?? entry;
34618
35556
  }
34619
35557
  function tier2TopPreview(summary, theme) {
34620
35558
  const lines = [];
@@ -34798,7 +35736,7 @@ function navigateParamsSchema() {
34798
35736
  description: "Source file containing the symbol (absolute or relative to project root)"
34799
35737
  }),
34800
35738
  symbol: Type9.String({ description: "Name of the symbol to analyze" }),
34801
- depth: optionalInt(1, Number.MAX_SAFE_INTEGER),
35739
+ depth: optionalInt(1, Number.MAX_SAFE_INTEGER, "Maximum call-graph depth to traverse"),
34802
35740
  expression: Type9.Optional(Type9.String({ description: "Expression to track (required for trace_data)" })),
34803
35741
  toSymbol: Type9.Optional(Type9.String({
34804
35742
  description: "Target symbol for trace_to_symbol; the returned path ends here"
@@ -35235,9 +36173,9 @@ var RefactorParams = Type11.Object({
35235
36173
  destination: Type11.Optional(Type11.String({ description: "Target file (for move)" })),
35236
36174
  scope: Type11.Optional(Type11.String({ description: "Disambiguation scope for move op" })),
35237
36175
  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)
36176
+ startLine: optionalInt(1, Number.MAX_SAFE_INTEGER, "1-based start line for extract"),
36177
+ endLine: optionalInt(1, Number.MAX_SAFE_INTEGER, "1-based end line (inclusive) for extract"),
36178
+ callSiteLine: optionalInt(1, Number.MAX_SAFE_INTEGER, "1-based call site line for inline")
35241
36179
  });
35242
36180
  function buildRefactorSections(args, payload, theme) {
35243
36181
  const response = asRecord3(payload);
@@ -35356,7 +36294,7 @@ function registerRefactorTool(pi, ctx) {
35356
36294
  import { StringEnum as StringEnum5 } from "@earendil-works/pi-ai";
35357
36295
  import { Type as Type12 } from "typebox";
35358
36296
  function responsePaths(response) {
35359
- return Array.isArray(response.paths) ? response.paths.filter((path3) => typeof path3 === "string" && path3.length > 0) : [];
36297
+ return Array.isArray(response.paths) ? response.paths.filter((path2) => typeof path2 === "string" && path2.length > 0) : [];
35360
36298
  }
35361
36299
  var SafetyParams = Type12.Object({
35362
36300
  op: StringEnum5(["undo", "history", "checkpoint", "restore", "list"], {
@@ -35738,7 +36676,7 @@ function buildWorkflowHints(opts) {
35738
36676
  }
35739
36677
  if (hasBgBash) {
35740
36678
  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.`,
36679
+ `**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
36680
  "- `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
36681
  "- `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
36682
  ].join(`
@@ -35809,18 +36747,18 @@ function createVersionMismatchHandler(getPool, ensureCompatibleBinary = ensureBi
35809
36747
  const upgradePromise = (async () => {
35810
36748
  warn2(`WARNING: aft binary v${binaryVersion} is older than plugin v${minVersion}. ` + "Some features may not work. Attempting to download a compatible binary...");
35811
36749
  try {
35812
- const path3 = await ensureCompatibleBinary(`v${minVersion}`);
35813
- if (!path3) {
36750
+ const path2 = await ensureCompatibleBinary(`v${minVersion}`);
36751
+ if (!path2) {
35814
36752
  warn2(`Could not find or download v${minVersion}. Continuing with v${binaryVersion}.`);
35815
36753
  return null;
35816
36754
  }
35817
36755
  const pool = getPool();
35818
36756
  if (!pool) {
35819
- warn2(`Found/downloaded compatible binary at ${path3}, but bridge pool is not ready.`);
36757
+ warn2(`Found/downloaded compatible binary at ${path2}, but bridge pool is not ready.`);
35820
36758
  return null;
35821
36759
  }
35822
- log2(`Found/downloaded compatible binary at ${path3}. Replacing running bridges...`);
35823
- const replaced = await pool.replaceBinary(path3);
36760
+ log2(`Found/downloaded compatible binary at ${path2}. Replacing running bridges...`);
36761
+ const replaced = await pool.replaceBinary(path2);
35824
36762
  log2("Binary replaced successfully. New bridges will use the updated binary.");
35825
36763
  return replaced;
35826
36764
  } catch (err) {
@@ -35842,12 +36780,14 @@ var PLUGIN_VERSION = (() => {
35842
36780
  return "0.0.0";
35843
36781
  }
35844
36782
  })();
35845
- var ANNOUNCEMENT_VERSION = "0.46.0";
36783
+ var ANNOUNCEMENT_VERSION = "0.47.1";
35846
36784
  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."
36785
+ "Fixed: apply_patch could delete a file when a patch moved it to its own path — same-path moves are now plain in-place updates.",
36786
+ "Fixed: moving a symlink across filesystems turned it into a regular file; the link itself is now preserved.",
36787
+ "Fixed: a failed delete could leave a phantom undo entry, so the next undo reverted the wrong operation.",
36788
+ "Fixed: a crashed process could leave a stale lock that permanently disabled update checks.",
36789
+ "Paths can be given as file: URLs (#162), and reading your own background task's output files no longer prompts for permission (#161).",
36790
+ "Faster hot paths: warm symbol lookups skip re-hashing unchanged files, search reads each posting list once per query, snippets stream only the needed lines, and edits no longer copy the whole undo history per mutation."
35851
36791
  ];
35852
36792
  var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/DSa65w8wuf";
35853
36793
  var ALL_ONLY_TOOLS = new Set(["aft_callgraph", "aft_delete", "aft_move", "aft_refactor"]);
@@ -36044,6 +36984,7 @@ async function src_default(pi) {
36044
36984
  const configOverrides = buildConfigTierConfigureParams(projectRoot, {
36045
36985
  storage_dir: storageDir
36046
36986
  });
36987
+ let lspInstallCompletion = null;
36047
36988
  try {
36048
36989
  const lspAutoInstall = config2.lsp?.auto_install ?? true;
36049
36990
  const lspGraceDays = config2.lsp?.grace_days ?? 7;
@@ -36073,10 +37014,24 @@ async function src_default(pi) {
36073
37014
  if (lspInflightInstalls.length > 0) {
36074
37015
  configOverrides.lsp_inflight_installs = lspInflightInstalls;
36075
37016
  }
36076
- if (npmResult.installsStarted > 0 || ghResult.installsStarted > 0) {
37017
+ const installsWereStarted = npmResult.installsStarted > 0 || ghResult.installsStarted > 0;
37018
+ if (installsWereStarted) {
36077
37019
  log2(`[lsp] auto-install: ${npmResult.installsStarted} npm + ${ghResult.installsStarted} github install(s) running in background`);
36078
37020
  }
36079
- Promise.all([npmResult.installsComplete, ghResult.installsComplete]).then(() => {
37021
+ const installCompletion = Promise.all([npmResult.installsComplete, ghResult.installsComplete]).then(() => {
37022
+ if (installsWereStarted) {
37023
+ const updatedPaths = [
37024
+ ...new Set([...npmResult.getCachedBinDirs(), ...ghResult.getCachedBinDirs()])
37025
+ ];
37026
+ if (updatedPaths.length > 0) {
37027
+ configOverrides.lsp_paths_extra = updatedPaths;
37028
+ } else {
37029
+ delete configOverrides.lsp_paths_extra;
37030
+ }
37031
+ return updatedPaths;
37032
+ }
37033
+ return null;
37034
+ }).then((updatedPaths) => {
36080
37035
  const actionable = [...npmResult.skipped, ...ghResult.skipped].filter((s) => {
36081
37036
  const r = s.reason.toLowerCase();
36082
37037
  if (r === "auto_install: false")
@@ -36091,16 +37046,20 @@ async function src_default(pi) {
36091
37046
  return false;
36092
37047
  return true;
36093
37048
  });
36094
- if (actionable.length === 0)
36095
- return;
36096
- const lines = actionable.map((s) => ` • ${s.id}: ${s.reason}`).join(`
37049
+ if (actionable.length > 0) {
37050
+ const lines = actionable.map((s) => ` • ${s.id}: ${s.reason}`).join(`
36097
37051
  `);
36098
- warn2(`[lsp] skipped or failed to install ${actionable.length} server(s):
37052
+ warn2(`[lsp] skipped or failed to install ${actionable.length} server(s):
36099
37053
  ${lines}
36100
37054
  ` + 'Pin a working version with `lsp.versions: { "<package>": "<version>" }` if grace is blocking, ' + "or set `lsp.auto_install: false` to suppress.");
37055
+ }
37056
+ return updatedPaths;
36101
37057
  }).catch((err) => {
36102
37058
  warn2(`[lsp] install-summary aggregation failed: ${err}`);
37059
+ return null;
36103
37060
  });
37061
+ if (installsWereStarted)
37062
+ lspInstallCompletion = installCompletion;
36104
37063
  } catch (err) {
36105
37064
  warn2(`[lsp] auto-install setup failed: ${err instanceof Error ? err.message : String(err)}`);
36106
37065
  }
@@ -36171,6 +37130,17 @@ ${lines}
36171
37130
  });
36172
37131
  }
36173
37132
  });
37133
+ if (lspInstallCompletion) {
37134
+ lspInstallCompletion.then((updatedPaths) => {
37135
+ if (!updatedPaths)
37136
+ return;
37137
+ pushLspPathsAfterAutoInstall(pool, projectRoot, updatedPaths).then(() => {
37138
+ log2(`[lsp] lsp_paths_extra updated after auto-install: ${updatedPaths.length} dirs pushed to live bridges`);
37139
+ }).catch((err) => {
37140
+ warn2(`[lsp] live bridge lsp_paths_extra update failed: ${err}`);
37141
+ });
37142
+ });
37143
+ }
36174
37144
  pool.setConfigureOverride("harness", "pi");
36175
37145
  pool.setConfigureOverride("aft_search_registered", resolveToolSurface(config2).semantic);
36176
37146
  const ctx = { pool, config: config2, storageDir };
@@ -36283,7 +37253,9 @@ ${lines}
36283
37253
  }
36284
37254
  });
36285
37255
  pi.on("input", (_event, extCtx) => {
36286
- signalSyncWatchAbort(resolveSessionId(extCtx));
37256
+ const sessionId = resolveSessionId(extCtx);
37257
+ signalSyncWatchAbort(sessionId);
37258
+ signalBashWaitDetachForProject(pool, extCtx.cwd, sessionId);
36287
37259
  });
36288
37260
  const unregisterShutdownCleanup = registerShutdownCleanup(async () => {
36289
37261
  try {