@cortexkit/aft-pi 0.58.0 → 0.58.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7935,25 +7935,79 @@ function error(message, meta) {
7935
7935
  function isTerminalStatus(status) {
7936
7936
  return status === "completed" || status === "failed" || status === "killed" || status === "timed_out" || status === "fate_unknown";
7937
7937
  }
7938
- function sleep(ms) {
7939
- return new Promise((resolve) => setTimeout(resolve, ms));
7940
- }
7941
7938
  // ../aft-bridge/dist/bash-hints.js
7942
7939
  import * as fs from "node:fs";
7943
7940
  import * as os from "node:os";
7944
7941
  import * as path from "node:path";
7945
7942
  var DEFAULT_PRIMARY_WATCH_TIMEOUT_MS = 30000;
7946
- var WATCH_TIMEOUT_PARAM_DESCRIPTION = "Sync-only timeout in milliseconds. Default 30000 (the configured maximum for delegated sessions); max `bash.watch_sync_max_ms` (120000 by default).";
7943
+ var MAX_WATCH_TIMEOUT_MS = 1800000;
7944
+ var LONGEST_TIMER_DELAY_MS = 2147483647;
7945
+ var WATCH_SYNC_DEFAULTS_DESCRIPTION = "Sync waits default to 30s in a main session (max `bash.watch_sync_max_ms`, 120s by default); in a delegated session a sync wait without a timeout waits until the command finishes";
7946
+ var WATCH_TIMEOUT_PARAM_DESCRIPTION = "Sync-only timeout in milliseconds. In a main session: default 30000, max `bash.watch_sync_max_ms` (120000 by default). In a delegated session: omit it to wait until the command finishes; a value you pass is used as given.";
7947
7947
  function resolveWatchTimeoutMs(requestedMs, role, capMs) {
7948
- const fallback = role === "worker" ? capMs : DEFAULT_PRIMARY_WATCH_TIMEOUT_MS;
7949
- return Math.min(requestedMs ?? fallback, capMs);
7948
+ if (role === "worker")
7949
+ return requestedMs;
7950
+ return Math.min(requestedMs ?? DEFAULT_PRIMARY_WATCH_TIMEOUT_MS, capMs);
7951
+ }
7952
+ function maxWatchTimeoutMs(role, capMs) {
7953
+ return role === "worker" ? MAX_WATCH_TIMEOUT_MS : capMs;
7954
+ }
7955
+ function watchPollDelayMs(elapsedMs) {
7956
+ if (elapsedMs < 5000)
7957
+ return 100;
7958
+ if (elapsedMs < 30000)
7959
+ return 250;
7960
+ if (elapsedMs < 120000)
7961
+ return 500;
7962
+ return 1000;
7963
+ }
7964
+ var watchClock = {
7965
+ now: () => performance.now(),
7966
+ sleep: abortableSleep
7967
+ };
7968
+ function abortableSleep(ms, signal) {
7969
+ if (signal?.aborted)
7970
+ return Promise.resolve();
7971
+ return new Promise((resolve) => {
7972
+ const done = () => {
7973
+ clearTimeout(timer);
7974
+ signal?.removeEventListener("abort", done);
7975
+ resolve();
7976
+ };
7977
+ const timer = setTimeout(done, ms);
7978
+ signal?.addEventListener("abort", done, { once: true });
7979
+ });
7950
7980
  }
7951
- function watchTimeoutSteer(role, capMs, timeoutParam = "timeoutMs") {
7981
+ function formatWatchWaited(elapsedMs, limitMs, capMs) {
7982
+ let limit;
7983
+ if (limitMs === undefined)
7984
+ limit = "no limit: waits until the command finishes";
7985
+ else if (capMs !== undefined && limitMs >= capMs)
7986
+ limit = `limit ${limitMs}ms, the bash.watch_sync_max_ms cap`;
7987
+ else
7988
+ limit = `limit ${limitMs}ms`;
7989
+ return `Waited ${Math.round(elapsedMs)}ms (${limit})`;
7990
+ }
7991
+ function watchTimeoutSteer(role, timeoutParam = "timeoutMs") {
7952
7992
  if (role === "worker") {
7953
- return `The command is still running; this is not a failure. Watch again (${timeoutParam} up to ${capMs}) and don't report a result until it finishes.`;
7993
+ return `The command is still running; this is not a failure. Watch again and don't report a result until it finishes; a bash_watch without ${timeoutParam} waits until the command finishes.`;
7954
7994
  }
7955
7995
  return "The command is still running; this is not a failure. Watch again, do other work, or end your turn: the completion reminder wakes you.";
7956
7996
  }
7997
+ var WATCH_UNAVAILABLE_GIVE_UP_MS = 120000;
7998
+ var WORKER_KEEP_WAITING = "Call bash_watch again to keep waiting; don't report a result until the command finishes.";
7999
+ function interruptedWatchTail(role, primaryTail) {
8000
+ return role === "worker" ? `The task is still running. ${WORKER_KEEP_WAITING}` : primaryTail;
8001
+ }
8002
+ function runningTaskStatusHint(role) {
8003
+ return role === "worker" ? "To wait for it, call bash_watch; don't poll." : "A completion reminder will be delivered automatically; don't poll.";
8004
+ }
8005
+ function watchUnavailableSteer(role) {
8006
+ const unknown = "the bridge was busy, so task state is unknown.";
8007
+ if (role === "worker")
8008
+ return `${unknown} ${WORKER_KEEP_WAITING}`;
8009
+ return `${unknown} Do not poll; let the task's completion notification wake the session, or use one bash_status snapshot on the next normal tool call.`;
8010
+ }
7957
8011
  var CONFLICT_HINT = `
7958
8012
 
7959
8013
  [Hint] Use aft_conflicts to see all conflict regions across files in a single call.`;
@@ -8334,7 +8388,7 @@ import { existsSync } from "node:fs";
8334
8388
  import { join as join3 } from "node:path";
8335
8389
 
8336
8390
  // ../aft-bridge/dist/child-process.js
8337
- import { execFileSync as nodeExecFileSync, execSync as nodeExecSync, spawn as nodeSpawn, spawnSync as nodeSpawnSync } from "node:child_process";
8391
+ import { execFile as nodeExecFile, execFileSync as nodeExecFileSync, execSync as nodeExecSync, spawn as nodeSpawn, spawnSync as nodeSpawnSync } from "node:child_process";
8338
8392
  var HIDDEN = { windowsHide: true };
8339
8393
  function withWindowsHidden(args, takesArgv) {
8340
8394
  const out = [...args];
@@ -8356,6 +8410,7 @@ function hidden(resolve, takesArgv) {
8356
8410
  }
8357
8411
  var spawn = hidden(() => nodeSpawn, true);
8358
8412
  var spawnSync = hidden(() => nodeSpawnSync, true);
8413
+ var execFile = hidden(() => nodeExecFile, true);
8359
8414
  var execFileSync = hidden(() => nodeExecFileSync, true);
8360
8415
  var execSync = hidden(() => nodeExecSync, false);
8361
8416
 
@@ -8811,6 +8866,9 @@ function isPassiveCommand(command) {
8811
8866
  return PASSIVE_COMMANDS.has(command);
8812
8867
  }
8813
8868
 
8869
+ // ../aft-bridge/dist/transport.js
8870
+ var WORKER_SESSION_FIELD = "worker_session";
8871
+
8814
8872
  // ../aft-bridge/dist/bridge.js
8815
8873
  var DEFAULT_BRIDGE_TIMEOUT_MS = 30000;
8816
8874
  var BRIDGE_HANG_TIMEOUT_THRESHOLD = 2;
@@ -9229,9 +9287,11 @@ class BinaryBridge {
9229
9287
  params.edit_slot_survives = editSlotSurvives;
9230
9288
  this.logHashlineRegistrationCarrier("tool_call", sessionId, editSlotSurvives);
9231
9289
  }
9232
- const { preview, ...sendOptions } = options ?? {};
9290
+ const { preview, workerSession, ...sendOptions } = options ?? {};
9233
9291
  if (preview === true)
9234
9292
  params.preview = true;
9293
+ if (workerSession === true)
9294
+ params[WORKER_SESSION_FIELD] = true;
9235
9295
  return await this.send("tool_call", params, Object.keys(sendOptions).length > 0 ? sendOptions : undefined);
9236
9296
  }
9237
9297
  async sendWithVersionMismatchRetry(command, params, options, canRetryAfterVersionSwap) {
@@ -9300,7 +9360,7 @@ class BinaryBridge {
9300
9360
  if (command === "cancel_request" || Object.hasOwn(params, "command") || Object.hasOwn(params, "method")) {
9301
9361
  const nested = { ...params };
9302
9362
  const reserved = {};
9303
- for (const key of ["session_id", "lsp_hints"]) {
9363
+ for (const key of ["session_id", "lsp_hints", WORKER_SESSION_FIELD]) {
9304
9364
  if (Object.hasOwn(nested, key)) {
9305
9365
  reserved[key] = nested[key];
9306
9366
  delete nested[key];
@@ -9682,17 +9742,30 @@ class BinaryBridge {
9682
9742
  }
9683
9743
  }
9684
9744
  onStderrData(data) {
9685
- this.stderrBuffer += data;
9686
- let newlineIdx;
9687
- while ((newlineIdx = this.stderrBuffer.indexOf(`
9688
- `)) !== -1) {
9689
- const line = this.stderrBuffer.slice(0, newlineIdx).replace(/\r$/, "");
9690
- this.stderrBuffer = this.stderrBuffer.slice(newlineIdx + 1);
9691
- if (!line || !shouldSurfaceStderrLine(line))
9692
- continue;
9693
- const tagged = tagStderrLine(line);
9694
- this.logVia(tagged);
9695
- this.pushStderrLine(tagged);
9745
+ const partialLimit = 64 * 1024;
9746
+ let offset = 0;
9747
+ while (offset < data.length) {
9748
+ const newline = data.indexOf(`
9749
+ `, offset);
9750
+ const end = newline < 0 ? data.length : newline;
9751
+ while (offset < end) {
9752
+ const count = Math.min(partialLimit - this.stderrBuffer.length, end - offset);
9753
+ this.stderrBuffer += data.slice(offset, offset + count);
9754
+ offset += count;
9755
+ if (this.stderrBuffer.length === partialLimit) {
9756
+ const last = this.stderrBuffer.charCodeAt(this.stderrBuffer.length - 1);
9757
+ const carry = last >= 55296 && last <= 56319 ? this.stderrBuffer.slice(-1) : "";
9758
+ const fragment = carry ? this.stderrBuffer.slice(0, -1) : this.stderrBuffer;
9759
+ const tagged = tagStderrLine(`${fragment} [stderr record truncated into bounded fragments; ${Buffer.byteLength(fragment)} bytes shown, continuation follows]`);
9760
+ this.stderrBuffer = carry;
9761
+ this.logVia(tagged);
9762
+ this.pushStderrLine(tagged);
9763
+ }
9764
+ }
9765
+ if (newline < 0)
9766
+ break;
9767
+ this.flushStderrBuffer();
9768
+ offset = newline + 1;
9696
9769
  }
9697
9770
  }
9698
9771
  flushStderrBuffer() {
@@ -10070,7 +10143,8 @@ function renderCallersGroupLines(group, theme) {
10070
10143
  const bySymbolProvenance = new Map;
10071
10144
  for (const caller of callers) {
10072
10145
  const symbol = asString(caller.symbol) ?? "(unknown)";
10073
- const provenanceKey = asString(caller.resolved_by) === "name_match" ? `${symbol}\x00name_match` : `${symbol}\x00exact`;
10146
+ const via = asString(caller.via) ?? "";
10147
+ const provenanceKey = asString(caller.resolved_by) === "name_match" ? `${symbol}\x00${via}\x00name_match` : `${symbol}\x00${via}\x00exact`;
10074
10148
  const line = asNumber(caller.line);
10075
10149
  const bucket = bySymbolProvenance.get(provenanceKey) ?? [];
10076
10150
  if (line !== undefined)
@@ -10079,12 +10153,13 @@ function renderCallersGroupLines(group, theme) {
10079
10153
  }
10080
10154
  const keys = [...bySymbolProvenance.keys()].sort((a, b) => a.localeCompare(b));
10081
10155
  for (const key of keys) {
10082
- const symbol = key.split("\x00")[0] ?? "(unknown)";
10156
+ const [symbol = "(unknown)", via = ""] = key.split("\x00");
10083
10157
  const isNameMatch = key.endsWith("\x00name_match");
10084
10158
  const lineNums = (bySymbolProvenance.get(key) ?? []).sort((a, b) => a - b);
10085
10159
  const linePart = lineNums.length > 0 ? lineNums.map(String).join(", ") : "?";
10086
10160
  const marker = isNameMatch ? ` ${theme.fg("warning", "~")}` : "";
10087
- lines.push(` ↳ ${symbol}:${linePart}${marker}`);
10161
+ const viaPart = via ? ` via ${via}` : "";
10162
+ lines.push(` ↳ ${symbol}:${linePart}${marker}${viaPart}`);
10088
10163
  }
10089
10164
  return lines;
10090
10165
  }
@@ -10118,6 +10193,9 @@ function formatCallgraphSections(op, response, theme = PLAIN_CALLGRAPH_THEME, op
10118
10193
  sections.push(renderCallersGroupLines(group, theme).join(`
10119
10194
  `));
10120
10195
  });
10196
+ const macroNote = asString(asRecord(record.macro_note)?.message);
10197
+ if (macroNote)
10198
+ sections.push(theme.fg("warning", macroNote));
10121
10199
  return sections;
10122
10200
  }
10123
10201
  if (op === "trace_to_symbol") {
@@ -10331,6 +10409,35 @@ function coerceJsonCollectionParam(value, field) {
10331
10409
  }
10332
10410
  throw new Error(`${field} must be ${shape} (got an invalid type)`);
10333
10411
  }
10412
+ function isBlankParam(value) {
10413
+ if (typeof value === "string")
10414
+ return value.trim().length === 0;
10415
+ return isEmptyParam(value);
10416
+ }
10417
+ function usableZoomTargets(targets) {
10418
+ if (isEmptyParam(targets))
10419
+ return [];
10420
+ const entries = Array.isArray(targets) ? targets : [targets];
10421
+ return entries.filter((entry) => {
10422
+ if (entry === null || entry === undefined)
10423
+ return false;
10424
+ if (typeof entry !== "object")
10425
+ return true;
10426
+ const record = entry;
10427
+ return !(isBlankParam(record.path) && isBlankParam(record.filePath) && isBlankParam(record.symbol));
10428
+ });
10429
+ }
10430
+ function isFindReplaceOnlyEdit(args) {
10431
+ const absent = (value) => value === undefined || value === null || value === "";
10432
+ const absentLine = (value) => absent(value) || value === 0;
10433
+ const edits = args.edits;
10434
+ return Array.isArray(edits) && edits.length > 0 && absent(args.appendContent) && absent(args.symbol) && absent(args.content) && edits.every((entry) => {
10435
+ if (entry === null || typeof entry !== "object")
10436
+ return false;
10437
+ const item = entry;
10438
+ return typeof item.oldString === "string" && (absent(item.newString) || typeof item.newString === "string") && absentLine(item.startLine) && absentLine(item.endLine) && absent(item.content);
10439
+ });
10440
+ }
10334
10441
  // ../aft-bridge/dist/config-error.js
10335
10442
  import { homedir as homedir6 } from "node:os";
10336
10443
  import { isAbsolute as isAbsolute2, join as join6 } from "node:path";
@@ -10438,9 +10545,366 @@ function formatDroppedKeyWarnings(dropped) {
10438
10545
  }
10439
10546
  return dropped.map((item) => `Ignoring ${item.key} from ${item.tier} config (${item.reason})`);
10440
10547
  }
10548
+ // ../aft-bridge/dist/config-watch.js
10549
+ import { readFileSync as readFileSync3, statSync as statSync3, watch } from "node:fs";
10550
+ import { basename, dirname } from "node:path";
10551
+ var CONFIG_WATCH_DEBOUNCE_MS = 150;
10552
+ var CONFIG_LIVE_KEEP_NOTE = "AFT keeps using the last valid configuration until the file is fixed.";
10553
+ function pathKey(name) {
10554
+ const segments = name.split(".");
10555
+ return {
10556
+ name,
10557
+ read(config) {
10558
+ let value = config;
10559
+ for (const segment of segments) {
10560
+ if (value === null || typeof value !== "object")
10561
+ return;
10562
+ value = value[segment];
10563
+ }
10564
+ return value;
10565
+ },
10566
+ write(config, value) {
10567
+ const set = (target, index) => {
10568
+ const copy = target !== null && typeof target === "object" && !Array.isArray(target) ? { ...target } : {};
10569
+ const segment = segments[index];
10570
+ if (index === segments.length - 1) {
10571
+ if (value === undefined)
10572
+ delete copy[segment];
10573
+ else
10574
+ copy[segment] = value;
10575
+ } else {
10576
+ copy[segment] = set(copy[segment], index + 1);
10577
+ }
10578
+ return copy;
10579
+ };
10580
+ return set(config, 0);
10581
+ }
10582
+ };
10583
+ }
10584
+ function bashKey(name, resolveBash) {
10585
+ return {
10586
+ name: `bash.${name}`,
10587
+ read: (config) => resolveBash(config)[name],
10588
+ write(config, value) {
10589
+ const resolved = { ...resolveBash(config), [name]: value };
10590
+ for (const key of Object.keys(resolved)) {
10591
+ if (resolved[key] === undefined)
10592
+ delete resolved[key];
10593
+ }
10594
+ return { ...config, bash: resolved };
10595
+ }
10596
+ };
10597
+ }
10598
+ function aftLiveConfigKeys(resolveBash) {
10599
+ return [
10600
+ pathKey("configure_warnings_delivery"),
10601
+ pathKey("restrict_to_project_root"),
10602
+ pathKey("inspect.diagnostics_timeout_ms"),
10603
+ pathKey("inspect.tier2_idle_minutes"),
10604
+ bashKey("foreground_wait_window_ms", resolveBash),
10605
+ bashKey("host_fallback", resolveBash),
10606
+ bashKey("subagent_background", resolveBash),
10607
+ bashKey("watch_sync_max_ms", resolveBash)
10608
+ ];
10609
+ }
10610
+ function sameValue(a, b) {
10611
+ return JSON.stringify(a) === JSON.stringify(b);
10612
+ }
10613
+ function flattenLeaves(value, prefix, out) {
10614
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
10615
+ const entries = Object.entries(value);
10616
+ if (entries.length === 0 && prefix)
10617
+ out.set(prefix, "{}");
10618
+ for (const [key, child] of entries) {
10619
+ flattenLeaves(child, prefix ? `${prefix}.${key}` : key, out);
10620
+ }
10621
+ return;
10622
+ }
10623
+ if (prefix)
10624
+ out.set(prefix, JSON.stringify(value));
10625
+ }
10626
+ function applyLiveConfigKeys(current, next, keys, baseline = current) {
10627
+ let config = current;
10628
+ const applied = [];
10629
+ for (const key of keys) {
10630
+ const value = key.read(next);
10631
+ if (sameValue(key.read(current), value))
10632
+ continue;
10633
+ config = key.write(config, value);
10634
+ applied.push(key.name);
10635
+ }
10636
+ const before = new Map;
10637
+ const after = new Map;
10638
+ flattenLeaves(baseline, "", before);
10639
+ flattenLeaves(next, "", after);
10640
+ const liveNames = keys.map((key) => key.name);
10641
+ const isLive = (leaf) => liveNames.some((name) => leaf === name || leaf.startsWith(`${name}.`));
10642
+ const deferred = new Set;
10643
+ for (const leaf of new Set([...before.keys(), ...after.keys()])) {
10644
+ if (before.get(leaf) === after.get(leaf) || isLive(leaf))
10645
+ continue;
10646
+ deferred.add(leaf);
10647
+ }
10648
+ return { config, applied, deferred: [...deferred].sort() };
10649
+ }
10650
+ function readTextOrNull(path) {
10651
+ try {
10652
+ return readFileSync3(path, "utf8");
10653
+ } catch {
10654
+ return null;
10655
+ }
10656
+ }
10657
+ var CONFIG_WATCH_REVALIDATE_MS = 2000;
10658
+ var CONFIG_WATCH_REJECTED_RETRIES = 3;
10659
+ var CONFIG_WATCH_MAX_DELAY_MS = 1000;
10660
+ function identityOf(dir) {
10661
+ try {
10662
+ const stat = statSync3(dir, { bigint: true });
10663
+ const base = `${stat.dev}:${stat.ino}`;
10664
+ return process.platform === "win32" ? base : `${base}:${stat.birthtimeNs}`;
10665
+ } catch {
10666
+ return null;
10667
+ }
10668
+ }
10669
+ function watchDirFor(file) {
10670
+ let dir = dirname(file);
10671
+ while (dir !== dirname(dir) && identityOf(dir) === null)
10672
+ dir = dirname(dir);
10673
+ return dir;
10674
+ }
10675
+ function watchAftConfigFiles(options) {
10676
+ const debounceMs = options.debounceMs ?? CONFIG_WATCH_DEBOUNCE_MS;
10677
+ const lastSeen = new Map;
10678
+ for (const path of options.paths) {
10679
+ if (options.initialTexts)
10680
+ lastSeen.set(path, options.initialTexts[path] ?? null);
10681
+ else
10682
+ lastSeen.set(path, options.checkAtStart ? undefined : readTextOrNull(path));
10683
+ }
10684
+ let timer = null;
10685
+ let firstPendingAt = null;
10686
+ let stopped = false;
10687
+ const watchers = new Map;
10688
+ const watchImpl = options.watchImpl ?? watch;
10689
+ let rejectedRetries = 0;
10690
+ const check = () => {
10691
+ timer = null;
10692
+ firstPendingAt = null;
10693
+ if (stopped)
10694
+ return;
10695
+ revalidate();
10696
+ const seen = new Map;
10697
+ for (const path of options.paths) {
10698
+ const text = readTextOrNull(path);
10699
+ if (text !== lastSeen.get(path))
10700
+ seen.set(path, text);
10701
+ }
10702
+ if (seen.size === 0)
10703
+ return;
10704
+ const outcome = options.onChange();
10705
+ if (outcome === false) {
10706
+ if (rejectedRetries < CONFIG_WATCH_REJECTED_RETRIES) {
10707
+ rejectedRetries += 1;
10708
+ retryTimer = setTimeout(check, CONFIG_WATCH_MAX_DELAY_MS);
10709
+ retryTimer.unref?.();
10710
+ }
10711
+ return;
10712
+ }
10713
+ rejectedRetries = 0;
10714
+ if (outcome instanceof Map) {
10715
+ for (const path of options.paths)
10716
+ lastSeen.set(path, outcome.get(path) ?? null);
10717
+ } else {
10718
+ for (const [path, text] of seen)
10719
+ lastSeen.set(path, text);
10720
+ }
10721
+ };
10722
+ let retryTimer = null;
10723
+ const schedule = () => {
10724
+ if (stopped)
10725
+ return;
10726
+ const now = Date.now();
10727
+ firstPendingAt ??= now;
10728
+ if (timer)
10729
+ clearTimeout(timer);
10730
+ const delay = Math.max(0, Math.min(debounceMs, firstPendingAt + CONFIG_WATCH_MAX_DELAY_MS - now));
10731
+ timer = setTimeout(check, delay);
10732
+ timer.unref?.();
10733
+ };
10734
+ function revalidate() {
10735
+ const wanted = new Set(options.paths.map(watchDirFor));
10736
+ let moved = false;
10737
+ for (const [dir, entry] of watchers) {
10738
+ if (!wanted.has(dir) || entry.stale || identityOf(dir) !== entry.identity) {
10739
+ entry.watcher.close();
10740
+ watchers.delete(dir);
10741
+ moved = true;
10742
+ }
10743
+ }
10744
+ for (const dir of wanted) {
10745
+ if (watchers.has(dir))
10746
+ continue;
10747
+ try {
10748
+ const identity = identityOf(dir);
10749
+ options.beforeWatchForTest?.(dir);
10750
+ const name = basename(dir);
10751
+ const watcher = watchImpl(dir, { persistent: false }, (event, filename) => {
10752
+ if (event === "rename" && filename === name) {
10753
+ const current = watchers.get(dir);
10754
+ if (current && current.watcher === watcher)
10755
+ current.stale = true;
10756
+ }
10757
+ schedule();
10758
+ });
10759
+ if (identityOf(dir) !== identity) {
10760
+ watcher.close();
10761
+ moved = true;
10762
+ continue;
10763
+ }
10764
+ const entry = { watcher, identity, stale: false };
10765
+ entry.watcher.on("error", () => {
10766
+ entry.watcher.close();
10767
+ if (watchers.get(dir) === entry)
10768
+ watchers.delete(dir);
10769
+ schedule();
10770
+ });
10771
+ watchers.set(dir, entry);
10772
+ moved = true;
10773
+ } catch {}
10774
+ }
10775
+ return moved;
10776
+ }
10777
+ revalidate();
10778
+ if (options.checkAtStart)
10779
+ check();
10780
+ const revalidateTimer = setInterval(() => {
10781
+ if (!stopped && revalidate())
10782
+ schedule();
10783
+ }, CONFIG_WATCH_REVALIDATE_MS);
10784
+ revalidateTimer.unref?.();
10785
+ return () => {
10786
+ stopped = true;
10787
+ if (timer)
10788
+ clearTimeout(timer);
10789
+ if (retryTimer)
10790
+ clearTimeout(retryTimer);
10791
+ clearInterval(revalidateTimer);
10792
+ for (const entry of watchers.values())
10793
+ entry.watcher.close();
10794
+ watchers.clear();
10795
+ };
10796
+ }
10797
+ function aftLiveSecurityKeys(keys) {
10798
+ const byName = new Map(keys.map((key) => [key.name, key]));
10799
+ const pick = (name) => {
10800
+ const key = byName.get(name);
10801
+ if (!key)
10802
+ throw new Error(`unknown live config key ${name}`);
10803
+ return key;
10804
+ };
10805
+ return [
10806
+ {
10807
+ key: pick("restrict_to_project_root"),
10808
+ loosens: (current, next) => current === true && next !== true
10809
+ },
10810
+ {
10811
+ key: pick("bash.host_fallback"),
10812
+ loosens: (current, next) => current !== true && next === true
10813
+ }
10814
+ ];
10815
+ }
10816
+ function liveConfigReloadLogLine(applied, deferred) {
10817
+ let line = "config reload";
10818
+ if (applied.length > 0)
10819
+ line += ` applied=[${applied.join(",")}]`;
10820
+ if (deferred.length > 0) {
10821
+ line += ` deferred=[${deferred.join(",")}] (deferred keys apply on next connect/restart)`;
10822
+ }
10823
+ return line;
10824
+ }
10825
+ function startLiveConfigReload(options) {
10826
+ const baseline = options.getConfig();
10827
+ let lastError = null;
10828
+ let acceptedSources = new Set(options.initialSources);
10829
+ let acceptedProjectText = options.initialProjectText ?? null;
10830
+ let acceptedTexts;
10831
+ const reload = () => {
10832
+ let loaded = options.load();
10833
+ if (loaded.ok) {
10834
+ const read = new Set(loaded.sources);
10835
+ const deleted = [...acceptedSources].find((path) => !read.has(path));
10836
+ if (deleted !== undefined) {
10837
+ loaded = { ok: false, message: `AFT config at ${deleted} was deleted` };
10838
+ } else {
10839
+ acceptedSources = read;
10840
+ }
10841
+ }
10842
+ if (!loaded.ok) {
10843
+ const message = `${loaded.message.trim().replace(/[.!?]?$/, ".")} ${CONFIG_LIVE_KEEP_NOTE}`;
10844
+ if (message !== lastError) {
10845
+ lastError = message;
10846
+ options.reportError(message);
10847
+ }
10848
+ return null;
10849
+ }
10850
+ lastError = null;
10851
+ acceptedTexts = loaded.texts;
10852
+ const current = options.getConfig();
10853
+ let next = loaded.config;
10854
+ const held = [];
10855
+ const projectText = loaded.projectText ?? null;
10856
+ if (loaded.projectText !== undefined && projectText !== acceptedProjectText) {
10857
+ for (const security of options.securityKeys ?? []) {
10858
+ const now = security.key.read(current);
10859
+ if (security.loosens(now, security.key.read(next))) {
10860
+ next = security.key.write(next, now);
10861
+ held.push(security.key.name);
10862
+ }
10863
+ }
10864
+ if (held.length === 0)
10865
+ acceptedProjectText = projectText;
10866
+ }
10867
+ const result = applyLiveConfigKeys(current, next, options.keys, baseline);
10868
+ if (result.applied.length > 0)
10869
+ options.setConfig(result.config);
10870
+ if (result.applied.length > 0 || result.deferred.length > 0 || held.length > 0) {
10871
+ let line = liveConfigReloadLogLine(result.applied, result.deferred);
10872
+ if (held.length > 0) {
10873
+ line += ` held=[${held.join(",")}] (not loosened while the project file differs from the one the published values came from; the next restart applies them)`;
10874
+ }
10875
+ options.log(line);
10876
+ }
10877
+ return { ...result, held };
10878
+ };
10879
+ const safeReload = () => {
10880
+ try {
10881
+ acceptedTexts = undefined;
10882
+ if (reload() === null)
10883
+ return false;
10884
+ const texts = acceptedTexts;
10885
+ if (!texts)
10886
+ return true;
10887
+ return new Map(options.paths.map((path) => [path, texts[path] ?? null]));
10888
+ } catch (err) {
10889
+ options.reportError(`AFT config reload failed: ${err instanceof Error ? err.message : String(err)}`);
10890
+ return false;
10891
+ }
10892
+ };
10893
+ if (options.watch === false) {
10894
+ return { reload, stop: () => {} };
10895
+ }
10896
+ const stop = watchAftConfigFiles({
10897
+ paths: options.paths,
10898
+ debounceMs: options.debounceMs,
10899
+ onChange: safeReload,
10900
+ initialTexts: options.initialTexts,
10901
+ checkAtStart: true
10902
+ });
10903
+ return { reload, stop };
10904
+ }
10441
10905
  // ../aft-bridge/dist/downloader.js
10442
10906
  import { createHash as createHash2, randomUUID } from "node:crypto";
10443
- import { chmodSync, closeSync, copyFileSync, createWriteStream, existsSync as existsSync3, mkdirSync, openSync, readdirSync, readFileSync as readFileSync3, renameSync as renameSync2, rmSync, statSync as statSync3, unlinkSync as unlinkSync2, writeSync } from "node:fs";
10907
+ import { chmodSync, closeSync, copyFileSync, createWriteStream, existsSync as existsSync3, mkdirSync, openSync, readdirSync, readFileSync as readFileSync4, renameSync as renameSync2, rmSync, statSync as statSync4, unlinkSync as unlinkSync2, writeSync } from "node:fs";
10444
10908
  import { hostname } from "node:os";
10445
10909
  import { join as join7 } from "node:path";
10446
10910
  import { Readable } from "node:stream";
@@ -10792,7 +11256,7 @@ function isReclaimableDownloadLock(owner, ageMs, staleMs) {
10792
11256
  }
10793
11257
  function reclaimDownloadLock(lockPath, expectedOwner) {
10794
11258
  try {
10795
- if (readFileSync3(lockPath, "utf-8") !== expectedOwner)
11259
+ if (readFileSync4(lockPath, "utf-8") !== expectedOwner)
10796
11260
  return false;
10797
11261
  rmSync(lockPath, { force: true });
10798
11262
  return true;
@@ -10810,7 +11274,7 @@ function sweepStaleDownloadTemps(versionedCacheDir, binaryName) {
10810
11274
  continue;
10811
11275
  }
10812
11276
  const tempPath = join7(versionedCacheDir, entry.name);
10813
- const ageMs = Date.now() - statSync3(tempPath).mtimeMs;
11277
+ const ageMs = Date.now() - statSync4(tempPath).mtimeMs;
10814
11278
  if (Math.abs(ageMs) > DOWNLOAD_LOCK_STALE_MS)
10815
11279
  unlinkSync2(tempPath);
10816
11280
  }
@@ -10832,7 +11296,7 @@ async function acquireDownloadLock(lockPath, timing = {}) {
10832
11296
  }
10833
11297
  return () => {
10834
11298
  try {
10835
- if (readFileSync3(lockPath, "utf-8") === owner) {
11299
+ if (readFileSync4(lockPath, "utf-8") === owner) {
10836
11300
  rmSync(lockPath, { force: true });
10837
11301
  }
10838
11302
  } catch {}
@@ -10844,8 +11308,8 @@ async function acquireDownloadLock(lockPath, timing = {}) {
10844
11308
  let existingOwner;
10845
11309
  let ageMs;
10846
11310
  try {
10847
- existingOwner = readFileSync3(lockPath, "utf-8");
10848
- ageMs = Date.now() - statSync3(lockPath).mtimeMs;
11311
+ existingOwner = readFileSync4(lockPath, "utf-8");
11312
+ ageMs = Date.now() - statSync4(lockPath).mtimeMs;
10849
11313
  } catch (readErr) {
10850
11314
  if (readErr.code === "ENOENT")
10851
11315
  continue;
@@ -10895,7 +11359,7 @@ async function fetchLatestTag() {
10895
11359
  }
10896
11360
  // ../aft-bridge/dist/durable-log.js
10897
11361
  import { appendFile, chmod, mkdir, rename, rm, stat } from "node:fs/promises";
10898
- import { dirname } from "node:path";
11362
+ import { dirname as dirname2 } from "node:path";
10899
11363
  var DEFAULT_LOG_BYTES = 32 * 1024 * 1024;
10900
11364
  var DEFAULT_LOG_GENERATIONS = 1;
10901
11365
  var LOG_DIR_MODE = 448;
@@ -10908,6 +11372,10 @@ class RotatingLogSink {
10908
11372
  queue = Promise.resolve();
10909
11373
  disabled = false;
10910
11374
  failureReported = false;
11375
+ queuedBytes = 0;
11376
+ queuedRecords = 0;
11377
+ droppedBytes = 0;
11378
+ overflowQueued = false;
10911
11379
  constructor(path, options = {}) {
10912
11380
  this.path = path;
10913
11381
  this.maxBytes = options.maxBytes ?? DEFAULT_LOG_BYTES;
@@ -10916,7 +11384,34 @@ class RotatingLogSink {
10916
11384
  append(data) {
10917
11385
  if (this.disabled || data.length === 0)
10918
11386
  return;
10919
- this.queue = this.queue.then(() => this.write(data)).catch((error) => {
11387
+ const bytes = Buffer.byteLength(data);
11388
+ const overflow = this.queuedBytes + bytes > 1024 * 1024 || this.queuedRecords >= 4096;
11389
+ if (overflow) {
11390
+ this.droppedBytes += bytes;
11391
+ if (this.overflowQueued)
11392
+ return;
11393
+ this.overflowQueued = true;
11394
+ data = "";
11395
+ } else {
11396
+ this.queuedBytes += bytes;
11397
+ this.queuedRecords += 1;
11398
+ }
11399
+ this.queue = this.queue.then(async () => {
11400
+ if (overflow) {
11401
+ const dropped = this.droppedBytes;
11402
+ this.droppedBytes = 0;
11403
+ this.overflowQueued = false;
11404
+ await this.write(`[aft-plugin] durable log queue overflow: dropped ${dropped} bytes (1MiB/4096-record pending limit); reduce diagnostic volume.
11405
+ `);
11406
+ } else {
11407
+ try {
11408
+ await this.write(data);
11409
+ } finally {
11410
+ this.queuedBytes -= bytes;
11411
+ this.queuedRecords -= 1;
11412
+ }
11413
+ }
11414
+ }).catch((error) => {
10920
11415
  this.disabled = true;
10921
11416
  if (!this.failureReported) {
10922
11417
  this.failureReported = true;
@@ -10931,8 +11426,8 @@ class RotatingLogSink {
10931
11426
  await this.queue;
10932
11427
  }
10933
11428
  async write(data) {
10934
- const dir = dirname(this.path);
10935
- await mkdir(dirname(dir), { recursive: true });
11429
+ const dir = dirname2(this.path);
11430
+ await mkdir(dirname2(dir), { recursive: true });
10936
11431
  try {
10937
11432
  await mkdir(dir, { mode: LOG_DIR_MODE });
10938
11433
  } catch (error) {
@@ -11038,6 +11533,15 @@ function formatEditSummary(data) {
11038
11533
  let s = `Edited (${detail}).`;
11039
11534
  if (data.formatted)
11040
11535
  s += formatAutoFormattedSuffix(data);
11536
+ const fuzzy = data.fuzzy_matches ?? (data.fuzzy_match ? [data.fuzzy_match] : []);
11537
+ for (const match of fuzzy) {
11538
+ s += `
11539
+ Fuzzy match (pass ${match.pass}).`;
11540
+ for (const [index, text] of match.replaced_text.entries()) {
11541
+ s += `
11542
+ Replaced text ${index + 1}: ${text}`;
11543
+ }
11544
+ }
11041
11545
  return s;
11042
11546
  }
11043
11547
  function formatAutoFormattedSuffix(data) {
@@ -12766,7 +13270,7 @@ function reverseRequestMethod(body) {
12766
13270
  }
12767
13271
  }
12768
13272
  // ../aft-bridge/dist/subc-transport.js
12769
- import { existsSync as existsSync4, statSync as statSync4 } from "node:fs";
13273
+ import { existsSync as existsSync4, statSync as statSync5 } from "node:fs";
12770
13274
 
12771
13275
  // ../aft-bridge/dist/lifecycle-registry.js
12772
13276
  import { lstat } from "node:fs/promises";
@@ -13238,6 +13742,13 @@ class SubcTransportShuttingDownError extends SubcCallError {
13238
13742
  this.name = "SubcTransportShuttingDownError";
13239
13743
  }
13240
13744
  }
13745
+
13746
+ class ReconnectKeyChangedError extends Error {
13747
+ constructor() {
13748
+ super("the subc daemon restarted and its connection key changed; the reconnect did not complete within the deadline, retry");
13749
+ this.name = "ReconnectKeyChangedError";
13750
+ }
13751
+ }
13241
13752
  function isSubcClientClosedError(error) {
13242
13753
  return error instanceof SubcError && error.message === "client closed";
13243
13754
  }
@@ -13289,6 +13800,7 @@ var RECONNECT_RETRY_FLOOR_MS = 100;
13289
13800
  var RECONNECT_RETRY_CAP_MS = 2000;
13290
13801
  var SUBC_DEFAULT_REQUEST_TIMEOUT_MS = 30000;
13291
13802
  var ROUTE_OPEN_RELOAD_WAIT_CEILING_MS = 45000;
13803
+ var CALL_DEADLINE_EXPIRED = Symbol("call deadline expired");
13292
13804
  function reloadWaitExhaustedSuffix(callDeadlineMs) {
13293
13805
  if (callDeadlineMs > ROUTE_OPEN_RELOAD_WAIT_CEILING_MS) {
13294
13806
  const ceilingSeconds = ROUTE_OPEN_RELOAD_WAIT_CEILING_MS / 1000;
@@ -13641,7 +14153,16 @@ function absentRootError(root) {
13641
14153
  return new SubcError(`invalid route project root: project root does not exist: ${root}`, "config_divergence");
13642
14154
  }
13643
14155
  function reloadWindowExhaustedError(error, callDeadlineMs) {
13644
- const suffix = reloadWaitExhaustedSuffix(callDeadlineMs);
14156
+ return withDeadlineSuffix(error, reloadWaitExhaustedSuffix(callDeadlineMs));
14157
+ }
14158
+ function callDeadlinePassedError(lastError, callDeadlineMs) {
14159
+ const seconds = Math.round(callDeadlineMs / 100) / 10;
14160
+ const suffix = ` The AFT daemon module did not return within this call's ${seconds}s deadline.`;
14161
+ if (lastError !== undefined)
14162
+ return withDeadlineSuffix(lastError, suffix);
14163
+ return new SubcError(`route to module '${AFT_MODULE_ID}' was not ready before the call ran out of time.${suffix}`, REQUEST_DEADLINE_CODE);
14164
+ }
14165
+ function withDeadlineSuffix(error, suffix) {
13645
14166
  if (error instanceof Error) {
13646
14167
  try {
13647
14168
  error.message += suffix;
@@ -13730,6 +14251,8 @@ class SubcTransport {
13730
14251
  body.edit_slot_survives = editSlotSurvives;
13731
14252
  if (preview === true)
13732
14253
  body.preview = true;
14254
+ if (options?.workerSession === true)
14255
+ body[WORKER_SESSION_FIELD] = true;
13733
14256
  const reply = await this.pool.routeRequest(this.identityFor(sessionId), body, timeoutMs, onProgress, this.generation, abortSignal);
13734
14257
  return reliftReply(reply);
13735
14258
  }
@@ -13740,7 +14263,10 @@ class SubcTransport {
13740
14263
  }
13741
14264
  const { timeoutMs, onProgress, abortSignal } = this.splitOptions(options);
13742
14265
  const session = typeof params.session_id === "string" ? params.session_id : undefined;
13743
- const body = { name: command, arguments: params };
14266
+ const { [WORKER_SESSION_FIELD]: workerSession, ...args } = params;
14267
+ const body = { name: command, arguments: args };
14268
+ if (workerSession === true)
14269
+ body[WORKER_SESSION_FIELD] = true;
13744
14270
  const editSlotSurvives = this.pool.getEditSlotSurvives();
13745
14271
  if (editSlotSurvives !== undefined)
13746
14272
  body.edit_slot_survives = editSlotSurvives;
@@ -13777,6 +14303,7 @@ class SubcTransportPool {
13777
14303
  lifecycleRegistration = null;
13778
14304
  outerFacadeEvictor;
13779
14305
  client = null;
14306
+ hadWorkingClient = false;
13780
14307
  connecting = null;
13781
14308
  routeReopenRetryDelayMs = RECONNECT_RETRY_FLOOR_MS;
13782
14309
  routeReopenRetry = null;
@@ -13880,7 +14407,7 @@ class SubcTransportPool {
13880
14407
  rootCanAttach(root) {
13881
14408
  let directoryExists = false;
13882
14409
  try {
13883
- directoryExists = statSync4(root).isDirectory();
14410
+ directoryExists = statSync5(root).isDirectory();
13884
14411
  } catch {}
13885
14412
  if (directoryExists) {
13886
14413
  this.dormantRoots.delete(root);
@@ -14220,6 +14747,11 @@ class SubcTransportPool {
14220
14747
  return this.rootReapedError(record);
14221
14748
  }
14222
14749
  async routeRequest(identity, body, timeoutMs, onProgress, expectedGeneration, abortSignal) {
14750
+ const callStartedAt = performance.now();
14751
+ const callDeadlineMs = timeoutMs ?? SUBC_DEFAULT_REQUEST_TIMEOUT_MS;
14752
+ let scheduledDelayMs = 0;
14753
+ const spentMs = () => Math.max(performance.now() - callStartedAt, scheduledDelayMs);
14754
+ const remainingMs = () => callDeadlineMs - spentMs();
14223
14755
  const root = asCanonicalRootPath(identity.project_root);
14224
14756
  let generation = expectedGeneration;
14225
14757
  if (this.lifecycleEnabled()) {
@@ -14235,7 +14767,49 @@ class SubcTransportPool {
14235
14767
  try {
14236
14768
  let client;
14237
14769
  try {
14238
- client = await this.ensureClient();
14770
+ let keyChanged;
14771
+ while (true) {
14772
+ const remaining = remainingMs();
14773
+ if (remaining <= 0)
14774
+ throw keyChanged ?? callDeadlinePassedError(undefined, callDeadlineMs);
14775
+ let timer;
14776
+ try {
14777
+ const acquired = await Promise.race([
14778
+ this.ensureClient(),
14779
+ new Promise((resolve) => {
14780
+ timer = setTimeout(() => resolve(CALL_DEADLINE_EXPIRED), remaining);
14781
+ })
14782
+ ]);
14783
+ if (acquired === CALL_DEADLINE_EXPIRED) {
14784
+ throw keyChanged ?? callDeadlinePassedError(undefined, callDeadlineMs);
14785
+ }
14786
+ client = acquired;
14787
+ break;
14788
+ } catch (error) {
14789
+ if (!(error instanceof ReconnectKeyChangedError))
14790
+ throw error;
14791
+ keyChanged = error;
14792
+ if (spentMs() + this.nextRouteReopenDelayMs() >= callDeadlineMs)
14793
+ throw error;
14794
+ } finally {
14795
+ clearTimeout(timer);
14796
+ }
14797
+ const { delayMs, wait } = this.waitForRouteReopenBackoff();
14798
+ scheduledDelayMs = spentMs() + delayMs;
14799
+ let retryTimer;
14800
+ try {
14801
+ const waited = await Promise.race([
14802
+ wait,
14803
+ new Promise((resolve) => {
14804
+ retryTimer = setTimeout(() => resolve(CALL_DEADLINE_EXPIRED), callDeadlineMs - (performance.now() - callStartedAt));
14805
+ })
14806
+ ]);
14807
+ if (waited === CALL_DEADLINE_EXPIRED)
14808
+ throw keyChanged;
14809
+ } finally {
14810
+ clearTimeout(retryTimer);
14811
+ }
14812
+ }
14239
14813
  this.assertRecordLive(record);
14240
14814
  } catch (error) {
14241
14815
  throw this.annotateReapError(error, record);
@@ -14270,21 +14844,39 @@ class SubcTransportPool {
14270
14844
  throw error;
14271
14845
  }
14272
14846
  };
14273
- const callDeadlineMs = timeoutMs ?? SUBC_DEFAULT_REQUEST_TIMEOUT_MS;
14274
14847
  const reloadWaitBudgetMs = Math.min(callDeadlineMs, ROUTE_OPEN_RELOAD_WAIT_CEILING_MS);
14275
- let reloadWaitedMs = 0;
14848
+ let lastRefusal;
14849
+ const openRouteWithinDeadline = async () => {
14850
+ const remaining = remainingMs();
14851
+ if (remaining <= 0)
14852
+ throw callDeadlinePassedError(lastRefusal, callDeadlineMs);
14853
+ let timer;
14854
+ const expired = new Promise((resolve) => {
14855
+ timer = setTimeout(() => resolve(CALL_DEADLINE_EXPIRED), remaining);
14856
+ });
14857
+ try {
14858
+ const opened = await Promise.race([openRoute(), expired]);
14859
+ if (opened === CALL_DEADLINE_EXPIRED) {
14860
+ throw callDeadlinePassedError(lastRefusal, callDeadlineMs);
14861
+ }
14862
+ return opened;
14863
+ } finally {
14864
+ clearTimeout(timer);
14865
+ }
14866
+ };
14276
14867
  const openRouteAfterReloadWindow = async () => {
14277
14868
  while (true) {
14278
14869
  try {
14279
- return await openRoute();
14870
+ return await openRouteWithinDeadline();
14280
14871
  } catch (error) {
14281
14872
  if (!isRouteOpenReloadWindowError(error))
14282
14873
  throw error;
14283
- if (reloadWaitedMs + this.nextRouteReopenDelayMs() >= reloadWaitBudgetMs) {
14874
+ lastRefusal = error;
14875
+ if (spentMs() + this.nextRouteReopenDelayMs() >= reloadWaitBudgetMs) {
14284
14876
  throw reloadWindowExhaustedError(error, callDeadlineMs);
14285
14877
  }
14286
14878
  const { delayMs, wait } = this.waitForRouteReopenBackoff();
14287
- reloadWaitedMs += delayMs;
14879
+ scheduledDelayMs = spentMs() + delayMs;
14288
14880
  await wait;
14289
14881
  }
14290
14882
  }
@@ -14320,7 +14912,7 @@ class SubcTransportPool {
14320
14912
  route: routeLabel(entry.handle)
14321
14913
  });
14322
14914
  };
14323
- const requestOnRoute = async (route, entry) => {
14915
+ const requestOnRoute = async (route, entry, requestTimeoutMs) => {
14324
14916
  this.assertRecordLive(record);
14325
14917
  if (abortSignal?.aborted) {
14326
14918
  clearRouteEntry(entry);
@@ -14336,8 +14928,10 @@ class SubcTransportPool {
14336
14928
  };
14337
14929
  abortSignal?.addEventListener("abort", onAbort, { once: true });
14338
14930
  try {
14339
- const requestTimeoutMs = reloadWaitedMs > 0 ? callDeadlineMs - reloadWaitedMs : timeoutMs;
14340
- const request = client.request(route, body, { timeoutMs: requestTimeoutMs, onProgress });
14931
+ const request = client.request(route, body, {
14932
+ timeoutMs: Math.ceil(requestTimeoutMs),
14933
+ onProgress
14934
+ });
14341
14935
  const reply = abortSignal ? await Promise.race([request, aborted]) : await request;
14342
14936
  if (!this.isCurrentSession(key, record)) {
14343
14937
  if (this.isReapInduced(record))
@@ -14358,8 +14952,11 @@ class SubcTransportPool {
14358
14952
  let reopened = false;
14359
14953
  let retriedAbsentRoute = false;
14360
14954
  while (true) {
14955
+ const requestTimeoutMs = remainingMs();
14956
+ if (requestTimeoutMs <= 0)
14957
+ throw callDeadlinePassedError(lastRefusal, callDeadlineMs);
14361
14958
  try {
14362
- const reply = await requestOnRoute(routeAndEntry.route, routeAndEntry.entry);
14959
+ const reply = await requestOnRoute(routeAndEntry.route, routeAndEntry.entry, requestTimeoutMs);
14363
14960
  if (reopened)
14364
14961
  this.resetRouteReopenBackoff();
14365
14962
  return reply;
@@ -14369,11 +14966,12 @@ class SubcTransportPool {
14369
14966
  const ownsRoute = this.isCurrentSession(key, record) && this.client === client;
14370
14967
  if (ownsRoute && isRouteRequestReloadRefusal(error)) {
14371
14968
  clearRouteEntry(routeAndEntry.entry);
14372
- if (reloadWaitedMs + this.nextRouteReopenDelayMs() >= reloadWaitBudgetMs) {
14969
+ lastRefusal = error;
14970
+ if (spentMs() + this.nextRouteReopenDelayMs() >= reloadWaitBudgetMs) {
14373
14971
  throw reloadWindowExhaustedError(error, callDeadlineMs);
14374
14972
  }
14375
14973
  const { delayMs, wait } = this.waitForRouteReopenBackoff();
14376
- reloadWaitedMs += delayMs;
14974
+ scheduledDelayMs = spentMs() + delayMs;
14377
14975
  await wait;
14378
14976
  routeAndEntry = await openRouteAfterReloadWindow();
14379
14977
  reopened = true;
@@ -14382,7 +14980,12 @@ class SubcTransportPool {
14382
14980
  if (ownsRoute && !retriedAbsentRoute && isRouteProvenAbsentError(error)) {
14383
14981
  retriedAbsentRoute = true;
14384
14982
  clearRouteEntry(routeAndEntry.entry);
14385
- await this.waitForRouteReopenBackoff().wait;
14983
+ if (spentMs() + this.nextRouteReopenDelayMs() >= callDeadlineMs) {
14984
+ throw callDeadlinePassedError(error, callDeadlineMs);
14985
+ }
14986
+ const { delayMs, wait } = this.waitForRouteReopenBackoff();
14987
+ scheduledDelayMs = spentMs() + delayMs;
14988
+ await wait;
14386
14989
  routeAndEntry = await openRouteAfterReloadWindow();
14387
14990
  reopened = true;
14388
14991
  continue;
@@ -14445,10 +15048,14 @@ class SubcTransportPool {
14445
15048
  throw new SubcTransportShuttingDownError;
14446
15049
  }
14447
15050
  this.client = client;
15051
+ this.hadWorkingClient = true;
14448
15052
  this.transportFailures = 0;
14449
15053
  return client;
14450
15054
  }).catch((error) => {
14451
15055
  this.connecting = null;
15056
+ if (this.hadWorkingClient && error instanceof AuthError) {
15057
+ throw new ReconnectKeyChangedError;
15058
+ }
14452
15059
  throw error;
14453
15060
  });
14454
15061
  return this.connecting;
@@ -16060,14 +16667,14 @@ function shouldInterruptWaitsForMessage(detachOnUserMessage, operatorText) {
16060
16667
  return detachOnUserMessage || containsStandaloneDetachKeyword(operatorText);
16061
16668
  }
16062
16669
  // ../aft-bridge/dist/migration.js
16063
- import { closeSync as closeSync3, existsSync as existsSync8, mkdirSync as mkdirSync3, openSync as openSync3, readFileSync as readFileSync6, renameSync as renameSync4, rmSync as rmSync2, statSync as statSync5, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "node:fs";
16670
+ import { closeSync as closeSync3, existsSync as existsSync8, mkdirSync as mkdirSync3, openSync as openSync3, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync2, statSync as statSync6, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "node:fs";
16064
16671
  import { homedir as homedir10, tmpdir as tmpdir2 } from "node:os";
16065
- import { basename, dirname as dirname4, join as join10, resolve as resolve6 } from "node:path";
16672
+ import { basename as basename2, dirname as dirname5, join as join10, resolve as resolve6 } from "node:path";
16066
16673
 
16067
16674
  // ../aft-bridge/dist/paths.js
16068
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync2 } from "node:fs";
16675
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync2 } from "node:fs";
16069
16676
  import { homedir as homedir8 } from "node:os";
16070
- import { dirname as dirname2, isAbsolute as isAbsolute3, join as join8, resolve as resolve5 } from "node:path";
16677
+ import { dirname as dirname3, isAbsolute as isAbsolute3, join as join8, resolve as resolve5 } from "node:path";
16071
16678
  function homeDir3() {
16072
16679
  if (process.platform === "win32")
16073
16680
  return process.env.USERPROFILE || process.env.HOME || homedir8();
@@ -16127,7 +16734,7 @@ function repairRootScopedStorageFile(storageRoot, harness, fileName) {
16127
16734
  if (existsSync6(harnessPath) || !existsSync6(rootPath))
16128
16735
  return harnessPath;
16129
16736
  try {
16130
- mkdirSync2(dirname2(harnessPath), { recursive: true });
16737
+ mkdirSync2(dirname3(harnessPath), { recursive: true });
16131
16738
  renameSync3(rootPath, harnessPath);
16132
16739
  } catch {}
16133
16740
  return harnessPath;
@@ -16139,7 +16746,7 @@ function shouldShowAnnouncement(storageRoot, harness, currentVersion) {
16139
16746
  let lastVersion = "";
16140
16747
  try {
16141
16748
  if (existsSync6(versionFile)) {
16142
- lastVersion = readFileSync4(versionFile, "utf-8").trim();
16749
+ lastVersion = readFileSync5(versionFile, "utf-8").trim();
16143
16750
  }
16144
16751
  } catch {
16145
16752
  return false;
@@ -16148,7 +16755,7 @@ function shouldShowAnnouncement(storageRoot, harness, currentVersion) {
16148
16755
  return false;
16149
16756
  if (!lastVersion) {
16150
16757
  try {
16151
- mkdirSync2(dirname2(versionFile), { recursive: true });
16758
+ mkdirSync2(dirname3(versionFile), { recursive: true });
16152
16759
  writeFileSync2(versionFile, currentVersion);
16153
16760
  } catch {}
16154
16761
  return false;
@@ -16160,7 +16767,7 @@ function markAnnouncementSeen(storageRoot, harness, currentVersion) {
16160
16767
  return;
16161
16768
  const versionFile = repairRootScopedStorageFile(storageRoot, harness, "last_announced_version");
16162
16769
  try {
16163
- mkdirSync2(dirname2(versionFile), { recursive: true });
16770
+ mkdirSync2(dirname3(versionFile), { recursive: true });
16164
16771
  writeFileSync2(versionFile, currentVersion);
16165
16772
  } catch {}
16166
16773
  }
@@ -16209,10 +16816,10 @@ function decodeFileUrl(target) {
16209
16816
  }
16210
16817
 
16211
16818
  // ../aft-bridge/dist/resolver.js
16212
- import { closeSync as closeSync2, existsSync as existsSync7, promises as fsPromises, openSync as openSync2, readFileSync as readFileSync5, readSync } from "node:fs";
16819
+ import { closeSync as closeSync2, existsSync as existsSync7, promises as fsPromises, openSync as openSync2, readFileSync as readFileSync6, readSync } from "node:fs";
16213
16820
  import { createRequire } from "node:module";
16214
16821
  import { homedir as homedir9 } from "node:os";
16215
- import { delimiter, dirname as dirname3, isAbsolute as isAbsolute4, join as join9 } from "node:path";
16822
+ import { delimiter, dirname as dirname4, isAbsolute as isAbsolute4, join as join9 } from "node:path";
16216
16823
  var ensureBinaryForResolver = ensureBinary;
16217
16824
  var npmPlatformPackageReader = null;
16218
16825
  var cacheCopiesInFlight = new Map;
@@ -16365,8 +16972,8 @@ function npmPlatformPackage(ext) {
16365
16972
  try {
16366
16973
  const req = createRequire(import.meta.url);
16367
16974
  const manifestPath = req.resolve(`@cortexkit/aft-${platformKey()}/package.json`);
16368
- const manifest = JSON.parse(readFileSync5(manifestPath, "utf8"));
16369
- const binaryPath = join9(dirname3(manifestPath), "bin", `aft${ext}`);
16975
+ const manifest = JSON.parse(readFileSync6(manifestPath, "utf8"));
16976
+ const binaryPath = join9(dirname4(manifestPath), "bin", `aft${ext}`);
16370
16977
  if (typeof manifest.version !== "string" || !existsSync7(binaryPath))
16371
16978
  return null;
16372
16979
  return { binaryPath, version: normalizeBareVersion(manifest.version) };
@@ -16620,7 +17227,7 @@ function acquireConfigMigrationLock(lockDir) {
16620
17227
  if (code !== "EEXIST")
16621
17228
  throw err;
16622
17229
  try {
16623
- const ageMs = Date.now() - statSync5(lockDir).mtimeMs;
17230
+ const ageMs = Date.now() - statSync6(lockDir).mtimeMs;
16624
17231
  if (ageMs > 60000) {
16625
17232
  rmSync2(lockDir, { recursive: true, force: true });
16626
17233
  continue;
@@ -16634,12 +17241,12 @@ function acquireConfigMigrationLock(lockDir) {
16634
17241
  }
16635
17242
  }
16636
17243
  function atomicCopyConfigFile(sourcePath, targetPath) {
16637
- mkdirSync3(dirname4(targetPath), { recursive: true });
16638
- const tmpPath = join10(dirname4(targetPath), `.${basename(targetPath)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
17244
+ mkdirSync3(dirname5(targetPath), { recursive: true });
17245
+ const tmpPath = join10(dirname5(targetPath), `.${basename2(targetPath)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
16639
17246
  let fd = null;
16640
17247
  try {
16641
17248
  fd = openSync3(tmpPath, "wx", 384);
16642
- writeFileSync3(fd, readFileSync6(sourcePath));
17249
+ writeFileSync3(fd, readFileSync7(sourcePath));
16643
17250
  closeSync3(fd);
16644
17251
  fd = null;
16645
17252
  renameSync4(tmpPath, targetPath);
@@ -16656,8 +17263,8 @@ function atomicCopyConfigFile(sourcePath, targetPath) {
16656
17263
  }
16657
17264
  }
16658
17265
  function atomicWriteConfigFile(targetPath, content) {
16659
- mkdirSync3(dirname4(targetPath), { recursive: true });
16660
- const tmpPath = join10(dirname4(targetPath), `.${basename(targetPath)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
17266
+ mkdirSync3(dirname5(targetPath), { recursive: true });
17267
+ const tmpPath = join10(dirname5(targetPath), `.${basename2(targetPath)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
16661
17268
  let fd = null;
16662
17269
  try {
16663
17270
  fd = openSync3(tmpPath, "wx", 384);
@@ -16718,11 +17325,11 @@ function markLegacySourcesMovedAside(sources, targetPath, logger) {
16718
17325
  const desiredMarkerPath = `${source.path}${MOVED_MARKER_SUFFIX}`;
16719
17326
  const markerPath = nonClobberingSidecarPath(desiredMarkerPath);
16720
17327
  try {
16721
- const original = readFileSync6(source.path, "utf-8");
17328
+ const original = readFileSync7(source.path, "utf-8");
16722
17329
  if (markerPath !== desiredMarkerPath) {
16723
17330
  logger?.warn?.(`Preserving existing legacy AFT marker ${desiredMarkerPath}; writing new marker to ${markerPath}`);
16724
17331
  }
16725
- atomicWriteConfigFile(markerPath, movedMarkerContent(targetPath, basename(source.path), original));
17332
+ atomicWriteConfigFile(markerPath, movedMarkerContent(targetPath, basename2(source.path), original));
16726
17333
  unlinkSync3(source.path);
16727
17334
  info?.(`Moved legacy AFT config ${source.path} aside to ${markerPath}; AFT now reads ${targetPath}`);
16728
17335
  } catch (err) {
@@ -16782,15 +17389,15 @@ function migrateAftConfigFile(opts) {
16782
17389
  if (existingSources.length === 0) {
16783
17390
  return { migrated: false, conflict: false, targetPath: opts.targetPath, warnings };
16784
17391
  }
16785
- mkdirSync3(dirname4(opts.targetPath), { recursive: true });
17392
+ mkdirSync3(dirname5(opts.targetPath), { recursive: true });
16786
17393
  const release = acquireConfigMigrationLock(`${opts.targetPath}.lock`);
16787
17394
  try {
16788
17395
  const sources = existingSources.map((source) => ({
16789
17396
  ...source,
16790
- content: readFileSync6(source.path, "utf-8")
17397
+ content: readFileSync7(source.path, "utf-8")
16791
17398
  }));
16792
17399
  if (existsSync8(opts.targetPath)) {
16793
- const targetContent = readFileSync6(opts.targetPath, "utf-8");
17400
+ const targetContent = readFileSync7(opts.targetPath, "utf-8");
16794
17401
  for (const source of sources) {
16795
17402
  if (fileSemanticsMatch(source.content, targetContent))
16796
17403
  continue;
@@ -16841,11 +17448,11 @@ function spawnErrorLabel(error) {
16841
17448
  function migrationLogPath(newRoot, harness, logger) {
16842
17449
  const desired = join10(newRoot, "logs", "migration", `${harness}-${Date.now()}.jsonl`);
16843
17450
  try {
16844
- mkdirSync3(dirname4(desired), { recursive: true });
17451
+ mkdirSync3(dirname5(desired), { recursive: true });
16845
17452
  return desired;
16846
17453
  } catch (err) {
16847
17454
  const fallback = join10(tmpdir2(), `aft-migration-${harness}-${Date.now()}.jsonl`);
16848
- logger?.warn?.(`Failed to create AFT migration log directory ${dirname4(desired)}: ${err instanceof Error ? err.message : String(err)}. ` + `Using fallback log path ${fallback}.`);
17455
+ logger?.warn?.(`Failed to create AFT migration log directory ${dirname5(desired)}: ${err instanceof Error ? err.message : String(err)}. ` + `Using fallback log path ${fallback}.`);
16849
17456
  return fallback;
16850
17457
  }
16851
17458
  }
@@ -16906,9 +17513,9 @@ async function ensureStorageMigrated(opts) {
16906
17513
  throw new Error(`AFT storage migration failed (${detail}). ` + `Harness: ${opts.harness}. Legacy: ${legacyRoot}. Target: ${newRoot}. ` + `See log: ${logPath}. ` + `Plugin load aborted to prevent legacy/new state divergence.` + (stderrTail ? ` Stderr tail: ${stderrTail}` : "") + (stdoutTail ? ` Stdout tail: ${stdoutTail}` : ""));
16907
17514
  }
16908
17515
  // ../aft-bridge/dist/migration-notices.js
16909
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, renameSync as renameSync5, rmdirSync, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
17516
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync8, renameSync as renameSync5, rmdirSync, statSync as statSync7, writeFileSync as writeFileSync4 } from "node:fs";
16910
17517
  import { homedir as homedir11 } from "node:os";
16911
- import { dirname as dirname5, join as join11, resolve as resolve7 } from "node:path";
17518
+ import { dirname as dirname6, join as join11, resolve as resolve7 } from "node:path";
16912
17519
  function migrationNoticeStorePath(env = process.env) {
16913
17520
  const stateHome = env.XDG_STATE_HOME && env.XDG_STATE_HOME.length > 0 ? env.XDG_STATE_HOME : join11(homedir11(), ".local", "state");
16914
17521
  return join11(stateHome, "cortexkit", "aft", "migration-notices.json");
@@ -16918,7 +17525,7 @@ function noticeKey(configPath, digest) {
16918
17525
  }
16919
17526
  function readRecords(storePath) {
16920
17527
  try {
16921
- const parsed = JSON.parse(readFileSync7(storePath, "utf8"));
17528
+ const parsed = JSON.parse(readFileSync8(storePath, "utf8"));
16922
17529
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
16923
17530
  } catch {
16924
17531
  return {};
@@ -16927,7 +17534,7 @@ function readRecords(storePath) {
16927
17534
  var LOCK_STALE_MS = 1e4;
16928
17535
  function withStoreLock(storePath, action) {
16929
17536
  const lockPath = `${storePath}.lock`;
16930
- mkdirSync4(dirname5(storePath), { recursive: true });
17537
+ mkdirSync4(dirname6(storePath), { recursive: true });
16931
17538
  const deadline = Date.now() + 2000;
16932
17539
  for (;; ) {
16933
17540
  try {
@@ -16937,7 +17544,7 @@ function withStoreLock(storePath, action) {
16937
17544
  if (error.code !== "EEXIST")
16938
17545
  throw error;
16939
17546
  try {
16940
- if (Date.now() - statSync6(lockPath).mtimeMs > LOCK_STALE_MS)
17547
+ if (Date.now() - statSync7(lockPath).mtimeMs > LOCK_STALE_MS)
16941
17548
  rmdirSync(lockPath);
16942
17549
  } catch {}
16943
17550
  if (Date.now() > deadline)
@@ -16978,9 +17585,9 @@ function deliverMigrationNoticeOnce(options) {
16978
17585
  }
16979
17586
  }
16980
17587
  // ../aft-bridge/dist/npm-resolver.js
16981
- import { readdirSync as readdirSync2, statSync as statSync7 } from "node:fs";
17588
+ import { readdirSync as readdirSync2, statSync as statSync8 } from "node:fs";
16982
17589
  import { homedir as homedir12 } from "node:os";
16983
- import { dirname as dirname6, isAbsolute as isAbsolute5, join as join12 } from "node:path";
17590
+ import { dirname as dirname7, isAbsolute as isAbsolute5, join as join12 } from "node:path";
16984
17591
  function defaultDeps() {
16985
17592
  return {
16986
17593
  platform: process.platform,
@@ -16994,7 +17601,7 @@ function npmBinaryName(platform) {
16994
17601
  }
16995
17602
  function isFile(p) {
16996
17603
  try {
16997
- return statSync7(p).isFile();
17604
+ return statSync8(p).isFile();
16998
17605
  } catch {
16999
17606
  return false;
17000
17607
  }
@@ -17015,7 +17622,7 @@ function npmFromPath(deps) {
17015
17622
  return null;
17016
17623
  }
17017
17624
  function npmAdjacentToNode(deps) {
17018
- const dir = dirname6(deps.execPath);
17625
+ const dir = dirname7(deps.execPath);
17019
17626
  return isFile(join12(dir, npmBinaryName(deps.platform))) ? dir : null;
17020
17627
  }
17021
17628
  function highestVersionedNodeBin(installsDir, name) {
@@ -17400,8 +18007,8 @@ function probeOnnxRuntimeLoadable(libPath, arch = process.arch) {
17400
18007
  }
17401
18008
  // ../aft-bridge/dist/onnx-runtime.js
17402
18009
  import { createHash as createHash4, randomBytes as randomBytes2 } from "node:crypto";
17403
- import { chmodSync as chmodSync2, closeSync as closeSync5, copyFileSync as copyFileSync2, createWriteStream as createWriteStream2, existsSync as existsSync11, lstatSync, mkdirSync as mkdirSync5, openSync as openSync5, readdirSync as readdirSync3, readFileSync as readFileSync8, readlinkSync, realpathSync as realpathSync3, renameSync as renameSync6, rmSync as rmSync3, statSync as statSync8, symlinkSync, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "node:fs";
17404
- import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute6, join as join14, relative as relative2, resolve as resolve8, win32 } from "node:path";
18010
+ import { chmodSync as chmodSync2, closeSync as closeSync5, copyFileSync as copyFileSync2, createWriteStream as createWriteStream2, existsSync as existsSync11, lstatSync, mkdirSync as mkdirSync5, openSync as openSync5, readdirSync as readdirSync3, readFileSync as readFileSync9, readlinkSync, realpathSync as realpathSync3, renameSync as renameSync6, rmSync as rmSync3, statSync as statSync9, symlinkSync, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "node:fs";
18011
+ import { basename as basename3, dirname as dirname8, isAbsolute as isAbsolute6, join as join14, relative as relative2, resolve as resolve8, win32 } from "node:path";
17405
18012
  import { Readable as Readable2 } from "node:stream";
17406
18013
  import { pipeline as pipeline2 } from "node:stream/promises";
17407
18014
 
@@ -17441,31 +18048,36 @@ var ORT_PLATFORM_MAP = {
17441
18048
  arm64: {
17442
18049
  assetName: `onnxruntime-osx-arm64-${ORT_VERSION}`,
17443
18050
  libName: "libonnxruntime.dylib",
17444
- archiveType: "tgz"
18051
+ archiveType: "tgz",
18052
+ librarySha256: "872533f130f1839a5bc01788ddb4f75c83a189763441ba1178788ed965449289"
17445
18053
  }
17446
18054
  },
17447
18055
  linux: {
17448
18056
  x64: {
17449
18057
  assetName: `onnxruntime-linux-x64-${ORT_VERSION}`,
17450
18058
  libName: "libonnxruntime.so",
17451
- archiveType: "tgz"
18059
+ archiveType: "tgz",
18060
+ librarySha256: "d132535d051344ff5c64c9c200004150559049a81ed330eb4422c1962fb6b7e4"
17452
18061
  },
17453
18062
  arm64: {
17454
18063
  assetName: `onnxruntime-linux-aarch64-${ORT_VERSION}`,
17455
18064
  libName: "libonnxruntime.so",
17456
- archiveType: "tgz"
18065
+ archiveType: "tgz",
18066
+ librarySha256: "52b2a0e75e79468404284fec38ad5ee1a7a996232274f5e1b84f3e793fb07554"
17457
18067
  }
17458
18068
  },
17459
18069
  win32: {
17460
18070
  x64: {
17461
18071
  assetName: `onnxruntime-win-x64-${ORT_VERSION}`,
17462
18072
  libName: "onnxruntime.dll",
17463
- archiveType: "zip"
18073
+ archiveType: "zip",
18074
+ librarySha256: "b95efb2113b603bbbf3f191061c5516a871ed546893c820e4f3b7b6c358dbf2a"
17464
18075
  },
17465
18076
  arm64: {
17466
18077
  assetName: `onnxruntime-win-arm64-${ORT_VERSION}`,
17467
18078
  libName: "onnxruntime.dll",
17468
- archiveType: "zip"
18079
+ archiveType: "zip",
18080
+ librarySha256: "724d81ac50b11bfaa01ab3ce01b99fb6734a4b762259a8be4395ca662ce99fe6"
17469
18081
  }
17470
18082
  }
17471
18083
  };
@@ -17605,7 +18217,7 @@ function cleanupAbandonedStagingDirs(onnxBaseDir) {
17605
18217
  abandoned = true;
17606
18218
  } else {
17607
18219
  try {
17608
- const ageMs = Date.now() - statSync8(stagingDir).mtimeMs;
18220
+ const ageMs = Date.now() - statSync9(stagingDir).mtimeMs;
17609
18221
  abandoned = ageMs > STALE_LOCK_MS;
17610
18222
  } catch {
17611
18223
  abandoned = true;
@@ -17642,7 +18254,7 @@ var REQUIRED_ORT_MAJOR = 1;
17642
18254
  var REQUIRED_ORT_MIN_MINOR = 20;
17643
18255
  var INVALID_ORT_VERSION = "<invalid>";
17644
18256
  function parseOnnxVersionFromPath(value) {
17645
- const name = basename2(value);
18257
+ const name = basename3(value);
17646
18258
  const semverish = name.match(/(?:^|[._-])(\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?)(?:\.(?:dylib|dll))?$/);
17647
18259
  if (semverish)
17648
18260
  return semverish[1].split(/[-+]/, 1)[0];
@@ -17847,7 +18459,7 @@ async function downloadFileWithCap(url, destPath) {
17847
18459
  if (Number.isFinite(advertised) && advertised > MAX_DOWNLOAD_BYTES2) {
17848
18460
  throw new Error(`Content-Length ${advertised} exceeds max ${MAX_DOWNLOAD_BYTES2}`);
17849
18461
  }
17850
- mkdirSync5(dirname7(destPath), { recursive: true });
18462
+ mkdirSync5(dirname8(destPath), { recursive: true });
17851
18463
  let bytesWritten = 0;
17852
18464
  const guard = new TransformStream({
17853
18465
  transform(chunk, transformController) {
@@ -17881,7 +18493,7 @@ function validateExtractedTree(stagingRoot) {
17881
18493
  const lst = lstatSync(fullPath);
17882
18494
  if (lst.isSymbolicLink()) {
17883
18495
  const linkTarget = readlinkSync(fullPath);
17884
- const resolvedTarget = resolve8(dirname7(fullPath), linkTarget);
18496
+ const resolvedTarget = resolve8(dirname8(fullPath), linkTarget);
17885
18497
  const rel = relative2(realRoot, resolvedTarget);
17886
18498
  if (rel.startsWith("..") || isAbsolute6(rel) || win32.isAbsolute(rel)) {
17887
18499
  throw new Error(`extracted symlink ${fullPath} points outside staging root: ${linkTarget}`);
@@ -17952,6 +18564,9 @@ async function downloadOnnxRuntime(info, targetDir) {
17952
18564
  copyOnnxLibraries(info, extractedDir, stagedInstallDir, realFiles, symlinks);
17953
18565
  const libPath = join14(stagedInstallDir, info.libName);
17954
18566
  const libHash = sha256File2(libPath);
18567
+ if (libHash !== info.librarySha256) {
18568
+ throw new Error(`onnx runtime library hash mismatch at ${libPath}: sha256 ${libHash}, ` + `expected the pinned ${info.librarySha256} for ${info.assetName}`);
18569
+ }
17955
18570
  writeOnnxInstalledMeta(stagedInstallDir, ORT_VERSION, libHash, archiveSha256);
17956
18571
  publishOnnxRuntime(stagedInstallDir, targetDir);
17957
18572
  rmSync3(tmpDir, { recursive: true, force: true });
@@ -18017,7 +18632,7 @@ function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, c
18017
18632
  unlinkSync4(dst);
18018
18633
  } catch {}
18019
18634
  const dstForContainment = join14(targetRoot, link.name);
18020
- const resolvedTarget = resolve8(dirname7(dstForContainment), link.target);
18635
+ const resolvedTarget = resolve8(dirname8(dstForContainment), link.target);
18021
18636
  if (!isPathInsideRoot(targetRoot, resolvedTarget)) {
18022
18637
  const message = `ONNX Runtime symlink ${link.name} points outside install dir: ${link.target}`;
18023
18638
  if (requiredLibs.has(link.name)) {
@@ -18069,9 +18684,9 @@ function writeOnnxInstalledMeta(installDir, version, sha256, archiveSha256) {
18069
18684
  function readOnnxInstalledMeta(installDir) {
18070
18685
  const path = join14(installDir, ONNX_INSTALLED_META_FILE);
18071
18686
  try {
18072
- if (!statSync8(path).isFile())
18687
+ if (!statSync9(path).isFile())
18073
18688
  return null;
18074
- const raw = readFileSync8(path, "utf8");
18689
+ const raw = readFileSync9(path, "utf8");
18075
18690
  const parsed = JSON.parse(raw);
18076
18691
  if (typeof parsed.version !== "string" || parsed.version.length === 0)
18077
18692
  return null;
@@ -18087,7 +18702,7 @@ function readOnnxInstalledMeta(installDir) {
18087
18702
  }
18088
18703
  function sha256File2(path) {
18089
18704
  const hash = createHash4("sha256");
18090
- hash.update(readFileSync8(path));
18705
+ hash.update(readFileSync9(path));
18091
18706
  return hash.digest("hex");
18092
18707
  }
18093
18708
  function acquireLock(lockPath) {
@@ -18115,12 +18730,12 @@ ${new Date().toISOString()}
18115
18730
  let owningPid = null;
18116
18731
  let lockMtimeMs = 0;
18117
18732
  try {
18118
- const raw = readFileSync8(lockPath, "utf8");
18733
+ const raw = readFileSync9(lockPath, "utf8");
18119
18734
  const firstLine = raw.split(/\r?\n/, 1)[0]?.trim() ?? "";
18120
18735
  const parsed = Number.parseInt(firstLine, 10);
18121
18736
  if (Number.isFinite(parsed) && parsed > 0)
18122
18737
  owningPid = parsed;
18123
- lockMtimeMs = statSync8(lockPath).mtimeMs;
18738
+ lockMtimeMs = statSync9(lockPath).mtimeMs;
18124
18739
  } catch {
18125
18740
  return tryClaim();
18126
18741
  }
@@ -18140,7 +18755,7 @@ function releaseLock(lockPath) {
18140
18755
  try {
18141
18756
  let owningPid = null;
18142
18757
  try {
18143
- const raw = readFileSync8(lockPath, "utf8");
18758
+ const raw = readFileSync9(lockPath, "utf8");
18144
18759
  const firstLine = raw.split(/\r?\n/, 1)[0]?.trim() ?? "";
18145
18760
  const parsed = Number.parseInt(firstLine, 10);
18146
18761
  if (Number.isFinite(parsed) && parsed > 0)
@@ -18294,12 +18909,13 @@ function normalizeZoomTargets(record) {
18294
18909
  throw new InvalidRequestError(`'targets[${index}].path' must be a non-empty string`);
18295
18910
  }
18296
18911
  const source = target;
18297
- const emptyTarget = source.symbol === "" && (hasOwn(source, "path") && source.path === "" || hasOwn(source, "filePath") && source.filePath === "");
18912
+ const blank = (value) => value === undefined || value === null || typeof value === "string" && !value.trim();
18913
+ const emptyTarget = blank(source.symbol) && blank(source.path) && blank(source.filePath);
18298
18914
  if (emptyTarget)
18299
18915
  return { ...source };
18300
18916
  const normalized = { ...source };
18301
18917
  try {
18302
- normalizeAliasPair(normalized, "path", "filePath", true);
18918
+ normalizeAliasPair(normalized, "path", "filePath", false);
18303
18919
  } catch (error) {
18304
18920
  if (error instanceof InvalidRequestError) {
18305
18921
  throw new InvalidRequestError(error.message.replace("'filePath'", `'targets[${index}].filePath'`).replace("'path'", `'targets[${index}].path'`));
@@ -19880,7 +20496,7 @@ import {
19880
20496
  // package.json
19881
20497
  var package_default = {
19882
20498
  name: "@cortexkit/aft-pi",
19883
- version: "0.58.0",
20499
+ version: "0.58.2",
19884
20500
  type: "module",
19885
20501
  description: "Pi coding agent extension for Agent File Tools (AFT) — tree-sitter and LSP-powered code analysis",
19886
20502
  main: "dist/index.js",
@@ -19903,30 +20519,32 @@ var package_default = {
19903
20519
  "test:unit": "bun test src/__tests__ --path-ignore-patterns 'src/__tests__/e2e/**'"
19904
20520
  },
19905
20521
  dependencies: {
19906
- "@cortexkit/aft-bridge": "0.58.0",
20522
+ "@cortexkit/aft-bridge": "0.58.2",
19907
20523
  "comment-json": "^5.0.0",
19908
20524
  diff: "^8.0.4",
19909
- typebox: "1.1.38",
19910
20525
  zod: "^4.4.3"
19911
20526
  },
19912
20527
  optionalDependencies: {
19913
- "@cortexkit/aft-darwin-arm64": "0.58.0",
19914
- "@cortexkit/aft-darwin-x64": "0.58.0",
19915
- "@cortexkit/aft-linux-arm64": "0.58.0",
19916
- "@cortexkit/aft-linux-x64": "0.58.0",
19917
- "@cortexkit/aft-win32-arm64": "0.58.0",
19918
- "@cortexkit/aft-win32-x64": "0.58.0"
20528
+ "@cortexkit/aft-darwin-arm64": "0.58.2",
20529
+ "@cortexkit/aft-darwin-x64": "0.58.2",
20530
+ "@cortexkit/aft-linux-arm64": "0.58.2",
20531
+ "@cortexkit/aft-linux-x64": "0.58.2",
20532
+ "@cortexkit/aft-win32-arm64": "0.58.2",
20533
+ "@cortexkit/aft-win32-x64": "0.58.2"
19919
20534
  },
19920
20535
  devDependencies: {
19921
20536
  "@earendil-works/pi-coding-agent": "^0.80.2",
19922
20537
  "@earendil-works/pi-ai": "^0.80.2",
19923
20538
  "@earendil-works/pi-tui": "^0.80.2",
19924
20539
  "@types/node": "^22.0.0",
20540
+ typebox: "1.1.38",
19925
20541
  typescript: "^5.8.0"
19926
20542
  },
19927
20543
  peerDependencies: {
20544
+ "@earendil-works/pi-ai": "*",
19928
20545
  "@earendil-works/pi-coding-agent": "*",
19929
- "@earendil-works/pi-tui": "*"
20546
+ "@earendil-works/pi-tui": "*",
20547
+ typebox: "*"
19930
20548
  },
19931
20549
  exports: {
19932
20550
  ".": {
@@ -20192,6 +20810,24 @@ function appendDegradedStatus(lines, status, markdown) {
20192
20810
  // src/tools/_shared.ts
20193
20811
  import { existsSync as existsSync12 } from "node:fs";
20194
20812
  import { Type } from "typebox";
20813
+
20814
+ // src/session-kind.ts
20815
+ var MAGIC_CONTEXT_SUBAGENT_ENV = "MAGIC_CONTEXT_PI_SUBAGENT";
20816
+ function piWorkerKind(extCtx, env = process.env) {
20817
+ if (env[MAGIC_CONTEXT_SUBAGENT_ENV] === "1")
20818
+ return "delegated";
20819
+ if (extCtx?.hasUI === false)
20820
+ return "headless";
20821
+ return;
20822
+ }
20823
+ function isPiWorkerSession(extCtx, env = process.env) {
20824
+ return piWorkerKind(extCtx, env) !== undefined;
20825
+ }
20826
+ function skipsEagerStartup(env = process.env) {
20827
+ return piWorkerKind(undefined, env) === "delegated";
20828
+ }
20829
+
20830
+ // src/tools/_shared.ts
20195
20831
  var optionalInt = (min, max, description = "(integer)") => Type.Optional(Type.Union([Type.Integer({ minimum: min, maximum: max }), Type.String()], {
20196
20832
  description
20197
20833
  }));
@@ -20277,6 +20913,9 @@ async function callBridge(bridge, command, params = {}, extCtx, options) {
20277
20913
  if (sessionId) {
20278
20914
  merged.session_id = sessionId;
20279
20915
  }
20916
+ if (isPiWorkerSession(extCtx)) {
20917
+ merged[WORKER_SESSION_FIELD] = true;
20918
+ }
20280
20919
  const sendOptions = {
20281
20920
  ...timeoutMs !== undefined ? { timeoutMs } : {},
20282
20921
  configureWarningClient: extCtx,
@@ -20304,6 +20943,7 @@ async function callToolCallForSession(bridge, name, rawArgs, sessionId, extCtx,
20304
20943
  const sendOptions = {
20305
20944
  ...timeoutMs !== undefined ? { timeoutMs } : {},
20306
20945
  configureWarningClient: extCtx,
20946
+ ...isPiWorkerSession(extCtx) ? { workerSession: true } : {},
20307
20947
  ...options
20308
20948
  };
20309
20949
  let response;
@@ -20623,7 +21263,7 @@ function registerStatusCommand(pi, ctx) {
20623
21263
  }
20624
21264
 
20625
21265
  // src/config.ts
20626
- import { existsSync as existsSync13, readFileSync as readFileSync9, renameSync as renameSync7, unlinkSync as unlinkSync5, writeFileSync as writeFileSync6 } from "node:fs";
21266
+ import { existsSync as existsSync13, readFileSync as readFileSync10, renameSync as renameSync7, unlinkSync as unlinkSync5, writeFileSync as writeFileSync6 } from "node:fs";
20627
21267
  import { createRequire as createRequire2 } from "node:module";
20628
21268
  import { homedir as homedir13 } from "node:os";
20629
21269
  import { isAbsolute as isAbsolute7 } from "node:path";
@@ -25436,7 +26076,7 @@ var IndexConfigSchema = object({
25436
26076
  roots: array(IndexRootSchema).optional()
25437
26077
  });
25438
26078
  var SemanticConfigSchema = object({
25439
- backend: _enum(["fastembed", "openai_compatible", "ollama"]).optional(),
26079
+ backend: _enum(["fastembed", "openai_compatible", "ollama", "synapse"]).optional(),
25440
26080
  model: string2().trim().min(1).optional(),
25441
26081
  base_url: string2().trim().min(1).optional(),
25442
26082
  api_key_env: string2().trim().min(1).optional(),
@@ -25447,6 +26087,17 @@ var SemanticConfigSchema = object({
25447
26087
  max_input_tokens: number2().int().positive().optional(),
25448
26088
  max_files: number2().int().positive().optional()
25449
26089
  });
26090
+ var RerankConfigSchema = object({
26091
+ backend: _enum(["off", "onnx", "remote", "synapse"]).optional(),
26092
+ model: string2().trim().min(1).optional(),
26093
+ endpoint: string2().trim().min(1).optional(),
26094
+ api_key_env: string2().trim().min(1).optional(),
26095
+ top_n: number2().int().positive().optional(),
26096
+ timeout_ms: number2().int().positive().optional()
26097
+ });
26098
+ var SearchConfigSchema = object({
26099
+ rerank: RerankConfigSchema.optional()
26100
+ });
25450
26101
  var LspExtensionSchema = string2().trim().min(1).refine((value) => value.replace(/^\.+/, "").length > 0, {
25451
26102
  message: "Extension must include characters other than leading dots"
25452
26103
  });
@@ -25510,6 +26161,10 @@ var SubcConfigSchema = object({
25510
26161
  var GhShimConfigSchema = object({
25511
26162
  binary_path: string2().trim().refine(isAbsolute7, "gh_shim.binary_path must be absolute").optional()
25512
26163
  });
26164
+ var OpenCodeHostConfigSchema = object({
26165
+ server_url: string2().optional(),
26166
+ server_password_env: string2().optional()
26167
+ });
25513
26168
  var GithubConfigSchema = object({
25514
26169
  shim: boolean2().optional(),
25515
26170
  read: boolean2().optional(),
@@ -25582,8 +26237,10 @@ var AftConfigFieldsSchema = object({
25582
26237
  lsp: LspConfigSchema.optional(),
25583
26238
  url_fetch_allow_private: boolean2().optional(),
25584
26239
  semantic: SemanticConfigSchema.optional(),
26240
+ search: SearchConfigSchema.optional(),
25585
26241
  bridge: BridgeConfigSchema.optional(),
25586
26242
  subc: SubcConfigSchema.optional(),
26243
+ opencode: OpenCodeHostConfigSchema.optional(),
25587
26244
  github: GithubConfigSchema.optional(),
25588
26245
  gh_shim: GhShimConfigSchema.optional(),
25589
26246
  git: GitConfigSchema.optional()
@@ -25729,7 +26386,7 @@ function migrateAftConfigFile2(configPath, logger = { log: log2, warn: warn2 })
25729
26386
  let tmpPath = null;
25730
26387
  let oldKeys = [];
25731
26388
  try {
25732
- const content = readFileSync9(configPath, "utf-8");
26389
+ const content = readFileSync10(configPath, "utf-8");
25733
26390
  const rawConfig = import_comment_json.parse(content);
25734
26391
  if (!rawConfig || typeof rawConfig !== "object" || Array.isArray(rawConfig)) {
25735
26392
  return { migrated: false, oldKeys: [] };
@@ -25764,6 +26421,18 @@ ${serialized}` : serialized;
25764
26421
  }
25765
26422
  }
25766
26423
  var configLoadErrors = [];
26424
+ var configValidationErrors = [];
26425
+ var configLoadSources = [];
26426
+ var configLoadTexts = new Map;
26427
+ function getConfigLoadSources() {
26428
+ return configLoadSources;
26429
+ }
26430
+ function getConfigLoadTexts() {
26431
+ return configLoadTexts;
26432
+ }
26433
+ function getConfigValidationErrors() {
26434
+ return configValidationErrors;
26435
+ }
25767
26436
  function getConfigLoadErrors() {
25768
26437
  return configLoadErrors;
25769
26438
  }
@@ -25808,7 +26477,9 @@ function loadConfigFromPath(configPath, tier) {
25808
26477
  try {
25809
26478
  if (!existsSync13(configPath))
25810
26479
  return null;
25811
- const content = readFileSync9(configPath, "utf-8");
26480
+ const content = readFileSync10(configPath, "utf-8");
26481
+ configLoadSources.push(configPath);
26482
+ configLoadTexts.set(configPath, content);
25812
26483
  const rawConfig = import_comment_json.parse(content);
25813
26484
  if (!rawConfig || typeof rawConfig !== "object" || Array.isArray(rawConfig)) {
25814
26485
  recordConfigParseFailure(configPath, "root must be an object");
@@ -25858,6 +26529,7 @@ function loadConfigFromPath(configPath, tier) {
25858
26529
  } else {
25859
26530
  const errorMsg = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
25860
26531
  warn2(`Config validation error in ${configPath}: ${errorMsg}`);
26532
+ configValidationErrors.push({ path: configPath, message: errorMsg });
25861
26533
  parsed = parseConfigPartially(cleanConfig);
25862
26534
  }
25863
26535
  if (tier === "user" && parsed.disabled_tools === undefined) {
@@ -26061,6 +26733,8 @@ function getStrippedTopLevelKeys(override) {
26061
26733
  stripped.push("sandbox.write_allow");
26062
26734
  if (override.subc !== undefined)
26063
26735
  stripped.push("subc");
26736
+ if (override.opencode !== undefined)
26737
+ stripped.push("opencode");
26064
26738
  if (override.github !== undefined)
26065
26739
  stripped.push("github");
26066
26740
  if (override.gh_shim !== undefined)
@@ -26068,14 +26742,33 @@ function getStrippedTopLevelKeys(override) {
26068
26742
  for (const tool of partitionProjectDisables(override.disabled_tools).ignored) {
26069
26743
  stripped.push(`disabled_tools.${tool}`);
26070
26744
  }
26745
+ stripped.push(...projectRerankStrippedKeys(override.search?.rerank));
26071
26746
  return stripped;
26072
26747
  }
26748
+ function projectRerankStrippedKeys(rerank) {
26749
+ if (rerank === undefined)
26750
+ return [];
26751
+ const stripped = [];
26752
+ if (rerank.backend !== undefined && rerank.backend !== "off")
26753
+ stripped.push("search.rerank.backend");
26754
+ for (const key of ["model", "endpoint", "api_key_env", "top_n", "timeout_ms"]) {
26755
+ if (rerank[key] !== undefined)
26756
+ stripped.push(`search.rerank.${key}`);
26757
+ }
26758
+ return stripped;
26759
+ }
26760
+ function mergeProjectSearchConfig(base, project) {
26761
+ if (project?.rerank?.backend !== "off")
26762
+ return base;
26763
+ return { ...base, rerank: { ...base?.rerank, backend: "off" } };
26764
+ }
26073
26765
  function mergeConfigs(base, override) {
26074
26766
  const disabledTools = unionDisabledTools(base.disabled_tools, partitionProjectDisables(override.disabled_tools).accepted);
26075
26767
  const indexes = mergeIndexes(base.indexes, override.indexes, true);
26076
26768
  const formatter = { ...base.formatter, ...override.formatter };
26077
26769
  const checker = { ...base.checker, ...override.checker };
26078
26770
  const semantic = mergeSemanticConfig(base.semantic, override.semantic);
26771
+ const search = mergeProjectSearchConfig(base.search, override.search);
26079
26772
  const lsp = mergeLspConfig(base.lsp, override.lsp);
26080
26773
  const experimental = mergeExperimentalConfig(base.experimental, override.experimental);
26081
26774
  const bash = mergeBashConfig(base.bash, override.bash);
@@ -26105,6 +26798,7 @@ function mergeConfigs(base, override) {
26105
26798
  ...pi !== undefined ? { pi } : {},
26106
26799
  experimental,
26107
26800
  semantic,
26801
+ search,
26108
26802
  ...bridge !== undefined ? { bridge } : {},
26109
26803
  ...indexes !== undefined ? { indexes } : {},
26110
26804
  ...disabledTools !== undefined ? { disabled_tools: disabledTools } : {}
@@ -26163,6 +26857,9 @@ function buildConfigTierConfigureParams(projectDirectory, processState = {}) {
26163
26857
  }
26164
26858
  function loadAftConfig(projectDirectory) {
26165
26859
  configLoadErrors = [];
26860
+ configValidationErrors = [];
26861
+ configLoadSources = [];
26862
+ configLoadTexts = new Map;
26166
26863
  configLoadNotices = [];
26167
26864
  semanticInputSupplied = false;
26168
26865
  const { userConfigPath, projectConfigPath } = resolveAftConfigPaths(projectDirectory);
@@ -26213,10 +26910,10 @@ import {
26213
26910
  createReadStream as createReadStream2,
26214
26911
  existsSync as existsSync15,
26215
26912
  mkdirSync as mkdirSync7,
26216
- readFileSync as readFileSync12,
26913
+ readFileSync as readFileSync13,
26217
26914
  renameSync as renameSync9,
26218
26915
  rmSync as rmSync5,
26219
- statSync as statSync10,
26916
+ statSync as statSync11,
26220
26917
  writeFileSync as writeFileSync8
26221
26918
  } from "node:fs";
26222
26919
  import { join as join17 } from "node:path";
@@ -26227,10 +26924,10 @@ import {
26227
26924
  closeSync as closeSync6,
26228
26925
  mkdirSync as mkdirSync6,
26229
26926
  openSync as openSync6,
26230
- readFileSync as readFileSync10,
26927
+ readFileSync as readFileSync11,
26231
26928
  renameSync as renameSync8,
26232
26929
  rmSync as rmSync4,
26233
- statSync as statSync9,
26930
+ statSync as statSync10,
26234
26931
  unlinkSync as unlinkSync6,
26235
26932
  writeFileSync as writeFileSync7
26236
26933
  } from "node:fs";
@@ -26253,7 +26950,7 @@ function lspBinDir(npmPackage) {
26253
26950
  function isInstalled(npmPackage, binary) {
26254
26951
  for (const candidate of lspBinaryCandidates(binary)) {
26255
26952
  try {
26256
- if (statSync9(join15(lspBinDir(npmPackage), candidate)).isFile())
26953
+ if (statSync10(join15(lspBinDir(npmPackage), candidate)).isFile())
26257
26954
  return true;
26258
26955
  } catch {}
26259
26956
  }
@@ -26281,9 +26978,9 @@ function writeInstalledMetaIn(installDir, version, sha256) {
26281
26978
  function readInstalledMetaIn(installDir) {
26282
26979
  const path = join15(installDir, INSTALLED_META_FILE);
26283
26980
  try {
26284
- if (!statSync9(path).isFile())
26981
+ if (!statSync10(path).isFile())
26285
26982
  return null;
26286
- const raw = readFileSync10(path, "utf8");
26983
+ const raw = readFileSync11(path, "utf8");
26287
26984
  const parsed = JSON.parse(raw);
26288
26985
  if (typeof parsed.version !== "string" || parsed.version.length === 0)
26289
26986
  return null;
@@ -26352,12 +27049,12 @@ ${token}
26352
27049
  let owningPid = null;
26353
27050
  let lockMtimeMs = 0;
26354
27051
  try {
26355
- const raw = readFileSync10(lock, "utf8");
27052
+ const raw = readFileSync11(lock, "utf8");
26356
27053
  const firstLine = raw.split(/\r?\n/, 1)[0]?.trim() ?? "";
26357
27054
  const parsed = Number.parseInt(firstLine, 10);
26358
27055
  if (Number.isFinite(parsed) && parsed > 0)
26359
27056
  owningPid = parsed;
26360
- lockMtimeMs = statSync9(lock).mtimeMs;
27057
+ lockMtimeMs = statSync10(lock).mtimeMs;
26361
27058
  } catch {
26362
27059
  return tryClaim();
26363
27060
  }
@@ -26391,7 +27088,7 @@ function releaseInstallLock(lockKey, acquisitionToken) {
26391
27088
  let owningPid = null;
26392
27089
  let owningToken = "";
26393
27090
  try {
26394
- const raw = readFileSync10(lock, "utf8");
27091
+ const raw = readFileSync11(lock, "utf8");
26395
27092
  const lines = raw.split(/\r?\n/);
26396
27093
  const parsed = Number.parseInt(lines[0]?.trim() ?? "", 10);
26397
27094
  if (Number.isFinite(parsed) && parsed > 0)
@@ -26438,7 +27135,7 @@ function autoInstallStampPath() {
26438
27135
  }
26439
27136
  function hasRecentAutoInstallStamp(path) {
26440
27137
  try {
26441
- const parsed = JSON.parse(readFileSync10(path, "utf8"));
27138
+ const parsed = JSON.parse(readFileSync11(path, "utf8"));
26442
27139
  return typeof parsed.lastRunMs === "number" && Number.isFinite(parsed.lastRunMs) && Math.abs(Date.now() - parsed.lastRunMs) < AUTO_INSTALL_WINDOW_MS;
26443
27140
  } catch {
26444
27141
  return false;
@@ -26446,7 +27143,7 @@ function hasRecentAutoInstallStamp(path) {
26446
27143
  }
26447
27144
  function readAutoInstallLockOwner(path) {
26448
27145
  try {
26449
- const parsed = JSON.parse(readFileSync10(path, "utf8"));
27146
+ const parsed = JSON.parse(readFileSync11(path, "utf8"));
26450
27147
  if (typeof parsed.pid !== "number" || typeof parsed.startedMs !== "number" || typeof parsed.token !== "string") {
26451
27148
  return null;
26452
27149
  }
@@ -26459,7 +27156,7 @@ function autoInstallLockIsStale(path) {
26459
27156
  const owner = readAutoInstallLockOwner(path);
26460
27157
  let age = Number.POSITIVE_INFINITY;
26461
27158
  try {
26462
- age = Math.abs(Date.now() - statSync9(path).mtimeMs);
27159
+ age = Math.abs(Date.now() - statSync10(path).mtimeMs);
26463
27160
  } catch {
26464
27161
  return true;
26465
27162
  }
@@ -26537,7 +27234,7 @@ var VERSION_CHECK_FILE = ".aft-version-check";
26537
27234
  function readVersionCheck(npmPackage) {
26538
27235
  const file = join15(lspPackageDir(npmPackage), VERSION_CHECK_FILE);
26539
27236
  try {
26540
- const raw = readFileSync10(file, "utf8");
27237
+ const raw = readFileSync11(file, "utf8");
26541
27238
  const parsed = JSON.parse(raw);
26542
27239
  if (typeof parsed.last_checked === "string") {
26543
27240
  return {
@@ -26640,6 +27337,14 @@ var NPM_LSP_TABLE = [
26640
27337
  extensions: ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"],
26641
27338
  rootMarkers: ["tsconfig.json", "jsconfig.json", "package.json"]
26642
27339
  },
27340
+ {
27341
+ id: "typescript-sdk",
27342
+ npm: "typescript",
27343
+ binary: "tsserver",
27344
+ extensions: ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"],
27345
+ rootMarkers: ["tsconfig.json", "jsconfig.json", "package.json"],
27346
+ supportedMajor: 5
27347
+ },
26643
27348
  {
26644
27349
  id: "python",
26645
27350
  npm: "pyright",
@@ -26712,9 +27417,18 @@ var NPM_LSP_TABLE = [
26712
27417
  rootMarkers: ["biome.json", "biome.jsonc"]
26713
27418
  }
26714
27419
  ];
27420
+ function majorVersionOf(version) {
27421
+ const match = /^v?(\d+)(?:\.|$)/.exec(version.trim());
27422
+ return match ? Number(match[1]) : null;
27423
+ }
27424
+ function isVersionSupported(spec, version) {
27425
+ if (spec.supportedMajor === undefined)
27426
+ return true;
27427
+ return majorVersionOf(version) === spec.supportedMajor;
27428
+ }
26715
27429
 
26716
27430
  // src/lsp-project-relevance.ts
26717
- import { existsSync as existsSync14, readdirSync as readdirSync4, readFileSync as readFileSync11 } from "node:fs";
27431
+ import { existsSync as existsSync14, readdirSync as readdirSync4, readFileSync as readFileSync12 } from "node:fs";
26718
27432
  import { join as join16 } from "node:path";
26719
27433
  var MAX_WALK_DIRS = 200;
26720
27434
  var MAX_WALK_DEPTH = 4;
@@ -26756,7 +27470,7 @@ function hasPackageJsonDep(projectRoot, depNames) {
26756
27470
  }
26757
27471
  function readPackageJson(projectRoot) {
26758
27472
  try {
26759
- const raw = readFileSync11(join16(projectRoot, "package.json"), "utf8");
27473
+ const raw = readFileSync12(join16(projectRoot, "package.json"), "utf8");
26760
27474
  const parsed = JSON.parse(raw);
26761
27475
  if (typeof parsed !== "object" || parsed === null)
26762
27476
  return null;
@@ -26808,10 +27522,11 @@ function extensionOf(fileName) {
26808
27522
 
26809
27523
  // src/lsp-registry-probe.ts
26810
27524
  var NPM_REGISTRY_BASE = "https://registry.npmjs.org";
26811
- function pickEligibleVersion(response, graceDays, now = Date.now()) {
27525
+ function pickEligibleVersion(response, graceDays, now = Date.now(), accept = () => true) {
26812
27526
  const times = response.time || {};
26813
27527
  const cutoff = now - graceDays * 24 * 60 * 60 * 1000;
26814
27528
  const candidates = [];
27529
+ let releaseCount = 0;
26815
27530
  for (const [version, publishedAt] of Object.entries(times)) {
26816
27531
  if (version === "created" || version === "modified")
26817
27532
  continue;
@@ -26822,6 +27537,9 @@ function pickEligibleVersion(response, graceDays, now = Date.now()) {
26822
27537
  const ts = Date.parse(publishedAt);
26823
27538
  if (Number.isNaN(ts))
26824
27539
  continue;
27540
+ releaseCount += 1;
27541
+ if (!accept(version))
27542
+ continue;
26825
27543
  candidates.push({ version, publishedAt, ts });
26826
27544
  }
26827
27545
  candidates.sort((a, b) => b.ts - a.ts);
@@ -26830,10 +27548,11 @@ function pickEligibleVersion(response, graceDays, now = Date.now()) {
26830
27548
  return {
26831
27549
  version: eligible[0]?.version ?? null,
26832
27550
  blockedByGrace,
27551
+ outsideSupportedRange: releaseCount > 0 && candidates.length === 0,
26833
27552
  eligible: eligible.map(({ version, publishedAt }) => ({ version, publishedAt }))
26834
27553
  };
26835
27554
  }
26836
- async function probeRegistry(npmPackage, graceDays, fetchImpl = fetch) {
27555
+ async function probeRegistry(npmPackage, graceDays, fetchImpl = fetch, accept) {
26837
27556
  const encoded = encodeURIComponent(npmPackage).replace(/^%40/, "@");
26838
27557
  const url = `${NPM_REGISTRY_BASE}/${encoded}`;
26839
27558
  try {
@@ -26846,7 +27565,7 @@ async function probeRegistry(npmPackage, graceDays, fetchImpl = fetch) {
26846
27565
  return null;
26847
27566
  }
26848
27567
  const json = await res.json();
26849
- return pickEligibleVersion(json, graceDays);
27568
+ return pickEligibleVersion(json, graceDays, Date.now(), accept);
26850
27569
  } catch (err) {
26851
27570
  warn2(`[lsp] registry probe failed for ${npmPackage}: ${err}`);
26852
27571
  return null;
@@ -26897,11 +27616,11 @@ async function resolveTargetVersion(spec, config, fetchImpl = fetch) {
26897
27616
  }
26898
27617
  const cached = readVersionCheck(spec.npm);
26899
27618
  const weeklyMs = config.graceDays * 24 * 60 * 60 * 1000;
26900
- const cachedSafe = isSafeVersion(cached?.latest_eligible ?? null);
27619
+ const cachedSafe = isSafeVersion(cached?.latest_eligible ?? null) && isVersionSupported(spec, cached?.latest_eligible);
26901
27620
  if (cached && !shouldRecheckVersion(cached, weeklyMs) && cachedSafe) {
26902
27621
  return { version: cached.latest_eligible, pinned: false, probe: null };
26903
27622
  }
26904
- const probe = await probeRegistry(spec.npm, config.graceDays, fetchImpl);
27623
+ const probe = await probeRegistry(spec.npm, config.graceDays, fetchImpl, (candidate) => isVersionSupported(spec, candidate));
26905
27624
  if (!probe) {
26906
27625
  return {
26907
27626
  version: cachedSafe ? cached?.latest_eligible ?? null : null,
@@ -27036,11 +27755,16 @@ async function ensureServerInstalled(spec, config, fetchImpl, signal) {
27036
27755
  const { version, probe } = await resolveTargetVersion(spec, config, fetchImpl);
27037
27756
  if (!version) {
27038
27757
  const installed = isInstalled(spec.npm, spec.binary);
27039
- if (installed) {
27758
+ if (installed && installedVersionAllowed(spec, config)) {
27040
27759
  warn2(`[lsp] no eligible version of ${spec.npm} (grace=${config.graceDays}d); keeping existing install`);
27041
27760
  return { started: false, reason: "kept existing install" };
27042
27761
  }
27043
- const blocked = probe?.blockedByGrace ? `all versions are within ${config.graceDays}-day grace window` : "registry probe failed";
27762
+ const blocked = probe?.blockedByGrace ? `all versions are within ${config.graceDays}-day grace window` : probe?.outsideSupportedRange ? `no ${spec.supportedMajor}.x release on the registry` : "registry probe failed";
27763
+ if (installed) {
27764
+ const reason = `cached ${spec.npm} ${readInstalledMeta(spec.npm)?.version ?? "(unknown version)"} is outside the supported ${spec.supportedMajor}.x line and no replacement is available: ${blocked}`;
27765
+ warn2(`[lsp] ${reason}`);
27766
+ return { started: false, reason };
27767
+ }
27044
27768
  warn2(`[lsp] skipping ${spec.npm}: ${blocked}`);
27045
27769
  return { started: false, reason: blocked };
27046
27770
  }
@@ -27104,7 +27828,7 @@ function hashInstalledBinary(spec) {
27104
27828
  let pathToHash = null;
27105
27829
  for (const p of candidates) {
27106
27830
  try {
27107
- if (statSync10(p).isFile()) {
27831
+ if (statSync11(p).isFile()) {
27108
27832
  pathToHash = p;
27109
27833
  break;
27110
27834
  }
@@ -27130,14 +27854,14 @@ function installedBinaryPath(spec) {
27130
27854
  ] : [lspBinaryPath(spec.npm, spec.binary)];
27131
27855
  for (const candidate of candidates) {
27132
27856
  try {
27133
- if (statSync10(candidate).isFile())
27857
+ if (statSync11(candidate).isFile())
27134
27858
  return candidate;
27135
27859
  } catch {}
27136
27860
  }
27137
27861
  return null;
27138
27862
  }
27139
27863
  function sha256OfFileSync(path) {
27140
- return createHash5("sha256").update(readFileSync12(path)).digest("hex");
27864
+ return createHash5("sha256").update(readFileSync13(path)).digest("hex");
27141
27865
  }
27142
27866
  function quarantineCachedNpmInstall(spec, reason) {
27143
27867
  const packageDir = lspPackageDir(spec.npm);
@@ -27165,6 +27889,15 @@ function validateCachedNpmInstall(spec) {
27165
27889
  }
27166
27890
  return true;
27167
27891
  }
27892
+ function installedVersionAllowed(spec, config) {
27893
+ const installed = readInstalledMeta(spec.npm)?.version;
27894
+ if (!installed)
27895
+ return spec.supportedMajor === undefined;
27896
+ return isVersionSupported(spec, installed) || config.versions[spec.npm] === installed;
27897
+ }
27898
+ function cachedInstallUsable(spec, config) {
27899
+ return isInstalled(spec.npm, spec.binary) && validateCachedNpmInstall(spec) && installedVersionAllowed(spec, config);
27900
+ }
27168
27901
  function runAutoInstall(projectRoot, config, fetchImpl = fetch) {
27169
27902
  const cachedBinDirs = [];
27170
27903
  const skipped = [];
@@ -27177,10 +27910,10 @@ function runAutoInstall(projectRoot, config, fetchImpl = fetch) {
27177
27910
  return projectExtensions;
27178
27911
  };
27179
27912
  for (const spec of NPM_LSP_TABLE) {
27180
- if (isInstalled(spec.npm, spec.binary) && validateCachedNpmInstall(spec)) {
27913
+ if (cachedInstallUsable(spec, config)) {
27181
27914
  cachedBinDirs.push(lspBinDir(spec.npm));
27182
27915
  }
27183
- if (config.disabled.has(spec.id)) {
27916
+ if (config.disabled.has(spec.id) || spec.id === "typescript-sdk" && config.disabled.has("typescript")) {
27184
27917
  skipped.push({ id: spec.id, reason: "disabled by config" });
27185
27918
  continue;
27186
27919
  }
@@ -27216,7 +27949,7 @@ function runAutoInstall(projectRoot, config, fetchImpl = fetch) {
27216
27949
  },
27217
27950
  skipped,
27218
27951
  installsComplete: Promise.all(installPromises).then(() => {}),
27219
- getCachedBinDirs: () => NPM_LSP_TABLE.filter((spec) => isInstalled(spec.npm, spec.binary) && validateCachedNpmInstall(spec)).map((spec) => lspBinDir(spec.npm))
27952
+ getCachedBinDirs: () => NPM_LSP_TABLE.filter((spec) => cachedInstallUsable(spec, config)).map((spec) => lspBinDir(spec.npm))
27220
27953
  };
27221
27954
  }
27222
27955
  async function pushLspPathsAfterAutoInstall(pool, projectRoot, cachedBinDirs) {
@@ -27235,16 +27968,16 @@ import {
27235
27968
  lstatSync as lstatSync2,
27236
27969
  mkdirSync as mkdirSync8,
27237
27970
  readdirSync as readdirSync5,
27238
- readFileSync as readFileSync13,
27971
+ readFileSync as readFileSync14,
27239
27972
  readlinkSync as readlinkSync2,
27240
27973
  realpathSync as realpathSync4,
27241
27974
  renameSync as renameSync10,
27242
27975
  rmSync as rmSync6,
27243
- statSync as statSync11,
27976
+ statSync as statSync12,
27244
27977
  unlinkSync as unlinkSync7,
27245
27978
  writeFileSync as writeFileSync9
27246
27979
  } from "node:fs";
27247
- import { dirname as dirname8, join as join18, relative as relative3, resolve as resolve9 } from "node:path";
27980
+ import { dirname as dirname9, join as join18, relative as relative3, resolve as resolve9 } from "node:path";
27248
27981
  import { Readable as Readable3 } from "node:stream";
27249
27982
  import { pipeline as pipeline3 } from "node:stream/promises";
27250
27983
 
@@ -27353,7 +28086,7 @@ function installFailureMarkerPath(spec) {
27353
28086
  }
27354
28087
  function readInstallFailureMarker(spec) {
27355
28088
  try {
27356
- const marker = JSON.parse(readFileSync13(installFailureMarkerPath(spec), "utf8"));
28089
+ const marker = JSON.parse(readFileSync14(installFailureMarkerPath(spec), "utf8"));
27357
28090
  if (typeof marker.release !== "string" || typeof marker.sha256 !== "string" || typeof marker.error !== "string" || typeof marker.at !== "string" || !Number.isFinite(Date.parse(marker.at))) {
27358
28091
  return null;
27359
28092
  }
@@ -27367,7 +28100,7 @@ function writeInstallFailureMarker(spec, release, sha256, installError) {
27367
28100
  const markerError = installError.replace(/\s+/g, " ").trim();
27368
28101
  const tempPath = `${path}.tmp-${process.pid}-${randomBytes3(6).toString("hex")}`;
27369
28102
  try {
27370
- mkdirSync8(dirname8(path), { recursive: true });
28103
+ mkdirSync8(dirname9(path), { recursive: true });
27371
28104
  writeFileSync9(tempPath, `${JSON.stringify({ release, sha256, error: markerError, at: new Date().toISOString() })}
27372
28105
  `, { encoding: "utf8", mode: 384 });
27373
28106
  renameSync10(tempPath, path);
@@ -27396,7 +28129,7 @@ function ghBinaryPath(spec, platform) {
27396
28129
  function isGithubInstalled(spec, platform) {
27397
28130
  for (const candidate of ghBinaryCandidates(spec, platform)) {
27398
28131
  try {
27399
- if (statSync11(join18(ghBinDir(spec), candidate)).isFile())
28132
+ if (statSync12(join18(ghBinDir(spec), candidate)).isFile())
27400
28133
  return true;
27401
28134
  } catch {}
27402
28135
  }
@@ -27410,9 +28143,9 @@ function ghBinaryCandidates(spec, platform) {
27410
28143
  function readGithubInstalledMetaIn(installDir) {
27411
28144
  try {
27412
28145
  const path = join18(installDir, INSTALLED_META_FILE2);
27413
- if (!statSync11(path).isFile())
28146
+ if (!statSync12(path).isFile())
27414
28147
  return null;
27415
- const parsed = JSON.parse(readFileSync13(path, "utf8"));
28148
+ const parsed = JSON.parse(readFileSync14(path, "utf8"));
27416
28149
  if (typeof parsed.version !== "string" || parsed.version.length === 0)
27417
28150
  return null;
27418
28151
  return {
@@ -27453,7 +28186,7 @@ function sha256OfFile(path) {
27453
28186
  });
27454
28187
  }
27455
28188
  function sha256OfFileSync2(path) {
27456
- return createHash6("sha256").update(readFileSync13(path)).digest("hex");
28189
+ return createHash6("sha256").update(readFileSync14(path)).digest("hex");
27457
28190
  }
27458
28191
  async function fetchReleaseByTag(githubRepo, tag, fetchImpl, signal) {
27459
28192
  const candidates = [];
@@ -27629,7 +28362,7 @@ async function downloadFile(url, destPath, fetchImpl, assetSize, signal) {
27629
28362
  if (Number.isFinite(advertised) && advertised > MAX_DOWNLOAD_BYTES3) {
27630
28363
  throw new Error(`Content-Length ${advertised} exceeds max ${MAX_DOWNLOAD_BYTES3}`);
27631
28364
  }
27632
- mkdirSync8(dirname8(destPath), { recursive: true });
28365
+ mkdirSync8(dirname9(destPath), { recursive: true });
27633
28366
  let bytesWritten = 0;
27634
28367
  const guard = new TransformStream({
27635
28368
  transform(chunk, controller) {
@@ -27799,7 +28532,7 @@ function quarantineCachedGithubInstall(spec, reason) {
27799
28532
  const dest = join18(ghCacheRoot(), ".quarantine", encodeURIComponent(spec.id), `${Date.now()}`);
27800
28533
  warn2(`[lsp] tofu_mismatch ${spec.id}: ${reason}; quarantining ${packageDir} -> ${dest}`);
27801
28534
  try {
27802
- mkdirSync8(dirname8(dest), { recursive: true });
28535
+ mkdirSync8(dirname9(dest), { recursive: true });
27803
28536
  rmSync6(dest, { recursive: true, force: true });
27804
28537
  renameSync10(packageDir, dest);
27805
28538
  } catch (err) {
@@ -27811,7 +28544,7 @@ function validateCachedGithubInstall(spec, platform) {
27811
28544
  const meta = readGithubInstalledMetaIn(packageDir);
27812
28545
  const binaryPath = ghBinaryCandidates(spec, platform).map((candidate) => join18(ghBinDir(spec), candidate)).find((candidate) => {
27813
28546
  try {
27814
- return statSync11(candidate).isFile();
28547
+ return statSync12(candidate).isFile();
27815
28548
  } catch {
27816
28549
  return false;
27817
28550
  }
@@ -27929,7 +28662,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
27929
28662
  return { ok: false, sha256: archiveSha256, error: message };
27930
28663
  }
27931
28664
  const targetBinary = ghBinaryPath(spec, platform);
27932
- mkdirSync8(dirname8(targetBinary), { recursive: true });
28665
+ mkdirSync8(dirname9(targetBinary), { recursive: true });
27933
28666
  try {
27934
28667
  copyFileSync3(innerBinaryPath, targetBinary);
27935
28668
  if (platform !== "win32") {
@@ -28347,6 +29080,23 @@ import {
28347
29080
  import { Container as Container2, Spacer as Spacer2, Text as Text2 } from "@earendil-works/pi-tui";
28348
29081
  import { Type as Type2 } from "typebox";
28349
29082
 
29083
+ // src/hashline-tag-sources.ts
29084
+ function hashlineTagSourceSentence(bashRewritesMintTags, isRegistered = () => true) {
29085
+ const rewrites = bashRewritesMintTags ? " (and accepted AFT `cat`/`head`/`tail` rewrites)" : "";
29086
+ const lookAlikes = [
29087
+ ...["aft_zoom", "aft_outline"].filter(isRegistered),
29088
+ "grep",
29089
+ ...isRegistered("aft_search") ? ["aft_search"] : []
29090
+ ].map((name) => `\`${name}\``);
29091
+ lookAlikes.push("conflict snippets");
29092
+ return `Only \`read\`${rewrites} mint hashline tags. ${joinList(lookAlikes)} do not. After navigation, call \`read\` on every file and range the patch addresses.`;
29093
+ }
29094
+ function joinList(items) {
29095
+ if (items.length <= 2)
29096
+ return items.join(" and ");
29097
+ return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
29098
+ }
29099
+
28350
29100
  // src/tools/diff-format.ts
28351
29101
  import { diffLines } from "diff";
28352
29102
 
@@ -28694,7 +29444,7 @@ var ISSUE_AND_PR_READ_DESCRIPTION = "GitHub issues and pull requests can be read
28694
29444
  function whenGhReadEnabled(enabled, description) {
28695
29445
  return enabled ? description : "";
28696
29446
  }
28697
- var READ_DESCRIPTION = "Read file contents with line numbers. Backed by AFT's indexed Rust reader — faster than the built-in `read` on large repos. Images are returned as attachments on vision-capable models; PDFs and non-vision models are not yet supported.";
29447
+ var READ_DESCRIPTION = "Read file contents with line numbers or sorted directory entries. Use startLine/endLine or 1-based offset/limit for either. Directory limit defaults to and is capped at 1000; enumeration stops at 10,000 entries and partial listings carry a shown/total trailer. Backed by AFT's indexed Rust reader — faster than the built-in `read` on large repos. Images are returned as attachments on vision-capable models; PDFs and non-vision models are not yet supported.";
28698
29448
  function readDescription(ghReadEnabled) {
28699
29449
  const githubDescription = whenGhReadEnabled(ghReadEnabled, ISSUE_AND_PR_READ_DESCRIPTION);
28700
29450
  return githubDescription ? `${READ_DESCRIPTION} ${githubDescription}` : READ_DESCRIPTION;
@@ -28790,18 +29540,20 @@ async function assertExternalDirectoryPermission(extCtx, target, options = {}) {
28790
29540
  }
28791
29541
  var ReadParams = Type2.Object({
28792
29542
  path: Type2.String({
28793
- description: "Path to the file to read (absolute or relative to project root)"
29543
+ description: "Path to the file or directory to read (absolute or relative to project root)"
28794
29544
  }),
28795
- startLine: optionalInt(1, Number.MAX_SAFE_INTEGER, "1-based line to start reading from"),
28796
- endLine: optionalInt(1, Number.MAX_SAFE_INTEGER, "1-based line to stop reading at (inclusive)"),
28797
- limit: optionalInt(1, Number.MAX_SAFE_INTEGER, "Maximum number of lines to return"),
28798
- offset: optionalInt(1, Number.MAX_SAFE_INTEGER, "1-based line number to start reading from (use with limit)")
29545
+ startLine: optionalInt(1, Number.MAX_SAFE_INTEGER, "1-based line or directory entry to start reading from"),
29546
+ endLine: optionalInt(1, Number.MAX_SAFE_INTEGER, "1-based line or directory entry to stop reading at (inclusive)"),
29547
+ limit: optionalInt(1, Number.MAX_SAFE_INTEGER, "Max lines (default: 2000) or directory entries (default and cap: 1000) to return"),
29548
+ offset: optionalInt(1, Number.MAX_SAFE_INTEGER, "1-based line or directory entry to start reading from (use with limit). Ignored if startLine is provided")
28799
29549
  });
28800
29550
  var WriteParams = Type2.Object({
28801
29551
  path: Type2.String({
28802
29552
  description: "Path to the file to write (absolute or relative to project root)"
28803
29553
  }),
28804
- content: Type2.String({ description: "Full file contents to write" })
29554
+ content: Type2.String({
29555
+ description: "Full file contents to write; for issue://N / pr://N, the comment body to post"
29556
+ })
28805
29557
  });
28806
29558
  var BatchEditParams = Type2.Object({
28807
29559
  oldString: Type2.Optional(Type2.String({ description: "Text to find for a batch find/replace edit" })),
@@ -28825,14 +29577,14 @@ var EditParams = Type2.Object({
28825
29577
  description: "Replacement content for symbol mode; only meaningful together with `symbol`. A call carrying one without the other is incomplete. For whole-file writes, use the `write` tool."
28826
29578
  })),
28827
29579
  appendContent: Type2.Optional(Type2.String({
28828
- description: "Append text to the end of the file (creates the file if missing, parent dirs auto-created). When set, other edit modes are ignored."
29580
+ description: "Append text to the end of the file (creates the file if missing, parent dirs auto-created). Mixing with other edit modes is rejected."
28829
29581
  })),
28830
29582
  edits: Type2.Optional(Type2.Array(BatchEditParams, {
28831
29583
  minItems: 1,
28832
29584
  description: "Batch edits — non-empty array of { oldString, newString }, { oldString, newString, replaceAll: true }, or { startLine, endLine, content } objects applied atomically."
28833
29585
  }))
28834
29586
  });
28835
- var HASHLINE_EDIT_DESCRIPTION = [
29587
+ var HASHLINE_EDIT_GRAMMAR = [
28836
29588
  "Apply a hashline patch. Arguments are exactly `{patch}` where `patch` is a non-empty string. Server-owned preview control is outside this schema.",
28837
29589
  "",
28838
29590
  "Quick reference:",
@@ -28840,10 +29592,15 @@ var HASHLINE_EDIT_DESCRIPTION = [
28840
29592
  "- Same canonical path: multiple sections compose in patch order against pre-request coordinates.",
28841
29593
  "- Addresses: `0` (BOF), `N` (one line), `N.=M` (range; `N..=M`/`N..M` also work), `<N`/`>N` (gap before/after), `N*`/`<N*`/`>N*` (block), and `$`/`$-K` (EOF-relative). A plain `N` PUT replaces; use `<N` or `>N` to insert.",
28842
29594
  "- PUT text: `PUT <address>:` followed by one or more `+` body rows (`+` alone is blank). A final patch newline is allowed. PUT without `:` copies `@name` (or the anonymous register) and takes no body; names use `@` plus ASCII letters, digits, `_`, or `-`.",
28843
- "- CUT: `CUT <address> [@name]`. REM: bare `REM` only, removing the whole file. MV: `MV <destination>` (one whitespace-free path, optional matching quotes), once and after any line operations. `*** Begin Patch`/`*** End Patch` is an optional envelope.",
28844
- "- Only `read` (and accepted AFT `cat`/`head`/`tail` rewrites) mint hashline tags. `aft_zoom`, `aft_outline`, `grep`, `aft_search`, and conflict snippets do not. After navigation, call `read` on every file and range the patch addresses."
29595
+ "- CUT: `CUT <address> [@name]`. REM: bare `REM` only, removing the whole file. MV: `MV <destination>` (one whitespace-free path, optional matching quotes), once and after any line operations. `*** Begin Patch`/`*** End Patch` is an optional envelope."
28845
29596
  ].join(`
28846
29597
  `);
29598
+ function hashlineEditDescription(config) {
29599
+ const bashRewrites = toolEnabled(config, "bash") && resolveBashConfig(config).rewrite;
29600
+ const tagSources = hashlineTagSourceSentence(bashRewrites, (name) => toolEnabled(config, name));
29601
+ return `${HASHLINE_EDIT_GRAMMAR}
29602
+ - ${tagSources}`;
29603
+ }
28847
29604
  var HashlineEditParams = Type2.Object({
28848
29605
  patch: Type2.String({
28849
29606
  minLength: 1,
@@ -28930,7 +29687,7 @@ function registerHoistedTools(pi, ctx, surface) {
28930
29687
  name: readName,
28931
29688
  label: readName,
28932
29689
  description: readDescription(resolveGithubConfig(ctx.config).read),
28933
- promptSnippet: "Read file contents (supports offset/limit for large files)",
29690
+ promptSnippet: "Read files or directories (supports offset/limit for either)",
28934
29691
  promptGuidelines: [`Use ${readName} to examine files instead of cat or sed.`],
28935
29692
  parameters: ReadParams,
28936
29693
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
@@ -29057,7 +29814,7 @@ PDFs aren't supported on the Pi harness yet.`, response);
29057
29814
  pi.registerTool({
29058
29815
  name: editName,
29059
29816
  label: editName,
29060
- description: HASHLINE_EDIT_DESCRIPTION,
29817
+ description: hashlineEditDescription(ctx.config),
29061
29818
  promptSnippet: `Apply a tagged hashline patch through the ${editName} tool`,
29062
29819
  promptGuidelines: [
29063
29820
  "Read a file to obtain its current hashline tag before editing it.",
@@ -29115,8 +29872,7 @@ PDFs aren't supported on the Pi harness yet.`, response);
29115
29872
  }
29116
29873
  if (isGithubResourcePath(filePathArg)) {
29117
29874
  const edits = params.edits;
29118
- const onlyFindReplace = Array.isArray(edits) && edits.length > 0 && params.appendContent === undefined && params.symbol === undefined && params.content === undefined && edits.every((entry) => typeof entry.oldString === "string" && (entry.newString === undefined || typeof entry.newString === "string") && entry.startLine === undefined && entry.endLine === undefined && entry.content === undefined);
29119
- if (!onlyFindReplace) {
29875
+ if (!isFindReplaceOnlyEdit(argsRecord)) {
29120
29876
  throw new Error("edit: GitHub resources support only edits[] find/replace entries with oldString and optional newString");
29121
29877
  }
29122
29878
  const bridge = bridgeFor(ctx, extCtx.cwd);
@@ -29644,34 +30400,17 @@ function registerAstTools(pi, ctx, surface) {
29644
30400
  // src/tools/bash.ts
29645
30401
  import { Container as Container3, Spacer as Spacer3, Text as Text3 } from "@earendil-works/pi-tui";
29646
30402
  import { Type as Type4 } from "typebox";
29647
-
29648
- // src/session-kind.ts
29649
- var MAGIC_CONTEXT_SUBAGENT_ENV = "MAGIC_CONTEXT_PI_SUBAGENT";
29650
- function piWorkerKind(extCtx, env = process.env) {
29651
- if (env[MAGIC_CONTEXT_SUBAGENT_ENV] === "1")
29652
- return "delegated";
29653
- if (extCtx?.hasUI === false)
29654
- return "headless";
29655
- return;
29656
- }
29657
- function isPiWorkerSession(extCtx, env = process.env) {
29658
- return piWorkerKind(extCtx, env) !== undefined;
29659
- }
29660
- function skipsEagerStartup(env = process.env) {
29661
- return piWorkerKind(undefined, env) === "delegated";
29662
- }
29663
-
29664
- // src/tools/bash.ts
29665
- var BASH_WAIT_POLL_INTERVAL_MS = 100;
29666
30403
  var REGEX_WAIT_SCAN_WINDOW_BYTES = 64 * 1024;
29667
30404
  function watchCallerRole(extCtx, env = process.env) {
29668
30405
  return isPiWorkerSession(extCtx, env) ? "worker" : "primary";
29669
30406
  }
29670
- function coerceConfiguredWatchTimeout(value, cap) {
30407
+ function coerceConfiguredWatchTimeout(value, role, cap) {
29671
30408
  try {
29672
- return coerceOptionalInt(value, "timeoutMs", 1, cap);
30409
+ return coerceOptionalInt(value, "timeoutMs", 1, maxWatchTimeoutMs(role, cap));
29673
30410
  } catch (error) {
29674
30411
  const message = error instanceof Error ? error.message : String(error);
30412
+ if (role === "worker")
30413
+ throw error;
29675
30414
  throw new Error(`${message} (bash.watch_sync_max_ms)`);
29676
30415
  }
29677
30416
  }
@@ -29687,7 +30426,9 @@ function resolveForegroundWaitMs(configured) {
29687
30426
  var BASH_TRANSPORT_TIMEOUT_MS2 = 30000;
29688
30427
  var DEFAULT_HARD_TIMEOUT_MS = 30 * 60 * 1000;
29689
30428
  var BASH_TRANSPORT_MARGIN_MS = 1e4;
29690
- function orchestratedTransportTimeoutMs(blockToCompletion, wait, effectiveTimeout, foregroundWaitMs) {
30429
+ function orchestratedTransportTimeoutMs(blockToCompletion, wait, effectiveTimeout, foregroundWaitMs, noHardKill = false) {
30430
+ if (noHardKill)
30431
+ return LONGEST_TIMER_DELAY_MS;
29691
30432
  const waitBudget = blockToCompletion || wait ? effectiveTimeout ?? DEFAULT_HARD_TIMEOUT_MS : foregroundWaitMs;
29692
30433
  return waitBudget + BASH_TRANSPORT_MARGIN_MS;
29693
30434
  }
@@ -29701,10 +30442,14 @@ var BashBaseParams = {
29701
30442
  })),
29702
30443
  description: Type4.Optional(Type4.String({
29703
30444
  description: "Human-readable description shown in UI logs. Helps users understand what the command does without reading shell syntax."
29704
- })),
30445
+ }))
30446
+ };
30447
+ var BashWaitParam = {
29705
30448
  wait: Type4.Optional(Type4.Boolean({
29706
30449
  description: "When true, run in the foreground without auto-promoting and wait until the command finishes or reaches its timeout; any new message detaches by default, while `bash.detach_on_user_message: false` keeps it blocking unless the message contains the literal `&detach`. The token is stripped before delivery; the rest of the message is preserved, and a token-only message becomes `(requested background detach)`. Use only when you know the result is required before doing anything else."
29707
- })),
30450
+ }))
30451
+ };
30452
+ var BashSandboxParam = {
29708
30453
  sandbox: Type4.Optional(Type4.Literal("host", {
29709
30454
  description: "Request one-command approval to run unsandboxed on the host; use only when native sandboxing blocks required work, and note that it is a no-op when sandboxing is disabled."
29710
30455
  }))
@@ -29728,16 +30473,65 @@ var BashPtyParams = {
29728
30473
  };
29729
30474
  var BashParams = Type4.Object({
29730
30475
  ...BashBaseParams,
30476
+ ...BashWaitParam,
30477
+ ...BashSandboxParam,
29731
30478
  ...BashBackgroundFlagParam,
29732
30479
  ...BashCompressionParam,
29733
30480
  ...BashPtyParams
29734
30481
  });
29735
- var BashForegroundOnlyParams = Type4.Object({
29736
- ...BashBaseParams,
29737
- ...BashCompressionParam
29738
- });
29739
- function bashParamsForConfig(backgroundEnabled) {
29740
- return backgroundEnabled ? BashParams : BashForegroundOnlyParams;
30482
+ function bashParamsForConfig(features, companions) {
30483
+ return Type4.Object({
30484
+ ...BashBaseParams,
30485
+ ...features.background ? BashWaitParam : {},
30486
+ ...features.sandbox ? BashSandboxParam : {},
30487
+ ...features.background ? {
30488
+ background: Type4.Optional(Type4.Boolean({ description: backgroundParamDescription(companions) }))
30489
+ } : {},
30490
+ ...features.compress ? BashCompressionParam : {},
30491
+ ...features.background ? {
30492
+ ...BashPtyParams,
30493
+ pty: Type4.Optional(Type4.Boolean({
30494
+ description: ptyParamDescription(companions, features.subagentBackground)
30495
+ }))
30496
+ } : {}
30497
+ });
30498
+ }
30499
+ function bashCompanionRegistered(config, companion) {
30500
+ return resolveBashConfig(config).background && toolEnabled(config, companion);
30501
+ }
30502
+ function registeredCompanions(config) {
30503
+ return {
30504
+ status: bashCompanionRegistered(config, "bash_status"),
30505
+ watch: bashCompanionRegistered(config, "bash_watch"),
30506
+ write: bashCompanionRegistered(config, "bash_write"),
30507
+ kill: bashCompanionRegistered(config, "bash_kill")
30508
+ };
30509
+ }
30510
+ function backgroundParamDescription(c) {
30511
+ let controls = "";
30512
+ if (c.watch) {
30513
+ controls += ` Use bash_watch to wait for completion or output patterns${c.status ? "; bash_status for a one-shot snapshot only." : "."}`;
30514
+ } else if (c.status) {
30515
+ controls += " Use bash_status for a one-shot snapshot.";
30516
+ }
30517
+ if (c.kill)
30518
+ controls += " Use bash_kill to terminate.";
30519
+ return `Spawn command in background and return immediately with a task_id.${controls} Ideal for long-running tasks like builds or dev servers.`;
30520
+ }
30521
+ function ptyParamDescription(c, subagentBackground) {
30522
+ let drive = "";
30523
+ if (c.status && c.write) {
30524
+ drive = ' Inspect with bash_status({ task_id, output_mode: "screen" }) and send input with bash_write.';
30525
+ } else if (c.status) {
30526
+ drive = ' Inspect with bash_status({ task_id, output_mode: "screen" }).';
30527
+ } else if (c.write) {
30528
+ drive = " Send input with bash_write.";
30529
+ }
30530
+ const workers = subagentBackground ? "" : " Unavailable in worker sessions because bash.subagent_background is false.";
30531
+ return `Spawn the command in a real PTY for interactive programs. Implies background: true automatically.${workers}${drive}`;
30532
+ }
30533
+ function nativeSandboxEnabled(config) {
30534
+ return config.sandbox?.enabled === true;
29741
30535
  }
29742
30536
  var BashTaskParams = Type4.Object({
29743
30537
  task_id: Type4.String({
@@ -29869,18 +30663,47 @@ function getBashSpawnHook(pi) {
29869
30663
  }
29870
30664
  return api.hooks?.bashSpawn;
29871
30665
  }
30666
+ function backgroundWaitSentence(c) {
30667
+ if (c.watch) {
30668
+ const noPolling = c.status ? ", and never loop `bash_status` to wait" : "";
30669
+ return `then \`bash_watch\` handles only a short remaining wait (in a main session a watch defaults to 30s, max bash.watch_sync_max_ms, 120s by default; in a delegated session a watch without a timeout waits until the command finishes); for anything longer end the turn and let the completion reminder wake you, or use bash({wait:true}) when the result is needed before anything else — never background a command and immediately \`bash_watch\` it (that wastes a turn for what foreground returns in one)${noPolling}.`;
30670
+ }
30671
+ return c.status ? "the task keeps running after the call returns, a completion reminder arrives when it exits, and `bash_status` reports its state and output. Use bash({wait:true}) instead when the result is needed before anything else." : "the task keeps running after the call returns and a completion reminder arrives when it exits. Use bash({wait:true}) instead when the result is needed before anything else.";
30672
+ }
30673
+ function ptyDriveClause(c) {
30674
+ if (c.status && c.write) {
30675
+ return ', and is driven with `bash_status({ output_mode: "screen" })` plus `bash_write`';
30676
+ }
30677
+ if (c.status)
30678
+ return ', and its screen is read with `bash_status({ output_mode: "screen" })`';
30679
+ if (c.write)
30680
+ return ", and is driven with `bash_write`";
30681
+ return "";
30682
+ }
29872
30683
  function registerBashTool(pi, ctx, aftSearchRegistered = false, registeredName = "bash", registerCompanions = true, shell = "bash") {
29873
30684
  const isPowerShell = shell === "powershell";
29874
30685
  const spawnHook = isPowerShell ? undefined : getBashSpawnHook(pi);
29875
30686
  const readToolName = "read";
29876
30687
  const grepToolName = "grep";
29877
30688
  const zoomSteer = toolEnabled(ctx.config, "aft_zoom") ? ", or `aft_zoom`" : "";
29878
- const searchSteer = aftSearchRegistered ? `use \`aft_search\` (concepts, identifiers, regex, literals), \`${readToolName}\`, \`aft_outline\`${zoomSteer} instead` : `use the \`${grepToolName}\` tool, \`${readToolName}\`, \`aft_outline\`${zoomSteer} instead`;
30689
+ const steerTools = [
30690
+ aftSearchRegistered ? "`aft_search` (concepts, identifiers, regex, literals)" : `the \`${grepToolName}\` tool`,
30691
+ `\`${readToolName}\``,
30692
+ ...toolEnabled(ctx.config, "aft_outline") ? ["`aft_outline`"] : []
30693
+ ].join(", ");
30694
+ const searchSteer = `use ${steerTools}${zoomSteer} instead`;
30695
+ const companions = registeredCompanions(ctx.config);
29879
30696
  const bashCfg = resolveBashConfig(ctx.config);
29880
30697
  let hostFallbackActive = false;
29881
30698
  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`." : "";
30699
+ const supported = bashCfg.background ? [
30700
+ "workdir",
30701
+ "background tasks",
30702
+ ...bashCfg.compress ? ["compressed output"] : [],
30703
+ "PTY mode"
30704
+ ].join(", ") : bashCfg.compress ? "workdir and compressed output" : "workdir";
29882
30705
  const detachSentence = bashCfg.detach_on_user_message ? "Any new message detaches this wait. Set `bash.detach_on_user_message: false` to keep it blocking; even then, a message containing the literal `&detach` forces detachment, and the token is stripped before delivery; the rest of the message is preserved, while a token-only message becomes `(requested background detach)`." : "Because `bash.detach_on_user_message` is false, a new message leaves this wait blocking; include the literal `&detach` anywhere to force detachment, and the token is stripped before delivery; the rest of the message is preserved, while a token-only message becomes `(requested background detach)`.";
29883
- const tasksSentence = bashCfg.background ? ` Commands run in the foreground and return inline; \`wait: true\` blocks until a long command finishes instead of auto-promoting; ${detachSentence} 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\` handles only a short remaining wait (default 30s, max bash.watch_sync_max_ms, 120s by default); for anything longer end the turn and let the completion reminder wake you, or use bash({wait:true}) when the result is needed before anything else — 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. A \`nohup … &\` launch still holds the call if the child keeps stdout/stderr; redirect both or use background:true. \`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).";
30706
+ const tasksSentence = bashCfg.background ? ` Commands run in the foreground and return inline; \`wait: true\` blocks until a long command finishes instead of auto-promoting; ${detachSentence} 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; ${backgroundWaitSentence(companions)} A \`nohup … &\` launch still holds the call if the child keeps stdout/stderr; redirect both or use background:true. \`pty: true\` runs interactive programs (REPLs, TUIs), implies background${ptyDriveClause(companions)}.` : " Commands run in the foreground to completion; `timeout` is the hard kill cap (default 30 minutes).";
29884
30707
  pi.registerTool({
29885
30708
  name: registeredName,
29886
30709
  label: registeredName,
@@ -29889,13 +30712,18 @@ function registerBashTool(pi, ctx, aftSearchRegistered = false, registeredName =
29889
30712
  PowerShell syntax is not analyzed as POSIX shell. Each command requires explicit approval so syntax AFT cannot safely interpret is never auto-allowed.` : `Execute shell commands.${compressionSentence}${tasksSentence}
29890
30713
 
29891
30714
  DO NOT use bash for code search or code exploration. If you are about to run grep, rg, sed, awk, find, or cat through bash to locate or read code: STOP — ${searchSteer}. When a list is cut, the reply ends with \`shown N of M <unit> (<reason>) · narrow: <knobs>\`; absence of that line means the list is complete.`,
29892
- promptSnippet: isPowerShell ? "Run PowerShell commands (timeout in milliseconds; supports workdir, background tasks, compressed output, PTY mode)" : bashCfg.background ? "Run shell commands (timeout in milliseconds; supports workdir, background tasks, compressed output, PTY mode)" : "Run shell commands (timeout in milliseconds; supports workdir and compressed output)",
30715
+ promptSnippet: isPowerShell ? `Run PowerShell commands (timeout in milliseconds; supports ${supported})` : `Run shell commands (timeout in milliseconds; supports ${supported})`,
29893
30716
  promptGuidelines: isPowerShell ? ["Use PowerShell syntax. Every command requires explicit approval."] : [
29894
30717
  `DO NOT use bash for code search or exploration — ${searchSteer}.`,
29895
- "Set compressed: false when you need ANSI color codes in the output.",
30718
+ ...bashCfg.compress ? ["Set compressed: false when you need ANSI color codes in the output."] : [],
29896
30719
  "Piped commands run verbatim and show the pipeline's output; run test/build tools without pipes when you need AFT's summary."
29897
30720
  ],
29898
- parameters: bashParamsForConfig(bashCfg.background),
30721
+ parameters: bashParamsForConfig({
30722
+ background: bashCfg.background,
30723
+ compress: bashCfg.compress,
30724
+ sandbox: nativeSandboxEnabled(ctx.config),
30725
+ subagentBackground: bashCfg.subagent_background
30726
+ }, companions),
29899
30727
  async execute(_toolCallId, params, signal, onUpdate, extCtx) {
29900
30728
  const bridge = bridgeFor(ctx, extCtx.cwd);
29901
30729
  const bashCfg = resolveBashConfig(ctx.config);
@@ -29905,7 +30733,7 @@ DO NOT use bash for code search or code exploration. If you are about to run gre
29905
30733
  const ptyRows = backgroundDisabled ? undefined : coerceOptionalInt(params.ptyRows, "ptyRows", 1, 60);
29906
30734
  const ptyCols = backgroundDisabled ? undefined : coerceOptionalInt(params.ptyCols, "ptyCols", 1, 140);
29907
30735
  const compressed = coerceBoolean(params.compressed, true);
29908
- const requestedWait = coerceBoolean(params.wait);
30736
+ const requestedWait = !backgroundDisabled && coerceBoolean(params.wait);
29909
30737
  const rawRequestedPty = coerceBoolean(params.pty);
29910
30738
  const rawRequestedBackground = coerceBoolean(params.background);
29911
30739
  if (requestedWait && rawRequestedPty) {
@@ -29921,6 +30749,8 @@ DO NOT use bash for code search or code exploration. If you are about to run gre
29921
30749
  }
29922
30750
  const blockToCompletion = backgroundDisabled || requestedWait || workerForcedForeground;
29923
30751
  const effectiveBackground = !blockToCompletion && (rawRequestedBackground || requestedPty);
30752
+ const isWorker = isPiWorkerSession(extCtx);
30753
+ const workerUnboundedWait = isWorker && requestedWait && timeout === undefined;
29924
30754
  let spawnContext = {
29925
30755
  command: params.command,
29926
30756
  cwd: params.workdir
@@ -29955,7 +30785,7 @@ DO NOT use bash for code search or code exploration. If you are about to run gre
29955
30785
  sandbox: params.sandbox,
29956
30786
  ...isPowerShell ? { shell: "powershell" } : {}
29957
30787
  }, extCtx, {
29958
- transportTimeoutMs: orchestratedTransportTimeoutMs(blockToCompletion, requestedWait, timeout, foregroundWaitMs),
30788
+ transportTimeoutMs: orchestratedTransportTimeoutMs(blockToCompletion, requestedWait, timeout, foregroundWaitMs, workerUnboundedWait),
29959
30789
  onProgress: ({ text }) => {
29960
30790
  streamed += text;
29961
30791
  const displayText = truncateToVisualLines(streamed, 100);
@@ -29966,10 +30796,10 @@ DO NOT use bash for code search or code exploration. If you are about to run gre
29966
30796
  const fallbackCause = classifyBashHostFallbackError(error);
29967
30797
  if (isPowerShell || !bashCfg.host_fallback || fallbackCause === undefined)
29968
30798
  throw error;
29969
- if (rawRequestedBackground) {
30799
+ if (!backgroundDisabled && rawRequestedBackground) {
29970
30800
  throw new Error(`${BASH_HOST_FALLBACK_REFUSAL}; background:true is unsupported.`);
29971
30801
  }
29972
- if (rawRequestedPty) {
30802
+ if (requestedPty) {
29973
30803
  throw new Error(`${BASH_HOST_FALLBACK_REFUSAL}; pty:true is unsupported.`);
29974
30804
  }
29975
30805
  if (!extCtx.hasUI || typeof extCtx.ui?.confirm !== "function") {
@@ -30024,6 +30854,8 @@ DO NOT use bash for code search or code exploration. If you are about to run gre
30024
30854
  registerBashCompanionTools(pi, ctx);
30025
30855
  }
30026
30856
  function registerBashCompanionTools(pi, ctx, enabled = { bashStatus: true, bashWatch: true, bashWrite: true, bashKill: true }) {
30857
+ if (!resolveBashConfig(ctx.config).background)
30858
+ return;
30027
30859
  if (enabled.bashStatus) {
30028
30860
  pi.registerTool(createBashStatusTool(ctx));
30029
30861
  }
@@ -30044,7 +30876,7 @@ function createBashStatusTool(ctx) {
30044
30876
  return {
30045
30877
  name: "bash_status",
30046
30878
  label: "bash_status",
30047
- description: "Read-only snapshot of a background bash task. Returns immediately. Never waits. One look to check on a task is fine — never loop it to wait for completion. To wait, use bash_watch.",
30879
+ description: `Read-only snapshot of a background bash task. Returns immediately. Never waits. One look to check on a task is fine — never loop it to wait for completion.${bashCompanionRegistered(ctx.config, "bash_watch") ? " To wait, use bash_watch." : ""}`,
30048
30880
  promptSnippet: "Inspect a background bash task by task_id",
30049
30881
  parameters: BashStatusParams,
30050
30882
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
@@ -30062,10 +30894,10 @@ function createBashWatchTool(ctx) {
30062
30894
  return {
30063
30895
  name: "bash_watch",
30064
30896
  label: "bash_watch",
30065
- description: "Watch a background bash task. Sync waits are for a short remaining wait on a task (default 30s, max `bash.watch_sync_max_ms`, 120s by default); for anything longer end the turn on `bash({background:true})` and let the completion reminder wake you, or use `bash({wait:true})` when the result is needed before anything else. The user can interrupt anytime; the wait auto-converts to an async notification. Async (background:true, requires pattern) registers a non-blocking notification and returns immediately — use when you have parallel work or want to end your turn. Never loop bash_status to wait.",
30897
+ description: `Watch a background bash task. ${WATCH_SYNC_DEFAULTS_DESCRIPTION}. In a main session sync waits are for a short remaining wait on a task; for anything longer end the turn on \`bash({background:true})\` and let the completion reminder wake you, or use \`bash({wait:true})\` when the result is needed before anything else. The user can interrupt anytime; the wait auto-converts to an async notification. Async (background:true, requires pattern) registers a non-blocking notification and returns immediately — use when you have parallel work or want to end your turn.${bashCompanionRegistered(ctx.config, "bash_status") ? " Never loop bash_status to wait." : ""}`,
30066
30898
  promptSnippet: "Wait for or watch a background bash task",
30067
30899
  parameters: BashWatchParams,
30068
- async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
30900
+ async execute(_toolCallId, params, signal, _onUpdate, extCtx) {
30069
30901
  const bridge = bridgeFor(ctx, extCtx.cwd);
30070
30902
  const waitFor = parseWaitPattern(params.pattern);
30071
30903
  const bashCfg = resolveBashConfig(ctx.config);
@@ -30102,10 +30934,10 @@ A notification will fire when the pattern matches or the task exits.`, watchDeta
30102
30934
  }
30103
30935
  const syncWaitCap = bashCfg.watch_sync_max_ms;
30104
30936
  const role = watchCallerRole(extCtx);
30105
- const effectiveWaitMs = workerForcedSync ? syncWaitCap : resolveWatchTimeoutMs(coerceConfiguredWatchTimeout(params.timeout_ms, syncWaitCap), role, syncWaitCap);
30106
- const data = await waitForBashStatus(ctx, bridge, extCtx, params.task_id, undefined, waitFor, true, effectiveWaitMs);
30937
+ const effectiveWaitMs = workerForcedSync ? undefined : resolveWatchTimeoutMs(coerceConfiguredWatchTimeout(params.timeout_ms, role, syncWaitCap), role, syncWaitCap);
30938
+ const data = await waitForBashStatus(ctx, bridge, extCtx, params.task_id, undefined, waitFor, true, effectiveWaitMs, signal);
30107
30939
  if (data.waited?.reason === "user_message") {
30108
- const convertedText = await convertToAsyncWatchOnAbort(bridge, extCtx, params.task_id, waitFor, coerceBoolean(params.once, true));
30940
+ const convertedText = await convertToAsyncWatchOnAbort(bridge, extCtx, params.task_id, waitFor, coerceBoolean(params.once, true), data.waited.elapsed_ms, role);
30109
30941
  return textResult(convertedText, { waited: data.waited });
30110
30942
  }
30111
30943
  const text = await formatBashStatus(extCtx, params.task_id, data, undefined, { role, capMs: syncWaitCap });
@@ -30113,9 +30945,11 @@ A notification will fire when the pattern matches or the task exits.`, watchDeta
30113
30945
  }
30114
30946
  };
30115
30947
  }
30116
- async function convertToAsyncWatchOnAbort(bridge, extCtx, taskId, waitFor, once) {
30948
+ async function convertToAsyncWatchOnAbort(bridge, extCtx, taskId, waitFor, once, elapsedMs, role) {
30949
+ const interrupted = `Sync watch for task ${taskId} was interrupted because you sent a message after ${elapsedMs}ms of waiting. `;
30950
+ const reminderFallback = interruptedWatchTail(role, `The task is still running in the background. A completion reminder will be ` + `delivered automatically when the task exits.`);
30117
30951
  if (!waitFor) {
30118
- return `Sync watch for task ${taskId} was interrupted because you sent a message. ` + `The task is still running in the background. A completion reminder will be ` + `delivered automatically when the task exits; don't poll bash_status.`;
30952
+ return interrupted + interruptedWatchTail(role, `The task is still running in the background. A completion reminder will be ` + `delivered automatically when the task exits; don't poll bash_status.`);
30119
30953
  }
30120
30954
  const notifyParams = {
30121
30955
  task_id: taskId,
@@ -30131,19 +30965,19 @@ async function convertToAsyncWatchOnAbort(bridge, extCtx, taskId, waitFor, once)
30131
30965
  const registered = await callBashBridge(bridge, "bash_notify", notifyParams, extCtx);
30132
30966
  if (registered.success === false) {
30133
30967
  unmarkExplicitControl(sessionId, taskId);
30134
- return `Sync watch for task ${taskId} was interrupted because you sent a message. ` + `Auto-registering an async watch failed (${String(registered.message ?? "unknown error")}). ` + `The task is still running in the background. A completion reminder will be ` + `delivered automatically when the task exits.`;
30968
+ return interrupted + `Auto-registering an async watch failed (${String(registered.message ?? "unknown error")}). ` + reminderFallback;
30135
30969
  }
30136
- return `Sync watch for task ${taskId} was interrupted because you sent a message. ` + `The wait has been converted to an async watch (${registered.watch_id}). ` + `A notification will fire when the pattern matches or the task exits.`;
30970
+ return interrupted + `The wait has been converted to an async watch (${registered.watch_id}). ` + interruptedWatchTail(role, `A notification will fire when the pattern matches or the task exits.`);
30137
30971
  } catch (err) {
30138
30972
  unmarkExplicitControl(sessionId, taskId);
30139
- return `Sync watch for task ${taskId} was interrupted because you sent a message. ` + `Auto-registering an async watch failed (${err instanceof Error ? err.message : String(err)}). ` + `The task is still running in the background. A completion reminder will be ` + `delivered automatically when the task exits.`;
30973
+ return interrupted + `Auto-registering an async watch failed (${err instanceof Error ? err.message : String(err)}). ` + reminderFallback;
30140
30974
  }
30141
30975
  }
30142
30976
  function createBashWriteTool(ctx) {
30143
30977
  return {
30144
30978
  name: "bash_write",
30145
30979
  label: "bash_write",
30146
- description: 'Write input bytes to a running PTY bash task. PTY-only; check bash_status reports mode: "pty" first. ' + 'Input is either a string (verbatim bytes) or an array mixing strings and { key: "esc" | "enter" | "up" | "ctrl-c" | ... } objects ' + 'for atomic text+key sequences such as [ "iHello", { key: "esc" }, ":wq", { key: "enter" } ]. ' + "Named keys cover enter/return/tab/space/backspace/esc/escape, arrows, home/end/page-up/page-down/delete/insert, f1..f12, and ctrl-a..ctrl-z. " + "Maximum 1 MiB per call (post-expansion).",
30980
+ description: `Write input bytes to a running PTY bash task. PTY-only${bashCompanionRegistered(ctx.config, "bash_status") ? '; check bash_status reports mode: "pty" first.' : "."} ` + 'Input is either a string (verbatim bytes) or an array mixing strings and { key: "esc" | "enter" | "up" | "ctrl-c" | ... } objects ' + 'for atomic text+key sequences such as [ "iHello", { key: "esc" }, ":wq", { key: "enter" } ]. ' + "Named keys cover enter/return/tab/space/backspace/esc/escape, arrows, home/end/page-up/page-down/delete/insert, f1..f12, and ctrl-a..ctrl-z. " + "Maximum 1 MiB per call (post-expansion).",
30147
30981
  promptSnippet: "Write keystrokes/input to a PTY bash task",
30148
30982
  parameters: BashWriteParams,
30149
30983
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
@@ -30207,9 +31041,17 @@ async function bashStatusSnapshot(bridge, extCtx, taskId, outputMode, options, c
30207
31041
  stderr_offset: cursor?.stderr
30208
31042
  }, extCtx, options);
30209
31043
  }
30210
- async function waitForBashStatus(ctx, bridge, extCtx, taskId, outputMode, waitFor, waitForExit, effectiveWaitMs) {
30211
- const startedAt = Date.now();
30212
- const deadline = startedAt + effectiveWaitMs;
31044
+ async function waitForBashStatus(ctx, bridge, extCtx, taskId, outputMode, waitFor, waitForExit, effectiveWaitMs, abortSignal) {
31045
+ const startedAt = watchClock.now();
31046
+ const deadline = effectiveWaitMs === undefined ? Number.POSITIVE_INFINITY : startedAt + effectiveWaitMs;
31047
+ const elapsedMs = () => Math.round(watchClock.now() - startedAt);
31048
+ const pause = () => watchClock.sleep(Math.min(watchPollDelayMs(elapsedMs()), Math.max(0, deadline - watchClock.now())), abortSignal);
31049
+ const waited = (reason, extra = {}) => ({
31050
+ reason,
31051
+ elapsed_ms: elapsedMs(),
31052
+ ...effectiveWaitMs === undefined ? {} : { limit_ms: effectiveWaitMs },
31053
+ ...extra
31054
+ });
30213
31055
  let spillCursor = { output: 0, stderr: 0 };
30214
31056
  const scanState = {
30215
31057
  output: { text: "", baseOffset: 0 },
@@ -30228,29 +31070,29 @@ async function waitForBashStatus(ctx, bridge, extCtx, taskId, outputMode, waitFo
30228
31070
  markTaskWaiting(sessionId, taskId);
30229
31071
  let sawTerminal = false;
30230
31072
  let lastData;
31073
+ let busySince;
30231
31074
  try {
30232
31075
  while (true) {
31076
+ if (abortSignal?.aborted) {
31077
+ return withWaited(lastData ?? unavailableSnapshot(), waited("aborted"));
31078
+ }
30233
31079
  let data;
30234
31080
  try {
30235
31081
  data = await bashStatusSnapshot(bridge, extCtx, taskId, outputMode, bridgeOptions, waitFor ? spillCursor : undefined);
30236
31082
  } catch (err) {
30237
31083
  if (!isBridgeTransportTimeout(err))
30238
31084
  throw err;
31085
+ busySince ??= watchClock.now();
30239
31086
  if (isSyncWatchAborted(sessionId)) {
30240
- return withWaited(lastData ?? unavailableSnapshot(), {
30241
- reason: "user_message",
30242
- elapsed_ms: Date.now() - startedAt
30243
- });
31087
+ return withWaited(lastData ?? unavailableSnapshot(), waited("user_message"));
30244
31088
  }
30245
- if (Date.now() >= deadline) {
30246
- return withWaited(lastData ?? unavailableSnapshot(), {
30247
- reason: "unavailable",
30248
- elapsed_ms: Date.now() - startedAt
30249
- });
31089
+ if (watchClock.now() >= deadline || watchClock.now() - busySince >= WATCH_UNAVAILABLE_GIVE_UP_MS) {
31090
+ return withWaited(lastData ?? unavailableSnapshot(), waited("unavailable"));
30250
31091
  }
30251
- await sleep(Math.min(BASH_WAIT_POLL_INTERVAL_MS, Math.max(0, deadline - Date.now())));
31092
+ await pause();
30252
31093
  continue;
30253
31094
  }
31095
+ busySince = undefined;
30254
31096
  lastData = data;
30255
31097
  const terminal = isTerminalStatus(data.status);
30256
31098
  if (waitFor) {
@@ -30275,13 +31117,11 @@ async function waitForBashStatus(ctx, bridge, extCtx, taskId, outputMode, waitFo
30275
31117
  await markBgCompletionDelivered({ ctx, directory: extCtx.cwd, sessionID: sessionId }, taskId);
30276
31118
  }
30277
31119
  const matchStream = data.mode === "pty" ? undefined : chunk.stream === "output" ? "stdout" : "stderr";
30278
- return withWaited(data, {
30279
- reason: "matched",
30280
- elapsed_ms: Date.now() - startedAt,
31120
+ return withWaited(data, waited("matched", {
30281
31121
  match: match.text,
30282
31122
  match_offset: state.baseOffset + match.byteOffset,
30283
31123
  match_stream: matchStream
30284
- });
31124
+ }));
30285
31125
  }
30286
31126
  if (waitFor.kind === "substring") {
30287
31127
  const trimmed = trimWaitScanBuffer(state.text, state.baseOffset, waitFor);
@@ -30297,15 +31137,15 @@ async function waitForBashStatus(ctx, bridge, extCtx, taskId, outputMode, waitFo
30297
31137
  consumeBgCompletion(sessionId, taskId);
30298
31138
  await markBgCompletionDelivered({ ctx, directory: extCtx.cwd, sessionID: sessionId }, taskId);
30299
31139
  }
30300
- return withWaited(data, { reason: "exited", elapsed_ms: Date.now() - startedAt });
31140
+ return withWaited(data, waited("exited"));
30301
31141
  }
30302
31142
  if (isSyncWatchAborted(sessionId)) {
30303
- return withWaited(data, { reason: "user_message", elapsed_ms: Date.now() - startedAt });
31143
+ return withWaited(data, waited("user_message"));
30304
31144
  }
30305
- if (Date.now() >= deadline) {
30306
- return withWaited(data, { reason: "timeout", elapsed_ms: Date.now() - startedAt });
31145
+ if (watchClock.now() >= deadline) {
31146
+ return withWaited(data, waited("timeout"));
30307
31147
  }
30308
- await sleep(Math.min(BASH_WAIT_POLL_INTERVAL_MS, Math.max(0, deadline - Date.now())));
31148
+ await pause();
30309
31149
  }
30310
31150
  } finally {
30311
31151
  if (waitForExit && !sawTerminal)
@@ -30412,18 +31252,22 @@ function withWaited(data, waited) {
30412
31252
  return { ...data, waited };
30413
31253
  }
30414
31254
  function formatWaitSummary(waited, details, watchRole) {
31255
+ const waitedText = formatWatchWaited(waited.elapsed_ms, waited.limit_ms, watchRole.role === "primary" ? watchRole.capMs : undefined);
30415
31256
  if (waited.reason === "matched") {
30416
31257
  const stream = waited.match_stream ? ` in ${waited.match_stream}` : "";
30417
- return `Waited ${waited.elapsed_ms}ms; matched ${JSON.stringify(waited.match ?? "")}${stream} at offset ${waited.match_offset ?? 0}.`;
31258
+ return `${waitedText}; matched ${JSON.stringify(waited.match ?? "")}${stream} at offset ${waited.match_offset ?? 0}.`;
30418
31259
  }
30419
31260
  if (waited.reason === "timeout") {
30420
- return `Waited ${waited.elapsed_ms}ms; timeout reached without match. ${watchTimeoutSteer(watchRole.role, watchRole.capMs, "timeout_ms")}`;
31261
+ return `${waitedText}; timeout reached without match. ${watchTimeoutSteer(watchRole.role, "timeout_ms")}`;
30421
31262
  }
30422
31263
  if (waited.reason === "unavailable") {
30423
- return `Waited ${waited.elapsed_ms}ms; the bridge was busy, so task state is unknown. Do not poll; let the task's completion notification wake the session, or use one bash_status snapshot on the next normal tool call.`;
31264
+ return `${waitedText}; ${watchUnavailableSteer(watchRole.role)}`;
31265
+ }
31266
+ if (waited.reason === "aborted") {
31267
+ return `${waitedText}; the watch was cancelled. The task keeps running.`;
30424
31268
  }
30425
31269
  const exit = typeof details.exit_code === "number" ? `, exit ${details.exit_code}` : "";
30426
- return `Waited ${waited.elapsed_ms}ms; task exited (${details.status}${exit}).`;
31270
+ return `${waitedText}; task exited (${details.status}${exit}).`;
30427
31271
  }
30428
31272
  async function formatBashStatus(extCtx, taskId, details, requestedOutputMode, watchRole) {
30429
31273
  const exit = typeof details.exit_code === "number" ? ` (exit ${details.exit_code})` : "";
@@ -30442,10 +31286,10 @@ ${formatWaitSummary(details.waited, details, watchRole)}`;
30442
31286
  text += `
30443
31287
  ${details.output_preview}`;
30444
31288
  }
30445
- const workerWatchTimeout = watchRole.role === "worker" && details.waited?.reason === "timeout";
30446
- if (!isTerminalStatus(details.status) && !workerWatchTimeout) {
31289
+ const workerWatchReturned = watchRole.role === "worker" && details.waited !== undefined;
31290
+ if (!isTerminalStatus(details.status) && !workerWatchReturned) {
30447
31291
  text += `
30448
- A completion reminder will be delivered automatically; don't poll.`;
31292
+ ${runningTaskStatusHint(watchRole.role)}`;
30449
31293
  }
30450
31294
  }
30451
31295
  return text;
@@ -30690,7 +31534,7 @@ function registerFsTools(pi, ctx, surface) {
30690
31534
  pi.registerTool(withPathAliasPreparation({
30691
31535
  name: "aft_delete",
30692
31536
  label: "delete",
30693
- description: "Delete one or more files (or directories). " + (backupsDisabled ? "Backup capture is disabled by user config, so this tool does not create undo snapshots. " : "Each file is backed up before deletion — use `aft_safety undo` to recover any of them. For directories, every file inside is individually backed up before removal. The deleted file's contents stay in the undo store until its retention expires. A recursive delete whose backup would copy more than 2,000 files or 100 MiB in one call is refused before anything is deleted; delete such a tree in smaller pieces, or use bash `rm -rf` when no undo is needed. ") + "Directory deletion requires recursive: true. " + "Returns { success, complete, deleted, skipped_files }: partial success is allowed; files that fail are reported in skipped_files.",
31537
+ description: "Delete one or more files (or directories). " + (backupsDisabled ? "Backup capture is disabled by user config, so this tool does not create undo snapshots. A directory tree containing a mount point of another filesystem is still refused. " : "Each file is backed up before deletion — use `aft_safety undo` to recover any of them. A recursive delete backs up the whole tree: directories (empty ones too, with their permissions), file contents, hard links (relinked by undo) and symlinks (the link itself, never its target; restored exactly, even when dangling). Sockets are deleted but undo does not restore them, and a file hard-linked to paths outside the tree comes back as an independent copy; both are reported as warnings. Refused before anything is deleted: mount points of another filesystem, named pipes, device nodes, and symlinks undo cannot recreate exactly (non-UTF-8 target, or on Windows). Paths under the system temp directory are never backed up, so only mount points are refused there. The deleted file's contents stay in the undo store until its retention expires. A recursive delete whose backup would record more than 2,000 entries (files, directories and links) or copy 100 MiB in one call is refused before anything is deleted; delete such a tree in smaller pieces, or use bash `rm -rf` when no undo is needed. ") + "Directory deletion requires recursive: true. " + "Returns { success, complete, deleted, skipped_files }: partial success is allowed; files that fail are reported in skipped_files.",
30694
31538
  parameters: DeleteParams,
30695
31539
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
30696
31540
  const inputs = coerceStringArray(params.files);
@@ -30698,14 +31542,13 @@ function registerFsTools(pi, ctx, surface) {
30698
31542
  throw new Error("delete: `files` must be a non-empty array of paths");
30699
31543
  }
30700
31544
  const files = await Promise.all(inputs.map((file) => resolvePathArg(extCtx.cwd, file)));
31545
+ const restrictToProjectRoot = ctx.config.restrict_to_project_root ?? false;
30701
31546
  const checked = new Set;
30702
31547
  for (const file of files) {
30703
31548
  if (checked.has(file))
30704
31549
  continue;
30705
31550
  checked.add(file);
30706
- await assertExternalDirectoryPermission(extCtx, file, {
30707
- restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
30708
- });
31551
+ await assertExternalDirectoryPermission(extCtx, file, { restrictToProjectRoot });
30709
31552
  }
30710
31553
  const bridge = bridgeFor(ctx, extCtx.cwd);
30711
31554
  const response = await callToolCall(bridge, "delete", {
@@ -30743,10 +31586,9 @@ function registerFsTools(pi, ctx, surface) {
30743
31586
  const filePath = await resolvePathArg(extCtx.cwd, params.path);
30744
31587
  const destination = await resolvePathArg(extCtx.cwd, params.destination);
30745
31588
  const checked = new Set([filePath, destination]);
31589
+ const restrictToProjectRoot = ctx.config.restrict_to_project_root ?? false;
30746
31590
  for (const file of checked) {
30747
- await assertExternalDirectoryPermission(extCtx, file, {
30748
- restrictToProjectRoot: ctx.config.restrict_to_project_root ?? false
30749
- });
31591
+ await assertExternalDirectoryPermission(extCtx, file, { restrictToProjectRoot });
30750
31592
  }
30751
31593
  const bridge = bridgeFor(ctx, extCtx.cwd);
30752
31594
  const response = await callToolCall(bridge, "move", {
@@ -31391,6 +32233,9 @@ function registerNavigateTool(pi, ctx) {
31391
32233
 
31392
32234
  // src/tools/reading.ts
31393
32235
  import { Type as Type10 } from "typebox";
32236
+ function isGithubPathArg(value) {
32237
+ return typeof value === "string" && (value.startsWith("issue://") || value.startsWith("pr://"));
32238
+ }
31394
32239
  var OutlineParams = Type10.Object({
31395
32240
  target: Type10.Union([Type10.String(), Type10.Array(Type10.String())], {
31396
32241
  description: "What to outline: a file path, directory path, URL (http:// or https://), or array of file paths. The mode is auto-detected: URLs by `http://`/`https://` prefix, directories by stat, arrays as multi-file."
@@ -31415,7 +32260,7 @@ var ZoomParams = Type10.Object({
31415
32260
  description: "Symbol name for code, or heading text for Markdown/HTML. Pass a string or array for batched lookups in the same file/URL."
31416
32261
  })),
31417
32262
  targets: Type10.Optional(Type10.Union([ZoomTarget, Type10.Array(ZoomTarget)], {
31418
- description: "Cross-file batch: `{ path, symbol }` or an array of them. Mutually exclusive with path/url/symbols."
32263
+ description: "Cross-file batch: `{ path, symbol }` or an array of them. May be combined with path/url + symbols; all lookups are answered in one batch."
31419
32264
  })),
31420
32265
  contextLines: optionalInt(1, Number.MAX_SAFE_INTEGER, "Lines of context before/after (default: 3)"),
31421
32266
  callgraph: Type10.Optional(Type10.Boolean({
@@ -31569,6 +32414,13 @@ function renderZoomResult(result, args, theme, context, options = { expanded: tr
31569
32414
  context
31570
32415
  });
31571
32416
  }
32417
+ function outlineFocusSteer(config) {
32418
+ const zoomEnabled = toolEnabled(config, "aft_zoom");
32419
+ const reader = zoomEnabled ? "aft_zoom" : "read";
32420
+ const locate = toolEnabled(config, "aft_search") ? `aft_search + ${reader}` : reader;
32421
+ const callgraph = zoomEnabled ? toolEnabled(config, "aft_callgraph") ? " aft_zoom with `callgraph:true` gives one-level forward calls-out; use aft_callgraph only for reverse callers or multi-level traces." : " aft_zoom with `callgraph:true` gives one-level forward calls-out." : "";
32422
+ return `For understanding a specific feature, prefer ${locate} on named symbols; use aft_outline on a whole directory only for high-level structure mapping.${callgraph}`;
32423
+ }
31572
32424
  function registerReadingTools(pi, ctx, surface) {
31573
32425
  const zoomEnabled = toolEnabled(ctx.config, "aft_zoom");
31574
32426
  const ghReadEnabled = resolveGithubConfig(ctx.config).read;
@@ -31580,7 +32432,7 @@ function registerReadingTools(pi, ctx, surface) {
31580
32432
  label: "outline",
31581
32433
  description: `Structural outline of source code, documentation files, or remote URLs. For code, returns symbols (functions, classes, types) with line ranges. For Markdown and HTML, returns heading hierarchy. Use this to explore structure before reading specific sections with ${zoomEnabled ? "aft_zoom" : "read"}. With \`files: true\`, the outline is breadth-first with directory rollups; drill in by outlining a subdirectory. Rows show language, symbol count, and line count.
31582
32434
 
31583
- ${zoomEnabled ? "For understanding a specific feature, prefer aft_search + aft_zoom on named symbols; use aft_outline on a whole directory only for high-level structure mapping. aft_zoom with `callgraph:true` gives one-level forward calls-out; use aft_callgraph only for reverse callers or multi-level traces." : "For understanding a specific feature, prefer aft_search + read on named symbols; use aft_outline on a whole directory only for high-level structure mapping."}
32435
+ ${outlineFocusSteer(ctx.config)}
31584
32436
 
31585
32437
  Pass a single \`target\`:
31586
32438
  • file path → outline that file (with signatures)
@@ -31647,56 +32499,49 @@ When a list is cut, the reply ends with \`shown N of M <unit> (<reason>) · narr
31647
32499
  pi.registerTool(withPathAliasPreparation({
31648
32500
  name: "aft_zoom",
31649
32501
  label: "zoom",
31650
- description: "Inspect code symbols or documentation sections. For code, returns the full source of a symbol. Pass `callgraph: true` to also include call-graph annotations (calls-out / called-by within the same file). For Markdown and HTML, returns the section content under the given heading.\n\nUse exactly ONE mode: `{ path, symbols }`, `{ url, symbols }`, or `{ targets }`. `symbols` can be a string or array (one or many lookups in the same file/URL). Use `targets` for cross-file batches: `{ path, symbol }` or an array of them." + (githubZoomDescription ? `
32502
+ description: "Inspect code symbols or documentation sections. For code, returns the full source of a symbol. Pass `callgraph: true` to also include call-graph annotations (calls-out / called-by within the same file). For Markdown and HTML, returns the section content under the given heading.\n\nUse `{ path, symbols }` or `{ url, symbols }` for one file/URL. `symbols` can be a string or array (one or many lookups in the same file/URL). Use `targets` for cross-file batches: `{ path, symbol }` or an array of them. Sending both merges them into one batch; a lookup that fails reports its own error line." + (githubZoomDescription ? `
31651
32503
 
31652
32504
  ${githubZoomDescription}` : ""),
31653
32505
  parameters: ZoomParams,
31654
32506
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
31655
32507
  const bridge = bridgeFor(ctx, extCtx.cwd);
31656
- const hasTargetsProvided = (t) => {
31657
- if (isEmptyParam(t))
31658
- return false;
31659
- const entryEmpty = (entry) => {
31660
- if (!entry || typeof entry !== "object")
31661
- return true;
31662
- const fp = entry.path;
31663
- const sym = entry.symbol;
31664
- const fpEmpty = typeof fp !== "string" || fp.length === 0;
31665
- const symEmpty = typeof sym !== "string" || sym.length === 0;
31666
- return fpEmpty && symEmpty;
31667
- };
31668
- if (Array.isArray(t))
31669
- return !t.every(entryEmpty);
31670
- return !entryEmpty(t);
31671
- };
31672
32508
  const targetsInput = coerceJsonCollectionParam(params.targets, "targets");
31673
- const hasPath = !isEmptyParam(params.path);
31674
- const hasUrl = !isEmptyParam(params.url);
31675
- const hasTargets = hasTargetsProvided(targetsInput);
31676
- const hasSymbols = !isEmptyParam(params.symbols);
32509
+ const targetEntries = usableZoomTargets(targetsInput);
32510
+ const hasPath = !isBlankParam(params.path);
32511
+ const hasUrl = !isBlankParam(params.url);
32512
+ const hasTargets = targetEntries.length > 0;
32513
+ const hasSymbols = !isBlankParam(params.symbols);
31677
32514
  const wantCallgraph = coerceBoolean(params.callgraph);
31678
32515
  const contextLines = coerceOptionalInt(params.contextLines, "contextLines", 1, Number.MAX_SAFE_INTEGER);
31679
32516
  if (hasTargets) {
31680
- if (hasPath || hasUrl || hasSymbols) {
31681
- throw new Error("'targets' is mutually exclusive with 'path', 'url', and 'symbols'");
32517
+ if (hasPath && hasUrl) {
32518
+ throw new Error("Provide exactly ONE of 'path' or 'url' — not both");
31682
32519
  }
31683
- const targets = Array.isArray(targetsInput) ? targetsInput : [targetsInput];
31684
- if (targets.length === 0) {
31685
- throw new Error("'targets' must be a non-empty object or array");
31686
- }
31687
- for (const [i, entry] of targets.entries()) {
31688
- if (!entry || typeof entry.path !== "string" || entry.path.length === 0) {
32520
+ const targets = targetEntries.map((entry, i) => {
32521
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
31689
32522
  throw new Error(`targets[${i}].path must be a non-empty string`);
31690
32523
  }
31691
- if (typeof entry.symbol !== "string" || entry.symbol.length === 0) {
31692
- throw new Error(`targets[${i}].symbol must be a non-empty string`);
31693
- }
31694
- }
31695
- const resolvedTargets = await Promise.all(targets.map((t) => resolvePathArg(extCtx.cwd, t.path)));
32524
+ const record = entry;
32525
+ const targetPath = isBlankParam(record.path) ? record.filePath : record.path;
32526
+ return {
32527
+ path: typeof targetPath === "string" ? targetPath : "",
32528
+ symbol: typeof record.symbol === "string" ? record.symbol : ""
32529
+ };
32530
+ });
32531
+ const localPaths = targets.map((target) => target.path).filter((path) => path.trim().length > 0 && !isGithubPathArg(path));
32532
+ if (hasPath && !isGithubPathArg(params.path))
32533
+ localPaths.push(params.path);
32534
+ const resolvedTargets = await Promise.all(localPaths.map((path) => resolvePathArg(extCtx.cwd, path)));
31696
32535
  await assertReadPathPermissions(extCtx, ctx, resolvedTargets);
31697
32536
  const rawArgs = {
31698
32537
  targets: targets.map((target) => ({ filePath: target.path, symbol: target.symbol }))
31699
32538
  };
32539
+ if (hasPath)
32540
+ rawArgs.filePath = params.path;
32541
+ else if (hasUrl)
32542
+ rawArgs.url = params.url;
32543
+ if (hasSymbols)
32544
+ rawArgs.symbols = params.symbols;
31700
32545
  if (contextLines !== undefined)
31701
32546
  rawArgs.contextLines = contextLines;
31702
32547
  if (wantCallgraph)
@@ -31713,7 +32558,7 @@ ${githubZoomDescription}` : ""),
31713
32558
  if (hasPath && hasUrl) {
31714
32559
  throw new Error("Provide exactly ONE of 'path' or 'url' — not both");
31715
32560
  }
31716
- const githubPath = hasPath && (String(params.path).startsWith("issue://") || String(params.path).startsWith("pr://")) || hasUrl && (String(params.url).startsWith("issue://") || String(params.url).startsWith("pr://"));
32561
+ const githubPath = hasPath && isGithubPathArg(params.path) || hasUrl && isGithubPathArg(params.url);
31717
32562
  if (!hasUrl && !githubPath) {
31718
32563
  const file = await resolvePathArg(extCtx.cwd, params.path);
31719
32564
  await assertReadPathPermissions(extCtx, ctx, file);
@@ -31752,7 +32597,7 @@ var SafetyParams = Type11.Object({
31752
32597
  description: "Safety operation"
31753
32598
  }),
31754
32599
  path: Type11.Optional(Type11.String({
31755
- description: "File path (required for history, optional for undo, optional for restore — restores only that file). Absolute or relative to project root."
32600
+ description: "File path (required for history, optional for undo, optional for checkpoint — snapshots only that file when files is omitted, optional for restore — restores only that file). Absolute or relative to project root."
31756
32601
  })),
31757
32602
  name: Type11.Optional(Type11.String({ description: "Checkpoint name (required for checkpoint, restore)" })),
31758
32603
  files: Type11.Optional(Type11.Array(Type11.String(), {
@@ -32176,9 +33021,17 @@ function piHashlineDowngrade(config, surface) {
32176
33021
  message: 'edit_mode "hashline" is not in effect because "read" is in disabled_tools (hashline edits need tagged reads); registered tools keep their ordinary behavior.'
32177
33022
  };
32178
33023
  }
33024
+ function withLiveRestriction(surface, ctx) {
33025
+ return Object.defineProperty({ ...surface }, "restrictToProjectRoot", {
33026
+ enumerable: true,
33027
+ get: () => ctx.config.restrict_to_project_root ?? false
33028
+ });
33029
+ }
32179
33030
  function resolvePiToolSurface(config, pi) {
32180
33031
  const disabled = new Set(resolvedDisabledTools(config));
32181
33032
  const ok = (name) => !disabled.has(name);
33033
+ const background = resolveBashConfig(config).background;
33034
+ const companion = (name) => background && ok(name);
32182
33035
  const powershellEnabled = (pi ? piPowerShellEnabledFromHost(pi) : undefined) ?? resolvePiPowerShellFallback(config);
32183
33036
  return {
32184
33037
  hoistBash: ok("bash"),
@@ -32200,10 +33053,10 @@ function resolvePiToolSurface(config, pi) {
32200
33053
  move: ok("aft_move"),
32201
33054
  astSearch: ok("ast_grep_search"),
32202
33055
  astReplace: ok("ast_grep_replace"),
32203
- bashStatus: ok("bash_status"),
32204
- bashWatch: ok("bash_watch"),
32205
- bashWrite: ok("bash_write"),
32206
- bashKill: ok("bash_kill")
33056
+ bashStatus: companion("bash_status"),
33057
+ bashWatch: companion("bash_watch"),
33058
+ bashWrite: companion("bash_write"),
33059
+ bashKill: companion("bash_kill")
32207
33060
  };
32208
33061
  }
32209
33062
  var FUNNEL_BOUND = Symbol.for("aft.pi.registration_funnel_bound");
@@ -32228,7 +33081,8 @@ function bindToolRegistrationFunnel(pi, ctx, harness) {
32228
33081
  }
32229
33082
  });
32230
33083
  }
32231
- function registerPiToolSurface(pi, ctx, surface, harness) {
33084
+ function registerPiToolSurface(pi, ctx, loadedSurface, harness) {
33085
+ const surface = withLiveRestriction(loadedSurface, ctx);
32232
33086
  const boundPi = bindToolRegistrationFunnel(pi, ctx, harness);
32233
33087
  if (surface.hoistBash)
32234
33088
  registerBashTool(boundPi, ctx, surface.semantic, "bash", false);
@@ -32262,6 +33116,8 @@ var CONFIG_ERROR_STATUS_KEY = "aft";
32262
33116
  var defaultDependencies = {
32263
33117
  loadConfig: loadAftConfig,
32264
33118
  configLoadErrors: getConfigLoadErrors,
33119
+ configLoadSources: getConfigLoadSources,
33120
+ configLoadTexts: getConfigLoadTexts,
32265
33121
  migrateConfigLocations: (directory) => migrateAftConfigLocations(directory, bridgeLogger).flatMap((result) => result.warnings),
32266
33122
  subcConnectionFileError
32267
33123
  };
@@ -32280,6 +33136,8 @@ function parseFailure(dependencies) {
32280
33136
  }
32281
33137
  async function resolvePiBootstrapConfig(directory, notify, dependencies = defaultDependencies) {
32282
33138
  let config;
33139
+ let sources = [];
33140
+ let sourceTexts = {};
32283
33141
  try {
32284
33142
  dependencies.loadConfig(directory);
32285
33143
  const firstFailure = parseFailure(dependencies);
@@ -32291,11 +33149,13 @@ async function resolvePiBootstrapConfig(directory, notify, dependencies = defaul
32291
33149
  const failure = parseFailure(dependencies);
32292
33150
  if (failure)
32293
33151
  return configErrorState(failure, notify);
33152
+ sources = [...dependencies.configLoadSources?.() ?? []];
33153
+ sourceTexts = Object.fromEntries(dependencies.configLoadTexts?.() ?? []);
32294
33154
  } catch (err) {
32295
33155
  return configErrorState(err instanceof Error ? err.message : String(err), notify);
32296
33156
  }
32297
33157
  const missing = await dependencies.subcConnectionFileError(config.subc?.connection_file);
32298
- return missing === null ? { ok: true, config } : configErrorState(missing, notify, config);
33158
+ return missing === null ? { ok: true, config, sources, sourceTexts } : configErrorState(missing, notify, config);
32299
33159
  }
32300
33160
  function failingRegistrations(pi, message) {
32301
33161
  const registerTool = (tool) => {
@@ -32331,6 +33191,58 @@ function registerPiConfigErrorState(pi, config, message) {
32331
33191
  log2("AFT is in the config error state; every tool call will fail");
32332
33192
  }
32333
33193
 
33194
+ // src/config-live-reload.ts
33195
+ var PI_LIVE_CONFIG_KEYS = aftLiveConfigKeys((config) => resolveBashConfig(config));
33196
+ function projectTextOf(directory) {
33197
+ const { projectConfigPath } = resolveCortexKitConfigPaths(directory);
33198
+ return getConfigLoadTexts().get(projectConfigPath) ?? null;
33199
+ }
33200
+ function loadPiConfigForLiveReload(directory) {
33201
+ try {
33202
+ const config = loadAftConfig(directory);
33203
+ const [failure] = getConfigLoadErrors();
33204
+ if (failure) {
33205
+ return { ok: false, message: formatConfigParseErrorMessage(failure.path, failure.message) };
33206
+ }
33207
+ const [invalid] = getConfigValidationErrors();
33208
+ if (invalid) {
33209
+ return {
33210
+ ok: false,
33211
+ message: `AFT config at ${invalid.path} has an invalid setting: ${invalid.message}`
33212
+ };
33213
+ }
33214
+ return {
33215
+ ok: true,
33216
+ config,
33217
+ sources: [...getConfigLoadSources()],
33218
+ projectText: projectTextOf(directory),
33219
+ texts: Object.fromEntries(getConfigLoadTexts())
33220
+ };
33221
+ } catch (err) {
33222
+ return { ok: false, message: err instanceof Error ? err.message : String(err) };
33223
+ }
33224
+ }
33225
+ function startPiLiveConfigReload(options) {
33226
+ const { userConfigPath, projectConfigPath } = resolveCortexKitConfigPaths(options.directory);
33227
+ return startLiveConfigReload({
33228
+ paths: [userConfigPath, projectConfigPath],
33229
+ load: () => loadPiConfigForLiveReload(options.directory),
33230
+ initialSources: options.initialSources,
33231
+ initialProjectText: options.initialSourceTexts?.[projectConfigPath] ?? null,
33232
+ initialTexts: options.initialSourceTexts,
33233
+ securityKeys: aftLiveSecurityKeys(PI_LIVE_CONFIG_KEYS),
33234
+ keys: PI_LIVE_CONFIG_KEYS,
33235
+ getConfig: options.getConfig,
33236
+ setConfig: options.setConfig,
33237
+ log: (message) => log2(`${message} (${options.directory})`),
33238
+ reportError: (message) => {
33239
+ error2(message);
33240
+ options.notify(message);
33241
+ },
33242
+ watch: options.watch
33243
+ });
33244
+ }
33245
+
32334
33246
  // src/shutdown-hooks.ts
32335
33247
  var GLOBAL_KEY = "__aftPiShutdownHooks__";
32336
33248
  function getState() {
@@ -32414,7 +33326,10 @@ function registerShutdownCleanup(fn) {
32414
33326
 
32415
33327
  // src/workflow-hints.ts
32416
33328
  var HEADING = "## IMPORTANT NOTICE about your tools";
32417
- var HASHLINE_TAG_SOURCE_HINT = "**Hashline edit tags**: Only `read` (and accepted AFT `cat`/`head`/`tail` rewrites) mint hashline tags. `aft_zoom`, `aft_outline`, `grep`, `aft_search`, and conflict snippets do not. After navigation, call `read` on every file and range the patch addresses.";
33329
+ function hashlineTagSourceHint(bashRewritesMintTags, isRegistered = () => true) {
33330
+ return `**Hashline edit tags**: ${hashlineTagSourceSentence(bashRewritesMintTags, isRegistered)}`;
33331
+ }
33332
+ var HASHLINE_TAG_SOURCE_HINT = hashlineTagSourceHint(true);
32418
33333
  function buildWorkflowHints(opts) {
32419
33334
  const sections = [];
32420
33335
  const grepName = "grep";
@@ -32429,6 +33344,8 @@ function buildWorkflowHints(opts) {
32429
33344
  const hasInspect = !opts.absentTools.has("aft_inspect");
32430
33345
  const hasBash = !opts.absentTools.has(bashName);
32431
33346
  const hasBgBash = opts.bashBackgroundEnabled && hasBash && !opts.absentTools.has("bash_status");
33347
+ const hasWatch = !opts.absentTools.has("bash_watch");
33348
+ const hasWrite = !opts.absentTools.has("bash_write");
32432
33349
  if (hasBash && opts.bashCompressionEnabled) {
32433
33350
  sections.push([
32434
33351
  "**Test/build output**: bash output is auto-compressed for non-piped commands. Piped commands run verbatim and show the pipeline's output. For AFT's test/build summary, run the runner without filters:",
@@ -32470,17 +33387,19 @@ function buildWorkflowHints(opts) {
32470
33387
  ].join(`
32471
33388
  `));
32472
33389
  }
32473
- if (hasBgBash) {
33390
+ if (hasBgBash && hasWatch) {
32474
33391
  sections.push([
32475
33392
  `**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.`,
32476
33393
  "- `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.",
32477
33394
  "- `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."
32478
33395
  ].join(`
32479
33396
  `));
33397
+ }
33398
+ if (hasBgBash && hasWrite) {
32480
33399
  sections.push(`**PTY / interactive commands**: PTY mode is for interactive REPLs and terminal apps (python, node, bash itself, vim). Start with \`${bashName}({ command: "python", pty: true, background: true })\`, read the screen with \`bash_status({ task_id, output_mode: "screen" })\`, and send input with \`bash_write({ task_id, input: "..." })\`.`);
32481
33400
  }
32482
33401
  if (opts.hashlineEffective === true) {
32483
- sections.push(HASHLINE_TAG_SOURCE_HINT);
33402
+ sections.push(hashlineTagSourceHint(hasBash && opts.bashRewriteEnabled !== false, (name) => !opts.absentTools.has(name)));
32484
33403
  }
32485
33404
  if (sections.length === 0) {
32486
33405
  return null;
@@ -32495,14 +33414,18 @@ ${sections.join(`
32495
33414
  `)}`;
32496
33415
  }
32497
33416
  function buildHintsFromConfig(config, absentTools, hashlineEffective = false) {
33417
+ const bashCfg = resolveBashConfig(config);
32498
33418
  return buildWorkflowHints({
32499
- bashBackgroundEnabled: resolveBashConfig(config).background,
32500
- bashCompressionEnabled: resolveBashConfig(config).compress,
33419
+ bashBackgroundEnabled: bashCfg.background,
33420
+ bashCompressionEnabled: bashCfg.compress,
33421
+ bashRewriteEnabled: bashCfg.rewrite,
32501
33422
  absentTools,
32502
33423
  hashlineEffective
32503
33424
  });
32504
33425
  }
32505
33426
  function registerWorkflowHints(pi, config, surface) {
33427
+ if (skipsEagerStartup())
33428
+ return;
32506
33429
  const absent = new Set;
32507
33430
  if (!surface.outline)
32508
33431
  absent.add("aft_outline");
@@ -32522,6 +33445,10 @@ function registerWorkflowHints(pi, config, surface) {
32522
33445
  absent.add("bash");
32523
33446
  if (!surface.bashStatus)
32524
33447
  absent.add("bash_status");
33448
+ if (!surface.bashWatch)
33449
+ absent.add("bash_watch");
33450
+ if (!surface.bashWrite)
33451
+ absent.add("bash_write");
32525
33452
  const hintsBlock = buildHintsFromConfig(config, absent, piHashlineEffective(config, surface));
32526
33453
  if (!hintsBlock)
32527
33454
  return;
@@ -32956,6 +33883,16 @@ ${lines}
32956
33883
  hashlineEffective: hashlineEditRegistered,
32957
33884
  storageDir
32958
33885
  };
33886
+ const liveConfigReload = startPiLiveConfigReload({
33887
+ directory: projectRoot,
33888
+ initialSources: bootstrap.sources ?? [],
33889
+ initialSourceTexts: bootstrap.sourceTexts,
33890
+ getConfig: () => ctx.config,
33891
+ setConfig: (next) => {
33892
+ ctx.config = next;
33893
+ },
33894
+ notify: (message) => deliverConfigMigrationWarnings([message])
33895
+ });
32959
33896
  if (onnxRuntimePromise) {
32960
33897
  onnxRuntimePromise.then((ortDylibDir) => {
32961
33898
  if (ortDylibDir) {
@@ -33062,6 +33999,7 @@ Retry with: npx @cortexkit/aft doctor --fix`
33062
33999
  });
33063
34000
  const unregisterShutdownCleanup = registerShutdownCleanup(async () => {
33064
34001
  hostShutdownStarted = true;
34002
+ liveConfigReload.stop();
33065
34003
  try {
33066
34004
  await Promise.allSettled([abortInFlightAutoInstalls(), abortInFlightGithubInstalls()]);
33067
34005
  await pool.shutdown();
@@ -33072,6 +34010,7 @@ Retry with: npx @cortexkit/aft doctor --fix`
33072
34010
  });
33073
34011
  pi.on("session_shutdown", async () => {
33074
34012
  hostShutdownStarted = true;
34013
+ liveConfigReload.stop();
33075
34014
  try {
33076
34015
  await Promise.allSettled([abortInFlightAutoInstalls(), abortInFlightGithubInstalls()]);
33077
34016
  await pool.shutdown();