@cortexkit/aft-opencode 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.
Files changed (65) hide show
  1. package/dist/bridge-bootstrap.d.ts +8 -0
  2. package/dist/bridge-bootstrap.d.ts.map +1 -1
  3. package/dist/config-live-reload.d.ts +323 -0
  4. package/dist/config-live-reload.d.ts.map +1 -0
  5. package/dist/config.d.ts +59 -0
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/entry/server.js +2543 -989
  8. package/dist/hashline-tag-sources.d.ts +18 -0
  9. package/dist/hashline-tag-sources.d.ts.map +1 -0
  10. package/dist/hooks/auto-update-checker/checker.d.ts +1 -0
  11. package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
  12. package/dist/hooks/auto-update-checker/index.d.ts.map +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1901 -748
  15. package/dist/lsp-auto-install.d.ts +30 -0
  16. package/dist/lsp-auto-install.d.ts.map +1 -1
  17. package/dist/lsp-github-table.d.ts +4 -3
  18. package/dist/lsp-github-table.d.ts.map +1 -1
  19. package/dist/lsp-npm-table.d.ts +11 -1
  20. package/dist/lsp-npm-table.d.ts.map +1 -1
  21. package/dist/lsp-registry-probe.d.ts +8 -4
  22. package/dist/lsp-registry-probe.d.ts.map +1 -1
  23. package/dist/permissions/v2-service.d.ts +110 -5
  24. package/dist/permissions/v2-service.d.ts.map +1 -1
  25. package/dist/rpc/register.d.ts +0 -4
  26. package/dist/rpc/register.d.ts.map +1 -1
  27. package/dist/shared/read-vision.d.ts +10 -0
  28. package/dist/shared/read-vision.d.ts.map +1 -0
  29. package/dist/shared/session-directory.d.ts +7 -0
  30. package/dist/shared/session-directory.d.ts.map +1 -1
  31. package/dist/shared/subagent-detect.d.ts +11 -4
  32. package/dist/shared/subagent-detect.d.ts.map +1 -1
  33. package/dist/tool-perf.d.ts +1 -0
  34. package/dist/tool-perf.d.ts.map +1 -1
  35. package/dist/tool-registration.d.ts +8 -5
  36. package/dist/tool-registration.d.ts.map +1 -1
  37. package/dist/tools/_shared.d.ts +16 -15
  38. package/dist/tools/_shared.d.ts.map +1 -1
  39. package/dist/tools/bash.d.ts +24 -1
  40. package/dist/tools/bash.d.ts.map +1 -1
  41. package/dist/tools/bash_watch.d.ts +5 -2
  42. package/dist/tools/bash_watch.d.ts.map +1 -1
  43. package/dist/tools/bash_write.d.ts.map +1 -1
  44. package/dist/tools/definitions/v2.d.ts +2 -0
  45. package/dist/tools/definitions/v2.d.ts.map +1 -1
  46. package/dist/tools/hoisted/v2.d.ts +8 -1
  47. package/dist/tools/hoisted/v2.d.ts.map +1 -1
  48. package/dist/tools/hoisted.d.ts +9 -7
  49. package/dist/tools/hoisted.d.ts.map +1 -1
  50. package/dist/tools/permissions.d.ts.map +1 -1
  51. package/dist/tools/reading.d.ts +0 -3
  52. package/dist/tools/reading.d.ts.map +1 -1
  53. package/dist/v2-prompt-detach.d.ts +69 -0
  54. package/dist/v2-prompt-detach.d.ts.map +1 -0
  55. package/dist/wakes/runtime-consumer.d.ts +2 -0
  56. package/dist/wakes/runtime-consumer.d.ts.map +1 -1
  57. package/dist/wakes/session-delivery.d.ts +9 -0
  58. package/dist/wakes/session-delivery.d.ts.map +1 -1
  59. package/dist/workflow-hints.d.ts +20 -7
  60. package/dist/workflow-hints.d.ts.map +1 -1
  61. package/package.json +8 -8
  62. package/src/entry/server-runtime.mjs +36 -5
  63. package/src/shared/read-vision.ts +100 -0
  64. package/src/shared/session-directory.ts +92 -0
  65. package/src/shared/subagent-detect.ts +89 -28
package/dist/index.js CHANGED
@@ -7883,17 +7883,79 @@ import * as fs from "node:fs";
7883
7883
  import * as os from "node:os";
7884
7884
  import * as path from "node:path";
7885
7885
  var DEFAULT_PRIMARY_WATCH_TIMEOUT_MS = 30000;
7886
- 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).";
7886
+ var MAX_WATCH_TIMEOUT_MS = 1800000;
7887
+ var LONGEST_TIMER_DELAY_MS = 2147483647;
7888
+ 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";
7889
+ 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.";
7887
7890
  function resolveWatchTimeoutMs(requestedMs, role, capMs) {
7888
- const fallback = role === "worker" ? capMs : DEFAULT_PRIMARY_WATCH_TIMEOUT_MS;
7889
- return Math.min(requestedMs ?? fallback, capMs);
7891
+ if (role === "worker")
7892
+ return requestedMs;
7893
+ return Math.min(requestedMs ?? DEFAULT_PRIMARY_WATCH_TIMEOUT_MS, capMs);
7894
+ }
7895
+ function maxWatchTimeoutMs(role, capMs) {
7896
+ return role === "worker" ? MAX_WATCH_TIMEOUT_MS : capMs;
7897
+ }
7898
+ function watchPollDelayMs(elapsedMs) {
7899
+ if (elapsedMs < 5000)
7900
+ return 100;
7901
+ if (elapsedMs < 30000)
7902
+ return 250;
7903
+ if (elapsedMs < 120000)
7904
+ return 500;
7905
+ return 1000;
7906
+ }
7907
+ var watchClock = {
7908
+ now: () => performance.now(),
7909
+ sleep: abortableSleep
7910
+ };
7911
+ function abortableSleep(ms, signal) {
7912
+ if (signal?.aborted)
7913
+ return Promise.resolve();
7914
+ return new Promise((resolve) => {
7915
+ const done = () => {
7916
+ clearTimeout(timer);
7917
+ signal?.removeEventListener("abort", done);
7918
+ resolve();
7919
+ };
7920
+ const timer = setTimeout(done, ms);
7921
+ signal?.addEventListener("abort", done, { once: true });
7922
+ });
7890
7923
  }
