@cortexkit/aft-opencode 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.
Files changed (45) hide show
  1. package/dist/bg-notifications.d.ts +5 -0
  2. package/dist/bg-notifications.d.ts.map +1 -1
  3. package/dist/bridge-bootstrap.d.ts +3 -3
  4. package/dist/bridge-bootstrap.d.ts.map +1 -1
  5. package/dist/config-live-reload.d.ts.map +1 -1
  6. package/dist/config.d.ts +0 -4
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/entry/server.js +1402 -536
  9. package/dist/index.js +1288 -376
  10. package/dist/lsp-github-install.d.ts +7 -1
  11. package/dist/lsp-github-install.d.ts.map +1 -1
  12. package/dist/shared/status.d.ts +9 -4
  13. package/dist/shared/status.d.ts.map +1 -1
  14. package/dist/tool-registration.d.ts +3 -0
  15. package/dist/tool-registration.d.ts.map +1 -1
  16. package/dist/tools/_shared.d.ts +2 -5
  17. package/dist/tools/_shared.d.ts.map +1 -1
  18. package/dist/tools/bash.d.ts.map +1 -1
  19. package/dist/tools/bash_watch.d.ts.map +1 -1
  20. package/dist/tools/definitions/v2.d.ts +9 -1
  21. package/dist/tools/definitions/v2.d.ts.map +1 -1
  22. package/dist/tools/hoisted.d.ts.map +1 -1
  23. package/dist/tools/inspect.d.ts.map +1 -1
  24. package/dist/tui/v2-status.d.ts.map +1 -1
  25. package/dist/types.d.ts +7 -1
  26. package/dist/types.d.ts.map +1 -1
  27. package/dist/v2-host-tool-names.d.ts +43 -0
  28. package/dist/v2-host-tool-names.d.ts.map +1 -0
  29. package/dist/v2-session-lifecycle.d.ts +22 -0
  30. package/dist/v2-session-lifecycle.d.ts.map +1 -0
  31. package/dist/v2-tool-hooks.d.ts +14 -2
  32. package/dist/v2-tool-hooks.d.ts.map +1 -1
  33. package/dist/wakes/runtime-consumer.d.ts +6 -1
  34. package/dist/wakes/runtime-consumer.d.ts.map +1 -1
  35. package/dist/wakes/session-delivery.d.ts +12 -7
  36. package/dist/wakes/session-delivery.d.ts.map +1 -1
  37. package/package.json +8 -8
  38. package/src/entry/server-runtime.mjs +26 -14
  39. package/src/shared/status.ts +25 -4
  40. package/src/tui/v2-status.ts +1 -0
  41. package/src/tui/v2.tsx +7 -4
  42. package/src/tui-compiled/v2-status.ts +1 -0
  43. package/src/tui-compiled/v2.tsx +7 -4
  44. package/dist/v2-host-tool-overlap.d.ts +0 -66
  45. package/dist/v2-host-tool-overlap.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -8594,11 +8594,11 @@ var DEFAULT_WORKER_WAIT_MAX_MS = 1800000;
8594
8594
  var MIN_WORKER_WAIT_MAX_MS = 60000;
8595
8595
  var LONGEST_TIMER_DELAY_MS = 2147483647;
8596
8596
  var WORKER_WAIT_LIMIT_PHRASE = "the worker wait limit (`bash.worker_wait_max_ms`, 30 minutes by default)";
8597
- 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";
8598
- 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.";
8597
+ 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";
8598
+ 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.";
8599
8599
  function resolveWatchTimeoutMs(requestedMs, role, capMs, workerWaitMaxMs) {
8600
8600
  if (role === "worker")
8601
- return requestedMs ?? workerWaitMaxMs;
8601
+ return Math.min(requestedMs ?? workerWaitMaxMs, workerWaitMaxMs);
8602
8602
  return Math.min(requestedMs ?? DEFAULT_PRIMARY_WATCH_TIMEOUT_MS, capMs);
8603
8603
  }
