@cortexkit/aft-pi 0.59.0 → 0.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7964,11 +7964,11 @@ var DEFAULT_WORKER_WAIT_MAX_MS = 1800000;
7964
7964
  var MIN_WORKER_WAIT_MAX_MS = 60000;
7965
7965
  var LONGEST_TIMER_DELAY_MS = 2147483647;
7966
7966
  var WORKER_WAIT_LIMIT_PHRASE = "the worker wait limit (`bash.worker_wait_max_ms`, 30 minutes by default)";
7967
- 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 up to the worker wait limit (`bash.worker_wait_max_ms`, 30 minutes by default), then reports it is still running; watch again to keep waiting";
7968
- 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 up to the worker wait limit (`bash.worker_wait_max_ms`, 30 minutes by default), after which the watch reports the command is still running; a value you pass is used as given.";
7967
+ 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 up to the worker wait limit (`bash.worker_wait_max_ms`, 30 minutes by default), then reports it is still running; watch again to keep waiting. An explicit timeout never exceeds that worker limit";
7968
+ 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 up to the worker wait limit (`bash.worker_wait_max_ms`, 30 minutes by default), after which the watch reports the command is still running; a value you pass is clamped to that worker limit.";
7969
7969
  function resolveWatchTimeoutMs(requestedMs, role, capMs, workerWaitMaxMs) {
7970
7970
  if (role === "worker")
7971
- return requestedMs ?? workerWaitMaxMs;
7971
+ return Math.min(requestedMs ?? workerWaitMaxMs, workerWaitMaxMs);
7972
7972
  return Math.min(requestedMs ?? DEFAULT_PRIMARY_WATCH_TIMEOUT_MS, capMs);
7973
7973
  }