7891
- function watchTimeoutSteer(role, capMs, timeoutParam = "timeoutMs") {
7924
+ function formatWatchWaited(elapsedMs, limitMs, capMs) {
7925
+ let limit;
7926
+ if (limitMs === undefined)
7927
+ limit = "no limit: waits until the command finishes";
7928
+ else if (capMs !== undefined && limitMs >= capMs)
7929
+ limit = `limit ${limitMs}ms, the bash.watch_sync_max_ms cap`;
7930
+ else
7931
+ limit = `limit ${limitMs}ms`;
7932
+ return `Waited ${Math.round(elapsedMs)}ms (${limit})`;
7933
+ }
7934
+ function watchTimeoutSteer(role, timeoutParam = "timeoutMs") {
7892
7935
  if (role === "worker") {
7893
- 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.`;
7936
+ 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.`;
7894
7937
  }
7895
7938
  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.";
7896
7939
  }
7940
+ var WATCH_UNAVAILABLE_GIVE_UP_MS = 120000;
7941
+ var WORKER_KEEP_WAITING = "Call bash_watch again to keep waiting; don't report a result until the command finishes.";
7942
+ function interruptedWatchTail(role, primaryTail) {
7943
+ return role === "worker" ? `The task is still running. ${WORKER_KEEP_WAITING}` : primaryTail;
7944
+ }
7945
+ function workerBackgroundTaskNote(taskId, taskIdArg = "taskId") {
7946
+ return `
7947
+
7948
+ NOTE (subagent session): Continue with other work if you have it. If you don't, call bash_watch({ ${taskIdArg}: "${taskId}" }) to wait for completion before returning to the parent; without a timeout it waits until the command finishes. Subagents don't survive turn-end and won't be woken when the command finishes.`;
7949
+ }
7950
+ function runningTaskStatusHint(role) {
7951
+ return role === "worker" ? "To wait for it, call bash_watch; don't poll." : "A completion reminder will be delivered automatically; don't poll.";
7952
+ }
7953
+ function watchUnavailableSteer(role) {
7954
+ const unknown = "the bridge was busy, so task state is unknown.";
7955
+ if (role === "worker")
7956
+ return `${unknown} ${WORKER_KEEP_WAITING}`;
7957
+ 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.`;
7958
+ }
7897
7959
  var CONFLICT_HINT = `
7898
7960
 
7899
7961
  [Hint] Use aft_conflicts to see all conflict regions across files in a single call.`;
@@ -8274,7 +8336,7 @@ import { existsSync } from "node:fs";
8274
8336
  import { join as join3 } from "node:path";
8275
8337
 
8276
8338
  // ../aft-bridge/dist/child-process.js
8277
- import { execFileSync as nodeExecFileSync, execSync as nodeExecSync, spawn as nodeSpawn, spawnSync as nodeSpawnSync } from "node:child_process";
8339
+ import { execFile as nodeExecFile, execFileSync as nodeExecFileSync, execSync as nodeExecSync, spawn as nodeSpawn, spawnSync as nodeSpawnSync } from "node:child_process";
8278
8340
  var HIDDEN = { windowsHide: true };
8279
8341
  function withWindowsHidden(args, takesArgv) {
8280
8342
  const out = [...args];
@@ -8296,6 +8358,7 @@ function hidden(resolve, takesArgv) {
8296
8358
  }
8297
8359
  var spawn = hidden(() => nodeSpawn, true);
8298
8360
  var spawnSync = hidden(() => nodeSpawnSync, true);
8361
+ var execFile = hidden(() => nodeExecFile, true);
8299
8362
  var execFileSync = hidden(() => nodeExecFileSync, true);
8300
8363
  var execSync = hidden(() => nodeExecSync, false);
8301
8364
 
@@ -8751,6 +8814,9 @@ function isPassiveCommand(command) {
8751
8814
  return PASSIVE_COMMANDS.has(command);
8752
8815
  }
8753
8816
 
8817
+ // ../aft-bridge/dist/transport.js
8818
+ var WORKER_SESSION_FIELD = "worker_session";
8819
+
8754
8820
  // ../aft-bridge/dist/bridge.js
8755
8821
  var DEFAULT_BRIDGE_TIMEOUT_MS = 30000;
8756
8822
  var BRIDGE_HANG_TIMEOUT_THRESHOLD = 2;
@@ -9169,9 +9235,11 @@ class BinaryBridge {
9169
9235
  params.edit_slot_survives = editSlotSurvives;
9170
9236
  this.logHashlineRegistrationCarrier("tool_call", sessionId, editSlotSurvives);
9171
9237
  }
9172
- const { preview, ...sendOptions } = options ?? {};
9238
+ const { preview, workerSession, ...sendOptions } = options ?? {};
9173
9239
  if (preview === true)
9174
9240
  params.preview = true;
9241
+ if (workerSession === true)
9242
+ params[WORKER_SESSION_FIELD] = true;
9175
9243
  return await this.send("tool_call", params, Object.keys(sendOptions).length > 0 ? sendOptions : undefined);
9176
9244
  }
9177
9245
  async sendWithVersionMismatchRetry(command, params, options, canRetryAfterVersionSwap) {
@@ -9240,7 +9308,7 @@ class BinaryBridge {
9240
9308
  if (command === "cancel_request" || Object.hasOwn(params, "command") || Object.hasOwn(params, "method")) {
9241
9309
  const nested = { ...params };
9242
9310
  const reserved = {};
9243
- for (const key of ["session_id", "lsp_hints"]) {
9311
+ for (const key of ["session_id", "lsp_hints", WORKER_SESSION_FIELD]) {
9244
9312
  if (Object.hasOwn(nested, key)) {
9245
9313
  reserved[key] = nested[key];
9246
9314
  delete nested[key];
@@ -9622,17 +9690,30 @@ class BinaryBridge {
9622
9690
  }
9623
9691
  }
9624
9692
  onStderrData(data) {
9625
- this.stderrBuffer += data;
9626
- let newlineIdx;
9627
- while ((newlineIdx = this.stderrBuffer.indexOf(`
9628
- `)) !== -1) {
9629
- const line = this.stderrBuffer.slice(0, newlineIdx).replace(/\r$/, "");
9630
- this.stderrBuffer = this.stderrBuffer.slice(newlineIdx + 1);
9631
- if (!line || !shouldSurfaceStderrLine(line))
9632
- continue;
9633
- const tagged = tagStderrLine(line);
9634
- this.logVia(tagged);
9635
- this.pushStderrLine(tagged);
9693
+ const partialLimit = 64 * 1024;
9694
+ let offset = 0;
9695
+ while (offset < data.length) {
9696
+ const newline = data.indexOf(`
9697
+ `, offset);
9698
+ const end = newline < 0 ? data.length : newline;
9699
+ while (offset < end) {
9700
+ const count = Math.min(partialLimit - this.stderrBuffer.length, end - offset);
9701
+ this.stderrBuffer += data.slice(offset, offset + count);
9702
+ offset += count;
9703
+ if (this.stderrBuffer.length === partialLimit) {
9704
+ const last = this.stderrBuffer.charCodeAt(this.stderrBuffer.length - 1);
9705
+ const carry = last >= 55296 && last <= 56319 ? this.stderrBuffer.slice(-1) : "";
9706
+ const fragment = carry ? this.stderrBuffer.slice(0, -1) : this.stderrBuffer;
9707
+ const tagged = tagStderrLine(`${fragment} [stderr record truncated into bounded fragments; ${Buffer.byteLength(fragment)} bytes shown, continuation follows]`);
9708
+ this.stderrBuffer = carry;
9709
+ this.logVia(tagged);
9710
+ this.pushStderrLine(tagged);
9711
+ }
9712
+ }
9713
+ if (newline < 0)
9714
+ break;
9715
+ this.flushStderrBuffer();
9716
+ offset = newline + 1;
9636
9717
  }
9637
9718
  }
9638
9719
  flushStderrBuffer() {
@@ -10005,6 +10086,35 @@ function coerceJsonCollectionParam(value, field) {
10005
10086
  }
10006
10087
  throw new Error(`${field} must be ${shape} (got an invalid type)`);
10007
10088
  }
10089
+ function isBlankParam(value) {
10090
+ if (typeof value === "string")
10091
+ return value.trim().length === 0;
10092
+ return isEmptyParam(value);
10093
+ }
10094
+ function usableZoomTargets(targets) {
10095
+ if (isEmptyParam(targets))
10096
+ return [];
10097
+ const entries = Array.isArray(targets) ? targets : [targets];
10098
+ return entries.filter((entry) => {
10099
+ if (entry === null || entry === undefined)
10100
+ return false;
10101
+ if (typeof entry !== "object")
10102
+ return true;
10103
+ const record = entry;
10104
+ return !(isBlankParam(record.path) && isBlankParam(record.filePath) && isBlankParam(record.symbol));
10105
+ });
10106
+ }
10107
+ function isFindReplaceOnlyEdit(args) {
10108
+ const absent = (value) => value === undefined || value === null || value === "";
10109
+ const absentLine = (value) => absent(value) || value === 0;
10110
+ const edits = args.edits;
10111
+ return Array.isArray(edits) && edits.length > 0 && absent(args.appendContent) && absent(args.symbol) && absent(args.content) && edits.every((entry) => {
10112
+ if (entry === null || typeof entry !== "object")
10113
+ return false;
10114
+ const item = entry;
10115
+ return typeof item.oldString === "string" && (absent(item.newString) || typeof item.newString === "string") && absentLine(item.startLine) && absentLine(item.endLine) && absent(item.content);
10116
+ });
10117
+ }
10008
10118
  // ../aft-bridge/dist/config-error.js
10009
10119
  import { homedir as homedir5 } from "node:os";
10010
10120
  import { isAbsolute as isAbsolute2, join as join6 } from "node:path";
@@ -10121,9 +10231,366 @@ function formatDroppedKeyWarnings(dropped) {
10121
10231
  }
10122
10232
  return dropped.map((item) => `Ignoring ${item.key} from ${item.tier} config (${item.reason})`);
10123
10233
  }
10234
+ // ../aft-bridge/dist/config-watch.js
10235
+ import { readFileSync as readFileSync3, statSync as statSync3, watch } from "node:fs";
10236
+ import { basename, dirname } from "node:path";
10237
+ var CONFIG_WATCH_DEBOUNCE_MS = 150;
10238
+ var CONFIG_LIVE_KEEP_NOTE = "AFT keeps using the last valid configuration until the file is fixed.";
10239
+ function pathKey(name) {
10240
+ const segments = name.split(".");
10241
+ return {
10242
+ name,
10243
+ read(config) {
10244
+ let value = config;
10245
+ for (const segment of segments) {
10246
+ if (value === null || typeof value !== "object")
10247
+ return;
10248
+ value = value[segment];
10249
+ }
10250
+ return value;
10251
+ },
10252
+ write(config, value) {
10253
+ const set = (target, index) => {
10254
+ const copy = target !== null && typeof target === "object" && !Array.isArray(target) ? { ...target } : {};
10255
+ const segment = segments[index];
10256
+ if (index === segments.length - 1) {
10257
+ if (value === undefined)
10258
+ delete copy[segment];
10259
+ else
10260
+ copy[segment] = value;
10261
+ } else {
10262
+ copy[segment] = set(copy[segment], index + 1);
10263
+ }
10264
+ return copy;
10265
+ };
10266
+ return set(config, 0);
10267
+ }
10268
+ };
10269
+ }
10270
+ function bashKey(name, resolveBash) {
10271
+ return {
10272
+ name: `bash.${name}`,
10273
+ read: (config) => resolveBash(config)[name],
10274
+ write(config, value) {
10275
+ const resolved = { ...resolveBash(config), [name]: value };
10276
+ for (const key of Object.keys(resolved)) {
10277
+ if (resolved[key] === undefined)
10278
+ delete resolved[key];
10279
+ }
10280
+ return { ...config, bash: resolved };
10281
+ }
10282
+ };
10283
+ }
10284
+ function aftLiveConfigKeys(resolveBash) {
10285
+ return [
10286
+ pathKey("configure_warnings_delivery"),
10287
+ pathKey("restrict_to_project_root"),
10288
+ pathKey("inspect.diagnostics_timeout_ms"),
10289
+ pathKey("inspect.tier2_idle_minutes"),
10290
+ bashKey("foreground_wait_window_ms", resolveBash),
10291
+ bashKey("host_fallback", resolveBash),
10292
+ bashKey("subagent_background", resolveBash),
10293
+ bashKey("watch_sync_max_ms", resolveBash)
10294
+ ];
10295
+ }
10296
+ function sameValue(a, b) {
10297
+ return JSON.stringify(a) === JSON.stringify(b);
10298
+ }
10299
+ function flattenLeaves(value, prefix, out) {
10300
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
10301
+ const entries = Object.entries(value);
10302
+ if (entries.length === 0 && prefix)
10303
+ out.set(prefix, "{}");
10304
+ for (const [key, child] of entries) {
10305
+ flattenLeaves(child, prefix ? `${prefix}.${key}` : key, out);
10306
+ }
10307
+ return;
10308
+ }
10309
+ if (prefix)
10310
+ out.set(prefix, JSON.stringify(value));
10311
+ }
10312
+ function applyLiveConfigKeys(current, next, keys, baseline = current) {
10313
+ let config = current;
10314
+ const applied = [];
10315
+ for (const key of keys) {
10316
+ const value = key.read(next);
10317
+ if (sameValue(key.read(current), value))
10318
+ continue;
10319
+ config = key.write(config, value);
10320
+ applied.push(key.name);
10321
+ }
10322
+ const before = new Map;
10323
+ const after = new Map;
10324
+ flattenLeaves(baseline, "", before);
10325
+ flattenLeaves(next, "", after);
10326
+ const liveNames = keys.map((key) => key.name);
10327
+ const isLive = (leaf) => liveNames.some((name) => leaf === name || leaf.startsWith(`${name}.`));
10328
+ const deferred = new Set;
10329
+ for (const leaf of new Set([...before.keys(), ...after.keys()])) {
10330
+ if (before.get(leaf) === after.get(leaf) || isLive(leaf))
10331
+ continue;
10332
+ deferred.add(leaf);
10333
+ }
10334
+ return { config, applied, deferred: [...deferred].sort() };
10335
+ }
10336
+ function readTextOrNull(path) {
10337
+ try {
10338
+ return readFileSync3(path, "utf8");
10339
+ } catch {
10340
+ return null;
10341
+ }
10342
+ }
10343
+ var CONFIG_WATCH_REVALIDATE_MS = 2000;
10344
+ var CONFIG_WATCH_REJECTED_RETRIES = 3;
10345
+ var CONFIG_WATCH_MAX_DELAY_MS = 1000;
10346
+ function identityOf(dir) {
10347
+ try {
10348
+ const stat = statSync3(dir, { bigint: true });
10349
+ const base = `${stat.dev}:${stat.ino}`;
10350
+ return process.platform === "win32" ? base : `${base}:${stat.birthtimeNs}`;
10351
+ } catch {
10352
+ return null;
10353
+ }
10354
+ }
10355
+ function watchDirFor(file) {
10356
+ let dir = dirname(file);
10357
+ while (dir !== dirname(dir) && identityOf(dir) === null)
10358
+ dir = dirname(dir);
10359
+ return dir;
10360
+ }
10361
+ function watchAftConfigFiles(options) {
10362
+ const debounceMs = options.debounceMs ?? CONFIG_WATCH_DEBOUNCE_MS;
10363
+ const lastSeen = new Map;
10364
+ for (const path of options.paths) {
10365
+ if (options.initialTexts)
10366
+ lastSeen.set(path, options.initialTexts[path] ?? null);
10367
+ else
10368
+ lastSeen.set(path, options.checkAtStart ? undefined : readTextOrNull(path));
10369
+ }
10370
+ let timer = null;
10371
+ let firstPendingAt = null;
10372
+ let stopped = false;
10373
+ const watchers = new Map;
10374
+ const watchImpl = options.watchImpl ?? watch;
10375
+ let rejectedRetries = 0;
10376
+ const check = () => {
10377
+ timer = null;
10378
+ firstPendingAt = null;
10379
+ if (stopped)
10380
+ return;
10381
+ revalidate();
10382
+ const seen = new Map;
10383
+ for (const path of options.paths) {
10384
+ const text = readTextOrNull(path);
10385
+ if (text !== lastSeen.get(path))
10386
+ seen.set(path, text);
10387
+ }
10388
+ if (seen.size === 0)
10389
+ return;
10390
+ const outcome = options.onChange();
10391
+ if (outcome === false) {
10392
+ if (rejectedRetries < CONFIG_WATCH_REJECTED_RETRIES) {
10393
+ rejectedRetries += 1;
10394
+ retryTimer = setTimeout(check, CONFIG_WATCH_MAX_DELAY_MS);
10395
+ retryTimer.unref?.();
10396
+ }
10397
+ return;
10398
+ }
10399
+ rejectedRetries = 0;
10400
+ if (outcome instanceof Map) {
10401
+ for (const path of options.paths)
10402
+ lastSeen.set(path, outcome.get(path) ?? null);
10403
+ } else {
10404
+ for (const [path, text] of seen)
10405
+ lastSeen.set(path, text);
10406
+ }
10407
+ };
10408
+ let retryTimer = null;
10409
+ const schedule = () => {
10410
+ if (stopped)
10411
+ return;
10412
+ const now = Date.now();
10413
+ firstPendingAt ??= now;
10414
+ if (timer)
10415
+ clearTimeout(timer);
10416
+ const delay = Math.max(0, Math.min(debounceMs, firstPendingAt + CONFIG_WATCH_MAX_DELAY_MS - now));
10417
+ timer = setTimeout(check, delay);
10418
+ timer.unref?.();
10419
+ };
10420
+ function revalidate() {
10421
+ const wanted = new Set(options.paths.map(watchDirFor));
10422
+ let moved = false;
10423
+ for (const [dir, entry] of watchers) {
10424
+ if (!wanted.has(dir) || entry.stale || identityOf(dir) !== entry.identity) {
10425
+ entry.watcher.close();
10426
+ watchers.delete(dir);
10427
+ moved = true;
10428
+ }
10429
+ }
10430
+ for (const dir of wanted) {
10431
+ if (watchers.has(dir))
10432
+ continue;
10433
+ try {
10434
+ const identity = identityOf(dir);
10435
+ options.beforeWatchForTest?.(dir);
10436
+ const name = basename(dir);
10437
+ const watcher = watchImpl(dir, { persistent: false }, (event, filename) => {
10438
+ if (event === "rename" && filename === name) {
10439
+ const current = watchers.get(dir);
10440
+ if (current && current.watcher === watcher)
10441
+ current.stale = true;
10442
+ }
10443
+ schedule();
10444
+ });
10445
+ if (identityOf(dir) !== identity) {
10446
+ watcher.close();
10447
+ moved = true;
10448
+ continue;
10449
+ }
10450
+ const entry = { watcher, identity, stale: false };
10451
+ entry.watcher.on("error", () => {
10452
+ entry.watcher.close();
10453
+ if (watchers.get(dir) === entry)
10454
+ watchers.delete(dir);
10455
+ schedule();
10456
+ });
10457
+ watchers.set(dir, entry);
10458
+ moved = true;
10459
+ } catch {}
10460
+ }
10461
+ return moved;
10462
+ }
10463
+ revalidate();
10464
+ if (options.checkAtStart)
10465
+ check();
10466
+ const revalidateTimer = setInterval(() => {
10467
+ if (!stopped && revalidate())
10468
+ schedule();
10469
+ }, CONFIG_WATCH_REVALIDATE_MS);
10470
+ revalidateTimer.unref?.();
10471
+ return () => {
10472
+ stopped = true;
10473
+ if (timer)
10474
+ clearTimeout(timer);
10475
+ if (retryTimer)
10476
+ clearTimeout(retryTimer);
10477
+ clearInterval(revalidateTimer);
10478
+ for (const entry of watchers.values())
10479
+ entry.watcher.close();
10480
+ watchers.clear();
10481
+ };
10482
+ }
10483
+ function aftLiveSecurityKeys(keys) {
10484
+ const byName = new Map(keys.map((key) => [key.name, key]));
10485
+ const pick = (name) => {
10486
+ const key = byName.get(name);
10487
+ if (!key)
10488
+ throw new Error(`unknown live config key ${name}`);
10489
+ return key;
10490
+ };
10491
+ return [
10492
+ {
10493
+ key: pick("restrict_to_project_root"),
10494
+ loosens: (current, next) => current === true && next !== true
10495
+ },
10496
+ {
10497
+ key: pick("bash.host_fallback"),
10498
+ loosens: (current, next) => current !== true && next === true
10499
+ }
10500
+ ];
10501
+ }
10502
+ function liveConfigReloadLogLine(applied, deferred) {
10503
+ let line = "config reload";
10504
+ if (applied.length > 0)
10505
+ line += ` applied=[${applied.join(",")}]`;
10506
+ if (deferred.length > 0) {
10507
+ line += ` deferred=[${deferred.join(",")}] (deferred keys apply on next connect/restart)`;
10508
+ }
10509
+ return line;
10510
+ }
10511
+ function startLiveConfigReload(options) {
10512
+ const baseline = options.getConfig();
10513
+ let lastError = null;
10514
+ let acceptedSources = new Set(options.initialSources);
10515
+ let acceptedProjectText = options.initialProjectText ?? null;
10516
+ let acceptedTexts;
10517
+ const reload = () => {
10518
+ let loaded = options.load();
10519
+ if (loaded.ok) {
10520
+ const read = new Set(loaded.sources);
10521
+ const deleted = [...acceptedSources].find((path) => !read.has(path));
10522
+ if (deleted !== undefined) {
10523
+ loaded = { ok: false, message: `AFT config at ${deleted} was deleted` };
10524
+ } else {
10525
+ acceptedSources = read;
10526
+ }
10527
+ }
10528
+ if (!loaded.ok) {
10529
+ const message = `${loaded.message.trim().replace(/[.!?]?$/, ".")} ${CONFIG_LIVE_KEEP_NOTE}`;
10530
+ if (message !== lastError) {
10531
+ lastError = message;
10532
+ options.reportError(message);
10533
+ }
10534
+ return null;
10535
+ }
10536
+ lastError = null;
10537
+ acceptedTexts = loaded.texts;
10538
+ const current = options.getConfig();
10539
+ let next = loaded.config;
10540
+ const held = [];
10541
+ const projectText = loaded.projectText ?? null;
10542
+ if (loaded.projectText !== undefined && projectText !== acceptedProjectText) {
10543
+ for (const security of options.securityKeys ?? []) {
10544
+ const now = security.key.read(current);
10545
+ if (security.loosens(now, security.key.read(next))) {
10546
+ next = security.key.write(next, now);
10547
+ held.push(security.key.name);
10548
+ }
10549
+ }
10550
+ if (held.length === 0)
10551
+ acceptedProjectText = projectText;
10552
+ }
10553
+ const result = applyLiveConfigKeys(current, next, options.keys, baseline);
10554
+ if (result.applied.length > 0)
10555
+ options.setConfig(result.config);
10556
+ if (result.applied.length > 0 || result.deferred.length > 0 || held.length > 0) {
10557
+ let line = liveConfigReloadLogLine(result.applied, result.deferred);
10558
+ if (held.length > 0) {
10559
+ line += ` held=[${held.join(",")}] (not loosened while the project file differs from the one the published values came from; the next restart applies them)`;
10560
+ }
10561
+ options.log(line);
10562
+ }
10563
+ return { ...result, held };
10564
+ };
10565
+ const safeReload = () => {
10566
+ try {
10567
+ acceptedTexts = undefined;
10568
+ if (reload() === null)
10569
+ return false;
10570
+ const texts = acceptedTexts;
10571
+ if (!texts)
10572
+ return true;
10573
+ return new Map(options.paths.map((path) => [path, texts[path] ?? null]));
10574
+ } catch (err) {
10575
+ options.reportError(`AFT config reload failed: ${err instanceof Error ? err.message : String(err)}`);
10576
+ return false;
10577
+ }
10578
+ };
10579
+ if (options.watch === false) {
10580
+ return { reload, stop: () => {} };
10581
+ }
10582
+ const stop = watchAftConfigFiles({
10583
+ paths: options.paths,
10584
+ debounceMs: options.debounceMs,
10585
+ onChange: safeReload,
10586
+ initialTexts: options.initialTexts,
10587
+ checkAtStart: true
10588
+ });
10589
+ return { reload, stop };
10590
+ }
10124
10591
  // ../aft-bridge/dist/downloader.js
10125
10592
  import { createHash as createHash2, randomUUID } from "node:crypto";
10126
- 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";
10593
+ 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";
10127
10594
  import { hostname } from "node:os";
10128
10595
  import { join as join7 } from "node:path";
10129
10596
  import { Readable } from "node:stream";
@@ -10475,7 +10942,7 @@ function isReclaimableDownloadLock(owner, ageMs, staleMs) {
10475
10942
  }
10476
10943
  function reclaimDownloadLock(lockPath, expectedOwner) {
10477
10944
  try {
10478
- if (readFileSync3(lockPath, "utf-8") !== expectedOwner)
10945
+ if (readFileSync4(lockPath, "utf-8") !== expectedOwner)
10479
10946
  return false;
10480
10947
  rmSync(lockPath, { force: true });
10481
10948
  return true;
@@ -10493,7 +10960,7 @@ function sweepStaleDownloadTemps(versionedCacheDir, binaryName) {
10493
10960
  continue;
10494
10961
  }
10495
10962
  const tempPath = join7(versionedCacheDir, entry.name);
10496
- const ageMs = Date.now() - statSync3(tempPath).mtimeMs;
10963
+ const ageMs = Date.now() - statSync4(tempPath).mtimeMs;
10497
10964
  if (Math.abs(ageMs) > DOWNLOAD_LOCK_STALE_MS)
10498
10965
  unlinkSync2(tempPath);
10499
10966
  }
@@ -10515,7 +10982,7 @@ async function acquireDownloadLock(lockPath, timing = {}) {
10515
10982
  }
10516
10983
  return () => {
10517
10984
  try {
10518
- if (readFileSync3(lockPath, "utf-8") === owner) {
10985
+ if (readFileSync4(lockPath, "utf-8") === owner) {
10519
10986
  rmSync(lockPath, { force: true });
10520
10987
  }
10521
10988
  } catch {}
@@ -10527,8 +10994,8 @@ async function acquireDownloadLock(lockPath, timing = {}) {
10527
10994
  let existingOwner;
10528
10995
  let ageMs;
10529
10996
  try {
10530
- existingOwner = readFileSync3(lockPath, "utf-8");
10531
- ageMs = Date.now() - statSync3(lockPath).mtimeMs;
10997
+ existingOwner = readFileSync4(lockPath, "utf-8");
10998
+ ageMs = Date.now() - statSync4(lockPath).mtimeMs;
10532
10999
  } catch (readErr) {
10533
11000
  if (readErr.code === "ENOENT")
10534
11001
  continue;
@@ -10578,7 +11045,7 @@ async function fetchLatestTag() {
10578
11045
  }
10579
11046
  // ../aft-bridge/dist/durable-log.js
10580
11047
  import { appendFile, chmod, mkdir, rename, rm, stat } from "node:fs/promises";
10581
- import { dirname } from "node:path";
11048
+ import { dirname as dirname2 } from "node:path";
10582
11049
  var DEFAULT_LOG_BYTES = 32 * 1024 * 1024;
10583
11050
  var DEFAULT_LOG_GENERATIONS = 1;
10584
11051
  var LOG_DIR_MODE = 448;
@@ -10591,6 +11058,10 @@ class RotatingLogSink {
10591
11058
  queue = Promise.resolve();
10592
11059
  disabled = false;
10593
11060
  failureReported = false;
11061
+ queuedBytes = 0;
11062
+ queuedRecords = 0;
11063
+ droppedBytes = 0;
11064
+ overflowQueued = false;
10594
11065
  constructor(path, options = {}) {
10595
11066
  this.path = path;
10596
11067
  this.maxBytes = options.maxBytes ?? DEFAULT_LOG_BYTES;
@@ -10599,7 +11070,34 @@ class RotatingLogSink {
10599
11070
  append(data) {
10600
11071
  if (this.disabled || data.length === 0)
10601
11072
  return;
10602
- this.queue = this.queue.then(() => this.write(data)).catch((error) => {
11073
+ const bytes = Buffer.byteLength(data);
11074
+ const overflow = this.queuedBytes + bytes > 1024 * 1024 || this.queuedRecords >= 4096;
11075
+ if (overflow) {
11076
+ this.droppedBytes += bytes;
11077
+ if (this.overflowQueued)
11078
+ return;
11079
+ this.overflowQueued = true;
11080
+ data = "";
11081
+ } else {
11082
+ this.queuedBytes += bytes;
11083
+ this.queuedRecords += 1;
11084
+ }
11085
+ this.queue = this.queue.then(async () => {
11086
+ if (overflow) {
11087
+ const dropped = this.droppedBytes;
11088
+ this.droppedBytes = 0;
11089
+ this.overflowQueued = false;
11090
+ await this.write(`[aft-plugin] durable log queue overflow: dropped ${dropped} bytes (1MiB/4096-record pending limit); reduce diagnostic volume.
11091
+ `);
11092
+ } else {
11093
+ try {
11094
+ await this.write(data);
11095
+ } finally {
11096
+ this.queuedBytes -= bytes;
11097
+ this.queuedRecords -= 1;
11098
+ }
11099
+ }
11100
+ }).catch((error) => {
10603
11101
  this.disabled = true;
10604
11102
  if (!this.failureReported) {
10605
11103
  this.failureReported = true;
@@ -10614,8 +11112,8 @@ class RotatingLogSink {
10614
11112
  await this.queue;
10615
11113
  }
10616
11114
  async write(data) {
10617
- const dir = dirname(this.path);
10618
- await mkdir(dirname(dir), { recursive: true });
11115
+ const dir = dirname2(this.path);
11116
+ await mkdir(dirname2(dir), { recursive: true });
10619
11117
  try {
10620
11118
  await mkdir(dir, { mode: LOG_DIR_MODE });
10621
11119
  } catch (error) {
@@ -12403,7 +12901,7 @@ function reverseRequestMethod(body) {
12403
12901
  }
12404
12902
  }
12405
12903
  // ../aft-bridge/dist/subc-transport.js
12406
- import { existsSync as existsSync4, statSync as statSync4 } from "node:fs";
12904
+ import { existsSync as existsSync4, statSync as statSync5 } from "node:fs";
12407
12905
 
12408
12906
  // ../aft-bridge/dist/lifecycle-registry.js
12409
12907
  import { lstat } from "node:fs/promises";
@@ -12879,6 +13377,13 @@ class SubcTransportShuttingDownError extends SubcCallError {
12879
13377
  this.name = "SubcTransportShuttingDownError";
12880
13378
  }
12881
13379
  }
13380
+
13381
+ class ReconnectKeyChangedError extends Error {
13382
+ constructor() {
13383
+ super("the subc daemon restarted and its connection key changed; the reconnect did not complete within the deadline, retry");
13384
+ this.name = "ReconnectKeyChangedError";
13385
+ }
13386
+ }
12882
13387
  function isSubcClientClosedError(error) {
12883
13388
  return error instanceof SubcError && error.message === "client closed";
12884
13389
  }
@@ -12930,6 +13435,7 @@ var RECONNECT_RETRY_FLOOR_MS = 100;
12930
13435
  var RECONNECT_RETRY_CAP_MS = 2000;
12931
13436
  var SUBC_DEFAULT_REQUEST_TIMEOUT_MS = 30000;
12932
13437
  var ROUTE_OPEN_RELOAD_WAIT_CEILING_MS = 45000;
13438
+ var CALL_DEADLINE_EXPIRED = Symbol("call deadline expired");
12933
13439
  function reloadWaitExhaustedSuffix(callDeadlineMs) {
12934
13440
  if (callDeadlineMs > ROUTE_OPEN_RELOAD_WAIT_CEILING_MS) {
12935
13441
  const ceilingSeconds = ROUTE_OPEN_RELOAD_WAIT_CEILING_MS / 1000;
@@ -13282,7 +13788,16 @@ function absentRootError(root) {
13282
13788
  return new SubcError(`invalid route project root: project root does not exist: ${root}`, "config_divergence");
13283
13789
  }
13284
13790
  function reloadWindowExhaustedError(error, callDeadlineMs) {
13285
- const suffix = reloadWaitExhaustedSuffix(callDeadlineMs);
13791
+ return withDeadlineSuffix(error, reloadWaitExhaustedSuffix(callDeadlineMs));
13792
+ }
13793
+ function callDeadlinePassedError(lastError, callDeadlineMs) {
13794
+ const seconds = Math.round(callDeadlineMs / 100) / 10;
13795
+ const suffix = ` The AFT daemon module did not return within this call's ${seconds}s deadline.`;
13796
+ if (lastError !== undefined)
13797
+ return withDeadlineSuffix(lastError, suffix);
13798
+ return new SubcError(`route to module '${AFT_MODULE_ID}' was not ready before the call ran out of time.${suffix}`, REQUEST_DEADLINE_CODE);
13799
+ }
13800
+ function withDeadlineSuffix(error, suffix) {
13286
13801
  if (error instanceof Error) {
13287
13802
  try {
13288
13803
  error.message += suffix;
@@ -13371,6 +13886,8 @@ class SubcTransport {
13371
13886
  body.edit_slot_survives = editSlotSurvives;
13372
13887
  if (preview === true)
13373
13888
  body.preview = true;
13889
+ if (options?.workerSession === true)
13890
+ body[WORKER_SESSION_FIELD] = true;
13374
13891
  const reply = await this.pool.routeRequest(this.identityFor(sessionId), body, timeoutMs, onProgress, this.generation, abortSignal);
13375
13892
  return reliftReply(reply);
13376
13893
  }
@@ -13381,7 +13898,10 @@ class SubcTransport {
13381
13898
  }
13382
13899
  const { timeoutMs, onProgress, abortSignal } = this.splitOptions(options);
13383
13900
  const session = typeof params.session_id === "string" ? params.session_id : undefined;
13384
- const body = { name: command, arguments: params };
13901
+ const { [WORKER_SESSION_FIELD]: workerSession, ...args } = params;
13902
+ const body = { name: command, arguments: args };
13903
+ if (workerSession === true)
13904
+ body[WORKER_SESSION_FIELD] = true;
13385
13905
  const editSlotSurvives = this.pool.getEditSlotSurvives();
13386
13906
  if (editSlotSurvives !== undefined)
13387
13907
  body.edit_slot_survives = editSlotSurvives;
@@ -13418,6 +13938,7 @@ class SubcTransportPool {
13418
13938
  lifecycleRegistration = null;
13419
13939
  outerFacadeEvictor;
13420
13940
  client = null;
13941
+ hadWorkingClient = false;
13421
13942
  connecting = null;
13422
13943
  routeReopenRetryDelayMs = RECONNECT_RETRY_FLOOR_MS;
13423
13944
  routeReopenRetry = null;
@@ -13521,7 +14042,7 @@ class SubcTransportPool {
13521
14042
  rootCanAttach(root) {
13522
14043
  let directoryExists = false;
13523
14044
  try {
13524
- directoryExists = statSync4(root).isDirectory();
14045
+ directoryExists = statSync5(root).isDirectory();
13525
14046
  } catch {}
13526
14047
  if (directoryExists) {
13527
14048
  this.dormantRoots.delete(root);
@@ -13861,6 +14382,11 @@ class SubcTransportPool {
13861
14382
  return this.rootReapedError(record);
13862
14383
  }
13863
14384
  async routeRequest(identity, body, timeoutMs, onProgress, expectedGeneration, abortSignal) {
14385
+ const callStartedAt = performance.now();
14386
+ const callDeadlineMs = timeoutMs ?? SUBC_DEFAULT_REQUEST_TIMEOUT_MS;
14387
+ let scheduledDelayMs = 0;
14388
+ const spentMs = () => Math.max(performance.now() - callStartedAt, scheduledDelayMs);
14389
+ const remainingMs = () => callDeadlineMs - spentMs();
13864
14390
  const root = asCanonicalRootPath(identity.project_root);
13865
14391
  let generation = expectedGeneration;
13866
14392
  if (this.lifecycleEnabled()) {
@@ -13876,7 +14402,49 @@ class SubcTransportPool {
13876
14402
  try {
13877
14403
  let client;
13878
14404
  try {
13879
- client = await this.ensureClient();
14405
+ let keyChanged;
14406
+ while (true) {
14407
+ const remaining = remainingMs();
14408
+ if (remaining <= 0)
14409
+ throw keyChanged ?? callDeadlinePassedError(undefined, callDeadlineMs);
14410
+ let timer;
14411
+ try {
14412
+ const acquired = await Promise.race([
14413
+ this.ensureClient(),
14414
+ new Promise((resolve) => {
14415
+ timer = setTimeout(() => resolve(CALL_DEADLINE_EXPIRED), remaining);
14416
+ })
14417
+ ]);
14418
+ if (acquired === CALL_DEADLINE_EXPIRED) {
14419
+ throw keyChanged ?? callDeadlinePassedError(undefined, callDeadlineMs);
14420
+ }
14421
+ client = acquired;
14422
+ break;
14423
+ } catch (error) {
14424
+ if (!(error instanceof ReconnectKeyChangedError))
14425
+ throw error;
14426
+ keyChanged = error;
14427
+ if (spentMs() + this.nextRouteReopenDelayMs() >= callDeadlineMs)
14428
+ throw error;
14429
+ } finally {
14430
+ clearTimeout(timer);
14431
+ }
14432
+ const { delayMs, wait } = this.waitForRouteReopenBackoff();
14433
+ scheduledDelayMs = spentMs() + delayMs;
14434
+ let retryTimer;
14435
+ try {
14436
+ const waited = await Promise.race([
14437
+ wait,
14438
+ new Promise((resolve) => {
14439
+ retryTimer = setTimeout(() => resolve(CALL_DEADLINE_EXPIRED), callDeadlineMs - (performance.now() - callStartedAt));
14440
+ })
14441
+ ]);
14442
+ if (waited === CALL_DEADLINE_EXPIRED)
14443
+ throw keyChanged;
14444
+ } finally {
14445
+ clearTimeout(retryTimer);
14446
+ }
14447
+ }
13880
14448
  this.assertRecordLive(record);
13881
14449
  } catch (error) {
13882
14450
  throw this.annotateReapError(error, record);
@@ -13911,21 +14479,39 @@ class SubcTransportPool {
13911
14479
  throw error;
13912
14480
  }
13913
14481
  };
13914
- const callDeadlineMs = timeoutMs ?? SUBC_DEFAULT_REQUEST_TIMEOUT_MS;
13915
14482
  const reloadWaitBudgetMs = Math.min(callDeadlineMs, ROUTE_OPEN_RELOAD_WAIT_CEILING_MS);
13916
- let reloadWaitedMs = 0;
14483
+ let lastRefusal;
14484
+ const openRouteWithinDeadline = async () => {
14485
+ const remaining = remainingMs();
14486
+ if (remaining <= 0)
14487
+ throw callDeadlinePassedError(lastRefusal, callDeadlineMs);
14488
+ let timer;
14489
+ const expired = new Promise((resolve) => {
14490
+ timer = setTimeout(() => resolve(CALL_DEADLINE_EXPIRED), remaining);
14491
+ });
14492
+ try {
14493
+ const opened = await Promise.race([openRoute(), expired]);
14494
+ if (opened === CALL_DEADLINE_EXPIRED) {
14495
+ throw callDeadlinePassedError(lastRefusal, callDeadlineMs);
14496
+ }
14497
+ return opened;
14498
+ } finally {
14499
+ clearTimeout(timer);
14500
+ }
14501
+ };
13917
14502
  const openRouteAfterReloadWindow = async () => {
13918
14503
  while (true) {
13919
14504
  try {
13920
- return await openRoute();
14505
+ return await openRouteWithinDeadline();
13921
14506
  } catch (error) {
13922
14507
  if (!isRouteOpenReloadWindowError(error))
13923
14508
  throw error;
13924
- if (reloadWaitedMs + this.nextRouteReopenDelayMs() >= reloadWaitBudgetMs) {
14509
+ lastRefusal = error;
14510
+ if (spentMs() + this.nextRouteReopenDelayMs() >= reloadWaitBudgetMs) {
13925
14511
  throw reloadWindowExhaustedError(error, callDeadlineMs);
13926
14512
  }
13927
14513
  const { delayMs, wait } = this.waitForRouteReopenBackoff();
13928
- reloadWaitedMs += delayMs;
14514
+ scheduledDelayMs = spentMs() + delayMs;
13929
14515
  await wait;
13930
14516
  }
13931
14517
  }
@@ -13961,7 +14547,7 @@ class SubcTransportPool {
13961
14547
  route: routeLabel(entry.handle)
13962
14548
  });
13963
14549
  };
13964
- const requestOnRoute = async (route, entry) => {
14550
+ const requestOnRoute = async (route, entry, requestTimeoutMs) => {
13965
14551
  this.assertRecordLive(record);
13966
14552
  if (abortSignal?.aborted) {
13967
14553
  clearRouteEntry(entry);
@@ -13977,8 +14563,10 @@ class SubcTransportPool {
13977
14563
  };
13978
14564
  abortSignal?.addEventListener("abort", onAbort, { once: true });
13979
14565
  try {
13980
- const requestTimeoutMs = reloadWaitedMs > 0 ? callDeadlineMs - reloadWaitedMs : timeoutMs;
13981
- const request = client.request(route, body, { timeoutMs: requestTimeoutMs, onProgress });
14566
+ const request = client.request(route, body, {
14567
+ timeoutMs: Math.ceil(requestTimeoutMs),
14568
+ onProgress
14569
+ });
13982
14570
  const reply = abortSignal ? await Promise.race([request, aborted]) : await request;
13983
14571
  if (!this.isCurrentSession(key, record)) {
13984
14572
  if (this.isReapInduced(record))
@@ -13999,8 +14587,11 @@ class SubcTransportPool {
13999
14587
  let reopened = false;
14000
14588
  let retriedAbsentRoute = false;
14001
14589
  while (true) {
14590
+ const requestTimeoutMs = remainingMs();
14591
+ if (requestTimeoutMs <= 0)
14592
+ throw callDeadlinePassedError(lastRefusal, callDeadlineMs);
14002
14593
  try {
14003
- const reply = await requestOnRoute(routeAndEntry.route, routeAndEntry.entry);
14594
+ const reply = await requestOnRoute(routeAndEntry.route, routeAndEntry.entry, requestTimeoutMs);
14004
14595
  if (reopened)
14005
14596
  this.resetRouteReopenBackoff();
14006
14597
  return reply;
@@ -14010,11 +14601,12 @@ class SubcTransportPool {
14010
14601
  const ownsRoute = this.isCurrentSession(key, record) && this.client === client;
14011
14602
  if (ownsRoute && isRouteRequestReloadRefusal(error)) {
14012
14603
  clearRouteEntry(routeAndEntry.entry);
14013
- if (reloadWaitedMs + this.nextRouteReopenDelayMs() >= reloadWaitBudgetMs) {
14604
+ lastRefusal = error;
14605
+ if (spentMs() + this.nextRouteReopenDelayMs() >= reloadWaitBudgetMs) {
14014
14606
  throw reloadWindowExhaustedError(error, callDeadlineMs);
14015
14607
  }
14016
14608
  const { delayMs, wait } = this.waitForRouteReopenBackoff();
14017
- reloadWaitedMs += delayMs;
14609
+ scheduledDelayMs = spentMs() + delayMs;
14018
14610
  await wait;
14019
14611
  routeAndEntry = await openRouteAfterReloadWindow();
14020
14612
  reopened = true;
@@ -14023,7 +14615,12 @@ class SubcTransportPool {
14023
14615
  if (ownsRoute && !retriedAbsentRoute && isRouteProvenAbsentError(error)) {
14024
14616
  retriedAbsentRoute = true;
14025
14617
  clearRouteEntry(routeAndEntry.entry);
14026
- await this.waitForRouteReopenBackoff().wait;
14618
+ if (spentMs() + this.nextRouteReopenDelayMs() >= callDeadlineMs) {
14619
+ throw callDeadlinePassedError(error, callDeadlineMs);
14620
+ }
14621
+ const { delayMs, wait } = this.waitForRouteReopenBackoff();
14622
+ scheduledDelayMs = spentMs() + delayMs;
14623
+ await wait;
14027
14624
  routeAndEntry = await openRouteAfterReloadWindow();
14028
14625
  reopened = true;
14029
14626
  continue;
@@ -14086,10 +14683,14 @@ class SubcTransportPool {
14086
14683
  throw new SubcTransportShuttingDownError;
14087
14684
  }
14088
14685
  this.client = client;
14686
+ this.hadWorkingClient = true;
14089
14687
  this.transportFailures = 0;
14090
14688
  return client;
14091
14689
  }).catch((error) => {
14092
14690
  this.connecting = null;
14691
+ if (this.hadWorkingClient && error instanceof AuthError) {
14692
+ throw new ReconnectKeyChangedError;
14693
+ }
14093
14694
  throw error;
14094
14695
  });
14095
14696
  return this.connecting;
@@ -15781,6 +16382,7 @@ async function acquireBridge(locationDirectory, options, dependencies = {}) {
15781
16382
  let owner = state.standalone;
15782
16383
  if (!owner) {
15783
16384
  const configByRoot = new Map;
16385
+ const versionMismatchHandlers = new Map;
15784
16386
  const configuredLoader = options.poolOptions.projectConfigLoader;
15785
16387
  const pool = await createPool({
15786
16388
  ...options,
@@ -15790,21 +16392,29 @@ async function acquireBridge(locationDirectory, options, dependencies = {}) {
15790
16392
  projectConfigLoader: (projectRoot) => ({
15791
16393
  ...configuredLoader?.(projectRoot) ?? {},
15792
16394
  ...configByRoot.get(canonicalizeProjectRoot(projectRoot)) ?? {}
15793
- })
16395
+ }),
16396
+ onVersionMismatch: (binaryVersion, minVersion) => [...versionMismatchHandlers.values()].at(-1)?.(binaryVersion, minVersion) ?? null
15794
16397
  }
15795
16398
  });
15796
- owner = { pool, configByRoot, referencesByRoot: new Map, references: 0 };
16399
+ owner = {
16400
+ pool,
16401
+ configByRoot,
16402
+ versionMismatchHandlers,
16403
+ referencesByRoot: new Map,
16404
+ references: 0
16405
+ };
15797
16406
  state.standalone = owner;
15798
16407
  }
15799
16408
  owner.references += 1;
15800
16409
  owner.referencesByRoot.set(directory, (owner.referencesByRoot.get(directory) ?? 0) + 1);
15801
16410
  owner.configByRoot.set(directory, { ...options.configOverrides });
15802
16411
  const acquiredOwner = owner;
15803
- return new LocationBridgeLease(directory, mode, owner.pool, async () => {
16412
+ const lease = new LocationBridgeLease(directory, mode, owner.pool, async () => {
15804
16413
  await withLock(directory, () => withLock(OWNER_LOCK, async () => {
15805
16414
  const current = lifecycleState().standalone;
15806
16415
  if (current !== acquiredOwner)
15807
16416
  return;
16417
+ current.versionMismatchHandlers?.delete(lease);
15808
16418
  current.references -= 1;
15809
16419
  const rootReferences = (current.referencesByRoot.get(directory) ?? 1) - 1;
15810
16420
  if (rootReferences > 0)
@@ -15819,6 +16429,10 @@ async function acquireBridge(locationDirectory, options, dependencies = {}) {
15819
16429
  await current.pool.shutdown();
15820
16430
  }));
15821
16431
  });
16432
+ const onVersionMismatch = options.poolOptions.onVersionMismatch;
16433
+ if (onVersionMismatch)
16434
+ owner.versionMismatchHandlers?.set(lease, onVersionMismatch);
16435
+ return lease;
15822
16436
  });
15823
16437
  });
15824
16438
  }
@@ -15839,6 +16453,14 @@ var STANDALONE_DETACH_KEYWORDS_PATTERN = new RegExp(STANDALONE_DETACH_KEYWORD_SO
15839
16453
  function containsStandaloneDetachKeyword(messageText) {
15840
16454
  return STANDALONE_DETACH_KEYWORD_PATTERN.test(messageText);
15841
16455
  }
16456
+ function standaloneDetachKeywordRanges(messageText) {
16457
+ const ranges = [];
16458
+ for (const match of messageText.matchAll(STANDALONE_DETACH_KEYWORDS_PATTERN)) {
16459
+ const start = (match.index ?? 0) + (match[1]?.length ?? 0);
16460
+ ranges.push([start, start + BASH_WAIT_DETACH_MAGIC_KEYWORD.length]);
16461
+ }
16462
+ return ranges;
16463
+ }
15842
16464
  function stripStandaloneDetachKeywords(messageText) {
15843
16465
  return messageText.replace(STANDALONE_DETACH_KEYWORDS_PATTERN, "$1");
15844
16466
  }
@@ -15846,14 +16468,14 @@ function shouldInterruptWaitsForMessage(detachOnUserMessage, operatorText) {
15846
16468
  return detachOnUserMessage || containsStandaloneDetachKeyword(operatorText);
15847
16469
  }
15848
16470
  // ../aft-bridge/dist/migration.js
15849
- 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";
16471
+ 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";
15850
16472
  import { homedir as homedir8, tmpdir as tmpdir2 } from "node:os";
15851
- import { basename, dirname as dirname4, join as join10, resolve as resolve6 } from "node:path";
16473
+ import { basename as basename2, dirname as dirname5, join as join10, resolve as resolve6 } from "node:path";
15852
16474
 
15853
16475
  // ../aft-bridge/dist/paths.js
15854
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync4, renameSync as renameSync3, writeFileSync as writeFileSync2 } from "node:fs";
16476
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync2 } from "node:fs";
15855
16477
  import { homedir as homedir6 } from "node:os";
15856
- import { dirname as dirname2, isAbsolute as isAbsolute3, join as join8, resolve as resolve5 } from "node:path";
16478
+ import { dirname as dirname3, isAbsolute as isAbsolute3, join as join8, resolve as resolve5 } from "node:path";
15857
16479
  function homeDir3() {
15858
16480
  if (process.platform === "win32")
15859
16481
  return process.env.USERPROFILE || process.env.HOME || homedir6();
@@ -15913,7 +16535,7 @@ function repairRootScopedStorageFile(storageRoot, harness, fileName) {
15913
16535
  if (existsSync6(harnessPath) || !existsSync6(rootPath))
15914
16536
  return harnessPath;
15915
16537
  try {
15916
- mkdirSync2(dirname2(harnessPath), { recursive: true });
16538
+ mkdirSync2(dirname3(harnessPath), { recursive: true });
15917
16539
  renameSync3(rootPath, harnessPath);
15918
16540
  } catch {}
15919
16541
  return harnessPath;
@@ -15925,7 +16547,7 @@ function shouldShowAnnouncement(storageRoot, harness, currentVersion) {
15925
16547
  let lastVersion = "";
15926
16548
  try {
15927
16549
  if (existsSync6(versionFile)) {
15928
- lastVersion = readFileSync4(versionFile, "utf-8").trim();
16550
+ lastVersion = readFileSync5(versionFile, "utf-8").trim();
15929
16551
  }
15930
16552
  } catch {
15931
16553
  return false;
@@ -15934,7 +16556,7 @@ function shouldShowAnnouncement(storageRoot, harness, currentVersion) {
15934
16556
  return false;
15935
16557
  if (!lastVersion) {
15936
16558
  try {
15937
- mkdirSync2(dirname2(versionFile), { recursive: true });
16559
+ mkdirSync2(dirname3(versionFile), { recursive: true });
15938
16560
  writeFileSync2(versionFile, currentVersion);
15939
16561
  } catch {}
15940
16562
  return false;
@@ -15946,7 +16568,7 @@ function markAnnouncementSeen(storageRoot, harness, currentVersion) {
15946
16568
  return;
15947
16569
  const versionFile = repairRootScopedStorageFile(storageRoot, harness, "last_announced_version");
15948
16570
  try {
15949
- mkdirSync2(dirname2(versionFile), { recursive: true });
16571
+ mkdirSync2(dirname3(versionFile), { recursive: true });
15950
16572
  writeFileSync2(versionFile, currentVersion);
15951
16573
  } catch {}
15952
16574
  }
@@ -15995,10 +16617,10 @@ function decodeFileUrl(target) {
15995
16617
  }
15996
16618
 
15997
16619
  // ../aft-bridge/dist/resolver.js
15998
- import { closeSync as closeSync2, existsSync as existsSync7, promises as fsPromises, openSync as openSync2, readFileSync as readFileSync5, readSync } from "node:fs";
16620
+ import { closeSync as closeSync2, existsSync as existsSync7, promises as fsPromises, openSync as openSync2, readFileSync as readFileSync6, readSync } from "node:fs";
15999
16621
  import { createRequire } from "node:module";
16000
16622
  import { homedir as homedir7 } from "node:os";
16001
- import { delimiter, dirname as dirname3, isAbsolute as isAbsolute4, join as join9 } from "node:path";
16623
+ import { delimiter, dirname as dirname4, isAbsolute as isAbsolute4, join as join9 } from "node:path";
16002
16624
  var ensureBinaryForResolver = ensureBinary;
16003
16625
  var npmPlatformPackageReader = null;
16004
16626
  var cacheCopiesInFlight = new Map;
@@ -16151,8 +16773,8 @@ function npmPlatformPackage(ext) {
16151
16773
  try {
16152
16774
  const req = createRequire(import.meta.url);
16153
16775
  const manifestPath = req.resolve(`@cortexkit/aft-${platformKey()}/package.json`);
16154
- const manifest = JSON.parse(readFileSync5(manifestPath, "utf8"));
16155
- const binaryPath = join9(dirname3(manifestPath), "bin", `aft${ext}`);
16776
+ const manifest = JSON.parse(readFileSync6(manifestPath, "utf8"));
16777
+ const binaryPath = join9(dirname4(manifestPath), "bin", `aft${ext}`);
16156
16778
  if (typeof manifest.version !== "string" || !existsSync7(binaryPath))
16157
16779
  return null;
16158
16780
  return { binaryPath, version: normalizeBareVersion(manifest.version) };
@@ -16406,7 +17028,7 @@ function acquireConfigMigrationLock(lockDir) {
16406
17028
  if (code !== "EEXIST")
16407
17029
  throw err;
16408
17030
  try {
16409
- const ageMs = Date.now() - statSync5(lockDir).mtimeMs;
17031
+ const ageMs = Date.now() - statSync6(lockDir).mtimeMs;
16410
17032
  if (ageMs > 60000) {
16411
17033
  rmSync2(lockDir, { recursive: true, force: true });
16412
17034
  continue;
@@ -16420,12 +17042,12 @@ function acquireConfigMigrationLock(lockDir) {
16420
17042
  }
16421
17043
  }
16422
17044
  function atomicCopyConfigFile(sourcePath, targetPath) {
16423
- mkdirSync3(dirname4(targetPath), { recursive: true });
16424
- const tmpPath = join10(dirname4(targetPath), `.${basename(targetPath)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
17045
+ mkdirSync3(dirname5(targetPath), { recursive: true });
17046
+ const tmpPath = join10(dirname5(targetPath), `.${basename2(targetPath)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
16425
17047
  let fd = null;
16426
17048
  try {
16427
17049
  fd = openSync3(tmpPath, "wx", 384);
16428
- writeFileSync3(fd, readFileSync6(sourcePath));
17050
+ writeFileSync3(fd, readFileSync7(sourcePath));
16429
17051
  closeSync3(fd);
16430
17052
  fd = null;
16431
17053
  renameSync4(tmpPath, targetPath);
@@ -16442,8 +17064,8 @@ function atomicCopyConfigFile(sourcePath, targetPath) {
16442
17064
  }
16443
17065
  }
16444
17066
  function atomicWriteConfigFile(targetPath, content) {
16445
- mkdirSync3(dirname4(targetPath), { recursive: true });
16446
- const tmpPath = join10(dirname4(targetPath), `.${basename(targetPath)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
17067
+ mkdirSync3(dirname5(targetPath), { recursive: true });
17068
+ const tmpPath = join10(dirname5(targetPath), `.${basename2(targetPath)}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
16447
17069
  let fd = null;
16448
17070
  try {
16449
17071
  fd = openSync3(tmpPath, "wx", 384);
@@ -16504,11 +17126,11 @@ function markLegacySourcesMovedAside(sources, targetPath, logger) {
16504
17126
  const desiredMarkerPath = `${source.path}${MOVED_MARKER_SUFFIX}`;
16505
17127
  const markerPath = nonClobberingSidecarPath(desiredMarkerPath);
16506
17128
  try {
16507
- const original = readFileSync6(source.path, "utf-8");
17129
+ const original = readFileSync7(source.path, "utf-8");
16508
17130
  if (markerPath !== desiredMarkerPath) {
16509
17131
  logger?.warn?.(`Preserving existing legacy AFT marker ${desiredMarkerPath}; writing new marker to ${markerPath}`);
16510
17132
  }
16511
- atomicWriteConfigFile(markerPath, movedMarkerContent(targetPath, basename(source.path), original));
17133
+ atomicWriteConfigFile(markerPath, movedMarkerContent(targetPath, basename2(source.path), original));
16512
17134
  unlinkSync3(source.path);
16513
17135
  info?.(`Moved legacy AFT config ${source.path} aside to ${markerPath}; AFT now reads ${targetPath}`);
16514
17136
  } catch (err) {
@@ -16568,15 +17190,15 @@ function migrateAftConfigFile(opts) {
16568
17190
  if (existingSources.length === 0) {
16569
17191
  return { migrated: false, conflict: false, targetPath: opts.targetPath, warnings };
16570
17192
  }
16571
- mkdirSync3(dirname4(opts.targetPath), { recursive: true });
17193
+ mkdirSync3(dirname5(opts.targetPath), { recursive: true });
16572
17194
  const release = acquireConfigMigrationLock(`${opts.targetPath}.lock`);
16573
17195
  try {
16574
17196
  const sources = existingSources.map((source) => ({
16575
17197
  ...source,
16576
- content: readFileSync6(source.path, "utf-8")
17198
+ content: readFileSync7(source.path, "utf-8")
16577
17199
  }));
16578
17200
  if (existsSync8(opts.targetPath)) {
16579
- const targetContent = readFileSync6(opts.targetPath, "utf-8");
17201
+ const targetContent = readFileSync7(opts.targetPath, "utf-8");
16580
17202
  for (const source of sources) {
16581
17203
  if (fileSemanticsMatch(source.content, targetContent))
16582
17204
  continue;
@@ -16627,11 +17249,11 @@ function spawnErrorLabel(error) {
16627
17249
  function migrationLogPath(newRoot, harness, logger) {
16628
17250
  const desired = join10(newRoot, "logs", "migration", `${harness}-${Date.now()}.jsonl`);
16629
17251
  try {
16630
- mkdirSync3(dirname4(desired), { recursive: true });
17252
+ mkdirSync3(dirname5(desired), { recursive: true });
16631
17253
  return desired;
16632
17254
  } catch (err) {
16633
17255
  const fallback = join10(tmpdir2(), `aft-migration-${harness}-${Date.now()}.jsonl`);
16634
- logger?.warn?.(`Failed to create AFT migration log directory ${dirname4(desired)}: ${err instanceof Error ? err.message : String(err)}. ` + `Using fallback log path ${fallback}.`);
17256
+ logger?.warn?.(`Failed to create AFT migration log directory ${dirname5(desired)}: ${err instanceof Error ? err.message : String(err)}. ` + `Using fallback log path ${fallback}.`);
16635
17257
  return fallback;
16636
17258
  }
16637
17259
  }
@@ -16692,9 +17314,9 @@ async function ensureStorageMigrated(opts) {
16692
17314
  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}` : ""));
16693
17315
  }
16694
17316
  // ../aft-bridge/dist/migration-notices.js
16695
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, renameSync as renameSync5, rmdirSync, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
17317
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync8, renameSync as renameSync5, rmdirSync, statSync as statSync7, writeFileSync as writeFileSync4 } from "node:fs";
16696
17318
  import { homedir as homedir9 } from "node:os";
16697
- import { dirname as dirname5, join as join11, resolve as resolve7 } from "node:path";
17319
+ import { dirname as dirname6, join as join11, resolve as resolve7 } from "node:path";
16698
17320
  function migrationNoticeStorePath(env = process.env) {
16699
17321
  const stateHome = env.XDG_STATE_HOME && env.XDG_STATE_HOME.length > 0 ? env.XDG_STATE_HOME : join11(homedir9(), ".local", "state");
16700
17322
  return join11(stateHome, "cortexkit", "aft", "migration-notices.json");
@@ -16704,7 +17326,7 @@ function noticeKey(configPath, digest) {
16704
17326
  }
16705
17327
  function readRecords(storePath) {
16706
17328
  try {
16707
- const parsed = JSON.parse(readFileSync7(storePath, "utf8"));
17329
+ const parsed = JSON.parse(readFileSync8(storePath, "utf8"));
16708
17330
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
16709
17331
  } catch {
16710
17332
  return {};
@@ -16713,7 +17335,7 @@ function readRecords(storePath) {
16713
17335
  var LOCK_STALE_MS = 1e4;
16714
17336
  function withStoreLock(storePath, action) {
16715
17337
  const lockPath = `${storePath}.lock`;
16716
- mkdirSync4(dirname5(storePath), { recursive: true });
17338
+ mkdirSync4(dirname6(storePath), { recursive: true });
16717
17339
  const deadline = Date.now() + 2000;
16718
17340
  for (;; ) {
16719
17341
  try {
@@ -16723,7 +17345,7 @@ function withStoreLock(storePath, action) {
16723
17345
  if (error.code !== "EEXIST")
16724
17346
  throw error;
16725
17347
  try {
16726
- if (Date.now() - statSync6(lockPath).mtimeMs > LOCK_STALE_MS)
17348
+ if (Date.now() - statSync7(lockPath).mtimeMs > LOCK_STALE_MS)
16727
17349
  rmdirSync(lockPath);
16728
17350
  } catch {}
16729
17351
  if (Date.now() > deadline)
@@ -16764,9 +17386,9 @@ function deliverMigrationNoticeOnce(options) {
16764
17386
  }
16765
17387
  }
16766
17388
  // ../aft-bridge/dist/npm-resolver.js
16767
- import { readdirSync as readdirSync2, statSync as statSync7 } from "node:fs";
17389
+ import { readdirSync as readdirSync2, statSync as statSync8 } from "node:fs";
16768
17390
  import { homedir as homedir10 } from "node:os";
16769
- import { dirname as dirname6, isAbsolute as isAbsolute5, join as join12 } from "node:path";
17391
+ import { dirname as dirname7, isAbsolute as isAbsolute5, join as join12 } from "node:path";
16770
17392
  function defaultDeps() {
16771
17393
  return {
16772
17394
  platform: process.platform,
@@ -16780,7 +17402,7 @@ function npmBinaryName(platform) {
16780
17402
  }
16781
17403
  function isFile(p) {
16782
17404
  try {
16783
- return statSync7(p).isFile();
17405
+ return statSync8(p).isFile();
16784
17406
  } catch {
16785
17407
  return false;
16786
17408
  }
@@ -16801,7 +17423,7 @@ function npmFromPath(deps) {
16801
17423
  return null;
16802
17424
  }
16803
17425
  function npmAdjacentToNode(deps) {
16804
- const dir = dirname6(deps.execPath);
17426
+ const dir = dirname7(deps.execPath);
16805
17427
  return isFile(join12(dir, npmBinaryName(deps.platform))) ? dir : null;
16806
17428
  }
16807
17429
  function highestVersionedNodeBin(installsDir, name) {
@@ -17189,8 +17811,8 @@ function probeOnnxRuntimeLoadable(libPath, arch = process.arch) {
17189
17811
  }
17190
17812
  // ../aft-bridge/dist/onnx-runtime.js
17191
17813
  import { createHash as createHash5, randomBytes as randomBytes2 } from "node:crypto";
17192
- 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";
17193
- import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute6, join as join14, relative as relative2, resolve as resolve8, win32 } from "node:path";
17814
+ 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";
17815
+ import { basename as basename3, dirname as dirname8, isAbsolute as isAbsolute6, join as join14, relative as relative2, resolve as resolve8, win32 } from "node:path";
17194
17816
  import { Readable as Readable2 } from "node:stream";
17195
17817
  import { pipeline as pipeline2 } from "node:stream/promises";
17196
17818
 
@@ -17230,31 +17852,36 @@ var ORT_PLATFORM_MAP = {
17230
17852
  arm64: {
17231
17853
  assetName: `onnxruntime-osx-arm64-${ORT_VERSION}`,
17232
17854
  libName: "libonnxruntime.dylib",
17233
- archiveType: "tgz"
17855
+ archiveType: "tgz",
17856
+ librarySha256: "872533f130f1839a5bc01788ddb4f75c83a189763441ba1178788ed965449289"
17234
17857
  }
17235
17858
  },
17236
17859
  linux: {
17237
17860
  x64: {
17238
17861
  assetName: `onnxruntime-linux-x64-${ORT_VERSION}`,
17239
17862
  libName: "libonnxruntime.so",
17240
- archiveType: "tgz"
17863
+ archiveType: "tgz",
17864
+ librarySha256: "d132535d051344ff5c64c9c200004150559049a81ed330eb4422c1962fb6b7e4"
17241
17865
  },
17242
17866
  arm64: {
17243
17867
  assetName: `onnxruntime-linux-aarch64-${ORT_VERSION}`,
17244
17868
  libName: "libonnxruntime.so",
17245
- archiveType: "tgz"
17869
+ archiveType: "tgz",
17870
+ librarySha256: "52b2a0e75e79468404284fec38ad5ee1a7a996232274f5e1b84f3e793fb07554"
17246
17871
  }
17247
17872
  },
17248
17873
  win32: {
17249
17874
  x64: {
17250
17875
  assetName: `onnxruntime-win-x64-${ORT_VERSION}`,
17251
17876
  libName: "onnxruntime.dll",
17252
- archiveType: "zip"
17877
+ archiveType: "zip",
17878
+ librarySha256: "b95efb2113b603bbbf3f191061c5516a871ed546893c820e4f3b7b6c358dbf2a"
17253
17879
  },
17254
17880
  arm64: {
17255
17881
  assetName: `onnxruntime-win-arm64-${ORT_VERSION}`,
17256
17882
  libName: "onnxruntime.dll",
17257
- archiveType: "zip"
17883
+ archiveType: "zip",
17884
+ librarySha256: "724d81ac50b11bfaa01ab3ce01b99fb6734a4b762259a8be4395ca662ce99fe6"
17258
17885
  }
17259
17886
  }
17260
17887
  };
@@ -17397,7 +18024,7 @@ function cleanupAbandonedStagingDirs(onnxBaseDir) {
17397
18024
  abandoned = true;
17398
18025
  } else {
17399
18026
  try {
17400
- const ageMs = Date.now() - statSync8(stagingDir).mtimeMs;
18027
+ const ageMs = Date.now() - statSync9(stagingDir).mtimeMs;
17401
18028
  abandoned = ageMs > STALE_LOCK_MS;
17402
18029
  } catch {
17403
18030
  abandoned = true;
@@ -17434,7 +18061,7 @@ var REQUIRED_ORT_MAJOR = 1;
17434
18061
  var REQUIRED_ORT_MIN_MINOR = 20;
17435
18062
  var INVALID_ORT_VERSION = "<invalid>";
17436
18063
  function parseOnnxVersionFromPath(value) {
17437
- const name = basename2(value);
18064
+ const name = basename3(value);
17438
18065
  const semverish = name.match(/(?:^|[._-])(\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?)(?:\.(?:dylib|dll))?$/);
17439
18066
  if (semverish)
17440
18067
  return semverish[1].split(/[-+]/, 1)[0];
@@ -17639,7 +18266,7 @@ async function downloadFileWithCap(url, destPath) {
17639
18266
  if (Number.isFinite(advertised) && advertised > MAX_DOWNLOAD_BYTES2) {
17640
18267
  throw new Error(`Content-Length ${advertised} exceeds max ${MAX_DOWNLOAD_BYTES2}`);
17641
18268
  }
17642
- mkdirSync5(dirname7(destPath), { recursive: true });
18269
+ mkdirSync5(dirname8(destPath), { recursive: true });
17643
18270
  let bytesWritten = 0;
17644
18271
  const guard = new TransformStream({
17645
18272
  transform(chunk, transformController) {
@@ -17673,7 +18300,7 @@ function validateExtractedTree(stagingRoot) {
17673
18300
  const lst = lstatSync(fullPath);
17674
18301
  if (lst.isSymbolicLink()) {
17675
18302
  const linkTarget = readlinkSync(fullPath);
17676
- const resolvedTarget = resolve8(dirname7(fullPath), linkTarget);
18303
+ const resolvedTarget = resolve8(dirname8(fullPath), linkTarget);
17677
18304
  const rel = relative2(realRoot, resolvedTarget);
17678
18305
  if (rel.startsWith("..") || isAbsolute6(rel) || win32.isAbsolute(rel)) {
17679
18306
  throw new Error(`extracted symlink ${fullPath} points outside staging root: ${linkTarget}`);
@@ -17744,6 +18371,9 @@ async function downloadOnnxRuntime(info, targetDir) {
17744
18371
  copyOnnxLibraries(info, extractedDir, stagedInstallDir, realFiles, symlinks);
17745
18372
  const libPath = join14(stagedInstallDir, info.libName);
17746
18373
  const libHash = sha256File2(libPath);
18374
+ if (libHash !== info.librarySha256) {
18375
+ throw new Error(`onnx runtime library hash mismatch at ${libPath}: sha256 ${libHash}, ` + `expected the pinned ${info.librarySha256} for ${info.assetName}`);
18376
+ }
17747
18377
  writeOnnxInstalledMeta(stagedInstallDir, ORT_VERSION, libHash, archiveSha256);
17748
18378
  publishOnnxRuntime(stagedInstallDir, targetDir);
17749
18379
  rmSync3(tmpDir, { recursive: true, force: true });
@@ -17809,7 +18439,7 @@ function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, c
17809
18439
  unlinkSync4(dst);
17810
18440
  } catch {}
17811
18441
  const dstForContainment = join14(targetRoot, link.name);
17812
- const resolvedTarget = resolve8(dirname7(dstForContainment), link.target);
18442
+ const resolvedTarget = resolve8(dirname8(dstForContainment), link.target);
17813
18443
  if (!isPathInsideRoot(targetRoot, resolvedTarget)) {
17814
18444
  const message = `ONNX Runtime symlink ${link.name} points outside install dir: ${link.target}`;
17815
18445
  if (requiredLibs.has(link.name)) {
@@ -17861,9 +18491,9 @@ function writeOnnxInstalledMeta(installDir, version, sha256, archiveSha256) {
17861
18491
  function readOnnxInstalledMeta(installDir) {
17862
18492
  const path = join14(installDir, ONNX_INSTALLED_META_FILE);
17863
18493
  try {
17864
- if (!statSync8(path).isFile())
18494
+ if (!statSync9(path).isFile())
17865
18495
  return null;
17866
- const raw = readFileSync8(path, "utf8");
18496
+ const raw = readFileSync9(path, "utf8");
17867
18497
  const parsed = JSON.parse(raw);
17868
18498
  if (typeof parsed.version !== "string" || parsed.version.length === 0)
17869
18499
  return null;
@@ -17879,7 +18509,7 @@ function readOnnxInstalledMeta(installDir) {
17879
18509
  }
17880
18510
  function sha256File2(path) {
17881
18511
  const hash = createHash5("sha256");
17882
- hash.update(readFileSync8(path));
18512
+ hash.update(readFileSync9(path));
17883
18513
  return hash.digest("hex");
17884
18514
  }
17885
18515
  function acquireLock(lockPath) {
@@ -17907,12 +18537,12 @@ ${new Date().toISOString()}
17907
18537
  let owningPid = null;
17908
18538
  let lockMtimeMs = 0;
17909
18539
  try {
17910
- const raw = readFileSync8(lockPath, "utf8");
18540
+ const raw = readFileSync9(lockPath, "utf8");
17911
18541
  const firstLine = raw.split(/\r?\n/, 1)[0]?.trim() ?? "";
17912
18542
  const parsed = Number.parseInt(firstLine, 10);
17913
18543
  if (Number.isFinite(parsed) && parsed > 0)
17914
18544
  owningPid = parsed;
17915
- lockMtimeMs = statSync8(lockPath).mtimeMs;
18545
+ lockMtimeMs = statSync9(lockPath).mtimeMs;
17916
18546
  } catch {
17917
18547
  return tryClaim();
17918
18548
  }
@@ -17932,7 +18562,7 @@ function releaseLock(lockPath) {
17932
18562
  try {
17933
18563
  let owningPid = null;
17934
18564
  try {
17935
- const raw = readFileSync8(lockPath, "utf8");
18565
+ const raw = readFileSync9(lockPath, "utf8");
17936
18566
  const firstLine = raw.split(/\r?\n/, 1)[0]?.trim() ?? "";
17937
18567
  const parsed = Number.parseInt(firstLine, 10);
17938
18568
  if (Number.isFinite(parsed) && parsed > 0)
@@ -18086,12 +18716,13 @@ function normalizeZoomTargets(record) {
18086
18716
  throw new InvalidRequestError(`'targets[${index}].path' must be a non-empty string`);
18087
18717
  }
18088
18718
  const source = target;
18089
- const emptyTarget = source.symbol === "" && (hasOwn(source, "path") && source.path === "" || hasOwn(source, "filePath") && source.filePath === "");
18719
+ const blank = (value) => value === undefined || value === null || typeof value === "string" && !value.trim();
18720
+ const emptyTarget = blank(source.symbol) && blank(source.path) && blank(source.filePath);
18090
18721
  if (emptyTarget)
18091
18722
  return { ...source };
18092
18723
  const normalized = { ...source };
18093
18724
  try {
18094
- normalizeAliasPair(normalized, "path", "filePath", true);
18725
+ normalizeAliasPair(normalized, "path", "filePath", false);
18095
18726
  } catch (error) {
18096
18727
  if (error instanceof InvalidRequestError) {
18097
18728
  throw new InvalidRequestError(error.message.replace("'filePath'", `'targets[${index}].filePath'`).replace("'path'", `'targets[${index}].path'`));
@@ -18623,7 +19254,7 @@ function formatSemanticIndexStatus(status, stage, error, backend) {
18623
19254
  return status;
18624
19255
  }
18625
19256
  // src/config.ts
18626
- import { existsSync as existsSync12, readFileSync as readFileSync9, renameSync as renameSync7, unlinkSync as unlinkSync5, writeFileSync as writeFileSync6 } from "node:fs";
19257
+ import { existsSync as existsSync12, readFileSync as readFileSync10, renameSync as renameSync7, unlinkSync as unlinkSync5, writeFileSync as writeFileSync6 } from "node:fs";
18627
19258
  import { homedir as homedir11 } from "node:os";
18628
19259
  import { isAbsolute as isAbsolute7, parse as parsePath, resolve as resolvePath } from "node:path";
18629
19260
  var import_comment_json = __toESM(require_src2(), 1);
@@ -23466,6 +24097,17 @@ var SemanticConfigSchema = object({
23466
24097
  max_input_tokens: number2().int().positive().optional(),
23467
24098
  max_files: number2().int().positive().optional()
23468
24099
  });
24100
+ var RerankConfigSchema = object({
24101
+ backend: _enum(["off", "onnx", "remote", "synapse"]).optional(),
24102
+ model: string2().trim().min(1).optional(),
24103
+ endpoint: string2().trim().min(1).optional(),
24104
+ api_key_env: string2().trim().min(1).optional(),
24105
+ top_n: number2().int().positive().optional(),
24106
+ timeout_ms: number2().int().positive().optional()
24107
+ });
24108
+ var SearchConfigSchema = object({
24109
+ rerank: RerankConfigSchema.optional()
24110
+ });
23469
24111
  var LspExtensionSchema = string2().trim().min(1).refine((value) => value.replace(/^\.+/, "").length > 0, {
23470
24112
  message: "Extension must include characters other than leading dots"
23471
24113
  });
@@ -23526,6 +24168,10 @@ var SubcConfigSchema = object({
23526
24168
  connection_file: string2().optional(),
23527
24169
  client_reaper: boolean2().optional()
23528
24170
  });
24171
+ var OpenCodeHostConfigSchema = object({
24172
+ server_url: string2().optional(),
24173
+ server_password_env: string2().optional()
24174
+ });
23529
24175
  var GhShimConfigSchema = object({
23530
24176
  binary_path: string2().trim().refine(isAbsolute7, "gh_shim.binary_path must be absolute").optional()
23531
24177
  });
@@ -23611,9 +24257,11 @@ var AftConfigFieldsSchema = object({
23611
24257
  lsp: LspConfigSchema.optional(),
23612
24258
  url_fetch_allow_private: boolean2().optional(),
23613
24259
  semantic: SemanticConfigSchema.optional(),
24260
+ search: SearchConfigSchema.optional(),
23614
24261
  auto_update: boolean2().optional(),
23615
24262
  bridge: BridgeConfigSchema.optional(),
23616
24263
  subc: SubcConfigSchema.optional(),
24264
+ opencode: OpenCodeHostConfigSchema.optional(),
23617
24265
  github: GithubConfigSchema.optional(),
23618
24266
  gh_shim: GhShimConfigSchema.optional(),
23619
24267
  git: GitConfigSchema.optional(),
@@ -23855,7 +24503,7 @@ function migrateAftConfigFile2(configPath, logger = { log: log2, warn: warn2 })
23855
24503
  let tmpPath = null;
23856
24504
  let oldKeys = [];
23857
24505
  try {
23858
- const content = readFileSync9(configPath, "utf-8");
24506
+ const content = readFileSync10(configPath, "utf-8");
23859
24507
  const rawConfig = import_comment_json.parse(content);
23860
24508
  if (!rawConfig || typeof rawConfig !== "object" || Array.isArray(rawConfig)) {
23861
24509
  return { migrated: false, oldKeys: [] };
@@ -23922,6 +24570,18 @@ function parseConfigPartially(rawConfig) {
23922
24570
  return partialConfig;
23923
24571
  }
23924
24572
  var configLoadErrors = [];
24573
+ var configValidationErrors = [];
24574
+ var configLoadSources = [];
24575
+ var configLoadTexts = new Map;
24576
+ function getConfigLoadSources() {
24577
+ return configLoadSources;
24578
+ }
24579
+ function getConfigLoadTexts() {
24580
+ return configLoadTexts;
24581
+ }
24582
+ function getConfigValidationErrors() {
24583
+ return configValidationErrors;
24584
+ }
23925
24585
  function getConfigLoadErrors() {
23926
24586
  return configLoadErrors;
23927
24587
  }
@@ -23954,7 +24614,9 @@ function loadConfigFromPath(configPath, tier) {
23954
24614
  if (!existsSync12(configPath)) {
23955
24615
  return null;
23956
24616
  }
23957
- const content = readFileSync9(configPath, "utf-8");
24617
+ const content = readFileSync10(configPath, "utf-8");
24618
+ configLoadSources.push(configPath);
24619
+ configLoadTexts.set(configPath, content);
23958
24620
  const rawConfig = import_comment_json.parse(content);
23959
24621
  migrateRawConfig(rawConfig, configPath, { log: log2, warn: warn2 });
23960
24622
  cleanConfig = stripJsoncSymbols(rawConfig);
@@ -23999,6 +24661,7 @@ function loadConfigFromPath(configPath, tier) {
23999
24661
  } else {
24000
24662
  const errorMsg = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
24001
24663
  warn2(`Config validation error in ${configPath}: ${errorMsg}`);
24664
+ configValidationErrors.push({ path: configPath, message: errorMsg });
24002
24665
  parsed = parseConfigPartially(cleanConfig);
24003
24666
  }
24004
24667
  if (!parsed)
@@ -24188,6 +24851,8 @@ function getStrippedTopLevelKeys(override) {
24188
24851
  stripped.push("sandbox.write_allow");
24189
24852
  if (override.subc !== undefined)
24190
24853
  stripped.push("subc");
24854
+ if (override.opencode !== undefined)
24855
+ stripped.push("opencode");
24191
24856
  if (override.github !== undefined)
24192
24857
  stripped.push("github");
24193
24858
  if (override.gh_shim !== undefined)
@@ -24195,14 +24860,33 @@ function getStrippedTopLevelKeys(override) {
24195
24860
  for (const tool of partitionProjectDisables(override.disabled_tools).ignored) {
24196
24861
  stripped.push(`disabled_tools.${tool}`);
24197
24862
  }
24863
+ stripped.push(...projectRerankStrippedKeys(override.search?.rerank));
24864
+ return stripped;
24865
+ }
24866
+ function projectRerankStrippedKeys(rerank) {
24867
+ if (rerank === undefined)
24868
+ return [];
24869
+ const stripped = [];
24870
+ if (rerank.backend !== undefined && rerank.backend !== "off")
24871
+ stripped.push("search.rerank.backend");
24872
+ for (const key of ["model", "endpoint", "api_key_env", "top_n", "timeout_ms"]) {
24873
+ if (rerank[key] !== undefined)
24874
+ stripped.push(`search.rerank.${key}`);
24875
+ }
24198
24876
  return stripped;
24199
24877
  }
24878
+ function mergeProjectSearchConfig(base, project) {
24879
+ if (project?.rerank?.backend !== "off")
24880
+ return base;
24881
+ return { ...base, rerank: { ...base?.rerank, backend: "off" } };
24882
+ }
24200
24883
  function mergeConfigs(base, override) {
24201
24884
  const disabledTools = unionDisabledTools(base.disabled_tools, partitionProjectDisables(override.disabled_tools).accepted);
24202
24885
  const indexes = mergeIndexes(base.indexes, override.indexes, true);
24203
24886
  const formatter = { ...base.formatter, ...override.formatter };
24204
24887
  const checker = { ...base.checker, ...override.checker };
24205
24888
  const semantic = mergeSemanticConfig(base.semantic, override.semantic);
24889
+ const search = mergeProjectSearchConfig(base.search, override.search);
24206
24890
  const lsp = mergeLspConfig(base.lsp, override.lsp);
24207
24891
  const experimental = mergeExperimentalConfig(base.experimental, override.experimental);
24208
24892
  const bash = mergeBashConfig(base.bash, override.bash);
@@ -24232,6 +24916,7 @@ function mergeConfigs(base, override) {
24232
24916
  ...pi !== undefined ? { pi } : {},
24233
24917
  experimental,
24234
24918
  semantic,
24919
+ search,
24235
24920
  ...bridge !== undefined ? { bridge } : {},
24236
24921
  ...indexes !== undefined ? { indexes } : {},
24237
24922
  ...disabledTools !== undefined ? { disabled_tools: disabledTools } : {}
@@ -24296,6 +24981,9 @@ function buildConfigTierConfigureParams(projectDirectory, processState = {}) {
24296
24981
  }
24297
24982
  function loadAftConfig(projectDirectory) {
24298
24983
  configLoadErrors = [];
24984
+ configValidationErrors = [];
24985
+ configLoadSources = [];
24986
+ configLoadTexts = new Map;
24299
24987
  configLoadNotices = [];
24300
24988
  semanticInputSupplied = false;
24301
24989
  const { userConfigPath, projectConfigPath } = resolveAftConfigPaths(projectDirectory);
@@ -38024,6 +38712,14 @@ function formatDuration(completion) {
38024
38712
  }
38025
38713
 
38026
38714
  // src/shared/session-directory.ts
38715
+ import { Effect } from "effect";
38716
+ function isV2PluginContext(client) {
38717
+ if (!client || typeof client !== "object")
38718
+ return false;
38719
+ const candidate = client;
38720
+ return typeof candidate.location?.directory === "string" && typeof candidate.session?.get === "function";
38721
+ }
38722
+ var V2_SESSION_LOOKUP_TIMEOUT_MS = 3000;
38027
38723
  var CACHE_MAX_ENTRIES = 200;
38028
38724
  var cache = new Map;
38029
38725
  async function getSessionDirectory(client, sessionId, fallbackDirectory) {
@@ -38035,6 +38731,8 @@ async function getSessionDirectory(client, sessionId, fallbackDirectory) {
38035
38731
  cache.set(sessionId, cached);
38036
38732
  return cached.directory;
38037
38733
  }
38734
+ if (isV2PluginContext(client))
38735
+ return lookupV2SessionDirectory(client, sessionId);
38038
38736
  const c = client;
38039
38737
  const sessionApi = c?.session;
38040
38738
  if (!sessionApi || typeof sessionApi.get !== "function") {
@@ -38055,6 +38753,25 @@ async function getSessionDirectory(client, sessionId, fallbackDirectory) {
38055
38753
  setCache(sessionId, dir);
38056
38754
  return dir;
38057
38755
  }
38756
+ async function lookupV2SessionDirectory(context, sessionId) {
38757
+ let dir = null;
38758
+ let failure;
38759
+ try {
38760
+ const session = await Effect.runPromise(Effect.timeout(context.session.get({ sessionID: sessionId }), V2_SESSION_LOOKUP_TIMEOUT_MS));
38761
+ const directory = session?.location?.directory;
38762
+ if (typeof directory === "string" && directory.length > 0)
38763
+ dir = directory;
38764
+ else
38765
+ failure = "the session record has no location.directory";
38766
+ } catch (err) {
38767
+ failure = err instanceof Error ? err.message || err.name : String(err);
38768
+ }
38769
+ if (failure !== undefined) {
38770
+ sessionWarn(sessionId, `[aft-plugin] OpenCode 2 session lookup failed (${failure}); tool paths and bash use the Location directory ${context.location.directory} for this session`);
38771
+ }
38772
+ setCache(sessionId, dir);
38773
+ return dir;
38774
+ }
38058
38775
  function setCache(sessionId, directory) {
38059
38776
  if (cache.has(sessionId))
38060
38777
  cache.delete(sessionId);
@@ -38113,6 +38830,108 @@ async function verifySessionDirectory(client, sessionId) {
38113
38830
  return dir;
38114
38831
  }
38115
38832
 
38833
+ // src/shared/subagent-detect.ts
38834
+ import { Effect as Effect2 } from "effect";
38835
+ var CACHE_MAX_ENTRIES2 = 200;
38836
+ var LOOKUP_TIMEOUT_MS = 1000;
38837
+ var FAILED_LOOKUP_RETRY_MS = 30000;
38838
+ var cache2 = new Map;
38839
+ var inflight = new Map;
38840
+ var now = () => Date.now();
38841
+ async function resolveIsSubagent(client, sessionId, _fallbackDirectory) {
38842
+ if (!sessionId) {
38843
+ sessionLog(undefined, "[subagent-detect] no sessionId provided → primary");
38844
+ return false;
38845
+ }
38846
+ const cached = cache2.get(sessionId);
38847
+ if (cached && (cached.retryAfter === undefined || now() < cached.retryAfter)) {
38848
+ cache2.delete(sessionId);
38849
+ cache2.set(sessionId, cached);
38850
+ return cached.isSubagent;
38851
+ }
38852
+ const pending = inflight.get(sessionId);
38853
+ if (pending)
38854
+ return pending;
38855
+ const lookup = boundedLookup(client, sessionId).finally(() => {
38856
+ inflight.delete(sessionId);
38857
+ });
38858
+ inflight.set(sessionId, lookup);
38859
+ return lookup;
38860
+ }
38861
+ async function boundedLookup(client, sessionId) {
38862
+ let timer;
38863
+ const timedOut = new Promise((resolve) => {
38864
+ timer = setTimeout(() => resolve("timeout"), LOOKUP_TIMEOUT_MS);
38865
+ });
38866
+ let answer;
38867
+ try {
38868
+ answer = await Promise.race([lookupSession(client, sessionId), timedOut]);
38869
+ } finally {
38870
+ clearTimeout(timer);
38871
+ }
38872
+ if (typeof answer === "boolean") {
38873
+ setCache2(sessionId, answer);
38874
+ return answer;
38875
+ }
38876
+ if (answer === "timeout") {
38877
+ sessionWarn(sessionId, `[subagent-detect] session lookup did not answer within ${LOOKUP_TIMEOUT_MS}ms → primary for now`);
38878
+ }
38879
+ setCache2(sessionId, false, now() + FAILED_LOOKUP_RETRY_MS);
38880
+ return false;
38881
+ }
38882
+ async function lookupSession(client, sessionId) {
38883
+ if (isV2PluginContext2(client))
38884
+ return resolveV2(client, sessionId);
38885
+ const c = client;
38886
+ const sessionApi = c?.session;
38887
+ if (!sessionApi || typeof sessionApi.get !== "function") {
38888
+ sessionLog(sessionId, `[subagent-detect] client.session.get unavailable (client=${typeof client}, session=${typeof sessionApi}, get=${typeof sessionApi?.get}) → caching as primary`);
38889
+ return false;
38890
+ }
38891
+ sessionLog(sessionId, `[subagent-detect] cache miss, calling client.session.get(id=${sessionId})`);
38892
+ try {
38893
+ const result = await sessionApi.get({
38894
+ path: { id: sessionId }
38895
+ });
38896
+ const session = result?.data ?? result;
38897
+ const parentIdRaw = session?.parentID;
38898
+ const isSubagent = session !== undefined && typeof session.parentID === "string" && session.parentID.length > 0;
38899
+ sessionLog(sessionId, `[subagent-detect] SDK returned session=${session !== undefined ? "present" : "undefined"}, parentID=${JSON.stringify(parentIdRaw)} → isSubagent=${isSubagent}`);
38900
+ return isSubagent;
38901
+ } catch (err) {
38902
+ sessionWarn(sessionId, `[subagent-detect] SDK lookup failed: ${err instanceof Error ? err.message : String(err)}`);
38903
+ return "failed";
38904
+ }
38905
+ }
38906
+ function isV2PluginContext2(client) {
38907
+ if (!client || typeof client !== "object")
38908
+ return false;
38909
+ const candidate = client;
38910
+ return typeof candidate.location?.directory === "string" && typeof candidate.session?.get === "function";
38911
+ }
38912
+ async function resolveV2(context, sessionId) {
38913
+ let session;
38914
+ try {
38915
+ session = await Effect2.runPromise(context.session.get({ sessionID: sessionId }));
38916
+ } catch (err) {
38917
+ sessionWarn(sessionId, `[subagent-detect] OpenCode 2 session lookup failed: ${err instanceof Error ? err.message : String(err)}`);
38918
+ return "failed";
38919
+ }
38920
+ const isSubagent = typeof session?.parentID === "string" && session.parentID.length > 0;
38921
+ sessionLog(sessionId, `[subagent-detect] OpenCode 2 session parentID=${JSON.stringify(session?.parentID)} → isSubagent=${isSubagent}`);
38922
+ return isSubagent;
38923
+ }
38924
+ function setCache2(sessionId, isSubagent, retryAfter) {
38925
+ if (cache2.has(sessionId))
38926
+ cache2.delete(sessionId);
38927
+ cache2.set(sessionId, retryAfter === undefined ? { isSubagent } : { isSubagent, retryAfter });
38928
+ if (cache2.size > CACHE_MAX_ENTRIES2) {
38929
+ const oldest = cache2.keys().next().value;
38930
+ if (oldest !== undefined)
38931
+ cache2.delete(oldest);
38932
+ }
38933
+ }
38934
+
38116
38935
  // src/tool-perf.ts
38117
38936
  import { AsyncLocalStorage } from "node:async_hooks";
38118
38937
  var perfStore = new AsyncLocalStorage;
@@ -38128,16 +38947,29 @@ function markBridgeEnd() {
38128
38947
  slot.bridgeEnd = performance.now();
38129
38948
  }
38130
38949
  }
38950
+ async function measurePreStage(stage, operation) {
38951
+ const start = performance.now();
38952
+ try {
38953
+ return await operation();
38954
+ } finally {
38955
+ const slot = perfStore.getStore();
38956
+ if (slot) {
38957
+ slot.stages ??= {};
38958
+ slot.stages[stage] = (slot.stages[stage] ?? 0) + performance.now() - start;
38959
+ }
38960
+ }
38961
+ }
38131
38962
  function emit(slot) {
38963
+ const stages = slot.stages ? ` directory=${Math.round(slot.stages.directory ?? 0)}ms permission=${Math.round(slot.stages.permission ?? 0)}ms capability=${Math.round(slot.stages.capability ?? 0)}ms` : "";
38132
38964
  const t3 = performance.now();
38133
38965
  const total = Math.round(t3 - slot.t0);
38134
38966
  if (slot.bridgeStart !== undefined && slot.bridgeEnd !== undefined) {
38135
38967
  const pre = Math.round(slot.bridgeStart - slot.t0);
38136
38968
  const bridge = Math.round(slot.bridgeEnd - slot.bridgeStart);
38137
38969
  const post = Math.round(t3 - slot.bridgeEnd);
38138
- sessionLog(slot.sessionID, `perf tool=${slot.tool} total=${total}ms pre=${pre}ms bridge=${bridge}ms post=${post}ms`);
38970
+ sessionLog(slot.sessionID, `perf tool=${slot.tool} total=${total}ms pre=${pre}ms bridge=${bridge}ms post=${post}ms${stages}`);
38139
38971
  } else {
38140
- sessionLog(slot.sessionID, `perf tool=${slot.tool} total=${total}ms (no bridge call)`);
38972
+ sessionLog(slot.sessionID, `perf tool=${slot.tool} total=${total}ms (no bridge call)${stages}`);
38141
38973
  }
38142
38974
  }
38143
38975
  function instrumentToolMap(tools) {
@@ -38175,7 +39007,7 @@ function projectRootFor(runtime) {
38175
39007
  if (typeof cached === "string" && cached.length > 0) {
38176
39008
  return canonicalizeDirectory(cached);
38177
39009
  }
38178
- const raw = runtime.worktree ?? runtime.directory;
39010
+ const raw = runtime.directoryIsSessionRoot ? runtime.directory : runtime.worktree ?? runtime.directory;
38179
39011
  return canonicalizeDirectory(raw);
38180
39012
  }
38181
39013
  async function resolveProjectRoot(ctx, runtime) {
@@ -38211,6 +39043,11 @@ function bridgeFor(ctx, runtime) {
38211
39043
  }
38212
39044
  return ctx.pool.getBridge(projectRoot);
38213
39045
  }
39046
+ async function isWorkerRuntime(ctx, runtime) {
39047
+ if (!runtime.sessionID)
39048
+ return false;
39049
+ return resolveIsSubagent(ctx.client, runtime.sessionID, runtime.directory);
39050
+ }
38214
39051
  async function callBridge(ctx, runtime, command, params = {}, options) {
38215
39052
  if (runtime.sessionID && getSessionDirectoryCached(runtime.sessionID) === undefined) {
38216
39053
  await getSessionDirectory(ctx.client, runtime.sessionID, runtime.directory);
@@ -38219,6 +39056,9 @@ async function callBridge(ctx, runtime, command, params = {}, options) {
38219
39056
  if (runtime.sessionID) {
38220
39057
  merged.session_id = runtime.sessionID;
38221
39058
  }
39059
+ if (await isWorkerRuntime(ctx, runtime)) {
39060
+ merged[WORKER_SESSION_FIELD] = true;
39061
+ }
38222
39062
  const timeoutMs = timeoutForCommand(command);
38223
39063
  const sendOptions = {
38224
39064
  ...timeoutMs !== undefined ? { timeoutMs } : {},
@@ -38245,10 +39085,12 @@ async function callToolCall(ctx, runtime, name, rawArgs = {}, options) {
38245
39085
  await getSessionDirectory(ctx.client, runtime.sessionID, runtime.directory);
38246
39086
  }
38247
39087
  const timeoutMs = timeoutForCommand(name);
39088
+ const workerSession = await isWorkerRuntime(ctx, runtime);
38248
39089
  const sendOptions = {
38249
39090
  ...timeoutMs !== undefined ? { timeoutMs } : {},
38250
39091
  configureWarningClient: ctx.client,
38251
39092
  ...runtime.effectAbort ? { abortSignal: runtime.effectAbort } : {},
39093
+ ...workerSession ? { workerSession: true } : {},
38252
39094
  ...options
38253
39095
  };
38254
39096
  markBridgeStart();
@@ -38367,10 +39209,10 @@ import {
38367
39209
  createReadStream as createReadStream2,
38368
39210
  existsSync as existsSync15,
38369
39211
  mkdirSync as mkdirSync7,
38370
- readFileSync as readFileSync12,
39212
+ readFileSync as readFileSync13,
38371
39213
  renameSync as renameSync9,
38372
39214
  rmSync as rmSync5,
38373
- statSync as statSync10,
39215
+ statSync as statSync11,
38374
39216
  writeFileSync as writeFileSync8
38375
39217
  } from "node:fs";
38376
39218
  import { join as join17 } from "node:path";
@@ -38381,10 +39223,10 @@ import {
38381
39223
  closeSync as closeSync6,
38382
39224
  mkdirSync as mkdirSync6,
38383
39225
  openSync as openSync6,
38384
- readFileSync as readFileSync10,
39226
+ readFileSync as readFileSync11,
38385
39227
  renameSync as renameSync8,
38386
39228
  rmSync as rmSync4,
38387
- statSync as statSync9,
39229
+ statSync as statSync10,
38388
39230
  unlinkSync as unlinkSync6,
38389
39231
  writeFileSync as writeFileSync7
38390
39232
  } from "node:fs";
@@ -38407,7 +39249,7 @@ function lspBinDir(npmPackage) {
38407
39249
  function isInstalled(npmPackage, binary) {
38408
39250
  for (const candidate of lspBinaryCandidates(binary)) {
38409
39251
  try {
38410
- if (statSync9(join15(lspBinDir(npmPackage), candidate)).isFile())
39252
+ if (statSync10(join15(lspBinDir(npmPackage), candidate)).isFile())
38411
39253
  return true;
38412
39254
  } catch {}
38413
39255
  }
@@ -38435,9 +39277,9 @@ function writeInstalledMetaIn(installDir, version, sha256) {
38435
39277
  function readInstalledMetaIn(installDir) {
38436
39278
  const path = join15(installDir, INSTALLED_META_FILE);
38437
39279
  try {
38438
- if (!statSync9(path).isFile())
39280
+ if (!statSync10(path).isFile())
38439
39281
  return null;
38440
- const raw = readFileSync10(path, "utf8");
39282
+ const raw = readFileSync11(path, "utf8");
38441
39283
  const parsed = JSON.parse(raw);
38442
39284
  if (typeof parsed.version !== "string" || parsed.version.length === 0)
38443
39285
  return null;
@@ -38506,12 +39348,12 @@ ${token}
38506
39348
  let owningPid = null;
38507
39349
  let lockMtimeMs = 0;
38508
39350
  try {
38509
- const raw = readFileSync10(lock, "utf8");
39351
+ const raw = readFileSync11(lock, "utf8");
38510
39352
  const firstLine = raw.split(/\r?\n/, 1)[0]?.trim() ?? "";
38511
39353
  const parsed = Number.parseInt(firstLine, 10);
38512
39354
  if (Number.isFinite(parsed) && parsed > 0)
38513
39355
  owningPid = parsed;
38514
- lockMtimeMs = statSync9(lock).mtimeMs;
39356
+ lockMtimeMs = statSync10(lock).mtimeMs;
38515
39357
  } catch {
38516
39358
  return tryClaim();
38517
39359
  }
@@ -38545,7 +39387,7 @@ function releaseInstallLock(lockKey, acquisitionToken) {
38545
39387
  let owningPid = null;
38546
39388
  let owningToken = "";
38547
39389
  try {
38548
- const raw = readFileSync10(lock, "utf8");
39390
+ const raw = readFileSync11(lock, "utf8");
38549
39391
  const lines = raw.split(/\r?\n/);
38550
39392
  const parsed = Number.parseInt(lines[0]?.trim() ?? "", 10);
38551
39393
  if (Number.isFinite(parsed) && parsed > 0)
@@ -38592,7 +39434,7 @@ function autoInstallStampPath() {
38592
39434
  }
38593
39435
  function hasRecentAutoInstallStamp(path) {
38594
39436
  try {
38595
- const parsed = JSON.parse(readFileSync10(path, "utf8"));
39437
+ const parsed = JSON.parse(readFileSync11(path, "utf8"));
38596
39438
  return typeof parsed.lastRunMs === "number" && Number.isFinite(parsed.lastRunMs) && Math.abs(Date.now() - parsed.lastRunMs) < AUTO_INSTALL_WINDOW_MS;
38597
39439
  } catch {
38598
39440
  return false;
@@ -38600,7 +39442,7 @@ function hasRecentAutoInstallStamp(path) {
38600
39442
  }
38601
39443
  function readAutoInstallLockOwner(path) {
38602
39444
  try {
38603
- const parsed = JSON.parse(readFileSync10(path, "utf8"));
39445
+ const parsed = JSON.parse(readFileSync11(path, "utf8"));
38604
39446
  if (typeof parsed.pid !== "number" || typeof parsed.startedMs !== "number" || typeof parsed.token !== "string") {
38605
39447
  return null;
38606
39448
  }
@@ -38613,7 +39455,7 @@ function autoInstallLockIsStale(path) {
38613
39455
  const owner = readAutoInstallLockOwner(path);
38614
39456
  let age = Number.POSITIVE_INFINITY;
38615
39457
  try {
38616
- age = Math.abs(Date.now() - statSync9(path).mtimeMs);
39458
+ age = Math.abs(Date.now() - statSync10(path).mtimeMs);
38617
39459
  } catch {
38618
39460
  return true;
38619
39461
  }
@@ -38691,7 +39533,7 @@ var VERSION_CHECK_FILE = ".aft-version-check";
38691
39533
  function readVersionCheck(npmPackage) {
38692
39534
  const file = join15(lspPackageDir(npmPackage), VERSION_CHECK_FILE);
38693
39535
  try {
38694
- const raw = readFileSync10(file, "utf8");
39536
+ const raw = readFileSync11(file, "utf8");
38695
39537
  const parsed = JSON.parse(raw);
38696
39538
  if (typeof parsed.last_checked === "string") {
38697
39539
  return {
@@ -38794,6 +39636,14 @@ var NPM_LSP_TABLE = [
38794
39636
  extensions: ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"],
38795
39637
  rootMarkers: ["tsconfig.json", "jsconfig.json", "package.json"]
38796
39638
  },
39639
+ {
39640
+ id: "typescript-sdk",
39641
+ npm: "typescript",
39642
+ binary: "tsserver",
39643
+ extensions: ["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts"],
39644
+ rootMarkers: ["tsconfig.json", "jsconfig.json", "package.json"],
39645
+ supportedMajor: 5
39646
+ },
38797
39647
  {
38798
39648
  id: "python",
38799
39649
  npm: "pyright",
@@ -38866,9 +39716,18 @@ var NPM_LSP_TABLE = [
38866
39716
  rootMarkers: ["biome.json", "biome.jsonc"]
38867
39717
  }
38868
39718
  ];
39719
+ function majorVersionOf(version) {
39720
+ const match = /^v?(\d+)(?:\.|$)/.exec(version.trim());
39721
+ return match ? Number(match[1]) : null;
39722
+ }
39723
+ function isVersionSupported(spec, version) {
39724
+ if (spec.supportedMajor === undefined)
39725
+ return true;
39726
+ return majorVersionOf(version) === spec.supportedMajor;
39727
+ }
38869
39728
 
38870
39729
  // src/lsp-project-relevance.ts
38871
- import { existsSync as existsSync14, readdirSync as readdirSync4, readFileSync as readFileSync11 } from "node:fs";
39730
+ import { existsSync as existsSync14, readdirSync as readdirSync4, readFileSync as readFileSync12 } from "node:fs";
38872
39731
  import { join as join16 } from "node:path";
38873
39732
  var MAX_WALK_DIRS = 200;
38874
39733
  var MAX_WALK_DEPTH = 4;
@@ -38910,7 +39769,7 @@ function hasPackageJsonDep(projectRoot, depNames) {
38910
39769
  }
38911
39770
  function readPackageJson(projectRoot) {
38912
39771
  try {
38913
- const raw = readFileSync11(join16(projectRoot, "package.json"), "utf8");
39772
+ const raw = readFileSync12(join16(projectRoot, "package.json"), "utf8");
38914
39773
  const parsed = JSON.parse(raw);
38915
39774
  if (typeof parsed !== "object" || parsed === null)
38916
39775
  return null;
@@ -38962,10 +39821,11 @@ function extensionOf(fileName) {
38962
39821
 
38963
39822
  // src/lsp-registry-probe.ts
38964
39823
  var NPM_REGISTRY_BASE = "https://registry.npmjs.org";
38965
- function pickEligibleVersion(response, graceDays, now = Date.now()) {
39824
+ function pickEligibleVersion(response, graceDays, now = Date.now(), accept = () => true) {
38966
39825
  const times = response.time || {};
38967
39826
  const cutoff = now - graceDays * 24 * 60 * 60 * 1000;
38968
39827
  const candidates = [];
39828
+ let releaseCount = 0;
38969
39829
  for (const [version, publishedAt] of Object.entries(times)) {
38970
39830
  if (version === "created" || version === "modified")
38971
39831
  continue;
@@ -38976,6 +39836,9 @@ function pickEligibleVersion(response, graceDays, now = Date.now()) {
38976
39836
  const ts = Date.parse(publishedAt);
38977
39837
  if (Number.isNaN(ts))
38978
39838
  continue;
39839
+ releaseCount += 1;
39840
+ if (!accept(version))
39841
+ continue;
38979
39842
  candidates.push({ version, publishedAt, ts });
38980
39843
  }
38981
39844
  candidates.sort((a, b) => b.ts - a.ts);
@@ -38984,10 +39847,11 @@ function pickEligibleVersion(response, graceDays, now = Date.now()) {
38984
39847
  return {
38985
39848
  version: eligible[0]?.version ?? null,
38986
39849
  blockedByGrace,
39850
+ outsideSupportedRange: releaseCount > 0 && candidates.length === 0,
38987
39851
  eligible: eligible.map(({ version, publishedAt }) => ({ version, publishedAt }))
38988
39852
  };
38989
39853
  }
38990
- async function probeRegistry(npmPackage, graceDays, fetchImpl = fetch) {
39854
+ async function probeRegistry(npmPackage, graceDays, fetchImpl = fetch, accept) {
38991
39855
  const encoded = encodeURIComponent(npmPackage).replace(/^%40/, "@");
38992
39856
  const url = `${NPM_REGISTRY_BASE}/${encoded}`;
38993
39857
  try {
@@ -39000,7 +39864,7 @@ async function probeRegistry(npmPackage, graceDays, fetchImpl = fetch) {
39000
39864
  return null;
39001
39865
  }
39002
39866
  const json = await res.json();
39003
- return pickEligibleVersion(json, graceDays);
39867
+ return pickEligibleVersion(json, graceDays, Date.now(), accept);
39004
39868
  } catch (err) {
39005
39869
  warn2(`[lsp] registry probe failed for ${npmPackage}: ${err}`);
39006
39870
  return null;
@@ -39051,11 +39915,11 @@ async function resolveTargetVersion(spec, config, fetchImpl = fetch) {
39051
39915
  }
39052
39916
  const cached = readVersionCheck(spec.npm);
39053
39917
  const weeklyMs = config.graceDays * 24 * 60 * 60 * 1000;
39054
- const cachedSafe = isSafeVersion(cached?.latest_eligible ?? null);
39918
+ const cachedSafe = isSafeVersion(cached?.latest_eligible ?? null) && isVersionSupported(spec, cached?.latest_eligible);
39055
39919
  if (cached && !shouldRecheckVersion(cached, weeklyMs) && cachedSafe) {
39056
39920
  return { version: cached.latest_eligible, pinned: false, probe: null };
39057
39921
  }
39058
- const probe = await probeRegistry(spec.npm, config.graceDays, fetchImpl);
39922
+ const probe = await probeRegistry(spec.npm, config.graceDays, fetchImpl, (candidate) => isVersionSupported(spec, candidate));
39059
39923
  if (!probe) {
39060
39924
  return {
39061
39925
  version: cachedSafe ? cached?.latest_eligible ?? null : null,
@@ -39190,11 +40054,16 @@ async function ensureServerInstalled(spec, config, fetchImpl, signal) {
39190
40054
  const { version, probe } = await resolveTargetVersion(spec, config, fetchImpl);
39191
40055
  if (!version) {
39192
40056
  const installed = isInstalled(spec.npm, spec.binary);
39193
- if (installed) {
40057
+ if (installed && installedVersionAllowed(spec, config)) {
39194
40058
  warn2(`[lsp] no eligible version of ${spec.npm} (grace=${config.graceDays}d); keeping existing install`);
39195
40059
  return { started: false, reason: "kept existing install" };
39196
40060
  }
39197
- const blocked = probe?.blockedByGrace ? `all versions are within ${config.graceDays}-day grace window` : "registry probe failed";
40061
+ 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";
40062
+ if (installed) {
40063
+ 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}`;
40064
+ warn2(`[lsp] ${reason}`);
40065
+ return { started: false, reason };
40066
+ }
39198
40067
  warn2(`[lsp] skipping ${spec.npm}: ${blocked}`);
39199
40068
  return { started: false, reason: blocked };
39200
40069
  }
@@ -39258,7 +40127,7 @@ function hashInstalledBinary(spec) {
39258
40127
  let pathToHash = null;
39259
40128
  for (const p of candidates) {
39260
40129
  try {
39261
- if (statSync10(p).isFile()) {
40130
+ if (statSync11(p).isFile()) {
39262
40131
  pathToHash = p;
39263
40132
  break;
39264
40133
  }
@@ -39284,14 +40153,14 @@ function installedBinaryPath(spec) {
39284
40153
  ] : [lspBinaryPath(spec.npm, spec.binary)];
39285
40154
  for (const candidate of candidates) {
39286
40155
  try {
39287
- if (statSync10(candidate).isFile())
40156
+ if (statSync11(candidate).isFile())
39288
40157
  return candidate;
39289
40158
  } catch {}
39290
40159
  }
39291
40160
  return null;
39292
40161
  }
39293
40162
  function sha256OfFileSync(path) {
39294
- return createHash7("sha256").update(readFileSync12(path)).digest("hex");
40163
+ return createHash7("sha256").update(readFileSync13(path)).digest("hex");
39295
40164
  }
39296
40165
  function quarantineCachedNpmInstall(spec, reason) {
39297
40166
  const packageDir = lspPackageDir(spec.npm);
@@ -39319,6 +40188,15 @@ function validateCachedNpmInstall(spec) {
39319
40188
  }
39320
40189
  return true;
39321
40190
  }
40191
+ function installedVersionAllowed(spec, config) {
40192
+ const installed = readInstalledMeta(spec.npm)?.version;
40193
+ if (!installed)
40194
+ return spec.supportedMajor === undefined;
40195
+ return isVersionSupported(spec, installed) || config.versions[spec.npm] === installed;
40196
+ }
40197
+ function cachedInstallUsable(spec, config) {
40198
+ return isInstalled(spec.npm, spec.binary) && validateCachedNpmInstall(spec) && installedVersionAllowed(spec, config);
40199
+ }
39322
40200
  function runAutoInstall(projectRoot, config, fetchImpl = fetch) {
39323
40201
  const cachedBinDirs = [];
39324
40202
  const skipped = [];
@@ -39331,10 +40209,10 @@ function runAutoInstall(projectRoot, config, fetchImpl = fetch) {
39331
40209
  return projectExtensions;
39332
40210
  };
39333
40211
  for (const spec of NPM_LSP_TABLE) {
39334
- if (isInstalled(spec.npm, spec.binary) && validateCachedNpmInstall(spec)) {
40212
+ if (cachedInstallUsable(spec, config)) {
39335
40213
  cachedBinDirs.push(lspBinDir(spec.npm));
39336
40214
  }
39337
- if (config.disabled.has(spec.id)) {
40215
+ if (config.disabled.has(spec.id) || spec.id === "typescript-sdk" && config.disabled.has("typescript")) {
39338
40216
  skipped.push({ id: spec.id, reason: "disabled by config" });
39339
40217
  continue;
39340
40218
  }
@@ -39371,7 +40249,7 @@ function runAutoInstall(projectRoot, config, fetchImpl = fetch) {
39371
40249
  },
39372
40250
  skipped,
39373
40251
  installsComplete: Promise.all(installPromises).then(() => {}),
39374
- getCachedBinDirs: () => NPM_LSP_TABLE.filter((spec) => isInstalled(spec.npm, spec.binary) && validateCachedNpmInstall(spec)).map((spec) => lspBinDir(spec.npm))
40252
+ getCachedBinDirs: () => NPM_LSP_TABLE.filter((spec) => cachedInstallUsable(spec, config)).map((spec) => lspBinDir(spec.npm))
39375
40253
  };
39376
40254
  }
39377
40255
  async function pushLspPathsAfterAutoInstall(pool, projectRoot, cachedBinDirs) {
@@ -39390,16 +40268,16 @@ import {
39390
40268
  lstatSync as lstatSync2,
39391
40269
  mkdirSync as mkdirSync8,
39392
40270
  readdirSync as readdirSync5,
39393
- readFileSync as readFileSync13,
40271
+ readFileSync as readFileSync14,
39394
40272
  readlinkSync as readlinkSync2,
39395
40273
  realpathSync as realpathSync4,
39396
40274
  renameSync as renameSync10,
39397
40275
  rmSync as rmSync6,
39398
- statSync as statSync11,
40276
+ statSync as statSync12,
39399
40277
  unlinkSync as unlinkSync7,
39400
40278
  writeFileSync as writeFileSync9
39401
40279
  } from "node:fs";
39402
- import { dirname as dirname8, join as join18, relative as relative3, resolve as resolve10 } from "node:path";
40280
+ import { dirname as dirname9, join as join18, relative as relative3, resolve as resolve10 } from "node:path";
39403
40281
  import { Readable as Readable3 } from "node:stream";
39404
40282
  import { pipeline as pipeline3 } from "node:stream/promises";
39405
40283
 
@@ -39508,7 +40386,7 @@ function installFailureMarkerPath(spec) {
39508
40386
  }
39509
40387
  function readInstallFailureMarker(spec) {
39510
40388
  try {
39511
- const marker = JSON.parse(readFileSync13(installFailureMarkerPath(spec), "utf8"));
40389
+ const marker = JSON.parse(readFileSync14(installFailureMarkerPath(spec), "utf8"));
39512
40390
  if (typeof marker.release !== "string" || typeof marker.sha256 !== "string" || typeof marker.error !== "string" || typeof marker.at !== "string" || !Number.isFinite(Date.parse(marker.at))) {
39513
40391
  return null;
39514
40392
  }
@@ -39522,7 +40400,7 @@ function writeInstallFailureMarker(spec, release, sha256, installError) {
39522
40400
  const markerError = installError.replace(/\s+/g, " ").trim();
39523
40401
  const tempPath = `${path}.tmp-${process.pid}-${randomBytes4(6).toString("hex")}`;
39524
40402
  try {
39525
- mkdirSync8(dirname8(path), { recursive: true });
40403
+ mkdirSync8(dirname9(path), { recursive: true });
39526
40404
  writeFileSync9(tempPath, `${JSON.stringify({ release, sha256, error: markerError, at: new Date().toISOString() })}
39527
40405
  `, { encoding: "utf8", mode: 384 });
39528
40406
  renameSync10(tempPath, path);
@@ -39551,7 +40429,7 @@ function ghBinaryPath(spec, platform) {
39551
40429
  function isGithubInstalled(spec, platform) {
39552
40430
  for (const candidate of ghBinaryCandidates(spec, platform)) {
39553
40431
  try {
39554
- if (statSync11(join18(ghBinDir(spec), candidate)).isFile())
40432
+ if (statSync12(join18(ghBinDir(spec), candidate)).isFile())
39555
40433
  return true;
39556
40434
  } catch {}
39557
40435
  }
@@ -39565,9 +40443,9 @@ function ghBinaryCandidates(spec, platform) {
39565
40443
  function readGithubInstalledMetaIn(installDir) {
39566
40444
  try {
39567
40445
  const path = join18(installDir, INSTALLED_META_FILE2);
39568
- if (!statSync11(path).isFile())
40446
+ if (!statSync12(path).isFile())
39569
40447
  return null;
39570
- const parsed = JSON.parse(readFileSync13(path, "utf8"));
40448
+ const parsed = JSON.parse(readFileSync14(path, "utf8"));
39571
40449
  if (typeof parsed.version !== "string" || parsed.version.length === 0)
39572
40450
  return null;
39573
40451
  return {
@@ -39608,7 +40486,7 @@ function sha256OfFile(path) {
39608
40486
  });
39609
40487
  }
39610
40488
  function sha256OfFileSync2(path) {
39611
- return createHash8("sha256").update(readFileSync13(path)).digest("hex");
40489
+ return createHash8("sha256").update(readFileSync14(path)).digest("hex");
39612
40490
  }
39613
40491
  async function fetchReleaseByTag(githubRepo, tag, fetchImpl, signal) {
39614
40492
  const candidates = [];
@@ -39784,7 +40662,7 @@ async function downloadFile(url, destPath, fetchImpl, assetSize, signal) {
39784
40662
  if (Number.isFinite(advertised) && advertised > MAX_DOWNLOAD_BYTES3) {
39785
40663
  throw new Error(`Content-Length ${advertised} exceeds max ${MAX_DOWNLOAD_BYTES3}`);
39786
40664
  }
39787
- mkdirSync8(dirname8(destPath), { recursive: true });
40665
+ mkdirSync8(dirname9(destPath), { recursive: true });
39788
40666
  let bytesWritten = 0;
39789
40667
  const guard = new TransformStream({
39790
40668
  transform(chunk, controller) {
@@ -39954,7 +40832,7 @@ function quarantineCachedGithubInstall(spec, reason) {
39954
40832
  const dest = join18(ghCacheRoot(), ".quarantine", encodeURIComponent(spec.id), `${Date.now()}`);
39955
40833
  warn2(`[lsp] tofu_mismatch ${spec.id}: ${reason}; quarantining ${packageDir} -> ${dest}`);
39956
40834
  try {
39957
- mkdirSync8(dirname8(dest), { recursive: true });
40835
+ mkdirSync8(dirname9(dest), { recursive: true });
39958
40836
  rmSync6(dest, { recursive: true, force: true });
39959
40837
  renameSync10(packageDir, dest);
39960
40838
  } catch (err) {
@@ -39966,7 +40844,7 @@ function validateCachedGithubInstall(spec, platform) {
39966
40844
  const meta = readGithubInstalledMetaIn(packageDir);
39967
40845
  const binaryPath = ghBinaryCandidates(spec, platform).map((candidate) => join18(ghBinDir(spec), candidate)).find((candidate) => {
39968
40846
  try {
39969
- return statSync11(candidate).isFile();
40847
+ return statSync12(candidate).isFile();
39970
40848
  } catch {
39971
40849
  return false;
39972
40850
  }
@@ -40084,7 +40962,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
40084
40962
  return { ok: false, sha256: archiveSha256, error: message };
40085
40963
  }
40086
40964
  const targetBinary = ghBinaryPath(spec, platform);
40087
- mkdirSync8(dirname8(targetBinary), { recursive: true });
40965
+ mkdirSync8(dirname9(targetBinary), { recursive: true });
40088
40966
  try {
40089
40967
  copyFileSync3(innerBinaryPath, targetBinary);
40090
40968
  if (platform !== "win32") {
@@ -40613,14 +41491,16 @@ async function assertExternalDirectoryPermission(ctx, context, target, options)
40613
41491
  const directory = root ? normalizePath(root) : root;
40614
41492
  const rawWorktree = context.worktree;
40615
41493
  const worktree = rawWorktree && rawWorktree !== "/" ? normalizePath(rawWorktree) : rawWorktree;
41494
+ const restrictToProjectRoot = ctx.config.restrict_to_project_root === true;
40616
41495
  if (directory && containsPath(directory, absoluteTarget))
40617
41496
  return;
40618
- if (worktree && worktree !== "/" && worktree !== directory && containsPath(worktree, absoluteTarget)) {
41497
+ if (!restrictToProjectRoot && worktree && worktree !== "/" && worktree !== directory && containsPath(worktree, absoluteTarget)) {
40619
41498
  return;
40620
41499
  }
40621
- if (ctx.config.restrict_to_project_root === true) {
40622
- if (options?.serverValidatedRead === true)
41500
+ if (restrictToProjectRoot) {
41501
+ if (options?.serverValidatedRead === true && await isSessionOwnedBashArtifact(ctx, context, absoluteTarget)) {
40623
41502
  return;
41503
+ }
40624
41504
  notifyRestrictBlocked(ctx, context, absoluteTarget);
40625
41505
  return restrictDenialMessage(absoluteTarget);
40626
41506
  }
@@ -40920,312 +41800,8 @@ function astTools(ctx) {
40920
41800
  };
40921
41801
  }
40922
41802
 
40923
- // src/tools/conflicts.ts
40924
- var z3 = tool.schema;
40925
- function conflictTools(ctx) {
40926
- return {
40927
- aft_conflicts: {
40928
- description: "Show all git merge conflicts across the repository — returns line-numbered conflict regions with context for every conflicted file in a single call. Conflicts are discovered from the git repository's top level. By default it inspects the session's project repository; pass `path` to inspect a different repository or git worktree (e.g. where a rebase/merge is running).",
40929
- args: {
40930
- path: z3.string().describe("Optional path inside the git repository or worktree to inspect (absolute or relative to project root). Conflicts are discovered from that repository's top level. Defaults to the session project root.").optional()
40931
- },
40932
- execute: async (args, context) => {
40933
- const rawArgs = {};
40934
- if (!isEmptyParam(args?.path)) {
40935
- const expanded = expandTilde2(String(args.path));
40936
- const projectRoot = await resolveProjectRoot(ctx, context);
40937
- const resolved = resolvePathFromProjectRoot(projectRoot, expanded);
40938
- const denied = await assertExternalDirectoryPermission(ctx, context, resolved, {
40939
- kind: "directory"
40940
- });
40941
- if (denied)
40942
- return permissionDeniedResponse(denied);
40943
- rawArgs.path = resolved;
40944
- }
40945
- const response = await callToolCall(ctx, context, "conflicts", rawArgs);
40946
- if (response.success === false) {
40947
- throw new Error(response.message || "git_conflicts failed");
40948
- }
40949
- return response.text;
40950
- }
40951
- }
40952
- };
40953
- }
40954
-
40955
- // src/tools/definitions/v2.ts
40956
- import { Effect } from "effect";
40957
- var V2_TOOL_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
40958
- var V2_PATH_HEADER_TOOLS = new Set(["read", "write", "edit"]);
40959
- var V2_BASH_TOOLS = new Set(["bash"]);
40960
- var V2_SHELL_PERMISSION = "shell";
40961
- var ROOT_COMBINATORS = ["anyOf", "oneOf", "allOf"];
40962
- function failure(error) {
40963
- if (error instanceof Error)
40964
- return error;
40965
- const detail = (() => {
40966
- try {
40967
- return JSON.stringify(error) ?? String(error);
40968
- } catch {
40969
- return String(error);
40970
- }
40971
- })();
40972
- return new Error(`V2 tool execution rejected with a non-Error value: ${detail}`);
40973
- }
40974
- function isPlainObject3(value) {
40975
- if (typeof value !== "object" || value === null)
40976
- return false;
40977
- const prototype = Object.getPrototypeOf(value);
40978
- return prototype === Object.prototype || prototype === null;
40979
- }
40980
- function jsonSafeMetadata(value) {
40981
- if (Array.isArray(value)) {
40982
- return value.map((entry) => entry === undefined ? null : jsonSafeMetadata(entry));
40983
- }
40984
- if (!isPlainObject3(value))
40985
- return value;
40986
- const cleaned = {};
40987
- for (const [key, entry] of Object.entries(value)) {
40988
- if (entry === undefined)
40989
- continue;
40990
- cleaned[key] = jsonSafeMetadata(entry);
40991
- }
40992
- return cleaned;
40993
- }
40994
- function resultContent(result) {
40995
- if (typeof result === "string")
40996
- return { content: result };
40997
- const attachments = (result.attachments ?? []).map((attachment) => ({
40998
- type: "file",
40999
- uri: attachment.url,
41000
- mime: attachment.mime,
41001
- ...attachment.filename ? { name: attachment.filename } : {}
41002
- }));
41003
- const content = attachments.length ? [{ type: "text", text: result.output }, ...attachments] : result.output;
41004
- const metadata = jsonSafeMetadata({
41005
- ...result.metadata ?? {},
41006
- ...result.title ? { title: result.title } : {}
41007
- });
41008
- return {
41009
- content,
41010
- ...Object.keys(metadata).length ? { metadata } : {}
41011
- };
41012
- }
41013
- function failureEnvelopeMessage(result) {
41014
- const output = typeof result === "string" ? result : result.output;
41015
- if (typeof output !== "string")
41016
- return;
41017
- const trimmed = output.trim();
41018
- if (!trimmed.startsWith("{") || !trimmed.includes('"success"'))
41019
- return;
41020
- let parsed;
41021
- try {
41022
- parsed = JSON.parse(trimmed);
41023
- } catch {
41024
- return;
41025
- }
41026
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
41027
- return;
41028
- const envelope = parsed;
41029
- if (envelope.success !== false)
41030
- return;
41031
- for (const field of [envelope.message, envelope.error, envelope.code]) {
41032
- if (typeof field === "string" && field.length > 0)
41033
- return field;
41034
- }
41035
- return "The tool call failed.";
41036
- }
41037
- function bareToolName3(name) {
41038
- return name.startsWith("aft_") ? name.slice(4) : name;
41039
- }
41040
- function projectArguments(name, definition) {
41041
- if (!V2_PATH_HEADER_TOOLS.has(bareToolName3(name)) || !("filePath" in definition.args)) {
41042
- return { ...definition.args };
41043
- }
41044
- const { filePath, ...rest } = definition.args;
41045
- return { path: filePath, ...rest };
41046
- }
41047
- function executionArguments(name, input) {
41048
- if (!V2_PATH_HEADER_TOOLS.has(bareToolName3(name)) || !("path" in input))
41049
- return input;
41050
- const { path, ...rest } = input;
41051
- return { filePath: path, ...rest };
41052
- }
41053
- function hostPermission(name) {
41054
- const bare = bareToolName3(name);
41055
- if (new Set(["read", "glob", "grep"]).has(bare))
41056
- return bare;
41057
- if (V2_BASH_TOOLS.has(name))
41058
- return V2_SHELL_PERMISSION;
41059
- if (new Set([
41060
- "write",
41061
- "edit",
41062
- "apply_patch",
41063
- "delete",
41064
- "move",
41065
- "ast_grep_replace",
41066
- "import",
41067
- "safety"
41068
- ]).has(bare)) {
41069
- return "edit";
41070
- }
41071
- return;
41072
- }
41073
- function assertV2Contract(name, input) {
41074
- if (!V2_TOOL_NAME.test(name)) {
41075
- throw new Error(`Invalid V2 tool name ${JSON.stringify(name)}; expected ${V2_TOOL_NAME.source}`);
41076
- }
41077
- const schema = tool.schema.toJSONSchema(input, { io: "input" });
41078
- if (schema.type !== "object") {
41079
- throw new Error(`V2 tool ${name} must have an object-rooted input schema`);
41080
- }
41081
- for (const keyword of ROOT_COMBINATORS) {
41082
- if (keyword in schema) {
41083
- throw new Error(`V2 tool ${name} input schema cannot use root ${keyword}`);
41084
- }
41085
- }
41086
- }
41087
- function v2PermissionRequest(name, request) {
41088
- if (!V2_BASH_TOOLS.has(name) || request.permission !== "bash")
41089
- return request;
41090
- return { ...request, permission: V2_SHELL_PERMISSION };
41091
- }
41092
- function runtimeFor(name, location, context, signal, consumers) {
41093
- const directory = location.directory;
41094
- const worktree = location.project?.canonical ?? location.project?.directory ?? directory;
41095
- return {
41096
- sessionID: context.sessionID,
41097
- messageID: context.messageID,
41098
- agent: context.agent,
41099
- directory,
41100
- worktree,
41101
- abort: signal,
41102
- effectAbort: signal,
41103
- metadata: (update) => {
41104
- Effect.runPromise(context.progress(jsonSafeMetadata({
41105
- ...update.metadata ?? {},
41106
- ...update.title ? { title: update.title } : {}
41107
- })));
41108
- },
41109
- ask: (asked) => {
41110
- const request = v2PermissionRequest(name, asked);
41111
- if (consumers.requestPermission)
41112
- return consumers.requestPermission(request, context);
41113
- return Promise.reject(new Error(`The "${request.permission}" operation was refused because this AFT runtime has no permission evaluator bound, so the host's permission rules for it could not be consulted.`));
41114
- },
41115
- progress: context.progress
41116
- };
41117
- }
41118
- function projectV2Tool(name, definition, location, consumers = {}) {
41119
- const input = tool.schema.object(projectArguments(name, definition));
41120
- assertV2Contract(name, input);
41121
- const sharedOptions = definition.options;
41122
- const permission = hostPermission(name);
41123
- return {
41124
- name,
41125
- description: definition.description,
41126
- input,
41127
- options: {
41128
- ...sharedOptions,
41129
- ...permission ? { permission } : {},
41130
- codemode: false
41131
- },
41132
- execute: (rawInput, context) => Effect.tryPromise({
41133
- try: async (signal) => {
41134
- const runtime = runtimeFor(name, location, context, signal, consumers);
41135
- const input = executionArguments(name, rawInput);
41136
- const result = consumers.executeBash && V2_BASH_TOOLS.has(name) ? await consumers.executeBash({
41137
- name,
41138
- input,
41139
- context: runtime,
41140
- definition
41141
- }) : await definition.execute(input, runtime);
41142
- const refusal = failureEnvelopeMessage(result);
41143
- if (refusal)
41144
- throw new Error(refusal);
41145
- return resultContent(result);
41146
- },
41147
- catch: failure
41148
- })
41149
- };
41150
- }
41151
-
41152
- // src/tools/hoisted.ts
41153
- import * as path4 from "node:path";
41154
-
41155
- // src/shared/subagent-detect.ts
41156
- import { Effect as Effect2 } from "effect";
41157
- var CACHE_MAX_ENTRIES2 = 200;
41158
- var cache2 = new Map;
41159
- async function resolveIsSubagent(client, sessionId, _fallbackDirectory) {
41160
- if (!sessionId) {
41161
- sessionLog(undefined, "[subagent-detect] no sessionId provided → primary");
41162
- return false;
41163
- }
41164
- const cached = cache2.get(sessionId);
41165
- if (cached) {
41166
- cache2.delete(sessionId);
41167
- cache2.set(sessionId, cached);
41168
- return cached.isSubagent;
41169
- }
41170
- if (isV2PluginContext(client))
41171
- return resolveV2(client, sessionId);
41172
- const c = client;
41173
- const sessionApi = c?.session;
41174
- if (!sessionApi || typeof sessionApi.get !== "function") {
41175
- sessionLog(sessionId, `[subagent-detect] client.session.get unavailable (client=${typeof client}, session=${typeof sessionApi}, get=${typeof sessionApi?.get}) → caching as primary`);
41176
- setCache2(sessionId, false);
41177
- return false;
41178
- }
41179
- sessionLog(sessionId, `[subagent-detect] cache miss, calling client.session.get(id=${sessionId})`);
41180
- let isSubagent = false;
41181
- let parentIdRaw;
41182
- try {
41183
- const result = await sessionApi.get({
41184
- path: { id: sessionId }
41185
- });
41186
- const session = result?.data ?? result;
41187
- parentIdRaw = session?.parentID;
41188
- isSubagent = session !== undefined && typeof session.parentID === "string" && session.parentID.length > 0;
41189
- sessionLog(sessionId, `[subagent-detect] SDK returned session=${session !== undefined ? "present" : "undefined"}, parentID=${JSON.stringify(parentIdRaw)} → isSubagent=${isSubagent}`);
41190
- } catch (err) {
41191
- sessionWarn(sessionId, `[subagent-detect] SDK lookup failed: ${err instanceof Error ? err.message : String(err)}`);
41192
- return false;
41193
- }
41194
- setCache2(sessionId, isSubagent);
41195
- return isSubagent;
41196
- }
41197
- function isV2PluginContext(client) {
41198
- if (!client || typeof client !== "object")
41199
- return false;
41200
- const candidate = client;
41201
- return typeof candidate.location?.directory === "string" && typeof candidate.session?.get === "function";
41202
- }
41203
- async function resolveV2(context, sessionId) {
41204
- let session;
41205
- try {
41206
- session = await Effect2.runPromise(context.session.get({ sessionID: sessionId }));
41207
- } catch (err) {
41208
- sessionWarn(sessionId, `[subagent-detect] OpenCode 2 session lookup failed: ${err instanceof Error ? err.message : String(err)}`);
41209
- return false;
41210
- }
41211
- const isSubagent = typeof session?.parentID === "string" && session.parentID.length > 0;
41212
- sessionLog(sessionId, `[subagent-detect] OpenCode 2 session parentID=${JSON.stringify(session?.parentID)} → isSubagent=${isSubagent}`);
41213
- setCache2(sessionId, isSubagent);
41214
- return isSubagent;
41215
- }
41216
- function setCache2(sessionId, isSubagent) {
41217
- if (cache2.has(sessionId))
41218
- cache2.delete(sessionId);
41219
- cache2.set(sessionId, { isSubagent, recordedAt: Date.now() });
41220
- if (cache2.size > CACHE_MAX_ENTRIES2) {
41221
- const oldest = cache2.keys().next().value;
41222
- if (oldest !== undefined)
41223
- cache2.delete(oldest);
41224
- }
41225
- }
41226
-
41227
41803
  // src/tools/bash.ts
41228
- var z4 = tool.schema;
41804
+ var z3 = tool.schema;
41229
41805
  var METADATA_PREVIEW_LIMIT = 30 * 1024;
41230
41806
  var DEFAULT_HARD_TIMEOUT_MS = 30 * 60 * 1000;
41231
41807
  var BASH_TRANSPORT_MARGIN_MS = 1e4;
@@ -41241,7 +41817,9 @@ function resolveForegroundWaitMs(configured) {
41241
41817
  }
41242
41818
  return configured;
41243
41819
  }
41244
- function orchestratedTransportTimeoutMs(blockToCompletion, wait, effectiveTimeout, foregroundWaitMs) {
41820
+ function orchestratedTransportTimeoutMs(blockToCompletion, wait, effectiveTimeout, foregroundWaitMs, noHardKill = false) {
41821
+ if (noHardKill)
41822
+ return LONGEST_TIMER_DELAY_MS;
41245
41823
  const waitBudget = blockToCompletion || wait ? effectiveTimeout ?? DEFAULT_HARD_TIMEOUT_MS : foregroundWaitMs;
41246
41824
  return waitBudget + BASH_TRANSPORT_MARGIN_MS;
41247
41825
  }
@@ -41318,13 +41896,38 @@ function listenForForegroundAbort(ctx, runtime, enabled) {
41318
41896
  function userMessageDetachDescription(detachOnUserMessage) {
41319
41897
  return detachOnUserMessage ? "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 and the rest of the message is preserved; 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 and the rest of the message is preserved; a token-only message becomes `(requested background detach)`.";
41320
41898
  }
41321
- function backgroundWaitDescription(watchToolRegistered) {
41322
- return watchToolRegistered ? "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." : "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.";
41899
+ function bashCompanionRegistered(config, companion) {
41900
+ return resolveBashConfig(config).background && toolEnabled(config, companion);
41323
41901
  }
41324
- function bashToolDescription(aftSearchRegistered, compressionOn, backgroundOn, detachOnUserMessage = true, zoomEnabled = true, watchToolRegistered = true) {
41325
- const searchSteer = aftSearchRegistered ? `use aft_search (concepts, identifiers, regex, literals), read, aft_outline${zoomEnabled ? ", or aft_zoom" : ""} instead` : `use the grep tool, read, aft_outline${zoomEnabled ? ", or aft_zoom" : ""} instead`;
41902
+ function backgroundWaitDescription(watchToolRegistered, statusRegistered) {
41903
+ if (watchToolRegistered) {
41904
+ const noPolling = statusRegistered ? ", and never loop bash_status to wait" : "";
41905
+ 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}.`;
41906
+ }
41907
+ return statusRegistered ? "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.";
41908
+ }
41909
+ function ptyDriveClause(statusRegistered, writeRegistered) {
41910
+ if (statusRegistered && writeRegistered) {
41911
+ return ', and is driven with bash_status({ outputMode: "screen" }) plus bash_write';
41912
+ }
41913
+ if (statusRegistered)
41914
+ return ', and its screen is read with bash_status({ outputMode: "screen" })';
41915
+ if (writeRegistered)
41916
+ return ", and is driven with bash_write";
41917
+ return "";
41918
+ }
41919
+ function bashToolDescription(aftSearchRegistered, compressionOn, backgroundOn, detachOnUserMessage = true, zoomEnabled = true, watchToolRegistered = true, surface = {}) {
41920
+ const outline = surface.outline !== false;
41921
+ const status = surface.status !== false;
41922
+ const write = surface.write !== false;
41923
+ const steerTools = [
41924
+ aftSearchRegistered ? "aft_search (concepts, identifiers, regex, literals)" : "the grep tool",
41925
+ "read",
41926
+ ...outline ? ["aft_outline"] : []
41927
+ ].join(", ");
41928
+ const searchSteer = `use ${steerTools}${zoomEnabled ? ", or aft_zoom" : ""} instead`;
41326
41929
  const compression = compressionOn ? " 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. Pipeline-failure notes cover single top-level pipelines only; multi-statement commands (`a; b | c; d`) are not instrumented, so masked failures inside them still need explicit exit-code checks." : "";
41327
- const tasks = backgroundOn ? ` Commands run in the foreground and return inline; wait: true blocks until a long command finishes instead of auto-promoting; ${userMessageDetachDescription(detachOnUserMessage)} 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; ${backgroundWaitDescription(watchToolRegistered)} 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({ outputMode: "screen" }) plus bash_write.` : " Commands run in the foreground to completion; timeout is the hard kill cap (default 30 minutes).";
41930
+ const tasks = backgroundOn ? ` Commands run in the foreground and return inline; wait: true blocks until a long command finishes instead of auto-promoting; ${userMessageDetachDescription(detachOnUserMessage)} 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; ${backgroundWaitDescription(watchToolRegistered, status)} 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(status, write)}.` : " Commands run in the foreground to completion; timeout is the hard kill cap (default 30 minutes).";
41328
41931
  return `Execute shell commands.${compression}${tasks}
41329
41932
 
41330
41933
  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.`;
@@ -41401,30 +42004,63 @@ async function withPermissionLoop(ctx, runtime, params, bridgeCall, options) {
41401
42004
  }
41402
42005
  throw new Error("bash permission retry failed: too many rounds");
41403
42006
  }
42007
+ function ptyDriveParamSentence(statusRegistered, writeRegistered) {
42008
+ const writeInput = 'its input accepts either a string OR an array like [ "iHello", { key: "esc" }, ":wq", { key: "enter" } ] for atomic text+key sequences.';
42009
+ if (statusRegistered && writeRegistered) {
42010
+ return ` Inspect with bash_status({ taskId, outputMode: "screen" }) and drive interactively with bash_write — ${writeInput}`;
42011
+ }
42012
+ if (statusRegistered)
42013
+ return ' Inspect with bash_status({ taskId, outputMode: "screen" }).';
42014
+ if (writeRegistered)
42015
+ return ` Drive interactively with bash_write — ${writeInput}`;
42016
+ return "";
42017
+ }
42018
+ function nativeSandboxEnabled(config) {
42019
+ return config.sandbox?.enabled === true;
42020
+ }
41404
42021
  function createBashTool(ctx, aftSearchRegisteredOverride) {
41405
42022
  const initialBashCfg = resolveBashConfig(ctx.config);
42023
+ const statusRegistered = bashCompanionRegistered(ctx.config, "bash_status");
42024
+ const writeRegistered = bashCompanionRegistered(ctx.config, "bash_write");
42025
+ const taskControls = [
42026
+ ...statusRegistered ? ["bash_status"] : [],
42027
+ ...bashCompanionRegistered(ctx.config, "bash_kill") ? ["bash_kill"] : []
42028
+ ];
42029
+ const waitArg = initialBashCfg.background ? {
42030
+ wait: z3.boolean().optional().describe(`When true, run in the foreground without auto-promoting and wait until the command finishes or reaches its timeout; ${userMessageDetachDescription(initialBashCfg.detach_on_user_message)} Use only when you know the result is required before doing anything else.`)
42031
+ } : {};
42032
+ const sandboxArg = nativeSandboxEnabled(ctx.config) ? {
42033
+ sandbox: z3.literal("host").optional().describe("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.")
42034
+ } : {};
42035
+ const compressedArg = initialBashCfg.compress ? {
42036
+ compressed: z3.boolean().optional().describe("When true or omitted, return compressed output with noisy terminal control sequences reduced. Set to false for raw output.")
42037
+ } : {};
41406
42038
  const backgroundFlagArg = initialBashCfg.background ? {
41407
- background: z4.boolean().optional().describe("When true, spawn the command in the background and return a taskId for bash_status/bash_kill instead of waiting for completion. Defaults to false.")
42039
+ background: z3.boolean().optional().describe(`When true, spawn the command in the background and return a taskId${taskControls.length > 0 ? ` for ${taskControls.join("/")}` : ""} instead of waiting for completion. Defaults to false.`)
41408
42040
  } : {};
41409
42041
  const ptyArgs = initialBashCfg.background ? {
41410
- pty: z4.boolean().optional().describe('When true, spawn the command in a real PTY for interactive programs (python/node/bash REPLs, vim). Implies background: true automatically. Unavailable in subagent sessions. Inspect with bash_status({ taskId, outputMode: "screen" }) and drive interactively with bash_write — its input accepts either a string OR an array like [ "iHello", { key: "esc" }, ":wq", { key: "enter" } ] for atomic text+key sequences.'),
42042
+ pty: z3.boolean().optional().describe(`When true, spawn the command in a real PTY for interactive programs (python/node/bash REPLs, vim). Implies background: true automatically.${initialBashCfg.subagent_background ? "" : " Unavailable in subagent sessions because bash.subagent_background is false."}${ptyDriveParamSentence(statusRegistered, writeRegistered)}`),
41411
42043
  ptyRows: optionalInt(1, 60).describe("PTY terminal height in rows — ignored when pty is false. Defaults to 24 when pty: true. Minimum 1, maximum 60."),
41412
42044
  ptyCols: optionalInt(1, 140).describe("PTY terminal width in columns — ignored when pty is false. Defaults to 80 when pty: true. Minimum 1, maximum 140.")
41413
42045
  } : {};
41414
42046
  const args = {
41415
- command: z4.string().describe("Shell command to execute. Supports pipes, redirection, and normal shell syntax."),
42047
+ command: z3.string().describe("Shell command to execute. Supports pipes, redirection, and normal shell syntax."),
41416
42048
  timeout: optionalInt(1, Number.MAX_SAFE_INTEGER).describe(initialBashCfg.background ? "Hard kill cap in milliseconds (positive integer). In the default foreground mode when wait is false, a command that exceeds the configured wait window is promoted to background and gets a completion reminder when it exits; wait:true disables promotion and remains inline until completion or timeout." : "Hard kill cap in milliseconds (positive integer). When omitted, the foreground command can run up to 30 minutes and returns inline when it finishes."),
41417
- workdir: z4.string().optional().describe("Working directory for command execution. Relative paths resolve through the bridge; defaults to the current tool context/project root when omitted."),
41418
- description: z4.string().optional().describe("Short 5-10 word human-readable summary shown in OpenCode UI metadata instead of raw shell syntax."),
41419
- wait: z4.boolean().optional().describe(`When true, run in the foreground without auto-promoting and wait until the command finishes or reaches its timeout; ${userMessageDetachDescription(initialBashCfg.detach_on_user_message)} Use only when you know the result is required before doing anything else.`),
41420
- sandbox: z4.literal("host").optional().describe("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."),
42049
+ workdir: z3.string().optional().describe("Working directory for command execution. Relative paths resolve through the bridge; defaults to the current tool context/project root when omitted."),
42050
+ description: z3.string().optional().describe("Short 5-10 word human-readable summary shown in OpenCode UI metadata instead of raw shell syntax."),
42051
+ ...waitArg,
42052
+ ...sandboxArg,
41421
42053
  ...backgroundFlagArg,
41422
- compressed: z4.boolean().optional().describe("When true or omitted, return compressed output with noisy terminal control sequences reduced. Set to false for raw output."),
42054
+ ...compressedArg,
41423
42055
  ...ptyArgs
41424
42056
  };
41425
42057
  let hostFallbackActive = false;
41426
42058
  return {
41427
- description: bashToolDescription(false, initialBashCfg.compress, initialBashCfg.background, true, toolEnabled(ctx.config, "aft_zoom")),
42059
+ description: bashToolDescription(false, initialBashCfg.compress, initialBashCfg.background, true, toolEnabled(ctx.config, "aft_zoom"), bashCompanionRegistered(ctx.config, "bash_watch"), {
42060
+ outline: toolEnabled(ctx.config, "aft_outline"),
42061
+ status: statusRegistered,
42062
+ write: writeRegistered
42063
+ }),
41428
42064
  args,
41429
42065
  execute: async (args, context) => {
41430
42066
  const bashCfg = resolveBashConfig(ctx.config);
@@ -41438,7 +42074,7 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
41438
42074
  const cwd = args.workdir ?? context.directory;
41439
42075
  const isSubagent = await resolveIsSubagent(ctx.client, context.sessionID, context.directory);
41440
42076
  const backgroundDisabled = !bashCfg.background;
41441
- const requestedWait = coerceBoolean(args.wait);
42077
+ const requestedWait = !backgroundDisabled && coerceBoolean(args.wait);
41442
42078
  const rawRequestedPty = coerceBoolean(args.pty);
41443
42079
  const rawRequestedBackground = coerceBoolean(args.background);
41444
42080
  if (requestedWait && rawRequestedPty) {
@@ -41449,14 +42085,15 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
41449
42085
  }
41450
42086
  const requestedPty = !backgroundDisabled && rawRequestedPty;
41451
42087
  const requestedBackground = !backgroundDisabled && (rawRequestedBackground || requestedPty);
41452
- if (requestedPty && isSubagent) {
41453
- throw new Error("PTY mode is not available in subagent sessions; subagents cannot drive interactive terminals.");
41454
- }
41455
42088
  const allowSubagentBg = bashCfg.subagent_background;
41456
42089
  const subagentForcedForeground = isSubagent && !allowSubagentBg;
42090
+ if (requestedPty && subagentForcedForeground) {
42091
+ throw new Error("pty:true is unavailable in this subagent session because bash.subagent_background is false; run the command without pty.");
42092
+ }
41457
42093
  const blockToCompletion = subagentForcedForeground || backgroundDisabled || requestedWait;
41458
42094
  const effectiveBackground = blockToCompletion ? false : requestedBackground;
41459
42095
  const rawTimeout = coerceOptionalInt(args.timeout, "timeout", 1, Number.MAX_SAFE_INTEGER);
42096
+ const workerUnboundedWait = isSubagent && requestedWait && rawTimeout === undefined;
41460
42097
  const ptyRows = coerceOptionalInt(args.ptyRows, "ptyRows", 1, 60);
41461
42098
  const ptyCols = coerceOptionalInt(args.ptyCols, "ptyCols", 1, 140);
41462
42099
  const compressed = coerceBoolean(args.compressed, true);
@@ -41487,7 +42124,7 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
41487
42124
  wait: requestedWait,
41488
42125
  sandbox: args.sandbox
41489
42126
  }, callBashBridge, {
41490
- transportTimeoutMs: orchestratedTransportTimeoutMs(blockToCompletion, requestedWait, rawTimeout, foregroundWaitMs),
42127
+ transportTimeoutMs: orchestratedTransportTimeoutMs(blockToCompletion, requestedWait, rawTimeout, foregroundWaitMs, workerUnboundedWait),
41491
42128
  onProgress: ({ text }) => {
41492
42129
  accumulatedOutput = preview(accumulatedOutput + text);
41493
42130
  metadata?.({ output: accumulatedOutput, description });
@@ -41497,10 +42134,10 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
41497
42134
  const fallbackCause = classifyBashHostFallbackError(error);
41498
42135
  if (!bashCfg.host_fallback || fallbackCause === undefined)
41499
42136
  throw error;
41500
- if (rawRequestedBackground) {
42137
+ if (!backgroundDisabled && rawRequestedBackground) {
41501
42138
  throw new Error(`${BASH_HOST_FALLBACK_REFUSAL}; background:true is unsupported.`);
41502
42139
  }
41503
- if (rawRequestedPty) {
42140
+ if (requestedPty) {
41504
42141
  throw new Error(`${BASH_HOST_FALLBACK_REFUSAL}; pty:true is unsupported.`);
41505
42142
  }
41506
42143
  const projectRoot = projectRootFor(context);
@@ -41534,7 +42171,7 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
41534
42171
  trackBgTask(context.sessionID, taskId);
41535
42172
  let rendered = data.output ?? "";
41536
42173
  if (isSubagent && allowSubagentBg) {
41537
- rendered += subagentGuidance(taskId, bashCfg.watch_sync_max_ms);
42174
+ rendered += workerBackgroundTaskNote(taskId);
41538
42175
  }
41539
42176
  const metadataPayload = { description, output: rendered, status: "running", taskId };
41540
42177
  metadata?.(metadataPayload);
@@ -41553,17 +42190,19 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
41553
42190
  };
41554
42191
  }
41555
42192
  function createBashStatusTool(ctx) {
42193
+ const waitSteer = bashCompanionRegistered(ctx.config, "bash_watch") ? " To wait, use bash_watch." : "";
41556
42194
  return {
41557
- description: "Read-only snapshot of a background or PTY bash task's current state and output. 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.",
42195
+ description: `Read-only snapshot of a background or PTY bash task's current state and output. Returns immediately. Never waits. One look to check on a task is fine — never loop it to wait for completion.${waitSteer}`,
41558
42196
  args: {
41559
- taskId: z4.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047a1c39ded."),
41560
- outputMode: z4.enum(["screen", "raw", "both"]).optional().describe("PTY output rendering mode. Defaults to screen for PTY tasks and preserves existing behavior for piped tasks when omitted.")
42197
+ taskId: z3.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047a1c39ded."),
42198
+ outputMode: z3.enum(["screen", "raw", "both"]).optional().describe("PTY output rendering mode. Defaults to screen for PTY tasks and preserves existing behavior for piped tasks when omitted.")
41561
42199
  },
41562
42200
  execute: async (args, context) => {
41563
42201
  const taskId = args.taskId;
41564
42202
  const outputMode = args.outputMode;
41565
42203
  const data = await bashStatusSnapshot(ctx, context, taskId, outputMode);
41566
- return await formatBashStatusText(context, taskId, data, outputMode);
42204
+ const isSubagent = await resolveIsSubagent(ctx.client, context.sessionID, context.directory);
42205
+ return await formatBashStatusText(context, taskId, data, outputMode, isSubagent ? "worker" : "primary");
41567
42206
  }
41568
42207
  };
41569
42208
  }
@@ -41571,7 +42210,7 @@ function createBashKillTool(ctx) {
41571
42210
  return {
41572
42211
  description: "Terminate a running background bash task spawned with bash({ background: true }). Returns confirmation of kill or an error if the task already finished.",
41573
42212
  args: {
41574
- taskId: z4.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047a1c39ded.")
42213
+ taskId: z3.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047a1c39ded.")
41575
42214
  },
41576
42215
  execute: async (args, context) => {
41577
42216
  const data = await callBashBridge(ctx, context, "bash_kill", {
@@ -41594,7 +42233,7 @@ async function bashStatusSnapshot(ctx, runtime, taskId, outputMode, options) {
41594
42233
  }
41595
42234
  return data;
41596
42235
  }
41597
- async function formatBashStatusText(runtime, taskId, data, requestedOutputMode) {
42236
+ async function formatBashStatusText(runtime, taskId, data, requestedOutputMode, role) {
41598
42237
  const status = data.status;
41599
42238
  const exit = typeof data.exit_code === "number" ? ` (exit ${data.exit_code})` : "";
41600
42239
  const dur = typeof data.duration_ms === "number" ? ` ${Math.round(data.duration_ms / 1000)}s` : "";
@@ -41612,7 +42251,7 @@ ${preview}`;
41612
42251
  }
41613
42252
  if (status === "running") {
41614
42253
  text += `
41615
- A completion reminder will be delivered automatically; don't poll.`;
42254
+ ${runningTaskStatusHint(role)}`;
41616
42255
  }
41617
42256
  }
41618
42257
  return text;
@@ -41641,11 +42280,6 @@ PTY task is still running. Use bash_status({ taskId: "${taskId}", outputMode: "s
41641
42280
  function preview(output) {
41642
42281
  return output.length <= METADATA_PREVIEW_LIMIT ? output : output.slice(-METADATA_PREVIEW_LIMIT);
41643
42282
  }
41644
- function subagentGuidance(taskId, watchSyncMaxMs) {
41645
- return `
41646
-
41647
- NOTE (subagent session): Continue with other work if you have it. If you don't, call bash_watch({ taskId: "${taskId}" }) to wait for completion before returning to the parent; without timeoutMs it waits up to ${watchSyncMaxMs} ms, the maximum. Subagents don't survive turn-end and won't receive the completion reminder.`;
41648
- }
41649
42283
  function foregroundMetadata(description, data, rendered) {
41650
42284
  const outputPath = data.output_path;
41651
42285
  const truncated = typeof data.truncated === "boolean" ? data.truncated : data.output_truncated;
@@ -41666,24 +42300,355 @@ function shortenCommand(command) {
41666
42300
  return collapsed.length <= 80 ? collapsed : `${collapsed.slice(0, 77)}...`;
41667
42301
  }
41668
42302
 
42303
+ // src/tools/conflicts.ts
42304
+ var z4 = tool.schema;
42305
+ function conflictTools(ctx) {
42306
+ return {
42307
+ aft_conflicts: {
42308
+ description: "Show all git merge conflicts across the repository — returns line-numbered conflict regions with context for every conflicted file in a single call. Conflicts are discovered from the git repository's top level. By default it inspects the session's project repository; pass `path` to inspect a different repository or git worktree (e.g. where a rebase/merge is running).",
42309
+ args: {
42310
+ path: z4.string().describe("Optional path inside the git repository or worktree to inspect (absolute or relative to project root). Conflicts are discovered from that repository's top level. Defaults to the session project root.").optional()
42311
+ },
42312
+ execute: async (args, context) => {
42313
+ const rawArgs = {};
42314
+ if (!isEmptyParam(args?.path)) {
42315
+ const expanded = expandTilde2(String(args.path));
42316
+ const projectRoot = await resolveProjectRoot(ctx, context);
42317
+ const resolved = resolvePathFromProjectRoot(projectRoot, expanded);
42318
+ const denied = await assertExternalDirectoryPermission(ctx, context, resolved, {
42319
+ kind: "directory"
42320
+ });
42321
+ if (denied)
42322
+ return permissionDeniedResponse(denied);
42323
+ rawArgs.path = resolved;
42324
+ }
42325
+ const response = await callToolCall(ctx, context, "conflicts", rawArgs);
42326
+ if (response.success === false) {
42327
+ throw new Error(response.message || "git_conflicts failed");
42328
+ }
42329
+ return response.text;
42330
+ }
42331
+ }
42332
+ };
42333
+ }
42334
+
42335
+ // src/tools/definitions/v2.ts
42336
+ import { Effect as Effect3 } from "effect";
42337
+ var V2_TOOL_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
42338
+ var V2_PATH_HEADER_TOOLS = new Set(["read", "write", "edit"]);
42339
+ var V2_BASH_TOOLS = new Set(["bash"]);
42340
+ var V2_SHELL_PERMISSION = "shell";
42341
+ var ROOT_COMBINATORS = ["anyOf", "oneOf", "allOf"];
42342
+ function failure(error) {
42343
+ if (error instanceof Error)
42344
+ return error;
42345
+ const detail = (() => {
42346
+ try {
42347
+ return JSON.stringify(error) ?? String(error);
42348
+ } catch {
42349
+ return String(error);
42350
+ }
42351
+ })();
42352
+ return new Error(`V2 tool execution rejected with a non-Error value: ${detail}`);
42353
+ }
42354
+ function isPlainObject3(value) {
42355
+ if (typeof value !== "object" || value === null)
42356
+ return false;
42357
+ const prototype = Object.getPrototypeOf(value);
42358
+ return prototype === Object.prototype || prototype === null;
42359
+ }
42360
+ function jsonSafeMetadata(value) {
42361
+ if (Array.isArray(value)) {
42362
+ return value.map((entry) => entry === undefined ? null : jsonSafeMetadata(entry));
42363
+ }
42364
+ if (!isPlainObject3(value))
42365
+ return value;
42366
+ const cleaned = {};
42367
+ for (const [key, entry] of Object.entries(value)) {
42368
+ if (entry === undefined)
42369
+ continue;
42370
+ cleaned[key] = jsonSafeMetadata(entry);
42371
+ }
42372
+ return cleaned;
42373
+ }
42374
+ function resultContent(result) {
42375
+ if (typeof result === "string")
42376
+ return { content: result };
42377
+ const attachments = (result.attachments ?? []).map((attachment) => ({
42378
+ type: "file",
42379
+ uri: attachment.url,
42380
+ mime: attachment.mime,
42381
+ ...attachment.filename ? { name: attachment.filename } : {}
42382
+ }));
42383
+ const content = attachments.length ? [{ type: "text", text: result.output }, ...attachments] : result.output;
42384
+ const metadata = jsonSafeMetadata({
42385
+ ...result.metadata ?? {},
42386
+ ...result.title ? { title: result.title } : {}
42387
+ });
42388
+ return {
42389
+ content,
42390
+ ...Object.keys(metadata).length ? { metadata } : {}
42391
+ };
42392
+ }
42393
+ function failureEnvelopeMessage(result) {
42394
+ const output = typeof result === "string" ? result : result.output;
42395
+ if (typeof output !== "string")
42396
+ return;
42397
+ const trimmed = output.trim();
42398
+ if (!trimmed.startsWith("{") || !trimmed.includes('"success"'))
42399
+ return;
42400
+ let parsed;
42401
+ try {
42402
+ parsed = JSON.parse(trimmed);
42403
+ } catch {
42404
+ return;
42405
+ }
42406
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
42407
+ return;
42408
+ const envelope = parsed;
42409
+ if (envelope.success !== false)
42410
+ return;
42411
+ for (const field of [envelope.message, envelope.error, envelope.code]) {
42412
+ if (typeof field === "string" && field.length > 0)
42413
+ return field;
42414
+ }
42415
+ return "The tool call failed.";
42416
+ }
42417
+ function bareToolName3(name) {
42418
+ return name.startsWith("aft_") ? name.slice(4) : name;
42419
+ }
42420
+ function projectArguments(name, definition) {
42421
+ if (!V2_PATH_HEADER_TOOLS.has(bareToolName3(name)) || !("filePath" in definition.args)) {
42422
+ return { ...definition.args };
42423
+ }
42424
+ const { filePath, ...rest } = definition.args;
42425
+ return { path: filePath, ...rest };
42426
+ }
42427
+ function executionArguments(name, input) {
42428
+ if (!V2_PATH_HEADER_TOOLS.has(bareToolName3(name)) || !("path" in input))
42429
+ return input;
42430
+ const { path, ...rest } = input;
42431
+ return { filePath: path, ...rest };
42432
+ }
42433
+ function hostPermission(name) {
42434
+ const bare = bareToolName3(name);
42435
+ if (new Set(["read", "glob", "grep"]).has(bare))
42436
+ return bare;
42437
+ if (V2_BASH_TOOLS.has(name))
42438
+ return V2_SHELL_PERMISSION;
42439
+ if (new Set([
42440
+ "write",
42441
+ "edit",
42442
+ "apply_patch",
42443
+ "delete",
42444
+ "move",
42445
+ "ast_grep_replace",
42446
+ "import",
42447
+ "safety"
42448
+ ]).has(bare)) {
42449
+ return "edit";
42450
+ }
42451
+ return;
42452
+ }
42453
+ function assertV2Contract(name, input) {
42454
+ if (!V2_TOOL_NAME.test(name)) {
42455
+ throw new Error(`Invalid V2 tool name ${JSON.stringify(name)}; expected ${V2_TOOL_NAME.source}`);
42456
+ }
42457
+ const schema = tool.schema.toJSONSchema(input, { io: "input" });
42458
+ if (schema.type !== "object") {
42459
+ throw new Error(`V2 tool ${name} must have an object-rooted input schema`);
42460
+ }
42461
+ for (const keyword of ROOT_COMBINATORS) {
42462
+ if (keyword in schema) {
42463
+ throw new Error(`V2 tool ${name} input schema cannot use root ${keyword}`);
42464
+ }
42465
+ }
42466
+ }
42467
+ function v2PermissionRequest(name, request) {
42468
+ if (!V2_BASH_TOOLS.has(name) || request.permission !== "bash")
42469
+ return request;
42470
+ return { ...request, permission: V2_SHELL_PERMISSION };
42471
+ }
42472
+ function runtimeFor(name, location, context, signal, consumers) {
42473
+ const directory = location.directory;
42474
+ const worktree = location.project?.canonical ?? location.project?.directory ?? directory;
42475
+ return {
42476
+ sessionID: context.sessionID,
42477
+ messageID: context.messageID,
42478
+ agent: context.agent,
42479
+ directory,
42480
+ directoryIsSessionRoot: true,
42481
+ worktree,
42482
+ abort: signal,
42483
+ effectAbort: signal,
42484
+ metadata: (update) => {
42485
+ Effect3.runPromise(context.progress(jsonSafeMetadata({
42486
+ ...update.metadata ?? {},
42487
+ ...update.title ? { title: update.title } : {}
42488
+ })));
42489
+ },
42490
+ ask: (asked) => {
42491
+ const request = v2PermissionRequest(name, asked);
42492
+ if (consumers.requestPermission)
42493
+ return consumers.requestPermission(request, context);
42494
+ return Promise.reject(new Error(`The "${request.permission}" operation was refused because this AFT runtime has no permission evaluator bound, so the host's permission rules for it could not be consulted.`));
42495
+ },
42496
+ progress: context.progress
42497
+ };
42498
+ }
42499
+ function projectV2Tool(name, definition, location, consumers = {}) {
42500
+ const input = tool.schema.object(projectArguments(name, definition));
42501
+ assertV2Contract(name, input);
42502
+ const sharedOptions = definition.options;
42503
+ const permission = hostPermission(name);
42504
+ return {
42505
+ name,
42506
+ description: definition.description,
42507
+ input,
42508
+ options: {
42509
+ ...sharedOptions,
42510
+ ...permission ? { permission } : {},
42511
+ codemode: false
42512
+ },
42513
+ execute: (rawInput, context) => Effect3.tryPromise({
42514
+ try: async (signal) => {
42515
+ const runtime = runtimeFor(name, location, context, signal, consumers);
42516
+ const input = executionArguments(name, rawInput);
42517
+ const result = consumers.executeBash && V2_BASH_TOOLS.has(name) ? await consumers.executeBash({
42518
+ name,
42519
+ input,
42520
+ context: runtime,
42521
+ definition
42522
+ }) : await definition.execute(input, runtime);
42523
+ const refusal = failureEnvelopeMessage(result);
42524
+ if (refusal)
42525
+ throw new Error(refusal);
42526
+ return resultContent(result);
42527
+ },
42528
+ catch: failure
42529
+ })
42530
+ };
42531
+ }
42532
+
42533
+ // src/tools/hoisted.ts
42534
+ import { stat as stat2 } from "node:fs/promises";
42535
+ import * as path4 from "node:path";
42536
+
42537
+ // src/hashline-tag-sources.ts
42538
+ function hashlineTagSourceSentence(bashRewritesMintTags, isRegistered = () => true) {
42539
+ const rewrites = bashRewritesMintTags ? " (and accepted AFT `cat`/`head`/`tail` rewrites)" : "";
42540
+ const lookAlikes = [
42541
+ ...["aft_zoom", "aft_outline"].filter(isRegistered),
42542
+ "grep",
42543
+ ...isRegistered("aft_search") ? ["aft_search"] : []
42544
+ ].map((name) => `\`${name}\``);
42545
+ lookAlikes.push("conflict snippets");
42546
+ return `Only \`read\`${rewrites} mint hashline tags. ${joinList(lookAlikes)} do not. After navigation, call \`read\` on every file and range the patch addresses.`;
42547
+ }
42548
+ function joinList(items) {
42549
+ if (items.length <= 2)
42550
+ return items.join(" and ");
42551
+ return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
42552
+ }
42553
+
42554
+ // src/shared/read-vision.ts
42555
+ var VISION_HOST_TIMEOUT_MS = 1500;
42556
+ var states = new WeakMap;
42557
+ function stateFor2(client) {
42558
+ let state = states.get(client);
42559
+ if (!state) {
42560
+ state = { models: new Map, history: new Map, capabilities: new Map };
42561
+ states.set(client, state);
42562
+ }
42563
+ return state;
42564
+ }
42565
+ function rememberReadModel(client, sessionID, model) {
42566
+ stateFor2(client).models.set(sessionID, model);
42567
+ }
42568
+ async function bounded(call) {
42569
+ let timer;
42570
+ try {
42571
+ return await Promise.race([
42572
+ Promise.resolve().then(call),
42573
+ new Promise((resolve) => {
42574
+ timer = setTimeout(() => resolve(undefined), VISION_HOST_TIMEOUT_MS);
42575
+ })
42576
+ ]);
42577
+ } catch {
42578
+ return;
42579
+ } finally {
42580
+ clearTimeout(timer);
42581
+ }
42582
+ }
42583
+ function record3(value) {
42584
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
42585
+ }
42586
+ async function currentSessionVisionCapability(client, sessionID) {
42587
+ if (!sessionID)
42588
+ return;
42589
+ const state = stateFor2(client);
42590
+ let model = state.models.get(sessionID);
42591
+ if (!model) {
42592
+ let pending = state.history.get(sessionID);
42593
+ if (!pending) {
42594
+ pending = bounded(() => resolvePromptContext(client, sessionID)).then((context) => context?.model);
42595
+ state.history.set(sessionID, pending);
42596
+ pending.finally(() => state.history.delete(sessionID));
42597
+ }
42598
+ const historical = await pending;
42599
+ model = state.models.get(sessionID) ?? historical;
42600
+ }
42601
+ if (!model)
42602
+ return;
42603
+ const currentModel = model;
42604
+ const key = JSON.stringify([model.providerID, model.modelID]);
42605
+ let capability = state.capabilities.get(key);
42606
+ if (!capability) {
42607
+ capability = (async () => {
42608
+ const api = client.provider;
42609
+ if (!api?.list)
42610
+ return;
42611
+ const list = api.list.bind(api);
42612
+ const listed = record3(await bounded(list));
42613
+ const catalog = record3(listed?.data) ?? listed;
42614
+ const providers = catalog?.all ?? catalog?.providers;
42615
+ if (!Array.isArray(providers))
42616
+ return;
42617
+ const provider = providers.map(record3).find((entry) => entry?.id === currentModel.providerID);
42618
+ const models = provider?.models;
42619
+ const entry = record3(Array.isArray(models) ? models.find((entry) => record3(entry)?.id === currentModel.modelID) : record3(models)?.[currentModel.modelID]);
42620
+ const inputs = record3(entry?.modalities)?.input;
42621
+ return Array.isArray(inputs) ? inputs.includes("image") : typeof entry?.attachment === "boolean" ? entry.attachment : undefined;
42622
+ })();
42623
+ state.capabilities.set(key, capability);
42624
+ capability.then((value) => {
42625
+ if (value === undefined)
42626
+ state.capabilities.delete(key);
42627
+ });
42628
+ }
42629
+ return capability;
42630
+ }
42631
+
41669
42632
  // src/tools/bash_watch.ts
41670
42633
  var z5 = tool.schema;
41671
- var BASH_WAIT_POLL_INTERVAL_MS = 100;
41672
42634
  var REGEX_WAIT_SCAN_WINDOW_BYTES = 64 * 1024;
41673
42635
  function unavailableSnapshot() {
41674
42636
  return { status: "unknown" };
41675
42637
  }
41676
- function coerceConfiguredWatchTimeout(value, cap) {
42638
+ function coerceConfiguredWatchTimeout(value, role, cap) {
41677
42639
  try {
41678
- return coerceOptionalInt(value, "timeoutMs", 1, cap);
42640
+ return coerceOptionalInt(value, "timeoutMs", 1, maxWatchTimeoutMs(role, cap));
41679
42641
  } catch (error) {
41680
42642
  const message = error instanceof Error ? error.message : String(error);
42643
+ if (role === "worker")
42644
+ throw error;
41681
42645
  throw new Error(`${message} (bash.watch_sync_max_ms)`);
41682
42646
  }
41683
42647
  }
41684
42648
  function createBashWatchTool(ctx) {
42649
+ const noPolling = bashCompanionRegistered(ctx.config, "bash_status") ? " Never loop bash_status to wait." : "";
41685
42650
  return {
41686
- 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.",
42651
+ 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.${noPolling}`,
41687
42652
  args: {
41688
42653
  taskId: z5.string().describe("Background task ID returned by bash({ background: true })."),
41689
42654
  pattern: z5.union([z5.string(), z5.object({ regex: z5.string() })]).optional().describe("Substring or regex pattern. Optional in sync mode; required with background:true. Sync substring watches keep only the overlap tail needed for boundary matches; sync regex watches use a 64 KB rolling output window."),
@@ -41734,11 +42699,11 @@ A notification will fire when the pattern matches or the task exits.`;
41734
42699
  }
41735
42700
  const syncWaitCap = bashCfg.watch_sync_max_ms;
41736
42701
  const role = isSubagent ? "worker" : "primary";
41737
- const effectiveWaitMs = subagentForcedSync ? syncWaitCap : resolveWatchTimeoutMs(coerceConfiguredWatchTimeout(args.timeoutMs, syncWaitCap), role, syncWaitCap);
42702
+ const effectiveWaitMs = subagentForcedSync ? undefined : resolveWatchTimeoutMs(coerceConfiguredWatchTimeout(args.timeoutMs, role, syncWaitCap), role, syncWaitCap);
41738
42703
  const data = await waitForBashStatus(ctx, context, taskId, undefined, waitFor, effectiveWaitMs);
41739
42704
  const waited = data.waited;
41740
42705
  if (waited?.reason === "user_message") {
41741
- const convertedText = await convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, coerceBoolean(args.once, true));
42706
+ const convertedText = await convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, coerceBoolean(args.once, true), waited.elapsed_ms, role);
41742
42707
  const metadata = context.metadata;
41743
42708
  metadata?.({ taskId, status: data.status, waited, convertedToAsync: true });
41744
42709
  return convertedText;
@@ -41750,9 +42715,11 @@ A notification will fire when the pattern matches or the task exits.`;
41750
42715
  }
41751
42716
  };
41752
42717
  }
41753
- async function convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, once) {
42718
+ async function convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, once, elapsedMs, role) {
42719
+ const interrupted = `Sync watch for task ${taskId} was interrupted because you sent a message after ${elapsedMs}ms of waiting. `;
42720
+ const reminderFallback = interruptedWatchTail(role, `The task is still running in the background. A completion reminder will be ` + `delivered automatically when the task exits.`);
41754
42721
  if (!waitFor) {
41755
- 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.`;
42722
+ 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.`);
41756
42723
  }
41757
42724
  const notifyParams = {
41758
42725
  task_id: taskId,
@@ -41767,12 +42734,12 @@ async function convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, once) {
41767
42734
  const registered = await callBashBridge(ctx, context, "bash_notify", notifyParams);
41768
42735
  if (registered.success === false) {
41769
42736
  unmarkExplicitControl(context.sessionID, taskId);
41770
- 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.`;
42737
+ return interrupted + `Auto-registering an async watch failed (${String(registered.message ?? "unknown error")}). ` + reminderFallback;
41771
42738
  }
41772
- 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.`;
42739
+ 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.`);
41773
42740
  } catch (err) {
41774
42741
  unmarkExplicitControl(context.sessionID, taskId);
41775
- 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.`;
42742
+ return interrupted + `Auto-registering an async watch failed (${err instanceof Error ? err.message : String(err)}). ` + reminderFallback;
41776
42743
  }
41777
42744
  }
41778
42745
  function formatWatchResultText(taskId, data, waited, role, syncWaitCap) {
@@ -41781,21 +42748,25 @@ function formatWatchResultText(taskId, data, waited, role, syncWaitCap) {
41781
42748
  const dur = typeof data.duration_ms === "number" ? ` ${Math.round(data.duration_ms / 1000)}s` : "";
41782
42749
  let text = `Task ${taskId}: ${status}${exit}${dur}`;
41783
42750
  if (waited) {
42751
+ const waitedText = formatWatchWaited(waited.elapsed_ms, waited.limit_ms, role === "primary" ? syncWaitCap : undefined);
41784
42752
  if (waited.reason === "matched") {
41785
42753
  const stream = waited.match_stream ? ` in ${waited.match_stream}` : "";
41786
42754
  text += `
41787
- Waited ${waited.elapsed_ms}ms; matched ${JSON.stringify(waited.match ?? "")}${stream} at offset ${waited.match_offset ?? 0}.`;
42755
+ ${waitedText}; matched ${JSON.stringify(waited.match ?? "")}${stream} at offset ${waited.match_offset ?? 0}.`;
41788
42756
  } else if (waited.reason === "timeout") {
41789
42757
  text += `
41790
- Waited ${waited.elapsed_ms}ms; timeout reached without match. ${watchTimeoutSteer(role, syncWaitCap)}`;
42758
+ ${waitedText}; timeout reached without match. ${watchTimeoutSteer(role)}`;
41791
42759
  } else if (waited.reason === "unavailable") {
41792
42760
  text += `
41793
- 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.`;
42761
+ ${waitedText}; ${watchUnavailableSteer(role)}`;
42762
+ } else if (waited.reason === "aborted") {
42763
+ text += `
42764
+ ${waitedText}; the watch was cancelled. The task keeps running.`;
41794
42765
  } else {
41795
42766
  const stat = String(data.status ?? "unknown");
41796
42767
  const e = typeof data.exit_code === "number" ? `, exit ${data.exit_code}` : "";
41797
42768
  text += `
41798
- Waited ${waited.elapsed_ms}ms; task exited (${stat}${e}).`;
42769
+ ${waitedText}; task exited (${stat}${e}).`;
41799
42770
  }
41800
42771
  }
41801
42772
  const preview = data.output_preview;
@@ -41817,8 +42788,17 @@ async function bashStatusSnapshot2(ctx, runtime, taskId, outputMode, cursor, opt
41817
42788
  return data;
41818
42789
  }
41819
42790
  async function waitForBashStatus(ctx, runtime, taskId, outputMode, waitFor, effectiveWaitMs) {
41820
- const startedAt = Date.now();
41821
- const deadline = startedAt + effectiveWaitMs;
42791
+ const startedAt = watchClock.now();
42792
+ const deadline = effectiveWaitMs === undefined ? Number.POSITIVE_INFINITY : startedAt + effectiveWaitMs;
42793
+ const elapsedMs = () => Math.round(watchClock.now() - startedAt);
42794
+ const abortSignal = runtime.abort;
42795
+ const pause = () => watchClock.sleep(Math.min(watchPollDelayMs(elapsedMs()), Math.max(0, deadline - watchClock.now())), abortSignal);
42796
+ const waited = (reason, extra = {}) => ({
42797
+ reason,
42798
+ elapsed_ms: elapsedMs(),
42799
+ ...effectiveWaitMs === undefined ? {} : { limit_ms: effectiveWaitMs },
42800
+ ...extra
42801
+ });
41822
42802
  let spillCursor = { output: 0, stderr: 0 };
41823
42803
  const scanState = {
41824
42804
  output: { text: "", baseOffset: 0 },
@@ -41832,29 +42812,29 @@ async function waitForBashStatus(ctx, runtime, taskId, outputMode, waitFor, effe
41832
42812
  markTaskWaiting(runtime.sessionID, taskId);
41833
42813
  let sawTerminal = false;
41834
42814
  let lastData;
42815
+ let busySince;
41835
42816
  try {
41836
42817
  while (true) {
42818
+ if (abortSignal?.aborted) {
42819
+ return withWaited(lastData ?? unavailableSnapshot(), waited("aborted"));
42820
+ }
41837
42821
  let data;
41838
42822
  try {
41839
42823
  data = await bashStatusSnapshot2(ctx, runtime, taskId, outputMode, waitFor ? spillCursor : undefined, bridgeOptions);
41840
42824
  } catch (err) {
41841
42825
  if (!isBridgeTransportTimeout(err))
41842
42826
  throw err;
42827
+ busySince ??= watchClock.now();
41843
42828
  if (isSyncWatchAborted(runtime.sessionID)) {
41844
- return withWaited(lastData ?? unavailableSnapshot(), {
41845
- reason: "user_message",
41846
- elapsed_ms: Date.now() - startedAt
41847
- });
42829
+ return withWaited(lastData ?? unavailableSnapshot(), waited("user_message"));
41848
42830
  }
41849
- if (Date.now() >= deadline) {
41850
- return withWaited(lastData ?? unavailableSnapshot(), {
41851
- reason: "unavailable",
41852
- elapsed_ms: Date.now() - startedAt
41853
- });
42831
+ if (watchClock.now() >= deadline || watchClock.now() - busySince >= WATCH_UNAVAILABLE_GIVE_UP_MS) {
42832
+ return withWaited(lastData ?? unavailableSnapshot(), waited("unavailable"));
41854
42833
  }
41855
- await sleep(Math.min(BASH_WAIT_POLL_INTERVAL_MS, Math.max(0, deadline - Date.now())));
42834
+ await pause();
41856
42835
  continue;
41857
42836
  }
42837
+ busySince = undefined;
41858
42838
  lastData = data;
41859
42839
  const terminal = isTerminalStatus(data.status);
41860
42840
  if (waitFor) {
@@ -41879,13 +42859,11 @@ async function waitForBashStatus(ctx, runtime, taskId, outputMode, waitFor, effe
41879
42859
  await markBgCompletionDelivered({ ctx, directory: projectRootFor(runtime), sessionID: runtime.sessionID }, taskId);
41880
42860
  }
41881
42861
  const matchStream = data.mode === "pty" ? undefined : chunk.stream === "output" ? "stdout" : "stderr";
41882
- return withWaited(data, {
41883
- reason: "matched",
41884
- elapsed_ms: Date.now() - startedAt,
42862
+ return withWaited(data, waited("matched", {
41885
42863
  match: match.text,
41886
42864
  match_offset: state.baseOffset + match.byteOffset,
41887
42865
  match_stream: matchStream
41888
- });
42866
+ }));
41889
42867
  }
41890
42868
  if (waitFor.kind === "substring") {
41891
42869
  const trimmed = trimWaitScanBuffer(state.text, state.baseOffset, waitFor);
@@ -41899,15 +42877,15 @@ async function waitForBashStatus(ctx, runtime, taskId, outputMode, waitFor, effe
41899
42877
  sawTerminal = true;
41900
42878
  consumeBgCompletion(runtime.sessionID, taskId);
41901
42879
  await markBgCompletionDelivered({ ctx, directory: projectRootFor(runtime), sessionID: runtime.sessionID }, taskId);
41902
- return withWaited(data, { reason: "exited", elapsed_ms: Date.now() - startedAt });
42880
+ return withWaited(data, waited("exited"));
41903
42881
  }
41904
42882
  if (isSyncWatchAborted(runtime.sessionID)) {
41905
- return withWaited(data, { reason: "user_message", elapsed_ms: Date.now() - startedAt });
42883
+ return withWaited(data, waited("user_message"));
41906
42884
  }
41907
- if (Date.now() >= deadline) {
41908
- return withWaited(data, { reason: "timeout", elapsed_ms: Date.now() - startedAt });
42885
+ if (watchClock.now() >= deadline) {
42886
+ return withWaited(data, waited("timeout"));
41909
42887
  }
41910
- await sleep(Math.min(BASH_WAIT_POLL_INTERVAL_MS, Math.max(0, deadline - Date.now())));
42888
+ await pause();
41911
42889
  }
41912
42890
  } finally {
41913
42891
  if (!sawTerminal)
@@ -42017,8 +42995,9 @@ function withWaited(data, waited) {
42017
42995
  // src/tools/bash_write.ts
42018
42996
  var z6 = tool.schema;
42019
42997
  function createBashWriteTool(ctx) {
42998
+ const modeCheck = bashCompanionRegistered(ctx.config, "bash_status") ? '; check bash_status reports mode: "pty" first.' : ".";
42020
42999
  return {
42021
- 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).",
43000
+ description: `Write input bytes to a running PTY bash task. PTY-only${modeCheck} ` + '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).",
42022
43001
  args: {
42023
43002
  taskId: z6.string().describe("Background PTY task ID returned by bash({ pty: true, background: true })."),
42024
43003
  input: z6.union([
@@ -42055,47 +43034,6 @@ var ISSUE_AND_PR_READ_DESCRIPTION = "GitHub issues and pull requests can be read
42055
43034
  function whenGhReadEnabled(enabled, description) {
42056
43035
  return enabled ? description : "";
42057
43036
  }
42058
- function asRecord(value) {
42059
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
42060
- }
42061
- function visionCapabilityForOpenCodeModel(model) {
42062
- const entry = model;
42063
- if (Array.isArray(entry?.modalities?.input)) {
42064
- return entry.modalities.input.includes("image");
42065
- }
42066
- return typeof entry?.attachment === "boolean" ? entry.attachment : undefined;
42067
- }
42068
- function modelFromOpenCodeProvider(provider, modelID) {
42069
- const models = provider.models;
42070
- if (Array.isArray(models)) {
42071
- return models.find((model) => asRecord(model)?.id === modelID);
42072
- }
42073
- return asRecord(models)?.[modelID];
42074
- }
42075
- async function currentSessionVisionCapability(client, sessionID) {
42076
- if (!sessionID)
42077
- return;
42078
- const promptContext = await resolvePromptContext(client, sessionID);
42079
- const currentModel = promptContext?.model;
42080
- if (!currentModel)
42081
- return;
42082
- const providerApi = client.provider;
42083
- if (typeof providerApi?.list !== "function")
42084
- return;
42085
- let listed;
42086
- try {
42087
- listed = await providerApi.list();
42088
- } catch {
42089
- return;
42090
- }
42091
- const result = asRecord(listed);
42092
- const catalog = asRecord(result?.data) ?? result;
42093
- const providers = Array.isArray(catalog?.all) ? catalog.all : Array.isArray(catalog?.providers) ? catalog.providers : [];
42094
- const provider = providers.map(asRecord).find((entry) => entry?.id === currentModel.providerID);
42095
- if (!provider)
42096
- return;
42097
- return visionCapabilityForOpenCodeModel(modelFromOpenCodeProvider(provider, currentModel.modelID));
42098
- }
42099
43037
  function persistFilePathAlias(args, context) {
42100
43038
  if (typeof args.path === "string" && !Object.hasOwn(args, "filePath")) {
42101
43039
  args.filePath = args.path;
@@ -42267,7 +43205,7 @@ function inferBeforeStart(ops, from, beforeLen) {
42267
43205
  var z7 = tool.schema;
42268
43206
  var READ_DESCRIPTION = `Read file contents or list directory entries.
42269
43207
 
42270
- Use either startLine/endLine OR offset/limit to read a section of a file.
43208
+ Use either startLine/endLine OR offset/limit to read a section of a file or sorted directory listing.
42271
43209
 
42272
43210
  Behavior:
42273
43211
  - Returns line-numbered content (e.g., "1: const x = 1")
@@ -42275,7 +43213,7 @@ Behavior:
42275
43213
  - Output capped at 50KB
42276
43214
  - Binary files are auto-detected and return a size-only message
42277
43215
  - Supported images (PNG, JPEG, GIF, WebP) and PDFs are returned as tool attachments; range arguments are ignored for media
42278
- - Directories return sorted entries with trailing / for subdirectories
43216
+ - Directories return sorted entries with trailing / for subdirectories; offset is 1-based, limit defaults to and is capped at 1000 entries. Enumeration stops at 10,000 entries; partial listings carry a shown/total trailer.
42279
43217
 
42280
43218
  Examples:
42281
43219
  Read full file: { "path": "src/app.ts" }
@@ -42299,54 +43237,49 @@ function createReadTool(ctx) {
42299
43237
  description: readDescription(resolveGithubConfig(ctx.config).read),
42300
43238
  args: {
42301
43239
  filePath: z7.string().describe("Path to file or directory (absolute or relative to project root)"),
42302
- startLine: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based line to start reading from"),
42303
- endLine: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based line to stop reading at (inclusive)"),
42304
- limit: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("Max lines to return (default: 2000)"),
42305
- offset: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based line number to start reading from (use with limit). Ignored if startLine is provided")
43240
+ startLine: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based line or directory entry to start reading from"),
43241
+ endLine: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based line or directory entry to stop reading at (inclusive)"),
43242
+ limit: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("Max lines (default: 2000) or directory entries (default and cap: 1000) to return"),
43243
+ offset: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based line or directory entry to start reading from (use with limit). Ignored if startLine is provided")
42306
43244
  },
42307
43245
  execute: async (args, context) => {
42308
43246
  const file = args.path;
42309
- const projectRoot = await resolveProjectRoot(ctx, context);
43247
+ const projectRoot = await measurePreStage("directory", () => resolveProjectRoot(ctx, context));
42310
43248
  const filePath = resolvePathFromProjectRoot(projectRoot, file);
42311
43249
  persistFilePathAlias(args, context);
42312
43250
  {
42313
- const denial = await assertExternalDirectoryPermission(ctx, context, filePath, {
43251
+ const denial = await measurePreStage("permission", () => assertExternalDirectoryPermission(ctx, context, filePath, {
42314
43252
  serverValidatedRead: true
42315
- });
43253
+ }));
42316
43254
  if (denial)
42317
43255
  return permissionDeniedResponse(denial);
42318
43256
  }
42319
43257
  try {
42320
- await runAsk(context.ask({
43258
+ await measurePreStage("permission", () => runAsk(context.ask({
42321
43259
  permission: "read",
42322
43260
  patterns: [permissionPath(context, filePath)],
42323
43261
  always: ["*"],
42324
43262
  metadata: {}
42325
- }));
43263
+ })));
42326
43264
  } catch (error) {
42327
43265
  const failure = classifyPermissionError(error);
42328
43266
  return permissionDeniedResponse(failure.kind === "rule_denied" ? permissionRuleDenial("read") : failure.message);
42329
43267
  }
42330
- const rawStartLine = coerceOptionalInt(args.startLine, "startLine", 1, Number.MAX_SAFE_INTEGER);
42331
- const rawEndLine = coerceOptionalInt(args.endLine, "endLine", 1, Number.MAX_SAFE_INTEGER);
42332
- const rawLimit = coerceOptionalInt(args.limit, "limit", 1, Number.MAX_SAFE_INTEGER);
42333
- const rawOffset = coerceOptionalInt(args.offset, "offset", 1, Number.MAX_SAFE_INTEGER);
42334
- let startLine = rawStartLine;
42335
- let endLine = rawEndLine;
42336
- if (startLine === undefined && rawOffset !== undefined) {
42337
- startLine = rawOffset;
42338
- if (rawLimit !== undefined) {
42339
- endLine = rawOffset + rawLimit - 1;
42340
- }
42341
- }
43268
+ const startLine = coerceOptionalInt(args.startLine, "startLine", 1, Number.MAX_SAFE_INTEGER);
43269
+ const endLine = coerceOptionalInt(args.endLine, "endLine", 1, Number.MAX_SAFE_INTEGER);
43270
+ const limit = coerceOptionalInt(args.limit, "limit", 1, Number.MAX_SAFE_INTEGER);
43271
+ const offset = coerceOptionalInt(args.offset, "offset", 1, Number.MAX_SAFE_INTEGER);
42342
43272
  const rawArgs = { filePath: file };
42343
43273
  if (startLine !== undefined)
42344
43274
  rawArgs.startLine = startLine;
42345
43275
  if (endLine !== undefined)
42346
43276
  rawArgs.endLine = endLine;
42347
- if (rawLimit !== undefined && rawOffset === undefined)
42348
- rawArgs.limit = rawLimit;
42349
- const visionCapability = await currentSessionVisionCapability(ctx.client, context.sessionID);
43277
+ if (offset !== undefined)
43278
+ rawArgs.offset = offset;
43279
+ if (limit !== undefined)
43280
+ rawArgs.limit = limit;
43281
+ const mediaCandidate = isGithubResourcePath(file) || /\.(png|jpe?g|gif|webp|pdf)$/i.test(filePath) && await stat2(filePath).then((info) => info.isFile(), () => false);
43282
+ const visionCapability = mediaCandidate ? await measurePreStage("capability", () => currentSessionVisionCapability(ctx.client, context.sessionID)) : undefined;
42350
43283
  if (visionCapability !== undefined)
42351
43284
  rawArgs.vision_capability = visionCapability;
42352
43285
  const response = await callToolCall(ctx, context, "read", rawArgs);
@@ -42400,7 +43333,7 @@ function createWriteTool(ctx, editToolName = "edit") {
42400
43333
  description: getWriteDescription(ctx, editToolName),
42401
43334
  args: {
42402
43335
  filePath: z7.string().describe("Path to the file to write (absolute or relative to project root)"),
42403
- content: z7.string().describe("The full content to write to the file")
43336
+ content: z7.string().describe("The full content to write to the file; for issue://N / pr://N, the comment body to post")
42404
43337
  },
42405
43338
  execute: async (args, context) => {
42406
43339
  const argsRecord = args;
@@ -42517,7 +43450,7 @@ ${backupBehavior}
42517
43450
  - Symbol replace includes decorators, attributes, and doc comments in range
42518
43451
  - Response is a compact server-rendered summary; before/after diff details are attached as UI metadata when available.`;
42519
43452
  }
42520
- var HASHLINE_EDIT_DESCRIPTION = [
43453
+ var HASHLINE_EDIT_GRAMMAR = [
42521
43454
  "Apply a hashline patch. Arguments are exactly `{patch}` where `patch` is a non-empty string. Server-owned preview control is outside this schema.",
42522
43455
  "",
42523
43456
  "Quick reference:",
@@ -42525,13 +43458,18 @@ var HASHLINE_EDIT_DESCRIPTION = [
42525
43458
  "- Same canonical path: multiple sections compose in patch order against pre-request coordinates.",
42526
43459
  "- 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.",
42527
43460
  "- 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 `-`.",
42528
- "- 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.",
42529
- "- 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."
43461
+ "- 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."
42530
43462
  ].join(`
42531
43463
  `);
43464
+ function hashlineEditDescription(config) {
43465
+ const bashRewrites = toolEnabled(config, "bash") && resolveBashConfig(config).rewrite;
43466
+ const tagSources = hashlineTagSourceSentence(bashRewrites, (name) => toolEnabled(config, name));
43467
+ return `${HASHLINE_EDIT_GRAMMAR}
43468
+ - ${tagSources}`;
43469
+ }
42532
43470
  function createHashlineEditTool(ctx) {
42533
43471
  return {
42534
- description: HASHLINE_EDIT_DESCRIPTION,
43472
+ description: hashlineEditDescription(ctx.config),
42535
43473
  args: {
42536
43474
  patch: z7.string().min(1).describe("Hashline patch text with one or more [path#TAG] sections and PUT/CUT/REM/MV operations")
42537
43475
  },
@@ -42606,8 +43544,7 @@ function createEditTool(ctx, writeToolName = "write") {
42606
43544
  throw new Error("'path' parameter is required");
42607
43545
  if (isGithubResourcePath(file)) {
42608
43546
  const edits = argsRecord.edits;
42609
- const onlyFindReplace = Array.isArray(edits) && edits.length > 0 && argsRecord.appendContent === undefined && argsRecord.symbol === undefined && argsRecord.content === undefined && edits.every((entry) => entry !== null && typeof entry === "object" && typeof entry.oldString === "string" && (entry.newString === undefined || typeof entry.newString === "string") && entry.startLine === undefined && entry.endLine === undefined && entry.content === undefined);
42610
- if (!onlyFindReplace) {
43547
+ if (!isFindReplaceOnlyEdit(argsRecord)) {
42611
43548
  throw new Error("edit: GitHub resources support only edits[] find/replace entries with oldString and optional newString");
42612
43549
  }
42613
43550
  const rawArgs = { path: file, edits };
@@ -42790,7 +43727,7 @@ function createApplyPatchTool(ctx) {
42790
43727
  };
42791
43728
  }
42792
43729
  function deleteDescription(ctx) {
42793
- const backupText = ctx.config.backup?.enabled === false ? "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 the tree is removed. 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. ";
43730
+ const backupText = ctx.config.backup?.enabled === false ? "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. ";
42794
43731
  return `Delete one or more files (or directories).
42795
43732
 
42796
43733
  ` + backupText + `Directory deletion requires recursive: true. Without it, passing a directory returns an error.
@@ -42894,6 +43831,12 @@ function createMoveTool(ctx) {
42894
43831
  };
42895
43832
  }
42896
43833
  function hoistedTools(ctx) {
43834
+ const backgroundCompanions = resolveBashConfig(ctx.config).background ? {
43835
+ bash_status: createBashStatusTool(ctx),
43836
+ bash_write: createBashWriteTool(ctx),
43837
+ bash_watch: createBashWatchTool(ctx),
43838
+ bash_kill: createBashKillTool(ctx)
43839
+ } : {};
42897
43840
  const tools = {
42898
43841
  read: createReadTool(ctx),
42899
43842
  write: createWriteTool(ctx, "edit"),
@@ -42902,10 +43845,7 @@ function hoistedTools(ctx) {
42902
43845
  aft_delete: createDeleteTool(ctx),
42903
43846
  aft_move: createMoveTool(ctx),
42904
43847
  bash: createBashTool(ctx),
42905
- bash_status: createBashStatusTool(ctx),
42906
- bash_write: createBashWriteTool(ctx),
42907
- bash_watch: createBashWatchTool(ctx),
42908
- bash_kill: createBashKillTool(ctx)
43848
+ ...backgroundCompanions
42909
43849
  };
42910
43850
  return prepareToolMap(tools, { hashlineEffective: ctx.hashlineEffective });
42911
43851
  }
@@ -43006,7 +43946,7 @@ async function resolveAndGateScope(ctx, context, scope) {
43006
43946
  }
43007
43947
  return { scope: Array.isArray(scope) ? resolved : resolved[0] };
43008
43948
  }
43009
- function asRecord2(value) {
43949
+ function asRecord(value) {
43010
43950
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
43011
43951
  }
43012
43952
  function asString(value) {
@@ -43014,7 +43954,7 @@ function asString(value) {
43014
43954
  }
43015
43955
  function firstRecord(...values) {
43016
43956
  for (const value of values) {
43017
- const record = asRecord2(value);
43957
+ const record = asRecord(value);
43018
43958
  if (record)
43019
43959
  return record;
43020
43960
  }
@@ -43049,7 +43989,7 @@ function parseInspectPhaseEntries(value) {
43049
43989
  if (!Array.isArray(value))
43050
43990
  return [];
43051
43991
  return value.flatMap((candidate) => {
43052
- const entry = asRecord2(candidate);
43992
+ const entry = asRecord(candidate);
43053
43993
  const id = asString(entry?.id);
43054
43994
  if (!id)
43055
43995
  return [];
@@ -43065,7 +44005,7 @@ function parseInspectPhaseEntries(value) {
43065
44005
  });
43066
44006
  }
43067
44007
  function parseInspectTerminal(payload) {
43068
- const response = asRecord2(payload);
44008
+ const response = asRecord(payload);
43069
44009
  if (!response)
43070
44010
  return;
43071
44011
  const kind = terminalKind(response);
@@ -43080,7 +44020,7 @@ function parseInspectTerminal(payload) {
43080
44020
  waitStampText: kind === "FRESH" ? firstString(waitStamp?.text, waitStamp?.human_text) : undefined
43081
44021
  };
43082
44022
  }
43083
- const failedPhaseRecord = asRecord2(response.failed_phase);
44023
+ const failedPhaseRecord = asRecord(response.failed_phase);
43084
44024
  const failedPhaseId = firstString(failedPhaseRecord?.id, response.failed_phase);
43085
44025
  const failedPhase = failedPhaseId ? parseInspectPhaseEntries([
43086
44026
  {
@@ -43301,6 +44241,9 @@ function navigationTools(ctx) {
43301
44241
 
43302
44242
  // src/tools/reading.ts
43303
44243
  var z11 = tool.schema;
44244
+ function isGithubPathArg(value) {
44245
+ return typeof value === "string" && (value.startsWith("issue://") || value.startsWith("pr://"));
44246
+ }
43304
44247
  function buildZoomTitle(args) {
43305
44248
  const targets = args.targets;
43306
44249
  if (!isEmptyParam(targets)) {
@@ -43324,6 +44267,13 @@ function buildZoomTitle(args) {
43324
44267
  }
43325
44268
  return path || "(no target)";
43326
44269
  }
44270
+ function outlineFocusSteer(config) {
44271
+ const zoomEnabled = toolEnabled(config, "aft_zoom");
44272
+ const reader = zoomEnabled ? "aft_zoom" : "read";
44273
+ const locate = toolEnabled(config, "aft_search") ? `aft_search + ${reader}` : reader;
44274
+ 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." : "";
44275
+ 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}`;
44276
+ }
43327
44277
  function readingTools(ctx) {
43328
44278
  const zoomEnabled = toolEnabled(ctx.config, "aft_zoom");
43329
44279
  const ghReadEnabled = resolveGithubConfig(ctx.config).read;
@@ -43331,9 +44281,9 @@ function readingTools(ctx) {
43331
44281
  const githubZoomDescription = whenGhReadEnabled(ghReadEnabled, "GitHub issue and pull-request discussion ordinals can be zoomed with `path: issue://…` or `path: pr://…` and `symbols`.");
43332
44282
  const tools = prepareToolMap({
43333
44283
  aft_outline: {
43334
- 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.\n\n" + (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.\n\n" : `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.
44284
+ 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.\n\n" + outlineFocusSteer(ctx.config) + `
43335
44285
 
43336
- `) + "Pass a single `target`:\n" + ` • file path → outline that file (with signatures)
44286
+ ` + "Pass a single `target`:\n" + ` • file path → outline that file (with signatures)
43337
44287
  ` + ` • directory path → outline source files under it
43338
44288
  ` + ` • URL (http:// or https://) → fetch and outline a remote HTML/Markdown document
43339
44289
  ` + " • array of paths → outline multiple files in one call; with files:true, every path must be a directory" + (githubOutlineDescription ? `
@@ -43382,7 +44332,7 @@ ${githubOutlineDescription}` : "") + "\n\nWhen a list is cut, the reply ends wit
43382
44332
  }
43383
44333
  },
43384
44334
  aft_zoom: {
43385
- 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 ? `
44335
+ 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 ? `
43386
44336
 
43387
44337
  ${githubZoomDescription}` : ""),
43388
44338
  args: {
@@ -43398,37 +44348,22 @@ ${githubZoomDescription}` : ""),
43398
44348
  path: z11.string().describe("Path to file (absolute or relative to project root)"),
43399
44349
  symbol: z11.string().describe("Symbol name in that file")
43400
44350
  }))
43401
- ]).optional().describe("Cross-file batch: `{ path, symbol }` or an array of them. Mutually exclusive with path/url/symbols."),
44351
+ ]).optional().describe("Cross-file batch: `{ path, symbol }` or an array of them. May be combined with path/url + symbols; all lookups are answered in one batch."),
43402
44352
  contextLines: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("Lines of context before/after the symbol (default: 3)"),
43403
44353
  callgraph: z11.boolean().optional().describe("Include call-graph annotations (calls-out / called-by within the same file). Default false; off keeps zoom output minimal.")
43404
44354
  },
43405
44355
  execute: async (args, context) => {
43406
- const hasTargetsProvided = (t) => {
43407
- if (isEmptyParam(t))
43408
- return false;
43409
- const entryEmpty = (entry) => {
43410
- if (!entry || typeof entry !== "object")
43411
- return true;
43412
- const fp = entry.path;
43413
- const sym = entry.symbol;
43414
- const fpEmpty = typeof fp !== "string" || fp.length === 0;
43415
- const symEmpty = typeof sym !== "string" || sym.length === 0;
43416
- return fpEmpty && symEmpty;
43417
- };
43418
- if (Array.isArray(t))
43419
- return !t.every(entryEmpty);
43420
- return !entryEmpty(t);
43421
- };
43422
44356
  const targetsInput = coerceJsonCollectionParam(args.targets, "targets");
43423
- const hasFilePath = !isEmptyParam(args.path);
43424
- const hasUrl = !isEmptyParam(args.url);
43425
- const hasTargets = hasTargetsProvided(targetsInput);
43426
- const hasSymbols = !isEmptyParam(args.symbols);
44357
+ const targetEntries = usableZoomTargets(targetsInput);
44358
+ const hasFilePath = !isBlankParam(args.path);
44359
+ const hasUrl = !isBlankParam(args.url);
44360
+ const hasTargets = targetEntries.length > 0;
44361
+ const hasSymbols = !isBlankParam(args.symbols);
43427
44362
  const wantCallgraph = coerceBoolean(args.callgraph);
43428
44363
  const contextLines = coerceOptionalInt(args.contextLines, "contextLines", 1, Number.MAX_SAFE_INTEGER);
43429
44364
  const zoomTitle = buildZoomTitle({
43430
44365
  ...args,
43431
- targets: targetsInput
44366
+ targets: hasTargets ? targetEntries : undefined
43432
44367
  });
43433
44368
  const zoomDisplay = { title: zoomTitle };
43434
44369
  if (hasFilePath)
@@ -43439,7 +44374,7 @@ ${githubZoomDescription}` : ""),
43439
44374
  zoomDisplay.symbols = typeof args.symbols === "string" ? args.symbols : JSON.stringify(args.symbols);
43440
44375
  }
43441
44376
  if (hasTargets)
43442
- zoomDisplay.targets = JSON.stringify(targetsInput);
44377
+ zoomDisplay.targets = JSON.stringify(targetEntries);
43443
44378
  if (contextLines !== undefined)
43444
44379
  zoomDisplay.contextLines = contextLines;
43445
44380
  if (wantCallgraph)
@@ -43450,23 +44385,24 @@ ${githubZoomDescription}` : ""),
43450
44385
  metadata: zoomDisplay
43451
44386
  });
43452
44387
  if (hasTargets) {
43453
- if (hasFilePath || hasUrl || hasSymbols) {
43454
- throw new Error("'targets' is mutually exclusive with 'path', 'url', and 'symbols'");
44388
+ if (hasFilePath && hasUrl) {
44389
+ throw new Error("Provide exactly ONE of 'path' or 'url' — not both");
43455
44390
  }
43456
- const targets = Array.isArray(targetsInput) ? targetsInput : [targetsInput];
43457
- if (targets.length === 0) {
43458
- throw new Error("'targets' must be a non-empty object or array");
43459
- }
43460
- for (const [i, entry] of targets.entries()) {
43461
- const targetPath = entry?.path;
43462
- if (typeof targetPath !== "string" || targetPath.length === 0) {
44391
+ const targets = targetEntries.map((entry, i) => {
44392
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
43463
44393
  throw new Error(`targets[${i}].path must be a non-empty string`);
43464
44394
  }
43465
- if (typeof entry.symbol !== "string" || entry.symbol.length === 0) {
43466
- throw new Error(`targets[${i}].symbol must be a non-empty string`);
43467
- }
43468
- }
43469
- const resolvedTargets = await Promise.all(targets.map((t) => resolvePathArg(ctx, context, t.path)));
44395
+ const record = entry;
44396
+ const targetPath = isBlankParam(record.path) ? record.filePath : record.path;
44397
+ return {
44398
+ path: typeof targetPath === "string" ? targetPath : "",
44399
+ symbol: typeof record.symbol === "string" ? record.symbol : ""
44400
+ };
44401
+ });
44402
+ const localPaths = targets.map((target) => target.path).filter((path) => path.trim().length > 0 && !isGithubPathArg(path));
44403
+ if (hasFilePath && !isGithubPathArg(args.path))
44404
+ localPaths.push(args.path);
44405
+ const resolvedTargets = await Promise.all(localPaths.map((path) => resolvePathArg(ctx, context, path)));
43470
44406
  const permissionDenied = await assertPathExternalPermissions(ctx, context, resolvedTargets);
43471
44407
  if (permissionDenied)
43472
44408
  return permissionDeniedResponse(permissionDenied);
@@ -43476,6 +44412,12 @@ ${githubZoomDescription}` : ""),
43476
44412
  symbol: target.symbol
43477
44413
  }))
43478
44414
  };
44415
+ if (hasFilePath)
44416
+ rawArgs.filePath = args.path;
44417
+ else if (hasUrl)
44418
+ rawArgs.url = args.url;
44419
+ if (hasSymbols)
44420
+ rawArgs.symbols = args.symbols;
43479
44421
  if (contextLines !== undefined)
43480
44422
  rawArgs.contextLines = contextLines;
43481
44423
  if (wantCallgraph)
@@ -43492,7 +44434,7 @@ ${githubZoomDescription}` : ""),
43492
44434
  if (hasFilePath && hasUrl) {
43493
44435
  throw new Error("Provide exactly ONE of 'path' or 'url' — not both");
43494
44436
  }
43495
- const githubPath = hasFilePath && (String(args.path).startsWith("issue://") || String(args.path).startsWith("pr://")) || hasUrl && (String(args.url).startsWith("issue://") || String(args.url).startsWith("pr://"));
44437
+ const githubPath = hasFilePath && isGithubPathArg(args.path) || hasUrl && isGithubPathArg(args.url);
43496
44438
  if (!hasUrl && !githubPath) {
43497
44439
  const file = await resolvePathArg(ctx, context, args.path);
43498
44440
  const permissionDenied = await assertPathExternalPermissions(ctx, context, file);
@@ -43583,7 +44525,7 @@ function safetyTools(ctx) {
43583
44525
  ` + "Use checkpoint before risky multi-file changes. Use undo for quick single-file rollback.",
43584
44526
  args: {
43585
44527
  op: z12.enum(["undo", "history", "checkpoint", "restore", "list"]).describe("Safety operation"),
43586
- path: z12.string().optional().describe("File path (required for history, optional for undo, optional for restore — restores only that file). Absolute or relative to project root"),
44528
+ path: z12.string().optional().describe("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"),
43587
44529
  name: z12.string().optional().describe("Checkpoint name (required for checkpoint, restore)"),
43588
44530
  files: z12.array(z12.string()).optional().describe("Specific files to include in checkpoint, or to restore from a checkpoint (optional; restore without path/files restores the whole checkpoint; explicit checkpoint files may be untracked or gitignored)")
43589
44531
  },
@@ -43853,7 +44795,7 @@ function hasMeaningfulProjectSearchFallback(query, pathArg, externalRoot) {
43853
44795
  return trimmedQuery.length > 0 && trimmedQuery !== pathArg && trimmedQuery !== externalRoot;
43854
44796
  }
43855
44797
  var GENERATION_CHANGED_DISCLOSURE = "index changed - order re-derived";
43856
- function asRecord3(value) {
44798
+ function asRecord2(value) {
43857
44799
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
43858
44800
  }
43859
44801
  function normalizeContinuityQuery(query) {
@@ -43868,14 +44810,14 @@ function continuityKey(projectRoot, query, includeTests, topK) {
43868
44810
  ]);
43869
44811
  }
43870
44812
  function responseGeneration(response) {
43871
- const envelope = asRecord3(response);
43872
- const structured = asRecord3(envelope?.structuredContent) ?? asRecord3(envelope?.structured_content);
43873
- const plan = asRecord3(envelope?.plan) ?? asRecord3(structured?.plan);
44813
+ const envelope = asRecord2(response);
44814
+ const structured = asRecord2(envelope?.structuredContent) ?? asRecord2(envelope?.structured_content);
44815
+ const plan = asRecord2(envelope?.plan) ?? asRecord2(structured?.plan);
43874
44816
  const generation = plan?.snapshot_generation ?? envelope?.snapshot_generation;
43875
44817
  return typeof generation === "string" ? generation : undefined;
43876
44818
  }
43877
44819
  function surfaceSearchText(response, continuity, sessionId, key) {
43878
- const envelope = asRecord3(response);
44820
+ const envelope = asRecord2(response);
43879
44821
  const text = typeof envelope?.text === "string" ? envelope.text : "";
43880
44822
  const generation = responseGeneration(response);
43881
44823
  if (generation === undefined)
@@ -44019,8 +44961,20 @@ function buildAftToolDefinitions(ctx, config, onUnknownDisabled) {
44019
44961
  const unknown = unknownDisabledTools(disabled);
44020
44962
  if (unknown.length > 0)
44021
44963
  onUnknownDisabled?.(unknown);
44964
+ describeBashForRegisteredSurface(allTools, config);
44022
44965
  return allTools;
44023
44966
  }
44967
+ function describeBashForRegisteredSurface(allTools, config) {
44968
+ const bash = allTools.bash;
44969
+ if (!bash)
44970
+ return;
44971
+ const bashCfg = resolveBashConfig(config);
44972
+ bash.description = bashToolDescription("aft_search" in allTools, bashCfg.compress, bashCfg.background, bashCfg.detach_on_user_message, "aft_zoom" in allTools, "bash_watch" in allTools, {
44973
+ outline: "aft_outline" in allTools,
44974
+ status: "bash_status" in allTools,
44975
+ write: "bash_write" in allTools
44976
+ });
44977
+ }
44024
44978
  function registerAftTools(context, location, definitions, consumers = {}) {
44025
44979
  const projected = Object.entries(definitions).map(([name, definition]) => projectV2Tool(name, definition, location, consumers));
44026
44980
  return context.tool.transform((editor) => {
@@ -44153,6 +45107,8 @@ var defaultBridgeBootstrapDependencies = {
44153
45107
  loadConfig: loadAftConfig,
44154
45108
  deliverLoadNotices: (notify) => deliverConfigLoadNotices(notify, getConfigLoadNotices()),
44155
45109
  configLoadErrors: getConfigLoadErrors,
45110
+ configLoadSources: getConfigLoadSources,
45111
+ configLoadTexts: getConfigLoadTexts,
44156
45112
  subcConnectionFileError,
44157
45113
  migrateConfigLocations: (directory) => migrateAftConfigLocations(directory, bridgeLogger).flatMap((result) => result.warnings),
44158
45114
  resolveBinary: resolveBinaryWithWarmup,
@@ -44194,8 +45150,12 @@ function loadBootstrapConfig(directory, notify, dependencies = defaultBridgeBoot
44194
45150
  const failure = parseFailure(dependencies);
44195
45151
  if (failure)
44196
45152
  return configErrorState(failure, notify);
45153
+ const provenance = dependencies.configLoadSources ? {
45154
+ sources: [...dependencies.configLoadSources()],
45155
+ sourceTexts: Object.fromEntries(dependencies.configLoadTexts?.() ?? [])
45156
+ } : {};
44197
45157
  dependencies.deliverLoadNotices?.(notify);
44198
- return { ok: true, config };
45158
+ return { ok: true, config, ...provenance };
44199
45159
  } catch (err) {
44200
45160
  return configErrorState(err instanceof Error ? err.message : String(err), notify);
44201
45161
  }
@@ -44387,8 +45347,60 @@ function buildConfigErrorToolMap(config, message, client, buildToolMap = buildAf
44387
45347
  return failEveryToolCall(buildToolMap(ctx, config), message);
44388
45348
  }
44389
45349
 
45350
+ // src/config-live-reload.ts
45351
+ var OPENCODE_LIVE_CONFIG_KEYS = aftLiveConfigKeys((config) => resolveBashConfig(config));
45352
+ function projectTextOf(directory) {
45353
+ const { projectConfigPath } = resolveCortexKitConfigPaths(directory);
45354
+ return getConfigLoadTexts().get(projectConfigPath) ?? null;
45355
+ }
45356
+ function loadOpenCodeConfigForLiveReload(directory) {
45357
+ try {
45358
+ const config = loadAftConfig(directory);
45359
+ const [failure] = getConfigLoadErrors();
45360
+ if (failure) {
45361
+ return { ok: false, message: formatConfigParseErrorMessage(failure.path, failure.message) };
45362
+ }
45363
+ const [invalid] = getConfigValidationErrors();
45364
+ if (invalid) {
45365
+ return {
45366
+ ok: false,
45367
+ message: `AFT config at ${invalid.path} has an invalid setting: ${invalid.message}`
45368
+ };
45369
+ }
45370
+ return {
45371
+ ok: true,
45372
+ config,
45373
+ sources: [...getConfigLoadSources()],
45374
+ projectText: projectTextOf(directory),
45375
+ texts: Object.fromEntries(getConfigLoadTexts())
45376
+ };
45377
+ } catch (err) {
45378
+ return { ok: false, message: err instanceof Error ? err.message : String(err) };
45379
+ }
45380
+ }
45381
+ function startOpenCodeLiveConfigReload(options) {
45382
+ const { userConfigPath, projectConfigPath } = resolveCortexKitConfigPaths(options.directory);
45383
+ return startLiveConfigReload({
45384
+ paths: [userConfigPath, projectConfigPath],
45385
+ load: () => loadOpenCodeConfigForLiveReload(options.directory),
45386
+ initialSources: options.initialSources,
45387
+ initialProjectText: options.initialSourceTexts?.[projectConfigPath] ?? null,
45388
+ initialTexts: options.initialSourceTexts,
45389
+ securityKeys: aftLiveSecurityKeys(OPENCODE_LIVE_CONFIG_KEYS),
45390
+ keys: OPENCODE_LIVE_CONFIG_KEYS,
45391
+ getConfig: options.getConfig,
45392
+ setConfig: options.setConfig,
45393
+ log: (message) => log2(`${message} (${options.directory})`),
45394
+ reportError: (message) => {
45395
+ error2(message);
45396
+ options.notify(message);
45397
+ },
45398
+ watch: options.watch
45399
+ });
45400
+ }
45401
+
44390
45402
  // src/notifications.ts
44391
- import { existsSync as existsSync18, readFileSync as readFileSync14 } from "node:fs";
45403
+ import { existsSync as existsSync18, readFileSync as readFileSync15 } from "node:fs";
44392
45404
  import { homedir as homedir13, platform } from "node:os";
44393
45405
  import { join as join20 } from "node:path";
44394
45406
  function isTuiMode() {
@@ -44430,7 +45442,7 @@ function readDesktopState() {
44430
45442
  return { serverUrl: null };
44431
45443
  }
44432
45444
  try {
44433
- const raw = readFileSync14(statePath, "utf-8");
45445
+ const raw = readFileSync15(statePath, "utf-8");
44434
45446
  const state = JSON.parse(raw);
44435
45447
  let serverUrl = null;
44436
45448
  const serverStr = state.server;
@@ -44929,23 +45941,25 @@ import {
44929
45941
  existsSync as existsSync21,
44930
45942
  mkdirSync as mkdirSync9,
44931
45943
  openSync as openSync7,
44932
- readFileSync as readFileSync17,
45944
+ readFileSync as readFileSync18,
44933
45945
  renameSync as renameSync11,
44934
45946
  rmSync as rmSync8,
44935
- statSync as statSync14,
45947
+ statSync as statSync15,
44936
45948
  writeFileSync as writeFileSync12
44937
45949
  } from "node:fs";
44938
- import { dirname as dirname14 } from "node:path";
45950
+ import { dirname as dirname15 } from "node:path";
44939
45951
 
44940
45952
  // src/hooks/auto-update-checker/cache.ts
44941
- import { cpSync, existsSync as existsSync20, mkdtempSync, readFileSync as readFileSync16, rmSync as rmSync7, writeFileSync as writeFileSync11 } from "node:fs";
45953
+ import { cpSync, existsSync as existsSync20, mkdtempSync, readFileSync as readFileSync17, rmSync as rmSync7, writeFileSync as writeFileSync11 } from "node:fs";
44942
45954
  import { tmpdir as tmpdir4 } from "node:os";
44943
- import { basename as basename5, dirname as dirname13, join as join23 } from "node:path";
45955
+ import { basename as basename6, dirname as dirname14, join as join23 } from "node:path";
44944
45956
  var import_comment_json3 = __toESM(require_src2(), 1);
44945
45957
 
44946
45958
  // src/hooks/auto-update-checker/checker.ts
44947
- import { existsSync as existsSync19, readFileSync as readFileSync15, statSync as statSync13, writeFileSync as writeFileSync10 } from "node:fs";
44948
- import { dirname as dirname12, isAbsolute as isAbsolute11, join as join22, resolve as resolve13 } from "node:path";
45959
+ import { existsSync as existsSync19, readFileSync as readFileSync16, statSync as statSync14, writeFileSync as writeFileSync10 } from "node:fs";
45960
+ import { readFile } from "node:fs/promises";
45961
+ import { homedir as homedir14 } from "node:os";
45962
+ import { dirname as dirname13, isAbsolute as isAbsolute11, join as join22, resolve as resolve13 } from "node:path";
44949
45963
  import { fileURLToPath } from "node:url";
44950
45964
  var import_comment_json2 = __toESM(require_src2(), 1);
44951
45965
 
@@ -45036,14 +46050,14 @@ function resolvePathPluginSpec(spec, configPath) {
45036
46050
  }
45037
46051
  if (isAbsolute11(spec) || /^[A-Za-z]:[\\/]/.test(spec))
45038
46052
  return spec;
45039
- return resolve13(dirname12(configPath), spec);
46053
+ return resolve13(dirname13(configPath), spec);
45040
46054
  }
45041
46055
  function getLocalDevPath(directory) {
45042
46056
  for (const configPath of getConfigPaths(directory)) {
45043
46057
  try {
45044
46058
  if (!existsSync19(configPath))
45045
46059
  continue;
45046
- const rawConfig = parseJsonConfig(readFileSync15(configPath, "utf-8"));
46060
+ const rawConfig = parseJsonConfig(readFileSync16(configPath, "utf-8"));
45047
46061
  const plugins = getPluginEntries(rawConfig);
45048
46062
  for (const entry of plugins) {
45049
46063
  if (entry === PACKAGE_NAME || entry.startsWith(`${PACKAGE_NAME}@`))
@@ -45053,7 +46067,7 @@ function getLocalDevPath(directory) {
45053
46067
  const pkgPath = findPackageJsonUp(localPath);
45054
46068
  if (!pkgPath)
45055
46069
  continue;
45056
- const pkg = PackageJsonSchema.safeParse(JSON.parse(readFileSync15(pkgPath, "utf-8")));
46070
+ const pkg = PackageJsonSchema.safeParse(JSON.parse(readFileSync16(pkgPath, "utf-8")));
45057
46071
  if (pkg.success && pkg.data.name === PACKAGE_NAME)
45058
46072
  return localPath;
45059
46073
  }
@@ -45064,18 +46078,18 @@ function getLocalDevPath(directory) {
45064
46078
  }
45065
46079
  function findPackageJsonUp(startPath) {
45066
46080
  try {
45067
- const stat = statSync13(startPath);
45068
- let dir = stat.isDirectory() ? startPath : dirname12(startPath);
46081
+ const stat = statSync14(startPath);
46082
+ let dir = stat.isDirectory() ? startPath : dirname13(startPath);
45069
46083
  for (let i = 0;i < 10; i++) {
45070
46084
  const pkgPath = join22(dir, "package.json");
45071
46085
  if (existsSync19(pkgPath)) {
45072
46086
  try {
45073
- const pkg = PackageJsonSchema.safeParse(JSON.parse(readFileSync15(pkgPath, "utf-8")));
46087
+ const pkg = PackageJsonSchema.safeParse(JSON.parse(readFileSync16(pkgPath, "utf-8")));
45074
46088
  if (pkg.success && pkg.data.name === PACKAGE_NAME)
45075
46089
  return pkgPath;
45076
46090
  } catch {}
45077
46091
  }
45078
- const parent = dirname12(dir);
46092
+ const parent = dirname13(dir);
45079
46093
  if (parent === dir)
45080
46094
  break;
45081
46095
  dir = parent;
@@ -45091,7 +46105,7 @@ function getLocalDevVersion(directory) {
45091
46105
  const pkgPath = findPackageJsonUp(localPath);
45092
46106
  if (!pkgPath)
45093
46107
  return null;
45094
- const pkg = PackageJsonSchema.safeParse(JSON.parse(readFileSync15(pkgPath, "utf-8")));
46108
+ const pkg = PackageJsonSchema.safeParse(JSON.parse(readFileSync16(pkgPath, "utf-8")));
45095
46109
  return pkg.success ? pkg.data.version ?? null : null;
45096
46110
  } catch {
45097
46111
  return null;
@@ -45099,7 +46113,7 @@ function getLocalDevVersion(directory) {
45099
46113
  }
45100
46114
  function getCurrentRuntimePackageJsonPath(currentModuleUrl = import.meta.url) {
45101
46115
  try {
45102
- return findPackageJsonUp(dirname12(fileURLToPath(currentModuleUrl)));
46116
+ return findPackageJsonUp(dirname13(fileURLToPath(currentModuleUrl)));
45103
46117
  } catch (err) {
45104
46118
  warn2(`[auto-update-checker] Failed to resolve runtime package path: ${String(err)}`);
45105
46119
  return null;
@@ -45110,7 +46124,7 @@ function findPluginEntry(directory) {
45110
46124
  try {
45111
46125
  if (!existsSync19(configPath))
45112
46126
  continue;
45113
- const rawConfig = parseJsonConfig(readFileSync15(configPath, "utf-8"));
46127
+ const rawConfig = parseJsonConfig(readFileSync16(configPath, "utf-8"));
45114
46128
  const plugins = getPluginEntries(rawConfig);
45115
46129
  for (const entry of plugins) {
45116
46130
  if (entry === PACKAGE_NAME) {
@@ -45143,7 +46157,7 @@ function getCachedVersion(spec) {
45143
46157
  try {
45144
46158
  if (!existsSync19(packageJsonPath))
45145
46159
  continue;
45146
- const pkg = PackageJsonSchema.safeParse(JSON.parse(readFileSync15(packageJsonPath, "utf-8")));
46160
+ const pkg = PackageJsonSchema.safeParse(JSON.parse(readFileSync16(packageJsonPath, "utf-8")));
45147
46161
  if (pkg.success && pkg.data.version) {
45148
46162
  if (!spec)
45149
46163
  cachedPackageVersion = pkg.data.version;
@@ -45153,6 +46167,117 @@ function getCachedVersion(spec) {
45153
46167
  }
45154
46168
  return null;
45155
46169
  }
46170
+ var registryCache = new Map;
46171
+ function npmConfigGet(key, directory) {
46172
+ return new Promise((resolve, reject) => {
46173
+ execFile(process.platform === "win32" ? "npm.cmd" : "npm", ["config", "get", key], {
46174
+ cwd: directory,
46175
+ timeout: NPM_FETCH_TIMEOUT,
46176
+ windowsHide: true,
46177
+ shell: process.platform === "win32"
46178
+ }, (error, stdout) => {
46179
+ if (error)
46180
+ reject(error);
46181
+ else
46182
+ resolve(stdout.trim());
46183
+ });
46184
+ });
46185
+ }
46186
+ function configuredRegistry(value) {
46187
+ return value && value !== "undefined" && value !== "null" ? value : undefined;
46188
+ }
46189
+ async function readRegistrySettings(path, required = false) {
46190
+ let text;
46191
+ try {
46192
+ text = await readFile(path, "utf8");
46193
+ } catch (error) {
46194
+ if (!required && error.code === "ENOENT")
46195
+ return {};
46196
+ throw error;
46197
+ }
46198
+ const settings = {};
46199
+ for (const line of text.split(/\r?\n/)) {
46200
+ const trimmed = line.trim();
46201
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith(";"))
46202
+ continue;
46203
+ const separator = trimmed.indexOf("=");
46204
+ if (separator < 0)
46205
+ continue;
46206
+ const key = trimmed.slice(0, separator).trim();
46207
+ if (key === "registry" || key === "@cortexkit:registry") {
46208
+ settings[key] = configuredRegistry(trimmed.slice(separator + 1).trim());
46209
+ }
46210
+ }
46211
+ return settings;
46212
+ }
46213
+ function registryEnvironment(key) {
46214
+ const variable = `npm_config_${key}`;
46215
+ const value = process.env[variable] ?? Object.entries(process.env).find(([name]) => name.toLowerCase() === variable)?.[1];
46216
+ return configuredRegistry(value ?? "");
46217
+ }
46218
+ async function registryWithoutNpm(directory) {
46219
+ let scoped = registryEnvironment("@cortexkit:registry");
46220
+ let registry = registryEnvironment("registry");
46221
+ const visited = new Set;
46222
+ for (let current = directory;; current = dirname13(current)) {
46223
+ if (scoped)
46224
+ break;
46225
+ const path = join22(current, ".npmrc");
46226
+ visited.add(path);
46227
+ const settings = await readRegistrySettings(path);
46228
+ scoped ??= settings["@cortexkit:registry"];
46229
+ registry ??= settings.registry;
46230
+ if (dirname13(current) === current)
46231
+ break;
46232
+ }
46233
+ const explicitUserConfig = process.env.npm_config_userconfig ?? process.env.NPM_CONFIG_USERCONFIG;
46234
+ const home = (process.platform === "win32" ? process.env.USERPROFILE : process.env.HOME) ?? homedir14();
46235
+ const userConfig = explicitUserConfig ?? join22(home, ".npmrc");
46236
+ if (!scoped && !visited.has(userConfig)) {
46237
+ const settings = await readRegistrySettings(userConfig, explicitUserConfig !== undefined);
46238
+ scoped = settings["@cortexkit:registry"];
46239
+ registry ??= settings.registry;
46240
+ }
46241
+ const selected = scoped ?? registry ?? NPM_REGISTRY_URL;
46242
+ return selected.replace(/\$\{([^}]+)\}/g, (_match, name) => {
46243
+ const value = process.env[name];
46244
+ if (value === undefined)
46245
+ throw new Error("Unresolved registry environment variable");
46246
+ return value;
46247
+ });
46248
+ }
46249
+ function validateRegistry(registry) {
46250
+ const url = new URL(registry);
46251
+ if (url.protocol !== "https:" && url.protocol !== "http:")
46252
+ throw new Error("Unsupported registry protocol");
46253
+ return registry;
46254
+ }
46255
+ var reportedRegistryFailures = new Set;
46256
+ function reportRegistryFailure(directory) {
46257
+ const key = resolve13(directory);
46258
+ if (reportedRegistryFailures.has(key))
46259
+ return;
46260
+ reportedRegistryFailures.add(key);
46261
+ debug2("[auto-update-checker] Configured npm registry could not be resolved or used; skipping update check.");
46262
+ }
46263
+ function resolveRegistry(directory) {
46264
+ const key = resolve13(directory);
46265
+ let pending = registryCache.get(key);
46266
+ if (!pending) {
46267
+ pending = (async () => {
46268
+ let registry;
46269
+ try {
46270
+ const scoped = configuredRegistry(await npmConfigGet("@cortexkit:registry", key));
46271
+ registry = scoped ?? configuredRegistry(await npmConfigGet("registry", key)) ?? NPM_REGISTRY_URL;
46272
+ } catch {
46273
+ registry = await registryWithoutNpm(key);
46274
+ }
46275
+ return validateRegistry(registry);
46276
+ })();
46277
+ registryCache.set(key, pending);
46278
+ }
46279
+ return pending;
46280
+ }
45156
46281
  function buildRegistryUrl(registryUrl) {
45157
46282
  return `${registryUrl.replace(/\/+$/, "")}/${encodeURIComponent(PACKAGE_NAME).replace("%2F", "/")}`;
45158
46283
  }
@@ -45164,17 +46289,24 @@ async function getLatestVersion(channel = "latest", options = {}) {
45164
46289
  try {
45165
46290
  if (options.signal?.aborted)
45166
46291
  return null;
45167
- const response = await fetch(buildRegistryUrl(options.registryUrl ?? NPM_REGISTRY_URL), {
46292
+ const registry = validateRegistry(options.registryUrl ?? await resolveRegistry(options.directory ?? process.cwd()));
46293
+ if (controller.signal.aborted)
46294
+ return null;
46295
+ const response = await fetch(buildRegistryUrl(registry), {
45168
46296
  signal: controller.signal,
45169
46297
  headers: { Accept: "application/json" }
45170
46298
  });
45171
- if (!response.ok)
46299
+ if (!response.ok) {
46300
+ reportRegistryFailure(options.directory ?? process.cwd());
45172
46301
  return null;
46302
+ }
45173
46303
  const data = NpmPackageEnvelopeSchema.safeParse(await response.json());
45174
46304
  if (!data.success)
45175
46305
  return null;
45176
46306
  return data.data["dist-tags"][channel] ?? data.data["dist-tags"].latest ?? null;
45177
46307
  } catch {
46308
+ if (!controller.signal.aborted)
46309
+ reportRegistryFailure(options.directory ?? process.cwd());
45178
46310
  return null;
45179
46311
  } finally {
45180
46312
  options.signal?.removeEventListener("abort", abortHandler);
@@ -45194,9 +46326,9 @@ function createAutoUpdateSnapshot(installDir, packageJsonPath, packageName) {
45194
46326
  cpSync(packageDir, stagedPackageDir, { recursive: true });
45195
46327
  return {
45196
46328
  packageJsonPath,
45197
- packageJson: existsSync20(packageJsonPath) ? readFileSync16(packageJsonPath, "utf-8") : null,
46329
+ packageJson: existsSync20(packageJsonPath) ? readFileSync17(packageJsonPath, "utf-8") : null,
45198
46330
  lockfilePath,
45199
- lockfile: existsSync20(lockfilePath) ? readFileSync16(lockfilePath, "utf-8") : null,
46331
+ lockfile: existsSync20(lockfilePath) ? readFileSync17(lockfilePath, "utf-8") : null,
45200
46332
  packageDir,
45201
46333
  stagedPackageDir,
45202
46334
  tempDir
@@ -45223,9 +46355,9 @@ function restoreAutoUpdateSnapshot(snapshot) {
45223
46355
  function stripPackageNameFromPath(pathValue, packageName) {
45224
46356
  let current = pathValue;
45225
46357
  for (const segment of [...packageName.split("/")].reverse()) {
45226
- if (basename5(current) !== segment)
46358
+ if (basename6(current) !== segment)
45227
46359
  return null;
45228
- current = dirname13(current);
46360
+ current = dirname14(current);
45229
46361
  }
45230
46362
  return current;
45231
46363
  }
@@ -45234,7 +46366,7 @@ function removeFromPackageLock(installDir, packageName) {
45234
46366
  if (!existsSync20(lockPath))
45235
46367
  return false;
45236
46368
  try {
45237
- const lock = import_comment_json3.parse(readFileSync16(lockPath, "utf-8"));
46369
+ const lock = import_comment_json3.parse(readFileSync17(lockPath, "utf-8"));
45238
46370
  let modified = false;
45239
46371
  if (lock.packages) {
45240
46372
  const key = `node_modules/${packageName}`;
@@ -45260,7 +46392,7 @@ function ensureDependencyVersion(packageJsonPath, packageName, version) {
45260
46392
  if (!existsSync20(packageJsonPath))
45261
46393
  return false;
45262
46394
  try {
45263
- const raw = import_comment_json3.parse(readFileSync16(packageJsonPath, "utf-8"));
46395
+ const raw = import_comment_json3.parse(readFileSync17(packageJsonPath, "utf-8"));
45264
46396
  const pkgJson = PackageJsonSchema.safeParse(raw);
45265
46397
  if (!pkgJson.success)
45266
46398
  return false;
@@ -45295,17 +46427,17 @@ function removeInstalledPackage(installDir, packageName) {
45295
46427
  }
45296
46428
  function resolveInstallContext(runtimePackageJsonPath = getCurrentRuntimePackageJsonPath()) {
45297
46429
  if (runtimePackageJsonPath) {
45298
- const packageDir = dirname13(runtimePackageJsonPath);
46430
+ const packageDir = dirname14(runtimePackageJsonPath);
45299
46431
  const nodeModulesDir = stripPackageNameFromPath(packageDir, PACKAGE_NAME);
45300
- if (nodeModulesDir && basename5(nodeModulesDir) === "node_modules") {
45301
- const installDir = dirname13(nodeModulesDir);
46432
+ if (nodeModulesDir && basename6(nodeModulesDir) === "node_modules") {
46433
+ const installDir = dirname14(nodeModulesDir);
45302
46434
  const packageJsonPath = join23(installDir, "package.json");
45303
46435
  if (existsSync20(packageJsonPath))
45304
46436
  return { installDir, packageJsonPath };
45305
46437
  }
45306
46438
  return null;
45307
46439
  }
45308
- const cacheRoot = dirname13(cacheDir());
46440
+ const cacheRoot = dirname14(cacheDir());
45309
46441
  const legacyPackageJsonPath = join23(cacheRoot, "package.json");
45310
46442
  if (existsSync20(legacyPackageJsonPath)) {
45311
46443
  return { installDir: cacheRoot, packageJsonPath: legacyPackageJsonPath };
@@ -45508,7 +46640,7 @@ function claimCheckSlot(storageDir, intervalMs) {
45508
46640
  try {
45509
46641
  if (hasRecentCheckTimestamp(file, intervalMs))
45510
46642
  return null;
45511
- mkdirSync9(dirname14(file), { recursive: true });
46643
+ mkdirSync9(dirname15(file), { recursive: true });
45512
46644
  const lockPath = `${file}.lock`;
45513
46645
  const owner = { pid: process.pid, startedMs: Date.now() };
45514
46646
  let lockFd = null;
@@ -45579,9 +46711,9 @@ function isOrphanedCheckLock(lockPath) {
45579
46711
  }
45580
46712
  }
45581
46713
  function readCheckLockState(lockPath) {
45582
- const fallbackStartedMs = statSync14(lockPath).mtimeMs;
46714
+ const fallbackStartedMs = statSync15(lockPath).mtimeMs;
45583
46715
  try {
45584
- const parsed = JSON.parse(readFileSync17(lockPath, "utf-8"));
46716
+ const parsed = JSON.parse(readFileSync18(lockPath, "utf-8"));
45585
46717
  const pid = Number.isSafeInteger(parsed.pid) && (parsed.pid ?? 0) > 0 ? parsed.pid ?? null : null;
45586
46718
  const startedMs = typeof parsed.startedMs === "number" && Number.isFinite(parsed.startedMs) ? parsed.startedMs : fallbackStartedMs;
45587
46719
  return { pid, startedMs };
@@ -45591,7 +46723,7 @@ function readCheckLockState(lockPath) {
45591
46723
  }
45592
46724
  function checkLockIsOwnedBy(lockPath, owner) {
45593
46725
  try {
45594
- const parsed = JSON.parse(readFileSync17(lockPath, "utf-8"));
46726
+ const parsed = JSON.parse(readFileSync18(lockPath, "utf-8"));
45595
46727
  return parsed.pid === owner.pid && parsed.startedMs === owner.startedMs;
45596
46728
  } catch {
45597
46729
  return false;
@@ -45601,7 +46733,7 @@ function hasRecentCheckTimestamp(file, intervalMs) {
45601
46733
  if (!existsSync21(file))
45602
46734
  return false;
45603
46735
  try {
45604
- const raw = JSON.parse(readFileSync17(file, "utf-8"));
46736
+ const raw = JSON.parse(readFileSync18(file, "utf-8"));
45605
46737
  const last = typeof raw.lastCheckedMs === "number" ? raw.lastCheckedMs : 0;
45606
46738
  return Number.isFinite(last) && Date.now() - last < intervalMs;
45607
46739
  } catch {
@@ -45657,6 +46789,7 @@ async function runBackgroundUpdateCheck(ctx, options) {
45657
46789
  const channel = extractChannel(pluginInfo.pinnedVersion ?? currentVersion);
45658
46790
  const latestVersion = await getLatestVersion(channel, {
45659
46791
  registryUrl: options.npmRegistryUrl,
46792
+ directory: ctx.directory,
45660
46793
  timeoutMs: options.fetchTimeoutMs,
45661
46794
  signal: options.signal
45662
46795
  });
@@ -45797,13 +46930,13 @@ import {
45797
46930
  existsSync as existsSync22,
45798
46931
  mkdirSync as mkdirSync10,
45799
46932
  readdirSync as readdirSync6,
45800
- readFileSync as readFileSync18,
46933
+ readFileSync as readFileSync19,
45801
46934
  renameSync as renameSync12,
45802
46935
  unlinkSync as unlinkSync8,
45803
46936
  writeFileSync as writeFileSync13
45804
46937
  } from "node:fs";
45805
46938
  import { createServer } from "node:http";
45806
- import { dirname as dirname15, join as join25 } from "node:path";
46939
+ import { dirname as dirname16, join as join25 } from "node:path";
45807
46940
 
45808
46941
  // src/shared/rpc-utils.ts
45809
46942
  import { join as join24 } from "node:path";
@@ -45953,7 +47086,7 @@ class AftRpcServer {
45953
47086
  }
45954
47087
  }
45955
47088
  writePortFile() {
45956
- const dir = dirname15(this.portFilePath);
47089
+ const dir = dirname16(this.portFilePath);
45957
47090
  mkdirSync10(dir, { recursive: true, mode: 448 });
45958
47091
  const tmpPath = `${this.portFilePath}.tmp`;
45959
47092
  writeFileSync13(tmpPath, JSON.stringify({
@@ -45978,7 +47111,7 @@ class AftRpcServer {
45978
47111
  if (filePath === this.portFilePath)
45979
47112
  continue;
45980
47113
  try {
45981
- const record = parseRpcPortRecord(readFileSync18(filePath, "utf-8"));
47114
+ const record = parseRpcPortRecord(readFileSync19(filePath, "utf-8"));
45982
47115
  if (record === null) {
45983
47116
  unlinkSync8(filePath);
45984
47117
  continue;
@@ -46193,7 +47326,7 @@ function formatSemanticIndexLabel(semantic) {
46193
47326
  backendUrl: semantic.backend_url
46194
47327
  });
46195
47328
  }
46196
- function asRecord4(value) {
47329
+ function asRecord3(value) {
46197
47330
  return typeof value === "object" && value !== null ? value : {};
46198
47331
  }
46199
47332
  function readString(value, fallback = "") {
@@ -46212,7 +47345,7 @@ function readOptionalNumber(value) {
46212
47345
  return typeof value === "number" && Number.isFinite(value) ? value : null;
46213
47346
  }
46214
47347
  function readCompressionAggregate(value) {
46215
- const aggregate = asRecord4(value);
47348
+ const aggregate = asRecord3(value);
46216
47349
  return {
46217
47350
  events: readNumber(aggregate.events),
46218
47351
  original_tokens: readNumber(aggregate.original_tokens),
@@ -46223,7 +47356,7 @@ function readCompressionAggregate(value) {
46223
47356
  function readCompression(value) {
46224
47357
  if (typeof value !== "object" || value === null)
46225
47358
  return;
46226
- const compression = asRecord4(value);
47359
+ const compression = asRecord3(value);
46227
47360
  return {
46228
47361
  project: readCompressionAggregate(compression.project),
46229
47362
  session: readCompressionAggregate(compression.session)
@@ -46232,7 +47365,7 @@ function readCompression(value) {
46232
47365
  function readStatusBar(value) {
46233
47366
  if (typeof value !== "object" || value === null)
46234
47367
  return;
46235
- const bar = asRecord4(value);
47368
+ const bar = asRecord3(value);
46236
47369
  const errors = readOptionalNumber(bar.errors);
46237
47370
  const warnings = readOptionalNumber(bar.warnings);
46238
47371
  const deadCode = readOptionalNumber(bar.dead_code);
@@ -46290,17 +47423,17 @@ function formatBytes(bytes) {
46290
47423
  return `${value.toFixed(decimals)} ${units[unitIndex]}`;
46291
47424
  }
46292
47425
  function coerceAftStatus(response) {
46293
- const features = asRecord4(response.features);
46294
- const searchIndex = asRecord4(response.search_index);
46295
- const semanticIndex = asRecord4(response.semantic_index);
47426
+ const features = asRecord3(response.features);
47427
+ const searchIndex = asRecord3(response.search_index);
47428
+ const semanticIndex = asRecord3(response.semantic_index);
46296
47429
  const semanticConfig = {
46297
- ...asRecord4(response.semantic),
46298
- ...asRecord4(response.semantic_config)
47430
+ ...asRecord3(response.semantic),
47431
+ ...asRecord3(response.semantic_config)
46299
47432
  };
46300
- const disk = asRecord4(response.disk);
46301
- const symbolCache = asRecord4(response.symbol_cache);
46302
- const runtime = asRecord4(response.runtime);
46303
- const session = asRecord4(response.session);
47433
+ const disk = asRecord3(response.disk);
47434
+ const symbolCache = asRecord3(response.symbol_cache);
47435
+ const runtime = asRecord3(response.runtime);
47436
+ const session = asRecord3(response.session);
46304
47437
  return {
46305
47438
  version: readString(response.version, "unknown"),
46306
47439
  project_root: readNullableString(response.project_root),
@@ -46552,7 +47685,10 @@ function registerShutdownCleanup(fn) {
46552
47685
 
46553
47686
  // src/workflow-hints.ts
46554
47687
  var HEADING = "## IMPORTANT NOTICE about your tools";
46555
- 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.";
47688
+ function hashlineTagSourceHint(bashRewritesMintTags, isRegistered = () => true) {
47689
+ return `**Hashline edit tags**: ${hashlineTagSourceSentence(bashRewritesMintTags, isRegistered)}`;
47690
+ }
47691
+ var HASHLINE_TAG_SOURCE_HINT = hashlineTagSourceHint(true);
46556
47692
  function buildWorkflowHints(opts) {
46557
47693
  const sections = [];
46558
47694
  const grepName = "grep";
@@ -46569,6 +47705,8 @@ function buildWorkflowHints(opts) {
46569
47705
  const hasInspect = !opts.disabledTools.has("aft_inspect");
46570
47706
  const hasBash = !opts.disabledTools.has(bashName);
46571
47707
  const hasBgBash = hasBash && opts.bashBackgroundEnabled && !opts.disabledTools.has(bashStatusName);
47708
+ const hasWatch = !opts.disabledTools.has("bash_watch");
47709
+ const hasWrite = !opts.disabledTools.has(bashWriteName);
46572
47710
  if (hasBash && opts.bashCompressionEnabled) {
46573
47711
  sections.push([
46574
47712
  "**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:",
@@ -46610,17 +47748,19 @@ function buildWorkflowHints(opts) {
46610
47748
  ].join(`
46611
47749
  `));
46612
47750
  }
46613
- if (hasBash && hasBgBash) {
47751
+ if (hasBgBash && hasWatch) {
46614
47752
  sections.push([
46615
47753
  `**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.`,
46616
47754
  "- `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.",
46617
47755
  "- `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."
46618
47756
  ].join(`
46619
47757
  `));
47758
+ }
47759
+ if (hasBgBash && hasWrite) {
46620
47760
  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 \`${bashStatusName}({ taskId, outputMode: "screen" })\`, and send input with \`${bashWriteName}({ taskId, input: "..." })\`.`);
46621
47761
  }
46622
47762
  if (opts.hashlineEffective === true) {
46623
- sections.push(HASHLINE_TAG_SOURCE_HINT);
47763
+ sections.push(hashlineTagSourceHint(hasBash && opts.bashRewriteEnabled !== false, (name) => !opts.disabledTools.has(name)));
46624
47764
  }
46625
47765
  if (sections.length === 0) {
46626
47766
  return null;
@@ -46635,13 +47775,32 @@ ${sections.join(`
46635
47775
  `)}`;
46636
47776
  }
46637
47777
  function buildHintsFromConfig(config, disabledTools, hashlineEffective = false) {
47778
+ const bashCfg = resolveBashConfig(config);
46638
47779
  return buildWorkflowHints({
46639
- bashBackgroundEnabled: resolveBashConfig(config).background,
46640
- bashCompressionEnabled: resolveBashConfig(config).compress,
47780
+ bashBackgroundEnabled: bashCfg.background,
47781
+ bashCompressionEnabled: bashCfg.compress,
47782
+ bashRewriteEnabled: bashCfg.rewrite,
46641
47783
  disabledTools,
46642
47784
  hashlineEffective
46643
47785
  });
46644
47786
  }
47787
+ var WORKFLOW_HINT_TOOL_NAMES = [
47788
+ "aft_outline",
47789
+ "aft_zoom",
47790
+ "aft_search",
47791
+ "aft_callgraph",
47792
+ "aft_inspect",
47793
+ "grep",
47794
+ "read",
47795
+ "bash",
47796
+ "bash_status",
47797
+ "bash_watch",
47798
+ "bash_write"
47799
+ ];
47800
+ function buildHintsForRegisteredTools(config, registeredTools, hashlineEffective = false) {
47801
+ const absent = new Set(WORKFLOW_HINT_TOOL_NAMES.filter((name) => !registeredTools.has(name)));
47802
+ return buildHintsFromConfig(config, absent, hashlineEffective);
47803
+ }
46645
47804
  function appendHintsToSystem(system, hintsBlock) {
46646
47805
  if (!hintsBlock)
46647
47806
  return;
@@ -46841,6 +48000,16 @@ async function initializePluginForDirectory(input) {
46841
48000
  storageDir: configOverrides.storage_dir,
46842
48001
  isProjectEnabled
46843
48002
  };
48003
+ const liveConfigReload = startOpenCodeLiveConfigReload({
48004
+ directory: registrationRoot,
48005
+ initialSources: bootstrap.sources ?? [],
48006
+ initialSourceTexts: bootstrap.sourceTexts,
48007
+ getConfig: () => ctx.config,
48008
+ setConfig: (config) => {
48009
+ ctx.config = config;
48010
+ },
48011
+ notify: (message) => deliverConfigMigrationWarnings(registrationRoot, [message])
48012
+ });
46844
48013
  const statusSubscribedBridges = new WeakSet;
46845
48014
  const statusUnsubscribes = new Set;
46846
48015
  const subscribeBridgeStatus = (bridge) => {
@@ -46878,6 +48047,7 @@ async function initializePluginForDirectory(input) {
46878
48047
  let clearInspectTier2Idle = () => {};
46879
48048
  const shutdownCleanup = registerShutdownCleanup(async (reason) => {
46880
48049
  autoUpdateAbort.abort();
48050
+ liveConfigReload.stop();
46881
48051
  clearInspectTier2Idle();
46882
48052
  for (const unsubscribe of statusUnsubscribes) {
46883
48053
  try {
@@ -47001,42 +48171,19 @@ Install: ${getManualInstallHint()}`);
47001
48171
  signal: autoUpdateAbort.signal,
47002
48172
  storageDir: ctx.storageDir
47003
48173
  });
47004
- const HINTS_TOOL_NAMES = [
47005
- "aft_outline",
47006
- "aft_zoom",
47007
- "aft_search",
47008
- "aft_callgraph",
47009
- "aft_inspect",
47010
- "grep",
47011
- "read",
47012
- "bash",
47013
- "bash_status"
47014
- ];
47015
48174
  const registeredTools = new Set(Object.keys(allTools));
47016
48175
  const { hashlineEditRegistered, aftSearchRegistered } = applyToolSurfaceOverrides(pool, aftConfig, registeredTools);
47017
48176
  ctx.hashlineEffective = hashlineEditRegistered;
47018
48177
  const hashlineDowngrade = reportHashlineDowngrade(aftConfig, registeredTools, (message) => deliverConfigMigrationWarnings(registrationRoot, [message]));
47019
48178
  log2(`hashline activation decision requested=${aftConfig.edit_mode === "hashline"} ` + `edit_slot_survives=${hashlineEditRegistered} effective=${ctx.hashlineEffective}` + (hashlineDowngrade ? ` downgraded=${hashlineDowngrade.code}` : ""));
47020
48179
  ctx.aftSearchRegistered = aftSearchRegistered;
47021
- for (const name of ["bash"]) {
47022
- const def = allTools[name];
47023
- if (def) {
47024
- const bashCfg = resolveBashConfig(aftConfig);
47025
- def.description = bashToolDescription(aftSearchRegistered, bashCfg.compress, bashCfg.background, bashCfg.detach_on_user_message);
47026
- }
47027
- }
47028
- const hintsAbsentTools = new Set;
47029
- for (const name of HINTS_TOOL_NAMES) {
47030
- if (!registeredTools.has(name))
47031
- hintsAbsentTools.add(name);
47032
- }
47033
- const hintsBlock = buildHintsFromConfig(aftConfig, hintsAbsentTools, hashlineEditRegistered);
48180
+ const hintsBlock = buildHintsForRegisteredTools(aftConfig, registeredTools, hashlineEditRegistered);
47034
48181
  if (hintsBlock) {
47035
48182
  log2(`Workflow hints injected (${hintsBlock.length} chars)`);
47036
48183
  }
47037
48184
  const inspectTier2Idle = createInspectTier2IdleScheduler({
47038
48185
  isEnabled: () => registeredTools.has("aft_inspect"),
47039
- idleMinutes: () => aftConfig.inspect?.tier2_idle_minutes,
48186
+ idleMinutes: () => ctx.config.inspect?.tier2_idle_minutes,
47040
48187
  warn: warn2,
47041
48188
  run: async (sessionID) => {
47042
48189
  const sessionDir = await getSessionDirectory(input.client, sessionID, input.directory) ?? input.directory;
@@ -47094,11 +48241,17 @@ Install: ${getManualInstallHint()}`);
47094
48241
  storageDir: configOverrides.storage_dir,
47095
48242
  pluginVersion: PLUGIN_VERSION,
47096
48243
  serverUrl: input.serverUrl?.toString(),
47097
- delivery: aftConfig.configure_warnings_delivery ?? "toast"
48244
+ delivery: ctx.config.configure_warnings_delivery ?? "toast"
47098
48245
  });
47099
48246
  }
47100
48247
  await flushConfigureWarningsOnIdle(sessionID);
47101
48248
  },
48249
+ "chat.params": async (params) => {
48250
+ rememberReadModel(input.client, params.sessionID, {
48251
+ providerID: params.model.providerID,
48252
+ modelID: params.model.id
48253
+ });
48254
+ },
47102
48255
  "chat.message": async (messageInput, messageOutput) => {
47103
48256
  const sid = messageInput.sessionID ?? messageInput.sessionId ?? messageInput.id;
47104
48257
  warmSessionDirectory(input.client, sid, input.directory);