8604
8604
  function maxWatchTimeoutMs(role, capMs) {
@@ -8661,6 +8661,10 @@ function taskKillDeadlineText(data, role, nowMs = Date.now()) {
8661
8661
  return "";
8662
8662
  if (data.status === "unknown")
8663
8663
  return "";
8664
+ if (typeof data.remote_phase_note === "string" && data.remote_phase_note !== "") {
8665
+ const note = data.remote_phase_note;
8666
+ return `${note.charAt(0).toUpperCase()}${note.slice(1)}.`;
8667
+ }
8664
8668
  const hardKill = data.hard_kill;
8665
8669
  if (!hardKill || typeof hardKill.limit_ms !== "number") {
8666
8670
  return "This task has no kill deadline.";
@@ -9395,8 +9399,8 @@ var BASH_HOST_FALLBACK_BANNER = "[AFT host fallback - module transport down; no
9395
9399
  var BASH_HOST_FALLBACK_MAX_OUTPUT_BYTES = 100 * 1024;
9396
9400
  var BASH_HOST_FALLBACK_MAX_TIMEOUT_MS = 10 * 60 * 1000;
9397
9401
  var BASH_HOST_FALLBACK_REFUSAL = "AFT transport is down; only foreground execution is available in host fallback";
9398
- var BASH_RUNON_DESCRIPTION = 'Run this command on the remote Linux build server instead of this machine; the only value is "linux".';
9399
- 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.';
9402
+ 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).';
9403
+ 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.';
9400
9404
  function pathKeyForPlatform(env, platform) {
9401
9405
  return platform === "win32" ? Object.keys(env).find((key) => key.toLowerCase() === "path") : "PATH";
9402
9406
  }
@@ -9840,6 +9844,7 @@ function isPassiveCommand(command) {
9840
9844
 
9841
9845
  // ../aft-bridge/dist/transport.js
9842
9846
  var WORKER_SESSION_FIELD = "worker_session";
9847
+ var HOST_TOOL_NAMES_FIELD = "host_tool_names";
9843
9848
  var PRESET_FIELD = "preset";
9844
9849
  function callPresetFor(workerSession) {
9845
9850
  return workerSession ? "worker" : "head";
@@ -10004,6 +10009,8 @@ class BinaryBridge {
10004
10009
  lastBinaryFingerprintCheckAt = 0;
10005
10010
  _retiringDueToBinaryChange = false;
10006
10011
  pending = new Map;
10012
+ activeSendCalls = 0;
10013
+ processLoopReferenced = true;
10007
10014
  outstandingBackgroundTaskIds = new Set;
10008
10015
  nextId = 1;
10009
10016
  processGeneration = 0;
@@ -10235,6 +10242,32 @@ class BinaryBridge {
10235
10242
  this.sessionLogVia(sessionId, `hashline registration carrier transport=ndjson phase=${phase} edit_slot_survives=${editSlotSurvives}${repeated}`);
10236
10243
  }
10237
10244
  async send(command, params = {}, options) {
10245
+ this.activeSendCalls += 1;
10246
+ this.syncEventLoopHold();
10247
+ try {
10248
+ return await this.dispatchSend(command, params, options);
10249
+ } finally {
10250
+ this.activeSendCalls -= 1;
10251
+ this.syncEventLoopHold();
10252
+ }
10253
+ }
10254
+ syncEventLoopHold() {
10255
+ const child = this.process;
10256
+ if (!child)
10257
+ return;
10258
+ const hold = this.activeSendCalls > 0 || this.outstandingBackgroundTaskIds.size > 0;
10259
+ if (hold === this.processLoopReferenced)
10260
+ return;
10261
+ this.processLoopReferenced = hold;
10262
+ const handles = [child, child.stdin, child.stdout, child.stderr];
10263
+ for (const handle of handles) {
10264
+ if (hold)
10265
+ handle?.ref?.();
10266
+ else
10267
+ handle?.unref?.();
10268
+ }
10269
+ }
10270
+ async dispatchSend(command, params, options) {
10238
10271
  let dispatchParams = params;
10239
10272
  if (command === "configure") {
10240
10273
  dispatchParams = { ...params };
@@ -10272,11 +10305,13 @@ class BinaryBridge {
10272
10305
  params.edit_slot_survives = editSlotSurvives;
10273
10306
  this.logHashlineRegistrationCarrier("tool_call", sessionId, editSlotSurvives);
10274
10307
  }
10275
- const { preview, workerSession, ...sendOptions } = options ?? {};
10308
+ const { preview, workerSession, hostToolNames, ...sendOptions } = options ?? {};
10276
10309
  if (preview === true)
10277
10310
  params.preview = true;
10278
10311
  if (workerSession === true)
10279
10312
  params[WORKER_SESSION_FIELD] = true;
10313
+ if (hostToolNames)
10314
+ params[HOST_TOOL_NAMES_FIELD] = hostToolNames;
10280
10315
  return await this.send("tool_call", params, Object.keys(sendOptions).length > 0 ? sendOptions : undefined);
10281
10316
  }
10282
10317
  async sendWithVersionMismatchRetry(command, params, options, canRetryAfterVersionSwap) {
@@ -10345,7 +10380,12 @@ class BinaryBridge {
10345
10380
  if (command === "cancel_request" || Object.hasOwn(params, "command") || Object.hasOwn(params, "method")) {
10346
10381
  const nested = { ...params };
10347
10382
  const reserved = {};
10348
- for (const key of ["session_id", "lsp_hints", WORKER_SESSION_FIELD]) {
10383
+ for (const key of [
10384
+ "session_id",
10385
+ "lsp_hints",
10386
+ WORKER_SESSION_FIELD,
10387
+ HOST_TOOL_NAMES_FIELD
10388
+ ]) {
10349
10389
  if (Object.hasOwn(nested, key)) {
10350
10390
  reserved[key] = nested[key];
10351
10391
  delete nested[key];
@@ -10720,6 +10760,8 @@ class BinaryBridge {
10720
10760
  this.lastChildActivityAt = 0;
10721
10761
  this.consecutiveRequestTimeouts = 0;
10722
10762
  this.stderrTail = [];
10763
+ this.processLoopReferenced = true;
10764
+ this.syncEventLoopHold();
10723
10765
  }
10724
10766
  pushStderrLine(line) {
10725
10767
  this.stderrTail.push(line);
@@ -10846,6 +10888,7 @@ class BinaryBridge {
10846
10888
  const taskId = bashTaskIdFrom(response);
10847
10889
  if (taskId)
10848
10890
  this.outstandingBackgroundTaskIds.delete(taskId);
10891
+ this.syncEventLoopHold();
10849
10892
  this.onBashCompletion?.(response, this);
10850
10893
  return;
10851
10894
  }
@@ -10959,7 +11002,7 @@ class BinaryBridge {
10959
11002
  const delay = 100 * 2 ** this._restartCount;
10960
11003
  this._restartCount++;
10961
11004
  this.logVia(`Auto-restart #${this._restartCount} in ${delay}ms`);
10962
- setTimeout(() => {
11005
+ const restartTimer = setTimeout(() => {
10963
11006
  if (!this._shuttingDown && !this.isAlive()) {
10964
11007
  try {
10965
11008
  this.spawnProcess();
@@ -10968,6 +11011,7 @@ class BinaryBridge {
10968
11011
  }
10969
11012
  }
10970
11013
  }, delay);
11014
+ restartTimer.unref?.();
10971
11015
  this.scheduleRestartCountReset();
10972
11016
  } else {
10973
11017
  this.errorVia(`Max restarts (${this.maxRestarts}) reached, giving up. Logs: ${this.getLogFilePathVia()}${tail}`);
@@ -10988,6 +11032,7 @@ class BinaryBridge {
10988
11032
  this._restartCount = 0;
10989
11033
  this.restartResetTimer = null;
10990
11034
  }, BinaryBridge.RESTART_RESET_MS);
11035
+ this.restartResetTimer.unref?.();
10991
11036
  }
10992
11037
  clearRestartResetTimer() {
10993
11038
  if (this.restartResetTimer) {
@@ -11604,10 +11649,12 @@ function startLiveConfigReload(options) {
11604
11649
  let acceptedTexts;
11605
11650
  const reload = () => {
11606
11651
  let loaded = options.load();
11652
+ let deletedProjectConfig = false;
11607
11653
  if (loaded.ok) {
11608
11654
  const read = new Set(loaded.sources);
11609
11655
  const deleted = [...acceptedSources].find((path) => !read.has(path));
11610
- if (deleted !== undefined) {
11656
+ deletedProjectConfig = deleted === options.projectPath && loaded.projectText === null;
11657
+ if (deleted !== undefined && !deletedProjectConfig) {
11611
11658
  loaded = { ok: false, message: `AFT config at ${deleted} was deleted` };
11612
11659
  } else {
11613
11660
  acceptedSources = read;
@@ -12379,17 +12426,17 @@ async function renameIfPresent(from, to) {
12379
12426
  throw error;
12380
12427
  }
12381
12428
  }
12382
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/client.js
12429
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/client.js
12383
12430
  import { promises as fs3 } from "node:fs";
12384
12431
  import { debuglog } from "node:util";
12385
12432
 
12386
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/auth.js
12433
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/auth.js
12387
12434
  import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
12388
12435
 
12389
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/socket.js
12436
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/socket.js
12390
12437
  import net from "node:net";
12391
12438
 
12392
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/envelope.js
12439
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/envelope.js
12393
12440
  var PROTOCOL_VERSION = 2;
12394
12441
  var HEADER_LEN = 21;
12395
12442
  var FROZEN_PREFIX_LEN = 5;
@@ -12492,12 +12539,12 @@ function validateHeaderFields(header) {
12492
12539
  if (admission === AdmissionClass.Sheddable && ty !== FrameType.Push && ty !== FrameType.StreamData) {
12493
12540
  throw new DecodeError(`SHEDDABLE admission class is illegal on ${FrameType[ty]} in flags 0b${flags.toString(2).padStart(8, "0")}`, "sheddable_illegal_frame_type");
12494
12541
  }
12495
- if (channel === 0 && epoch !== 0) {
12496
- throw new DecodeError(`control channel carried nonzero epoch ${epoch}`, "nonzero_epoch_on_control_channel");
12497
- }
12498
12542
  if (isPureHeader(ty) && len !== 0) {
12499
12543
  throw new DecodeError(`pure-header frame ${FrameType[ty]} declared non-zero body length ${len}`, "pure_header_frame_with_body");
12500
12544
  }
12545
+ if (channel === 0 && epoch !== 0) {
12546
+ throw new DecodeError(`control channel carried nonzero epoch ${epoch}`, "nonzero_epoch_on_control_channel");
12547
+ }
12501
12548
  }
12502
12549
  function decodeHeader(bytes) {
12503
12550
  if (bytes.length < FROZEN_PREFIX_LEN) {
@@ -12531,12 +12578,12 @@ function decodeHeader(bytes) {
12531
12578
  }
12532
12579
  const channel = view.getUint16(7, true);
12533
12580
  const epoch = view.getUint32(9, true);
12534
- if (channel === 0 && epoch !== 0) {
12535
- throw new DecodeError(`control channel carried nonzero epoch ${epoch}`, "nonzero_epoch_on_control_channel");
12536
- }
12537
12581
  if (isPureHeader(ty) && len !== 0) {
12538
12582
  throw new DecodeError(`pure-header frame ${FrameType[ty]} declared non-zero body length ${len}`, "pure_header_frame_with_body");
12539
12583
  }
12584
+ if (channel === 0 && epoch !== 0) {
12585
+ throw new DecodeError(`control channel carried nonzero epoch ${epoch}`, "nonzero_epoch_on_control_channel");
12586
+ }
12540
12587
  return { len, ver, ty, flags, channel, epoch, corr: view.getBigUint64(13, true) };
12541
12588
  }
12542
12589
  function buildFrame(ty, flags, channel, epoch, corr, body) {
@@ -12554,14 +12601,18 @@ function encodeFrame(frame) {
12554
12601
  if (frame.header.len !== frame.body.length) {
12555
12602
  throw new DecodeError(`frame header length ${frame.header.len} does not match body length ${frame.body.length}`, "frame_length_mismatch");
12556
12603
  }
12604
+ if (frame.body.length > MAX_FRAME_BODY_LEN) {
12605
+ throw new DecodeError(`frame body ${frame.body.length} exceeds max ${MAX_FRAME_BODY_LEN}`, "frame_body_too_large");
12606
+ }
12557
12607
  const header = encodeHeader(frame.header);
12608
+ decodeHeader(header);
12558
12609
  const output = new Uint8Array(header.length + frame.body.length);
12559
12610
  output.set(header, 0);
12560
12611
  output.set(frame.body, header.length);
12561
12612
  return output;
12562
12613
  }
12563
12614
 
12564
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/socket.js
12615
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/socket.js
12565
12616
  class SocketClosedError extends Error {
12566
12617
  }
12567
12618
 
@@ -12611,6 +12662,15 @@ class SubcSocket {
12611
12662
  sock.on("data", (chunk) => {
12612
12663
  this.chunks.push(chunk);
12613
12664
  this.buffered += chunk.length;
12665
+ if (this.waiter) {
12666
+ if (this.waiter.immediate) {
12667
+ clearImmediate(this.waiter.immediate);
12668
+ this.waiter.immediate = null;
12669
+ }
12670
+ if (this.waiter.extendOnProgress && this.buffered < this.waiter.need) {
12671
+ this.armWaiterTimer(this.waiter);
12672
+ }
12673
+ }
12614
12674
  this.tryServe();
12615
12675
  });
12616
12676
  const fail = (err) => {
@@ -12657,30 +12717,80 @@ class SubcSocket {
12657
12717
  throw new DecodeError(`frame body ${header.len} exceeds max ${MAX_FRAME_BODY_LEN}`, "frame_body_too_large");
12658
12718
  }
12659
12719
  onHeader?.();
12660
- const bodyDeadlineMs = typeof bodyDeadline === "number" ? bodyDeadline : Date.now() + bodyDeadline.afterHeaderMs;
12661
- const body = header.len === 0 ? new Uint8Array(0) : await this.readExact(header.len, bodyDeadlineMs);
12720
+ const bodyDeadlineArg = typeof bodyDeadline === "number" ? bodyDeadline : { idleTimeoutMs: bodyDeadline.afterHeaderMs };
12721
+ const body = header.len === 0 ? new Uint8Array(0) : await this.readExact(header.len, bodyDeadlineArg);
12662
12722
  return { header, body };
12663
12723
  }
12664
- readExact(n, deadlineMs) {
12724
+ armWaiterTimer(w) {
12725
+ if (!Number.isFinite(w.budget))
12726
+ return;
12727
+ if (w.timer) {
12728
+ clearTimeout(w.timer);
12729
+ w.timer = null;
12730
+ }
12731
+ w.timer = setTimeout(() => {
12732
+ w.timer = null;
12733
+ const bufferedAtTimeout = this.buffered;
12734
+ const verifyTimeout = () => {
12735
+ if (this.waiter !== w)
12736
+ return;
12737
+ if (w.extendOnProgress && this.buffered > bufferedAtTimeout) {
12738
+ this.armWaiterTimer(w);
12739
+ return;
12740
+ }
12741
+ this.waiter = null;
12742
+ w.reject(new SocketTimeoutError(`timed out waiting for ${w.need} bytes`));
12743
+ };
12744
+ w.immediate = setImmediate(() => {
12745
+ if (this.waiter !== w)
12746
+ return;
12747
+ if (this.buffered > bufferedAtTimeout) {
12748
+ verifyTimeout();
12749
+ } else {
12750
+ w.immediate = setImmediate(() => {
12751
+ w.immediate = null;
12752
+ verifyTimeout();
12753
+ });
12754
+ }
12755
+ });
12756
+ }, w.budget);
12757
+ }
12758
+ readExact(n, deadline) {
12665
12759
  if (this.waiter) {
12666
12760
  return Promise.reject(new Error("concurrent readExact is not supported"));
12667
12761
  }
12668
12762
  if (n === 0)
12669
12763
  return Promise.resolve(new Uint8Array(0));
12670
12764
  return new Promise((resolve, reject) => {
12671
- let timer = null;
12672
- if (Number.isFinite(deadlineMs)) {
12673
- const remaining = deadlineMs - Date.now();
12674
- if (remaining <= 0) {
12765
+ let budget;
12766
+ const extendOnProgress = typeof deadline === "object";
12767
+ if (typeof deadline === "object") {
12768
+ budget = deadline.idleTimeoutMs;
12769
+ if (budget <= 0) {
12675
12770
  reject(new SocketTimeoutError(`timed out waiting for ${n} bytes`));
12676
12771
  return;
12677
12772
  }
12678
- timer = setTimeout(() => {
12679
- this.waiter = null;
12773
+ } else if (Number.isFinite(deadline)) {
12774
+ const remaining = deadline - Date.now();
12775
+ if (remaining <= 0) {
12680
12776
  reject(new SocketTimeoutError(`timed out waiting for ${n} bytes`));
12681
- }, remaining);
12777
+ return;
12778
+ }
12779
+ budget = remaining;
12780
+ } else {
12781
+ budget = Number.POSITIVE_INFINITY;
12682
12782
  }
12683
- this.waiter = { need: n, resolve, reject, timer };
12783
+ const waiter = {
12784
+ need: n,
12785
+ resolve,
12786
+ reject,
12787
+ timer: null,
12788
+ immediate: null,
12789
+ budget,
12790
+ extendOnProgress
12791
+ };
12792
+ this.waiter = waiter;
12793
+ this.armWaiterTimer(waiter);
12684
12794
  this.tryServe();
12685
12795
  });
12686
12796
  }
@@ -12763,6 +12873,8 @@ class SubcSocket {
12763
12873
  this.waiter = null;
12764
12874
  if (w.timer)
12765
12875
  clearTimeout(w.timer);
12876
+ if (w.immediate)
12877
+ clearImmediate(w.immediate);
12766
12878
  w.resolve(out);
12767
12879
  return;
12768
12880
  }
@@ -12770,6 +12882,8 @@ class SubcSocket {
12770
12882
  this.waiter = null;
12771
12883
  if (w.timer)
12772
12884
  clearTimeout(w.timer);
12885
+ if (w.immediate)
12886
+ clearImmediate(w.immediate);
12773
12887
  w.reject(this.closedErr);
12774
12888
  }
12775
12889
  }
@@ -12794,7 +12908,7 @@ class SubcSocket {
12794
12908
  }
12795
12909
  }
12796
12910
 
12797
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/auth.js
12911
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/auth.js
12798
12912
  var NONCE_LEN = 32;
12799
12913
  var MAX_AUTH_MESSAGE_LEN = 4096;
12800
12914
  var SERVER_PROOF_DOMAIN = "subc-server-v1";
@@ -12868,7 +12982,7 @@ async function authenticateClient(sock, conn, deadlineMs) {
12868
12982
  await writeMessage(sock, { client_auth: Array.from(clientAuth) }, deadlineMs);
12869
12983
  }
12870
12984
 
12871
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/connection-file.js
12985
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/connection-file.js
12872
12986
  import { promises as fs2 } from "node:fs";
12873
12987
  var SCHEMA_VERSION = 1;
12874
12988
  var MIN_KEY_LEN = 32;
@@ -12901,18 +13015,29 @@ function validate(info) {
12901
13015
  throw new ConnectionFileError(`connection file daemon_id must be ${DAEMON_ID_LEN} bytes, got ${info.daemonId.length}`);
12902
13016
  }
12903
13017
  }
12904
- async function verifyOwnerOnly(path) {
13018
+ function verifyOwnerOnly(path, stat, expectedUid) {
12905
13019
  if (process.platform === "win32")
12906
13020
  return;
12907
- const stat = await fs2.stat(path);
13021
+ if (expectedUid !== undefined && stat.uid !== expectedUid) {
13022
+ throw new ConnectionFileError(`connection file ${path} is owned by uid ${stat.uid}, expected effective uid ${expectedUid}`);
13023
+ }
12908
13024
  const mode = stat.mode & 511;
12909
13025
  if ((mode & 63) !== 0) {
12910
13026
  throw new ConnectionFileError(`connection file ${path} has insecure permissions 0o${mode.toString(8)}; expected owner-only 0600`);
12911
13027
  }
12912
13028
  }
12913
13029
  async function readConnectionFile(path) {
12914
- await verifyOwnerOnly(path);
12915
- const raw = await fs2.readFile(path, "utf8");
13030
+ return readConnectionFileForUid(path, process.geteuid?.());
13031
+ }
13032
+ async function readConnectionFileForUid(path, expectedUid) {
13033
+ const handle = await fs2.open(path, "r");
13034
+ let raw;
13035
+ try {
13036
+ verifyOwnerOnly(path, await handle.stat(), expectedUid);
13037
+ raw = await handle.readFile({ encoding: "utf8" });
13038
+ } finally {
13039
+ await handle.close();
13040
+ }
12916
13041
  let parsed;
12917
13042
  try {
12918
13043
  parsed = JSON.parse(raw);
@@ -12946,7 +13071,7 @@ async function readConnectionFile(path) {
12946
13071
  return info;
12947
13072
  }
12948
13073
 
12949
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/launch-nonce.js
13074
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/launch-nonce.js
12950
13075
  import { closeSync as closeSync4, fstatSync as fstatSync2, readSync as readSync3 } from "node:fs";
12951
13076
  var SUBC_LAUNCH_NONCE_ENV = "SUBC_LAUNCH_NONCE";
12952
13077
  var SUBC_LAUNCH_NONCE_FD_ENV = "SUBC_LAUNCH_NONCE_FD";
@@ -13135,7 +13260,7 @@ function closeQuietly(fd) {
13135
13260
  } catch {}
13136
13261
  }
13137
13262
 
13138
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/route-handle.js
13263
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/route-handle.js
13139
13264
  var connectionToken = new WeakMap;
13140
13265
  var reverseRequests = new WeakMap;
13141
13266
 
@@ -13228,7 +13353,7 @@ function sameRouteHandle(left, right) {
13228
13353
  return left === right;
13229
13354
  }
13230
13355
 
13231
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/client.js
13356
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/client.js
13232
13357
  var debug = debuglog("subc-client");
13233
13358
  var DEFAULT_HANDSHAKE_TIMEOUT_MS = 1e4;
13234
13359
  var DEFAULT_REQUEST_TIMEOUT_MS = 30000;
@@ -13285,6 +13410,12 @@ function requireBinaryBody(body) {
13285
13410
  const type = body === null ? "null" : Array.isArray(body) ? "array" : typeof body;
13286
13411
  throw new SubcError(`binary request body must be a Uint8Array; got ${type}`, "binary_body_required");
13287
13412
  }
13413
+ var SCOPE_ROUTE_CLOSE_REASONS = new Set([
13414
+ "scope_ended",
13415
+ "scope_carrier_removed",
13416
+ "scope_delegation_changed",
13417
+ "scope_parent_ended"
13418
+ ]);
13288
13419
  function parseRouteCloseReason(reason) {
13289
13420
  switch (reason) {
13290
13421
  case "reload":
@@ -13427,14 +13558,16 @@ class SubcClient {
13427
13558
  return modules.filter((module) => module.capabilities?.provides.includes(capability) ?? false).map((module) => module.module_id).sort();
13428
13559
  }
13429
13560
  async routeOpen(target, identity, opts = {}) {
13561
+ const scope = validatedRouteScope(opts.scope);
13430
13562
  const release = await this.routeOpenGate.acquire();
13431
13563
  try {
13432
- return await this.sendRouteOpen(target, identity, opts);
13564
+ return await this.sendRouteOpen(target, identity, { ...opts, scope });
13433
13565
  } finally {
13434
13566
  release();
13435
13567
  }
13436
13568
  }
13437
13569
  async sendRouteOpen(target, identity, opts = {}) {
13570
+ const scope = validatedRouteScope(opts.scope);
13438
13571
  const consumerIdentity = routeOpenConsumerIdentity(opts);
13439
13572
  const reverseRequests = opts.reverseRequests ?? new ReverseRequestRegistry;
13440
13573
  reverseRequests.seal();
@@ -13444,7 +13577,8 @@ class SubcClient {
13444
13577
  target,
13445
13578
  identity,
13446
13579
  ...consumerIdentity ? { consumer_identity: consumerIdentity } : {},
13447
- ...consumerCapabilities.length > 0 ? { consumer_capabilities: consumerCapabilities } : {}
13580
+ ...consumerCapabilities.length > 0 ? { consumer_capabilities: consumerCapabilities } : {},
13581
+ ...scope ? { scope: { owner: scope.owner, ref: scope.ref, scope_epoch: scope.scopeEpoch } } : {}
13448
13582
  });
13449
13583
  let installed = null;
13450
13584
  const install = (frame) => {
@@ -13647,7 +13781,7 @@ class SubcClient {
13647
13781
  this.sendRouteGoodbye(handle);
13648
13782
  }
13649
13783
  async closeManagedRoute(target, identity, opts = {}) {
13650
- const key = routeCacheKey(target, identity, routeOpenConsumerIdentity(opts), (opts.reverseRequests ?? new ReverseRequestRegistry).capabilities());
13784
+ const key = routeCacheKey(target, identity, routeOpenConsumerIdentity(opts), (opts.reverseRequests ?? new ReverseRequestRegistry).capabilities(), validatedRouteScope(opts.scope));
13651
13785
  const cached = this.routes.get(key);
13652
13786
  if (!cached)
13653
13787
  return;
@@ -13743,13 +13877,13 @@ class SubcClient {
13743
13877
  };
13744
13878
  pending.timer = setTimeout(() => this.arbitrateTimeout(key, pending, channel, corr, ms), ms);
13745
13879
  this.pending.set(key, pending);
13746
- if (signal)
13747
- this.attachCancelSignal(signal, key, handle, corr, priority);
13748
13880
  writeBorrowed(this.sock, encodeFrame(frame), Date.now() + ms).catch((error) => {
13749
13881
  const current = this.pending.get(key);
13750
13882
  if (current)
13751
13883
  this.rejectPending(key, current, error instanceof Error ? error : new SubcError(String(error)));
13752
13884
  });
13885
+ if (signal)
13886
+ this.attachCancelSignal(signal, key, handle, corr, priority);
13753
13887
  });
13754
13888
  }
13755
13889
  arbitrateTimeout(key, pending, channel, corr, ms) {
@@ -13823,8 +13957,6 @@ class SubcClient {
13823
13957
  };
13824
13958
  pending.timer = setTimeout(() => this.arbitrateTimeout(key, pending, handle.channel, corr, ms), ms);
13825
13959
  this.pending.set(key, pending);
13826
- if (signal)
13827
- this.attachCancelSignal(signal, key, handle, corr, priority);
13828
13960
  const write = writeTrackedBorrowed(this.sock, encodeFrame(frame), Date.now() + ms);
13829
13961
  handedToSocket = write.queued;
13830
13962
  write.completed.catch((error) => {
@@ -13832,9 +13964,12 @@ class SubcClient {
13832
13964
  if (current)
13833
13965
  this.rejectPending(key, current, error instanceof Error ? error : new SubcError(String(error)));
13834
13966
  });
13967
+ if (signal && write.queued)
13968
+ this.attachCancelSignal(signal, key, handle, corr, priority);
13835
13969
  });
13836
13970
  }
13837
13971
  async cachedRouteHandle(moduleId, opts) {
13972
+ const scope = validatedRouteScope(opts.scope);
13838
13973
  const identity = opts.identity ?? this.opts.identity;
13839
13974
  if (!identity) {
13840
13975
  throw new SubcCallError("terminal", "managed call requires a BindIdentity in SubcClient.connect({ identity }) or call(..., { identity })", "missing_identity");
@@ -13843,7 +13978,7 @@ class SubcClient {
13843
13978
  const consumerIdentity = routeOpenConsumerIdentity(opts);
13844
13979
  const reverseRequests = opts.reverseRequests ?? new ReverseRequestRegistry;
13845
13980
  reverseRequests.seal();
13846
- const key = routeCacheKey(target, identity, consumerIdentity, reverseRequests.capabilities());
13981
+ const key = routeCacheKey(target, identity, consumerIdentity, reverseRequests.capabilities(), scope);
13847
13982
  let cached = this.routes.get(key);
13848
13983
  if (!cached) {
13849
13984
  cached = {
@@ -13851,6 +13986,7 @@ class SubcClient {
13851
13986
  moduleId,
13852
13987
  target,
13853
13988
  identity,
13989
+ scope,
13854
13990
  consumerIdentity,
13855
13991
  reverseRequests,
13856
13992
  handle: null,
@@ -13858,6 +13994,8 @@ class SubcClient {
13858
13994
  };
13859
13995
  this.routes.set(key, cached);
13860
13996
  }
13997
+ if (cached.endReason !== undefined)
13998
+ throw this.endedCachedRoute(cached.endReason);
13861
13999
  if (cached.handle && this.isLiveHandle(cached.handle))
13862
14000
  return cached.handle;
13863
14001
  const startedAt = this.opts.now();
@@ -13893,6 +14031,8 @@ class SubcClient {
13893
14031
  for (;; ) {
13894
14032
  if (cached.closed)
13895
14033
  throw this.routeClosedDuringOpen();
14034
+ if (cached.endReason !== undefined)
14035
+ throw this.endedCachedRoute(cached.endReason);
13896
14036
  try {
13897
14037
  await this.ensureConnectedForManaged();
13898
14038
  } catch (error) {
@@ -13907,6 +14047,7 @@ class SubcClient {
13907
14047
  try {
13908
14048
  outcome = {
13909
14049
  handle: await this.sendRouteOpen(cached.target, cached.identity, {
14050
+ scope: cached.scope,
13910
14051
  consumerIdentity: cached.consumerIdentity ?? null,
13911
14052
  reverseRequests: cached.reverseRequests
13912
14053
  })
@@ -14074,10 +14215,11 @@ class SubcClient {
14074
14215
  }
14075
14216
  for (const key of routeKeys) {
14076
14217
  const cached = this.routes.get(key);
14077
- if (!cached || cached.closed)
14218
+ if (!cached || cached.closed || cached.endReason !== undefined)
14078
14219
  continue;
14079
14220
  try {
14080
14221
  const handle = await this.routeOpen(cached.target, cached.identity, {
14222
+ scope: cached.scope,
14081
14223
  consumerIdentity: cached.consumerIdentity ?? null,
14082
14224
  reverseRequests: cached.reverseRequests
14083
14225
  });
@@ -14105,11 +14247,15 @@ class SubcClient {
14105
14247
  const cause = new SubcError("route was closed before route.open completed", "route_closed", undefined, "closed_by_caller");
14106
14248
  return new SubcCallError("not_sent", cause.message, cause.code, cause);
14107
14249
  }
14250
+ endedCachedRoute(reason) {
14251
+ const cause = new SubcError(`cached route closed by subc (${reason}); must not reopen`, "route_closed", undefined, reason);
14252
+ return this.terminalCallError(cause.message, cause);
14253
+ }
14108
14254
  async readLoop(sock, generation) {
14109
14255
  try {
14110
14256
  for (;; ) {
14111
14257
  this.readerActive = false;
14112
- const frame = await sock.readFrame(Number.POSITIVE_INFINITY, { afterHeaderMs: BODY_READ_TIMEOUT_MS }, () => {
14258
+ const frame = await sock.readFrame(Number.POSITIVE_INFINITY, { afterHeaderMs: this.opts.bodyReadTimeoutMs ?? BODY_READ_TIMEOUT_MS }, () => {
14113
14259
  this.readerActive = true;
14114
14260
  });
14115
14261
  try {
@@ -14188,6 +14334,7 @@ class SubcClient {
14188
14334
  }
14189
14335
  if (frame.header.ty === FrameType.Goodbye && handle) {
14190
14336
  this.failHandle(handle, new SubcError("route closed by subc (GOODBYE)", "route_closed", undefined, this.routeEndReason(handle.channel)));
14337
+ this.handleCachedRouteClose(handle.channel, this.routeEndReason(handle.channel));
14191
14338
  if (this.liveRoutes.get(handle.channel) === handle)
14192
14339
  this.liveRoutes.delete(handle.channel);
14193
14340
  this.evictRouteHandle(handle);
@@ -14277,13 +14424,29 @@ class SubcClient {
14277
14424
  const final = push.op === "route.closed";
14278
14425
  if (final || !this.routeEndReasons.get(channel)?.final) {
14279
14426
  this.routeEndReasons.set(channel, { reason, final });
14427
+ if (final)
14428
+ this.handleCachedRouteClose(channel, reason);
14280
14429
  }
14281
14430
  }
14282
14431
  }
14283
14432
  } else if (typeof push.body.module_id === "string") {
14284
14433
  for (const [channel, module] of this.routeModules) {
14285
- if (module === push.body.module_id)
14434
+ if (module === push.body.module_id) {
14286
14435
  this.legacyChannelReasons.set(channel, reason);
14436
+ if (push.op === "route.closed")
14437
+ this.handleCachedRouteClose(channel, reason);
14438
+ }
14439
+ }
14440
+ }
14441
+ }
14442
+ handleCachedRouteClose(channel, reason) {
14443
+ for (const [key, cached] of this.routes) {
14444
+ if (cached.handle?.channel !== channel)
14445
+ continue;
14446
+ if (SCOPE_ROUTE_CLOSE_REASONS.has(reason)) {
14447
+ cached.endReason = reason;
14448
+ } else {
14449
+ this.routes.delete(key);
14287
14450
  }
14288
14451
  }
14289
14452
  }
@@ -14306,6 +14469,10 @@ class SubcClient {
14306
14469
  this.routeEndReasons.clear();
14307
14470
  this.legacyChannelReasons.clear();
14308
14471
  this.routeModules.clear();
14472
+ for (const [key, cached] of this.routes) {
14473
+ if (cached.endReason === undefined)
14474
+ this.routes.delete(key);
14475
+ }
14309
14476
  for (const [key, pending] of this.pending) {
14310
14477
  this.rejectPending(key, pending, pending.handle ? routeError : err);
14311
14478
  }
@@ -14400,6 +14567,10 @@ function isRouteOpenRefusal(err) {
14400
14567
  function isRetryableRouteOpenCode(code) {
14401
14568
  if (code === "capability_forbidden")
14402
14569
  return false;
14570
+ if (code === "target_flow_unsupported")
14571
+ return false;
14572
+ if (code === "target_agent_run_unsupported")
14573
+ return false;
14403
14574
  return code === "module_reloading" || code === "module_warming" || code === "target_unavailable" || code === "module_timeout" || code === "scope_not_synced" || code === "scope_changed";
14404
14575
  }
14405
14576
  function equalJitter(delayMs, random) {
@@ -14457,6 +14628,7 @@ function normalizeConnectOptions(opts) {
14457
14628
  random: opts.random ?? Math.random,
14458
14629
  routeOpenRetryDeadlineMs: opts.routeOpenRetryDeadlineMs ?? ROUTE_OPEN_RETRY_DEADLINE_MS,
14459
14630
  timeoutArbitrationGraceMs: opts.timeoutArbitrationGraceMs ?? TIMEOUT_ARBITRATION_GRACE_MS,
14631
+ bodyReadTimeoutMs: opts.bodyReadTimeoutMs,
14460
14632
  livenessProbeWindowMs: opts.livenessProbeWindowMs ?? LIVENESS_PROBE_WINDOW_MS,
14461
14633
  onControlPush: opts.onControlPush
14462
14634
  };
@@ -14480,10 +14652,28 @@ function isValidCapabilityIdentifier(identifier) {
14480
14652
  const numericVersion = Number(version);
14481
14653
  return Number.isSafeInteger(numericVersion) && numericVersion >= 1 && numericVersion <= 4294967295;
14482
14654
  }
14483
- function routeCacheKey(target, identity, consumerIdentity, consumerCapabilities) {
14655
+ function routeCacheKey(target, identity, consumerIdentity, consumerCapabilities, scope) {
14484
14656
  const consumerPart = consumerIdentity ? `${consumerIdentity.module_id}\x00${consumerIdentity.launch_nonce}` : "";
14485
14657
  const capabilitiesPart = [...consumerCapabilities].sort().join("\x00");
14486
- return `${target.kind}\x00${target.module_id}\x00${identity.project_root}\x00${identity.harness}\x00${identity.session}\x00${consumerPart}\x00${capabilitiesPart}`;
14658
+ const scopePart = scope ? JSON.stringify([scope.owner.kind, scope.owner.kind === "reserved" ? scope.owner.module_id : null, scope.ref, scope.scopeEpoch]) : "";
14659
+ return `${target.kind}\x00${target.module_id}\x00${identity.project_root}\x00${identity.harness}\x00${identity.session}\x00${consumerPart}\x00${capabilitiesPart}\x00${scopePart}`;
14660
+ }
14661
+ function validatedRouteScope(scope) {
14662
+ if (scope === undefined)
14663
+ return;
14664
+ 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)) {
14665
+ throw new SubcError("scope.owner must be a Principal with a non-empty reserved module_id", "invalid_scope");
14666
+ }
14667
+ if (typeof scope.ref !== "string" || scope.ref.length === 0) {
14668
+ throw new SubcError("scope.ref must be a non-empty string", "invalid_scope");
14669
+ }
14670
+ if (scope.scopeEpoch === undefined) {
14671
+ throw new SubcError("scope.scopeEpoch is required", "scope_epoch_required");
14672
+ }
14673
+ if (!Number.isSafeInteger(scope.scopeEpoch) || scope.scopeEpoch < 0) {
14674
+ throw new SubcError("scope.scopeEpoch must be a safe non-negative integer", "invalid_scope");
14675
+ }
14676
+ return { owner: { ...scope.owner }, ref: scope.ref, scopeEpoch: scope.scopeEpoch };
14487
14677
  }
14488
14678
  function routeOpenConsumerIdentity(opts = {}) {
14489
14679
  if (opts.consumerIdentity !== undefined)
@@ -15004,6 +15194,41 @@ class SubcRouteClosedMidCallError extends SubcCallError {
15004
15194
  this.reason = reason;
15005
15195
  }
15006
15196
  }
15197
+
15198
+ class SubcFrameReadTimeoutError extends SubcCallError {
15199
+ constructor(cause) {
15200
+ super("outcome_unknown", "the command may have run; timed out waiting for frame body bytes from the AFT daemon", "frame_read_timeout", cause);
15201
+ this.name = "SubcFrameReadTimeoutError";
15202
+ }
15203
+ }
15204
+ function isFrameBodyReadTimeoutMessage(message) {
15205
+ return typeof message === "string" && (/timed out waiting for \d+ bytes/i.test(message) || message.includes("timed out waiting for frame body bytes"));
15206
+ }
15207
+ function isSubcFrameReadTimeoutError(error) {
15208
+ if (error instanceof SubcFrameReadTimeoutError)
15209
+ return true;
15210
+ if (error instanceof SocketTimeoutError && isFrameBodyReadTimeoutMessage(error.message)) {
15211
+ return true;
15212
+ }
15213
+ if (error instanceof SubcError && isFrameBodyReadTimeoutMessage(error.message)) {
15214
+ return true;
15215
+ }
15216
+ if (error instanceof SubcCallError) {
15217
+ if (error.code === "frame_read_timeout")
15218
+ return true;
15219
+ if (error.cause !== undefined && error.cause !== error && isSubcFrameReadTimeoutError(error.cause)) {
15220
+ return true;
15221
+ }
15222
+ if (isFrameBodyReadTimeoutMessage(error.message))
15223
+ return true;
15224
+ }
15225
+ return false;
15226
+ }
15227
+ function frameReadTimeoutError(error) {
15228
+ if (!isSubcFrameReadTimeoutError(error))
15229
+ return error;
15230
+ return error instanceof SubcFrameReadTimeoutError ? error : new SubcFrameReadTimeoutError(error);
15231
+ }
15007
15232
  function isLocalRouteCloseRejection(error) {
15008
15233
  return error instanceof SubcError && error.closeReason === "closed_by_caller";
15009
15234
  }
@@ -15030,6 +15255,8 @@ function isSubcClientClosedError(error) {
15030
15255
  return error instanceof SubcError && error.message === "client closed";
15031
15256
  }
15032
15257
  function isSocketDeathError(error, depth = 0) {
15258
+ if (isSubcFrameReadTimeoutError(error))
15259
+ return true;
15033
15260
  if (error instanceof SubcError && error.closeReason === "connection_lost")
15034
15261
  return true;
15035
15262
  if (error instanceof SubcCallError) {
@@ -15072,6 +15299,12 @@ class SubcLivenessProbeTimeoutError extends Error {
15072
15299
  }
15073
15300
  }
15074
15301
  var AFT_MODULE_ID = "aft";
15302
+ var HOST_SCOPE_LOOKUP_TIMEOUT_MS = 2000;
15303
+ var HOST_SCOPE_RETRY_FLOOR_MS = 5000;
15304
+ var HOST_SCOPE_RETRY_CAP_MS = 300000;
15305
+ function isHostScopeSyncRefusal(code) {
15306
+ return code === "scope_not_live" || code === "scope_not_synced" || code === "scope_changed";
15307
+ }
15075
15308
  var MAX_CONSECUTIVE_TRANSPORT_FAILURES = 3;
15076
15309
  var LIVENESS_PROBE_TIMEOUT_MS = 15000;
15077
15310
  var REQUEST_DEADLINE_CODE = "request_deadline";
@@ -15150,7 +15383,7 @@ class BgSubscription {
15150
15383
  identity;
15151
15384
  acquireClient;
15152
15385
  dropClient;
15153
- consumerIdentity;
15386
+ openRoute;
15154
15387
  onNudge;
15155
15388
  sleep;
15156
15389
  canAttach;
@@ -15161,21 +15394,25 @@ class BgSubscription {
15161
15394
  isCurrent;
15162
15395
  noteClientAlive;
15163
15396
  noteUnansweredCall;
15397
+ onRouteAttached;
15164
15398
  stopped = false;
15165
15399
  current = null;
15166
15400
  attached = null;
15401
+ route = null;
15167
15402
  loop;
15168
15403
  lifecycleLogState = new Map;
15169
15404
  nudgeReceiptLogState = null;
15170
- constructor(identity, acquireClient, dropClient, consumerIdentity, onNudge, sleep, canAttach, onRootAttachFailure, onDormant, dispatchProbeIntervalMs, nudgeRef, isCurrent = () => true, noteClientAlive = () => {
15405
+ constructor(identity, acquireClient, dropClient, openRoute, onNudge, sleep, canAttach, onRootAttachFailure, onDormant, dispatchProbeIntervalMs, nudgeRef, isCurrent = () => true, noteClientAlive = () => {
15171
15406
  return;
15172
15407
  }, noteUnansweredCall = () => {
15173
15408
  return;
15409
+ }, onRouteAttached = () => {
15410
+ return;
15174
15411
  }) {
15175
15412
  this.identity = identity;
15176
15413
  this.acquireClient = acquireClient;
15177
15414
  this.dropClient = dropClient;
15178
- this.consumerIdentity = consumerIdentity;
15415
+ this.openRoute = openRoute;
15179
15416
  this.onNudge = onNudge;
15180
15417
  this.sleep = sleep;
15181
15418
  this.canAttach = canAttach;
@@ -15186,11 +15423,15 @@ class BgSubscription {
15186
15423
  this.isCurrent = isCurrent;
15187
15424
  this.noteClientAlive = noteClientAlive;
15188
15425
  this.noteUnansweredCall = noteUnansweredCall;
15426
+ this.onRouteAttached = onRouteAttached;
15189
15427
  this.loop = this.run();
15190
15428
  }
15191
15429
  get attachedClient() {
15192
15430
  return this.attached;
15193
15431
  }
15432
+ get attachedRoute() {
15433
+ return this.route;
15434
+ }
15194
15435
  async stop() {
15195
15436
  this.stopped = true;
15196
15437
  const sub = this.current;
@@ -15315,7 +15556,7 @@ class BgSubscription {
15315
15556
  }
15316
15557
  let route;
15317
15558
  try {
15318
- route = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, this.identity, { consumerIdentity: this.consumerIdentity });
15559
+ route = await this.openRoute(client);
15319
15560
  } catch (err) {
15320
15561
  if (this.isCurrent() && this.onRootAttachFailure(err)) {
15321
15562
  if (reconnecting)
@@ -15373,6 +15614,8 @@ class BgSubscription {
15373
15614
  });
15374
15615
  this.current = sub;
15375
15616
  this.attached = client;
15617
+ this.route = route;
15618
+ this.onRouteAttached();
15376
15619
  stopDispatchProbe = this.startDispatchProbe(client, routeId);
15377
15620
  this.info("subscription-open", `subscription open channel=${routeId}`);
15378
15621
  if (reconnecting) {
@@ -15415,6 +15658,7 @@ class BgSubscription {
15415
15658
  stopDispatchProbe();
15416
15659
  this.current = null;
15417
15660
  this.attached = null;
15661
+ this.route = null;
15418
15662
  safeCloseRoute(client, route);
15419
15663
  }
15420
15664
  await this.backoff(backoffAttempt++);
@@ -15430,7 +15674,7 @@ function isRecord(value) {
15430
15674
  return typeof value === "object" && value !== null && !Array.isArray(value);
15431
15675
  }
15432
15676
  function isRouteProvenAbsentError(err) {
15433
- return err instanceof SubcError && err.code === "unknown_channel" || err instanceof StaleRouteHandleError;
15677
+ return err instanceof SubcError && (err.code === "unknown_channel" || err.code === "stale_route_epoch") || err instanceof StaleRouteHandleError;
15434
15678
  }
15435
15679
  function isAbsentRootRouteError(err) {
15436
15680
  return isRecord(err) && err.code === "config_divergence" && typeof err.message === "string" && err.message.includes("project root does not exist");
@@ -15557,6 +15801,8 @@ class SubcTransport {
15557
15801
  body.preview = true;
15558
15802
  if (options?.workerSession === true)
15559
15803
  body[WORKER_SESSION_FIELD] = true;
15804
+ if (options?.hostToolNames)
15805
+ body[HOST_TOOL_NAMES_FIELD] = options.hostToolNames;
15560
15806
  body[PRESET_FIELD] = callPresetFor(options?.workerSession === true);
15561
15807
  const reply = await this.pool.routeRequest(this.identityFor(sessionId), body, timeoutMs, onProgress, this.generation, abortSignal);
15562
15808
  return reliftReply(reply);
@@ -15568,10 +15814,12 @@ class SubcTransport {
15568
15814
  }
15569
15815
  const { timeoutMs, onProgress, abortSignal } = this.splitOptions(options);
15570
15816
  const session = typeof params.session_id === "string" ? params.session_id : undefined;
15571
- const { [WORKER_SESSION_FIELD]: workerSession, ...args } = params;
15817
+ const { [WORKER_SESSION_FIELD]: workerSession, [HOST_TOOL_NAMES_FIELD]: hostToolNames, ...args } = params;
15572
15818
  const body = { name: command, arguments: args };
15573
15819
  if (workerSession === true)
15574
15820
  body[WORKER_SESSION_FIELD] = true;
15821
+ if (hostToolNames !== undefined)
15822
+ body[HOST_TOOL_NAMES_FIELD] = hostToolNames;
15575
15823
  body[PRESET_FIELD] = callPresetFor(workerSession === true);
15576
15824
  const editSlotSurvives = this.pool.getEditSlotSurvives();
15577
15825
  if (editSlotSurvives !== undefined)
@@ -15594,6 +15842,10 @@ class SubcTransportPool {
15594
15842
  connectionFile;
15595
15843
  handshakeTimeoutMs;
15596
15844
  consumerIdentity;
15845
+ hostScopes;
15846
+ hostScopeCache = new Map;
15847
+ hostScopeRefusals = new Set;
15848
+ hostScopeOutcomes = new Map;
15597
15849
  connectFn;
15598
15850
  onBgEventsNudge;
15599
15851
  onBgEventsNudgeRef;
@@ -15615,6 +15867,7 @@ class SubcTransportPool {
15615
15867
  routeReopenRetry = null;
15616
15868
  routeReopenRetryMs = null;
15617
15869
  sessions = new Map;
15870
+ sessionIndex = new Map;
15618
15871
  observedStarts = new Set;
15619
15872
  sessionSweep = null;
15620
15873
  rootIndex = new Map;
@@ -15634,6 +15887,7 @@ class SubcTransportPool {
15634
15887
  this.harness = options.harness;
15635
15888
  this.handshakeTimeoutMs = options.handshakeTimeoutMs;
15636
15889
  this.consumerIdentity = options.consumerIdentity;
15890
+ this.hostScopes = options.hostScopes ?? false;
15637
15891
  this.connectFn = options.connect ?? ((opts) => SubcClient.connect(opts));
15638
15892
  this.onBgEventsNudge = options.onBgEventsNudge;
15639
15893
  this.onBgEventsNudgeRef = options.onBgEventsNudgeRef;
@@ -15853,7 +16107,9 @@ class SubcTransportPool {
15853
16107
  return transport;
15854
16108
  }
15855
16109
  getBridge(projectRoot) {
15856
- const root = this.canonicalRoot(projectRoot);
16110
+ return this.getBridgeForCanonicalRoot(this.canonicalRoot(projectRoot));
16111
+ }
16112
+ getBridgeForCanonicalRoot(root) {
15857
16113
  if (this.shuttingDown && this.lifecycleEnabled()) {
15858
16114
  throw new SubcTransportShuttingDownError;
15859
16115
  }
@@ -15950,6 +16206,12 @@ class SubcTransportPool {
15950
16206
  pendingDetach: false
15951
16207
  };
15952
16208
  this.sessions.set(key, record);
16209
+ let records = this.sessionIndex.get(identity.session);
16210
+ if (!records) {
16211
+ records = new Set;
16212
+ this.sessionIndex.set(identity.session, records);
16213
+ }
16214
+ records.add(record);
15953
16215
  let keys = this.rootIndex.get(root);
15954
16216
  if (!keys) {
15955
16217
  keys = new Set;
@@ -16127,6 +16389,10 @@ class SubcTransportPool {
16127
16389
  log(`subc bg_events: ${message}${suffix}`, { sessionId: record.identity.session });
16128
16390
  }
16129
16391
  removeIndexMembership(record) {
16392
+ const records = this.sessionIndex.get(record.identity.session);
16393
+ records?.delete(record);
16394
+ if (records?.size === 0)
16395
+ this.sessionIndex.delete(record.identity.session);
16130
16396
  const keys = this.rootIndex.get(record.canonicalRoot);
16131
16397
  if (!keys)
16132
16398
  return;
@@ -16139,6 +16405,7 @@ class SubcTransportPool {
16139
16405
  this.sessions.delete(record.identityKey);
16140
16406
  this.removeIndexMembership(record);
16141
16407
  }
16408
+ this.stopUnusedHostScopeRetry(record.identity.session);
16142
16409
  }
16143
16410
  detachSession(key, reason) {
16144
16411
  const record = this.sessions.get(key);
@@ -16158,6 +16425,7 @@ class SubcTransportPool {
16158
16425
  record.routeEntry = null;
16159
16426
  if (routeEntry)
16160
16427
  routeEntry.closed = true;
16428
+ this.stopUnusedHostScopeRetry(record.identity.session);
16161
16429
  return { record, bgSub, routeEntry };
16162
16430
  }
16163
16431
  async cleanupDetached(detached) {
@@ -16214,6 +16482,17 @@ class SubcTransportPool {
16214
16482
  entry.inflight -= 1;
16215
16483
  this.closeRetiredRoute(record, entry);
16216
16484
  }
16485
+ retireHostToolRoute(record) {
16486
+ const entry = record.routeEntry;
16487
+ if (!entry)
16488
+ return;
16489
+ entry.closed = true;
16490
+ entry.closedBy ??= "route replaced";
16491
+ record.routeEntry = null;
16492
+ record.retiredRoutes.add(entry);
16493
+ this.closeRetiredRoute(record, entry);
16494
+ this.stopUnusedHostScopeRetry(record.identity.session);
16495
+ }
16217
16496
  async routeRequest(identity, body, timeoutMs, onProgress, expectedGeneration, abortSignal) {
16218
16497
  const callStartedAt = performance.now();
16219
16498
  const callDeadlineMs = timeoutMs ?? SUBC_DEFAULT_REQUEST_TIMEOUT_MS;
@@ -16352,15 +16631,14 @@ class SubcTransportPool {
16352
16631
  const clearRouteEntry = (entry) => {
16353
16632
  if (record.routeEntry !== entry)
16354
16633
  return;
16355
- entry.closed = true;
16356
- entry.closedBy ??= "route replaced";
16357
- record.routeEntry = null;
16358
- record.retiredRoutes.add(entry);
16359
- this.closeRetiredRoute(record, entry);
16634
+ this.retireHostToolRoute(record);
16360
16635
  };
16361
16636
  const handleRequestFailure = (error, entry) => {
16362
16637
  const aborted = error instanceof Error && error.name === "AbortError";
16363
16638
  const ownsClient = this.isCurrentSession(key, record) && this.client === client && !this.isReapInduced(record);
16639
+ if (ownsClient && error instanceof SubcError && error.closeReason?.startsWith("scope_")) {
16640
+ this.invalidateHostScope(identity.session);
16641
+ }
16364
16642
  if (!aborted && ownsClient && isSocketDeathError(error)) {
16365
16643
  this.dropClient(client, {
16366
16644
  error,
@@ -16369,7 +16647,7 @@ class SubcTransportPool {
16369
16647
  route: routeLabel(entry.handle)
16370
16648
  });
16371
16649
  }
16372
- if (aborted || isRouteProvenAbsentError(error) || error instanceof SubcError && error.closeReason !== undefined) {
16650
+ if (aborted || isRouteProvenAbsentError(error) || error instanceof SubcError && error.closeReason !== undefined || isSubcFrameReadTimeoutError(error)) {
16373
16651
  clearRouteEntry(entry);
16374
16652
  }
16375
16653
  if (aborted || !ownsClient || this.client !== client)
@@ -16448,10 +16726,13 @@ class SubcTransportPool {
16448
16726
  this.resetRouteReopenBackoff();
16449
16727
  return reply;
16450
16728
  } catch (requestError) {
16451
- const error = routeClosedMidCallError(requestError, routeAndEntry.entry, record);
16729
+ const error = frameReadTimeoutError(routeClosedMidCallError(requestError, routeAndEntry.entry, record));
16452
16730
  if (this.isReapInduced(record))
16453
16731
  throw this.annotateReapError(error, record);
16454
16732
  const ownsRoute = this.isCurrentSession(key, record) && this.client === client;
16733
+ if (ownsRoute && (error instanceof SubcError && error.code === "stale_route_epoch" || error instanceof StaleRouteHandleError)) {
16734
+ this.invalidateHostScope(identity.session);
16735
+ }
16455
16736
  if (ownsRoute && isRouteRequestReloadRefusal(error)) {
16456
16737
  clearRouteEntry(routeAndEntry.entry);
16457
16738
  lastRefusal = error;
@@ -16528,9 +16809,14 @@ class SubcTransportPool {
16528
16809
  return this.client;
16529
16810
  if (this.connecting)
16530
16811
  return this.connecting;
16812
+ let owner;
16531
16813
  this.connecting = this.connectFn({
16532
16814
  connectionFile: this.connectionFile,
16533
- handshakeTimeoutMs: this.handshakeTimeoutMs
16815
+ handshakeTimeoutMs: this.handshakeTimeoutMs,
16816
+ onControlPush: (push) => {
16817
+ if (owner && this.client === owner)
16818
+ this.onScopeRouteClose(push);
16819
+ }
16534
16820
  }).then((client) => {
16535
16821
  this.connecting = null;
16536
16822
  if (this.shuttingDown) {
@@ -16540,6 +16826,7 @@ class SubcTransportPool {
16540
16826
  throw new SubcTransportShuttingDownError;
16541
16827
  }
16542
16828
  this.client = client;
16829
+ owner = client;
16543
16830
  this.hadWorkingClient = true;
16544
16831
  this.transportFailures = 0;
16545
16832
  return client;
@@ -16569,9 +16856,7 @@ class SubcTransportPool {
16569
16856
  closedBy: null
16570
16857
  };
16571
16858
  this.assertRootCanAttach(record.canonicalRoot);
16572
- const opening = client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, {
16573
- consumerIdentity: this.consumerIdentity
16574
- }).then((route) => {
16859
+ const opening = this.openHostToolRoute(client, identity, record, entry).then((route) => {
16575
16860
  if (!this.isCurrentSession(record.identityKey, record) || record.routeEntry !== entry || entry.closed || this.client !== client || this.lifecycleEnabled() && !this.isCurrentLiveGeneration(record.canonicalRoot, record.generation)) {
16576
16861
  safeCloseRoute(client, route);
16577
16862
  if (record.routeEntry === entry)
@@ -16581,6 +16866,12 @@ class SubcTransportPool {
16581
16866
  entry.handle = route;
16582
16867
  entry.opening = null;
16583
16868
  this.noteClientAlive(client);
16869
+ const state = this.hostScopeCache.get(identity.session);
16870
+ if (state)
16871
+ this.scheduleHostScopeRetry(identity.session, state);
16872
+ if (state?.answer?.result === "scope" && !state.scopeBindPending && !entry.hostScope && !this.hostScopeRefusals.has(identity.session)) {
16873
+ this.promoteHostScope(identity.session);
16874
+ }
16584
16875
  return route;
16585
16876
  }).catch((error) => {
16586
16877
  const currentEntry = this.isCurrentSession(record.identityKey, record) && record.routeEntry === entry;
@@ -16588,6 +16879,7 @@ class SubcTransportPool {
16588
16879
  entry.closed = true;
16589
16880
  record.routeEntry = null;
16590
16881
  }
16882
+ this.stopUnusedHostScopeRetry(identity.session);
16591
16883
  if (!this.isCurrentSession(record.identityKey, record) && !(error instanceof RouteTornDownError)) {
16592
16884
  throw new RouteTornDownError("subc route opened after session closed");
16593
16885
  }
@@ -16600,6 +16892,327 @@ class SubcTransportPool {
16600
16892
  record.routeEntry = entry;
16601
16893
  return { route: await opening, entry };
16602
16894
  }
16895
+ hostScope(client, identity) {
16896
+ if (this.hostScopeRefusals.has(identity.session))
16897
+ return Promise.resolve(undefined);
16898
+ let state = this.hostScopeCache.get(identity.session);
16899
+ if (!state) {
16900
+ state = { retryAt: 0, retryMs: HOST_SCOPE_RETRY_FLOOR_MS };
16901
+ this.hostScopeCache.set(identity.session, state);
16902
+ }
16903
+ if (state.answer) {
16904
+ return Promise.resolve(state.answer.result === "scope" && !state.scopeBindPending ? state.answer.scope : undefined);
16905
+ }
16906
+ if (state.pending)
16907
+ return state.pending;
16908
+ if (Date.now() < state.retryAt)
16909
+ return Promise.resolve(undefined);
16910
+ const current = state;
16911
+ clearTimeout(current.retryTimer);
16912
+ current.retryTimer = undefined;
16913
+ const abort = new AbortController;
16914
+ current.lookupAbort = abort;
16915
+ const lookup = this.readHostScope(client, identity, abort.signal).then((outcome) => {
16916
+ if (this.hostScopeCache.get(identity.session) !== current)
16917
+ return;
16918
+ current.pending = undefined;
16919
+ current.lookupAbort = undefined;
16920
+ if (abort.signal.aborted)
16921
+ return;
16922
+ this.logHostScopeOutcome(identity.session, outcome);
16923
+ if (outcome.result === "scope" || outcome.result === "no_scope") {
16924
+ current.answer = outcome;
16925
+ current.retryAt = 0;
16926
+ current.retryMs = HOST_SCOPE_RETRY_FLOOR_MS;
16927
+ if (outcome.result === "scope")
16928
+ this.promoteHostScope(identity.session);
16929
+ } else {
16930
+ current.retryAt = Date.now() + current.retryMs;
16931
+ current.retryMs = Math.min(current.retryMs * 2, HOST_SCOPE_RETRY_CAP_MS);
16932
+ this.scheduleHostScopeRetry(identity.session, current);
16933
+ }
16934
+ return outcome.result === "scope" ? outcome.scope : undefined;
16935
+ });
16936
+ current.pending = lookup;
16937
+ return lookup;
16938
+ }
16939
+ unscopedHostRecord(session) {
16940
+ for (const record of this.sessionIndex.get(session) ?? []) {
16941
+ const entry = record.routeEntry;
16942
+ 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))
16943
+ return record;
16944
+ }
16945
+ return;
16946
+ }
16947
+ cancelHostScopeRetry(state) {
16948
+ clearTimeout(state.retryTimer);
16949
+ state.retryTimer = undefined;
16950
+ state.lookupAbort?.abort();
16951
+ }
16952
+ stopUnusedHostScopeRetry(session) {
16953
+ const state = this.hostScopeCache.get(session);
16954
+ if (state && !this.unscopedHostRecord(session)) {
16955
+ if (state.retryTimer || state.scopeBindPending && state.bindPending)
16956
+ log(`subc head scope: session=${session} retry stopped reason=route_closed`);
16957
+ this.cancelHostScopeRetry(state);
16958
+ state.scopeBindPending = false;
16959
+ }
16960
+ }
16961
+ scheduleHostScopeRetry(session, state) {
16962
+ 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))
16963
+ return;
16964
+ state.retryTimer = setTimeout(() => {
16965
+ state.retryTimer = undefined;
16966
+ const record = this.unscopedHostRecord(session);
16967
+ const client = record?.routeEntry?.client ?? record?.bgSub?.attachedClient;
16968
+ if (this.shuttingDown || this.hostScopeCache.get(session) !== state || !record || !client)
16969
+ return;
16970
+ let retry;
16971
+ if (state.scopeBindPending && state.answer?.result === "scope") {
16972
+ state.bindPending = this.retryHostScopeBind(client, record.identity, state, state.answer.scope).finally(() => {
16973
+ state.bindPending = undefined;
16974
+ this.scheduleHostScopeRetry(session, state);
16975
+ });
16976
+ retry = state.bindPending;
16977
+ } else {
16978
+ retry = this.hostScope(client, record.identity);
16979
+ }
16980
+ retry.catch((error) => {
16981
+ warn(`subc head scope: background retry failed code=${error instanceof Error ? error.name : "Error"}`);
16982
+ });
16983
+ }, Math.max(0, state.retryAt - Date.now()));
16984
+ state.retryTimer.unref();
16985
+ }
16986
+ deferHostScopeBind(session, state, code) {
16987
+ state.scopeBindPending = true;
16988
+ if (Date.now() >= state.retryAt) {
16989
+ state.retryAt = Date.now() + state.retryMs;
16990
+ state.retryMs = Math.min(state.retryMs * 2, HOST_SCOPE_RETRY_CAP_MS);
16991
+ }
16992
+ warn(`subc head scope: session=${session} scoped bind refused code=${code}; binding unscoped; retry in=${Math.max(0, state.retryAt - Date.now())}ms`);
16993
+ this.scheduleHostScopeRetry(session, state);
16994
+ }
16995
+ refuseHostScopeBind(session, state, code) {
16996
+ if (!this.hostScopeRefusals.has(session)) {
16997
+ this.hostScopeRefusals.add(session);
16998
+ warn(`subc head scope: session=${session} scoped bind refused code=${code}; binding unscoped; retry stopped`);
16999
+ }
17000
+ if (state) {
17001
+ state.scopeBindPending = false;
17002
+ this.cancelHostScopeRetry(state);
17003
+ }
17004
+ }
17005
+ async retryHostScopeBind(client, identity, state, scope) {
17006
+ try {
17007
+ const route = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, { consumerIdentity: this.consumerIdentity, scope });
17008
+ safeCloseRoute(client, route);
17009
+ if (this.hostScopeCache.get(identity.session) !== state || !state.scopeBindPending || this.shuttingDown || client !== this.client || !this.unscopedHostRecord(identity.session))
17010
+ return;
17011
+ state.scopeBindPending = false;
17012
+ state.retryAt = 0;
17013
+ state.retryMs = HOST_SCOPE_RETRY_FLOOR_MS;
17014
+ log(`subc head scope: session=${identity.session} scoped bind recovered ref=${scope.ref.slice(0, 8)} epoch=${scope.scopeEpoch}`);
17015
+ this.promoteHostScope(identity.session);
17016
+ } catch (error) {
17017
+ if (this.hostScopeCache.get(identity.session) !== state || !state.scopeBindPending || this.shuttingDown || !this.unscopedHostRecord(identity.session))
17018
+ return;
17019
+ if (error instanceof SubcError && error.code && !isSocketDeathError(error) && !isRouteOpenReloadWindowError(error) && !isHostScopeSyncRefusal(error.code)) {
17020
+ this.refuseHostScopeBind(identity.session, state, error.code);
17021
+ } else {
17022
+ this.deferHostScopeBind(identity.session, state, error instanceof SubcError ? error.code ?? error.name : "Error");
17023
+ }
17024
+ }
17025
+ }
17026
+ promoteHostScope(session) {
17027
+ for (const record of this.sessions.values()) {
17028
+ const entry = record.routeEntry;
17029
+ if (record.identity.session !== session)
17030
+ continue;
17031
+ if (entry?.handle && !entry.hostScope)
17032
+ this.retireHostToolRoute(record);
17033
+ const sub = record.bgSub;
17034
+ if (!sub?.attachedRoute)
17035
+ continue;
17036
+ sub.stop().then(() => {
17037
+ if (record.bgSub !== sub)
17038
+ return;
17039
+ record.bgSub = null;
17040
+ this.ensureBgSubscription(record.identity, record);
17041
+ });
17042
+ }
17043
+ }
17044
+ logHostScopeOutcome(session, outcome) {
17045
+ let key;
17046
+ let detail;
17047
+ switch (outcome.result) {
17048
+ case "scope":
17049
+ key = JSON.stringify([outcome.result, outcome.scope.ref, outcome.scope.scopeEpoch]);
17050
+ detail = `scope ref=${outcome.scope.ref.slice(0, 8)} epoch=${outcome.scope.scopeEpoch}`;
17051
+ break;
17052
+ case "error":
17053
+ key = JSON.stringify([outcome.result, outcome.code]);
17054
+ detail = `error code=${outcome.code} after=${outcome.afterMs}`;
17055
+ break;
17056
+ case "timeout":
17057
+ key = outcome.result;
17058
+ detail = `timeout after=${outcome.afterMs}`;
17059
+ break;
17060
+ default:
17061
+ key = outcome.result;
17062
+ detail = outcome.result;
17063
+ }
17064
+ if (this.hostScopeOutcomes.get(session) === key)
17065
+ return;
17066
+ this.hostScopeOutcomes.set(session, key);
17067
+ log(`subc head scope: session=${session} result=${detail}`);
17068
+ }
17069
+ async readHostScope(client, identity, signal) {
17070
+ const startedAt = Date.now();
17071
+ const timeout = () => ({ result: "timeout", afterMs: Date.now() - startedAt });
17072
+ let expired = false;
17073
+ let timer;
17074
+ const abort = new AbortController;
17075
+ let cancel;
17076
+ const cancelled = new Promise((resolve) => {
17077
+ cancel = () => {
17078
+ expired = true;
17079
+ abort.abort();
17080
+ resolve({ result: "error", code: "AbortError", afterMs: Date.now() - startedAt });
17081
+ };
17082
+ signal.addEventListener("abort", cancel, { once: true });
17083
+ });
17084
+ const read = async () => {
17085
+ let route;
17086
+ try {
17087
+ route = await client.routeOpen({ kind: "management_surface", module_id: "prefrontal-core" }, identity, { consumerIdentity: this.consumerIdentity });
17088
+ if (expired)
17089
+ return timeout();
17090
+ const envelope = await client.request(route, {
17091
+ method: "session.scope_identity",
17092
+ params: { harness: identity.harness, session: identity.session }
17093
+ }, { timeoutMs: HOST_SCOPE_LOOKUP_TIMEOUT_MS, signal: abort.signal });
17094
+ const reply = isRecord(envelope) ? envelope.result : undefined;
17095
+ if (isRecord(reply) && reply.answer === "no_scope")
17096
+ return { result: "no_scope" };
17097
+ 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)
17098
+ return { result: "malformed" };
17099
+ return {
17100
+ result: "scope",
17101
+ scope: {
17102
+ owner: { kind: "reserved", module_id: "prefrontal-core" },
17103
+ ref: reply.ref,
17104
+ scopeEpoch: reply.scope_epoch
17105
+ }
17106
+ };
17107
+ } catch (error) {
17108
+ if (expired)
17109
+ return timeout();
17110
+ return {
17111
+ result: "error",
17112
+ code: error instanceof SubcError && error.code ? error.code : error instanceof Error ? error.name : "Error",
17113
+ afterMs: Date.now() - startedAt
17114
+ };
17115
+ } finally {
17116
+ if (route)
17117
+ safeCloseRoute(client, route);
17118
+ }
17119
+ };
17120
+ try {
17121
+ return await Promise.race([
17122
+ read(),
17123
+ cancelled,
17124
+ new Promise((resolve) => {
17125
+ timer = setTimeout(() => {
17126
+ expired = true;
17127
+ abort.abort();
17128
+ resolve(timeout());
17129
+ }, HOST_SCOPE_LOOKUP_TIMEOUT_MS);
17130
+ })
17131
+ ]);
17132
+ } finally {
17133
+ clearTimeout(timer);
17134
+ signal.removeEventListener("abort", cancel);
17135
+ }
17136
+ }
17137
+ openHostToolRoute(client, identity, record, entry) {
17138
+ if (!this.hostScopes || !["opencode", "pi"].includes(identity.harness) || identity.session === DEFAULT_SESSION_ID) {
17139
+ return client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, {
17140
+ consumerIdentity: this.consumerIdentity
17141
+ });
17142
+ }
17143
+ return this.openScopedHostToolRoute(client, identity, record, entry);
17144
+ }
17145
+ async openScopedHostToolRoute(client, identity, record, entry) {
17146
+ const scope = await this.hostScope(client, identity);
17147
+ this.assertRecordLive(record);
17148
+ if (scope) {
17149
+ try {
17150
+ const route = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, { consumerIdentity: this.consumerIdentity, scope });
17151
+ if (entry)
17152
+ entry.hostScope = scope;
17153
+ return route;
17154
+ } catch (error) {
17155
+ if (!(error instanceof SubcError) || !error.code || isSocketDeathError(error))
17156
+ throw error;
17157
+ if (isRouteOpenReloadWindowError(error))
17158
+ throw error;
17159
+ this.assertRecordLive(record);
17160
+ const state = this.hostScopeCache.get(identity.session);
17161
+ if (state && isHostScopeSyncRefusal(error.code)) {
17162
+ this.deferHostScopeBind(identity.session, state, error.code);
17163
+ } else {
17164
+ this.refuseHostScopeBind(identity.session, state, error.code);
17165
+ }
17166
+ }
17167
+ }
17168
+ this.assertRecordLive(record);
17169
+ const route = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, {
17170
+ consumerIdentity: this.consumerIdentity
17171
+ });
17172
+ if (this.hostScopeCache.get(identity.session)?.answer?.result === "scope" && !this.hostScopeCache.get(identity.session)?.scopeBindPending && !this.hostScopeRefusals.has(identity.session)) {
17173
+ safeCloseRoute(client, route);
17174
+ return this.openScopedHostToolRoute(client, identity, record, entry);
17175
+ }
17176
+ return route;
17177
+ }
17178
+ invalidateHostScope(session) {
17179
+ const state = this.hostScopeCache.get(session);
17180
+ if (state) {
17181
+ if (state.scopeBindPending)
17182
+ log(`subc head scope: session=${session} retry stopped reason=scope_route_closed`);
17183
+ this.cancelHostScopeRetry(state);
17184
+ }
17185
+ this.hostScopeCache.delete(session);
17186
+ this.hostScopeRefusals.delete(session);
17187
+ for (const record of this.sessions.values()) {
17188
+ if (record.identity.session !== session || !record.routeEntry)
17189
+ continue;
17190
+ this.retireHostToolRoute(record);
17191
+ }
17192
+ }
17193
+ onScopeRouteClose(push) {
17194
+ if (push.op !== "route.closed")
17195
+ return;
17196
+ const reason = routeCloseReason(push);
17197
+ if (!reason?.startsWith("scope_")) {
17198
+ for (const record of this.sessions.values()) {
17199
+ const route = record.routeEntry?.handle;
17200
+ if (!route)
17201
+ continue;
17202
+ if (Array.isArray(push.body.channels) && push.body.channels.includes(route.channel) || !Array.isArray(push.body.channels) && push.body.module_id === AFT_MODULE_ID)
17203
+ this.retireHostToolRoute(record);
17204
+ }
17205
+ return;
17206
+ }
17207
+ const sessions = new Set;
17208
+ for (const record of this.sessions.values()) {
17209
+ const routes = [record.routeEntry?.handle, record.bgSub?.attachedRoute].filter((route) => route != null);
17210
+ 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)
17211
+ sessions.add(record.identity.session);
17212
+ }
17213
+ for (const session of sessions)
17214
+ this.invalidateHostScope(session);
17215
+ }
16603
17216
  ensureBgSubscription(identity, record) {
16604
17217
  if (this.shuttingDown || !this.onBgEventsNudge && !this.onBgEventsNudgeRef)
16605
17218
  return;
@@ -16639,17 +17252,28 @@ class SubcTransportPool {
16639
17252
  return;
16640
17253
  this.logNudgeDelivery("drop-no-compatible-handler", currentRecord, `nudge dropped cause=generation-provenance-unavailable-and-root-session-handler-unwired root=${currentRecord.canonicalRoot}`);
16641
17254
  };
17255
+ const binding = {};
16642
17256
  let sub = null;
16643
17257
  const clearDormantSubscription = () => {
16644
17258
  if (sub && record.bgSub === sub)
16645
17259
  record.bgSub = null;
16646
17260
  };
16647
- sub = new BgSubscription(identity, () => this.ensureClient(), (client, cause) => this.dropClient(client, cause), this.consumerIdentity, onNudge, this.bgBackoffSleep, () => this.rootCanAttach(record.canonicalRoot), (error) => {
17261
+ sub = new BgSubscription(identity, () => this.ensureClient(), (client, cause) => this.dropClient(client, cause), (client) => {
17262
+ binding.hostScope = undefined;
17263
+ return this.openHostToolRoute(client, identity, record, binding);
17264
+ }, onNudge, this.bgBackoffSleep, () => this.rootCanAttach(record.canonicalRoot), (error) => {
16648
17265
  if (!isAbsentRootRouteError(error))
16649
17266
  return false;
16650
17267
  this.markRootDormant(record.canonicalRoot, error);
16651
17268
  return true;
16652
- }, 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));
17269
+ }, 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), () => {
17270
+ const state = this.hostScopeCache.get(identity.session);
17271
+ if (state)
17272
+ this.scheduleHostScopeRetry(identity.session, state);
17273
+ if (state?.answer?.result === "scope" && !state.scopeBindPending && !binding.hostScope && !this.hostScopeRefusals.has(identity.session)) {
17274
+ this.promoteHostScope(identity.session);
17275
+ }
17276
+ });
16653
17277
  record.bgSub = sub;
16654
17278
  if (!this.rootCanAttach(record.canonicalRoot)) {
16655
17279
  record.bgSub = null;
@@ -16685,6 +17309,8 @@ class SubcTransportPool {
16685
17309
  record.routeEntry = null;
16686
17310
  this.deleteSessionIfEmpty(record.identityKey, record);
16687
17311
  }
17312
+ for (const session of this.hostScopeCache.keys())
17313
+ this.stopUnusedHostScopeRetry(session);
16688
17314
  this.transportFailures = 0;
16689
17315
  try {
16690
17316
  client.close();
@@ -16822,6 +17448,13 @@ class SubcTransportPool {
16822
17448
  this.generationRejections.clear();
16823
17449
  this.observedStarts.clear();
16824
17450
  this.dormantRoots.clear();
17451
+ const scopeLookups = [...this.hostScopeCache.values()].flatMap((state) => {
17452
+ this.cancelHostScopeRetry(state);
17453
+ return state.pending ? [state.pending] : [];
17454
+ });
17455
+ this.hostScopeCache.clear();
17456
+ this.hostScopeRefusals.clear();
17457
+ this.hostScopeOutcomes.clear();
16825
17458
  const registration = this.lifecycleRegistration;
16826
17459
  registration?.deregister();
16827
17460
  this.lifecycleRegistration = null;
@@ -16832,11 +17465,16 @@ class SubcTransportPool {
16832
17465
  detached.push(owner);
16833
17466
  }
16834
17467
  this.rootIndex.clear();
17468
+ this.sessionIndex.clear();
16835
17469
  this.transports.clear();
16836
17470
  const client = this.client;
16837
17471
  this.client = null;
16838
17472
  const pending = [...this.pendingRootCleanups];
16839
- await Promise.allSettled([...detached.map((owner) => this.cleanupDetached(owner)), ...pending]);
17473
+ await Promise.allSettled([
17474
+ ...detached.map((owner) => this.cleanupDetached(owner)),
17475
+ ...pending,
17476
+ ...scopeLookups
17477
+ ]);
16840
17478
  if (client) {
16841
17479
  try {
16842
17480
  client.close();
@@ -16850,6 +17488,13 @@ class SubcTransportPool {
16850
17488
  session: session && session.length > 0 ? session : DEFAULT_SESSION_ID
16851
17489
  };
16852
17490
  const owner = this.detachSession(identityKey(identity), "session_closed");
17491
+ if (!this.sessionIndex.has(identity.session)) {
17492
+ const state = this.hostScopeCache.get(identity.session);
17493
+ if (state)
17494
+ this.cancelHostScopeRetry(state);
17495
+ this.hostScopeCache.delete(identity.session);
17496
+ this.hostScopeRefusals.delete(identity.session);
17497
+ }
16853
17498
  if (owner)
16854
17499
  await this.cleanupDetached(owner);
16855
17500
  }
@@ -16890,6 +17535,8 @@ var BASH_TRANSPORT_DISPOSITION = "The transport to the AFT daemon was interrupte
16890
17535
  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.";
16891
17536
  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.";
16892
17537
  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.";
17538
+ 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.";
17539
+ 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.";
16893
17540
  function isRouteClosedMidCallError(error) {
16894
17541
  return error instanceof SubcRouteClosedMidCallError || isLocalRouteCloseRejection(error);
16895
17542
  }
@@ -16951,6 +17598,8 @@ function classifyBashHostFallbackError(error) {
16951
17598
  return;
16952
17599
  if (isRouteClosedMidCallError(error))
16953
17600
  return;
17601
+ if (isSubcFrameReadTimeoutError(error))
17602
+ return;
16954
17603
  if (error instanceof SubcTransportShuttingDownError)
16955
17604
  return "transport down";
16956
17605
  if (isSubcRouteTornDownError(error))
@@ -16974,6 +17623,14 @@ function isTransportClassError(error) {
16974
17623
  function adaptToolError(command, error) {
16975
17624
  if (!(error instanceof Error))
16976
17625
  return error;
17626
+ if (isSubcFrameReadTimeoutError(error)) {
17627
+ const typedError = error instanceof SubcFrameReadTimeoutError ? error : new SubcFrameReadTimeoutError(error);
17628
+ const disposition = command === "bash" ? BASH_FRAME_READ_TIMEOUT_DISPOSITION : SUBC_FRAME_READ_TIMEOUT_DISPOSITION;
17629
+ if (typedError.message.includes(disposition))
17630
+ return typedError;
17631
+ typedError.message = typedError.message ? `${typedError.message} ${disposition}` : disposition;
17632
+ return typedError;
17633
+ }
16977
17634
  if (error instanceof BridgeTransportUnknownOutcomeError) {
16978
17635
  if (error.message.includes(BRIDGE_TRANSPORT_UNKNOWN_OUTCOME_DISPOSITION))
16979
17636
  return error;
@@ -17055,7 +17712,12 @@ var RETIRED_PATHS = [
17055
17712
  ["semantic_search", "indexes.semantic"],
17056
17713
  ["experimental_semantic_search", "indexes.semantic"],
17057
17714
  ["callgraph_store", "indexes.callgraph"],
17058
- ["github.enabled", "github.read,github.write,github.shim"]
17715
+ ["github.enabled", "github.read,github.write,github.shim"],
17716
+ ["experimental_lsp_ty", "experimental.lsp_ty"],
17717
+ ["experimental_bash_rewrite", "bash.rewrite"],
17718
+ ["experimental_bash_compress", "bash.compress"],
17719
+ ["experimental_bash_background", "bash.background"],
17720
+ ["experimental.bash", "bash"]
17059
17721
  ];
17060
17722
  var INDEX_INPUTS = [
17061
17723
  ["trigram", "search_index", "experimental_search_index"],
@@ -17187,10 +17849,68 @@ function translateInspectLspPaths(map, prefix, out) {
17187
17849
  recordRetired(out, prefix, `inspect.${leaf}`);
17188
17850
  }
17189
17851
  }
17852
+ function translateExperimentalPaths(map, prefix, out) {
17853
+ for (const [oldKey, leaf] of [
17854
+ ["experimental_lsp_ty", "lsp_ty"],
17855
+ ["experimental_bash_rewrite", "rewrite"],
17856
+ ["experimental_bash_compress", "compress"],
17857
+ ["experimental_bash_background", "background"]
17858
+ ]) {
17859
+ if (!Object.hasOwn(map, oldKey))
17860
+ continue;
17861
+ recordRetired(out, prefix, oldKey);
17862
+ const value = map[oldKey];
17863
+ delete map[oldKey];
17864
+ if (!isRecord2(map.experimental))
17865
+ map.experimental = {};
17866
+ const experimental = map.experimental;
17867
+ let destination = experimental;
17868
+ if (leaf !== "lsp_ty") {
17869
+ if (!isRecord2(experimental.bash))
17870
+ experimental.bash = {};
17871
+ destination = experimental.bash;
17872
+ }
17873
+ if (Object.hasOwn(destination, leaf)) {
17874
+ superseded(out, oldKey, `${oldKey} is ignored because experimental.${leaf === "lsp_ty" ? leaf : `bash.${leaf}`} is already set`);
17875
+ } else {
17876
+ destination[leaf] = value;
17877
+ }
17878
+ }
17879
+ if (!isRecord2(map.experimental) || !Object.hasOwn(map.experimental, "bash"))
17880
+ return;
17881
+ const experimental = map.experimental;
17882
+ const legacy = experimental.bash;
17883
+ if (isRecord2(legacy)) {
17884
+ if (!["rewrite", "compress", "background"].some((leaf) => Object.hasOwn(legacy, leaf)))
17885
+ return;
17886
+ for (const leaf of Object.keys(legacy))
17887
+ recordRetired(out, prefix, `experimental.bash.${leaf}`);
17888
+ if (Object.hasOwn(map, "bash")) {
17889
+ superseded(out, "experimental.bash", 'experimental.bash is ignored because top-level "bash" is already set');
17890
+ } else {
17891
+ const bash = {
17892
+ rewrite: legacy.rewrite === true,
17893
+ compress: legacy.compress === true,
17894
+ background: legacy.background === true
17895
+ };
17896
+ for (const leaf of ["long_running_reminder_enabled", "long_running_reminder_interval_ms"]) {
17897
+ if (Object.hasOwn(legacy, leaf))
17898
+ bash[leaf] = legacy[leaf];
17899
+ }
17900
+ map.bash = bash;
17901
+ }
17902
+ } else {
17903
+ recordRetired(out, prefix, "experimental.bash");
17904
+ }
17905
+ delete experimental.bash;
17906
+ if (Object.keys(experimental).length === 0)
17907
+ delete map.experimental;
17908
+ }
17190
17909
  function translateBlock(map, userBase, blockLabel, out) {
17191
17910
  const prefix = blockLabel === "base" ? "" : `${blockLabel}.`;
17192
17911
  translateGithubAliases(map, prefix, out);
17193
17912
  translateInspectLspPaths(map, prefix, out);
17913
+ translateExperimentalPaths(map, prefix, out);
17194
17914
  let explicitList;
17195
17915
  const rawList = map.disabled_tools;
17196
17916
  if (Array.isArray(rawList) && rawList.every((entry) => typeof entry === "string")) {
@@ -17203,6 +17923,8 @@ function translateBlock(map, userBase, blockLabel, out) {
17203
17923
  });
17204
17924
  }
17205
17925
  for (const [path] of RETIRED_PATHS) {
17926
+ if (path === "experimental.bash")
17927
+ continue;
17206
17928
  if (hasPath(map, path))
17207
17929
  recordRetired(out, prefix, path);
17208
17930
  }
@@ -17636,11 +18358,13 @@ class BridgePool {
17636
18358
  return alive;
17637
18359
  }
17638
18360
  getBridge(projectRoot) {
18361
+ return this.getBridgeForCanonicalRoot(asCanonicalRootPath(normalizeKey(projectRoot)));
18362
+ }
18363
+ getBridgeForCanonicalRoot(key) {
17639
18364
  if (this.shutdownCalled) {
17640
18365
  this.shutdownCalled = false;
17641
18366
  this.startCleanupTimer();
17642
18367
  }
17643
- const key = normalizeKey(projectRoot);
17644
18368
  const existing = this.bridges.get(key);
17645
18369
  if (existing) {
17646
18370
  existing.lastUsed = Date.now();
@@ -17846,7 +18570,9 @@ class RevivableTransportPool {
17846
18570
  this.activePool = initialPool;
17847
18571
  }
17848
18572
  getBridge(projectRoot) {
17849
- const key = canonicalizeProjectRoot(projectRoot);
18573
+ return this.getBridgeForCanonicalRoot(asCanonicalRootPath(canonicalizeProjectRoot(projectRoot)));
18574
+ }
18575
+ getBridgeForCanonicalRoot(key) {
17850
18576
  let transport = this.transports.get(key);
17851
18577
  if (!transport) {
17852
18578
  transport = new RevivableProjectTransport(this, key);
@@ -17881,7 +18607,7 @@ class RevivableTransportPool {
17881
18607
  return transport;
17882
18608
  }
17883
18609
  async toolCall(projectRoot, runtime, name, rawArgs = {}, options) {
17884
- return this.getBridge(projectRoot).toolCall(runtime.sessionID, name, rawArgs, options);
18610
+ return this.toolCallForCanonicalRoot(asCanonicalRootPath(canonicalizeProjectRoot(projectRoot)), runtime.sessionID, name, rawArgs, options);
17885
18611
  }
17886
18612
  setConfigureOverride(key, value) {
17887
18613
  if (key === "edit_slot_survives") {
@@ -18021,14 +18747,22 @@ class RevivableTransportPool {
18021
18747
  return this.activePool?.getActiveBridgeForRoot(projectRoot) ?? null;
18022
18748
  }
18023
18749
  async send(projectRoot, command, params, options) {
18750
+ return this.sendForCanonicalRoot(asCanonicalRootPath(canonicalizeProjectRoot(projectRoot)), command, params, options);
18751
+ }
18752
+ async sendForCanonicalRoot(projectRoot, command, params, options) {
18753
+ this.getBridgeForCanonicalRoot(projectRoot);
18024
18754
  const pool = await this.ensureActivePool();
18025
- const bridge = pool.getBridge(projectRoot);
18755
+ const bridge = pool.getBridgeForCanonicalRoot?.(projectRoot) ?? pool.getBridge(projectRoot);
18026
18756
  this.transports.get(projectRoot)?.refreshStatusSubscription(bridge);
18027
18757
  return bridge.send(command, params, options);
18028
18758
  }
18029
18759
  async toolCallOnProject(projectRoot, sessionId, name, rawArgs, options) {
18760
+ return this.toolCallForCanonicalRoot(asCanonicalRootPath(canonicalizeProjectRoot(projectRoot)), sessionId, name, rawArgs, options);
18761
+ }
18762
+ async toolCallForCanonicalRoot(projectRoot, sessionId, name, rawArgs, options) {
18763
+ this.getBridgeForCanonicalRoot(projectRoot);
18030
18764
  const pool = await this.ensureActivePool();
18031
- const bridge = pool.getBridge(projectRoot);
18765
+ const bridge = pool.getBridgeForCanonicalRoot?.(projectRoot) ?? pool.getBridge(projectRoot);
18032
18766
  this.transports.get(projectRoot)?.refreshStatusSubscription(bridge);
18033
18767
  return bridge.toolCall(sessionId, name, rawArgs, options);
18034
18768
  }
@@ -18106,7 +18840,7 @@ function satisfyLifecycleDemand(wrapper, projectRoot) {
18106
18840
  const activePool = wrapper.activePool;
18107
18841
  if (activePool.isShutdown() || !(activePool instanceof SubcTransportPool))
18108
18842
  return;
18109
- activePool.getBridge(projectRoot);
18843
+ activePool.getBridgeForCanonicalRoot(asCanonicalRootPath(projectRoot));
18110
18844
  }
18111
18845
  async function createAftTransportPool(opts) {
18112
18846
  let binaryPath = opts.binaryPath;
@@ -18141,8 +18875,8 @@ async function createAftTransportPool(opts) {
18141
18875
  });
18142
18876
  if (reapingEnabled) {
18143
18877
  const owner = wrapper;
18144
- const getBridge = owner.getBridge.bind(owner);
18145
- owner.getBridge = (projectRoot) => {
18878
+ const getBridge = owner.getBridgeForCanonicalRoot.bind(owner);
18879
+ owner.getBridgeForCanonicalRoot = (projectRoot) => {
18146
18880
  satisfyLifecycleDemand(owner, projectRoot);
18147
18881
  return getBridge(projectRoot);
18148
18882
  };
@@ -18168,6 +18902,7 @@ async function createConcreteAftTransportPool(opts) {
18168
18902
  throw new Error(missing);
18169
18903
  }
18170
18904
  return new SubcTransportPool({
18905
+ hostScopes: opts.harness === "opencode" || opts.harness === "pi",
18171
18906
  connectionFile,
18172
18907
  harness: opts.harness,
18173
18908
  consumerIdentity: opts.subcConsumerIdentity,
@@ -18429,7 +19164,7 @@ import { homedir as homedir9, tmpdir as tmpdir3 } from "node:os";
18429
19164
  import { basename as basename2, dirname as dirname6, join as join12, resolve as resolve8 } from "node:path";
18430
19165
 
18431
19166
  // ../aft-bridge/dist/paths.js
18432
- import { existsSync as existsSync5, readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync2 } from "node:fs";
19167
+ import { existsSync as existsSync5, readFileSync as readFileSync5, realpathSync as realpathSync3, renameSync as renameSync3, writeFileSync as writeFileSync2 } from "node:fs";
18433
19168
  import { homedir as homedir7 } from "node:os";
18434
19169
  import { dirname as dirname4, isAbsolute as isAbsolute5, join as join10, resolve as resolve7 } from "node:path";
18435
19170
  function homeDir3() {
@@ -18464,6 +19199,23 @@ function resolveCortexKitUserConfigPath() {
18464
19199
  function resolveCortexKitProjectConfigPath(projectDirectory) {
18465
19200
  return join10(projectDirectory, ".cortexkit", "aft.jsonc");
18466
19201
  }
19202
+ function resolveProjectConfigReadPath(projectDirectory, operatingHarness) {
19203
+ const canonical = resolveCortexKitProjectConfigPath(projectDirectory);
19204
+ if (existsSync5(canonical))
19205
+ return canonical;
19206
+ const sources = resolveLegacyAftConfigSources(projectDirectory).project;
19207
+ const ordered = operatingHarness ? [
19208
+ ...sources.filter((source) => source.harness === operatingHarness),
19209
+ ...sources.filter((source) => source.harness !== operatingHarness)
19210
+ ] : sources;
19211
+ return ordered.find((source) => existsSync5(source.path))?.path ?? canonical;
19212
+ }
19213
+ function legacyProjectConfigLocationNotice(projectDirectory, path) {
19214
+ const canonical = resolveCortexKitProjectConfigPath(projectDirectory);
19215
+ if (path === canonical)
19216
+ return null;
19217
+ return `Legacy project config ${path} is read in place; move it to ${canonical} yourself, and use aft doctor --fix to update retired keys.`;
19218
+ }
18467
19219
  function resolveCortexKitConfigPaths(projectDirectory) {
18468
19220
  return {
18469
19221
  userConfigPath: resolveCortexKitUserConfigPath(),
@@ -18471,15 +19223,27 @@ function resolveCortexKitConfigPaths(projectDirectory) {
18471
19223
  };
18472
19224
  }
18473
19225
  function resolveLegacyAftConfigSources(projectDirectory) {
19226
+ const project = [
19227
+ ...legacySources(join10(projectDirectory, ".opencode", "aft"), "OpenCode project", "opencode"),
19228
+ ...legacySources(join10(projectDirectory, ".pi", "aft"), "Pi project", "pi")
19229
+ ];
19230
+ const identity = (path) => {
19231
+ try {
19232
+ return realpathSync3(path);
19233
+ } catch {
19234
+ return resolve7(path);
19235
+ }
19236
+ };
19237
+ const protectedPaths = new Set([
19238
+ resolveCortexKitProjectConfigPath(projectDirectory),
19239
+ ...project.map((source) => source.path)
19240
+ ].map(identity));
18474
19241
  return {
18475
19242
  user: [
18476
19243
  ...legacySources(join10(legacyOpenCodeConfigDir(), "aft"), "OpenCode user", "opencode"),
18477
19244
  ...legacySources(join10(legacyPiAgentDir(), "aft"), "Pi user", "pi")
18478
- ],
18479
- project: [
18480
- ...legacySources(join10(projectDirectory, ".opencode", "aft"), "OpenCode project", "opencode"),
18481
- ...legacySources(join10(projectDirectory, ".pi", "aft"), "Pi project", "pi")
18482
- ]
19245
+ ].filter((source) => !protectedPaths.has(identity(source.path))),
19246
+ project
18483
19247
  };
18484
19248
  }
18485
19249
  function resolveHarnessStoragePath(storageRoot, harness, ...segments) {
@@ -19667,7 +20431,7 @@ function isNpmAvailable(deps = defaultDeps()) {
19667
20431
  return resolveNpm(deps) !== null;
19668
20432
  }
19669
20433
  // ../aft-bridge/dist/onnx-probe.js
19670
- import { closeSync as closeSync7, existsSync as existsSync8, openSync as openSync6, readSync as readSync5, realpathSync as realpathSync3 } from "node:fs";
20434
+ import { closeSync as closeSync7, existsSync as existsSync8, openSync as openSync6, readSync as readSync5, realpathSync as realpathSync4 } from "node:fs";
19671
20435
  var MH_MAGIC = 4277009102;
19672
20436
  var MH_MAGIC_64 = 4277009103;
19673
20437
  var FAT_MAGIC = 3405691582;
@@ -19754,7 +20518,7 @@ function isUncheckableDependency(path) {
19754
20518
  function probeOnnxRuntimeLoadable(libPath, arch = process.arch) {
19755
20519
  let fd;
19756
20520
  try {
19757
- fd = openSync6(realpathSync3(libPath), "r");
20521
+ fd = openSync6(realpathSync4(libPath), "r");
19758
20522
  } catch (err) {
19759
20523
  return {
19760
20524
  loadable: false,
@@ -19793,7 +20557,7 @@ function probeOnnxRuntimeLoadable(libPath, arch = process.arch) {
19793
20557
  }
19794
20558
  // ../aft-bridge/dist/onnx-runtime.js
19795
20559
  import { randomBytes as randomBytes2 } from "node:crypto";
19796
- 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";
20560
+ 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";
19797
20561
  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";
19798
20562
  import { Readable as Readable2 } from "node:stream";
19799
20563
  import { pipeline as pipeline2 } from "node:stream/promises";
@@ -20077,7 +20841,7 @@ function detectOnnxVersion(libDir, libName) {
20077
20841
  try {
20078
20842
  const entries = readdirSync3(libDir);
20079
20843
  try {
20080
- const resolved = parseOnnxVersionFromPath(realpathSync4(join16(libDir, libName)));
20844
+ const resolved = parseOnnxVersionFromPath(realpathSync5(join16(libDir, libName)));
20081
20845
  if (resolved && resolved !== INVALID_ORT_VERSION)
20082
20846
  return resolved;
20083
20847
  } catch {}
@@ -20094,7 +20858,7 @@ function detectOnnxVersion(libDir, libName) {
20094
20858
  const base = join16(libDir, libName);
20095
20859
  if (existsSync10(base)) {
20096
20860
  try {
20097
- const real = realpathSync4(base);
20861
+ const real = realpathSync5(base);
20098
20862
  const version = parseOnnxVersionFromPath(real) ?? parseOnnxVersionFromDirectoryPath(real);
20099
20863
  if (version)
20100
20864
  return version;
@@ -20286,7 +21050,7 @@ async function downloadFileWithCap(url, destPath) {
20286
21050
  }
20287
21051
  }
20288
21052
  function validateExtractedTree(stagingRoot) {
20289
- const realRoot = realpathSync4(stagingRoot);
21053
+ const realRoot = realpathSync5(stagingRoot);
20290
21054
  let totalBytes = 0;
20291
21055
  const walk = (dir) => {
20292
21056
  const entries = readdirSync3(dir);
@@ -20435,7 +21199,7 @@ function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, c
20435
21199
  log(`ORT extract: failed to copy optional ${libFile}: ${copyErr}`);
20436
21200
  }
20437
21201
  }
20438
- const targetRoot = realpathSync4(targetDir);
21202
+ const targetRoot = realpathSync5(targetDir);
20439
21203
  for (const link of symlinks) {
20440
21204
  const dst = join16(targetDir, link.name);
20441
21205
  try {
@@ -21255,7 +22019,7 @@ function formatSemanticIndexStatus(status, stage, error, backend) {
21255
22019
  return status;
21256
22020
  }
21257
22021
  // src/config.ts
21258
- import { existsSync as existsSync11, readFileSync as readFileSync10, renameSync as renameSync7, statSync as statSync10, unlinkSync as unlinkSync5, writeFileSync as writeFileSync6 } from "node:fs";
22022
+ import { existsSync as existsSync11, readFileSync as readFileSync10, statSync as statSync10 } from "node:fs";
21259
22023
  import { homedir as homedir12 } from "node:os";
21260
22024
  import { isAbsolute as isAbsolute9, parse as parsePath, resolve as resolvePath } from "node:path";
21261
22025
  var import_comment_json = __toESM(require_src2(), 1);
@@ -26477,155 +27241,6 @@ function resolveBashConfig(config) {
26477
27241
  }
26478
27242
  return { ...base, enabled: true, rewrite: true, compress: true, background: true };
26479
27243
  }
26480
- var CONFIG_MIGRATIONS = [
26481
- { oldKey: "experimental_lsp_ty", newPath: ["experimental", "lsp_ty"] },
26482
- { oldKey: "experimental_bash_rewrite", newPath: ["experimental", "bash", "rewrite"] },
26483
- { oldKey: "experimental_bash_compress", newPath: ["experimental", "bash", "compress"] },
26484
- { oldKey: "experimental_bash_background", newPath: ["experimental", "bash", "background"] }
26485
- ];
26486
- function isWritableMigrationError(errorValue) {
26487
- const code = errorValue?.code;
26488
- return code === "EROFS" || code === "EACCES" || code === "EPERM";
26489
- }
26490
- function extractCommentsForPreservation(content) {
26491
- const comments = [];
26492
- const linePattern = /\/\/[^\n]*/g;
26493
- for (const match of content.match(linePattern) ?? []) {
26494
- comments.push(match.trim());
26495
- }
26496
- const blockPattern = /\/\*[\s\S]*?\*\//g;
26497
- for (const match of content.match(blockPattern) ?? []) {
26498
- comments.push(match.replace(/\s+/g, " ").trim());
26499
- }
26500
- return comments;
26501
- }
26502
- function ensureRecordAtPath(root, path) {
26503
- let current = root;
26504
- for (const segment of path) {
26505
- const existing = current[segment];
26506
- if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
26507
- current[segment] = {};
26508
- }
26509
- current = current[segment];
26510
- }
26511
- return current;
26512
- }
26513
- function hasPath2(root, path) {
26514
- let current = root;
26515
- for (const segment of path) {
26516
- if (!current || typeof current !== "object" || Array.isArray(current))
26517
- return false;
26518
- const record2 = current;
26519
- if (!Object.hasOwn(record2, segment))
26520
- return false;
26521
- current = record2[segment];
26522
- }
26523
- return true;
26524
- }
26525
- function setPath(root, path, value) {
26526
- const parent = ensureRecordAtPath(root, path.slice(0, -1));
26527
- parent[path[path.length - 1]] = value;
26528
- }
26529
- function migrateRawConfig(rawConfig, configPath, logger) {
26530
- const oldKeys = [];
26531
- for (const migration of CONFIG_MIGRATIONS) {
26532
- if (!Object.hasOwn(rawConfig, migration.oldKey))
26533
- continue;
26534
- if (hasPath2(rawConfig, migration.newPath)) {
26535
- logger?.warn(`Config migration conflict at ${configPath}: ${migration.oldKey} ignored because ${migration.newPath.join(".")} is already set`);
26536
- } else {
26537
- setPath(rawConfig, migration.newPath, rawConfig[migration.oldKey]);
26538
- }
26539
- delete rawConfig[migration.oldKey];
26540
- oldKeys.push(migration.oldKey);
26541
- }
26542
- oldKeys.push(...migrateExperimentalBashBlock(rawConfig, configPath, logger));
26543
- return oldKeys;
26544
- }
26545
- function migrateExperimentalBashBlock(rawConfig, configPath, logger) {
26546
- const experimental = rawConfig.experimental;
26547
- if (typeof experimental !== "object" || experimental === null || Array.isArray(experimental)) {
26548
- return [];
26549
- }
26550
- const expRecord = experimental;
26551
- if (!Object.hasOwn(expRecord, "bash"))
26552
- return [];
26553
- const legacyBash = expRecord.bash;
26554
- if (typeof legacyBash !== "object" || legacyBash === null || Array.isArray(legacyBash)) {
26555
- delete expRecord.bash;
26556
- if (Object.keys(expRecord).length === 0)
26557
- delete rawConfig.experimental;
26558
- return ["experimental.bash"];
26559
- }
26560
- const bashRecord = legacyBash;
26561
- const hasFeatureFlag = "rewrite" in bashRecord || "compress" in bashRecord || "background" in bashRecord;
26562
- if (!hasFeatureFlag)
26563
- return [];
26564
- const movedKeys = Object.keys(bashRecord).map((k) => `experimental.bash.${k}`);
26565
- if (Object.hasOwn(rawConfig, "bash")) {
26566
- logger?.warn(`Config migration conflict at ${configPath}: experimental.bash dropped because top-level "bash" is already set`);
26567
- } else {
26568
- const migrated = {
26569
- rewrite: bashRecord.rewrite === true,
26570
- compress: bashRecord.compress === true,
26571
- background: bashRecord.background === true
26572
- };
26573
- if (bashRecord.long_running_reminder_enabled !== undefined) {
26574
- migrated.long_running_reminder_enabled = bashRecord.long_running_reminder_enabled;
26575
- }
26576
- if (bashRecord.long_running_reminder_interval_ms !== undefined) {
26577
- migrated.long_running_reminder_interval_ms = bashRecord.long_running_reminder_interval_ms;
26578
- }
26579
- rawConfig.bash = migrated;
26580
- }
26581
- delete expRecord.bash;
26582
- if (Object.keys(expRecord).length === 0) {
26583
- delete rawConfig.experimental;
26584
- }
26585
- return movedKeys;
26586
- }
26587
- function migrateAftConfigFile2(configPath, logger = { log: log2, warn: warn2 }) {
26588
- if (!existsSync11(configPath)) {
26589
- return { migrated: false, oldKeys: [] };
26590
- }
26591
- let tmpPath = null;
26592
- let oldKeys = [];
26593
- try {
26594
- const content = readFileSync10(configPath, "utf-8");
26595
- const rawConfig = import_comment_json.parse(content);
26596
- if (!rawConfig || typeof rawConfig !== "object" || Array.isArray(rawConfig)) {
26597
- return { migrated: false, oldKeys: [] };
26598
- }
26599
- oldKeys = migrateRawConfig(rawConfig, configPath, logger);
26600
- if (oldKeys.length === 0) {
26601
- return { migrated: false, oldKeys: [] };
26602
- }
26603
- const serialized = `${import_comment_json.stringify(rawConfig, null, 2)}
26604
- `;
26605
- const originalComments = extractCommentsForPreservation(content);
26606
- const droppedComments = originalComments.filter((comment) => !serialized.includes(comment.trim()));
26607
- const nextContent = droppedComments.length > 0 ? `${droppedComments.join(`
26608
- `)}
26609
- ${serialized}` : serialized;
26610
- tmpPath = `${configPath}.tmp.${process.pid}`;
26611
- writeFileSync6(tmpPath, nextContent, "utf-8");
26612
- renameSync7(tmpPath, configPath);
26613
- logger.log(`Migrated config at ${configPath}: removed ${oldKeys.join(", ")}`);
26614
- return { migrated: true, oldKeys };
26615
- } catch (err) {
26616
- if (tmpPath) {
26617
- try {
26618
- unlinkSync5(tmpPath);
26619
- } catch {}
26620
- }
26621
- if (isWritableMigrationError(err)) {
26622
- const errorMsg = err instanceof Error ? err.message : String(err);
26623
- logger.warn(`Config migration could not write ${configPath} (${errorMsg}); using migrated config in memory`);
26624
- return { migrated: oldKeys.length > 0, oldKeys };
26625
- }
26626
- return { migrated: false, oldKeys: [] };
26627
- }
26628
- }
26629
27244
  function parseConfigPartially(rawConfig) {
26630
27245
  let configForParsing = rawConfig;
26631
27246
  if (Object.hasOwn(rawConfig, "edit_mode") && rawConfig.edit_mode !== "default" && rawConfig.edit_mode !== "hashline") {
@@ -26700,7 +27315,6 @@ function loadConfigFromPath(configPath, tier) {
26700
27315
  configLoadSources.push(configPath);
26701
27316
  configLoadTexts.set(configPath, content);
26702
27317
  const rawConfig = import_comment_json.parse(content);
26703
- migrateRawConfig(rawConfig, configPath, { log: log2, warn: warn2 });
26704
27318
  cleanConfig = stripJsoncSymbols(rawConfig);
26705
27319
  } catch (err) {
26706
27320
  const errorMsg = err instanceof Error ? err.message : String(err);
@@ -27060,21 +27674,14 @@ function migrateAftConfigLocations(projectDirectory, logger = { log: log2, warn:
27060
27674
  legacySources: legacy.user,
27061
27675
  operatingHarness: "opencode",
27062
27676
  logger
27063
- }),
27064
- migrateAftConfigFile({
27065
- scope: "project",
27066
- targetPath: paths.projectConfigPath,
27067
- legacySources: legacy.project,
27068
- operatingHarness: "opencode",
27069
- logger
27070
27677
  })
27071
27678
  ];
27072
27679
  }
27073
27680
  function resolveAftConfigPaths(projectDirectory) {
27074
- const paths = resolveCortexKitConfigPaths(projectDirectory);
27075
- migrateAftConfigFile2(paths.userConfigPath);
27076
- migrateAftConfigFile2(paths.projectConfigPath);
27077
- return paths;
27681
+ return {
27682
+ ...resolveCortexKitConfigPaths(projectDirectory),
27683
+ projectConfigPath: resolveProjectConfigReadPath(projectDirectory, ACTIVE_HARNESS)
27684
+ };
27078
27685
  }
27079
27686
  function resolveOpenCodeRegistrationRoot(directory, worktree) {
27080
27687
  if (!worktree)
@@ -27102,6 +27709,15 @@ function loadAftConfig(projectDirectory) {
27102
27709
  configLoadNotices = [];
27103
27710
  semanticInputSupplied = false;
27104
27711
  const { userConfigPath, projectConfigPath } = resolveAftConfigPaths(projectDirectory);
27712
+ const locationNotice = legacyProjectConfigLocationNotice(projectDirectory, projectConfigPath);
27713
+ if (locationNotice) {
27714
+ log2(locationNotice);
27715
+ configLoadNotices.push({
27716
+ configPath: projectConfigPath,
27717
+ digest: "legacy-project-config-location",
27718
+ message: locationNotice
27719
+ });
27720
+ }
27105
27721
  let config = loadConfigFromPath(userConfigPath, "user") ?? {
27106
27722
  disabled_tools: [...DEFAULT_DISABLED_TOOLS]
27107
27723
  };
@@ -39030,6 +39646,24 @@ function markExplicitControl(sessionID, taskId, trackOutstanding = true) {
39030
39646
  function unmarkExplicitControl(sessionID, taskId) {
39031
39647
  stateFor(sessionID).explicitControlTasks.delete(taskId);
39032
39648
  }
39649
+ function holdBgCompletion(sessionID, completion, stopped = false) {
39650
+ const state = stateFor(sessionID);
39651
+ if (!stopped && state.consumedTaskIds.has(completion.task_id))
39652
+ return;
39653
+ state.heldWorkerCompletions = true;
39654
+ state.explicitControlTasks.delete(completion.task_id);
39655
+ state.outstandingTaskIds.delete(completion.task_id);
39656
+ if (!state.pendingCompletions.some((pending) => pending.task_id === completion.task_id)) {
39657
+ state.pendingCompletions.push(completion);
39658
+ }
39659
+ }
39660
+ function hasHeldBgCompletions(sessionID) {
39661
+ const state = stateFor(sessionID);
39662
+ return state.heldWorkerCompletions === true && (state.pendingCompletions.length > 0 || state.pendingPatternMatches.length > 0);
39663
+ }
39664
+ function markHeldBgCompletions(sessionID) {
39665
+ stateFor(sessionID).heldWorkerCompletions = true;
39666
+ }
39033
39667
  function queuePendingPatternMatch(state, entry) {
39034
39668
  const normalized = entry.reason ? entry : { ...entry, reason: "pattern_match" };
39035
39669
  const existingIdx = state.pendingPatternMatches.findIndex((match) => match.task_id === normalized.task_id);
@@ -39115,7 +39749,7 @@ async function appendInTurnBgCompletions(drainContext, output) {
39115
39749
  return;
39116
39750
  const state = stateFor(drainContext.sessionID);
39117
39751
  if (state.outstandingTaskIds.size === 0 && state.pendingCompletions.length === 0 && state.pendingLongRunning.length === 0 && state.pendingPatternMatches.length === 0) {
39118
- if (!idleDrainMayFindWork(state))
39752
+ if (!idleDrainMayFindWork(state, drainContext))
39119
39753
  return;
39120
39754
  await drainCompletions(drainContext);
39121
39755
  if (state.outstandingTaskIds.size === 0 && state.pendingCompletions.length === 0 && state.pendingLongRunning.length === 0 && state.pendingPatternMatches.length === 0) {
@@ -39141,6 +39775,7 @@ async function appendInTurnBgCompletions(drainContext, output) {
39141
39775
  reminder_chars: reminder.length
39142
39776
  });
39143
39777
  state.pendingCompletions = [];
39778
+ state.heldWorkerCompletions = false;
39144
39779
  for (const completion of deliveredCompletions) {
39145
39780
  state.wakeDeferredTaskIds.delete(completion.task_id);
39146
39781
  }
@@ -39494,7 +40129,8 @@ function observeOpenCodeBgNotificationEvent(event) {
39494
40129
  }
39495
40130
  async function triggerWakeIfPending(drainContext, skipDrain, includeDeferredCompletions = true, forceDrain = false, deferWhileBusy = false) {
39496
40131
  const state = stateFor(drainContext.sessionID);
39497
- if (!skipDrain && (forceDrain || state.outstandingTaskIds.size > 0 || !state.forcedDrainCompleted)) {
40132
+ const hasTrackedOrPendingWork = state.outstandingTaskIds.size > 0 || state.pendingCompletions.length > 0 || state.pendingLongRunning.length > 0 || state.pendingPatternMatches.length > 0;
40133
+ if (!skipDrain && (forceDrain || state.outstandingTaskIds.size > 0 || !state.forcedDrainCompleted) && (forceDrain || hasTrackedOrPendingWork || idleDrainMayFindWork(state, drainContext))) {
39498
40134
  await drainCompletions(drainContext);
39499
40135
  }
39500
40136
  routeExplicitControlCompletions(state);
@@ -39729,8 +40365,13 @@ function bridgeForDrain(drainContext) {
39729
40365
  function isTerminalNudgeError(error) {
39730
40366
  return error instanceof Error && error.code === "root_generation_expired";
39731
40367
  }
39732
- function idleDrainMayFindWork(state) {
39733
- return !state.forcedDrainCompleted || state.untrackedCompletionSinceDrain || state.deliveredAwaitingAckTaskIds.size > 0;
40368
+ function idleDrainMayFindWork(state, drainContext) {
40369
+ if (state.untrackedCompletionSinceDrain || state.deliveredAwaitingAckTaskIds.size > 0) {
40370
+ return true;
40371
+ }
40372
+ if (state.forcedDrainCompleted)
40373
+ return false;
40374
+ return Boolean(drainContext.resolvedBridge ?? drainContext.ctx.pool.getActiveBridgeForRoot(drainContext.directory));
39734
40375
  }
39735
40376
  async function drainCompletions(drainContext) {
39736
40377
  const state = stateFor(drainContext.sessionID);
@@ -40530,7 +41171,7 @@ function resolvePathFromProjectRoot(projectRoot, target) {
40530
41171
  async function resolvePathArg(ctx, runtime, target) {
40531
41172
  return resolvePathFromProjectRoot(await resolveProjectRoot(ctx, runtime), target);
40532
41173
  }
40533
- function bridgeFor(ctx, runtime) {
41174
+ function checkedProjectRoot(ctx, runtime) {
40534
41175
  const projectRoot = projectRootFor(runtime);
40535
41176
  if (ctx.isProjectEnabled?.(projectRoot) === false) {
40536
41177
  throw new Error(`AFT disabled by config for ${projectRoot}`);
@@ -40538,7 +41179,7 @@ function bridgeFor(ctx, runtime) {
40538
41179
  if (!existsSync12(projectRoot)) {
40539
41180
  throw new Error(`project directory no longer exists: ${projectRoot} (stale restored session?)`);
40540
41181
  }
40541
- return ctx.pool.getBridge(projectRoot);
41182
+ return projectRoot;
40542
41183
  }
40543
41184
  async function isWorkerRuntime(ctx, runtime) {
40544
41185
  if (!runtime.sessionID)
@@ -40557,6 +41198,9 @@ async function callBridge(ctx, runtime, command, params = {}, options) {
40557
41198
  if (workerSession) {
40558
41199
  merged[WORKER_SESSION_FIELD] = true;
40559
41200
  }
41201
+ if (ctx.hostToolNames) {
41202
+ merged[HOST_TOOL_NAMES_FIELD] = ctx.hostToolNames;
41203
+ }
40560
41204
  const timeoutMs = timeoutForCommand(command);
40561
41205
  const sendOptions = {
40562
41206
  ...timeoutMs !== undefined ? { timeoutMs } : {},
@@ -40567,7 +41211,9 @@ async function callBridge(ctx, runtime, command, params = {}, options) {
40567
41211
  markBridgeStart();
40568
41212
  let response;
40569
41213
  try {
40570
- response = await bridgeFor(ctx, runtime).send(command, merged, Object.keys(sendOptions).length > 0 ? sendOptions : undefined);
41214
+ const root = asCanonicalRootPath(checkedProjectRoot(ctx, runtime));
41215
+ const opts = Object.keys(sendOptions).length > 0 ? sendOptions : undefined;
41216
+ response = ctx.pool.sendForCanonicalRoot ? await ctx.pool.sendForCanonicalRoot(root, command, merged, opts) : await ctx.pool.getBridge(root).send(command, merged, opts);
40571
41217
  } catch (error) {
40572
41218
  if (command === "bash" && isBashTransportDeadError(error))
40573
41219
  throw error;
@@ -40589,12 +41235,15 @@ async function callToolCall(ctx, runtime, name, rawArgs = {}, options) {
40589
41235
  configureWarningClient: ctx.client,
40590
41236
  ...runtime.effectAbort ? { abortSignal: runtime.effectAbort } : {},
40591
41237
  ...workerSession ? { workerSession: true } : {},
41238
+ ...ctx.hostToolNames ? { hostToolNames: ctx.hostToolNames } : {},
40592
41239
  ...options
40593
41240
  };
40594
41241
  markBridgeStart();
40595
41242
  let response;
40596
41243
  try {
40597
- response = await bridgeFor(ctx, runtime).toolCall(runtime.sessionID, name, rawArgs, Object.keys(sendOptions).length > 0 ? sendOptions : undefined);
41244
+ const root = asCanonicalRootPath(checkedProjectRoot(ctx, runtime));
41245
+ const opts = Object.keys(sendOptions).length > 0 ? sendOptions : undefined;
41246
+ response = ctx.pool.toolCallForCanonicalRoot ? await ctx.pool.toolCallForCanonicalRoot(root, runtime.sessionID, name, rawArgs, opts) : await ctx.pool.getBridge(root).toolCall(runtime.sessionID, name, rawArgs, opts);
40598
41247
  } catch (error) {
40599
41248
  throw adaptToolError(name, error);
40600
41249
  } finally {
@@ -40702,7 +41351,7 @@ async function signalBashWaitDetachForProject(pool, projectRoot, sessionID) {
40702
41351
  }
40703
41352
 
40704
41353
  // src/lsp-auto-install.ts
40705
- import { existsSync as existsSync14, mkdirSync as mkdirSync6, renameSync as renameSync9, rmSync as rmSync5, statSync as statSync12, writeFileSync as writeFileSync8 } from "node:fs";
41354
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6, renameSync as renameSync8, rmSync as rmSync5, statSync as statSync12, writeFileSync as writeFileSync7 } from "node:fs";
40706
41355
  import { join as join19 } from "node:path";
40707
41356
 
40708
41357
  // src/lsp-cache.ts
@@ -40712,11 +41361,11 @@ import {
40712
41361
  mkdirSync as mkdirSync5,
40713
41362
  openSync as openSync8,
40714
41363
  readFileSync as readFileSync11,
40715
- renameSync as renameSync8,
41364
+ renameSync as renameSync7,
40716
41365
  rmSync as rmSync4,
40717
41366
  statSync as statSync11,
40718
- unlinkSync as unlinkSync6,
40719
- writeFileSync as writeFileSync7
41367
+ unlinkSync as unlinkSync5,
41368
+ writeFileSync as writeFileSync6
40720
41369
  } from "node:fs";
40721
41370
  import { join as join17 } from "node:path";
40722
41371
  function aftCacheBase() {
@@ -40757,7 +41406,7 @@ function writeInstalledMetaIn(installDir, version, sha256) {
40757
41406
  installedAt: new Date().toISOString(),
40758
41407
  ...sha256 ? { sha256 } : {}
40759
41408
  };
40760
- writeFileSync7(join17(installDir, INSTALLED_META_FILE), JSON.stringify(meta), "utf8");
41409
+ writeFileSync6(join17(installDir, INSTALLED_META_FILE), JSON.stringify(meta), "utf8");
40761
41410
  } catch (err) {
40762
41411
  log2(`[lsp-cache] failed to write installed-meta in ${installDir}: ${err}`);
40763
41412
  }
@@ -40798,7 +41447,7 @@ function acquireInstallLock(lockKey) {
40798
41447
  try {
40799
41448
  const fd = openSync8(lock, "wx");
40800
41449
  try {
40801
- writeFileSync7(fd, `${process.pid}
41450
+ writeFileSync6(fd, `${process.pid}
40802
41451
  ${new Date().toISOString()}
40803
41452
  ${token}
40804
41453
  `);
@@ -40854,7 +41503,7 @@ ${token}
40854
41503
  }
40855
41504
  log2(`[lsp] reclaiming install lock for ${lockKey} (owner_pid=${owningPid ?? "unknown"}, alive=${ownerAlive}, age_ms=${age})`);
40856
41505
  try {
40857
- unlinkSync6(lock);
41506
+ unlinkSync5(lock);
40858
41507
  } catch {}
40859
41508
  return tryClaim();
40860
41509
  }
@@ -40893,7 +41542,7 @@ function releaseInstallLock(lockKey, acquisitionToken) {
40893
41542
  return;
40894
41543
  }
40895
41544
  try {
40896
- unlinkSync6(lock);
41545
+ unlinkSync5(lock);
40897
41546
  } catch (unlinkErr) {
40898
41547
  const code = unlinkErr.code;
40899
41548
  if (code !== "ENOENT") {
@@ -40968,7 +41617,7 @@ function claimLspAutoInstallPass() {
40968
41617
  try {
40969
41618
  const fd = openSync8(lock, "wx");
40970
41619
  try {
40971
- writeFileSync7(fd, JSON.stringify(owner));
41620
+ writeFileSync6(fd, JSON.stringify(owner));
40972
41621
  } finally {
40973
41622
  closeSync9(fd);
40974
41623
  }
@@ -40999,12 +41648,12 @@ function claimLspAutoInstallPass() {
40999
41648
  return null;
41000
41649
  }
41001
41650
  const tempPath = `${stampPath}.tmp-${process.pid}-${owner.token}`;
41002
- writeFileSync7(tempPath, `${JSON.stringify({ lastRunMs: Date.now() })}
41651
+ writeFileSync6(tempPath, `${JSON.stringify({ lastRunMs: Date.now() })}
41003
41652
  `, {
41004
41653
  encoding: "utf8",
41005
41654
  mode: 384
41006
41655
  });
41007
- renameSync8(tempPath, stampPath);
41656
+ renameSync7(tempPath, stampPath);
41008
41657
  return { release };
41009
41658
  } catch (err) {
41010
41659
  if (claimed) {
@@ -41041,7 +41690,7 @@ function writeVersionCheck(npmPackage, latest) {
41041
41690
  last_checked: new Date().toISOString(),
41042
41691
  latest_eligible: latest
41043
41692
  };
41044
- writeFileSync7(file, JSON.stringify(record, null, 2));
41693
+ writeFileSync6(file, JSON.stringify(record, null, 2));
41045
41694
  }
41046
41695
  function shouldRecheckVersion(record, weeklyCheckIntervalMs = 7 * 24 * 60 * 60 * 1000) {
41047
41696
  if (!record)
@@ -41422,7 +42071,7 @@ function ensureInstallAnchor(cwd) {
41422
42071
  try {
41423
42072
  const stub = join19(cwd, "package.json");
41424
42073
  if (!existsSync14(stub)) {
41425
- writeFileSync8(stub, `${JSON.stringify({ name: "aft-lsp-cache", version: "0.0.0", private: true })}
42074
+ writeFileSync7(stub, `${JSON.stringify({ name: "aft-lsp-cache", version: "0.0.0", private: true })}
41426
42075
  `);
41427
42076
  }
41428
42077
  } catch (err) {
@@ -41635,7 +42284,7 @@ function quarantineCachedNpmInstall(spec, reason) {
41635
42284
  try {
41636
42285
  mkdirSync6(join19(dest, ".."), { recursive: true });
41637
42286
  rmSync5(dest, { recursive: true, force: true });
41638
- renameSync9(packageDir, dest);
42287
+ renameSync8(packageDir, dest);
41639
42288
  } catch (err) {
41640
42289
  warn2(`[lsp] tofu_mismatch ${spec.npm}: failed to quarantine cache entry: ${err}`);
41641
42290
  }
@@ -41739,12 +42388,12 @@ import {
41739
42388
  readFileSync as readFileSync13,
41740
42389
  readlinkSync as readlinkSync2,
41741
42390
  readSync as readSync6,
41742
- realpathSync as realpathSync5,
41743
- renameSync as renameSync10,
42391
+ realpathSync as realpathSync6,
42392
+ renameSync as renameSync9,
41744
42393
  rmSync as rmSync6,
41745
42394
  statSync as statSync13,
41746
- unlinkSync as unlinkSync7,
41747
- writeFileSync as writeFileSync9
42395
+ unlinkSync as unlinkSync6,
42396
+ writeFileSync as writeFileSync8
41748
42397
  } from "node:fs";
41749
42398
  import { dirname as dirname10, join as join20, relative as relative5, resolve as resolve12 } from "node:path";
41750
42399
  import { Readable as Readable3 } from "node:stream";
@@ -41870,9 +42519,9 @@ function writeInstallFailureMarker(spec, release, sha256, installError) {
41870
42519
  const tempPath = `${path}.tmp-${process.pid}-${randomBytes4(6).toString("hex")}`;
41871
42520
  try {
41872
42521
  mkdirSync7(dirname10(path), { recursive: true });
41873
- writeFileSync9(tempPath, `${JSON.stringify({ release, sha256, error: markerError, at: new Date().toISOString() })}
42522
+ writeFileSync8(tempPath, `${JSON.stringify({ release, sha256, error: markerError, at: new Date().toISOString() })}
41874
42523
  `, { encoding: "utf8", mode: 384 });
41875
- renameSync10(tempPath, path);
42524
+ renameSync9(tempPath, path);
41876
42525
  } catch (err) {
41877
42526
  try {
41878
42527
  rmSync6(tempPath, { force: true });
@@ -41938,7 +42587,7 @@ function writeGithubInstalledMetaIn(installDir, version, binarySha256, archiveSh
41938
42587
  binarySha256,
41939
42588
  ...archiveSha256 ? { archiveSha256 } : {}
41940
42589
  };
41941
- writeFileSync9(join20(installDir, INSTALLED_META_FILE2), JSON.stringify(meta), "utf8");
42590
+ writeFileSync8(join20(installDir, INSTALLED_META_FILE2), JSON.stringify(meta), "utf8");
41942
42591
  } catch (err) {
41943
42592
  warn2(`[lsp] failed to write github installed metadata in ${installDir}: ${err}`);
41944
42593
  }
@@ -42178,7 +42827,7 @@ async function downloadFile(url, destPath, fetchImpl, assetSize, signal) {
42178
42827
  await pipeline3(nodeStream, createWriteStream3(destPath), { signal: timeout.signal });
42179
42828
  } catch (err) {
42180
42829
  try {
42181
- unlinkSync7(destPath);
42830
+ unlinkSync6(destPath);
42182
42831
  } catch {}
42183
42832
  throw err;
42184
42833
  } finally {
@@ -42207,7 +42856,7 @@ async function fetchFollowingRedirects(url, fetchImpl, signal) {
42207
42856
  throw new Error(`too many redirects (>${maxRedirects})`);
42208
42857
  }
42209
42858
  function validateExtraction(stagingRoot) {
42210
- const realStagingRoot = realpathSync5(stagingRoot);
42859
+ const realStagingRoot = realpathSync6(stagingRoot);
42211
42860
  let totalBytes = 0;
42212
42861
  const walk = (dir) => {
42213
42862
  let entries;
@@ -42233,7 +42882,7 @@ function validateExtraction(stagingRoot) {
42233
42882
  }
42234
42883
  let realFull;
42235
42884
  try {
42236
- realFull = realpathSync5(full);
42885
+ realFull = realpathSync6(full);
42237
42886
  } catch (err) {
42238
42887
  throw new Error(`failed to realpath ${full}: ${err}`);
42239
42888
  }
@@ -42261,14 +42910,30 @@ function assertArchiveEntryPath(entry) {
42261
42910
  throw new Error(`archive entry escapes archive root: ${entry}`);
42262
42911
  }
42263
42912
  }
42913
+ var ARCHIVE_LIST_TIMEOUT_MS = 30000;
42264
42914
  function runArchiveTool(command, args, timeout) {
42265
42915
  return new Promise((resolvePromise, reject) => {
42266
- execFile(command, args, { encoding: "utf8", ...timeout !== undefined ? { timeout } : {} }, (err, stdout) => {
42267
- if (err)
42268
- reject(err);
42269
- else
42270
- resolvePromise(stdout);
42271
- });
42916
+ let child;
42917
+ const deadline = setTimeout(() => {
42918
+ reject(new Error(`${command} ${args[0]} timed out after ${timeout} ms`));
42919
+ child?.kill("SIGKILL");
42920
+ child?.stdin?.destroy();
42921
+ child?.stdout?.destroy();
42922
+ child?.stderr?.destroy();
42923
+ }, timeout);
42924
+ try {
42925
+ child = execFile(command, args, { encoding: "utf8", timeout, killSignal: "SIGKILL" }, (err, stdout) => {
42926
+ clearTimeout(deadline);
42927
+ if (err)
42928
+ reject(err);
42929
+ else
42930
+ resolvePromise(stdout);
42931
+ });
42932
+ child.stdin?.end();
42933
+ } catch (error) {
42934
+ clearTimeout(deadline);
42935
+ reject(error);
42936
+ }
42272
42937
  });
42273
42938
  }
42274
42939
  async function runTarExtraction(args, timeout) {
@@ -42282,7 +42947,7 @@ async function runTarExtraction(args, timeout) {
42282
42947
  }
42283
42948
  async function precheckWithTar(archivePath, command) {
42284
42949
  let totalBytes = 0;
42285
- const verbose = await runArchiveTool(command, ["-tvf", archivePath]);
42950
+ const verbose = await runArchiveTool(command, ["-tvf", archivePath], ARCHIVE_LIST_TIMEOUT_MS);
42286
42951
  for (const line of verbose.split(`
42287
42952
  `)) {
42288
42953
  if (line.startsWith("h")) {
@@ -42295,7 +42960,7 @@ async function precheckWithTar(archivePath, command) {
42295
42960
  totalBytes += Math.max(...numeric);
42296
42961
  }
42297
42962
  }
42298
- const names = await runArchiveTool(command, ["-tf", archivePath]);
42963
+ const names = await runArchiveTool(command, ["-tf", archivePath], ARCHIVE_LIST_TIMEOUT_MS);
42299
42964
  for (const entry of names.split(`
42300
42965
  `)) {
42301
42966
  if (entry.length > 0)
@@ -42306,11 +42971,11 @@ async function precheckWithTar(archivePath, command) {
42306
42971
  async function precheckArchiveContents(archivePath, archiveType, zipListingMode = "platform") {
42307
42972
  let totalBytes = 0;
42308
42973
  if (archiveType === "zip" && zipListingMode === "platform" && process.platform !== "win32") {
42309
- const out = await runArchiveTool("unzip", ["-l", archivePath]);
42974
+ const out = await runArchiveTool("unzip", ["-l", archivePath], ARCHIVE_LIST_TIMEOUT_MS);
42310
42975
  const match = out.match(/^\s*(\d+)\s+\d+\s+files?\s*$/m);
42311
42976
  if (match)
42312
42977
  totalBytes = Number.parseInt(match[1] ?? "0", 10);
42313
- const names = await runArchiveTool("unzip", ["-Z1", archivePath]);
42978
+ const names = await runArchiveTool("unzip", ["-Z1", archivePath], ARCHIVE_LIST_TIMEOUT_MS);
42314
42979
  for (const entry of names.split(`
42315
42980
  `)) {
42316
42981
  if (entry.length > 0)
@@ -42337,7 +43002,7 @@ async function extractArchiveSafely(archivePath, destDir, archiveType) {
42337
43002
  try {
42338
43003
  rmSync6(destDir, { recursive: true, force: true });
42339
43004
  } catch {}
42340
- renameSync10(stagingDir, destDir);
43005
+ renameSync9(stagingDir, destDir);
42341
43006
  } catch (err) {
42342
43007
  try {
42343
43008
  rmSync6(stagingDir, { recursive: true, force: true });
@@ -42352,7 +43017,7 @@ function quarantineCachedGithubInstall(spec, reason) {
42352
43017
  try {
42353
43018
  mkdirSync7(dirname10(dest), { recursive: true });
42354
43019
  rmSync6(dest, { recursive: true, force: true });
42355
- renameSync10(packageDir, dest);
43020
+ renameSync9(packageDir, dest);
42356
43021
  } catch (err) {
42357
43022
  warn2(`[lsp] tofu_mismatch ${spec.id}: failed to quarantine cache entry: ${err}`);
42358
43023
  }
@@ -42444,7 +43109,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
42444
43109
  const message = `hash failed: ${err instanceof Error ? err.message : String(err)}`;
42445
43110
  error2(`[lsp] hash ${spec.id} failed: ${err}`);
42446
43111
  try {
42447
- unlinkSync7(archivePath);
43112
+ unlinkSync6(archivePath);
42448
43113
  } catch {}
42449
43114
  return { ok: false, sha256: "", error: message };
42450
43115
  }
@@ -42455,7 +43120,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
42455
43120
  const message = `TOFU sha256 mismatch: previously installed archive sha256=${previousArchiveSha256}, ` + `downloaded sha256=${archiveSha256}`;
42456
43121
  error2(`[lsp] ${spec.id} ${tag}: ${message}. ` + `This means the published release for tag ${tag} changed. Investigate before proceeding. ` + `Run \`npx @cortexkit/aft doctor --clear\` to wipe the cache and force a fresh install if you've verified the change.`);
42457
43122
  try {
42458
- unlinkSync7(archivePath);
43123
+ unlinkSync6(archivePath);
42459
43124
  } catch {}
42460
43125
  return { ok: false, sha256: archiveSha256, error: message };
42461
43126
  }
@@ -42467,7 +43132,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
42467
43132
  return { ok: false, sha256: archiveSha256, error: message };
42468
43133
  } finally {
42469
43134
  try {
42470
- unlinkSync7(archivePath);
43135
+ unlinkSync6(archivePath);
42471
43136
  } catch {}
42472
43137
  }
42473
43138
  const innerBinaryPath = join20(extractDir, spec.binaryPathInArchive(platform, arch, version));
@@ -43364,6 +44029,190 @@ function astTools(ctx) {
43364
44029
  };
43365
44030
  }
43366
44031
 
44032
+ // src/v2-session-lifecycle.ts
44033
+ import { Effect as Effect4, Stream } from "effect";
44034
+ function noteV2ToolAbort(client, sessionID) {
44035
+ const state = stateFor2(client, sessionID);
44036
+ if (!state || state.settling)
44037
+ return;
44038
+ let resolve = () => {};
44039
+ const promise = new Promise((done) => {
44040
+ resolve = done;
44041
+ });
44042
+ state.settling = { promise, resolve };
44043
+ }
44044
+ async function awaitV2ExecutionSettlement(client, sessionID) {
44045
+ await stateFor2(client, sessionID)?.settling?.promise;
44046
+ }
44047
+ var locations = new WeakMap;
44048
+ function stateFor2(client, sessionID) {
44049
+ if (!client || typeof client !== "object")
44050
+ return;
44051
+ const sessions = locations.get(client);
44052
+ if (!sessions)
44053
+ return;
44054
+ let state = sessions.get(sessionID);
44055
+ if (!state) {
44056
+ state = {
44057
+ stopped: false,
44058
+ stopEpoch: 0,
44059
+ tasks: new Map,
44060
+ wakes: new Set,
44061
+ watches: new Map
44062
+ };
44063
+ sessions.set(sessionID, state);
44064
+ }
44065
+ return state;
44066
+ }
44067
+ function v2SubagentStopped(client, sessionID) {
44068
+ return stateFor2(client, sessionID)?.stopped === true;
44069
+ }
44070
+ function v2StopEpoch(client, sessionID) {
44071
+ return stateFor2(client, sessionID)?.stopEpoch ?? 0;
44072
+ }
44073
+ async function killBackgroundTask(ctx, runtime, taskId) {
44074
+ const result = await callBashBridge(ctx, runtime, "bash_kill", { task_id: taskId }, { abortSignal: undefined });
44075
+ if (result.success === false)
44076
+ throw new Error(String(result.message ?? "bash_kill failed"));
44077
+ markHeldBgCompletions(runtime.sessionID);
44078
+ }
44079
+ async function trackV2BackgroundTask(ctx, runtime, taskId, startedEpoch) {
44080
+ const state = stateFor2(ctx.client, runtime.sessionID);
44081
+ if (!state)
44082
+ return;
44083
+ const kill = () => killBackgroundTask(ctx, runtime, taskId);
44084
+ if (state.stopped || state.stopEpoch !== startedEpoch) {
44085
+ await kill();
44086
+ return;
44087
+ }
44088
+ state.tasks.set(taskId, kill);
44089
+ }
44090
+ async function killStoppedBackgroundTasks(client, sessionID, ctx, startedBeforeMs) {
44091
+ const kills = [];
44092
+ const visited = new Set;
44093
+ const dispatch = (taskId, kill) => {
44094
+ if (visited.has(taskId))
44095
+ return;
44096
+ visited.add(taskId);
44097
+ kills.push(kill().catch((error) => {
44098
+ warn2(`[bash] failed to kill stopped session ${sessionID}'s task ${taskId}: ${String(error)}`);
44099
+ }));
44100
+ };
44101
+ for (const [owner, state] of locations.get(client) ?? []) {
44102
+ if (owner !== sessionID)
44103
+ continue;
44104
+ const tasks = [...state.tasks];
44105
+ state.tasks.clear();
44106
+ for (const [taskId, kill] of tasks) {
44107
+ dispatch(taskId, kill);
44108
+ }
44109
+ }
44110
+ if (ctx) {
44111
+ const runtime = {
44112
+ sessionID,
44113
+ directory: client.location.directory,
44114
+ directoryIsSessionRoot: true
44115
+ };
44116
+ try {
44117
+ const result = await callBashBridge(ctx, runtime, "bash_owned_tasks", { started_before_ms: startedBeforeMs }, { abortSignal: undefined });
44118
+ if (result.success === false)
44119
+ throw new Error(String(result.message ?? "bash_owned_tasks failed"));
44120
+ for (const taskId of Array.isArray(result.task_ids) ? result.task_ids : []) {
44121
+ if (typeof taskId === "string")
44122
+ dispatch(taskId, () => killBackgroundTask(ctx, runtime, taskId));
44123
+ }
44124
+ } catch (error) {
44125
+ warn2(`[bash] failed to enumerate stopped session ${sessionID}'s tasks: ${String(error)}`);
44126
+ }
44127
+ }
44128
+ await Promise.all(kills);
44129
+ }
44130
+ function allowV2WatchWake(client, sessionID, taskId) {
44131
+ const state = stateFor2(client, sessionID);
44132
+ if (state && !state.stopped)
44133
+ state.wakes.add(taskId);
44134
+ }
44135
+ function v2WatchWakeAllowed(client, sessionID, taskId) {
44136
+ const state = stateFor2(client, sessionID);
44137
+ return state !== undefined && !state.stopped && state.wakes.has(taskId);
44138
+ }
44139
+ function forgetV2Watch(client, sessionID, taskId) {
44140
+ const state = stateFor2(client, sessionID);
44141
+ if (!state)
44142
+ return;
44143
+ state.wakes.delete(taskId);
44144
+ state.tasks.delete(taskId);
44145
+ for (const [watchId, watch] of state.watches) {
44146
+ if (watch.taskId === taskId)
44147
+ state.watches.delete(watchId);
44148
+ }
44149
+ }
44150
+ async function trackV2Watch(ctx, runtime, taskId, watchId) {
44151
+ const state = stateFor2(ctx.client, runtime.sessionID);
44152
+ if (!state || typeof watchId !== "string")
44153
+ return;
44154
+ const cancel = () => callBashBridge(ctx, runtime, "bash_unnotify", { task_id: taskId, watch_id: watchId }, { abortSignal: undefined });
44155
+ if (state.stopped) {
44156
+ await cancel();
44157
+ return;
44158
+ }
44159
+ state.wakes.add(taskId);
44160
+ state.watches.set(watchId, { taskId, cancel });
44161
+ }
44162
+ function registerV2SessionLifecycle(context, ctx) {
44163
+ const host = context;
44164
+ return Effect4.gen(function* () {
44165
+ if (!host.event?.subscribe || !context || typeof context !== "object")
44166
+ return;
44167
+ const sessions = new Map;
44168
+ locations.set(context, sessions);
44169
+ yield* Effect4.addFinalizer(() => Effect4.sync(() => {
44170
+ for (const state of sessions.values())
44171
+ state.settling?.resolve();
44172
+ locations.delete(context);
44173
+ }));
44174
+ const pull = yield* Stream.toPull(host.event.subscribe());
44175
+ yield* Effect4.forkScoped(Effect4.forever(pull.pipe(Effect4.flatMap((events) => Effect4.forEach(events, (event) => Effect4.promise(async () => {
44176
+ const sessionID = event.data.sessionID;
44177
+ if (!sessionID)
44178
+ return;
44179
+ const state = stateFor2(context, sessionID);
44180
+ if (!state)
44181
+ return;
44182
+ if (event.type === "session.execution.started") {
44183
+ state.stopped = false;
44184
+ return;
44185
+ }
44186
+ const deliberateStop = event.type === "session.execution.interrupted" && (event.data.reason === "user" || event.data.reason === "inactivity");
44187
+ if (!deliberateStop) {
44188
+ if (event.type === "session.execution.failed" || event.type === "session.execution.succeeded" || event.type === "session.execution.interrupted") {
44189
+ state.settling?.resolve();
44190
+ state.settling = undefined;
44191
+ }
44192
+ return;
44193
+ }
44194
+ if (!await resolveIsSubagent(context, sessionID)) {
44195
+ state.settling?.resolve();
44196
+ state.settling = undefined;
44197
+ return;
44198
+ }
44199
+ state.stopped = true;
44200
+ state.stopEpoch += 1;
44201
+ state.wakes.clear();
44202
+ state.settling?.resolve();
44203
+ state.settling = undefined;
44204
+ const watches = [...state.watches.values()];
44205
+ state.watches.clear();
44206
+ Promise.all([
44207
+ killStoppedBackgroundTasks(context, sessionID, ctx, event.created),
44208
+ ...watches.map((watch) => watch.cancel().catch((error) => {
44209
+ warn2(`[bash_watch] failed to cancel stopped session ${sessionID}'s watch on ${watch.taskId}: ${String(error)}`);
44210
+ }))
44211
+ ]);
44212
+ }), { discard: true })))), { startImmediately: true });
44213
+ });
44214
+ }
44215
+
43367
44216
  // src/tools/bash.ts
43368
44217
  var z3 = tool.schema;
43369
44218
  var METADATA_PREVIEW_LIMIT = 30 * 1024;
@@ -43499,7 +44348,7 @@ function bashToolDescription(aftSearchRegistered, compressionOn, backgroundOn, d
43499
44348
  ].join(", ");
43500
44349
  const searchSteer = `use ${steerTools}${zoomEnabled ? ", or aft_zoom" : ""} instead`;
43501
44350
  const compression = compressionOn ? " Output is compressed by default; pass compressed: false for raw output. Piped commands run verbatim and show the pipeline's output; for AFT's test/build summary, run the runner without | head, | tail, or | grep. Pipeline-failure notes cover single top-level pipelines only; multi-statement commands (`a; b | c; d`) are not instrumented, so masked failures inside them still need explicit exit-code checks." : "";
43502
- const tasks = backgroundOn ? ` 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); ${userMessageDetachDescription(detachOnUserMessage)} Use it when you need the result before doing anything else; ${autoPromote}. Use background: true yourself ONLY when you have other useful work to do while it runs; ${backgroundWaitDescription(watchToolRegistered, status, role)} A \`nohup … &\` launch still holds the call if the child keeps stdout/stderr; redirect both or use background:true. pty: true runs interactive programs (REPLs, TUIs), implies background${ptyDriveClause(status, write)}.` : " Commands run in the foreground to completion; timeout is the hard kill cap (default 30 minutes).";
44351
+ const tasks = backgroundOn ? ` 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); ${userMessageDetachDescription(detachOnUserMessage)} Use it when you need the result before doing anything else; ${autoPromote}. Use background: true yourself ONLY when you have other useful work to do while it runs; ${backgroundWaitDescription(watchToolRegistered, status, role)} A \`nohup … &\` launch still holds the call if the child keeps stdout/stderr; redirect both or use background:true. pty: true runs interactive programs (REPLs, TUIs), implies background${ptyDriveClause(status, write)}.` : " Commands run in the foreground to completion; timeout is the hard kill cap (default 30 minutes).";
43503
44352
  const outputRules = backgroundOn ? " Finished output expires after the task is 24 hours old and its completion has been delivered; under project-root restrictions, only the starting session can read output outside the project—copy cited lines into your report." : "";
43504
44353
  const timeoutRules = " `timeout` starts after spawn and kills the Unix process group (exit 124; Windows uses taskkill /T /F); processes that leave the group survive.";
43505
44354
  const remoteRuns = surface.remoteRuns ? ` ${BASH_RUNON_GUIDANCE}` : "";
@@ -43597,7 +44446,7 @@ function bashTimeoutDescription(backgroundOn, role = "primary") {
43597
44446
  if (!backgroundOn) {
43598
44447
  return "Hard kill cap in milliseconds (positive integer). When omitted, the foreground command can run up to 30 minutes and returns inline when it finishes.";
43599
44448
  }
43600
- const promoted = role === "worker" ? "moves to the background as a task that won't wake you, so wait on it with bash_watch; wait:true disables promotion and remains inline until completion, the timeout, or the worker wait limit" : "is promoted to background and gets a completion reminder when it exits; wait:true disables promotion and remains inline until completion or timeout";
44449
+ const promoted = role === "worker" ? "moves to the background as a task that won't wake you, so wait on it with bash_watch; wait:true disables promotion and remains inline until completion, the timeout, or the worker wait limit" : "is promoted to background and gets a completion reminder when it exits; wait:true disables promotion and remains inline until completion, timeout, or bash.worker_wait_max_ms";
43601
44450
  return `Hard kill cap in milliseconds (positive integer). In the default foreground mode when wait is false, a command that exceeds the configured wait window ${promoted}. A background task with no timeout is killed after 30 minutes; pass a longer timeout for long jobs.`;
43602
44451
  }
43603
44452
  function createBashTool(ctx, aftSearchRegisteredOverride) {
@@ -43609,7 +44458,7 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
43609
44458
  ...bashCompanionRegistered(ctx.config, "bash_kill") ? ["bash_kill"] : []
43610
44459
  ];
43611
44460
  const waitArg = initialBashCfg.background ? {
43612
- wait: z3.boolean().optional().describe(`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); ${userMessageDetachDescription(initialBashCfg.detach_on_user_message)} Use only when you know the result is required before doing anything else.`)
44461
+ wait: z3.boolean().optional().describe(`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); ${userMessageDetachDescription(initialBashCfg.detach_on_user_message)} Use only when you know the result is required before doing anything else.`)
43613
44462
  } : {};
43614
44463
  const sandboxArg = nativeSandboxEnabled(ctx.config) ? {
43615
44464
  sandbox: z3.literal("host").optional().describe("Request one-command approval to run unsandboxed on the host; use only when native sandboxing blocks required work, and note that it is a no-op when sandboxing is disabled.")
@@ -43664,6 +44513,7 @@ ${BASH_RUNON_GUIDANCE}`, "");
43664
44513
  },
43665
44514
  execute: async (args, context) => {
43666
44515
  const bashCfg = resolveBashConfig(ctx.config);
44516
+ const startedEpoch = v2StopEpoch(ctx.client, context.sessionID);
43667
44517
  const ctxAftSearchRegistered = ctx.aftSearchRegistered === true;
43668
44518
  const aftSearchRegistered = aftSearchRegisteredOverride ?? ctxAftSearchRegistered;
43669
44519
  let accumulatedOutput = "";
@@ -43693,7 +44543,7 @@ ${BASH_RUNON_GUIDANCE}`, "");
43693
44543
  const blockToCompletion = subagentForcedForeground || backgroundDisabled || requestedWait;
43694
44544
  const effectiveBackground = blockToCompletion ? false : requestedBackground;
43695
44545
  const rawTimeout = coerceOptionalInt(args.timeout, "timeout", 1, Number.MAX_SAFE_INTEGER);
43696
- const workerWaitMaxMs = isSubagent ? bashCfg.worker_wait_max_ms : undefined;
44546
+ const workerWaitMaxMs = bashCfg.worker_wait_max_ms;
43697
44547
  const ptyRows = coerceOptionalInt(args.ptyRows, "ptyRows", 1, 60);
43698
44548
  const ptyCols = coerceOptionalInt(args.ptyCols, "ptyCols", 1, 140);
43699
44549
  const compressed = coerceBoolean(args.compressed, true);
@@ -43777,6 +44627,7 @@ ${BASH_RUNON_GUIDANCE}`, "");
43777
44627
  if (data.status === "running" && typeof data.task_id === "string") {
43778
44628
  const taskId = data.task_id;
43779
44629
  trackBgTask(context.sessionID, taskId);
44630
+ await trackV2BackgroundTask(ctx, context, taskId, startedEpoch);
43780
44631
  let rendered = data.output ?? "";
43781
44632
  if (isSubagent) {
43782
44633
  rendered += workerBackgroundTaskNote(taskId);
@@ -43849,7 +44700,10 @@ async function formatBashStatusText(runtime, taskId, data, requestedOutputMode,
43849
44700
  if (typeof data.live_descendants_summary === "string") {
43850
44701
  text += ` · ${data.live_descendants_summary}`;
43851
44702
  }
43852
- if (data.output_incomplete === true) {
44703
+ if (typeof data.remote_phase_note === "string") {
44704
+ text += ` · ${data.remote_phase_note}`;
44705
+ }
44706
+ if (data.output_incomplete === true || status === "failed" && data.status_reason) {
43853
44707
  const reason = typeof data.status_reason === "string" && data.status_reason ? data.status_reason : "PTY output may be incomplete";
43854
44708
  text += `
43855
44709
  [${reason}]`;
@@ -43946,7 +44800,25 @@ function conflictTools(ctx) {
43946
44800
  }
43947
44801
 
43948
44802
  // src/tools/definitions/v2.ts
43949
- import { Effect as Effect4 } from "effect";
44803
+ import { Effect as Effect5 } from "effect";
44804
+
44805
+ // src/v2-host-tool-names.ts
44806
+ var V2_HOST_TOOL_NAMES = Object.freeze({
44807
+ bash: "shell",
44808
+ apply_patch: "patch"
44809
+ });
44810
+ var AFT_TOOL_FOR_V2_HOST = Object.freeze(Object.fromEntries(Object.entries(V2_HOST_TOOL_NAMES).map(([aft, host]) => [host, aft])));
44811
+ function v2HostToolName(aftName) {
44812
+ return V2_HOST_TOOL_NAMES[aftName] ?? aftName;
44813
+ }
44814
+ function aftToolNameForV2Host(hostName) {
44815
+ return AFT_TOOL_FOR_V2_HOST[hostName] ?? hostName;
44816
+ }
44817
+ function renameV2ToolReferences(text) {
44818
+ return text.replace(/(?<![\w./-])bash(?![\w-]|\.\w|\s+(?:itself|REPLs|syntax)\b)/g, "shell").replace(/(?<![\w./-])apply_patch(?![\w-]|\.\w)/g, "patch");
44819
+ }
44820
+
44821
+ // src/tools/definitions/v2.ts
43950
44822
  var V2_TOOL_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
43951
44823
  var V2_PATH_HEADER_TOOLS = new Set(["read", "write", "edit"]);
43952
44824
  var V2_BASH_TOOLS = new Set(["bash"]);
@@ -44031,12 +44903,23 @@ function bareToolName3(name) {
44031
44903
  return name.startsWith("aft_") ? name.slice(4) : name;
44032
44904
  }
44033
44905
  function projectArguments(name, definition) {
44034
- if (!V2_PATH_HEADER_TOOLS.has(bareToolName3(name)) || !("filePath" in definition.args)) {
44035
- return { ...definition.args };
44906
+ const args = renameArgumentDescriptions(definition.args);
44907
+ if (!V2_PATH_HEADER_TOOLS.has(bareToolName3(name)) || !("filePath" in args)) {
44908
+ return args;
44036
44909
  }
44037
- const { filePath, ...rest } = definition.args;
44910
+ const { filePath, ...rest } = args;
44038
44911
  return { path: filePath, ...rest };
44039
44912
  }
44913
+ function renameArgumentDescriptions(args) {
44914
+ const renamed = {};
44915
+ for (const [key, schema] of Object.entries(args)) {
44916
+ const described = schema;
44917
+ const description = described.description;
44918
+ const next = description === undefined ? undefined : renameV2ToolReferences(description);
44919
+ renamed[key] = next === undefined || next === description ? schema : described.describe(next);
44920
+ }
44921
+ return renamed;
44922
+ }
44040
44923
  function executionArguments(name, input) {
44041
44924
  if (!V2_PATH_HEADER_TOOLS.has(bareToolName3(name)) || !("path" in input))
44042
44925
  return input;
@@ -44095,7 +44978,7 @@ function runtimeFor(name, location, context, signal, consumers) {
44095
44978
  abort: signal,
44096
44979
  effectAbort: signal,
44097
44980
  metadata: (update) => {
44098
- Effect4.runPromise(context.progress(jsonSafeMetadata({
44981
+ Effect5.runPromise(context.progress(jsonSafeMetadata({
44099
44982
  ...update.metadata ?? {},
44100
44983
  ...update.title ? { title: update.title } : {}
44101
44984
  })));
@@ -44110,36 +44993,46 @@ function runtimeFor(name, location, context, signal, consumers) {
44110
44993
  };
44111
44994
  }
44112
44995
  function projectV2Tool(name, definition, location, consumers = {}) {
44996
+ const hostName = v2HostToolName(name);
44113
44997
  const input = tool.schema.object(projectArguments(name, definition));
44114
- assertV2Contract(name, input);
44998
+ assertV2Contract(hostName, input);
44115
44999
  const sharedOptions = definition.options;
44116
45000
  const permission = hostPermission(name);
44117
45001
  return {
44118
- name,
44119
- description: definition.description,
45002
+ name: hostName,
45003
+ description: renameV2ToolReferences(definition.description),
44120
45004
  input,
44121
45005
  options: {
44122
45006
  ...sharedOptions,
44123
45007
  ...permission ? { permission } : {},
44124
45008
  codemode: false
44125
45009
  },
44126
- execute: (rawInput, context) => Effect4.tryPromise({
45010
+ execute: (rawInput, context) => Effect5.tryPromise({
44127
45011
  try: async (signal) => {
44128
- if (name === "bash_status" || name === "bash_watch") {
44129
- consumers.registerSession?.(context.sessionID);
45012
+ const onAbort = () => {
45013
+ if (context.sessionID)
45014
+ consumers.onToolAbort?.(context.sessionID);
45015
+ };
45016
+ signal.addEventListener("abort", onAbort, { once: true });
45017
+ try {
45018
+ if (name === "bash_status" || name === "bash_watch") {
45019
+ consumers.registerSession?.(context.sessionID);
45020
+ }
45021
+ const runtime = runtimeFor(name, location, context, signal, consumers);
45022
+ const input = executionArguments(name, rawInput);
45023
+ const result = consumers.executeBash && V2_BASH_TOOLS.has(name) ? await consumers.executeBash({
45024
+ name,
45025
+ input,
45026
+ context: runtime,
45027
+ definition
45028
+ }) : await definition.execute(input, runtime);
45029
+ const refusal = failureEnvelopeMessage(result);
45030
+ if (refusal)
45031
+ throw new Error(refusal);
45032
+ return resultContent(result);
45033
+ } finally {
45034
+ signal.removeEventListener("abort", onAbort);
44130
45035
  }
44131
- const runtime = runtimeFor(name, location, context, signal, consumers);
44132
- const input = executionArguments(name, rawInput);
44133
- const result = consumers.executeBash && V2_BASH_TOOLS.has(name) ? await consumers.executeBash({
44134
- name,
44135
- input,
44136
- context: runtime,
44137
- definition
44138
- }) : await definition.execute(input, runtime);
44139
- const refusal = failureEnvelopeMessage(result);
44140
- if (refusal)
44141
- throw new Error(refusal);
44142
- return resultContent(result);
44143
45036
  },
44144
45037
  catch: failure
44145
45038
  })
@@ -44168,10 +45061,10 @@ function joinList(items) {
44168
45061
  }
44169
45062
 
44170
45063
  // src/shared/read-vision.ts
44171
- import { Effect as Effect5 } from "effect";
45064
+ import { Effect as Effect6 } from "effect";
44172
45065
  var VISION_HOST_TIMEOUT_MS = 1500;
44173
45066
  var states = new WeakMap;
44174
- function stateFor2(client) {
45067
+ function stateFor3(client) {
44175
45068
  let state = states.get(client);
44176
45069
  if (!state) {
44177
45070
  state = { models: new Map, history: new Map, capabilities: new Map };
@@ -44180,7 +45073,7 @@ function stateFor2(client) {
44180
45073
  return state;
44181
45074
  }
44182
45075
  function rememberReadModel(client, sessionID, model) {
44183
- stateFor2(client).models.set(sessionID, model);
45076
+ stateFor3(client).models.set(sessionID, model);
44184
45077
  }
44185
45078
  async function bounded(call) {
44186
45079
  let timer;
@@ -44203,7 +45096,7 @@ function record3(value) {
44203
45096
  async function currentSessionVisionCapability(client, sessionID) {
44204
45097
  if (!sessionID)
44205
45098
  return;
44206
- const state = stateFor2(client);
45099
+ const state = stateFor3(client);
44207
45100
  let model = state.models.get(sessionID);
44208
45101
  if (!model) {
44209
45102
  let pending = state.history.get(sessionID);
@@ -44227,7 +45120,7 @@ async function currentSessionVisionCapability(client, sessionID) {
44227
45120
  return;
44228
45121
  const list = async () => {
44229
45122
  const result = api.list?.();
44230
- return isV2PluginContext3(client) ? Effect5.runPromise(result) : await result;
45123
+ return isV2PluginContext3(client) ? Effect6.runPromise(result) : await result;
44231
45124
  };
44232
45125
  const listed = record3(await bounded(list));
44233
45126
  const catalog = record3(listed?.data) ?? listed;
@@ -44318,6 +45211,7 @@ function createBashWatchTool(ctx) {
44318
45211
  throw new Error(`invalid_request: ${message}`);
44319
45212
  throw new Error(`${code}: ${message}`);
44320
45213
  }
45214
+ await trackV2Watch(ctx, context, taskId, registered.watch_id);
44321
45215
  const metadata = context.metadata;
44322
45216
  metadata?.({ taskId, registered: true, watchId: registered.watch_id });
44323
45217
  return `Watch registered: ${registered.watch_id} on task ${taskId}
@@ -44327,6 +45221,11 @@ A notification will fire when the pattern matches or the task exits.`;
44327
45221
  const role = isSubagent ? "worker" : "primary";
44328
45222
  const effectiveWaitMs = subagentForcedSync ? bashCfg.worker_wait_max_ms : resolveWatchTimeoutMs(coerceConfiguredWatchTimeout(args.timeoutMs, role, syncWaitCap), role, syncWaitCap, bashCfg.worker_wait_max_ms);
44329
45223
  const data = await waitForBashStatus(ctx, context, taskId, undefined, waitFor, effectiveWaitMs, role);
45224
+ if (isSubagent && context.abort.aborted)
45225
+ await awaitV2ExecutionSettlement(ctx.client, context.sessionID);
45226
+ const stopped = isSubagent && v2SubagentStopped(ctx.client, context.sessionID);
45227
+ if (stopped && data.waited?.reason === "user_message")
45228
+ data.waited.reason = "aborted";
44330
45229
  const waited = data.waited;
44331
45230
  if (waited?.reason === "user_message") {
44332
45231
  const convertedText = await convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, coerceBoolean(args.once, true), waited.elapsed_ms, role);
@@ -44345,6 +45244,7 @@ async function convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, once, e
44345
45244
  const interrupted = `Sync watch for task ${taskId} was interrupted because you sent a message after ${elapsedMs}ms of waiting. `;
44346
45245
  const reminderFallback = interruptedWatchTail(role, `The task is still running in the background. A completion reminder will be ` + `delivered automatically when the task exits.`);
44347
45246
  if (!waitFor) {
45247
+ allowV2WatchWake(ctx.client, context.sessionID, taskId);
44348
45248
  return interrupted + interruptedWatchTail(role, `The task is still running in the background. A completion reminder will be ` + `delivered automatically when the task exits; don't poll bash_status.`);
44349
45249
  }
44350
45250
  const notifyParams = {
@@ -44362,6 +45262,7 @@ async function convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, once, e
44362
45262
  unmarkExplicitControl(context.sessionID, taskId);
44363
45263
  return interrupted + `Auto-registering an async watch failed (${String(registered.message ?? "unknown error")}). ` + reminderFallback;
44364
45264
  }
45265
+ await trackV2Watch(ctx, context, taskId, registered.watch_id);
44365
45266
  return interrupted + `The wait has been converted to an async watch (${registered.watch_id}). ` + interruptedWatchTail(role, `A notification will fire when the pattern matches or the task exits.`);
44366
45267
  } catch (err) {
44367
45268
  unmarkExplicitControl(context.sessionID, taskId);
@@ -44478,6 +45379,9 @@ async function waitForBashStatus(ctx, runtime, taskId, outputMode, waitFor, effe
44478
45379
  }
44479
45380
  busySince = undefined;
44480
45381
  lastData = data;
45382
+ if (role === "worker" && abortSignal?.aborted) {
45383
+ return withWaited(data, waited("aborted"));
45384
+ }
44481
45385
  const terminal = isTerminalStatus(data.status);
44482
45386
  if (waitFor) {
44483
45387
  const scan = await readNewTaskOutput(data, spillCursor);
@@ -45166,9 +46070,6 @@ function createHashlineEditTool(ctx) {
45166
46070
  });
45167
46071
  if (denial)
45168
46072
  return permissionDeniedResponse(denial);
45169
- const preview = await callToolCall(ctx, context, "edit", rawArgs, { preview: true });
45170
- if (preview.success === false)
45171
- throw toolErrorFromResponse("edit", preview);
45172
46073
  const data = await callToolCall(ctx, context, "edit", rawArgs);
45173
46074
  if (data.success === false)
45174
46075
  throw toolErrorFromResponse("edit", data);
@@ -45797,12 +46698,13 @@ function createInspectTier2IdleScheduler(options) {
45797
46698
  }
45798
46699
  function inspectTools(ctx) {
45799
46700
  const inspectTool = {
45800
- 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.
46701
+ 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.
45801
46702
 
45802
46703
  ` + "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.",
45803
46704
  args: {
45804
- sections: arg(z9.union([z9.string(), z9.array(z9.string())]).optional().describe("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.")),
45805
- scope: arg(z9.union([z9.string(), z9.array(z9.string())]).optional().describe("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.")),
46705
+ sections: arg(z9.union([z9.string(), z9.array(z9.string())]).optional().describe("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.")),
46706
+ scope: arg(z9.union([z9.string(), z9.array(z9.string())]).optional().describe("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'.")),
46707
+ offset: arg(z9.number().int().nonnegative().optional().describe("Rows to skip independently in each drill-down list. Default 0; follow next_offset in each list envelope.")),
45806
46708
  topK: arg(z9.number().int().positive().max(100).optional().describe("Max drill-down items per category. Default 20, max 100."))
45807
46709
  },
45808
46710
  execute: async (args, context) => {
@@ -45817,6 +46719,8 @@ function inspectTools(ctx) {
45817
46719
  rawArgs.scope = scoped.scope;
45818
46720
  if (args.topK !== undefined && args.topK !== null)
45819
46721
  rawArgs.topK = args.topK;
46722
+ if (args.offset !== undefined && args.offset !== null)
46723
+ rawArgs.offset = args.offset;
45820
46724
  const response = await callToolCall(ctx, context, "inspect", rawArgs, {
45821
46725
  transportTimeoutMs: resolveInspectDiagnosticsTimeoutMs(ctx.config) + INSPECT_TRANSPORT_HEADROOM_MS
45822
46726
  });
@@ -46696,11 +47600,11 @@ function describeBashForRegisteredSurface(allTools, config) {
46696
47600
  }
46697
47601
  function registerAftTools(context, location, definitions, consumers = {}) {
46698
47602
  return context.tool.transform((editor) => {
46699
- const projected = Object.entries(definitions).map(([name, definition]) => projectV2Tool(name, definition, location, consumers));
46700
- for (const definition of projected) {
46701
- if (V2_BUILTIN_REPLACEMENTS.has(definition.name))
46702
- editor.remove(definition.name);
46703
- editor.add(definition);
47603
+ for (const [name, definition] of Object.entries(definitions)) {
47604
+ const projected = projectV2Tool(name, definition, location, consumers);
47605
+ if (V2_BUILTIN_REPLACEMENTS.has(name))
47606
+ editor.remove(projected.name);
47607
+ editor.add(projected);
46704
47608
  }
46705
47609
  });
46706
47610
  }
@@ -47101,6 +48005,7 @@ function startOpenCodeLiveConfigReload(options) {
47101
48005
  const { userConfigPath, projectConfigPath } = resolveCortexKitConfigPaths(options.directory);
47102
48006
  return startLiveConfigReload({
47103
48007
  paths: [userConfigPath, projectConfigPath],
48008
+ projectPath: projectConfigPath,
47104
48009
  load: () => loadOpenCodeConfigForLiveReload(options.directory),
47105
48010
  initialSources: options.initialSources,
47106
48011
  initialProjectText: options.initialSourceTexts?.[projectConfigPath] ?? null,
@@ -47667,21 +48572,21 @@ import {
47667
48572
  mkdirSync as mkdirSync8,
47668
48573
  openSync as openSync11,
47669
48574
  readFileSync as readFileSync17,
47670
- renameSync as renameSync11,
48575
+ renameSync as renameSync10,
47671
48576
  rmSync as rmSync8,
47672
48577
  statSync as statSync16,
47673
- writeFileSync as writeFileSync12
48578
+ writeFileSync as writeFileSync11
47674
48579
  } from "node:fs";
47675
48580
  import { dirname as dirname16 } from "node:path";
47676
48581
 
47677
48582
  // src/hooks/auto-update-checker/cache.ts
47678
- import { cpSync, existsSync as existsSync19, mkdtempSync, readFileSync as readFileSync16, rmSync as rmSync7, writeFileSync as writeFileSync11 } from "node:fs";
48583
+ import { cpSync, existsSync as existsSync19, mkdtempSync, readFileSync as readFileSync16, rmSync as rmSync7, writeFileSync as writeFileSync10 } from "node:fs";
47679
48584
  import { tmpdir as tmpdir5 } from "node:os";
47680
48585
  import { basename as basename6, dirname as dirname15, join as join25 } from "node:path";
47681
48586
  var import_comment_json3 = __toESM(require_src2(), 1);
47682
48587
 
47683
48588
  // src/hooks/auto-update-checker/checker.ts
47684
- import { existsSync as existsSync18, readFileSync as readFileSync15, statSync as statSync15, writeFileSync as writeFileSync10 } from "node:fs";
48589
+ import { existsSync as existsSync18, readFileSync as readFileSync15, statSync as statSync15, writeFileSync as writeFileSync9 } from "node:fs";
47685
48590
  import { readFile } from "node:fs/promises";
47686
48591
  import { homedir as homedir15 } from "node:os";
47687
48592
  import { dirname as dirname14, isAbsolute as isAbsolute13, join as join24, resolve as resolve15 } from "node:path";
@@ -48069,11 +48974,11 @@ function restoreAutoUpdateSnapshot(snapshot) {
48069
48974
  if (snapshot.packageJson === null)
48070
48975
  rmSync7(snapshot.packageJsonPath, { force: true });
48071
48976
  else
48072
- writeFileSync11(snapshot.packageJsonPath, snapshot.packageJson);
48977
+ writeFileSync10(snapshot.packageJsonPath, snapshot.packageJson);
48073
48978
  if (snapshot.lockfile === null)
48074
48979
  rmSync7(snapshot.lockfilePath, { force: true });
48075
48980
  else
48076
- writeFileSync11(snapshot.lockfilePath, snapshot.lockfile);
48981
+ writeFileSync10(snapshot.lockfilePath, snapshot.lockfile);
48077
48982
  rmSync7(snapshot.packageDir, { recursive: true, force: true });
48078
48983
  if (snapshot.stagedPackageDir) {
48079
48984
  cpSync(snapshot.stagedPackageDir, snapshot.packageDir, { recursive: true });
@@ -48110,7 +49015,7 @@ function removeFromPackageLock(installDir, packageName) {
48110
49015
  modified = true;
48111
49016
  }
48112
49017
  if (modified) {
48113
- writeFileSync11(lockPath, JSON.stringify(lock, null, 2));
49018
+ writeFileSync10(lockPath, JSON.stringify(lock, null, 2));
48114
49019
  log2(`[auto-update-checker] Removed from package-lock.json: ${packageName}`);
48115
49020
  }
48116
49021
  return modified;
@@ -48132,7 +49037,7 @@ function ensureDependencyVersion(packageJsonPath, packageName, version) {
48132
49037
  return true;
48133
49038
  dependencies[packageName] = version;
48134
49039
  nextPackageJson.dependencies = dependencies;
48135
- writeFileSync11(packageJsonPath, JSON.stringify(nextPackageJson, null, 2));
49040
+ writeFileSync10(packageJsonPath, JSON.stringify(nextPackageJson, null, 2));
48136
49041
  log2(`[auto-update-checker] Updated dependency in package.json: ${packageName} → ${version}`);
48137
49042
  return true;
48138
49043
  } catch (err) {
@@ -48376,7 +49281,7 @@ function claimCheckSlot(storageDir, intervalMs) {
48376
49281
  let lockFd = null;
48377
49282
  try {
48378
49283
  lockFd = openSync11(lockPath, "wx");
48379
- writeFileSync12(lockFd, JSON.stringify(owner));
49284
+ writeFileSync11(lockFd, JSON.stringify(owner));
48380
49285
  closeSync12(lockFd);
48381
49286
  lockFd = null;
48382
49287
  } catch (err) {
@@ -48390,9 +49295,9 @@ function claimCheckSlot(storageDir, intervalMs) {
48390
49295
  if (!isOrphanedCheckLock(lockPath))
48391
49296
  return null;
48392
49297
  const replacementPath = `${lockPath}.tmp.${process.pid}`;
48393
- writeFileSync12(replacementPath, JSON.stringify(owner), { encoding: "utf-8", mode: 384 });
49298
+ writeFileSync11(replacementPath, JSON.stringify(owner), { encoding: "utf-8", mode: 384 });
48394
49299
  try {
48395
- renameSync11(replacementPath, lockPath);
49300
+ renameSync10(replacementPath, lockPath);
48396
49301
  } catch (replaceError) {
48397
49302
  rmSync8(replacementPath, { force: true });
48398
49303
  throw replaceError;
@@ -48472,8 +49377,8 @@ function hasRecentCheckTimestamp(file, intervalMs) {
48472
49377
  }
48473
49378
  function writeCheckTimestamp(file) {
48474
49379
  const tmp = `${file}.tmp.${process.pid}`;
48475
- writeFileSync12(tmp, JSON.stringify({ lastCheckedMs: Date.now() }), "utf-8");
48476
- renameSync11(tmp, file);
49380
+ writeFileSync11(tmp, JSON.stringify({ lastCheckedMs: Date.now() }), "utf-8");
49381
+ renameSync10(tmp, file);
48477
49382
  }
48478
49383
  function isNewerVersion(candidate, installed) {
48479
49384
  const semver = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
@@ -48677,9 +49582,9 @@ import {
48677
49582
  mkdirSync as mkdirSync9,
48678
49583
  readdirSync as readdirSync6,
48679
49584
  readFileSync as readFileSync18,
48680
- renameSync as renameSync12,
48681
- unlinkSync as unlinkSync8,
48682
- writeFileSync as writeFileSync13
49585
+ renameSync as renameSync11,
49586
+ unlinkSync as unlinkSync7,
49587
+ writeFileSync as writeFileSync12
48683
49588
  } from "node:fs";
48684
49589
  import { createServer } from "node:http";
48685
49590
  import { dirname as dirname17, join as join27 } from "node:path";
@@ -48835,13 +49740,13 @@ class AftRpcServer {
48835
49740
  const dir = dirname17(this.portFilePath);
48836
49741
  mkdirSync9(dir, { recursive: true, mode: 448 });
48837
49742
  const tmpPath = `${this.portFilePath}.tmp`;
48838
- writeFileSync13(tmpPath, JSON.stringify({
49743
+ writeFileSync12(tmpPath, JSON.stringify({
48839
49744
  port: this.port,
48840
49745
  token: this.token,
48841
49746
  pid: process.pid,
48842
49747
  started_at: Date.now()
48843
49748
  }), { encoding: "utf-8", mode: 384 });
48844
- renameSync12(tmpPath, this.portFilePath);
49749
+ renameSync11(tmpPath, this.portFilePath);
48845
49750
  }
48846
49751
  sweepDeadPortFiles() {
48847
49752
  let entries;
@@ -48859,11 +49764,11 @@ class AftRpcServer {
48859
49764
  try {
48860
49765
  const record = parseRpcPortRecord(readFileSync18(filePath, "utf-8"));
48861
49766
  if (record === null) {
48862
- unlinkSync8(filePath);
49767
+ unlinkSync7(filePath);
48863
49768
  continue;
48864
49769
  }
48865
49770
  if (record.pid !== undefined && !isPidAlive(record.pid)) {
48866
- unlinkSync8(filePath);
49771
+ unlinkSync7(filePath);
48867
49772
  }
48868
49773
  } catch {}
48869
49774
  }
@@ -48899,7 +49804,7 @@ class AftRpcServer {
48899
49804
  this.bunServer = null;
48900
49805
  }
48901
49806
  try {
48902
- unlinkSync8(this.portFilePath);
49807
+ unlinkSync7(this.portFilePath);
48903
49808
  } catch {}
48904
49809
  }
48905
49810
  async handleFetch(req, srv) {
@@ -49128,7 +50033,13 @@ function readStatusBar(value) {
49128
50033
  const duplicates = readOptionalNumber(bar.duplicates);
49129
50034
  const todos = readOptionalNumber(bar.todos);
49130
50035
  const noLanguageServer = bar.diagnostics === "no_language_server";
49131
- if (errors === null && warnings === null && deadCode === null && unusedExports === null && duplicates === null && todos === null && !noLanguageServer && disabled.length === 0) {
50036
+ const producerMissing = bar.diagnostics === "producer_missing";
50037
+ const diagnosticsGaps = Array.isArray(bar.diagnostics_gaps) ? bar.diagnostics_gaps.map(asRecord3).filter((gap) => typeof gap.language === "string" && typeof gap.producer === "string" && typeof gap.root === "string").map((gap) => ({
50038
+ language: gap.language,
50039
+ producer: gap.producer,
50040
+ root: gap.root
50041
+ })) : [];
50042
+ if (errors === null && warnings === null && deadCode === null && unusedExports === null && duplicates === null && todos === null && !noLanguageServer && !producerMissing && disabled.length === 0) {
49132
50043
  return;
49133
50044
  }
49134
50045
  return {
@@ -49140,7 +50051,8 @@ function readStatusBar(value) {
49140
50051
  ...duplicates !== null ? { duplicates } : {},
49141
50052
  ...todos !== null ? { todos } : {},
49142
50053
  ...bar.tier2_stale === true ? { tier2_stale: true } : {},
49143
- ...noLanguageServer ? { diagnostics: "no_language_server" } : {}
50054
+ ...noLanguageServer ? { diagnostics: "no_language_server" } : {},
50055
+ ...producerMissing ? { diagnostics: "producer_missing", diagnostics_gaps: diagnosticsGaps } : {}
49144
50056
  };
49145
50057
  }
49146
50058
  function formatFlag(enabled) {
@@ -49592,11 +50504,11 @@ function throwSentinel(command) {
49592
50504
  throw sentinel;
49593
50505
  }
49594
50506
  var PLUGIN_VERSION = resolvePluginVersion(import.meta.url);
49595
- var ANNOUNCEMENT_VERSION = "0.59.0";
50507
+ var ANNOUNCEMENT_VERSION = "0.60.0";
49596
50508
  var ANNOUNCEMENT_FEATURES = [
49597
- "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.",
49598
- "New: live pull-request diffs with `read` on `pr://N/diff`, a `pattern` argument for `aft_search`, and per-category switches for `aft_inspect`.",
49599
- "Fixes for issues reported on OpenCode 2, Pi and OMP, and much less repeated indexing and disk work."
50509
+ "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.",
50510
+ "`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.",
50511
+ "OpenCode 2 gets native `shell` and `patch` tools, and stopped subagents are no longer woken by their own background commands."
49600
50512
  ];
49601
50513
  var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/DSa65w8wuf";
49602
50514
  var plugin = async (input) => initializePluginForDirectory(input);