7974
7974
  function maxWatchTimeoutMs(role, capMs) {
@@ -8031,6 +8031,10 @@ function taskKillDeadlineText(data, role, nowMs = Date.now()) {
8031
8031
  return "";
8032
8032
  if (data.status === "unknown")
8033
8033
  return "";
8034
+ if (typeof data.remote_phase_note === "string" && data.remote_phase_note !== "") {
8035
+ const note = data.remote_phase_note;
8036
+ return `${note.charAt(0).toUpperCase()}${note.slice(1)}.`;
8037
+ }
8034
8038
  const hardKill = data.hard_kill;
8035
8039
  if (!hardKill || typeof hardKill.limit_ms !== "number") {
8036
8040
  return "This task has no kill deadline.";
@@ -8765,8 +8769,8 @@ var BASH_HOST_FALLBACK_BANNER = "[AFT host fallback - module transport down; no
8765
8769
  var BASH_HOST_FALLBACK_MAX_OUTPUT_BYTES = 100 * 1024;
8766
8770
  var BASH_HOST_FALLBACK_MAX_TIMEOUT_MS = 10 * 60 * 1000;
8767
8771
  var BASH_HOST_FALLBACK_REFUSAL = "AFT transport is down; only foreground execution is available in host fallback";
8768
- var BASH_RUNON_DESCRIPTION = 'Run this command on the remote Linux build server instead of this machine; the only value is "linux".';
8769
- var BASH_RUNON_GUIDANCE = 'When remote runs are available, put `runon: "linux"` on build and test lines (cargo, bun test), including chains and pipes. Keep git, gh, interactive and file-editing commands local, and keep a line local if it needs macOS (Seatbelt, codesign, launchd, TCC, AppKit) or runs binaries built on this machine: a remote build leaves no binaries or target/ output here.';
8772
+ var BASH_RUNON_DESCRIPTION = 'Run on a remote build server: `linux` (2 vCPUs), or `linux,Nc` for N vCPUs such as `linux,8c`; add ",net" for outbound internet (offline by default). `windows` runs the command in Git Bash on a Windows build machine (no vCPU or network options).';
8773
+ var BASH_RUNON_GUIDANCE = 'When remote runs are available, put `runon: "linux"` on build and test lines (cargo, bun test), including chains and pipes. Keep git, gh, interactive and file-editing commands local, and keep a line local if it needs macOS (Seatbelt, codesign, launchd, TCC, AppKit) or runs binaries built on this machine: a remote build leaves no binaries or target/ output in the local worktree. Add `,Nc` to request N vCPUs (`linux,4c`); without it the job gets 2 vCPUs. The job sees exactly the CPUs it gets, so ask for what the command uses: 2c (the default) for a single test binary or script, 4c for `cargo check`/clippy on one crate, 8c for a workspace test suite, and 16c for a large release build.';
8770
8774
  function pathKeyForPlatform(env, platform) {
8771
8775
  return platform === "win32" ? Object.keys(env).find((key) => key.toLowerCase() === "path") : "PATH";
8772
8776
  }
@@ -9210,6 +9214,7 @@ function isPassiveCommand(command) {
9210
9214
 
9211
9215
  // ../aft-bridge/dist/transport.js
9212
9216
  var WORKER_SESSION_FIELD = "worker_session";
9217
+ var HOST_TOOL_NAMES_FIELD = "host_tool_names";
9213
9218
  var PRESET_FIELD = "preset";
9214
9219
  function callPresetFor(workerSession) {
9215
9220
  return workerSession ? "worker" : "head";
@@ -9374,6 +9379,8 @@ class BinaryBridge {
9374
9379
  lastBinaryFingerprintCheckAt = 0;
9375
9380
  _retiringDueToBinaryChange = false;
9376
9381
  pending = new Map;
9382
+ activeSendCalls = 0;
9383
+ processLoopReferenced = true;
9377
9384
  outstandingBackgroundTaskIds = new Set;
9378
9385
  nextId = 1;
9379
9386
  processGeneration = 0;
@@ -9605,6 +9612,32 @@ class BinaryBridge {
9605
9612
  this.sessionLogVia(sessionId, `hashline registration carrier transport=ndjson phase=${phase} edit_slot_survives=${editSlotSurvives}${repeated}`);
9606
9613
  }
9607
9614
  async send(command, params = {}, options) {
9615
+ this.activeSendCalls += 1;
9616
+ this.syncEventLoopHold();
9617
+ try {
9618
+ return await this.dispatchSend(command, params, options);
9619
+ } finally {
9620
+ this.activeSendCalls -= 1;
9621
+ this.syncEventLoopHold();
9622
+ }
9623
+ }
9624
+ syncEventLoopHold() {
9625
+ const child = this.process;
9626
+ if (!child)
9627
+ return;
9628
+ const hold = this.activeSendCalls > 0 || this.outstandingBackgroundTaskIds.size > 0;
9629
+ if (hold === this.processLoopReferenced)
9630
+ return;
9631
+ this.processLoopReferenced = hold;
9632
+ const handles = [child, child.stdin, child.stdout, child.stderr];
9633
+ for (const handle of handles) {
9634
+ if (hold)
9635
+ handle?.ref?.();
9636
+ else
9637
+ handle?.unref?.();
9638
+ }
9639
+ }
9640
+ async dispatchSend(command, params, options) {
9608
9641
  let dispatchParams = params;
9609
9642
  if (command === "configure") {
9610
9643
  dispatchParams = { ...params };
@@ -9642,11 +9675,13 @@ class BinaryBridge {
9642
9675
  params.edit_slot_survives = editSlotSurvives;
9643
9676
  this.logHashlineRegistrationCarrier("tool_call", sessionId, editSlotSurvives);
9644
9677
  }
9645
- const { preview, workerSession, ...sendOptions } = options ?? {};
9678
+ const { preview, workerSession, hostToolNames, ...sendOptions } = options ?? {};
9646
9679
  if (preview === true)
9647
9680
  params.preview = true;
9648
9681
  if (workerSession === true)
9649
9682
  params[WORKER_SESSION_FIELD] = true;
9683
+ if (hostToolNames)
9684
+ params[HOST_TOOL_NAMES_FIELD] = hostToolNames;
9650
9685
  return await this.send("tool_call", params, Object.keys(sendOptions).length > 0 ? sendOptions : undefined);
9651
9686
  }
9652
9687
  async sendWithVersionMismatchRetry(command, params, options, canRetryAfterVersionSwap) {
@@ -9715,7 +9750,12 @@ class BinaryBridge {
9715
9750
  if (command === "cancel_request" || Object.hasOwn(params, "command") || Object.hasOwn(params, "method")) {
9716
9751
  const nested = { ...params };
9717
9752
  const reserved = {};
9718
- for (const key of ["session_id", "lsp_hints", WORKER_SESSION_FIELD]) {
9753
+ for (const key of [
9754
+ "session_id",
9755
+ "lsp_hints",
9756
+ WORKER_SESSION_FIELD,
9757
+ HOST_TOOL_NAMES_FIELD
9758
+ ]) {
9719
9759
  if (Object.hasOwn(nested, key)) {
9720
9760
  reserved[key] = nested[key];
9721
9761
  delete nested[key];
@@ -10090,6 +10130,8 @@ class BinaryBridge {
10090
10130
  this.lastChildActivityAt = 0;
10091
10131
  this.consecutiveRequestTimeouts = 0;
10092
10132
  this.stderrTail = [];
10133
+ this.processLoopReferenced = true;
10134
+ this.syncEventLoopHold();
10093
10135
  }
10094
10136
  pushStderrLine(line) {
10095
10137
  this.stderrTail.push(line);
@@ -10216,6 +10258,7 @@ class BinaryBridge {
10216
10258
  const taskId = bashTaskIdFrom(response);
10217
10259
  if (taskId)
10218
10260
  this.outstandingBackgroundTaskIds.delete(taskId);
10261
+ this.syncEventLoopHold();
10219
10262
  this.onBashCompletion?.(response, this);
10220
10263
  return;
10221
10264
  }
@@ -10329,7 +10372,7 @@ class BinaryBridge {
10329
10372
  const delay = 100 * 2 ** this._restartCount;
10330
10373
  this._restartCount++;
10331
10374
  this.logVia(`Auto-restart #${this._restartCount} in ${delay}ms`);
10332
- setTimeout(() => {
10375
+ const restartTimer = setTimeout(() => {
10333
10376
  if (!this._shuttingDown && !this.isAlive()) {
10334
10377
  try {
10335
10378
  this.spawnProcess();
@@ -10338,6 +10381,7 @@ class BinaryBridge {
10338
10381
  }
10339
10382
  }
10340
10383
  }, delay);
10384
+ restartTimer.unref?.();
10341
10385
  this.scheduleRestartCountReset();
10342
10386
  } else {
10343
10387
  this.errorVia(`Max restarts (${this.maxRestarts}) reached, giving up. Logs: ${this.getLogFilePathVia()}${tail}`);
@@ -10358,6 +10402,7 @@ class BinaryBridge {
10358
10402
  this._restartCount = 0;
10359
10403
  this.restartResetTimer = null;
10360
10404
  }, BinaryBridge.RESTART_RESET_MS);
10405
+ this.restartResetTimer.unref?.();
10361
10406
  }
10362
10407
  clearRestartResetTimer() {
10363
10408
  if (this.restartResetTimer) {
@@ -11235,10 +11280,12 @@ function startLiveConfigReload(options) {
11235
11280
  let acceptedTexts;
11236
11281
  const reload = () => {
11237
11282
  let loaded = options.load();
11283
+ let deletedProjectConfig = false;
11238
11284
  if (loaded.ok) {
11239
11285
  const read = new Set(loaded.sources);
11240
11286
  const deleted = [...acceptedSources].find((path) => !read.has(path));
11241
- if (deleted !== undefined) {
11287
+ deletedProjectConfig = deleted === options.projectPath && loaded.projectText === null;
11288
+ if (deleted !== undefined && !deletedProjectConfig) {
11242
11289
  loaded = { ok: false, message: `AFT config at ${deleted} was deleted` };
11243
11290
  } else {
11244
11291
  acceptedSources = read;
@@ -12065,17 +12112,17 @@ ${reflowText}`;
12065
12112
  }
12066
12113
  return " Auto-formatted.";
12067
12114
  }
12068
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/client.js
12115
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/client.js
12069
12116
  import { promises as fs3 } from "node:fs";
12070
12117
  import { debuglog } from "node:util";
12071
12118
 
12072
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/auth.js
12119
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/auth.js
12073
12120
  import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
12074
12121
 
12075
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/socket.js
12122
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/socket.js
12076
12123
  import net from "node:net";
12077
12124
 
12078
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/envelope.js
12125
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/envelope.js
12079
12126
  var PROTOCOL_VERSION = 2;
12080
12127
  var HEADER_LEN = 21;
12081
12128
  var FROZEN_PREFIX_LEN = 5;
@@ -12178,12 +12225,12 @@ function validateHeaderFields(header) {
12178
12225
  if (admission === AdmissionClass.Sheddable && ty !== FrameType.Push && ty !== FrameType.StreamData) {
12179
12226
  throw new DecodeError(`SHEDDABLE admission class is illegal on ${FrameType[ty]} in flags 0b${flags.toString(2).padStart(8, "0")}`, "sheddable_illegal_frame_type");
12180
12227
  }
12181
- if (channel === 0 && epoch !== 0) {
12182
- throw new DecodeError(`control channel carried nonzero epoch ${epoch}`, "nonzero_epoch_on_control_channel");
12183
- }
12184
12228
  if (isPureHeader(ty) && len !== 0) {
12185
12229
  throw new DecodeError(`pure-header frame ${FrameType[ty]} declared non-zero body length ${len}`, "pure_header_frame_with_body");
12186
12230
  }
12231
+ if (channel === 0 && epoch !== 0) {
12232
+ throw new DecodeError(`control channel carried nonzero epoch ${epoch}`, "nonzero_epoch_on_control_channel");
12233
+ }
12187
12234
  }
12188
12235
  function decodeHeader(bytes) {
12189
12236
  if (bytes.length < FROZEN_PREFIX_LEN) {
@@ -12217,12 +12264,12 @@ function decodeHeader(bytes) {
12217
12264
  }
12218
12265
  const channel = view.getUint16(7, true);
12219
12266
  const epoch = view.getUint32(9, true);
12220
- if (channel === 0 && epoch !== 0) {
12221
- throw new DecodeError(`control channel carried nonzero epoch ${epoch}`, "nonzero_epoch_on_control_channel");
12222
- }
12223
12267
  if (isPureHeader(ty) && len !== 0) {
12224
12268
  throw new DecodeError(`pure-header frame ${FrameType[ty]} declared non-zero body length ${len}`, "pure_header_frame_with_body");
12225
12269
  }
12270
+ if (channel === 0 && epoch !== 0) {
12271
+ throw new DecodeError(`control channel carried nonzero epoch ${epoch}`, "nonzero_epoch_on_control_channel");
12272
+ }
12226
12273
  return { len, ver, ty, flags, channel, epoch, corr: view.getBigUint64(13, true) };
12227
12274
  }
12228
12275
  function buildFrame(ty, flags, channel, epoch, corr, body) {
@@ -12240,14 +12287,18 @@ function encodeFrame(frame) {
12240
12287
  if (frame.header.len !== frame.body.length) {
12241
12288
  throw new DecodeError(`frame header length ${frame.header.len} does not match body length ${frame.body.length}`, "frame_length_mismatch");
12242
12289
  }
12290
+ if (frame.body.length > MAX_FRAME_BODY_LEN) {
12291
+ throw new DecodeError(`frame body ${frame.body.length} exceeds max ${MAX_FRAME_BODY_LEN}`, "frame_body_too_large");
12292
+ }
12243
12293
  const header = encodeHeader(frame.header);
12294
+ decodeHeader(header);
12244
12295
  const output = new Uint8Array(header.length + frame.body.length);
12245
12296
  output.set(header, 0);
12246
12297
  output.set(frame.body, header.length);
12247
12298
  return output;
12248
12299
  }
12249
12300
 
12250
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/socket.js
12301
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/socket.js
12251
12302
  class SocketClosedError extends Error {
12252
12303
  }
12253
12304
 
@@ -12297,6 +12348,15 @@ class SubcSocket {
12297
12348
  sock.on("data", (chunk) => {
12298
12349
  this.chunks.push(chunk);
12299
12350
  this.buffered += chunk.length;
12351
+ if (this.waiter) {
12352
+ if (this.waiter.immediate) {
12353
+ clearImmediate(this.waiter.immediate);
12354
+ this.waiter.immediate = null;
12355
+ }
12356
+ if (this.waiter.extendOnProgress && this.buffered < this.waiter.need) {
12357
+ this.armWaiterTimer(this.waiter);
12358
+ }
12359
+ }
12300
12360
  this.tryServe();
12301
12361
  });
12302
12362
  const fail = (err) => {
@@ -12343,30 +12403,80 @@ class SubcSocket {
12343
12403
  throw new DecodeError(`frame body ${header.len} exceeds max ${MAX_FRAME_BODY_LEN}`, "frame_body_too_large");
12344
12404
  }
12345
12405
  onHeader?.();
12346
- const bodyDeadlineMs = typeof bodyDeadline === "number" ? bodyDeadline : Date.now() + bodyDeadline.afterHeaderMs;
12347
- const body = header.len === 0 ? new Uint8Array(0) : await this.readExact(header.len, bodyDeadlineMs);
12406
+ const bodyDeadlineArg = typeof bodyDeadline === "number" ? bodyDeadline : { idleTimeoutMs: bodyDeadline.afterHeaderMs };
12407
+ const body = header.len === 0 ? new Uint8Array(0) : await this.readExact(header.len, bodyDeadlineArg);
12348
12408
  return { header, body };
12349
12409
  }
12350
- readExact(n, deadlineMs) {
12410
+ armWaiterTimer(w) {
12411
+ if (!Number.isFinite(w.budget))
12412
+ return;
12413
+ if (w.timer) {
12414
+ clearTimeout(w.timer);
12415
+ w.timer = null;
12416
+ }
12417
+ w.timer = setTimeout(() => {
12418
+ w.timer = null;
12419
+ const bufferedAtTimeout = this.buffered;
12420
+ const verifyTimeout = () => {
12421
+ if (this.waiter !== w)
12422
+ return;
12423
+ if (w.extendOnProgress && this.buffered > bufferedAtTimeout) {
12424
+ this.armWaiterTimer(w);
12425
+ return;
12426
+ }
12427
+ this.waiter = null;
12428
+ w.reject(new SocketTimeoutError(`timed out waiting for ${w.need} bytes`));
12429
+ };
12430
+ w.immediate = setImmediate(() => {
12431
+ if (this.waiter !== w)
12432
+ return;
12433
+ if (this.buffered > bufferedAtTimeout) {
12434
+ verifyTimeout();
12435
+ } else {
12436
+ w.immediate = setImmediate(() => {
12437
+ w.immediate = null;
12438
+ verifyTimeout();
12439
+ });
12440
+ }
12441
+ });
12442
+ }, w.budget);
12443
+ }
12444
+ readExact(n, deadline) {
12351
12445
  if (this.waiter) {
12352
12446
  return Promise.reject(new Error("concurrent readExact is not supported"));
12353
12447
  }
12354
12448
  if (n === 0)
12355
12449
  return Promise.resolve(new Uint8Array(0));
12356
12450
  return new Promise((resolve, reject) => {
12357
- let timer = null;
12358
- if (Number.isFinite(deadlineMs)) {
12359
- const remaining = deadlineMs - Date.now();
12360
- if (remaining <= 0) {
12451
+ let budget;
12452
+ const extendOnProgress = typeof deadline === "object";
12453
+ if (typeof deadline === "object") {
12454
+ budget = deadline.idleTimeoutMs;
12455
+ if (budget <= 0) {
12361
12456
  reject(new SocketTimeoutError(`timed out waiting for ${n} bytes`));
12362
12457
  return;
12363
12458
  }
12364
- timer = setTimeout(() => {
12365
- this.waiter = null;
12459
+ } else if (Number.isFinite(deadline)) {
12460
+ const remaining = deadline - Date.now();
12461
+ if (remaining <= 0) {
12366
12462
  reject(new SocketTimeoutError(`timed out waiting for ${n} bytes`));
12367
- }, remaining);
12463
+ return;
12464
+ }
12465
+ budget = remaining;
12466
+ } else {
12467
+ budget = Number.POSITIVE_INFINITY;
12368
12468
  }
12369
- this.waiter = { need: n, resolve, reject, timer };
12469
+ const waiter = {
12470
+ need: n,
12471
+ resolve,
12472
+ reject,
12473
+ timer: null,
12474
+ immediate: null,
12475
+ budget,
12476
+ extendOnProgress
12477
+ };
12478
+ this.waiter = waiter;
12479
+ this.armWaiterTimer(waiter);
12370
12480
  this.tryServe();
12371
12481
  });
12372
12482
  }
@@ -12449,6 +12559,8 @@ class SubcSocket {
12449
12559
  this.waiter = null;
12450
12560
  if (w.timer)
12451
12561
  clearTimeout(w.timer);
12562
+ if (w.immediate)
12563
+ clearImmediate(w.immediate);
12452
12564
  w.resolve(out);
12453
12565
  return;
12454
12566
  }
@@ -12456,6 +12568,8 @@ class SubcSocket {
12456
12568
  this.waiter = null;
12457
12569
  if (w.timer)
12458
12570
  clearTimeout(w.timer);
12571
+ if (w.immediate)
12572
+ clearImmediate(w.immediate);
12459
12573
  w.reject(this.closedErr);
12460
12574
  }
12461
12575
  }
@@ -12480,7 +12594,7 @@ class SubcSocket {
12480
12594
  }
12481
12595
  }
12482
12596
 
12483
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/auth.js
12597
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/auth.js
12484
12598
  var NONCE_LEN = 32;
12485
12599
  var MAX_AUTH_MESSAGE_LEN = 4096;
12486
12600
  var SERVER_PROOF_DOMAIN = "subc-server-v1";
@@ -12554,7 +12668,7 @@ async function authenticateClient(sock, conn, deadlineMs) {
12554
12668
  await writeMessage(sock, { client_auth: Array.from(clientAuth) }, deadlineMs);
12555
12669
  }
12556
12670
 
12557
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/connection-file.js
12671
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/connection-file.js
12558
12672
  import { promises as fs2 } from "node:fs";
12559
12673
  var SCHEMA_VERSION = 1;
12560
12674
  var MIN_KEY_LEN = 32;
@@ -12587,18 +12701,29 @@ function validate(info) {
12587
12701
  throw new ConnectionFileError(`connection file daemon_id must be ${DAEMON_ID_LEN} bytes, got ${info.daemonId.length}`);
12588
12702
  }
12589
12703
  }
12590
- async function verifyOwnerOnly(path) {
12704
+ function verifyOwnerOnly(path, stat, expectedUid) {
12591
12705
  if (process.platform === "win32")
12592
12706
  return;
12593
- const stat = await fs2.stat(path);
12707
+ if (expectedUid !== undefined && stat.uid !== expectedUid) {
12708
+ throw new ConnectionFileError(`connection file ${path} is owned by uid ${stat.uid}, expected effective uid ${expectedUid}`);
12709
+ }
12594
12710
  const mode = stat.mode & 511;
12595
12711
  if ((mode & 63) !== 0) {
12596
12712
  throw new ConnectionFileError(`connection file ${path} has insecure permissions 0o${mode.toString(8)}; expected owner-only 0600`);
12597
12713
  }
12598
12714
  }
12599
12715
  async function readConnectionFile(path) {
12600
- await verifyOwnerOnly(path);
12601
- const raw = await fs2.readFile(path, "utf8");
12716
+ return readConnectionFileForUid(path, process.geteuid?.());
12717
+ }
12718
+ async function readConnectionFileForUid(path, expectedUid) {
12719
+ const handle = await fs2.open(path, "r");
12720
+ let raw;
12721
+ try {
12722
+ verifyOwnerOnly(path, await handle.stat(), expectedUid);
12723
+ raw = await handle.readFile({ encoding: "utf8" });
12724
+ } finally {
12725
+ await handle.close();
12726
+ }
12602
12727
  let parsed;
12603
12728
  try {
12604
12729
  parsed = JSON.parse(raw);
@@ -12632,7 +12757,7 @@ async function readConnectionFile(path) {
12632
12757
  return info;
12633
12758
  }
12634
12759
 
12635
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/launch-nonce.js
12760
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/launch-nonce.js
12636
12761
  import { closeSync as closeSync4, fstatSync as fstatSync2, readSync as readSync3 } from "node:fs";
12637
12762
  var SUBC_LAUNCH_NONCE_ENV = "SUBC_LAUNCH_NONCE";
12638
12763
  var SUBC_LAUNCH_NONCE_FD_ENV = "SUBC_LAUNCH_NONCE_FD";
@@ -12821,7 +12946,7 @@ function closeQuietly(fd) {
12821
12946
  } catch {}
12822
12947
  }
12823
12948
 
12824
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/route-handle.js
12949
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/route-handle.js
12825
12950
  var connectionToken = new WeakMap;
12826
12951
  var reverseRequests = new WeakMap;
12827
12952
 
@@ -12914,7 +13039,7 @@ function sameRouteHandle(left, right) {
12914
13039
  return left === right;
12915
13040
  }
12916
13041
 
12917
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/client.js
13042
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/client.js
12918
13043
  var debug = debuglog("subc-client");
12919
13044
  var DEFAULT_HANDSHAKE_TIMEOUT_MS = 1e4;
12920
13045
  var DEFAULT_REQUEST_TIMEOUT_MS = 30000;
@@ -12971,6 +13096,12 @@ function requireBinaryBody(body) {
12971
13096
  const type = body === null ? "null" : Array.isArray(body) ? "array" : typeof body;
12972
13097
  throw new SubcError(`binary request body must be a Uint8Array; got ${type}`, "binary_body_required");
12973
13098
  }
13099
+ var SCOPE_ROUTE_CLOSE_REASONS = new Set([
13100
+ "scope_ended",
13101
+ "scope_carrier_removed",
13102
+ "scope_delegation_changed",
13103
+ "scope_parent_ended"
13104
+ ]);
12974
13105
  function parseRouteCloseReason(reason) {
12975
13106
  switch (reason) {
12976
13107
  case "reload":
@@ -13113,14 +13244,16 @@ class SubcClient {
13113
13244
  return modules.filter((module) => module.capabilities?.provides.includes(capability) ?? false).map((module) => module.module_id).sort();
13114
13245
  }
13115
13246
  async routeOpen(target, identity, opts = {}) {
13247
+ const scope = validatedRouteScope(opts.scope);
13116
13248
  const release = await this.routeOpenGate.acquire();
13117
13249
  try {
13118
- return await this.sendRouteOpen(target, identity, opts);
13250
+ return await this.sendRouteOpen(target, identity, { ...opts, scope });
13119
13251
  } finally {
13120
13252
  release();
13121
13253
  }
13122
13254
  }
13123
13255
  async sendRouteOpen(target, identity, opts = {}) {
13256
+ const scope = validatedRouteScope(opts.scope);
13124
13257
  const consumerIdentity = routeOpenConsumerIdentity(opts);
13125
13258
  const reverseRequests = opts.reverseRequests ?? new ReverseRequestRegistry;
13126
13259
  reverseRequests.seal();
@@ -13130,7 +13263,8 @@ class SubcClient {
13130
13263
  target,
13131
13264
  identity,
13132
13265
  ...consumerIdentity ? { consumer_identity: consumerIdentity } : {},
13133
- ...consumerCapabilities.length > 0 ? { consumer_capabilities: consumerCapabilities } : {}
13266
+ ...consumerCapabilities.length > 0 ? { consumer_capabilities: consumerCapabilities } : {},
13267
+ ...scope ? { scope: { owner: scope.owner, ref: scope.ref, scope_epoch: scope.scopeEpoch } } : {}
13134
13268
  });
13135
13269
  let installed = null;
13136
13270
  const install = (frame) => {
@@ -13333,7 +13467,7 @@ class SubcClient {
13333
13467
  this.sendRouteGoodbye(handle);
13334
13468
  }
13335
13469
  async closeManagedRoute(target, identity, opts = {}) {
13336
- const key = routeCacheKey(target, identity, routeOpenConsumerIdentity(opts), (opts.reverseRequests ?? new ReverseRequestRegistry).capabilities());
13470
+ const key = routeCacheKey(target, identity, routeOpenConsumerIdentity(opts), (opts.reverseRequests ?? new ReverseRequestRegistry).capabilities(), validatedRouteScope(opts.scope));
13337
13471
  const cached = this.routes.get(key);
13338
13472
  if (!cached)
13339
13473
  return;
@@ -13429,13 +13563,13 @@ class SubcClient {
13429
13563
  };
13430
13564
  pending.timer = setTimeout(() => this.arbitrateTimeout(key, pending, channel, corr, ms), ms);
13431
13565
  this.pending.set(key, pending);
13432
- if (signal)
13433
- this.attachCancelSignal(signal, key, handle, corr, priority);
13434
13566
  writeBorrowed(this.sock, encodeFrame(frame), Date.now() + ms).catch((error) => {
13435
13567
  const current = this.pending.get(key);
13436
13568
  if (current)
13437
13569
  this.rejectPending(key, current, error instanceof Error ? error : new SubcError(String(error)));
13438
13570
  });
13571
+ if (signal)
13572
+ this.attachCancelSignal(signal, key, handle, corr, priority);
13439
13573
  });
13440
13574
  }
13441
13575
  arbitrateTimeout(key, pending, channel, corr, ms) {
@@ -13509,8 +13643,6 @@ class SubcClient {
13509
13643
  };
13510
13644
  pending.timer = setTimeout(() => this.arbitrateTimeout(key, pending, handle.channel, corr, ms), ms);
13511
13645
  this.pending.set(key, pending);
13512
- if (signal)
13513
- this.attachCancelSignal(signal, key, handle, corr, priority);
13514
13646
  const write = writeTrackedBorrowed(this.sock, encodeFrame(frame), Date.now() + ms);
13515
13647
  handedToSocket = write.queued;
13516
13648
  write.completed.catch((error) => {
@@ -13518,9 +13650,12 @@ class SubcClient {
13518
13650
  if (current)
13519
13651
  this.rejectPending(key, current, error instanceof Error ? error : new SubcError(String(error)));
13520
13652
  });
13653
+ if (signal && write.queued)
13654
+ this.attachCancelSignal(signal, key, handle, corr, priority);
13521
13655
  });
13522
13656
  }
13523
13657
  async cachedRouteHandle(moduleId, opts) {
13658
+ const scope = validatedRouteScope(opts.scope);
13524
13659
  const identity = opts.identity ?? this.opts.identity;
13525
13660
  if (!identity) {
13526
13661
  throw new SubcCallError("terminal", "managed call requires a BindIdentity in SubcClient.connect({ identity }) or call(..., { identity })", "missing_identity");
@@ -13529,7 +13664,7 @@ class SubcClient {
13529
13664
  const consumerIdentity = routeOpenConsumerIdentity(opts);
13530
13665
  const reverseRequests = opts.reverseRequests ?? new ReverseRequestRegistry;
13531
13666
  reverseRequests.seal();
13532
- const key = routeCacheKey(target, identity, consumerIdentity, reverseRequests.capabilities());
13667
+ const key = routeCacheKey(target, identity, consumerIdentity, reverseRequests.capabilities(), scope);
13533
13668
  let cached = this.routes.get(key);
13534
13669
  if (!cached) {
13535
13670
  cached = {
@@ -13537,6 +13672,7 @@ class SubcClient {
13537
13672
  moduleId,
13538
13673
  target,
13539
13674
  identity,
13675
+ scope,
13540
13676
  consumerIdentity,
13541
13677
  reverseRequests,
13542
13678
  handle: null,
@@ -13544,6 +13680,8 @@ class SubcClient {
13544
13680
  };
13545
13681
  this.routes.set(key, cached);
13546
13682
  }
13683
+ if (cached.endReason !== undefined)
13684
+ throw this.endedCachedRoute(cached.endReason);
13547
13685
  if (cached.handle && this.isLiveHandle(cached.handle))
13548
13686
  return cached.handle;
13549
13687
  const startedAt = this.opts.now();
@@ -13579,6 +13717,8 @@ class SubcClient {
13579
13717
  for (;; ) {
13580
13718
  if (cached.closed)
13581
13719
  throw this.routeClosedDuringOpen();
13720
+ if (cached.endReason !== undefined)
13721
+ throw this.endedCachedRoute(cached.endReason);
13582
13722
  try {
13583
13723
  await this.ensureConnectedForManaged();
13584
13724
  } catch (error) {
@@ -13593,6 +13733,7 @@ class SubcClient {
13593
13733
  try {
13594
13734
  outcome = {
13595
13735
  handle: await this.sendRouteOpen(cached.target, cached.identity, {
13736
+ scope: cached.scope,
13596
13737
  consumerIdentity: cached.consumerIdentity ?? null,
13597
13738
  reverseRequests: cached.reverseRequests
13598
13739
  })
@@ -13760,10 +13901,11 @@ class SubcClient {
13760
13901
  }
13761
13902
  for (const key of routeKeys) {
13762
13903
  const cached = this.routes.get(key);
13763
- if (!cached || cached.closed)
13904
+ if (!cached || cached.closed || cached.endReason !== undefined)
13764
13905
  continue;
13765
13906
  try {
13766
13907
  const handle = await this.routeOpen(cached.target, cached.identity, {
13908
+ scope: cached.scope,
13767
13909
  consumerIdentity: cached.consumerIdentity ?? null,
13768
13910
  reverseRequests: cached.reverseRequests
13769
13911
  });
@@ -13791,11 +13933,15 @@ class SubcClient {
13791
13933
  const cause = new SubcError("route was closed before route.open completed", "route_closed", undefined, "closed_by_caller");
13792
13934
  return new SubcCallError("not_sent", cause.message, cause.code, cause);
13793
13935
  }
13936
+ endedCachedRoute(reason) {
13937
+ const cause = new SubcError(`cached route closed by subc (${reason}); must not reopen`, "route_closed", undefined, reason);
13938
+ return this.terminalCallError(cause.message, cause);
13939
+ }
13794
13940
  async readLoop(sock, generation) {
13795
13941
  try {
13796
13942
  for (;; ) {
13797
13943
  this.readerActive = false;
13798
- const frame = await sock.readFrame(Number.POSITIVE_INFINITY, { afterHeaderMs: BODY_READ_TIMEOUT_MS }, () => {
13944
+ const frame = await sock.readFrame(Number.POSITIVE_INFINITY, { afterHeaderMs: this.opts.bodyReadTimeoutMs ?? BODY_READ_TIMEOUT_MS }, () => {
13799
13945
  this.readerActive = true;
13800
13946
  });
13801
13947
  try {
@@ -13874,6 +14020,7 @@ class SubcClient {
13874
14020
  }
13875
14021
  if (frame.header.ty === FrameType.Goodbye && handle) {
13876
14022
  this.failHandle(handle, new SubcError("route closed by subc (GOODBYE)", "route_closed", undefined, this.routeEndReason(handle.channel)));
14023
+ this.handleCachedRouteClose(handle.channel, this.routeEndReason(handle.channel));
13877
14024
  if (this.liveRoutes.get(handle.channel) === handle)
13878
14025
  this.liveRoutes.delete(handle.channel);
13879
14026
  this.evictRouteHandle(handle);
@@ -13963,13 +14110,29 @@ class SubcClient {
13963
14110
  const final = push.op === "route.closed";
13964
14111
  if (final || !this.routeEndReasons.get(channel)?.final) {
13965
14112
  this.routeEndReasons.set(channel, { reason, final });
14113
+ if (final)
14114
+ this.handleCachedRouteClose(channel, reason);
13966
14115
  }
13967
14116
  }
13968
14117
  }
13969
14118
  } else if (typeof push.body.module_id === "string") {
13970
14119
  for (const [channel, module] of this.routeModules) {
13971
- if (module === push.body.module_id)
14120
+ if (module === push.body.module_id) {
13972
14121
  this.legacyChannelReasons.set(channel, reason);
14122
+ if (push.op === "route.closed")
14123
+ this.handleCachedRouteClose(channel, reason);
14124
+ }
14125
+ }
14126
+ }
14127
+ }
14128
+ handleCachedRouteClose(channel, reason) {
14129
+ for (const [key, cached] of this.routes) {
14130
+ if (cached.handle?.channel !== channel)
14131
+ continue;
14132
+ if (SCOPE_ROUTE_CLOSE_REASONS.has(reason)) {
14133
+ cached.endReason = reason;
14134
+ } else {
14135
+ this.routes.delete(key);
13973
14136
  }
13974
14137
  }
13975
14138
  }
@@ -13992,6 +14155,10 @@ class SubcClient {
13992
14155
  this.routeEndReasons.clear();
13993
14156
  this.legacyChannelReasons.clear();
13994
14157
  this.routeModules.clear();
14158
+ for (const [key, cached] of this.routes) {
14159
+ if (cached.endReason === undefined)
14160
+ this.routes.delete(key);
14161
+ }
13995
14162
  for (const [key, pending] of this.pending) {
13996
14163
  this.rejectPending(key, pending, pending.handle ? routeError : err);
13997
14164
  }
@@ -14086,6 +14253,10 @@ function isRouteOpenRefusal(err) {
14086
14253
  function isRetryableRouteOpenCode(code) {
14087
14254
  if (code === "capability_forbidden")
14088
14255
  return false;
14256
+ if (code === "target_flow_unsupported")
14257
+ return false;
14258
+ if (code === "target_agent_run_unsupported")
14259
+ return false;
14089
14260
  return code === "module_reloading" || code === "module_warming" || code === "target_unavailable" || code === "module_timeout" || code === "scope_not_synced" || code === "scope_changed";
14090
14261
  }
14091
14262
  function equalJitter(delayMs, random) {
@@ -14143,6 +14314,7 @@ function normalizeConnectOptions(opts) {
14143
14314
  random: opts.random ?? Math.random,
14144
14315
  routeOpenRetryDeadlineMs: opts.routeOpenRetryDeadlineMs ?? ROUTE_OPEN_RETRY_DEADLINE_MS,
14145
14316
  timeoutArbitrationGraceMs: opts.timeoutArbitrationGraceMs ?? TIMEOUT_ARBITRATION_GRACE_MS,
14317
+ bodyReadTimeoutMs: opts.bodyReadTimeoutMs,
14146
14318
  livenessProbeWindowMs: opts.livenessProbeWindowMs ?? LIVENESS_PROBE_WINDOW_MS,
14147
14319
  onControlPush: opts.onControlPush
14148
14320
  };
@@ -14166,10 +14338,28 @@ function isValidCapabilityIdentifier(identifier) {
14166
14338
  const numericVersion = Number(version);
14167
14339
  return Number.isSafeInteger(numericVersion) && numericVersion >= 1 && numericVersion <= 4294967295;
14168
14340
  }
14169
- function routeCacheKey(target, identity, consumerIdentity, consumerCapabilities) {
14341
+ function routeCacheKey(target, identity, consumerIdentity, consumerCapabilities, scope) {
14170
14342
  const consumerPart = consumerIdentity ? `${consumerIdentity.module_id}\x00${consumerIdentity.launch_nonce}` : "";
14171
14343
  const capabilitiesPart = [...consumerCapabilities].sort().join("\x00");
14172
- return `${target.kind}\x00${target.module_id}\x00${identity.project_root}\x00${identity.harness}\x00${identity.session}\x00${consumerPart}\x00${capabilitiesPart}`;
14344
+ const scopePart = scope ? JSON.stringify([scope.owner.kind, scope.owner.kind === "reserved" ? scope.owner.module_id : null, scope.ref, scope.scopeEpoch]) : "";
14345
+ return `${target.kind}\x00${target.module_id}\x00${identity.project_root}\x00${identity.harness}\x00${identity.session}\x00${consumerPart}\x00${capabilitiesPart}\x00${scopePart}`;
14346
+ }
14347
+ function validatedRouteScope(scope) {
14348
+ if (scope === undefined)
14349
+ return;
14350
+ if (!scope || typeof scope.owner !== "object" || !scope.owner || !["reserved", "direct", "unverified"].includes(scope.owner.kind) || scope.owner.kind === "reserved" && (typeof scope.owner.module_id !== "string" || scope.owner.module_id.length === 0)) {
14351
+ throw new SubcError("scope.owner must be a Principal with a non-empty reserved module_id", "invalid_scope");
14352
+ }
14353
+ if (typeof scope.ref !== "string" || scope.ref.length === 0) {
14354
+ throw new SubcError("scope.ref must be a non-empty string", "invalid_scope");
14355
+ }
14356
+ if (scope.scopeEpoch === undefined) {
14357
+ throw new SubcError("scope.scopeEpoch is required", "scope_epoch_required");
14358
+ }
14359
+ if (!Number.isSafeInteger(scope.scopeEpoch) || scope.scopeEpoch < 0) {
14360
+ throw new SubcError("scope.scopeEpoch must be a safe non-negative integer", "invalid_scope");
14361
+ }
14362
+ return { owner: { ...scope.owner }, ref: scope.ref, scopeEpoch: scope.scopeEpoch };
14173
14363
  }
14174
14364
  function routeOpenConsumerIdentity(opts = {}) {
14175
14365
  if (opts.consumerIdentity !== undefined)
@@ -14686,6 +14876,41 @@ class SubcRouteClosedMidCallError extends SubcCallError {
14686
14876
  this.reason = reason;
14687
14877
  }
14688
14878
  }
14879
+
14880
+ class SubcFrameReadTimeoutError extends SubcCallError {
14881
+ constructor(cause) {
14882
+ super("outcome_unknown", "the command may have run; timed out waiting for frame body bytes from the AFT daemon", "frame_read_timeout", cause);
14883
+ this.name = "SubcFrameReadTimeoutError";
14884
+ }
14885
+ }
14886
+ function isFrameBodyReadTimeoutMessage(message) {
14887
+ return typeof message === "string" && (/timed out waiting for \d+ bytes/i.test(message) || message.includes("timed out waiting for frame body bytes"));
14888
+ }
14889
+ function isSubcFrameReadTimeoutError(error) {
14890
+ if (error instanceof SubcFrameReadTimeoutError)
14891
+ return true;
14892
+ if (error instanceof SocketTimeoutError && isFrameBodyReadTimeoutMessage(error.message)) {
14893
+ return true;
14894
+ }
14895
+ if (error instanceof SubcError && isFrameBodyReadTimeoutMessage(error.message)) {
14896
+ return true;
14897
+ }
14898
+ if (error instanceof SubcCallError) {
14899
+ if (error.code === "frame_read_timeout")
14900
+ return true;
14901
+ if (error.cause !== undefined && error.cause !== error && isSubcFrameReadTimeoutError(error.cause)) {
14902
+ return true;
14903
+ }
14904
+ if (isFrameBodyReadTimeoutMessage(error.message))
14905
+ return true;
14906
+ }
14907
+ return false;
14908
+ }
14909
+ function frameReadTimeoutError(error) {
14910
+ if (!isSubcFrameReadTimeoutError(error))
14911
+ return error;
14912
+ return error instanceof SubcFrameReadTimeoutError ? error : new SubcFrameReadTimeoutError(error);
14913
+ }
14689
14914
  function isLocalRouteCloseRejection(error) {
14690
14915
  return error instanceof SubcError && error.closeReason === "closed_by_caller";
14691
14916
  }
@@ -14712,6 +14937,8 @@ function isSubcClientClosedError(error) {
14712
14937
  return error instanceof SubcError && error.message === "client closed";
14713
14938
  }
14714
14939
  function isSocketDeathError(error, depth = 0) {
14940
+ if (isSubcFrameReadTimeoutError(error))
14941
+ return true;
14715
14942
  if (error instanceof SubcError && error.closeReason === "connection_lost")
14716
14943
  return true;
14717
14944
  if (error instanceof SubcCallError) {
@@ -14754,6 +14981,12 @@ class SubcLivenessProbeTimeoutError extends Error {
14754
14981
  }
14755
14982
  }
14756
14983
  var AFT_MODULE_ID = "aft";
14984
+ var HOST_SCOPE_LOOKUP_TIMEOUT_MS = 2000;
14985
+ var HOST_SCOPE_RETRY_FLOOR_MS = 5000;
14986
+ var HOST_SCOPE_RETRY_CAP_MS = 300000;
14987
+ function isHostScopeSyncRefusal(code) {
14988
+ return code === "scope_not_live" || code === "scope_not_synced" || code === "scope_changed";
14989
+ }
14757
14990
  var MAX_CONSECUTIVE_TRANSPORT_FAILURES = 3;
14758
14991
  var LIVENESS_PROBE_TIMEOUT_MS = 15000;
14759
14992
  var REQUEST_DEADLINE_CODE = "request_deadline";
@@ -14832,7 +15065,7 @@ class BgSubscription {
14832
15065
  identity;
14833
15066
  acquireClient;
14834
15067
  dropClient;
14835
- consumerIdentity;
15068
+ openRoute;
14836
15069
  onNudge;
14837
15070
  sleep;
14838
15071
  canAttach;
@@ -14843,21 +15076,25 @@ class BgSubscription {
14843
15076
  isCurrent;
14844
15077
  noteClientAlive;
14845
15078
  noteUnansweredCall;
15079
+ onRouteAttached;
14846
15080
  stopped = false;
14847
15081
  current = null;
14848
15082
  attached = null;
15083
+ route = null;
14849
15084
  loop;
14850
15085
  lifecycleLogState = new Map;
14851
15086
  nudgeReceiptLogState = null;
14852
- constructor(identity, acquireClient, dropClient, consumerIdentity, onNudge, sleep, canAttach, onRootAttachFailure, onDormant, dispatchProbeIntervalMs, nudgeRef, isCurrent = () => true, noteClientAlive = () => {
15087
+ constructor(identity, acquireClient, dropClient, openRoute, onNudge, sleep, canAttach, onRootAttachFailure, onDormant, dispatchProbeIntervalMs, nudgeRef, isCurrent = () => true, noteClientAlive = () => {
14853
15088
  return;
14854
15089
  }, noteUnansweredCall = () => {
14855
15090
  return;
15091
+ }, onRouteAttached = () => {
15092
+ return;
14856
15093
  }) {
14857
15094
  this.identity = identity;
14858
15095
  this.acquireClient = acquireClient;
14859
15096
  this.dropClient = dropClient;
14860
- this.consumerIdentity = consumerIdentity;
15097
+ this.openRoute = openRoute;
14861
15098
  this.onNudge = onNudge;
14862
15099
  this.sleep = sleep;
14863
15100
  this.canAttach = canAttach;
@@ -14868,11 +15105,15 @@ class BgSubscription {
14868
15105
  this.isCurrent = isCurrent;
14869
15106
  this.noteClientAlive = noteClientAlive;
14870
15107
  this.noteUnansweredCall = noteUnansweredCall;
15108
+ this.onRouteAttached = onRouteAttached;
14871
15109
  this.loop = this.run();
14872
15110
  }
14873
15111
  get attachedClient() {
14874
15112
  return this.attached;
14875
15113
  }
15114
+ get attachedRoute() {
15115
+ return this.route;
15116
+ }
14876
15117
  async stop() {
14877
15118
  this.stopped = true;
14878
15119
  const sub = this.current;
@@ -14997,7 +15238,7 @@ class BgSubscription {
14997
15238
  }
14998
15239
  let route;
14999
15240
  try {
15000
- route = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, this.identity, { consumerIdentity: this.consumerIdentity });
15241
+ route = await this.openRoute(client);
15001
15242
  } catch (err) {
15002
15243
  if (this.isCurrent() && this.onRootAttachFailure(err)) {
15003
15244
  if (reconnecting)
@@ -15055,6 +15296,8 @@ class BgSubscription {
15055
15296
  });
15056
15297
  this.current = sub;
15057
15298
  this.attached = client;
15299
+ this.route = route;
15300
+ this.onRouteAttached();
15058
15301
  stopDispatchProbe = this.startDispatchProbe(client, routeId);
15059
15302
  this.info("subscription-open", `subscription open channel=${routeId}`);
15060
15303
  if (reconnecting) {
@@ -15097,6 +15340,7 @@ class BgSubscription {
15097
15340
  stopDispatchProbe();
15098
15341
  this.current = null;
15099
15342
  this.attached = null;
15343
+ this.route = null;
15100
15344
  safeCloseRoute(client, route);
15101
15345
  }
15102
15346
  await this.backoff(backoffAttempt++);
@@ -15112,7 +15356,7 @@ function isRecord(value) {
15112
15356
  return typeof value === "object" && value !== null && !Array.isArray(value);
15113
15357
  }
15114
15358
  function isRouteProvenAbsentError(err) {
15115
- return err instanceof SubcError && err.code === "unknown_channel" || err instanceof StaleRouteHandleError;
15359
+ return err instanceof SubcError && (err.code === "unknown_channel" || err.code === "stale_route_epoch") || err instanceof StaleRouteHandleError;
15116
15360
  }
15117
15361
  function isAbsentRootRouteError(err) {
15118
15362
  return isRecord(err) && err.code === "config_divergence" && typeof err.message === "string" && err.message.includes("project root does not exist");
@@ -15239,6 +15483,8 @@ class SubcTransport {
15239
15483
  body.preview = true;
15240
15484
  if (options?.workerSession === true)
15241
15485
  body[WORKER_SESSION_FIELD] = true;
15486
+ if (options?.hostToolNames)
15487
+ body[HOST_TOOL_NAMES_FIELD] = options.hostToolNames;
15242
15488
  body[PRESET_FIELD] = callPresetFor(options?.workerSession === true);
15243
15489
  const reply = await this.pool.routeRequest(this.identityFor(sessionId), body, timeoutMs, onProgress, this.generation, abortSignal);
15244
15490
  return reliftReply(reply);
@@ -15250,10 +15496,12 @@ class SubcTransport {
15250
15496
  }
15251
15497
  const { timeoutMs, onProgress, abortSignal } = this.splitOptions(options);
15252
15498
  const session = typeof params.session_id === "string" ? params.session_id : undefined;
15253
- const { [WORKER_SESSION_FIELD]: workerSession, ...args } = params;
15499
+ const { [WORKER_SESSION_FIELD]: workerSession, [HOST_TOOL_NAMES_FIELD]: hostToolNames, ...args } = params;
15254
15500
  const body = { name: command, arguments: args };
15255
15501
  if (workerSession === true)
15256
15502
  body[WORKER_SESSION_FIELD] = true;
15503
+ if (hostToolNames !== undefined)
15504
+ body[HOST_TOOL_NAMES_FIELD] = hostToolNames;
15257
15505
  body[PRESET_FIELD] = callPresetFor(workerSession === true);
15258
15506
  const editSlotSurvives = this.pool.getEditSlotSurvives();
15259
15507
  if (editSlotSurvives !== undefined)
@@ -15276,6 +15524,10 @@ class SubcTransportPool {
15276
15524
  connectionFile;
15277
15525
  handshakeTimeoutMs;
15278
15526
  consumerIdentity;
15527
+ hostScopes;
15528
+ hostScopeCache = new Map;
15529
+ hostScopeRefusals = new Set;
15530
+ hostScopeOutcomes = new Map;
15279
15531
  connectFn;
15280
15532
  onBgEventsNudge;
15281
15533
  onBgEventsNudgeRef;
@@ -15297,6 +15549,7 @@ class SubcTransportPool {
15297
15549
  routeReopenRetry = null;
15298
15550
  routeReopenRetryMs = null;
15299
15551
  sessions = new Map;
15552
+ sessionIndex = new Map;
15300
15553
  observedStarts = new Set;
15301
15554
  sessionSweep = null;
15302
15555
  rootIndex = new Map;
@@ -15316,6 +15569,7 @@ class SubcTransportPool {
15316
15569
  this.harness = options.harness;
15317
15570
  this.handshakeTimeoutMs = options.handshakeTimeoutMs;
15318
15571
  this.consumerIdentity = options.consumerIdentity;
15572
+ this.hostScopes = options.hostScopes ?? false;
15319
15573
  this.connectFn = options.connect ?? ((opts) => SubcClient.connect(opts));
15320
15574
  this.onBgEventsNudge = options.onBgEventsNudge;
15321
15575
  this.onBgEventsNudgeRef = options.onBgEventsNudgeRef;
@@ -15535,7 +15789,9 @@ class SubcTransportPool {
15535
15789
  return transport;
15536
15790
  }
15537
15791
  getBridge(projectRoot) {
15538
- const root = this.canonicalRoot(projectRoot);
15792
+ return this.getBridgeForCanonicalRoot(this.canonicalRoot(projectRoot));
15793
+ }
15794
+ getBridgeForCanonicalRoot(root) {
15539
15795
  if (this.shuttingDown && this.lifecycleEnabled()) {
15540
15796
  throw new SubcTransportShuttingDownError;
15541
15797
  }
@@ -15632,6 +15888,12 @@ class SubcTransportPool {
15632
15888
  pendingDetach: false
15633
15889
  };
15634
15890
  this.sessions.set(key, record);
15891
+ let records = this.sessionIndex.get(identity.session);
15892
+ if (!records) {
15893
+ records = new Set;
15894
+ this.sessionIndex.set(identity.session, records);
15895
+ }
15896
+ records.add(record);
15635
15897
  let keys = this.rootIndex.get(root);
15636
15898
  if (!keys) {
15637
15899
  keys = new Set;
@@ -15809,6 +16071,10 @@ class SubcTransportPool {
15809
16071
  log(`subc bg_events: ${message}${suffix}`, { sessionId: record.identity.session });
15810
16072
  }
15811
16073
  removeIndexMembership(record) {
16074
+ const records = this.sessionIndex.get(record.identity.session);
16075
+ records?.delete(record);
16076
+ if (records?.size === 0)
16077
+ this.sessionIndex.delete(record.identity.session);
15812
16078
  const keys = this.rootIndex.get(record.canonicalRoot);
15813
16079
  if (!keys)
15814
16080
  return;
@@ -15821,6 +16087,7 @@ class SubcTransportPool {
15821
16087
  this.sessions.delete(record.identityKey);
15822
16088
  this.removeIndexMembership(record);
15823
16089
  }
16090
+ this.stopUnusedHostScopeRetry(record.identity.session);
15824
16091
  }
15825
16092
  detachSession(key, reason) {
15826
16093
  const record = this.sessions.get(key);
@@ -15840,6 +16107,7 @@ class SubcTransportPool {
15840
16107
  record.routeEntry = null;
15841
16108
  if (routeEntry)
15842
16109
  routeEntry.closed = true;
16110
+ this.stopUnusedHostScopeRetry(record.identity.session);
15843
16111
  return { record, bgSub, routeEntry };
15844
16112
  }
15845
16113
  async cleanupDetached(detached) {
@@ -15896,6 +16164,17 @@ class SubcTransportPool {
15896
16164
  entry.inflight -= 1;
15897
16165
  this.closeRetiredRoute(record, entry);
15898
16166
  }
16167
+ retireHostToolRoute(record) {
16168
+ const entry = record.routeEntry;
16169
+ if (!entry)
16170
+ return;
16171
+ entry.closed = true;
16172
+ entry.closedBy ??= "route replaced";
16173
+ record.routeEntry = null;
16174
+ record.retiredRoutes.add(entry);
16175
+ this.closeRetiredRoute(record, entry);
16176
+ this.stopUnusedHostScopeRetry(record.identity.session);
16177
+ }
15899
16178
  async routeRequest(identity, body, timeoutMs, onProgress, expectedGeneration, abortSignal) {
15900
16179
  const callStartedAt = performance.now();
15901
16180
  const callDeadlineMs = timeoutMs ?? SUBC_DEFAULT_REQUEST_TIMEOUT_MS;
@@ -16034,15 +16313,14 @@ class SubcTransportPool {
16034
16313
  const clearRouteEntry = (entry) => {
16035
16314
  if (record.routeEntry !== entry)
16036
16315
  return;
16037
- entry.closed = true;
16038
- entry.closedBy ??= "route replaced";
16039
- record.routeEntry = null;
16040
- record.retiredRoutes.add(entry);
16041
- this.closeRetiredRoute(record, entry);
16316
+ this.retireHostToolRoute(record);
16042
16317
  };
16043
16318
  const handleRequestFailure = (error, entry) => {
16044
16319
  const aborted = error instanceof Error && error.name === "AbortError";
16045
16320
  const ownsClient = this.isCurrentSession(key, record) && this.client === client && !this.isReapInduced(record);
16321
+ if (ownsClient && error instanceof SubcError && error.closeReason?.startsWith("scope_")) {
16322
+ this.invalidateHostScope(identity.session);
16323
+ }
16046
16324
  if (!aborted && ownsClient && isSocketDeathError(error)) {
16047
16325
  this.dropClient(client, {
16048
16326
  error,
@@ -16051,7 +16329,7 @@ class SubcTransportPool {
16051
16329
  route: routeLabel(entry.handle)
16052
16330
  });
16053
16331
  }
16054
- if (aborted || isRouteProvenAbsentError(error) || error instanceof SubcError && error.closeReason !== undefined) {
16332
+ if (aborted || isRouteProvenAbsentError(error) || error instanceof SubcError && error.closeReason !== undefined || isSubcFrameReadTimeoutError(error)) {
16055
16333
  clearRouteEntry(entry);
16056
16334
  }
16057
16335
  if (aborted || !ownsClient || this.client !== client)
@@ -16130,10 +16408,13 @@ class SubcTransportPool {
16130
16408
  this.resetRouteReopenBackoff();
16131
16409
  return reply;
16132
16410
  } catch (requestError) {
16133
- const error = routeClosedMidCallError(requestError, routeAndEntry.entry, record);
16411
+ const error = frameReadTimeoutError(routeClosedMidCallError(requestError, routeAndEntry.entry, record));
16134
16412
  if (this.isReapInduced(record))
16135
16413
  throw this.annotateReapError(error, record);
16136
16414
  const ownsRoute = this.isCurrentSession(key, record) && this.client === client;
16415
+ if (ownsRoute && (error instanceof SubcError && error.code === "stale_route_epoch" || error instanceof StaleRouteHandleError)) {
16416
+ this.invalidateHostScope(identity.session);
16417
+ }
16137
16418
  if (ownsRoute && isRouteRequestReloadRefusal(error)) {
16138
16419
  clearRouteEntry(routeAndEntry.entry);
16139
16420
  lastRefusal = error;
@@ -16210,9 +16491,14 @@ class SubcTransportPool {
16210
16491
  return this.client;
16211
16492
  if (this.connecting)
16212
16493
  return this.connecting;
16494
+ let owner;
16213
16495
  this.connecting = this.connectFn({
16214
16496
  connectionFile: this.connectionFile,
16215
- handshakeTimeoutMs: this.handshakeTimeoutMs
16497
+ handshakeTimeoutMs: this.handshakeTimeoutMs,
16498
+ onControlPush: (push) => {
16499
+ if (owner && this.client === owner)
16500
+ this.onScopeRouteClose(push);
16501
+ }
16216
16502
  }).then((client) => {
16217
16503
  this.connecting = null;
16218
16504
  if (this.shuttingDown) {
@@ -16222,6 +16508,7 @@ class SubcTransportPool {
16222
16508
  throw new SubcTransportShuttingDownError;
16223
16509
  }
16224
16510
  this.client = client;
16511
+ owner = client;
16225
16512
  this.hadWorkingClient = true;
16226
16513
  this.transportFailures = 0;
16227
16514
  return client;
@@ -16251,9 +16538,7 @@ class SubcTransportPool {
16251
16538
  closedBy: null
16252
16539
  };
16253
16540
  this.assertRootCanAttach(record.canonicalRoot);
16254
- const opening = client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, {
16255
- consumerIdentity: this.consumerIdentity
16256
- }).then((route) => {
16541
+ const opening = this.openHostToolRoute(client, identity, record, entry).then((route) => {
16257
16542
  if (!this.isCurrentSession(record.identityKey, record) || record.routeEntry !== entry || entry.closed || this.client !== client || this.lifecycleEnabled() && !this.isCurrentLiveGeneration(record.canonicalRoot, record.generation)) {
16258
16543
  safeCloseRoute(client, route);
16259
16544
  if (record.routeEntry === entry)
@@ -16263,6 +16548,12 @@ class SubcTransportPool {
16263
16548
  entry.handle = route;
16264
16549
  entry.opening = null;
16265
16550
  this.noteClientAlive(client);
16551
+ const state = this.hostScopeCache.get(identity.session);
16552
+ if (state)
16553
+ this.scheduleHostScopeRetry(identity.session, state);
16554
+ if (state?.answer?.result === "scope" && !state.scopeBindPending && !entry.hostScope && !this.hostScopeRefusals.has(identity.session)) {
16555
+ this.promoteHostScope(identity.session);
16556
+ }
16266
16557
  return route;
16267
16558
  }).catch((error) => {
16268
16559
  const currentEntry = this.isCurrentSession(record.identityKey, record) && record.routeEntry === entry;
@@ -16270,6 +16561,7 @@ class SubcTransportPool {
16270
16561
  entry.closed = true;
16271
16562
  record.routeEntry = null;
16272
16563
  }
16564
+ this.stopUnusedHostScopeRetry(identity.session);
16273
16565
  if (!this.isCurrentSession(record.identityKey, record) && !(error instanceof RouteTornDownError)) {
16274
16566
  throw new RouteTornDownError("subc route opened after session closed");
16275
16567
  }
@@ -16282,6 +16574,327 @@ class SubcTransportPool {
16282
16574
  record.routeEntry = entry;
16283
16575
  return { route: await opening, entry };
16284
16576
  }
16577
+ hostScope(client, identity) {
16578
+ if (this.hostScopeRefusals.has(identity.session))
16579
+ return Promise.resolve(undefined);
16580
+ let state = this.hostScopeCache.get(identity.session);
16581
+ if (!state) {
16582
+ state = { retryAt: 0, retryMs: HOST_SCOPE_RETRY_FLOOR_MS };
16583
+ this.hostScopeCache.set(identity.session, state);
16584
+ }
16585
+ if (state.answer) {
16586
+ return Promise.resolve(state.answer.result === "scope" && !state.scopeBindPending ? state.answer.scope : undefined);
16587
+ }
16588
+ if (state.pending)
16589
+ return state.pending;
16590
+ if (Date.now() < state.retryAt)
16591
+ return Promise.resolve(undefined);
16592
+ const current = state;
16593
+ clearTimeout(current.retryTimer);
16594
+ current.retryTimer = undefined;
16595
+ const abort = new AbortController;
16596
+ current.lookupAbort = abort;
16597
+ const lookup = this.readHostScope(client, identity, abort.signal).then((outcome) => {
16598
+ if (this.hostScopeCache.get(identity.session) !== current)
16599
+ return;
16600
+ current.pending = undefined;
16601
+ current.lookupAbort = undefined;
16602
+ if (abort.signal.aborted)
16603
+ return;
16604
+ this.logHostScopeOutcome(identity.session, outcome);
16605
+ if (outcome.result === "scope" || outcome.result === "no_scope") {
16606
+ current.answer = outcome;
16607
+ current.retryAt = 0;
16608
+ current.retryMs = HOST_SCOPE_RETRY_FLOOR_MS;
16609
+ if (outcome.result === "scope")
16610
+ this.promoteHostScope(identity.session);
16611
+ } else {
16612
+ current.retryAt = Date.now() + current.retryMs;
16613
+ current.retryMs = Math.min(current.retryMs * 2, HOST_SCOPE_RETRY_CAP_MS);
16614
+ this.scheduleHostScopeRetry(identity.session, current);
16615
+ }
16616
+ return outcome.result === "scope" ? outcome.scope : undefined;
16617
+ });
16618
+ current.pending = lookup;
16619
+ return lookup;
16620
+ }
16621
+ unscopedHostRecord(session) {
16622
+ for (const record of this.sessionIndex.get(session) ?? []) {
16623
+ const entry = record.routeEntry;
16624
+ if (record.identity.session === session && !record.closed && (entry && !entry.closed && !entry.hostScope && entry.client === this.client && (entry.handle || entry.opening) || record.bgSub?.attachedClient === this.client && record.bgSub?.attachedRoute))
16625
+ return record;
16626
+ }
16627
+ return;
16628
+ }
16629
+ cancelHostScopeRetry(state) {
16630
+ clearTimeout(state.retryTimer);
16631
+ state.retryTimer = undefined;
16632
+ state.lookupAbort?.abort();
16633
+ }
16634
+ stopUnusedHostScopeRetry(session) {
16635
+ const state = this.hostScopeCache.get(session);
16636
+ if (state && !this.unscopedHostRecord(session)) {
16637
+ if (state.retryTimer || state.scopeBindPending && state.bindPending)
16638
+ log(`subc head scope: session=${session} retry stopped reason=route_closed`);
16639
+ this.cancelHostScopeRetry(state);
16640
+ state.scopeBindPending = false;
16641
+ }
16642
+ }
16643
+ scheduleHostScopeRetry(session, state) {
16644
+ if (this.shuttingDown || this.hostScopeCache.get(session) !== state || state.answer && !state.scopeBindPending || state.pending || state.bindPending || state.retryTimer || this.hostScopeRefusals.has(session) || !this.unscopedHostRecord(session))
16645
+ return;
16646
+ state.retryTimer = setTimeout(() => {
16647
+ state.retryTimer = undefined;
16648
+ const record = this.unscopedHostRecord(session);
16649
+ const client = record?.routeEntry?.client ?? record?.bgSub?.attachedClient;
16650
+ if (this.shuttingDown || this.hostScopeCache.get(session) !== state || !record || !client)
16651
+ return;
16652
+ let retry;
16653
+ if (state.scopeBindPending && state.answer?.result === "scope") {
16654
+ state.bindPending = this.retryHostScopeBind(client, record.identity, state, state.answer.scope).finally(() => {
16655
+ state.bindPending = undefined;
16656
+ this.scheduleHostScopeRetry(session, state);
16657
+ });
16658
+ retry = state.bindPending;
16659
+ } else {
16660
+ retry = this.hostScope(client, record.identity);
16661
+ }
16662
+ retry.catch((error) => {
16663
+ warn(`subc head scope: background retry failed code=${error instanceof Error ? error.name : "Error"}`);
16664
+ });
16665
+ }, Math.max(0, state.retryAt - Date.now()));
16666
+ state.retryTimer.unref();
16667
+ }
16668
+ deferHostScopeBind(session, state, code) {
16669
+ state.scopeBindPending = true;
16670
+ if (Date.now() >= state.retryAt) {
16671
+ state.retryAt = Date.now() + state.retryMs;
16672
+ state.retryMs = Math.min(state.retryMs * 2, HOST_SCOPE_RETRY_CAP_MS);
16673
+ }
16674
+ warn(`subc head scope: session=${session} scoped bind refused code=${code}; binding unscoped; retry in=${Math.max(0, state.retryAt - Date.now())}ms`);
16675
+ this.scheduleHostScopeRetry(session, state);
16676
+ }
16677
+ refuseHostScopeBind(session, state, code) {
16678
+ if (!this.hostScopeRefusals.has(session)) {
16679
+ this.hostScopeRefusals.add(session);
16680
+ warn(`subc head scope: session=${session} scoped bind refused code=${code}; binding unscoped; retry stopped`);
16681
+ }
16682
+ if (state) {
16683
+ state.scopeBindPending = false;
16684
+ this.cancelHostScopeRetry(state);
16685
+ }
16686
+ }
16687
+ async retryHostScopeBind(client, identity, state, scope) {
16688
+ try {
16689
+ const route = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, { consumerIdentity: this.consumerIdentity, scope });
16690
+ safeCloseRoute(client, route);
16691
+ if (this.hostScopeCache.get(identity.session) !== state || !state.scopeBindPending || this.shuttingDown || client !== this.client || !this.unscopedHostRecord(identity.session))
16692
+ return;
16693
+ state.scopeBindPending = false;
16694
+ state.retryAt = 0;
16695
+ state.retryMs = HOST_SCOPE_RETRY_FLOOR_MS;
16696
+ log(`subc head scope: session=${identity.session} scoped bind recovered ref=${scope.ref.slice(0, 8)} epoch=${scope.scopeEpoch}`);
16697
+ this.promoteHostScope(identity.session);
16698
+ } catch (error) {
16699
+ if (this.hostScopeCache.get(identity.session) !== state || !state.scopeBindPending || this.shuttingDown || !this.unscopedHostRecord(identity.session))
16700
+ return;
16701
+ if (error instanceof SubcError && error.code && !isSocketDeathError(error) && !isRouteOpenReloadWindowError(error) && !isHostScopeSyncRefusal(error.code)) {
16702
+ this.refuseHostScopeBind(identity.session, state, error.code);
16703
+ } else {
16704
+ this.deferHostScopeBind(identity.session, state, error instanceof SubcError ? error.code ?? error.name : "Error");
16705
+ }
16706
+ }
16707
+ }
16708
+ promoteHostScope(session) {
16709
+ for (const record of this.sessions.values()) {
16710
+ const entry = record.routeEntry;
16711
+ if (record.identity.session !== session)
16712
+ continue;
16713
+ if (entry?.handle && !entry.hostScope)
16714
+ this.retireHostToolRoute(record);
16715
+ const sub = record.bgSub;
16716
+ if (!sub?.attachedRoute)
16717
+ continue;
16718
+ sub.stop().then(() => {
16719
+ if (record.bgSub !== sub)
16720
+ return;
16721
+ record.bgSub = null;
16722
+ this.ensureBgSubscription(record.identity, record);
16723
+ });
16724
+ }
16725
+ }
16726
+ logHostScopeOutcome(session, outcome) {
16727
+ let key;
16728
+ let detail;
16729
+ switch (outcome.result) {
16730
+ case "scope":
16731
+ key = JSON.stringify([outcome.result, outcome.scope.ref, outcome.scope.scopeEpoch]);
16732
+ detail = `scope ref=${outcome.scope.ref.slice(0, 8)} epoch=${outcome.scope.scopeEpoch}`;
16733
+ break;
16734
+ case "error":
16735
+ key = JSON.stringify([outcome.result, outcome.code]);
16736
+ detail = `error code=${outcome.code} after=${outcome.afterMs}`;
16737
+ break;
16738
+ case "timeout":
16739
+ key = outcome.result;
16740
+ detail = `timeout after=${outcome.afterMs}`;
16741
+ break;
16742
+ default:
16743
+ key = outcome.result;
16744
+ detail = outcome.result;
16745
+ }
16746
+ if (this.hostScopeOutcomes.get(session) === key)
16747
+ return;
16748
+ this.hostScopeOutcomes.set(session, key);
16749
+ log(`subc head scope: session=${session} result=${detail}`);
16750
+ }
16751
+ async readHostScope(client, identity, signal) {
16752
+ const startedAt = Date.now();
16753
+ const timeout = () => ({ result: "timeout", afterMs: Date.now() - startedAt });
16754
+ let expired = false;
16755
+ let timer;
16756
+ const abort = new AbortController;
16757
+ let cancel;
16758
+ const cancelled = new Promise((resolve) => {
16759
+ cancel = () => {
16760
+ expired = true;
16761
+ abort.abort();
16762
+ resolve({ result: "error", code: "AbortError", afterMs: Date.now() - startedAt });
16763
+ };
16764
+ signal.addEventListener("abort", cancel, { once: true });
16765
+ });
16766
+ const read = async () => {
16767
+ let route;
16768
+ try {
16769
+ route = await client.routeOpen({ kind: "management_surface", module_id: "prefrontal-core" }, identity, { consumerIdentity: this.consumerIdentity });
16770
+ if (expired)
16771
+ return timeout();
16772
+ const envelope = await client.request(route, {
16773
+ method: "session.scope_identity",
16774
+ params: { harness: identity.harness, session: identity.session }
16775
+ }, { timeoutMs: HOST_SCOPE_LOOKUP_TIMEOUT_MS, signal: abort.signal });
16776
+ const reply = isRecord(envelope) ? envelope.result : undefined;
16777
+ if (isRecord(reply) && reply.answer === "no_scope")
16778
+ return { result: "no_scope" };
16779
+ if (!isRecord(reply) || !isRecord(reply.owner) || reply.owner.kind !== "reserved" || reply.owner.module_id !== "prefrontal-core" || typeof reply.ref !== "string" || reply.ref.length === 0 || !Number.isSafeInteger(reply.scope_epoch) || reply.scope_epoch < 0)
16780
+ return { result: "malformed" };
16781
+ return {
16782
+ result: "scope",
16783
+ scope: {
16784
+ owner: { kind: "reserved", module_id: "prefrontal-core" },
16785
+ ref: reply.ref,
16786
+ scopeEpoch: reply.scope_epoch
16787
+ }
16788
+ };
16789
+ } catch (error) {
16790
+ if (expired)
16791
+ return timeout();
16792
+ return {
16793
+ result: "error",
16794
+ code: error instanceof SubcError && error.code ? error.code : error instanceof Error ? error.name : "Error",
16795
+ afterMs: Date.now() - startedAt
16796
+ };
16797
+ } finally {
16798
+ if (route)
16799
+ safeCloseRoute(client, route);
16800
+ }
16801
+ };
16802
+ try {
16803
+ return await Promise.race([
16804
+ read(),
16805
+ cancelled,
16806
+ new Promise((resolve) => {
16807
+ timer = setTimeout(() => {
16808
+ expired = true;
16809
+ abort.abort();
16810
+ resolve(timeout());
16811
+ }, HOST_SCOPE_LOOKUP_TIMEOUT_MS);
16812
+ })
16813
+ ]);
16814
+ } finally {
16815
+ clearTimeout(timer);
16816
+ signal.removeEventListener("abort", cancel);
16817
+ }
16818
+ }
16819
+ openHostToolRoute(client, identity, record, entry) {
16820
+ if (!this.hostScopes || !["opencode", "pi"].includes(identity.harness) || identity.session === DEFAULT_SESSION_ID) {
16821
+ return client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, {
16822
+ consumerIdentity: this.consumerIdentity
16823
+ });
16824
+ }
16825
+ return this.openScopedHostToolRoute(client, identity, record, entry);
16826
+ }
16827
+ async openScopedHostToolRoute(client, identity, record, entry) {
16828
+ const scope = await this.hostScope(client, identity);
16829
+ this.assertRecordLive(record);
16830
+ if (scope) {
16831
+ try {
16832
+ const route = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, { consumerIdentity: this.consumerIdentity, scope });
16833
+ if (entry)
16834
+ entry.hostScope = scope;
16835
+ return route;
16836
+ } catch (error) {
16837
+ if (!(error instanceof SubcError) || !error.code || isSocketDeathError(error))
16838
+ throw error;
16839
+ if (isRouteOpenReloadWindowError(error))
16840
+ throw error;
16841
+ this.assertRecordLive(record);
16842
+ const state = this.hostScopeCache.get(identity.session);
16843
+ if (state && isHostScopeSyncRefusal(error.code)) {
16844
+ this.deferHostScopeBind(identity.session, state, error.code);
16845
+ } else {
16846
+ this.refuseHostScopeBind(identity.session, state, error.code);
16847
+ }
16848
+ }
16849
+ }
16850
+ this.assertRecordLive(record);
16851
+ const route = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, {
16852
+ consumerIdentity: this.consumerIdentity
16853
+ });
16854
+ if (this.hostScopeCache.get(identity.session)?.answer?.result === "scope" && !this.hostScopeCache.get(identity.session)?.scopeBindPending && !this.hostScopeRefusals.has(identity.session)) {
16855
+ safeCloseRoute(client, route);
16856
+ return this.openScopedHostToolRoute(client, identity, record, entry);
16857
+ }
16858
+ return route;
16859
+ }
16860
+ invalidateHostScope(session) {
16861
+ const state = this.hostScopeCache.get(session);
16862
+ if (state) {
16863
+ if (state.scopeBindPending)
16864
+ log(`subc head scope: session=${session} retry stopped reason=scope_route_closed`);
16865
+ this.cancelHostScopeRetry(state);
16866
+ }
16867
+ this.hostScopeCache.delete(session);
16868
+ this.hostScopeRefusals.delete(session);
16869
+ for (const record of this.sessions.values()) {
16870
+ if (record.identity.session !== session || !record.routeEntry)
16871
+ continue;
16872
+ this.retireHostToolRoute(record);
16873
+ }
16874
+ }
16875
+ onScopeRouteClose(push) {
16876
+ if (push.op !== "route.closed")
16877
+ return;
16878
+ const reason = routeCloseReason(push);
16879
+ if (!reason?.startsWith("scope_")) {
16880
+ for (const record of this.sessions.values()) {
16881
+ const route = record.routeEntry?.handle;
16882
+ if (!route)
16883
+ continue;
16884
+ if (Array.isArray(push.body.channels) && push.body.channels.includes(route.channel) || !Array.isArray(push.body.channels) && push.body.module_id === AFT_MODULE_ID)
16885
+ this.retireHostToolRoute(record);
16886
+ }
16887
+ return;
16888
+ }
16889
+ const sessions = new Set;
16890
+ for (const record of this.sessions.values()) {
16891
+ const routes = [record.routeEntry?.handle, record.bgSub?.attachedRoute].filter((route) => route != null);
16892
+ if (Array.isArray(push.body.channels) && routes.some((route) => push.body.channels.includes(route.channel)) || !Array.isArray(push.body.channels) && push.body.module_id === AFT_MODULE_ID)
16893
+ sessions.add(record.identity.session);
16894
+ }
16895
+ for (const session of sessions)
16896
+ this.invalidateHostScope(session);
16897
+ }
16285
16898
  ensureBgSubscription(identity, record) {
16286
16899
  if (this.shuttingDown || !this.onBgEventsNudge && !this.onBgEventsNudgeRef)
16287
16900
  return;
@@ -16321,17 +16934,28 @@ class SubcTransportPool {
16321
16934
  return;
16322
16935
  this.logNudgeDelivery("drop-no-compatible-handler", currentRecord, `nudge dropped cause=generation-provenance-unavailable-and-root-session-handler-unwired root=${currentRecord.canonicalRoot}`);
16323
16936
  };
16937
+ const binding = {};
16324
16938
  let sub = null;
16325
16939
  const clearDormantSubscription = () => {
16326
16940
  if (sub && record.bgSub === sub)
16327
16941
  record.bgSub = null;
16328
16942
  };
16329
- sub = new BgSubscription(identity, () => this.ensureClient(), (client, cause) => this.dropClient(client, cause), this.consumerIdentity, onNudge, this.bgBackoffSleep, () => this.rootCanAttach(record.canonicalRoot), (error) => {
16943
+ sub = new BgSubscription(identity, () => this.ensureClient(), (client, cause) => this.dropClient(client, cause), (client) => {
16944
+ binding.hostScope = undefined;
16945
+ return this.openHostToolRoute(client, identity, record, binding);
16946
+ }, onNudge, this.bgBackoffSleep, () => this.rootCanAttach(record.canonicalRoot), (error) => {
16330
16947
  if (!isAbsentRootRouteError(error))
16331
16948
  return false;
16332
16949
  this.markRootDormant(record.canonicalRoot, error);
16333
16950
  return true;
16334
- }, clearDormantSubscription, this.bgDispatchProbeIntervalMs, nudgeRef, () => this.isCurrentSession(record.identityKey, record) && this.isCurrentLiveGeneration(record.canonicalRoot, record.generation), (client) => this.noteClientAlive(client), (client, cause) => this.noteUnansweredCall(client, cause));
16951
+ }, clearDormantSubscription, this.bgDispatchProbeIntervalMs, nudgeRef, () => this.isCurrentSession(record.identityKey, record) && this.isCurrentLiveGeneration(record.canonicalRoot, record.generation), (client) => this.noteClientAlive(client), (client, cause) => this.noteUnansweredCall(client, cause), () => {
16952
+ const state = this.hostScopeCache.get(identity.session);
16953
+ if (state)
16954
+ this.scheduleHostScopeRetry(identity.session, state);
16955
+ if (state?.answer?.result === "scope" && !state.scopeBindPending && !binding.hostScope && !this.hostScopeRefusals.has(identity.session)) {
16956
+ this.promoteHostScope(identity.session);
16957
+ }
16958
+ });
16335
16959
  record.bgSub = sub;
16336
16960
  if (!this.rootCanAttach(record.canonicalRoot)) {
16337
16961
  record.bgSub = null;
@@ -16367,6 +16991,8 @@ class SubcTransportPool {
16367
16991
  record.routeEntry = null;
16368
16992
  this.deleteSessionIfEmpty(record.identityKey, record);
16369
16993
  }
16994
+ for (const session of this.hostScopeCache.keys())
16995
+ this.stopUnusedHostScopeRetry(session);
16370
16996
  this.transportFailures = 0;
16371
16997
  try {
16372
16998
  client.close();
@@ -16504,6 +17130,13 @@ class SubcTransportPool {
16504
17130
  this.generationRejections.clear();
16505
17131
  this.observedStarts.clear();
16506
17132
  this.dormantRoots.clear();
17133
+ const scopeLookups = [...this.hostScopeCache.values()].flatMap((state) => {
17134
+ this.cancelHostScopeRetry(state);
17135
+ return state.pending ? [state.pending] : [];
17136
+ });
17137
+ this.hostScopeCache.clear();
17138
+ this.hostScopeRefusals.clear();
17139
+ this.hostScopeOutcomes.clear();
16507
17140
  const registration = this.lifecycleRegistration;
16508
17141
  registration?.deregister();
16509
17142
  this.lifecycleRegistration = null;
@@ -16514,11 +17147,16 @@ class SubcTransportPool {
16514
17147
  detached.push(owner);
16515
17148
  }
16516
17149
  this.rootIndex.clear();
17150
+ this.sessionIndex.clear();
16517
17151
  this.transports.clear();
16518
17152
  const client = this.client;
16519
17153
  this.client = null;
16520
17154
  const pending = [...this.pendingRootCleanups];
16521
- await Promise.allSettled([...detached.map((owner) => this.cleanupDetached(owner)), ...pending]);
17155
+ await Promise.allSettled([
17156
+ ...detached.map((owner) => this.cleanupDetached(owner)),
17157
+ ...pending,
17158
+ ...scopeLookups
17159
+ ]);
16522
17160
  if (client) {
16523
17161
  try {
16524
17162
  client.close();
@@ -16532,6 +17170,13 @@ class SubcTransportPool {
16532
17170
  session: session && session.length > 0 ? session : DEFAULT_SESSION_ID
16533
17171
  };
16534
17172
  const owner = this.detachSession(identityKey(identity), "session_closed");
17173
+ if (!this.sessionIndex.has(identity.session)) {
17174
+ const state = this.hostScopeCache.get(identity.session);
17175
+ if (state)
17176
+ this.cancelHostScopeRetry(state);
17177
+ this.hostScopeCache.delete(identity.session);
17178
+ this.hostScopeRefusals.delete(identity.session);
17179
+ }
16535
17180
  if (owner)
16536
17181
  await this.cleanupDetached(owner);
16537
17182
  }
@@ -16572,6 +17217,8 @@ var BASH_TRANSPORT_DISPOSITION = "The transport to the AFT daemon was interrupte
16572
17217
  var SUBC_MODULE_RESTART_DISPOSITION = "The AFT daemon module restarted while this call was in flight, so its outcome is UNKNOWN: it may or may not have executed. Verify actual state before re-running, and never blind-retry a mutation.";
16573
17218
  var BRIDGE_TRANSPORT_UNKNOWN_OUTCOME_DISPOSITION = "The standalone AFT transport failed after this call may have been sent, so its outcome is UNKNOWN: it may or may not have executed. Verify actual state before re-running, and never blind-retry a mutation.";
16574
17219
  var SUBC_ROUTE_CLOSED_MID_CALL_DISPOSITION = "The plugin closed this call's route while the call was in flight, so its outcome is UNKNOWN: it may or may not have executed. Verify actual state before re-running, and never blind-retry a mutation.";
17220
+ var BASH_FRAME_READ_TIMEOUT_DISPOSITION = "The command's state is UNKNOWN: the transport timed out while reading the response frame, so it may or may not have executed. Check bash_status if a task ID could have been assigned, and do not re-run side-effecting commands blindly.";
17221
+ var SUBC_FRAME_READ_TIMEOUT_DISPOSITION = "The transport timed out while reading the response frame for this call, so its outcome is UNKNOWN: it may or may not have executed. Verify actual state before re-running, and never blind-retry a mutation.";
16575
17222
  function isRouteClosedMidCallError(error) {
16576
17223
  return error instanceof SubcRouteClosedMidCallError || isLocalRouteCloseRejection(error);
16577
17224
  }
@@ -16633,6 +17280,8 @@ function classifyBashHostFallbackError(error) {
16633
17280
  return;
16634
17281
  if (isRouteClosedMidCallError(error))
16635
17282
  return;
17283
+ if (isSubcFrameReadTimeoutError(error))
17284
+ return;
16636
17285
  if (error instanceof SubcTransportShuttingDownError)
16637
17286
  return "transport down";
16638
17287
  if (isSubcRouteTornDownError(error))
@@ -16656,6 +17305,14 @@ function isTransportClassError(error) {
16656
17305
  function adaptToolError(command, error) {
16657
17306
  if (!(error instanceof Error))
16658
17307
  return error;
17308
+ if (isSubcFrameReadTimeoutError(error)) {
17309
+ const typedError = error instanceof SubcFrameReadTimeoutError ? error : new SubcFrameReadTimeoutError(error);
17310
+ const disposition = command === "bash" ? BASH_FRAME_READ_TIMEOUT_DISPOSITION : SUBC_FRAME_READ_TIMEOUT_DISPOSITION;
17311
+ if (typedError.message.includes(disposition))
17312
+ return typedError;
17313
+ typedError.message = typedError.message ? `${typedError.message} ${disposition}` : disposition;
17314
+ return typedError;
17315
+ }
16659
17316
  if (error instanceof BridgeTransportUnknownOutcomeError) {
16660
17317
  if (error.message.includes(BRIDGE_TRANSPORT_UNKNOWN_OUTCOME_DISPOSITION))
16661
17318
  return error;
@@ -16737,7 +17394,12 @@ var RETIRED_PATHS = [
16737
17394
  ["semantic_search", "indexes.semantic"],
16738
17395
  ["experimental_semantic_search", "indexes.semantic"],
16739
17396
  ["callgraph_store", "indexes.callgraph"],
16740
- ["github.enabled", "github.read,github.write,github.shim"]
17397
+ ["github.enabled", "github.read,github.write,github.shim"],
17398
+ ["experimental_lsp_ty", "experimental.lsp_ty"],
17399
+ ["experimental_bash_rewrite", "bash.rewrite"],
17400
+ ["experimental_bash_compress", "bash.compress"],
17401
+ ["experimental_bash_background", "bash.background"],
17402
+ ["experimental.bash", "bash"]
16741
17403
  ];
16742
17404
  var INDEX_INPUTS = [
16743
17405
  ["trigram", "search_index", "experimental_search_index"],
@@ -16875,10 +17537,68 @@ function translateInspectLspPaths(map, prefix, out) {
16875
17537
  recordRetired(out, prefix, `inspect.${leaf}`);
16876
17538
  }
16877
17539
  }
17540
+ function translateExperimentalPaths(map, prefix, out) {
17541
+ for (const [oldKey, leaf] of [
17542
+ ["experimental_lsp_ty", "lsp_ty"],
17543
+ ["experimental_bash_rewrite", "rewrite"],
17544
+ ["experimental_bash_compress", "compress"],
17545
+ ["experimental_bash_background", "background"]
17546
+ ]) {
17547
+ if (!Object.hasOwn(map, oldKey))
17548
+ continue;
17549
+ recordRetired(out, prefix, oldKey);
17550
+ const value = map[oldKey];
17551
+ delete map[oldKey];
17552
+ if (!isRecord2(map.experimental))
17553
+ map.experimental = {};
17554
+ const experimental = map.experimental;
17555
+ let destination = experimental;
17556
+ if (leaf !== "lsp_ty") {
17557
+ if (!isRecord2(experimental.bash))
17558
+ experimental.bash = {};
17559
+ destination = experimental.bash;
17560
+ }
17561
+ if (Object.hasOwn(destination, leaf)) {
17562
+ superseded(out, oldKey, `${oldKey} is ignored because experimental.${leaf === "lsp_ty" ? leaf : `bash.${leaf}`} is already set`);
17563
+ } else {
17564
+ destination[leaf] = value;
17565
+ }
17566
+ }
17567
+ if (!isRecord2(map.experimental) || !Object.hasOwn(map.experimental, "bash"))
17568
+ return;
17569
+ const experimental = map.experimental;
17570
+ const legacy = experimental.bash;
17571
+ if (isRecord2(legacy)) {
17572
+ if (!["rewrite", "compress", "background"].some((leaf) => Object.hasOwn(legacy, leaf)))
17573
+ return;
17574
+ for (const leaf of Object.keys(legacy))
17575
+ recordRetired(out, prefix, `experimental.bash.${leaf}`);
17576
+ if (Object.hasOwn(map, "bash")) {
17577
+ superseded(out, "experimental.bash", 'experimental.bash is ignored because top-level "bash" is already set');
17578
+ } else {
17579
+ const bash = {
17580
+ rewrite: legacy.rewrite === true,
17581
+ compress: legacy.compress === true,
17582
+ background: legacy.background === true
17583
+ };
17584
+ for (const leaf of ["long_running_reminder_enabled", "long_running_reminder_interval_ms"]) {
17585
+ if (Object.hasOwn(legacy, leaf))
17586
+ bash[leaf] = legacy[leaf];
17587
+ }
17588
+ map.bash = bash;
17589
+ }
17590
+ } else {
17591
+ recordRetired(out, prefix, "experimental.bash");
17592
+ }
17593
+ delete experimental.bash;
17594
+ if (Object.keys(experimental).length === 0)
17595
+ delete map.experimental;
17596
+ }
16878
17597
  function translateBlock(map, userBase, blockLabel, out) {
16879
17598
  const prefix = blockLabel === "base" ? "" : `${blockLabel}.`;
16880
17599
  translateGithubAliases(map, prefix, out);
16881
17600
  translateInspectLspPaths(map, prefix, out);
17601
+ translateExperimentalPaths(map, prefix, out);
16882
17602
  let explicitList;
16883
17603
  const rawList = map.disabled_tools;
16884
17604
  if (Array.isArray(rawList) && rawList.every((entry) => typeof entry === "string")) {
@@ -16891,6 +17611,8 @@ function translateBlock(map, userBase, blockLabel, out) {
16891
17611
  });
16892
17612
  }
16893
17613
  for (const [path] of RETIRED_PATHS) {
17614
+ if (path === "experimental.bash")
17615
+ continue;
16894
17616
  if (hasPath(map, path))
16895
17617
  recordRetired(out, prefix, path);
16896
17618
  }
@@ -17349,11 +18071,13 @@ class BridgePool {
17349
18071
  return alive;
17350
18072
  }
17351
18073
  getBridge(projectRoot) {
18074
+ return this.getBridgeForCanonicalRoot(asCanonicalRootPath(normalizeKey(projectRoot)));
18075
+ }
18076
+ getBridgeForCanonicalRoot(key) {
17352
18077
  if (this.shutdownCalled) {
17353
18078
  this.shutdownCalled = false;
17354
18079
  this.startCleanupTimer();
17355
18080
  }
17356
- const key = normalizeKey(projectRoot);
17357
18081
  const existing = this.bridges.get(key);
17358
18082
  if (existing) {
17359
18083
  existing.lastUsed = Date.now();
@@ -17559,7 +18283,9 @@ class RevivableTransportPool {
17559
18283
  this.activePool = initialPool;
17560
18284
  }
17561
18285
  getBridge(projectRoot) {
17562
- const key = canonicalizeProjectRoot(projectRoot);
18286
+ return this.getBridgeForCanonicalRoot(asCanonicalRootPath(canonicalizeProjectRoot(projectRoot)));
18287
+ }
18288
+ getBridgeForCanonicalRoot(key) {
17563
18289
  let transport = this.transports.get(key);
17564
18290
  if (!transport) {
17565
18291
  transport = new RevivableProjectTransport(this, key);
@@ -17594,7 +18320,7 @@ class RevivableTransportPool {
17594
18320
  return transport;
17595
18321
  }
17596
18322
  async toolCall(projectRoot, runtime, name, rawArgs = {}, options) {
17597
- return this.getBridge(projectRoot).toolCall(runtime.sessionID, name, rawArgs, options);
18323
+ return this.toolCallForCanonicalRoot(asCanonicalRootPath(canonicalizeProjectRoot(projectRoot)), runtime.sessionID, name, rawArgs, options);
17598
18324
  }
17599
18325
  setConfigureOverride(key, value) {
17600
18326
  if (key === "edit_slot_survives") {
@@ -17734,14 +18460,22 @@ class RevivableTransportPool {
17734
18460
  return this.activePool?.getActiveBridgeForRoot(projectRoot) ?? null;
17735
18461
  }
17736
18462
  async send(projectRoot, command, params, options) {
18463
+ return this.sendForCanonicalRoot(asCanonicalRootPath(canonicalizeProjectRoot(projectRoot)), command, params, options);
18464
+ }
18465
+ async sendForCanonicalRoot(projectRoot, command, params, options) {
18466
+ this.getBridgeForCanonicalRoot(projectRoot);
17737
18467
  const pool = await this.ensureActivePool();
17738
- const bridge = pool.getBridge(projectRoot);
18468
+ const bridge = pool.getBridgeForCanonicalRoot?.(projectRoot) ?? pool.getBridge(projectRoot);
17739
18469
  this.transports.get(projectRoot)?.refreshStatusSubscription(bridge);
17740
18470
  return bridge.send(command, params, options);
17741
18471
  }
17742
18472
  async toolCallOnProject(projectRoot, sessionId, name, rawArgs, options) {
18473
+ return this.toolCallForCanonicalRoot(asCanonicalRootPath(canonicalizeProjectRoot(projectRoot)), sessionId, name, rawArgs, options);
18474
+ }
18475
+ async toolCallForCanonicalRoot(projectRoot, sessionId, name, rawArgs, options) {
18476
+ this.getBridgeForCanonicalRoot(projectRoot);
17743
18477
  const pool = await this.ensureActivePool();
17744
- const bridge = pool.getBridge(projectRoot);
18478
+ const bridge = pool.getBridgeForCanonicalRoot?.(projectRoot) ?? pool.getBridge(projectRoot);
17745
18479
  this.transports.get(projectRoot)?.refreshStatusSubscription(bridge);
17746
18480
  return bridge.toolCall(sessionId, name, rawArgs, options);
17747
18481
  }
@@ -17819,7 +18553,7 @@ function satisfyLifecycleDemand(wrapper, projectRoot) {
17819
18553
  const activePool = wrapper.activePool;
17820
18554
  if (activePool.isShutdown() || !(activePool instanceof SubcTransportPool))
17821
18555
  return;
17822
- activePool.getBridge(projectRoot);
18556
+ activePool.getBridgeForCanonicalRoot(asCanonicalRootPath(projectRoot));
17823
18557
  }
17824
18558
  async function createAftTransportPool(opts) {
17825
18559
  let binaryPath = opts.binaryPath;
@@ -17854,8 +18588,8 @@ async function createAftTransportPool(opts) {
17854
18588
  });
17855
18589
  if (reapingEnabled) {
17856
18590
  const owner = wrapper;
17857
- const getBridge = owner.getBridge.bind(owner);
17858
- owner.getBridge = (projectRoot) => {
18591
+ const getBridge = owner.getBridgeForCanonicalRoot.bind(owner);
18592
+ owner.getBridgeForCanonicalRoot = (projectRoot) => {
17859
18593
  satisfyLifecycleDemand(owner, projectRoot);
17860
18594
  return getBridge(projectRoot);
17861
18595
  };
@@ -17881,6 +18615,7 @@ async function createConcreteAftTransportPool(opts) {
17881
18615
  throw new Error(missing);
17882
18616
  }
17883
18617
  return new SubcTransportPool({
18618
+ hostScopes: opts.harness === "opencode" || opts.harness === "pi",
17884
18619
  connectionFile,
17885
18620
  harness: opts.harness,
17886
18621
  consumerIdentity: opts.subcConsumerIdentity,
@@ -17952,7 +18687,7 @@ import { homedir as homedir11, tmpdir as tmpdir3 } from "node:os";
17952
18687
  import { basename as basename2, dirname as dirname6, join as join12, resolve as resolve8 } from "node:path";
17953
18688
 
17954
18689
  // ../aft-bridge/dist/paths.js
17955
- import { existsSync as existsSync5, readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync2 } from "node:fs";
18690
+ import { existsSync as existsSync5, readFileSync as readFileSync5, realpathSync as realpathSync3, renameSync as renameSync3, writeFileSync as writeFileSync2 } from "node:fs";
17956
18691
  import { homedir as homedir9 } from "node:os";
17957
18692
  import { dirname as dirname4, isAbsolute as isAbsolute5, join as join10, resolve as resolve7 } from "node:path";
17958
18693
  function homeDir3() {
@@ -17987,6 +18722,23 @@ function resolveCortexKitUserConfigPath() {
17987
18722
  function resolveCortexKitProjectConfigPath(projectDirectory) {
17988
18723
  return join10(projectDirectory, ".cortexkit", "aft.jsonc");
17989
18724
  }
18725
+ function resolveProjectConfigReadPath(projectDirectory, operatingHarness) {
18726
+ const canonical = resolveCortexKitProjectConfigPath(projectDirectory);
18727
+ if (existsSync5(canonical))
18728
+ return canonical;
18729
+ const sources = resolveLegacyAftConfigSources(projectDirectory).project;
18730
+ const ordered = operatingHarness ? [
18731
+ ...sources.filter((source) => source.harness === operatingHarness),
18732
+ ...sources.filter((source) => source.harness !== operatingHarness)
18733
+ ] : sources;
18734
+ return ordered.find((source) => existsSync5(source.path))?.path ?? canonical;
18735
+ }
18736
+ function legacyProjectConfigLocationNotice(projectDirectory, path) {
18737
+ const canonical = resolveCortexKitProjectConfigPath(projectDirectory);
18738
+ if (path === canonical)
18739
+ return null;
18740
+ return `Legacy project config ${path} is read in place; move it to ${canonical} yourself, and use aft doctor --fix to update retired keys.`;
18741
+ }
17990
18742
  function resolveCortexKitConfigPaths(projectDirectory) {
17991
18743
  return {
17992
18744
  userConfigPath: resolveCortexKitUserConfigPath(),
@@ -17994,15 +18746,27 @@ function resolveCortexKitConfigPaths(projectDirectory) {
17994
18746
  };
17995
18747
  }
17996
18748
  function resolveLegacyAftConfigSources(projectDirectory) {
18749
+ const project = [
18750
+ ...legacySources(join10(projectDirectory, ".opencode", "aft"), "OpenCode project", "opencode"),
18751
+ ...legacySources(join10(projectDirectory, ".pi", "aft"), "Pi project", "pi")
18752
+ ];
18753
+ const identity = (path) => {
18754
+ try {
18755
+ return realpathSync3(path);
18756
+ } catch {
18757
+ return resolve7(path);
18758
+ }
18759
+ };
18760
+ const protectedPaths = new Set([
18761
+ resolveCortexKitProjectConfigPath(projectDirectory),
18762
+ ...project.map((source) => source.path)
18763
+ ].map(identity));
17997
18764
  return {
17998
18765
  user: [
17999
18766
  ...legacySources(join10(legacyOpenCodeConfigDir(), "aft"), "OpenCode user", "opencode"),
18000
18767
  ...legacySources(join10(legacyPiAgentDir(), "aft"), "Pi user", "pi")
18001
- ],
18002
- project: [
18003
- ...legacySources(join10(projectDirectory, ".opencode", "aft"), "OpenCode project", "opencode"),
18004
- ...legacySources(join10(projectDirectory, ".pi", "aft"), "Pi project", "pi")
18005
- ]
18768
+ ].filter((source) => !protectedPaths.has(identity(source.path))),
18769
+ project
18006
18770
  };
18007
18771
  }
18008
18772
  function resolveHarnessStoragePath(storageRoot, harness, ...segments) {
@@ -19187,7 +19951,7 @@ function npmSpawnEnv(resolved, baseEnv = process.env, platform = process.platfor
19187
19951
  return withPathPrepended(baseEnv, resolved.binDir, platform);
19188
19952
  }
19189
19953
  // ../aft-bridge/dist/onnx-probe.js
19190
- import { closeSync as closeSync7, existsSync as existsSync8, openSync as openSync6, readSync as readSync5, realpathSync as realpathSync3 } from "node:fs";
19954
+ import { closeSync as closeSync7, existsSync as existsSync8, openSync as openSync6, readSync as readSync5, realpathSync as realpathSync4 } from "node:fs";
19191
19955
  var MH_MAGIC = 4277009102;
19192
19956
  var MH_MAGIC_64 = 4277009103;
19193
19957
  var FAT_MAGIC = 3405691582;
@@ -19274,7 +20038,7 @@ function isUncheckableDependency(path) {
19274
20038
  function probeOnnxRuntimeLoadable(libPath, arch = process.arch) {
19275
20039
  let fd;
19276
20040
  try {
19277
- fd = openSync6(realpathSync3(libPath), "r");
20041
+ fd = openSync6(realpathSync4(libPath), "r");
19278
20042
  } catch (err) {
19279
20043
  return {
19280
20044
  loadable: false,
@@ -19313,7 +20077,7 @@ function probeOnnxRuntimeLoadable(libPath, arch = process.arch) {
19313
20077
  }
19314
20078
  // ../aft-bridge/dist/onnx-runtime.js
19315
20079
  import { randomBytes as randomBytes2 } from "node:crypto";
19316
- import { chmodSync as chmodSync2, closeSync as closeSync8, copyFileSync as copyFileSync3, createWriteStream as createWriteStream2, existsSync as existsSync10, lstatSync as lstatSync2, openSync as openSync7, readdirSync as readdirSync3, readFileSync as readFileSync9, readlinkSync, realpathSync as realpathSync4, renameSync as renameSync6, rmSync as rmSync3, statSync as statSync9, symlinkSync, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "node:fs";
20080
+ import { chmodSync as chmodSync2, closeSync as closeSync8, copyFileSync as copyFileSync3, createWriteStream as createWriteStream2, existsSync as existsSync10, lstatSync as lstatSync2, openSync as openSync7, readdirSync as readdirSync3, readFileSync as readFileSync9, readlinkSync, realpathSync as realpathSync5, renameSync as renameSync6, rmSync as rmSync3, statSync as statSync9, symlinkSync, unlinkSync as unlinkSync4, writeFileSync as writeFileSync5 } from "node:fs";
19317
20081
  import { basename as basename3, dirname as dirname9, isAbsolute as isAbsolute8, join as join16, relative as relative4, resolve as resolve10, win32 as win322 } from "node:path";
19318
20082
  import { Readable as Readable2 } from "node:stream";
19319
20083
  import { pipeline as pipeline2 } from "node:stream/promises";
@@ -19594,7 +20358,7 @@ function detectOnnxVersion(libDir, libName) {
19594
20358
  try {
19595
20359
  const entries = readdirSync3(libDir);
19596
20360
  try {
19597
- const resolved = parseOnnxVersionFromPath(realpathSync4(join16(libDir, libName)));
20361
+ const resolved = parseOnnxVersionFromPath(realpathSync5(join16(libDir, libName)));
19598
20362
  if (resolved && resolved !== INVALID_ORT_VERSION)
19599
20363
  return resolved;
19600
20364
  } catch {}
@@ -19611,7 +20375,7 @@ function detectOnnxVersion(libDir, libName) {
19611
20375
  const base = join16(libDir, libName);
19612
20376
  if (existsSync10(base)) {
19613
20377
  try {
19614
- const real = realpathSync4(base);
20378
+ const real = realpathSync5(base);
19615
20379
  const version = parseOnnxVersionFromPath(real) ?? parseOnnxVersionFromDirectoryPath(real);
19616
20380
  if (version)
19617
20381
  return version;
@@ -19803,7 +20567,7 @@ async function downloadFileWithCap(url, destPath) {
19803
20567
  }
19804
20568
  }
19805
20569
  function validateExtractedTree(stagingRoot) {
19806
- const realRoot = realpathSync4(stagingRoot);
20570
+ const realRoot = realpathSync5(stagingRoot);
19807
20571
  let totalBytes = 0;
19808
20572
  const walk = (dir) => {
19809
20573
  const entries = readdirSync3(dir);
@@ -19952,7 +20716,7 @@ function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, c
19952
20716
  log(`ORT extract: failed to copy optional ${libFile}: ${copyErr}`);
19953
20717
  }
19954
20718
  }
19955
- const targetRoot = realpathSync4(targetDir);
20719
+ const targetRoot = realpathSync5(targetDir);
19956
20720
  for (const link of symlinks) {
19957
20721
  const dst = join16(targetDir, link.name);
19958
20722
  try {
@@ -21156,7 +21920,7 @@ async function handlePushedBgLongRunning(drainContext, reminder) {
21156
21920
  async function appendToolResultBgCompletions(drainContext, content) {
21157
21921
  const state = stateFor(drainContext.sessionID);
21158
21922
  if (state.outstandingTaskIds.size === 0 && state.pendingCompletions.length === 0 && state.pendingLongRunning.length === 0 && state.pendingPatternMatches.length === 0) {
21159
- if (!idleDrainMayFindWork(state))
21923
+ if (!idleDrainMayFindWork(state, drainContext))
21160
21924
  return;
21161
21925
  await drainCompletions(drainContext);
21162
21926
  }
@@ -21288,7 +22052,8 @@ async function withBgHopTimeout(operation, label) {
21288
22052
  }
21289
22053
  async function triggerWakeIfPending(drainContext, skipDrain, includeDeferredCompletions = true, forceDrain = false) {
21290
22054
  const state = stateFor(drainContext.sessionID);
21291
- if (!skipDrain && (forceDrain || state.outstandingTaskIds.size > 0 || !state.forcedDrainCompleted)) {
22055
+ const hasTrackedOrPendingWork = state.outstandingTaskIds.size > 0 || state.pendingCompletions.length > 0 || state.pendingLongRunning.length > 0 || state.pendingPatternMatches.length > 0;
22056
+ if (!skipDrain && (forceDrain || state.outstandingTaskIds.size > 0 || !state.forcedDrainCompleted) && (forceDrain || hasTrackedOrPendingWork || idleDrainMayFindWork(state, drainContext))) {
21292
22057
  await drainCompletions(drainContext);
21293
22058
  }
21294
22059
  routeExplicitControlCompletions(state);
@@ -21409,8 +22174,13 @@ function bridgeForDrain(drainContext) {
21409
22174
  function isTerminalNudgeError(error) {
21410
22175
  return error instanceof Error && error.code === "root_generation_expired";
21411
22176
  }
21412
- function idleDrainMayFindWork(state) {
21413
- return !state.forcedDrainCompleted || state.untrackedCompletionSinceDrain || state.deliveredAwaitingAckTaskIds.size > 0;
22177
+ function idleDrainMayFindWork(state, drainContext) {
22178
+ if (state.untrackedCompletionSinceDrain || state.deliveredAwaitingAckTaskIds.size > 0) {
22179
+ return true;
22180
+ }
22181
+ if (state.forcedDrainCompleted)
22182
+ return false;
22183
+ return Boolean(drainContext.resolvedBridge ?? drainContext.ctx.pool.getActiveBridgeForRoot(drainContext.directory));
21414
22184
  }
21415
22185
  async function drainCompletions(drainContext) {
21416
22186
  const state = stateFor(drainContext.sessionID);
@@ -21840,7 +22610,7 @@ import {
21840
22610
  // package.json
21841
22611
  var package_default = {
21842
22612
  name: "@cortexkit/aft-pi",
21843
- version: "0.59.0",
22613
+ version: "0.60.0",
21844
22614
  type: "module",
21845
22615
  description: "Pi coding agent extension for Agent File Tools (AFT) — tree-sitter and LSP-powered code analysis",
21846
22616
  main: "dist/index.js",
@@ -21864,18 +22634,18 @@ var package_default = {
21864
22634
  "test:e2e": "bun test src/__tests__/e2e/"
21865
22635
  },
21866
22636
  dependencies: {
21867
- "@cortexkit/aft-bridge": "0.59.0",
22637
+ "@cortexkit/aft-bridge": "0.60.0",
21868
22638
  "comment-json": "^5.0.0",
21869
22639
  diff: "^8.0.4",
21870
22640
  zod: "^4.4.3"
21871
22641
  },
21872
22642
  optionalDependencies: {
21873
- "@cortexkit/aft-darwin-arm64": "0.59.0",
21874
- "@cortexkit/aft-darwin-x64": "0.59.0",
21875
- "@cortexkit/aft-linux-arm64": "0.59.0",
21876
- "@cortexkit/aft-linux-x64": "0.59.0",
21877
- "@cortexkit/aft-win32-arm64": "0.59.0",
21878
- "@cortexkit/aft-win32-x64": "0.59.0"
22643
+ "@cortexkit/aft-darwin-arm64": "0.60.0",
22644
+ "@cortexkit/aft-darwin-x64": "0.60.0",
22645
+ "@cortexkit/aft-linux-arm64": "0.60.0",
22646
+ "@cortexkit/aft-linux-x64": "0.60.0",
22647
+ "@cortexkit/aft-win32-arm64": "0.60.0",
22648
+ "@cortexkit/aft-win32-x64": "0.60.0"
21879
22649
  },
21880
22650
  devDependencies: {
21881
22651
  "@earendil-works/pi-coding-agent": "^0.80.2",
@@ -22613,7 +23383,7 @@ function registerStatusCommand(pi, ctx) {
22613
23383
  }
22614
23384
 
22615
23385
  // src/config.ts
22616
- import { existsSync as existsSync12, readFileSync as readFileSync10, renameSync as renameSync7, unlinkSync as unlinkSync5, writeFileSync as writeFileSync6 } from "node:fs";
23386
+ import { existsSync as existsSync12, readFileSync as readFileSync10 } from "node:fs";
22617
23387
  import { homedir as homedir14 } from "node:os";
22618
23388
  import { isAbsolute as isAbsolute9 } from "node:path";
22619
23389
  var import_comment_json = __toESM(require_src2(), 1);
@@ -27723,154 +28493,6 @@ function applyActiveHarnessOverride(config, trusted) {
27723
28493
  merged.indexes = indexes;
27724
28494
  return merged;
27725
28495
  }
27726
- var CONFIG_MIGRATIONS = [
27727
- { oldKey: "experimental_lsp_ty", newPath: ["experimental", "lsp_ty"] },
27728
- { oldKey: "experimental_bash_rewrite", newPath: ["experimental", "bash", "rewrite"] },
27729
- { oldKey: "experimental_bash_compress", newPath: ["experimental", "bash", "compress"] },
27730
- { oldKey: "experimental_bash_background", newPath: ["experimental", "bash", "background"] }
27731
- ];
27732
- function isWritableMigrationError(errorValue) {
27733
- const code = errorValue?.code;
27734
- return code === "EROFS" || code === "EACCES" || code === "EPERM";
27735
- }
27736
- function extractCommentsForPreservation(content) {
27737
- const comments = [];
27738
- const linePattern = /\/\/[^\n]*/g;
27739
- for (const match of content.match(linePattern) ?? []) {
27740
- comments.push(match.trim());
27741
- }
27742
- const blockPattern = /\/\*[\s\S]*?\*\//g;
27743
- for (const match of content.match(blockPattern) ?? []) {
27744
- comments.push(match.replace(/\s+/g, " ").trim());
27745
- }
27746
- return comments;
27747
- }
27748
- function ensureRecordAtPath(root, path) {
27749
- let current = root;
27750
- for (const segment of path) {
27751
- const existing = current[segment];
27752
- if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
27753
- current[segment] = {};
27754
- }
27755
- current = current[segment];
27756
- }
27757
- return current;
27758
- }
27759
- function hasPath2(root, path) {
27760
- let current = root;
27761
- for (const segment of path) {
27762
- if (!current || typeof current !== "object" || Array.isArray(current))
27763
- return false;
27764
- const record2 = current;
27765
- if (!Object.hasOwn(record2, segment))
27766
- return false;
27767
- current = record2[segment];
27768
- }
27769
- return true;
27770
- }
27771
- function setPath(root, path, value) {
27772
- const parent = ensureRecordAtPath(root, path.slice(0, -1));
27773
- parent[path[path.length - 1]] = value;
27774
- }
27775
- function migrateRawConfig(rawConfig, configPath, logger) {
27776
- const oldKeys = [];
27777
- for (const migration of CONFIG_MIGRATIONS) {
27778
- if (!Object.hasOwn(rawConfig, migration.oldKey))
27779
- continue;
27780
- if (hasPath2(rawConfig, migration.newPath)) {
27781
- logger?.warn(`Config migration conflict at ${configPath}: ${migration.oldKey} ignored because ${migration.newPath.join(".")} is already set`);
27782
- } else {
27783
- setPath(rawConfig, migration.newPath, rawConfig[migration.oldKey]);
27784
- }
27785
- delete rawConfig[migration.oldKey];
27786
- oldKeys.push(migration.oldKey);
27787
- }
27788
- oldKeys.push(...migrateExperimentalBashBlock(rawConfig, configPath, logger));
27789
- return oldKeys;
27790
- }
27791
- function migrateExperimentalBashBlock(rawConfig, configPath, logger) {
27792
- const experimental = rawConfig.experimental;
27793
- if (typeof experimental !== "object" || experimental === null || Array.isArray(experimental)) {
27794
- return [];
27795
- }
27796
- const expRecord = experimental;
27797
- if (!Object.hasOwn(expRecord, "bash"))
27798
- return [];
27799
- const legacyBash = expRecord.bash;
27800
- if (typeof legacyBash !== "object" || legacyBash === null || Array.isArray(legacyBash)) {
27801
- delete expRecord.bash;
27802
- if (Object.keys(expRecord).length === 0)
27803
- delete rawConfig.experimental;
27804
- return ["experimental.bash"];
27805
- }
27806
- const bashRecord = legacyBash;
27807
- const hasFeatureFlag = "rewrite" in bashRecord || "compress" in bashRecord || "background" in bashRecord;
27808
- if (!hasFeatureFlag)
27809
- return [];
27810
- const movedKeys = Object.keys(bashRecord).map((k) => `experimental.bash.${k}`);
27811
- if (Object.hasOwn(rawConfig, "bash")) {
27812
- logger?.warn(`Config migration conflict at ${configPath}: experimental.bash dropped because top-level "bash" is already set`);
27813
- } else {
27814
- const migrated = {
27815
- rewrite: bashRecord.rewrite === true,
27816
- compress: bashRecord.compress === true,
27817
- background: bashRecord.background === true
27818
- };
27819
- if (bashRecord.long_running_reminder_enabled !== undefined) {
27820
- migrated.long_running_reminder_enabled = bashRecord.long_running_reminder_enabled;
27821
- }
27822
- if (bashRecord.long_running_reminder_interval_ms !== undefined) {
27823
- migrated.long_running_reminder_interval_ms = bashRecord.long_running_reminder_interval_ms;
27824
- }
27825
- rawConfig.bash = migrated;
27826
- }
27827
- delete expRecord.bash;
27828
- if (Object.keys(expRecord).length === 0) {
27829
- delete rawConfig.experimental;
27830
- }
27831
- return movedKeys;
27832
- }
27833
- function migrateAftConfigFile2(configPath, logger = { log: log2, warn: warn2 }) {
27834
- if (!existsSync12(configPath)) {
27835
- return { migrated: false, oldKeys: [] };
27836
- }
27837
- let tmpPath = null;
27838
- let oldKeys = [];
27839
- try {
27840
- const content = readFileSync10(configPath, "utf-8");
27841
- const rawConfig = import_comment_json.parse(content);
27842
- if (!rawConfig || typeof rawConfig !== "object" || Array.isArray(rawConfig)) {
27843
- return { migrated: false, oldKeys: [] };
27844
- }
27845
- oldKeys = migrateRawConfig(rawConfig, configPath, logger);
27846
- if (oldKeys.length === 0) {
27847
- return { migrated: false, oldKeys: [] };
27848
- }
27849
- const serialized = `${import_comment_json.stringify(rawConfig, null, 2)}
27850
- `;
27851
- const preservedComments = extractCommentsForPreservation(content).filter((comment) => !serialized.includes(comment.trim()));
27852
- const nextContent = preservedComments.length > 0 ? `${preservedComments.join(`
27853
- `)}
27854
- ${serialized}` : serialized;
27855
- tmpPath = `${configPath}.tmp.${process.pid}`;
27856
- writeFileSync6(tmpPath, nextContent, "utf-8");
27857
- renameSync7(tmpPath, configPath);
27858
- logger.log(`Migrated config at ${configPath}: removed ${oldKeys.join(", ")}`);
27859
- return { migrated: true, oldKeys };
27860
- } catch (err) {
27861
- if (tmpPath) {
27862
- try {
27863
- unlinkSync5(tmpPath);
27864
- } catch {}
27865
- }
27866
- if (isWritableMigrationError(err)) {
27867
- const errorMsg = err instanceof Error ? err.message : String(err);
27868
- logger.warn(`Config migration could not write ${configPath} (${errorMsg}); using migrated config in memory`);
27869
- return { migrated: oldKeys.length > 0, oldKeys };
27870
- }
27871
- return { migrated: false, oldKeys: [] };
27872
- }
27873
- }
27874
28496
  var configLoadErrors = [];
27875
28497
  var configValidationErrors = [];
27876
28498
  var configLoadSources = [];
@@ -27919,7 +28541,6 @@ function loadConfigFromPath(configPath, tier) {
27919
28541
  recordConfigParseFailure(configPath, "root must be an object");
27920
28542
  return null;
27921
28543
  }
27922
- migrateRawConfig(rawConfig, configPath, { log: log2, warn: warn2 });
27923
28544
  cleanConfig = stripJsoncSymbols(rawConfig);
27924
28545
  } catch (err) {
27925
28546
  const errorMsg = err instanceof Error ? err.message : String(err);
@@ -28293,21 +28914,14 @@ function migrateAftConfigLocations(projectDirectory, logger = { log: log2, warn:
28293
28914
  legacySources: legacy.user,
28294
28915
  operatingHarness: "pi",
28295
28916
  logger
28296
- }),
28297
- migrateAftConfigFile({
28298
- scope: "project",
28299
- targetPath: paths.projectConfigPath,
28300
- legacySources: legacy.project,
28301
- operatingHarness: "pi",
28302
- logger
28303
28917
  })
28304
28918
  ];
28305
28919
  }
28306
28920
  function resolveAftConfigPaths(projectDirectory) {
28307
- const paths = resolveCortexKitConfigPaths(projectDirectory);
28308
- migrateAftConfigFile2(paths.userConfigPath);
28309
- migrateAftConfigFile2(paths.projectConfigPath);
28310
- return paths;
28921
+ return {
28922
+ ...resolveCortexKitConfigPaths(projectDirectory),
28923
+ projectConfigPath: resolveProjectConfigReadPath(projectDirectory, ACTIVE_HARNESS)
28924
+ };
28311
28925
  }
28312
28926
  function buildConfigTierConfigureParams(projectDirectory, processState = {}) {
28313
28927
  const paths = resolveAftConfigPaths(projectDirectory);
@@ -28329,6 +28943,15 @@ function loadAftConfig(projectDirectory) {
28329
28943
  configLoadNotices = [];
28330
28944
  semanticInputSupplied = false;
28331
28945
  const { userConfigPath, projectConfigPath } = resolveAftConfigPaths(projectDirectory);
28946
+ const locationNotice = legacyProjectConfigLocationNotice(projectDirectory, projectConfigPath);
28947
+ if (locationNotice) {
28948
+ log2(locationNotice);
28949
+ configLoadNotices.push({
28950
+ configPath: projectConfigPath,
28951
+ digest: "legacy-project-config-location",
28952
+ message: locationNotice
28953
+ });
28954
+ }
28332
28955
  let config = loadConfigFromPath(userConfigPath, "user") ?? {
28333
28956
  disabled_tools: [...DEFAULT_DISABLED_TOOLS]
28334
28957
  };
@@ -28371,7 +28994,7 @@ function loadAftConfig(projectDirectory) {
28371
28994
  }
28372
28995
 
28373
28996
  // src/lsp-auto-install.ts
28374
- import { existsSync as existsSync14, mkdirSync as mkdirSync6, renameSync as renameSync9, rmSync as rmSync5, statSync as statSync11, writeFileSync as writeFileSync8 } from "node:fs";
28997
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6, renameSync as renameSync8, rmSync as rmSync5, statSync as statSync11, writeFileSync as writeFileSync7 } from "node:fs";
28375
28998
  import { join as join19 } from "node:path";
28376
28999
 
28377
29000
  // src/lsp-cache.ts
@@ -28381,11 +29004,11 @@ import {
28381
29004
  mkdirSync as mkdirSync5,
28382
29005
  openSync as openSync8,
28383
29006
  readFileSync as readFileSync11,
28384
- renameSync as renameSync8,
29007
+ renameSync as renameSync7,
28385
29008
  rmSync as rmSync4,
28386
29009
  statSync as statSync10,
28387
- unlinkSync as unlinkSync6,
28388
- writeFileSync as writeFileSync7
29010
+ unlinkSync as unlinkSync5,
29011
+ writeFileSync as writeFileSync6
28389
29012
  } from "node:fs";
28390
29013
  import { join as join17 } from "node:path";
28391
29014
  function aftCacheBase() {
@@ -28426,7 +29049,7 @@ function writeInstalledMetaIn(installDir, version, sha256) {
28426
29049
  installedAt: new Date().toISOString(),
28427
29050
  ...sha256 ? { sha256 } : {}
28428
29051
  };
28429
- writeFileSync7(join17(installDir, INSTALLED_META_FILE), JSON.stringify(meta), "utf8");
29052
+ writeFileSync6(join17(installDir, INSTALLED_META_FILE), JSON.stringify(meta), "utf8");
28430
29053
  } catch (err) {
28431
29054
  log2(`[lsp-cache] failed to write installed-meta in ${installDir}: ${err}`);
28432
29055
  }
@@ -28467,7 +29090,7 @@ function acquireInstallLock(lockKey) {
28467
29090
  try {
28468
29091
  const fd = openSync8(lock, "wx");
28469
29092
  try {
28470
- writeFileSync7(fd, `${process.pid}
29093
+ writeFileSync6(fd, `${process.pid}
28471
29094
  ${new Date().toISOString()}
28472
29095
  ${token}
28473
29096
  `);
@@ -28523,7 +29146,7 @@ ${token}
28523
29146
  }
28524
29147
  log2(`[lsp] reclaiming install lock for ${lockKey} (owner_pid=${owningPid ?? "unknown"}, alive=${ownerAlive}, age_ms=${age})`);
28525
29148
  try {
28526
- unlinkSync6(lock);
29149
+ unlinkSync5(lock);
28527
29150
  } catch {}
28528
29151
  return tryClaim();
28529
29152
  }
@@ -28562,7 +29185,7 @@ function releaseInstallLock(lockKey, acquisitionToken) {
28562
29185
  return;
28563
29186
  }
28564
29187
  try {
28565
- unlinkSync6(lock);
29188
+ unlinkSync5(lock);
28566
29189
  } catch (unlinkErr) {
28567
29190
  const code = unlinkErr.code;
28568
29191
  if (code !== "ENOENT") {
@@ -28637,7 +29260,7 @@ function claimLspAutoInstallPass() {
28637
29260
  try {
28638
29261
  const fd = openSync8(lock, "wx");
28639
29262
  try {
28640
- writeFileSync7(fd, JSON.stringify(owner));
29263
+ writeFileSync6(fd, JSON.stringify(owner));
28641
29264
  } finally {
28642
29265
  closeSync9(fd);
28643
29266
  }
@@ -28668,12 +29291,12 @@ function claimLspAutoInstallPass() {
28668
29291
  return null;
28669
29292
  }
28670
29293
  const tempPath = `${stampPath}.tmp-${process.pid}-${owner.token}`;
28671
- writeFileSync7(tempPath, `${JSON.stringify({ lastRunMs: Date.now() })}
29294
+ writeFileSync6(tempPath, `${JSON.stringify({ lastRunMs: Date.now() })}
28672
29295
  `, {
28673
29296
  encoding: "utf8",
28674
29297
  mode: 384
28675
29298
  });
28676
- renameSync8(tempPath, stampPath);
29299
+ renameSync7(tempPath, stampPath);
28677
29300
  return { release };
28678
29301
  } catch (err) {
28679
29302
  if (claimed) {
@@ -28710,7 +29333,7 @@ function writeVersionCheck(npmPackage, latest) {
28710
29333
  last_checked: new Date().toISOString(),
28711
29334
  latest_eligible: latest
28712
29335
  };
28713
- writeFileSync7(file, JSON.stringify(record, null, 2));
29336
+ writeFileSync6(file, JSON.stringify(record, null, 2));
28714
29337
  }
28715
29338
  function shouldRecheckVersion(record, weeklyCheckIntervalMs = 7 * 24 * 60 * 60 * 1000) {
28716
29339
  if (!record)
@@ -29091,7 +29714,7 @@ function ensureInstallAnchor(cwd) {
29091
29714
  try {
29092
29715
  const stub = join19(cwd, "package.json");
29093
29716
  if (!existsSync14(stub)) {
29094
- writeFileSync8(stub, `${JSON.stringify({ name: "aft-lsp-cache", version: "0.0.0", private: true })}
29717
+ writeFileSync7(stub, `${JSON.stringify({ name: "aft-lsp-cache", version: "0.0.0", private: true })}
29095
29718
  `);
29096
29719
  }
29097
29720
  } catch (err) {
@@ -29304,7 +29927,7 @@ function quarantineCachedNpmInstall(spec, reason) {
29304
29927
  try {
29305
29928
  mkdirSync6(join19(dest, ".."), { recursive: true });
29306
29929
  rmSync5(dest, { recursive: true, force: true });
29307
- renameSync9(packageDir, dest);
29930
+ renameSync8(packageDir, dest);
29308
29931
  } catch (err) {
29309
29932
  warn2(`[lsp] tofu_mismatch ${spec.npm}: failed to quarantine cache entry: ${err}`);
29310
29933
  }
@@ -29407,12 +30030,12 @@ import {
29407
30030
  readFileSync as readFileSync13,
29408
30031
  readlinkSync as readlinkSync2,
29409
30032
  readSync as readSync6,
29410
- realpathSync as realpathSync5,
29411
- renameSync as renameSync10,
30033
+ realpathSync as realpathSync6,
30034
+ renameSync as renameSync9,
29412
30035
  rmSync as rmSync6,
29413
30036
  statSync as statSync12,
29414
- unlinkSync as unlinkSync7,
29415
- writeFileSync as writeFileSync9
30037
+ unlinkSync as unlinkSync6,
30038
+ writeFileSync as writeFileSync8
29416
30039
  } from "node:fs";
29417
30040
  import { dirname as dirname10, join as join20, relative as relative5, resolve as resolve11 } from "node:path";
29418
30041
  import { Readable as Readable3 } from "node:stream";
@@ -29538,9 +30161,9 @@ function writeInstallFailureMarker(spec, release, sha256, installError) {
29538
30161
  const tempPath = `${path}.tmp-${process.pid}-${randomBytes3(6).toString("hex")}`;
29539
30162
  try {
29540
30163
  mkdirSync7(dirname10(path), { recursive: true });
29541
- writeFileSync9(tempPath, `${JSON.stringify({ release, sha256, error: markerError, at: new Date().toISOString() })}
30164
+ writeFileSync8(tempPath, `${JSON.stringify({ release, sha256, error: markerError, at: new Date().toISOString() })}
29542
30165
  `, { encoding: "utf8", mode: 384 });
29543
- renameSync10(tempPath, path);
30166
+ renameSync9(tempPath, path);
29544
30167
  } catch (err) {
29545
30168
  try {
29546
30169
  rmSync6(tempPath, { force: true });
@@ -29606,7 +30229,7 @@ function writeGithubInstalledMetaIn(installDir, version, binarySha256, archiveSh
29606
30229
  binarySha256,
29607
30230
  ...archiveSha256 ? { archiveSha256 } : {}
29608
30231
  };
29609
- writeFileSync9(join20(installDir, INSTALLED_META_FILE2), JSON.stringify(meta), "utf8");
30232
+ writeFileSync8(join20(installDir, INSTALLED_META_FILE2), JSON.stringify(meta), "utf8");
29610
30233
  } catch (err) {
29611
30234
  warn2(`[lsp] failed to write github installed metadata in ${installDir}: ${err}`);
29612
30235
  }
@@ -29846,7 +30469,7 @@ async function downloadFile(url, destPath, fetchImpl, assetSize, signal) {
29846
30469
  await pipeline3(nodeStream, createWriteStream3(destPath), { signal: timeout.signal });
29847
30470
  } catch (err) {
29848
30471
  try {
29849
- unlinkSync7(destPath);
30472
+ unlinkSync6(destPath);
29850
30473
  } catch {}
29851
30474
  throw err;
29852
30475
  } finally {
@@ -29875,7 +30498,7 @@ async function fetchFollowingRedirects(url, fetchImpl, signal) {
29875
30498
  throw new Error(`too many redirects (>${maxRedirects})`);
29876
30499
  }
29877
30500
  function validateExtraction(stagingRoot) {
29878
- const realStagingRoot = realpathSync5(stagingRoot);
30501
+ const realStagingRoot = realpathSync6(stagingRoot);
29879
30502
  let totalBytes = 0;
29880
30503
  const walk = (dir) => {
29881
30504
  let entries;
@@ -29901,7 +30524,7 @@ function validateExtraction(stagingRoot) {
29901
30524
  }
29902
30525
  let realFull;
29903
30526
  try {
29904
- realFull = realpathSync5(full);
30527
+ realFull = realpathSync6(full);
29905
30528
  } catch (err) {
29906
30529
  throw new Error(`failed to realpath ${full}: ${err}`);
29907
30530
  }
@@ -29929,14 +30552,30 @@ function assertArchiveEntryPath(entry) {
29929
30552
  throw new Error(`archive entry escapes archive root: ${entry}`);
29930
30553
  }
29931
30554
  }
30555
+ var ARCHIVE_LIST_TIMEOUT_MS = 30000;
29932
30556
  function runArchiveTool(command, args, timeout) {
29933
30557
  return new Promise((resolvePromise, reject) => {
29934
- execFile(command, args, { encoding: "utf8", ...timeout !== undefined ? { timeout } : {} }, (err, stdout) => {
29935
- if (err)
29936
- reject(err);
29937
- else
29938
- resolvePromise(stdout);
29939
- });
30558
+ let child;
30559
+ const deadline = setTimeout(() => {
30560
+ reject(new Error(`${command} ${args[0]} timed out after ${timeout} ms`));
30561
+ child?.kill("SIGKILL");
30562
+ child?.stdin?.destroy();
30563
+ child?.stdout?.destroy();
30564
+ child?.stderr?.destroy();
30565
+ }, timeout);
30566
+ try {
30567
+ child = execFile(command, args, { encoding: "utf8", timeout, killSignal: "SIGKILL" }, (err, stdout) => {
30568
+ clearTimeout(deadline);
30569
+ if (err)
30570
+ reject(err);
30571
+ else
30572
+ resolvePromise(stdout);
30573
+ });
30574
+ child.stdin?.end();
30575
+ } catch (error) {
30576
+ clearTimeout(deadline);
30577
+ reject(error);
30578
+ }
29940
30579
  });
29941
30580
  }
29942
30581
  async function runTarExtraction(args, timeout) {
@@ -29950,7 +30589,7 @@ async function runTarExtraction(args, timeout) {
29950
30589
  }
29951
30590
  async function precheckWithTar(archivePath, command) {
29952
30591
  let totalBytes = 0;
29953
- const verbose = await runArchiveTool(command, ["-tvf", archivePath]);
30592
+ const verbose = await runArchiveTool(command, ["-tvf", archivePath], ARCHIVE_LIST_TIMEOUT_MS);
29954
30593
  for (const line of verbose.split(`
29955
30594
  `)) {
29956
30595
  if (line.startsWith("h")) {
@@ -29963,7 +30602,7 @@ async function precheckWithTar(archivePath, command) {
29963
30602
  totalBytes += Math.max(...numeric);
29964
30603
  }
29965
30604
  }
29966
- const names = await runArchiveTool(command, ["-tf", archivePath]);
30605
+ const names = await runArchiveTool(command, ["-tf", archivePath], ARCHIVE_LIST_TIMEOUT_MS);
29967
30606
  for (const entry of names.split(`
29968
30607
  `)) {
29969
30608
  if (entry.length > 0)
@@ -29974,11 +30613,11 @@ async function precheckWithTar(archivePath, command) {
29974
30613
  async function precheckArchiveContents(archivePath, archiveType, zipListingMode = "platform") {
29975
30614
  let totalBytes = 0;
29976
30615
  if (archiveType === "zip" && zipListingMode === "platform" && process.platform !== "win32") {
29977
- const out = await runArchiveTool("unzip", ["-l", archivePath]);
30616
+ const out = await runArchiveTool("unzip", ["-l", archivePath], ARCHIVE_LIST_TIMEOUT_MS);
29978
30617
  const match = out.match(/^\s*(\d+)\s+\d+\s+files?\s*$/m);
29979
30618
  if (match)
29980
30619
  totalBytes = Number.parseInt(match[1] ?? "0", 10);
29981
- const names = await runArchiveTool("unzip", ["-Z1", archivePath]);
30620
+ const names = await runArchiveTool("unzip", ["-Z1", archivePath], ARCHIVE_LIST_TIMEOUT_MS);
29982
30621
  for (const entry of names.split(`
29983
30622
  `)) {
29984
30623
  if (entry.length > 0)
@@ -30005,7 +30644,7 @@ async function extractArchiveSafely(archivePath, destDir, archiveType) {
30005
30644
  try {
30006
30645
  rmSync6(destDir, { recursive: true, force: true });
30007
30646
  } catch {}
30008
- renameSync10(stagingDir, destDir);
30647
+ renameSync9(stagingDir, destDir);
30009
30648
  } catch (err) {
30010
30649
  try {
30011
30650
  rmSync6(stagingDir, { recursive: true, force: true });
@@ -30020,7 +30659,7 @@ function quarantineCachedGithubInstall(spec, reason) {
30020
30659
  try {
30021
30660
  mkdirSync7(dirname10(dest), { recursive: true });
30022
30661
  rmSync6(dest, { recursive: true, force: true });
30023
- renameSync10(packageDir, dest);
30662
+ renameSync9(packageDir, dest);
30024
30663
  } catch (err) {
30025
30664
  warn2(`[lsp] tofu_mismatch ${spec.id}: failed to quarantine cache entry: ${err}`);
30026
30665
  }
@@ -30112,7 +30751,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
30112
30751
  const message = `hash failed: ${err instanceof Error ? err.message : String(err)}`;
30113
30752
  error2(`[lsp] hash ${spec.id} failed: ${err}`);
30114
30753
  try {
30115
- unlinkSync7(archivePath);
30754
+ unlinkSync6(archivePath);
30116
30755
  } catch {}
30117
30756
  return { ok: false, sha256: "", error: message };
30118
30757
  }
@@ -30123,7 +30762,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
30123
30762
  const message = `TOFU sha256 mismatch: previously installed archive sha256=${previousArchiveSha256}, ` + `downloaded sha256=${archiveSha256}`;
30124
30763
  error2(`[lsp] ${spec.id} ${tag}: ${message}. ` + `This means the published release for tag ${tag} changed. Investigate before proceeding.`);
30125
30764
  try {
30126
- unlinkSync7(archivePath);
30765
+ unlinkSync6(archivePath);
30127
30766
  } catch {}
30128
30767
  return { ok: false, sha256: archiveSha256, error: message };
30129
30768
  }
@@ -30135,7 +30774,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
30135
30774
  return { ok: false, sha256: archiveSha256, error: message };
30136
30775
  } finally {
30137
30776
  try {
30138
- unlinkSync7(archivePath);
30777
+ unlinkSync6(archivePath);
30139
30778
  } catch {}
30140
30779
  }
30141
30780
  const innerBinaryPath = join20(extractDir, spec.binaryPathInArchive(platform, arch, version));
@@ -31571,11 +32210,6 @@ PDFs aren't supported on the Pi harness yet.`, response);
31571
32210
  restrictToProjectRoot: surface.restrictToProjectRoot
31572
32211
  });
31573
32212
  }
31574
- const preview = await callToolCallForSession(bridge, "edit", rawArgs, sessionId, extCtx, {
31575
- preview: true
31576
- });
31577
- if (preview.success === false)
31578
- throw toolErrorFromResponse("edit", preview);
31579
32213
  const response = await callToolCallForSession(bridge, "edit", rawArgs, sessionId, extCtx);
31580
32214
  if (response.success === false)
31581
32215
  throw toolErrorFromResponse("edit", response);
@@ -31718,7 +32352,7 @@ function buildMutationResult(response) {
31718
32352
  let text = response.text;
31719
32353
  if (typeof text !== "string") {
31720
32354
  text = formatEditSummary(response);
31721
- if (noOp) {
32355
+ if (noOp && response.rolled_back !== true) {
31722
32356
  text += `
31723
32357
 
31724
32358
  Note: no net file change — the match was found and applied, but the file content is byte-identical to before. Likely causes: oldString and newString are identical, or a formatter normalized the change away.`;
@@ -32190,7 +32824,7 @@ var BashBackgroundTimeoutParam = {
32190
32824
  };
32191
32825
  var BashWaitParam = {
32192
32826
  wait: Type5.Optional(Type5.Boolean({
32193
- description: "When true, run in the foreground without auto-promoting and wait until the command finishes or reaches its timeout (in a delegated session at most the worker wait limit, 30 minutes by default; the command then keeps running in the background and the reply says how to keep waiting); any new message detaches by default, while `bash.detach_on_user_message: false` keeps it blocking unless the message contains the literal `&detach`. The token is stripped before delivery; the rest of the message is preserved, and a token-only message becomes `(requested background detach)`. Use only when you know the result is required before doing anything else."
32827
+ description: "When true, run in the foreground without auto-promoting and wait until the command finishes or reaches its timeout (at most bash.worker_wait_max_ms, 30 minutes by default; the command then keeps running in the background and the reply says how to keep waiting); any new message detaches by default, while `bash.detach_on_user_message: false` keeps it blocking unless the message contains the literal `&detach`. The token is stripped before delivery; the rest of the message is preserved, and a token-only message becomes `(requested background detach)`. Use only when you know the result is required before doing anything else."
32194
32828
  }))
32195
32829
  };
32196
32830
  var BashSandboxParam = {
@@ -32453,7 +33087,7 @@ function registerBashTool(pi, ctx, aftSearchRegistered = false, registeredName =
32453
33087
  "PTY mode"
32454
33088
  ].join(", ") : bashCfg.compress ? "workdir and compressed output" : "workdir";
32455
33089
  const detachSentence = bashCfg.detach_on_user_message ? "Any new message detaches this wait. Set `bash.detach_on_user_message: false` to keep it blocking; even then, a message containing the literal `&detach` forces detachment, and the token is stripped before delivery; the rest of the message is preserved, while a token-only message becomes `(requested background detach)`." : "Because `bash.detach_on_user_message` is false, a new message leaves this wait blocking; include the literal `&detach` anywhere to force detachment, and the token is stripped before delivery; the rest of the message is preserved, while a token-only message becomes `(requested background detach)`.";
32456
- const tasksSentence = bashCfg.background ? ` Commands run in the foreground and return inline; \`wait: true\` blocks until a long command finishes instead of auto-promoting (in a delegated session it blocks up to the worker wait limit, \`bash.worker_wait_max_ms\`, 30 minutes by default, then reports the command is still running; watch again to keep waiting); ${detachSentence} Use it when you need the result before doing anything else; keep it off otherwise so auto-promote can remind you while you work. Use \`background: true\` yourself ONLY when you have other useful work to do while it runs; ${backgroundWaitSentence(companions)} A \`nohup … &\` launch still holds the call if the child keeps stdout/stderr; redirect both or use background:true. \`pty: true\` runs interactive programs (REPLs, TUIs), implies background${ptyDriveClause(companions)}.` : " Commands run in the foreground to completion; `timeout` is the hard kill cap (default 30 minutes).";
33090
+ const tasksSentence = bashCfg.background ? ` Commands run in the foreground and return inline; \`wait: true\` blocks until a long command finishes instead of auto-promoting (it blocks up to \`bash.worker_wait_max_ms\`, 30 minutes by default, then reports the command is still running; watch again to keep waiting); ${detachSentence} Use it when you need the result before doing anything else; keep it off otherwise so auto-promote can remind you while you work. Use \`background: true\` yourself ONLY when you have other useful work to do while it runs; ${backgroundWaitSentence(companions)} A \`nohup … &\` launch still holds the call if the child keeps stdout/stderr; redirect both or use background:true. \`pty: true\` runs interactive programs (REPLs, TUIs), implies background${ptyDriveClause(companions)}.` : " Commands run in the foreground to completion; `timeout` is the hard kill cap (default 30 minutes).";
32457
33091
  const remoteRuns = () => !isPowerShell && remoteRunsOffered(ctx.config);
32458
33092
  pi.registerTool({
32459
33093
  name: registeredName,
@@ -32509,7 +33143,7 @@ DO NOT use bash for code search or code exploration. If you are about to run gre
32509
33143
  const blockToCompletion = backgroundDisabled || requestedWait || workerForcedForeground;
32510
33144
  const effectiveBackground = !blockToCompletion && (rawRequestedBackground || requestedPty);
32511
33145
  const isWorker = isPiWorkerSession(extCtx);
32512
- const workerWaitMaxMs = isWorker ? bashCfg.worker_wait_max_ms : undefined;
33146
+ const workerWaitMaxMs = bashCfg.worker_wait_max_ms;
32513
33147
  let spawnContext = {
32514
33148
  command: params.command,
32515
33149
  cwd: params.workdir
@@ -33064,7 +33698,10 @@ async function formatBashStatus(extCtx, taskId, details, requestedOutputMode, wa
33064
33698
  if (details.live_descendants_summary) {
33065
33699
  text += ` · ${details.live_descendants_summary}`;
33066
33700
  }
33067
- if (details.output_incomplete) {
33701
+ if (details.remote_phase_note && !details.waited) {
33702
+ text += ` · ${details.remote_phase_note}`;
33703
+ }
33704
+ if (details.output_incomplete || details.status === "failed" && details.status_reason) {
33068
33705
  text += `
33069
33706
  [${details.status_reason || "PTY output may be incomplete"}]`;
33070
33707
  }
@@ -33570,10 +34207,16 @@ import { Type as Type9 } from "typebox";
33570
34207
  var INSPECT_TRANSPORT_HEADROOM_MS = 30000;
33571
34208
  var InspectParams = Type9.Object({
33572
34209
  sections: Type9.Optional(Type9.Union([Type9.String(), Type9.Array(Type9.String())], {
33573
- description: "Categories to include in detailed drill-down (e.g. 'todos' or ['todos', 'dead_code', 'cycles']). Use 'all' for every active category. Omit for summary-only mode. `sections` changes detail, not the categories verified."
34210
+ description: "Categories to include in detailed drill-down (e.g. 'todos' or ['todos', 'dead_code', 'cycles']). Use 'all' for every active category. Omit for summary-only mode. With scope, listing sections without 'diagnostics' (or 'all') skips diagnostics work; omitting sections keeps diagnostics. Other categories are verified regardless of sections."
33574
34211
  })),
33575
34212
  scope: Type9.Optional(Type9.Union([Type9.String(), Type9.Array(Type9.String())], {
33576
- description: "Restrict returned results to paths under this scope (one path, or an array of paths — not a space-separated list; file or directory; absolute or relative to project root). `scope=` narrows results and limits Rust LSP startup to owning Cargo workspaces; it does not trigger per-file diagnostic collection. Scoped files no producer has authoritatively analyzed are reported as named gaps (complete: false), never as a clean empty result."
34213
+ description: "Restrict returned results to paths under this scope (one path, or an array of paths — not a space-separated list; file or directory; absolute or relative to project root). `scope=` narrows results. Scoped requests collect diagnostics for the scoped files and report coverage gaps, unless sections lists other categories and leaves out 'diagnostics'."
34214
+ })),
34215
+ offset: Type9.Optional(Type9.Integer({
34216
+ minimum: 0,
34217
+ maximum: Number.MAX_SAFE_INTEGER,
34218
+ default: 0,
34219
+ description: "Rows to skip independently in each drill-down list. Default 0; follow next_offset in each list envelope."
33577
34220
  })),
33578
34221
  topK: Type9.Optional(Type9.Integer({
33579
34222
  minimum: 1,
@@ -33885,7 +34528,7 @@ function registerInspectTool(pi, ctx) {
33885
34528
  pi.registerTool({
33886
34529
  name: "aft_inspect",
33887
34530
  label: "inspect",
33888
- description: "Codebase health inspection that waits for current analysis. FRESH means the reported analysis is current. PARTIAL means some diagnostics are unknown; the header names the analyzer, reason, and retry guidance. INTERRUPTED means the request stopped without a fresh snapshot; retry aft_inspect. PHASE-FAILED means inspection could not finish; address the reported reason and retry, or narrow the scope. `sections` selects drill-down detail, not the categories verified. Categories switched off in `inspect.categories` are not computed or refreshed, render as off, and never make the header PARTIAL.\n\n" + "Use `scope=` to narrow all findings, counts, and examples to those paths. Cross-boundary duplicates are labeled as groups touching the scope. Scope also limits Rust analyzer startup to the Cargo workspaces owning those paths. Files without an authoritative diagnostic report remain named gaps (complete: false), not a clean result.\n\n" + `Use when: starting work on unfamiliar code, after multi-edit batches to check diagnostics, before a refactor, before review, or to verify cleanup completeness.
34531
+ description: "Codebase health inspection that waits for current analysis. FRESH means the reported analysis is current. PARTIAL means some diagnostics are unknown; the header names the analyzer, reason, and retry guidance. INTERRUPTED means the request stopped without a fresh snapshot; retry aft_inspect. PHASE-FAILED means inspection could not finish; address the reported reason and retry, or narrow the scope. `sections` selects drill-down detail. Scoped requests collect diagnostics when sections is omitted or includes 'diagnostics' or 'all'; sections that leave diagnostics out skip diagnostics work. Unscoped requests keep warm diagnostics. Categories switched off in `inspect.categories` are not computed or refreshed, render as off, and never make the header PARTIAL.\n\n" + "Use `scope=` to narrow all findings, counts, and examples to those paths. Cross-boundary duplicates are labeled as groups touching the scope. Scope also limits Rust analyzer startup to the Cargo workspaces owning those paths when diagnostics are requested. Files without an authoritative diagnostic report remain named gaps (complete: false), not a clean result.\n\n" + `Use when: starting work on unfamiliar code, after multi-edit batches to check diagnostics, before a refactor, before review, or to verify cleanup completeness.
33889
34532
 
33890
34533
  ` + "Treat `dead_code` as a hint, not proof: reachability is call-based, so symbols reached only via method dispatch or referenced only in type position may be false positives — verify before deleting.\n\n" + "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.",
33891
34534
  parameters: InspectParams,
@@ -33901,6 +34544,8 @@ function registerInspectTool(pi, ctx) {
33901
34544
  rawArgs.scope = scope;
33902
34545
  if (topK !== undefined)
33903
34546
  rawArgs.topK = topK;
34547
+ if (params.offset !== undefined)
34548
+ rawArgs.offset = params.offset;
33904
34549
  const response = await callToolCall(bridge, "inspect", rawArgs, extCtx, {
33905
34550
  transportTimeoutMs: resolveInspectDiagnosticsTimeoutMs(ctx.config) + INSPECT_TRANSPORT_HEADROOM_MS
33906
34551
  });
@@ -35115,6 +35760,7 @@ function startPiLiveConfigReload(options) {
35115
35760
  const { userConfigPath, projectConfigPath } = resolveCortexKitConfigPaths(options.directory);
35116
35761
  return startLiveConfigReload({
35117
35762
  paths: [userConfigPath, projectConfigPath],
35763
+ projectPath: projectConfigPath,
35118
35764
  load: () => loadPiConfigForLiveReload(options.directory),
35119
35765
  initialSources: options.initialSources,
35120
35766
  initialProjectText: options.initialSourceTexts?.[projectConfigPath] ?? null,
@@ -35411,11 +36057,11 @@ var PLUGIN_VERSION = (() => {
35411
36057
  return "0.0.0";
35412
36058
  }
35413
36059
  })();
35414
- var ANNOUNCEMENT_VERSION = "0.59.0";
36060
+ var ANNOUNCEMENT_VERSION = "0.60.0";
35415
36061
  var ANNOUNCEMENT_FEATURES = [
35416
- "Config keys retired in 0.58 keep working: AFT updates your user config file to the current keys (keeping a backup) and translates a project's in memory; `npx @cortexkit/aft doctor --fix` updates a project file.",
35417
- "New: live pull-request diffs with `read` on `pr://N/diff`, a `pattern` argument for `aft_search`, and per-category switches for `aft_inspect`.",
35418
- "Fixes for issues reported on OpenCode 2, Pi and OMP, and much less repeated indexing and disk work."
36062
+ "Call graphs stay current cheaply: a one-file edit updates a project's graph in seconds instead of minutes, and linked worktrees reuse their main checkout's graph until they change.",
36063
+ "`aft_inspect` dead-code and unused-export results are more accurate on Rust, Go and TypeScript, and inspect uses far less memory on large projects.",
36064
+ "OpenCode 2 gets native `shell` and `patch` tools, and stopped subagents are no longer woken by their own background commands."
35419
36065
  ];
35420
36066
  var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/DSa65w8wuf";
35421
36067
  var pendingEagerWarnings = new Map;