@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
@@ -10290,11 +10290,11 @@ var DEFAULT_WORKER_WAIT_MAX_MS = 1800000;
10290
10290
  var MIN_WORKER_WAIT_MAX_MS = 60000;
10291
10291
  var LONGEST_TIMER_DELAY_MS = 2147483647;
10292
10292
  var WORKER_WAIT_LIMIT_PHRASE = "the worker wait limit (`bash.worker_wait_max_ms`, 30 minutes by default)";
10293
- 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";
10294
- 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.";
10293
+ 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";
10294
+ 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.";
10295
10295
  function resolveWatchTimeoutMs(requestedMs, role, capMs, workerWaitMaxMs) {
10296
10296
  if (role === "worker")
10297
- return requestedMs ?? workerWaitMaxMs;
10297
+ return Math.min(requestedMs ?? workerWaitMaxMs, workerWaitMaxMs);
10298
10298
  return Math.min(requestedMs ?? DEFAULT_PRIMARY_WATCH_TIMEOUT_MS, capMs);
10299
10299
  }
10300
10300
  function maxWatchTimeoutMs(role, capMs) {
@@ -10357,6 +10357,10 @@ function taskKillDeadlineText(data, role, nowMs = Date.now()) {
10357
10357
  return "";
10358
10358
  if (data.status === "unknown")
10359
10359
  return "";
10360
+ if (typeof data.remote_phase_note === "string" && data.remote_phase_note !== "") {
10361
+ const note = data.remote_phase_note;
10362
+ return `${note.charAt(0).toUpperCase()}${note.slice(1)}.`;
10363
+ }
10360
10364
  const hardKill = data.hard_kill;
10361
10365
  if (!hardKill || typeof hardKill.limit_ms !== "number") {
10362
10366
  return "This task has no kill deadline.";
@@ -11091,8 +11095,8 @@ var BASH_HOST_FALLBACK_BANNER = "[AFT host fallback - module transport down; no
11091
11095
  var BASH_HOST_FALLBACK_MAX_OUTPUT_BYTES = 100 * 1024;
11092
11096
  var BASH_HOST_FALLBACK_MAX_TIMEOUT_MS = 10 * 60 * 1000;
11093
11097
  var BASH_HOST_FALLBACK_REFUSAL = "AFT transport is down; only foreground execution is available in host fallback";
11094
- var BASH_RUNON_DESCRIPTION = 'Run this command on the remote Linux build server instead of this machine; the only value is "linux".';
11095
- 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.';
11098
+ 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).';
11099
+ 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.';
11096
11100
  function pathKeyForPlatform(env, platform) {
11097
11101
  return platform === "win32" ? Object.keys(env).find((key) => key.toLowerCase() === "path") : "PATH";
11098
11102
  }
@@ -11536,6 +11540,7 @@ function isPassiveCommand(command) {
11536
11540
 
11537
11541
  // ../aft-bridge/dist/transport.js
11538
11542
  var WORKER_SESSION_FIELD = "worker_session";
11543
+ var HOST_TOOL_NAMES_FIELD = "host_tool_names";
11539
11544
  var PRESET_FIELD = "preset";
11540
11545
  function callPresetFor(workerSession) {
11541
11546
  return workerSession ? "worker" : "head";
@@ -11700,6 +11705,8 @@ class BinaryBridge {
11700
11705
  lastBinaryFingerprintCheckAt = 0;
11701
11706
  _retiringDueToBinaryChange = false;
11702
11707
  pending = new Map;
11708
+ activeSendCalls = 0;
11709
+ processLoopReferenced = true;
11703
11710
  outstandingBackgroundTaskIds = new Set;
11704
11711
  nextId = 1;
11705
11712
  processGeneration = 0;
@@ -11931,6 +11938,32 @@ class BinaryBridge {
11931
11938
  this.sessionLogVia(sessionId, `hashline registration carrier transport=ndjson phase=${phase} edit_slot_survives=${editSlotSurvives}${repeated}`);
11932
11939
  }
11933
11940
  async send(command, params = {}, options) {
11941
+ this.activeSendCalls += 1;
11942
+ this.syncEventLoopHold();
11943
+ try {
11944
+ return await this.dispatchSend(command, params, options);
11945
+ } finally {
11946
+ this.activeSendCalls -= 1;
11947
+ this.syncEventLoopHold();
11948
+ }
11949
+ }
11950
+ syncEventLoopHold() {
11951
+ const child = this.process;
11952
+ if (!child)
11953
+ return;
11954
+ const hold = this.activeSendCalls > 0 || this.outstandingBackgroundTaskIds.size > 0;
11955
+ if (hold === this.processLoopReferenced)
11956
+ return;
11957
+ this.processLoopReferenced = hold;
11958
+ const handles = [child, child.stdin, child.stdout, child.stderr];
11959
+ for (const handle of handles) {
11960
+ if (hold)
11961
+ handle?.ref?.();
11962
+ else
11963
+ handle?.unref?.();
11964
+ }
11965
+ }
11966
+ async dispatchSend(command, params, options) {
11934
11967
  let dispatchParams = params;
11935
11968
  if (command === "configure") {
11936
11969
  dispatchParams = { ...params };
@@ -11968,11 +12001,13 @@ class BinaryBridge {
11968
12001
  params.edit_slot_survives = editSlotSurvives;
11969
12002
  this.logHashlineRegistrationCarrier("tool_call", sessionId, editSlotSurvives);
11970
12003
  }
11971
- const { preview, workerSession, ...sendOptions } = options ?? {};
12004
+ const { preview, workerSession, hostToolNames, ...sendOptions } = options ?? {};
11972
12005
  if (preview === true)
11973
12006
  params.preview = true;
11974
12007
  if (workerSession === true)
11975
12008
  params[WORKER_SESSION_FIELD] = true;
12009
+ if (hostToolNames)
12010
+ params[HOST_TOOL_NAMES_FIELD] = hostToolNames;
11976
12011
  return await this.send("tool_call", params, Object.keys(sendOptions).length > 0 ? sendOptions : undefined);
11977
12012
  }
11978
12013
  async sendWithVersionMismatchRetry(command, params, options, canRetryAfterVersionSwap) {
@@ -12041,7 +12076,12 @@ class BinaryBridge {
12041
12076
  if (command === "cancel_request" || Object.hasOwn(params, "command") || Object.hasOwn(params, "method")) {
12042
12077
  const nested = { ...params };
12043
12078
  const reserved = {};
12044
- for (const key of ["session_id", "lsp_hints", WORKER_SESSION_FIELD]) {
12079
+ for (const key of [
12080
+ "session_id",
12081
+ "lsp_hints",
12082
+ WORKER_SESSION_FIELD,
12083
+ HOST_TOOL_NAMES_FIELD
12084
+ ]) {
12045
12085
  if (Object.hasOwn(nested, key)) {
12046
12086
  reserved[key] = nested[key];
12047
12087
  delete nested[key];
@@ -12416,6 +12456,8 @@ class BinaryBridge {
12416
12456
  this.lastChildActivityAt = 0;
12417
12457
  this.consecutiveRequestTimeouts = 0;
12418
12458
  this.stderrTail = [];
12459
+ this.processLoopReferenced = true;
12460
+ this.syncEventLoopHold();
12419
12461
  }
12420
12462
  pushStderrLine(line) {
12421
12463
  this.stderrTail.push(line);
@@ -12542,6 +12584,7 @@ class BinaryBridge {
12542
12584
  const taskId = bashTaskIdFrom(response);
12543
12585
  if (taskId)
12544
12586
  this.outstandingBackgroundTaskIds.delete(taskId);
12587
+ this.syncEventLoopHold();
12545
12588
  this.onBashCompletion?.(response, this);
12546
12589
  return;
12547
12590
  }
@@ -12655,7 +12698,7 @@ class BinaryBridge {
12655
12698
  const delay = 100 * 2 ** this._restartCount;
12656
12699
  this._restartCount++;
12657
12700
  this.logVia(`Auto-restart #${this._restartCount} in ${delay}ms`);
12658
- setTimeout(() => {
12701
+ const restartTimer = setTimeout(() => {
12659
12702
  if (!this._shuttingDown && !this.isAlive()) {
12660
12703
  try {
12661
12704
  this.spawnProcess();
@@ -12664,6 +12707,7 @@ class BinaryBridge {
12664
12707
  }
12665
12708
  }
12666
12709
  }, delay);
12710
+ restartTimer.unref?.();
12667
12711
  this.scheduleRestartCountReset();
12668
12712
  } else {
12669
12713
  this.errorVia(`Max restarts (${this.maxRestarts}) reached, giving up. Logs: ${this.getLogFilePathVia()}${tail}`);
@@ -12684,6 +12728,7 @@ class BinaryBridge {
12684
12728
  this._restartCount = 0;
12685
12729
  this.restartResetTimer = null;
12686
12730
  }, BinaryBridge.RESTART_RESET_MS);
12731
+ this.restartResetTimer.unref?.();
12687
12732
  }
12688
12733
  clearRestartResetTimer() {
12689
12734
  if (this.restartResetTimer) {
@@ -13300,10 +13345,12 @@ function startLiveConfigReload(options) {
13300
13345
  let acceptedTexts;
13301
13346
  const reload = () => {
13302
13347
  let loaded = options.load();
13348
+ let deletedProjectConfig = false;
13303
13349
  if (loaded.ok) {
13304
13350
  const read = new Set(loaded.sources);
13305
13351
  const deleted = [...acceptedSources].find((path) => !read.has(path));
13306
- if (deleted !== undefined) {
13352
+ deletedProjectConfig = deleted === options.projectPath && loaded.projectText === null;
13353
+ if (deleted !== undefined && !deletedProjectConfig) {
13307
13354
  loaded = { ok: false, message: `AFT config at ${deleted} was deleted` };
13308
13355
  } else {
13309
13356
  acceptedSources = read;
@@ -14075,17 +14122,17 @@ async function renameIfPresent(from, to) {
14075
14122
  throw error;
14076
14123
  }
14077
14124
  }
14078
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/client.js
14125
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/client.js
14079
14126
  import { promises as fs3 } from "node:fs";
14080
14127
  import { debuglog } from "node:util";
14081
14128
 
14082
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/auth.js
14129
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/auth.js
14083
14130
  import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
14084
14131
 
14085
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/socket.js
14132
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/socket.js
14086
14133
  import net from "node:net";
14087
14134
 
14088
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/envelope.js
14135
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/envelope.js
14089
14136
  var PROTOCOL_VERSION = 2;
14090
14137
  var HEADER_LEN = 21;
14091
14138
  var FROZEN_PREFIX_LEN = 5;
@@ -14188,12 +14235,12 @@ function validateHeaderFields(header) {
14188
14235
  if (admission === AdmissionClass.Sheddable && ty !== FrameType.Push && ty !== FrameType.StreamData) {
14189
14236
  throw new DecodeError(`SHEDDABLE admission class is illegal on ${FrameType[ty]} in flags 0b${flags.toString(2).padStart(8, "0")}`, "sheddable_illegal_frame_type");
14190
14237
  }
14191
- if (channel === 0 && epoch !== 0) {
14192
- throw new DecodeError(`control channel carried nonzero epoch ${epoch}`, "nonzero_epoch_on_control_channel");
14193
- }
14194
14238
  if (isPureHeader(ty) && len !== 0) {
14195
14239
  throw new DecodeError(`pure-header frame ${FrameType[ty]} declared non-zero body length ${len}`, "pure_header_frame_with_body");
14196
14240
  }
14241
+ if (channel === 0 && epoch !== 0) {
14242
+ throw new DecodeError(`control channel carried nonzero epoch ${epoch}`, "nonzero_epoch_on_control_channel");
14243
+ }
14197
14244
  }
14198
14245
  function decodeHeader(bytes) {
14199
14246
  if (bytes.length < FROZEN_PREFIX_LEN) {
@@ -14227,12 +14274,12 @@ function decodeHeader(bytes) {
14227
14274
  }
14228
14275
  const channel = view.getUint16(7, true);
14229
14276
  const epoch = view.getUint32(9, true);
14230
- if (channel === 0 && epoch !== 0) {
14231
- throw new DecodeError(`control channel carried nonzero epoch ${epoch}`, "nonzero_epoch_on_control_channel");
14232
- }
14233
14277
  if (isPureHeader(ty) && len !== 0) {
14234
14278
  throw new DecodeError(`pure-header frame ${FrameType[ty]} declared non-zero body length ${len}`, "pure_header_frame_with_body");
14235
14279
  }
14280
+ if (channel === 0 && epoch !== 0) {
14281
+ throw new DecodeError(`control channel carried nonzero epoch ${epoch}`, "nonzero_epoch_on_control_channel");
14282
+ }
14236
14283
  return { len, ver, ty, flags, channel, epoch, corr: view.getBigUint64(13, true) };
14237
14284
  }
14238
14285
  function buildFrame(ty, flags, channel, epoch, corr, body) {
@@ -14250,14 +14297,18 @@ function encodeFrame(frame) {
14250
14297
  if (frame.header.len !== frame.body.length) {
14251
14298
  throw new DecodeError(`frame header length ${frame.header.len} does not match body length ${frame.body.length}`, "frame_length_mismatch");
14252
14299
  }
14300
+ if (frame.body.length > MAX_FRAME_BODY_LEN) {
14301
+ throw new DecodeError(`frame body ${frame.body.length} exceeds max ${MAX_FRAME_BODY_LEN}`, "frame_body_too_large");
14302
+ }
14253
14303
  const header = encodeHeader(frame.header);
14304
+ decodeHeader(header);
14254
14305
  const output = new Uint8Array(header.length + frame.body.length);
14255
14306
  output.set(header, 0);
14256
14307
  output.set(frame.body, header.length);
14257
14308
  return output;
14258
14309
  }
14259
14310
 
14260
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/socket.js
14311
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/socket.js
14261
14312
  class SocketClosedError extends Error {
14262
14313
  }
14263
14314
 
@@ -14307,6 +14358,15 @@ class SubcSocket {
14307
14358
  sock.on("data", (chunk) => {
14308
14359
  this.chunks.push(chunk);
14309
14360
  this.buffered += chunk.length;
14361
+ if (this.waiter) {
14362
+ if (this.waiter.immediate) {
14363
+ clearImmediate(this.waiter.immediate);
14364
+ this.waiter.immediate = null;
14365
+ }
14366
+ if (this.waiter.extendOnProgress && this.buffered < this.waiter.need) {
14367
+ this.armWaiterTimer(this.waiter);
14368
+ }
14369
+ }
14310
14370
  this.tryServe();
14311
14371
  });
14312
14372
  const fail = (err) => {
@@ -14353,30 +14413,80 @@ class SubcSocket {
14353
14413
  throw new DecodeError(`frame body ${header.len} exceeds max ${MAX_FRAME_BODY_LEN}`, "frame_body_too_large");
14354
14414
  }
14355
14415
  onHeader?.();
14356
- const bodyDeadlineMs = typeof bodyDeadline === "number" ? bodyDeadline : Date.now() + bodyDeadline.afterHeaderMs;
14357
- const body = header.len === 0 ? new Uint8Array(0) : await this.readExact(header.len, bodyDeadlineMs);
14416
+ const bodyDeadlineArg = typeof bodyDeadline === "number" ? bodyDeadline : { idleTimeoutMs: bodyDeadline.afterHeaderMs };
14417
+ const body = header.len === 0 ? new Uint8Array(0) : await this.readExact(header.len, bodyDeadlineArg);
14358
14418
  return { header, body };
14359
14419
  }
14360
- readExact(n, deadlineMs) {
14420
+ armWaiterTimer(w) {
14421
+ if (!Number.isFinite(w.budget))
14422
+ return;
14423
+ if (w.timer) {
14424
+ clearTimeout(w.timer);
14425
+ w.timer = null;
14426
+ }
14427
+ w.timer = setTimeout(() => {
14428
+ w.timer = null;
14429
+ const bufferedAtTimeout = this.buffered;
14430
+ const verifyTimeout = () => {
14431
+ if (this.waiter !== w)
14432
+ return;
14433
+ if (w.extendOnProgress && this.buffered > bufferedAtTimeout) {
14434
+ this.armWaiterTimer(w);
14435
+ return;
14436
+ }
14437
+ this.waiter = null;
14438
+ w.reject(new SocketTimeoutError(`timed out waiting for ${w.need} bytes`));
14439
+ };
14440
+ w.immediate = setImmediate(() => {
14441
+ if (this.waiter !== w)
14442
+ return;
14443
+ if (this.buffered > bufferedAtTimeout) {
14444
+ verifyTimeout();
14445
+ } else {
14446
+ w.immediate = setImmediate(() => {
14447
+ w.immediate = null;
14448
+ verifyTimeout();
14449
+ });
14450
+ }
14451
+ });
14452
+ }, w.budget);
14453
+ }
14454
+ readExact(n, deadline) {
14361
14455
  if (this.waiter) {
14362
14456
  return Promise.reject(new Error("concurrent readExact is not supported"));
14363
14457
  }
14364
14458
  if (n === 0)
14365
14459
  return Promise.resolve(new Uint8Array(0));
14366
14460
  return new Promise((resolve, reject) => {
14367
- let timer = null;
14368
- if (Number.isFinite(deadlineMs)) {
14369
- const remaining = deadlineMs - Date.now();
14370
- if (remaining <= 0) {
14461
+ let budget;
14462
+ const extendOnProgress = typeof deadline === "object";
14463
+ if (typeof deadline === "object") {
14464
+ budget = deadline.idleTimeoutMs;
14465
+ if (budget <= 0) {
14371
14466
  reject(new SocketTimeoutError(`timed out waiting for ${n} bytes`));
14372
14467
  return;
14373
14468
  }
14374
- timer = setTimeout(() => {
14375
- this.waiter = null;
14469
+ } else if (Number.isFinite(deadline)) {
14470
+ const remaining = deadline - Date.now();
14471
+ if (remaining <= 0) {
14376
14472
  reject(new SocketTimeoutError(`timed out waiting for ${n} bytes`));
14377
- }, remaining);
14473
+ return;
14474
+ }
14475
+ budget = remaining;
14476
+ } else {
14477
+ budget = Number.POSITIVE_INFINITY;
14378
14478
  }
14379
- this.waiter = { need: n, resolve, reject, timer };
14479
+ const waiter = {
14480
+ need: n,
14481
+ resolve,
14482
+ reject,
14483
+ timer: null,
14484
+ immediate: null,
14485
+ budget,
14486
+ extendOnProgress
14487
+ };
14488
+ this.waiter = waiter;
14489
+ this.armWaiterTimer(waiter);
14380
14490
  this.tryServe();
14381
14491
  });
14382
14492
  }
@@ -14459,6 +14569,8 @@ class SubcSocket {
14459
14569
  this.waiter = null;
14460
14570
  if (w.timer)
14461
14571
  clearTimeout(w.timer);
14572
+ if (w.immediate)
14573
+ clearImmediate(w.immediate);
14462
14574
  w.resolve(out);
14463
14575
  return;
14464
14576
  }
@@ -14466,6 +14578,8 @@ class SubcSocket {
14466
14578
  this.waiter = null;
14467
14579
  if (w.timer)
14468
14580
  clearTimeout(w.timer);
14581
+ if (w.immediate)
14582
+ clearImmediate(w.immediate);
14469
14583
  w.reject(this.closedErr);
14470
14584
  }
14471
14585
  }
@@ -14490,7 +14604,7 @@ class SubcSocket {
14490
14604
  }
14491
14605
  }
14492
14606
 
14493
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/auth.js
14607
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/auth.js
14494
14608
  var NONCE_LEN = 32;
14495
14609
  var MAX_AUTH_MESSAGE_LEN = 4096;
14496
14610
  var SERVER_PROOF_DOMAIN = "subc-server-v1";
@@ -14564,7 +14678,7 @@ async function authenticateClient(sock, conn, deadlineMs) {
14564
14678
  await writeMessage(sock, { client_auth: Array.from(clientAuth) }, deadlineMs);
14565
14679
  }
14566
14680
 
14567
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/connection-file.js
14681
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/connection-file.js
14568
14682
  import { promises as fs2 } from "node:fs";
14569
14683
  var SCHEMA_VERSION = 1;
14570
14684
  var MIN_KEY_LEN = 32;
@@ -14597,18 +14711,29 @@ function validate(info) {
14597
14711
  throw new ConnectionFileError(`connection file daemon_id must be ${DAEMON_ID_LEN} bytes, got ${info.daemonId.length}`);
14598
14712
  }
14599
14713
  }
14600
- async function verifyOwnerOnly(path) {
14714
+ function verifyOwnerOnly(path, stat, expectedUid) {
14601
14715
  if (process.platform === "win32")
14602
14716
  return;
14603
- const stat = await fs2.stat(path);
14717
+ if (expectedUid !== undefined && stat.uid !== expectedUid) {
14718
+ throw new ConnectionFileError(`connection file ${path} is owned by uid ${stat.uid}, expected effective uid ${expectedUid}`);
14719
+ }
14604
14720
  const mode = stat.mode & 511;
14605
14721
  if ((mode & 63) !== 0) {
14606
14722
  throw new ConnectionFileError(`connection file ${path} has insecure permissions 0o${mode.toString(8)}; expected owner-only 0600`);
14607
14723
  }
14608
14724
  }
14609
14725
  async function readConnectionFile(path) {
14610
- await verifyOwnerOnly(path);
14611
- const raw = await fs2.readFile(path, "utf8");
14726
+ return readConnectionFileForUid(path, process.geteuid?.());
14727
+ }
14728
+ async function readConnectionFileForUid(path, expectedUid) {
14729
+ const handle = await fs2.open(path, "r");
14730
+ let raw;
14731
+ try {
14732
+ verifyOwnerOnly(path, await handle.stat(), expectedUid);
14733
+ raw = await handle.readFile({ encoding: "utf8" });
14734
+ } finally {
14735
+ await handle.close();
14736
+ }
14612
14737
  let parsed;
14613
14738
  try {
14614
14739
  parsed = JSON.parse(raw);
@@ -14642,7 +14767,7 @@ async function readConnectionFile(path) {
14642
14767
  return info;
14643
14768
  }
14644
14769
 
14645
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/launch-nonce.js
14770
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/launch-nonce.js
14646
14771
  import { closeSync as closeSync4, fstatSync as fstatSync2, readSync as readSync3 } from "node:fs";
14647
14772
  var SUBC_LAUNCH_NONCE_ENV = "SUBC_LAUNCH_NONCE";
14648
14773
  var SUBC_LAUNCH_NONCE_FD_ENV = "SUBC_LAUNCH_NONCE_FD";
@@ -14831,7 +14956,7 @@ function closeQuietly(fd) {
14831
14956
  } catch {}
14832
14957
  }
14833
14958
 
14834
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/route-handle.js
14959
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/route-handle.js
14835
14960
  var connectionToken = new WeakMap;
14836
14961
  var reverseRequests = new WeakMap;
14837
14962
 
@@ -14924,7 +15049,7 @@ function sameRouteHandle(left, right) {
14924
15049
  return left === right;
14925
15050
  }
14926
15051
 
14927
- // ../../node_modules/.bun/@cortexkit+subc-client@0.20.0/node_modules/@cortexkit/subc-client/dist/client.js
15052
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.21.2/node_modules/@cortexkit/subc-client/dist/client.js
14928
15053
  var debug = debuglog("subc-client");
14929
15054
  var DEFAULT_HANDSHAKE_TIMEOUT_MS = 1e4;
14930
15055
  var DEFAULT_REQUEST_TIMEOUT_MS = 30000;
@@ -14981,6 +15106,12 @@ function requireBinaryBody(body) {
14981
15106
  const type = body === null ? "null" : Array.isArray(body) ? "array" : typeof body;
14982
15107
  throw new SubcError(`binary request body must be a Uint8Array; got ${type}`, "binary_body_required");
14983
15108
  }
15109
+ var SCOPE_ROUTE_CLOSE_REASONS = new Set([
15110
+ "scope_ended",
15111
+ "scope_carrier_removed",
15112
+ "scope_delegation_changed",
15113
+ "scope_parent_ended"
15114
+ ]);
14984
15115
  function parseRouteCloseReason(reason) {
14985
15116
  switch (reason) {
14986
15117
  case "reload":
@@ -15123,14 +15254,16 @@ class SubcClient {
15123
15254
  return modules.filter((module) => module.capabilities?.provides.includes(capability) ?? false).map((module) => module.module_id).sort();
15124
15255
  }
15125
15256
  async routeOpen(target, identity, opts = {}) {
15257
+ const scope = validatedRouteScope(opts.scope);
15126
15258
  const release = await this.routeOpenGate.acquire();
15127
15259
  try {
15128
- return await this.sendRouteOpen(target, identity, opts);
15260
+ return await this.sendRouteOpen(target, identity, { ...opts, scope });
15129
15261
  } finally {
15130
15262
  release();
15131
15263
  }
15132
15264
  }
15133
15265
  async sendRouteOpen(target, identity, opts = {}) {
15266
+ const scope = validatedRouteScope(opts.scope);
15134
15267
  const consumerIdentity = routeOpenConsumerIdentity(opts);
15135
15268
  const reverseRequests = opts.reverseRequests ?? new ReverseRequestRegistry;
15136
15269
  reverseRequests.seal();
@@ -15140,7 +15273,8 @@ class SubcClient {
15140
15273
  target,
15141
15274
  identity,
15142
15275
  ...consumerIdentity ? { consumer_identity: consumerIdentity } : {},
15143
- ...consumerCapabilities.length > 0 ? { consumer_capabilities: consumerCapabilities } : {}
15276
+ ...consumerCapabilities.length > 0 ? { consumer_capabilities: consumerCapabilities } : {},
15277
+ ...scope ? { scope: { owner: scope.owner, ref: scope.ref, scope_epoch: scope.scopeEpoch } } : {}
15144
15278
  });
15145
15279
  let installed = null;
15146
15280
  const install = (frame) => {
@@ -15343,7 +15477,7 @@ class SubcClient {
15343
15477
  this.sendRouteGoodbye(handle);
15344
15478
  }
15345
15479
  async closeManagedRoute(target, identity, opts = {}) {
15346
- const key = routeCacheKey(target, identity, routeOpenConsumerIdentity(opts), (opts.reverseRequests ?? new ReverseRequestRegistry).capabilities());
15480
+ const key = routeCacheKey(target, identity, routeOpenConsumerIdentity(opts), (opts.reverseRequests ?? new ReverseRequestRegistry).capabilities(), validatedRouteScope(opts.scope));
15347
15481
  const cached = this.routes.get(key);
15348
15482
  if (!cached)
15349
15483
  return;
@@ -15439,13 +15573,13 @@ class SubcClient {
15439
15573
  };
15440
15574
  pending.timer = setTimeout(() => this.arbitrateTimeout(key, pending, channel, corr, ms), ms);
15441
15575
  this.pending.set(key, pending);
15442
- if (signal)
15443
- this.attachCancelSignal(signal, key, handle, corr, priority);
15444
15576
  writeBorrowed(this.sock, encodeFrame(frame), Date.now() + ms).catch((error) => {
15445
15577
  const current = this.pending.get(key);
15446
15578
  if (current)
15447
15579
  this.rejectPending(key, current, error instanceof Error ? error : new SubcError(String(error)));
15448
15580
  });
15581
+ if (signal)
15582
+ this.attachCancelSignal(signal, key, handle, corr, priority);
15449
15583
  });
15450
15584
  }
15451
15585
  arbitrateTimeout(key, pending, channel, corr, ms) {
@@ -15519,8 +15653,6 @@ class SubcClient {
15519
15653
  };
15520
15654
  pending.timer = setTimeout(() => this.arbitrateTimeout(key, pending, handle.channel, corr, ms), ms);
15521
15655
  this.pending.set(key, pending);
15522
- if (signal)
15523
- this.attachCancelSignal(signal, key, handle, corr, priority);
15524
15656
  const write = writeTrackedBorrowed(this.sock, encodeFrame(frame), Date.now() + ms);
15525
15657
  handedToSocket = write.queued;
15526
15658
  write.completed.catch((error) => {
@@ -15528,9 +15660,12 @@ class SubcClient {
15528
15660
  if (current)
15529
15661
  this.rejectPending(key, current, error instanceof Error ? error : new SubcError(String(error)));
15530
15662
  });
15663
+ if (signal && write.queued)
15664
+ this.attachCancelSignal(signal, key, handle, corr, priority);
15531
15665
  });
15532
15666
  }
15533
15667
  async cachedRouteHandle(moduleId, opts) {
15668
+ const scope = validatedRouteScope(opts.scope);
15534
15669
  const identity = opts.identity ?? this.opts.identity;
15535
15670
  if (!identity) {
15536
15671
  throw new SubcCallError("terminal", "managed call requires a BindIdentity in SubcClient.connect({ identity }) or call(..., { identity })", "missing_identity");
@@ -15539,7 +15674,7 @@ class SubcClient {
15539
15674
  const consumerIdentity = routeOpenConsumerIdentity(opts);
15540
15675
  const reverseRequests = opts.reverseRequests ?? new ReverseRequestRegistry;
15541
15676
  reverseRequests.seal();
15542
- const key = routeCacheKey(target, identity, consumerIdentity, reverseRequests.capabilities());
15677
+ const key = routeCacheKey(target, identity, consumerIdentity, reverseRequests.capabilities(), scope);
15543
15678
  let cached = this.routes.get(key);
15544
15679
  if (!cached) {
15545
15680
  cached = {
@@ -15547,6 +15682,7 @@ class SubcClient {
15547
15682
  moduleId,
15548
15683
  target,
15549
15684
  identity,
15685
+ scope,
15550
15686
  consumerIdentity,
15551
15687
  reverseRequests,
15552
15688
  handle: null,
@@ -15554,6 +15690,8 @@ class SubcClient {
15554
15690
  };
15555
15691
  this.routes.set(key, cached);
15556
15692
  }
15693
+ if (cached.endReason !== undefined)
15694
+ throw this.endedCachedRoute(cached.endReason);
15557
15695
  if (cached.handle && this.isLiveHandle(cached.handle))
15558
15696
  return cached.handle;
15559
15697
  const startedAt = this.opts.now();
@@ -15589,6 +15727,8 @@ class SubcClient {
15589
15727
  for (;; ) {
15590
15728
  if (cached.closed)
15591
15729
  throw this.routeClosedDuringOpen();
15730
+ if (cached.endReason !== undefined)
15731
+ throw this.endedCachedRoute(cached.endReason);
15592
15732
  try {
15593
15733
  await this.ensureConnectedForManaged();
15594
15734
  } catch (error) {
@@ -15603,6 +15743,7 @@ class SubcClient {
15603
15743
  try {
15604
15744
  outcome = {
15605
15745
  handle: await this.sendRouteOpen(cached.target, cached.identity, {
15746
+ scope: cached.scope,
15606
15747
  consumerIdentity: cached.consumerIdentity ?? null,
15607
15748
  reverseRequests: cached.reverseRequests
15608
15749
  })
@@ -15770,10 +15911,11 @@ class SubcClient {
15770
15911
  }
15771
15912
  for (const key of routeKeys) {
15772
15913
  const cached = this.routes.get(key);
15773
- if (!cached || cached.closed)
15914
+ if (!cached || cached.closed || cached.endReason !== undefined)
15774
15915
  continue;
15775
15916
  try {
15776
15917
  const handle = await this.routeOpen(cached.target, cached.identity, {
15918
+ scope: cached.scope,
15777
15919
  consumerIdentity: cached.consumerIdentity ?? null,
15778
15920
  reverseRequests: cached.reverseRequests
15779
15921
  });
@@ -15801,11 +15943,15 @@ class SubcClient {
15801
15943
  const cause = new SubcError("route was closed before route.open completed", "route_closed", undefined, "closed_by_caller");
15802
15944
  return new SubcCallError("not_sent", cause.message, cause.code, cause);
15803
15945
  }
15946
+ endedCachedRoute(reason) {
15947
+ const cause = new SubcError(`cached route closed by subc (${reason}); must not reopen`, "route_closed", undefined, reason);
15948
+ return this.terminalCallError(cause.message, cause);
15949
+ }
15804
15950
  async readLoop(sock, generation) {
15805
15951
  try {
15806
15952
  for (;; ) {
15807
15953
  this.readerActive = false;
15808
- const frame = await sock.readFrame(Number.POSITIVE_INFINITY, { afterHeaderMs: BODY_READ_TIMEOUT_MS }, () => {
15954
+ const frame = await sock.readFrame(Number.POSITIVE_INFINITY, { afterHeaderMs: this.opts.bodyReadTimeoutMs ?? BODY_READ_TIMEOUT_MS }, () => {
15809
15955
  this.readerActive = true;
15810
15956
  });
15811
15957
  try {
@@ -15884,6 +16030,7 @@ class SubcClient {
15884
16030
  }
15885
16031
  if (frame.header.ty === FrameType.Goodbye && handle) {
15886
16032
  this.failHandle(handle, new SubcError("route closed by subc (GOODBYE)", "route_closed", undefined, this.routeEndReason(handle.channel)));
16033
+ this.handleCachedRouteClose(handle.channel, this.routeEndReason(handle.channel));
15887
16034
  if (this.liveRoutes.get(handle.channel) === handle)
15888
16035
  this.liveRoutes.delete(handle.channel);
15889
16036
  this.evictRouteHandle(handle);
@@ -15973,13 +16120,29 @@ class SubcClient {
15973
16120
  const final = push.op === "route.closed";
15974
16121
  if (final || !this.routeEndReasons.get(channel)?.final) {
15975
16122
  this.routeEndReasons.set(channel, { reason, final });
16123
+ if (final)
16124
+ this.handleCachedRouteClose(channel, reason);
15976
16125
  }
15977
16126
  }
15978
16127
  }
15979
16128
  } else if (typeof push.body.module_id === "string") {
15980
16129
  for (const [channel, module] of this.routeModules) {
15981
- if (module === push.body.module_id)
16130
+ if (module === push.body.module_id) {
15982
16131
  this.legacyChannelReasons.set(channel, reason);
16132
+ if (push.op === "route.closed")
16133
+ this.handleCachedRouteClose(channel, reason);
16134
+ }
16135
+ }
16136
+ }
16137
+ }
16138
+ handleCachedRouteClose(channel, reason) {
16139
+ for (const [key, cached] of this.routes) {
16140
+ if (cached.handle?.channel !== channel)
16141
+ continue;
16142
+ if (SCOPE_ROUTE_CLOSE_REASONS.has(reason)) {
16143
+ cached.endReason = reason;
16144
+ } else {
16145
+ this.routes.delete(key);
15983
16146
  }
15984
16147
  }
15985
16148
  }
@@ -16002,6 +16165,10 @@ class SubcClient {
16002
16165
  this.routeEndReasons.clear();
16003
16166
  this.legacyChannelReasons.clear();
16004
16167
  this.routeModules.clear();
16168
+ for (const [key, cached] of this.routes) {
16169
+ if (cached.endReason === undefined)
16170
+ this.routes.delete(key);
16171
+ }
16005
16172
  for (const [key, pending] of this.pending) {
16006
16173
  this.rejectPending(key, pending, pending.handle ? routeError : err);
16007
16174
  }
@@ -16096,6 +16263,10 @@ function isRouteOpenRefusal(err) {
16096
16263
  function isRetryableRouteOpenCode(code) {
16097
16264
  if (code === "capability_forbidden")
16098
16265
  return false;
16266
+ if (code === "target_flow_unsupported")
16267
+ return false;
16268
+ if (code === "target_agent_run_unsupported")
16269
+ return false;
16099
16270
  return code === "module_reloading" || code === "module_warming" || code === "target_unavailable" || code === "module_timeout" || code === "scope_not_synced" || code === "scope_changed";
16100
16271
  }
16101
16272
  function equalJitter(delayMs, random) {
@@ -16153,6 +16324,7 @@ function normalizeConnectOptions(opts) {
16153
16324
  random: opts.random ?? Math.random,
16154
16325
  routeOpenRetryDeadlineMs: opts.routeOpenRetryDeadlineMs ?? ROUTE_OPEN_RETRY_DEADLINE_MS,
16155
16326
  timeoutArbitrationGraceMs: opts.timeoutArbitrationGraceMs ?? TIMEOUT_ARBITRATION_GRACE_MS,
16327
+ bodyReadTimeoutMs: opts.bodyReadTimeoutMs,
16156
16328
  livenessProbeWindowMs: opts.livenessProbeWindowMs ?? LIVENESS_PROBE_WINDOW_MS,
16157
16329
  onControlPush: opts.onControlPush
16158
16330
  };
@@ -16176,10 +16348,28 @@ function isValidCapabilityIdentifier(identifier) {
16176
16348
  const numericVersion = Number(version);
16177
16349
  return Number.isSafeInteger(numericVersion) && numericVersion >= 1 && numericVersion <= 4294967295;
16178
16350
  }
16179
- function routeCacheKey(target, identity, consumerIdentity, consumerCapabilities) {
16351
+ function routeCacheKey(target, identity, consumerIdentity, consumerCapabilities, scope) {
16180
16352
  const consumerPart = consumerIdentity ? `${consumerIdentity.module_id}\x00${consumerIdentity.launch_nonce}` : "";
16181
16353
  const capabilitiesPart = [...consumerCapabilities].sort().join("\x00");
16182
- return `${target.kind}\x00${target.module_id}\x00${identity.project_root}\x00${identity.harness}\x00${identity.session}\x00${consumerPart}\x00${capabilitiesPart}`;
16354
+ const scopePart = scope ? JSON.stringify([scope.owner.kind, scope.owner.kind === "reserved" ? scope.owner.module_id : null, scope.ref, scope.scopeEpoch]) : "";
16355
+ return `${target.kind}\x00${target.module_id}\x00${identity.project_root}\x00${identity.harness}\x00${identity.session}\x00${consumerPart}\x00${capabilitiesPart}\x00${scopePart}`;
16356
+ }
16357
+ function validatedRouteScope(scope) {
16358
+ if (scope === undefined)
16359
+ return;
16360
+ 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)) {
16361
+ throw new SubcError("scope.owner must be a Principal with a non-empty reserved module_id", "invalid_scope");
16362
+ }
16363
+ if (typeof scope.ref !== "string" || scope.ref.length === 0) {
16364
+ throw new SubcError("scope.ref must be a non-empty string", "invalid_scope");
16365
+ }
16366
+ if (scope.scopeEpoch === undefined) {
16367
+ throw new SubcError("scope.scopeEpoch is required", "scope_epoch_required");
16368
+ }
16369
+ if (!Number.isSafeInteger(scope.scopeEpoch) || scope.scopeEpoch < 0) {
16370
+ throw new SubcError("scope.scopeEpoch must be a safe non-negative integer", "invalid_scope");
16371
+ }
16372
+ return { owner: { ...scope.owner }, ref: scope.ref, scopeEpoch: scope.scopeEpoch };
16183
16373
  }
16184
16374
  function routeOpenConsumerIdentity(opts = {}) {
16185
16375
  if (opts.consumerIdentity !== undefined)
@@ -16700,6 +16890,41 @@ class SubcRouteClosedMidCallError extends SubcCallError {
16700
16890
  this.reason = reason;
16701
16891
  }
16702
16892
  }
16893
+
16894
+ class SubcFrameReadTimeoutError extends SubcCallError {
16895
+ constructor(cause) {
16896
+ super("outcome_unknown", "the command may have run; timed out waiting for frame body bytes from the AFT daemon", "frame_read_timeout", cause);
16897
+ this.name = "SubcFrameReadTimeoutError";
16898
+ }
16899
+ }
16900
+ function isFrameBodyReadTimeoutMessage(message) {
16901
+ return typeof message === "string" && (/timed out waiting for \d+ bytes/i.test(message) || message.includes("timed out waiting for frame body bytes"));
16902
+ }
16903
+ function isSubcFrameReadTimeoutError(error) {
16904
+ if (error instanceof SubcFrameReadTimeoutError)
16905
+ return true;
16906
+ if (error instanceof SocketTimeoutError && isFrameBodyReadTimeoutMessage(error.message)) {
16907
+ return true;
16908
+ }
16909
+ if (error instanceof SubcError && isFrameBodyReadTimeoutMessage(error.message)) {
16910
+ return true;
16911
+ }
16912
+ if (error instanceof SubcCallError) {
16913
+ if (error.code === "frame_read_timeout")
16914
+ return true;
16915
+ if (error.cause !== undefined && error.cause !== error && isSubcFrameReadTimeoutError(error.cause)) {
16916
+ return true;
16917
+ }
16918
+ if (isFrameBodyReadTimeoutMessage(error.message))
16919
+ return true;
16920
+ }
16921
+ return false;
16922
+ }
16923
+ function frameReadTimeoutError(error) {
16924
+ if (!isSubcFrameReadTimeoutError(error))
16925
+ return error;
16926
+ return error instanceof SubcFrameReadTimeoutError ? error : new SubcFrameReadTimeoutError(error);
16927
+ }
16703
16928
  function isLocalRouteCloseRejection(error) {
16704
16929
  return error instanceof SubcError && error.closeReason === "closed_by_caller";
16705
16930
  }
@@ -16726,6 +16951,8 @@ function isSubcClientClosedError(error) {
16726
16951
  return error instanceof SubcError && error.message === "client closed";
16727
16952
  }
16728
16953
  function isSocketDeathError(error, depth = 0) {
16954
+ if (isSubcFrameReadTimeoutError(error))
16955
+ return true;
16729
16956
  if (error instanceof SubcError && error.closeReason === "connection_lost")
16730
16957
  return true;
16731
16958
  if (error instanceof SubcCallError) {
@@ -16768,6 +16995,12 @@ class SubcLivenessProbeTimeoutError extends Error {
16768
16995
  }
16769
16996
  }
16770
16997
  var AFT_MODULE_ID = "aft";
16998
+ var HOST_SCOPE_LOOKUP_TIMEOUT_MS = 2000;
16999
+ var HOST_SCOPE_RETRY_FLOOR_MS = 5000;
17000
+ var HOST_SCOPE_RETRY_CAP_MS = 300000;
17001
+ function isHostScopeSyncRefusal(code) {
17002
+ return code === "scope_not_live" || code === "scope_not_synced" || code === "scope_changed";
17003
+ }
16771
17004
  var MAX_CONSECUTIVE_TRANSPORT_FAILURES = 3;
16772
17005
  var LIVENESS_PROBE_TIMEOUT_MS = 15000;
16773
17006
  var REQUEST_DEADLINE_CODE = "request_deadline";
@@ -16846,7 +17079,7 @@ class BgSubscription {
16846
17079
  identity;
16847
17080
  acquireClient;
16848
17081
  dropClient;
16849
- consumerIdentity;
17082
+ openRoute;
16850
17083
  onNudge;
16851
17084
  sleep;
16852
17085
  canAttach;
@@ -16857,21 +17090,25 @@ class BgSubscription {
16857
17090
  isCurrent;
16858
17091
  noteClientAlive;
16859
17092
  noteUnansweredCall;
17093
+ onRouteAttached;
16860
17094
  stopped = false;
16861
17095
  current = null;
16862
17096
  attached = null;
17097
+ route = null;
16863
17098
  loop;
16864
17099
  lifecycleLogState = new Map;
16865
17100
  nudgeReceiptLogState = null;
16866
- constructor(identity, acquireClient, dropClient, consumerIdentity, onNudge, sleep, canAttach, onRootAttachFailure, onDormant, dispatchProbeIntervalMs, nudgeRef, isCurrent = () => true, noteClientAlive = () => {
17101
+ constructor(identity, acquireClient, dropClient, openRoute, onNudge, sleep, canAttach, onRootAttachFailure, onDormant, dispatchProbeIntervalMs, nudgeRef, isCurrent = () => true, noteClientAlive = () => {
16867
17102
  return;
16868
17103
  }, noteUnansweredCall = () => {
16869
17104
  return;
17105
+ }, onRouteAttached = () => {
17106
+ return;
16870
17107
  }) {
16871
17108
  this.identity = identity;
16872
17109
  this.acquireClient = acquireClient;
16873
17110
  this.dropClient = dropClient;
16874
- this.consumerIdentity = consumerIdentity;
17111
+ this.openRoute = openRoute;
16875
17112
  this.onNudge = onNudge;
16876
17113
  this.sleep = sleep;
16877
17114
  this.canAttach = canAttach;
@@ -16882,11 +17119,15 @@ class BgSubscription {
16882
17119
  this.isCurrent = isCurrent;
16883
17120
  this.noteClientAlive = noteClientAlive;
16884
17121
  this.noteUnansweredCall = noteUnansweredCall;
17122
+ this.onRouteAttached = onRouteAttached;
16885
17123
  this.loop = this.run();
16886
17124
  }
16887
17125
  get attachedClient() {
16888
17126
  return this.attached;
16889
17127
  }
17128
+ get attachedRoute() {
17129
+ return this.route;
17130
+ }
16890
17131
  async stop() {
16891
17132
  this.stopped = true;
16892
17133
  const sub = this.current;
@@ -17011,7 +17252,7 @@ class BgSubscription {
17011
17252
  }
17012
17253
  let route;
17013
17254
  try {
17014
- route = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, this.identity, { consumerIdentity: this.consumerIdentity });
17255
+ route = await this.openRoute(client);
17015
17256
  } catch (err) {
17016
17257
  if (this.isCurrent() && this.onRootAttachFailure(err)) {
17017
17258
  if (reconnecting)
@@ -17069,6 +17310,8 @@ class BgSubscription {
17069
17310
  });
17070
17311
  this.current = sub;
17071
17312
  this.attached = client;
17313
+ this.route = route;
17314
+ this.onRouteAttached();
17072
17315
  stopDispatchProbe = this.startDispatchProbe(client, routeId);
17073
17316
  this.info("subscription-open", `subscription open channel=${routeId}`);
17074
17317
  if (reconnecting) {
@@ -17111,6 +17354,7 @@ class BgSubscription {
17111
17354
  stopDispatchProbe();
17112
17355
  this.current = null;
17113
17356
  this.attached = null;
17357
+ this.route = null;
17114
17358
  safeCloseRoute(client, route);
17115
17359
  }
17116
17360
  await this.backoff(backoffAttempt++);
@@ -17126,7 +17370,7 @@ function isRecord(value) {
17126
17370
  return typeof value === "object" && value !== null && !Array.isArray(value);
17127
17371
  }
17128
17372
  function isRouteProvenAbsentError(err) {
17129
- return err instanceof SubcError && err.code === "unknown_channel" || err instanceof StaleRouteHandleError;
17373
+ return err instanceof SubcError && (err.code === "unknown_channel" || err.code === "stale_route_epoch") || err instanceof StaleRouteHandleError;
17130
17374
  }
17131
17375
  function isAbsentRootRouteError(err) {
17132
17376
  return isRecord(err) && err.code === "config_divergence" && typeof err.message === "string" && err.message.includes("project root does not exist");
@@ -17253,6 +17497,8 @@ class SubcTransport {
17253
17497
  body.preview = true;
17254
17498
  if (options?.workerSession === true)
17255
17499
  body[WORKER_SESSION_FIELD] = true;
17500
+ if (options?.hostToolNames)
17501
+ body[HOST_TOOL_NAMES_FIELD] = options.hostToolNames;
17256
17502
  body[PRESET_FIELD] = callPresetFor(options?.workerSession === true);
17257
17503
  const reply = await this.pool.routeRequest(this.identityFor(sessionId), body, timeoutMs, onProgress, this.generation, abortSignal);
17258
17504
  return reliftReply(reply);
@@ -17264,10 +17510,12 @@ class SubcTransport {
17264
17510
  }
17265
17511
  const { timeoutMs, onProgress, abortSignal } = this.splitOptions(options);
17266
17512
  const session = typeof params.session_id === "string" ? params.session_id : undefined;
17267
- const { [WORKER_SESSION_FIELD]: workerSession, ...args } = params;
17513
+ const { [WORKER_SESSION_FIELD]: workerSession, [HOST_TOOL_NAMES_FIELD]: hostToolNames, ...args } = params;
17268
17514
  const body = { name: command, arguments: args };
17269
17515
  if (workerSession === true)
17270
17516
  body[WORKER_SESSION_FIELD] = true;
17517
+ if (hostToolNames !== undefined)
17518
+ body[HOST_TOOL_NAMES_FIELD] = hostToolNames;
17271
17519
  body[PRESET_FIELD] = callPresetFor(workerSession === true);
17272
17520
  const editSlotSurvives = this.pool.getEditSlotSurvives();
17273
17521
  if (editSlotSurvives !== undefined)
@@ -17290,6 +17538,10 @@ class SubcTransportPool {
17290
17538
  connectionFile;
17291
17539
  handshakeTimeoutMs;
17292
17540
  consumerIdentity;
17541
+ hostScopes;
17542
+ hostScopeCache = new Map;
17543
+ hostScopeRefusals = new Set;
17544
+ hostScopeOutcomes = new Map;
17293
17545
  connectFn;
17294
17546
  onBgEventsNudge;
17295
17547
  onBgEventsNudgeRef;
@@ -17311,6 +17563,7 @@ class SubcTransportPool {
17311
17563
  routeReopenRetry = null;
17312
17564
  routeReopenRetryMs = null;
17313
17565
  sessions = new Map;
17566
+ sessionIndex = new Map;
17314
17567
  observedStarts = new Set;
17315
17568
  sessionSweep = null;
17316
17569
  rootIndex = new Map;
@@ -17330,6 +17583,7 @@ class SubcTransportPool {
17330
17583
  this.harness = options.harness;
17331
17584
  this.handshakeTimeoutMs = options.handshakeTimeoutMs;
17332
17585
  this.consumerIdentity = options.consumerIdentity;
17586
+ this.hostScopes = options.hostScopes ?? false;
17333
17587
  this.connectFn = options.connect ?? ((opts) => SubcClient.connect(opts));
17334
17588
  this.onBgEventsNudge = options.onBgEventsNudge;
17335
17589
  this.onBgEventsNudgeRef = options.onBgEventsNudgeRef;
@@ -17549,7 +17803,9 @@ class SubcTransportPool {
17549
17803
  return transport;
17550
17804
  }
17551
17805
  getBridge(projectRoot) {
17552
- const root = this.canonicalRoot(projectRoot);
17806
+ return this.getBridgeForCanonicalRoot(this.canonicalRoot(projectRoot));
17807
+ }
17808
+ getBridgeForCanonicalRoot(root) {
17553
17809
  if (this.shuttingDown && this.lifecycleEnabled()) {
17554
17810
  throw new SubcTransportShuttingDownError;
17555
17811
  }
@@ -17646,6 +17902,12 @@ class SubcTransportPool {
17646
17902
  pendingDetach: false
17647
17903
  };
17648
17904
  this.sessions.set(key, record);
17905
+ let records = this.sessionIndex.get(identity.session);
17906
+ if (!records) {
17907
+ records = new Set;
17908
+ this.sessionIndex.set(identity.session, records);
17909
+ }
17910
+ records.add(record);
17649
17911
  let keys = this.rootIndex.get(root);
17650
17912
  if (!keys) {
17651
17913
  keys = new Set;
@@ -17823,6 +18085,10 @@ class SubcTransportPool {
17823
18085
  log(`subc bg_events: ${message}${suffix}`, { sessionId: record.identity.session });
17824
18086
  }
17825
18087
  removeIndexMembership(record) {
18088
+ const records = this.sessionIndex.get(record.identity.session);
18089
+ records?.delete(record);
18090
+ if (records?.size === 0)
18091
+ this.sessionIndex.delete(record.identity.session);
17826
18092
  const keys = this.rootIndex.get(record.canonicalRoot);
17827
18093
  if (!keys)
17828
18094
  return;
@@ -17835,6 +18101,7 @@ class SubcTransportPool {
17835
18101
  this.sessions.delete(record.identityKey);
17836
18102
  this.removeIndexMembership(record);
17837
18103
  }
18104
+ this.stopUnusedHostScopeRetry(record.identity.session);
17838
18105
  }
17839
18106
  detachSession(key, reason) {
17840
18107
  const record = this.sessions.get(key);
@@ -17854,6 +18121,7 @@ class SubcTransportPool {
17854
18121
  record.routeEntry = null;
17855
18122
  if (routeEntry)
17856
18123
  routeEntry.closed = true;
18124
+ this.stopUnusedHostScopeRetry(record.identity.session);
17857
18125
  return { record, bgSub, routeEntry };
17858
18126
  }
17859
18127
  async cleanupDetached(detached) {
@@ -17910,6 +18178,17 @@ class SubcTransportPool {
17910
18178
  entry.inflight -= 1;
17911
18179
  this.closeRetiredRoute(record, entry);
17912
18180
  }
18181
+ retireHostToolRoute(record) {
18182
+ const entry = record.routeEntry;
18183
+ if (!entry)
18184
+ return;
18185
+ entry.closed = true;
18186
+ entry.closedBy ??= "route replaced";
18187
+ record.routeEntry = null;
18188
+ record.retiredRoutes.add(entry);
18189
+ this.closeRetiredRoute(record, entry);
18190
+ this.stopUnusedHostScopeRetry(record.identity.session);
18191
+ }
17913
18192
  async routeRequest(identity, body, timeoutMs, onProgress, expectedGeneration, abortSignal) {
17914
18193
  const callStartedAt = performance.now();
17915
18194
  const callDeadlineMs = timeoutMs ?? SUBC_DEFAULT_REQUEST_TIMEOUT_MS;
@@ -18048,15 +18327,14 @@ class SubcTransportPool {
18048
18327
  const clearRouteEntry = (entry) => {
18049
18328
  if (record.routeEntry !== entry)
18050
18329
  return;
18051
- entry.closed = true;
18052
- entry.closedBy ??= "route replaced";
18053
- record.routeEntry = null;
18054
- record.retiredRoutes.add(entry);
18055
- this.closeRetiredRoute(record, entry);
18330
+ this.retireHostToolRoute(record);
18056
18331
  };
18057
18332
  const handleRequestFailure = (error, entry) => {
18058
18333
  const aborted = error instanceof Error && error.name === "AbortError";
18059
18334
  const ownsClient = this.isCurrentSession(key, record) && this.client === client && !this.isReapInduced(record);
18335
+ if (ownsClient && error instanceof SubcError && error.closeReason?.startsWith("scope_")) {
18336
+ this.invalidateHostScope(identity.session);
18337
+ }
18060
18338
  if (!aborted && ownsClient && isSocketDeathError(error)) {
18061
18339
  this.dropClient(client, {
18062
18340
  error,
@@ -18065,7 +18343,7 @@ class SubcTransportPool {
18065
18343
  route: routeLabel(entry.handle)
18066
18344
  });
18067
18345
  }
18068
- if (aborted || isRouteProvenAbsentError(error) || error instanceof SubcError && error.closeReason !== undefined) {
18346
+ if (aborted || isRouteProvenAbsentError(error) || error instanceof SubcError && error.closeReason !== undefined || isSubcFrameReadTimeoutError(error)) {
18069
18347
  clearRouteEntry(entry);
18070
18348
  }
18071
18349
  if (aborted || !ownsClient || this.client !== client)
@@ -18144,10 +18422,13 @@ class SubcTransportPool {
18144
18422
  this.resetRouteReopenBackoff();
18145
18423
  return reply;
18146
18424
  } catch (requestError) {
18147
- const error = routeClosedMidCallError(requestError, routeAndEntry.entry, record);
18425
+ const error = frameReadTimeoutError(routeClosedMidCallError(requestError, routeAndEntry.entry, record));
18148
18426
  if (this.isReapInduced(record))
18149
18427
  throw this.annotateReapError(error, record);
18150
18428
  const ownsRoute = this.isCurrentSession(key, record) && this.client === client;
18429
+ if (ownsRoute && (error instanceof SubcError && error.code === "stale_route_epoch" || error instanceof StaleRouteHandleError)) {
18430
+ this.invalidateHostScope(identity.session);
18431
+ }
18151
18432
  if (ownsRoute && isRouteRequestReloadRefusal(error)) {
18152
18433
  clearRouteEntry(routeAndEntry.entry);
18153
18434
  lastRefusal = error;
@@ -18224,9 +18505,14 @@ class SubcTransportPool {
18224
18505
  return this.client;
18225
18506
  if (this.connecting)
18226
18507
  return this.connecting;
18508
+ let owner;
18227
18509
  this.connecting = this.connectFn({
18228
18510
  connectionFile: this.connectionFile,
18229
- handshakeTimeoutMs: this.handshakeTimeoutMs
18511
+ handshakeTimeoutMs: this.handshakeTimeoutMs,
18512
+ onControlPush: (push) => {
18513
+ if (owner && this.client === owner)
18514
+ this.onScopeRouteClose(push);
18515
+ }
18230
18516
  }).then((client) => {
18231
18517
  this.connecting = null;
18232
18518
  if (this.shuttingDown) {
@@ -18236,6 +18522,7 @@ class SubcTransportPool {
18236
18522
  throw new SubcTransportShuttingDownError;
18237
18523
  }
18238
18524
  this.client = client;
18525
+ owner = client;
18239
18526
  this.hadWorkingClient = true;
18240
18527
  this.transportFailures = 0;
18241
18528
  return client;
@@ -18265,9 +18552,7 @@ class SubcTransportPool {
18265
18552
  closedBy: null
18266
18553
  };
18267
18554
  this.assertRootCanAttach(record.canonicalRoot);
18268
- const opening = client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, {
18269
- consumerIdentity: this.consumerIdentity
18270
- }).then((route) => {
18555
+ const opening = this.openHostToolRoute(client, identity, record, entry).then((route) => {
18271
18556
  if (!this.isCurrentSession(record.identityKey, record) || record.routeEntry !== entry || entry.closed || this.client !== client || this.lifecycleEnabled() && !this.isCurrentLiveGeneration(record.canonicalRoot, record.generation)) {
18272
18557
  safeCloseRoute(client, route);
18273
18558
  if (record.routeEntry === entry)
@@ -18277,6 +18562,12 @@ class SubcTransportPool {
18277
18562
  entry.handle = route;
18278
18563
  entry.opening = null;
18279
18564
  this.noteClientAlive(client);
18565
+ const state = this.hostScopeCache.get(identity.session);
18566
+ if (state)
18567
+ this.scheduleHostScopeRetry(identity.session, state);
18568
+ if (state?.answer?.result === "scope" && !state.scopeBindPending && !entry.hostScope && !this.hostScopeRefusals.has(identity.session)) {
18569
+ this.promoteHostScope(identity.session);
18570
+ }
18280
18571
  return route;
18281
18572
  }).catch((error) => {
18282
18573
  const currentEntry = this.isCurrentSession(record.identityKey, record) && record.routeEntry === entry;
@@ -18284,6 +18575,7 @@ class SubcTransportPool {
18284
18575
  entry.closed = true;
18285
18576
  record.routeEntry = null;
18286
18577
  }
18578
+ this.stopUnusedHostScopeRetry(identity.session);
18287
18579
  if (!this.isCurrentSession(record.identityKey, record) && !(error instanceof RouteTornDownError)) {
18288
18580
  throw new RouteTornDownError("subc route opened after session closed");
18289
18581
  }
@@ -18296,6 +18588,327 @@ class SubcTransportPool {
18296
18588
  record.routeEntry = entry;
18297
18589
  return { route: await opening, entry };
18298
18590
  }
18591
+ hostScope(client, identity) {
18592
+ if (this.hostScopeRefusals.has(identity.session))
18593
+ return Promise.resolve(undefined);
18594
+ let state = this.hostScopeCache.get(identity.session);
18595
+ if (!state) {
18596
+ state = { retryAt: 0, retryMs: HOST_SCOPE_RETRY_FLOOR_MS };
18597
+ this.hostScopeCache.set(identity.session, state);
18598
+ }
18599
+ if (state.answer) {
18600
+ return Promise.resolve(state.answer.result === "scope" && !state.scopeBindPending ? state.answer.scope : undefined);
18601
+ }
18602
+ if (state.pending)
18603
+ return state.pending;
18604
+ if (Date.now() < state.retryAt)
18605
+ return Promise.resolve(undefined);
18606
+ const current = state;
18607
+ clearTimeout(current.retryTimer);
18608
+ current.retryTimer = undefined;
18609
+ const abort = new AbortController;
18610
+ current.lookupAbort = abort;
18611
+ const lookup = this.readHostScope(client, identity, abort.signal).then((outcome) => {
18612
+ if (this.hostScopeCache.get(identity.session) !== current)
18613
+ return;
18614
+ current.pending = undefined;
18615
+ current.lookupAbort = undefined;
18616
+ if (abort.signal.aborted)
18617
+ return;
18618
+ this.logHostScopeOutcome(identity.session, outcome);
18619
+ if (outcome.result === "scope" || outcome.result === "no_scope") {
18620
+ current.answer = outcome;
18621
+ current.retryAt = 0;
18622
+ current.retryMs = HOST_SCOPE_RETRY_FLOOR_MS;
18623
+ if (outcome.result === "scope")
18624
+ this.promoteHostScope(identity.session);
18625
+ } else {
18626
+ current.retryAt = Date.now() + current.retryMs;
18627
+ current.retryMs = Math.min(current.retryMs * 2, HOST_SCOPE_RETRY_CAP_MS);
18628
+ this.scheduleHostScopeRetry(identity.session, current);
18629
+ }
18630
+ return outcome.result === "scope" ? outcome.scope : undefined;
18631
+ });
18632
+ current.pending = lookup;
18633
+ return lookup;
18634
+ }
18635
+ unscopedHostRecord(session) {
18636
+ for (const record of this.sessionIndex.get(session) ?? []) {
18637
+ const entry = record.routeEntry;
18638
+ 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))
18639
+ return record;
18640
+ }
18641
+ return;
18642
+ }
18643
+ cancelHostScopeRetry(state) {
18644
+ clearTimeout(state.retryTimer);
18645
+ state.retryTimer = undefined;
18646
+ state.lookupAbort?.abort();
18647
+ }
18648
+ stopUnusedHostScopeRetry(session) {
18649
+ const state = this.hostScopeCache.get(session);
18650
+ if (state && !this.unscopedHostRecord(session)) {
18651
+ if (state.retryTimer || state.scopeBindPending && state.bindPending)
18652
+ log(`subc head scope: session=${session} retry stopped reason=route_closed`);
18653
+ this.cancelHostScopeRetry(state);
18654
+ state.scopeBindPending = false;
18655
+ }
18656
+ }
18657
+ scheduleHostScopeRetry(session, state) {
18658
+ 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))
18659
+ return;
18660
+ state.retryTimer = setTimeout(() => {
18661
+ state.retryTimer = undefined;
18662
+ const record = this.unscopedHostRecord(session);
18663
+ const client = record?.routeEntry?.client ?? record?.bgSub?.attachedClient;
18664
+ if (this.shuttingDown || this.hostScopeCache.get(session) !== state || !record || !client)
18665
+ return;
18666
+ let retry;
18667
+ if (state.scopeBindPending && state.answer?.result === "scope") {
18668
+ state.bindPending = this.retryHostScopeBind(client, record.identity, state, state.answer.scope).finally(() => {
18669
+ state.bindPending = undefined;
18670
+ this.scheduleHostScopeRetry(session, state);
18671
+ });
18672
+ retry = state.bindPending;
18673
+ } else {
18674
+ retry = this.hostScope(client, record.identity);
18675
+ }
18676
+ retry.catch((error) => {
18677
+ warn(`subc head scope: background retry failed code=${error instanceof Error ? error.name : "Error"}`);
18678
+ });
18679
+ }, Math.max(0, state.retryAt - Date.now()));
18680
+ state.retryTimer.unref();
18681
+ }
18682
+ deferHostScopeBind(session, state, code) {
18683
+ state.scopeBindPending = true;
18684
+ if (Date.now() >= state.retryAt) {
18685
+ state.retryAt = Date.now() + state.retryMs;
18686
+ state.retryMs = Math.min(state.retryMs * 2, HOST_SCOPE_RETRY_CAP_MS);
18687
+ }
18688
+ warn(`subc head scope: session=${session} scoped bind refused code=${code}; binding unscoped; retry in=${Math.max(0, state.retryAt - Date.now())}ms`);
18689
+ this.scheduleHostScopeRetry(session, state);
18690
+ }
18691
+ refuseHostScopeBind(session, state, code) {
18692
+ if (!this.hostScopeRefusals.has(session)) {
18693
+ this.hostScopeRefusals.add(session);
18694
+ warn(`subc head scope: session=${session} scoped bind refused code=${code}; binding unscoped; retry stopped`);
18695
+ }
18696
+ if (state) {
18697
+ state.scopeBindPending = false;
18698
+ this.cancelHostScopeRetry(state);
18699
+ }
18700
+ }
18701
+ async retryHostScopeBind(client, identity, state, scope) {
18702
+ try {
18703
+ const route = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, { consumerIdentity: this.consumerIdentity, scope });
18704
+ safeCloseRoute(client, route);
18705
+ if (this.hostScopeCache.get(identity.session) !== state || !state.scopeBindPending || this.shuttingDown || client !== this.client || !this.unscopedHostRecord(identity.session))
18706
+ return;
18707
+ state.scopeBindPending = false;
18708
+ state.retryAt = 0;
18709
+ state.retryMs = HOST_SCOPE_RETRY_FLOOR_MS;
18710
+ log(`subc head scope: session=${identity.session} scoped bind recovered ref=${scope.ref.slice(0, 8)} epoch=${scope.scopeEpoch}`);
18711
+ this.promoteHostScope(identity.session);
18712
+ } catch (error) {
18713
+ if (this.hostScopeCache.get(identity.session) !== state || !state.scopeBindPending || this.shuttingDown || !this.unscopedHostRecord(identity.session))
18714
+ return;
18715
+ if (error instanceof SubcError && error.code && !isSocketDeathError(error) && !isRouteOpenReloadWindowError(error) && !isHostScopeSyncRefusal(error.code)) {
18716
+ this.refuseHostScopeBind(identity.session, state, error.code);
18717
+ } else {
18718
+ this.deferHostScopeBind(identity.session, state, error instanceof SubcError ? error.code ?? error.name : "Error");
18719
+ }
18720
+ }
18721
+ }
18722
+ promoteHostScope(session) {
18723
+ for (const record of this.sessions.values()) {
18724
+ const entry = record.routeEntry;
18725
+ if (record.identity.session !== session)
18726
+ continue;
18727
+ if (entry?.handle && !entry.hostScope)
18728
+ this.retireHostToolRoute(record);
18729
+ const sub = record.bgSub;
18730
+ if (!sub?.attachedRoute)
18731
+ continue;
18732
+ sub.stop().then(() => {
18733
+ if (record.bgSub !== sub)
18734
+ return;
18735
+ record.bgSub = null;
18736
+ this.ensureBgSubscription(record.identity, record);
18737
+ });
18738
+ }
18739
+ }
18740
+ logHostScopeOutcome(session, outcome) {
18741
+ let key;
18742
+ let detail;
18743
+ switch (outcome.result) {
18744
+ case "scope":
18745
+ key = JSON.stringify([outcome.result, outcome.scope.ref, outcome.scope.scopeEpoch]);
18746
+ detail = `scope ref=${outcome.scope.ref.slice(0, 8)} epoch=${outcome.scope.scopeEpoch}`;
18747
+ break;
18748
+ case "error":
18749
+ key = JSON.stringify([outcome.result, outcome.code]);
18750
+ detail = `error code=${outcome.code} after=${outcome.afterMs}`;
18751
+ break;
18752
+ case "timeout":
18753
+ key = outcome.result;
18754
+ detail = `timeout after=${outcome.afterMs}`;
18755
+ break;
18756
+ default:
18757
+ key = outcome.result;
18758
+ detail = outcome.result;
18759
+ }
18760
+ if (this.hostScopeOutcomes.get(session) === key)
18761
+ return;
18762
+ this.hostScopeOutcomes.set(session, key);
18763
+ log(`subc head scope: session=${session} result=${detail}`);
18764
+ }
18765
+ async readHostScope(client, identity, signal) {
18766
+ const startedAt = Date.now();
18767
+ const timeout = () => ({ result: "timeout", afterMs: Date.now() - startedAt });
18768
+ let expired = false;
18769
+ let timer;
18770
+ const abort = new AbortController;
18771
+ let cancel;
18772
+ const cancelled = new Promise((resolve) => {
18773
+ cancel = () => {
18774
+ expired = true;
18775
+ abort.abort();
18776
+ resolve({ result: "error", code: "AbortError", afterMs: Date.now() - startedAt });
18777
+ };
18778
+ signal.addEventListener("abort", cancel, { once: true });
18779
+ });
18780
+ const read = async () => {
18781
+ let route;
18782
+ try {
18783
+ route = await client.routeOpen({ kind: "management_surface", module_id: "prefrontal-core" }, identity, { consumerIdentity: this.consumerIdentity });
18784
+ if (expired)
18785
+ return timeout();
18786
+ const envelope = await client.request(route, {
18787
+ method: "session.scope_identity",
18788
+ params: { harness: identity.harness, session: identity.session }
18789
+ }, { timeoutMs: HOST_SCOPE_LOOKUP_TIMEOUT_MS, signal: abort.signal });
18790
+ const reply = isRecord(envelope) ? envelope.result : undefined;
18791
+ if (isRecord(reply) && reply.answer === "no_scope")
18792
+ return { result: "no_scope" };
18793
+ 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)
18794
+ return { result: "malformed" };
18795
+ return {
18796
+ result: "scope",
18797
+ scope: {
18798
+ owner: { kind: "reserved", module_id: "prefrontal-core" },
18799
+ ref: reply.ref,
18800
+ scopeEpoch: reply.scope_epoch
18801
+ }
18802
+ };
18803
+ } catch (error) {
18804
+ if (expired)
18805
+ return timeout();
18806
+ return {
18807
+ result: "error",
18808
+ code: error instanceof SubcError && error.code ? error.code : error instanceof Error ? error.name : "Error",
18809
+ afterMs: Date.now() - startedAt
18810
+ };
18811
+ } finally {
18812
+ if (route)
18813
+ safeCloseRoute(client, route);
18814
+ }
18815
+ };
18816
+ try {
18817
+ return await Promise.race([
18818
+ read(),
18819
+ cancelled,
18820
+ new Promise((resolve) => {
18821
+ timer = setTimeout(() => {
18822
+ expired = true;
18823
+ abort.abort();
18824
+ resolve(timeout());
18825
+ }, HOST_SCOPE_LOOKUP_TIMEOUT_MS);
18826
+ })
18827
+ ]);
18828
+ } finally {
18829
+ clearTimeout(timer);
18830
+ signal.removeEventListener("abort", cancel);
18831
+ }
18832
+ }
18833
+ openHostToolRoute(client, identity, record, entry) {
18834
+ if (!this.hostScopes || !["opencode", "pi"].includes(identity.harness) || identity.session === DEFAULT_SESSION_ID) {
18835
+ return client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, {
18836
+ consumerIdentity: this.consumerIdentity
18837
+ });
18838
+ }
18839
+ return this.openScopedHostToolRoute(client, identity, record, entry);
18840
+ }
18841
+ async openScopedHostToolRoute(client, identity, record, entry) {
18842
+ const scope = await this.hostScope(client, identity);
18843
+ this.assertRecordLive(record);
18844
+ if (scope) {
18845
+ try {
18846
+ const route = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, { consumerIdentity: this.consumerIdentity, scope });
18847
+ if (entry)
18848
+ entry.hostScope = scope;
18849
+ return route;
18850
+ } catch (error) {
18851
+ if (!(error instanceof SubcError) || !error.code || isSocketDeathError(error))
18852
+ throw error;
18853
+ if (isRouteOpenReloadWindowError(error))
18854
+ throw error;
18855
+ this.assertRecordLive(record);
18856
+ const state = this.hostScopeCache.get(identity.session);
18857
+ if (state && isHostScopeSyncRefusal(error.code)) {
18858
+ this.deferHostScopeBind(identity.session, state, error.code);
18859
+ } else {
18860
+ this.refuseHostScopeBind(identity.session, state, error.code);
18861
+ }
18862
+ }
18863
+ }
18864
+ this.assertRecordLive(record);
18865
+ const route = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, {
18866
+ consumerIdentity: this.consumerIdentity
18867
+ });
18868
+ if (this.hostScopeCache.get(identity.session)?.answer?.result === "scope" && !this.hostScopeCache.get(identity.session)?.scopeBindPending && !this.hostScopeRefusals.has(identity.session)) {
18869
+ safeCloseRoute(client, route);
18870
+ return this.openScopedHostToolRoute(client, identity, record, entry);
18871
+ }
18872
+ return route;
18873
+ }
18874
+ invalidateHostScope(session) {
18875
+ const state = this.hostScopeCache.get(session);
18876
+ if (state) {
18877
+ if (state.scopeBindPending)
18878
+ log(`subc head scope: session=${session} retry stopped reason=scope_route_closed`);
18879
+ this.cancelHostScopeRetry(state);
18880
+ }
18881
+ this.hostScopeCache.delete(session);
18882
+ this.hostScopeRefusals.delete(session);
18883
+ for (const record of this.sessions.values()) {
18884
+ if (record.identity.session !== session || !record.routeEntry)
18885
+ continue;
18886
+ this.retireHostToolRoute(record);
18887
+ }
18888
+ }
18889
+ onScopeRouteClose(push) {
18890
+ if (push.op !== "route.closed")
18891
+ return;
18892
+ const reason = routeCloseReason(push);
18893
+ if (!reason?.startsWith("scope_")) {
18894
+ for (const record of this.sessions.values()) {
18895
+ const route = record.routeEntry?.handle;
18896
+ if (!route)
18897
+ continue;
18898
+ if (Array.isArray(push.body.channels) && push.body.channels.includes(route.channel) || !Array.isArray(push.body.channels) && push.body.module_id === AFT_MODULE_ID)
18899
+ this.retireHostToolRoute(record);
18900
+ }
18901
+ return;
18902
+ }
18903
+ const sessions = new Set;
18904
+ for (const record of this.sessions.values()) {
18905
+ const routes = [record.routeEntry?.handle, record.bgSub?.attachedRoute].filter((route) => route != null);
18906
+ 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)
18907
+ sessions.add(record.identity.session);
18908
+ }
18909
+ for (const session of sessions)
18910
+ this.invalidateHostScope(session);
18911
+ }
18299
18912
  ensureBgSubscription(identity, record) {
18300
18913
  if (this.shuttingDown || !this.onBgEventsNudge && !this.onBgEventsNudgeRef)
18301
18914
  return;
@@ -18335,17 +18948,28 @@ class SubcTransportPool {
18335
18948
  return;
18336
18949
  this.logNudgeDelivery("drop-no-compatible-handler", currentRecord, `nudge dropped cause=generation-provenance-unavailable-and-root-session-handler-unwired root=${currentRecord.canonicalRoot}`);
18337
18950
  };
18951
+ const binding = {};
18338
18952
  let sub = null;
18339
18953
  const clearDormantSubscription = () => {
18340
18954
  if (sub && record.bgSub === sub)
18341
18955
  record.bgSub = null;
18342
18956
  };
18343
- sub = new BgSubscription(identity, () => this.ensureClient(), (client, cause) => this.dropClient(client, cause), this.consumerIdentity, onNudge, this.bgBackoffSleep, () => this.rootCanAttach(record.canonicalRoot), (error) => {
18957
+ sub = new BgSubscription(identity, () => this.ensureClient(), (client, cause) => this.dropClient(client, cause), (client) => {
18958
+ binding.hostScope = undefined;
18959
+ return this.openHostToolRoute(client, identity, record, binding);
18960
+ }, onNudge, this.bgBackoffSleep, () => this.rootCanAttach(record.canonicalRoot), (error) => {
18344
18961
  if (!isAbsentRootRouteError(error))
18345
18962
  return false;
18346
18963
  this.markRootDormant(record.canonicalRoot, error);
18347
18964
  return true;
18348
- }, 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));
18965
+ }, 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), () => {
18966
+ const state = this.hostScopeCache.get(identity.session);
18967
+ if (state)
18968
+ this.scheduleHostScopeRetry(identity.session, state);
18969
+ if (state?.answer?.result === "scope" && !state.scopeBindPending && !binding.hostScope && !this.hostScopeRefusals.has(identity.session)) {
18970
+ this.promoteHostScope(identity.session);
18971
+ }
18972
+ });
18349
18973
  record.bgSub = sub;
18350
18974
  if (!this.rootCanAttach(record.canonicalRoot)) {
18351
18975
  record.bgSub = null;
@@ -18381,6 +19005,8 @@ class SubcTransportPool {
18381
19005
  record.routeEntry = null;
18382
19006
  this.deleteSessionIfEmpty(record.identityKey, record);
18383
19007
  }
19008
+ for (const session of this.hostScopeCache.keys())
19009
+ this.stopUnusedHostScopeRetry(session);
18384
19010
  this.transportFailures = 0;
18385
19011
  try {
18386
19012
  client.close();
@@ -18518,6 +19144,13 @@ class SubcTransportPool {
18518
19144
  this.generationRejections.clear();
18519
19145
  this.observedStarts.clear();
18520
19146
  this.dormantRoots.clear();
19147
+ const scopeLookups = [...this.hostScopeCache.values()].flatMap((state) => {
19148
+ this.cancelHostScopeRetry(state);
19149
+ return state.pending ? [state.pending] : [];
19150
+ });
19151
+ this.hostScopeCache.clear();
19152
+ this.hostScopeRefusals.clear();
19153
+ this.hostScopeOutcomes.clear();
18521
19154
  const registration = this.lifecycleRegistration;
18522
19155
  registration?.deregister();
18523
19156
  this.lifecycleRegistration = null;
@@ -18528,11 +19161,16 @@ class SubcTransportPool {
18528
19161
  detached.push(owner);
18529
19162
  }
18530
19163
  this.rootIndex.clear();
19164
+ this.sessionIndex.clear();
18531
19165
  this.transports.clear();
18532
19166
  const client = this.client;
18533
19167
  this.client = null;
18534
19168
  const pending = [...this.pendingRootCleanups];
18535
- await Promise.allSettled([...detached.map((owner) => this.cleanupDetached(owner)), ...pending]);
19169
+ await Promise.allSettled([
19170
+ ...detached.map((owner) => this.cleanupDetached(owner)),
19171
+ ...pending,
19172
+ ...scopeLookups
19173
+ ]);
18536
19174
  if (client) {
18537
19175
  try {
18538
19176
  client.close();
@@ -18546,6 +19184,13 @@ class SubcTransportPool {
18546
19184
  session: session && session.length > 0 ? session : DEFAULT_SESSION_ID
18547
19185
  };
18548
19186
  const owner = this.detachSession(identityKey(identity), "session_closed");
19187
+ if (!this.sessionIndex.has(identity.session)) {
19188
+ const state = this.hostScopeCache.get(identity.session);
19189
+ if (state)
19190
+ this.cancelHostScopeRetry(state);
19191
+ this.hostScopeCache.delete(identity.session);
19192
+ this.hostScopeRefusals.delete(identity.session);
19193
+ }
18549
19194
  if (owner)
18550
19195
  await this.cleanupDetached(owner);
18551
19196
  }
@@ -18586,6 +19231,8 @@ var BASH_TRANSPORT_DISPOSITION = "The transport to the AFT daemon was interrupte
18586
19231
  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.";
18587
19232
  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.";
18588
19233
  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.";
19234
+ 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.";
19235
+ 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.";
18589
19236
  function isRouteClosedMidCallError(error) {
18590
19237
  return error instanceof SubcRouteClosedMidCallError || isLocalRouteCloseRejection(error);
18591
19238
  }
@@ -18647,6 +19294,8 @@ function classifyBashHostFallbackError(error) {
18647
19294
  return;
18648
19295
  if (isRouteClosedMidCallError(error))
18649
19296
  return;
19297
+ if (isSubcFrameReadTimeoutError(error))
19298
+ return;
18650
19299
  if (error instanceof SubcTransportShuttingDownError)
18651
19300
  return "transport down";
18652
19301
  if (isSubcRouteTornDownError(error))
@@ -18670,6 +19319,14 @@ function isTransportClassError(error) {
18670
19319
  function adaptToolError(command, error) {
18671
19320
  if (!(error instanceof Error))
18672
19321
  return error;
19322
+ if (isSubcFrameReadTimeoutError(error)) {
19323
+ const typedError = error instanceof SubcFrameReadTimeoutError ? error : new SubcFrameReadTimeoutError(error);
19324
+ const disposition = command === "bash" ? BASH_FRAME_READ_TIMEOUT_DISPOSITION : SUBC_FRAME_READ_TIMEOUT_DISPOSITION;
19325
+ if (typedError.message.includes(disposition))
19326
+ return typedError;
19327
+ typedError.message = typedError.message ? `${typedError.message} ${disposition}` : disposition;
19328
+ return typedError;
19329
+ }
18673
19330
  if (error instanceof BridgeTransportUnknownOutcomeError) {
18674
19331
  if (error.message.includes(BRIDGE_TRANSPORT_UNKNOWN_OUTCOME_DISPOSITION))
18675
19332
  return error;
@@ -18751,7 +19408,12 @@ var RETIRED_PATHS = [
18751
19408
  ["semantic_search", "indexes.semantic"],
18752
19409
  ["experimental_semantic_search", "indexes.semantic"],
18753
19410
  ["callgraph_store", "indexes.callgraph"],
18754
- ["github.enabled", "github.read,github.write,github.shim"]
19411
+ ["github.enabled", "github.read,github.write,github.shim"],
19412
+ ["experimental_lsp_ty", "experimental.lsp_ty"],
19413
+ ["experimental_bash_rewrite", "bash.rewrite"],
19414
+ ["experimental_bash_compress", "bash.compress"],
19415
+ ["experimental_bash_background", "bash.background"],
19416
+ ["experimental.bash", "bash"]
18755
19417
  ];
18756
19418
  var INDEX_INPUTS = [
18757
19419
  ["trigram", "search_index", "experimental_search_index"],
@@ -18883,10 +19545,68 @@ function translateInspectLspPaths(map, prefix, out) {
18883
19545
  recordRetired(out, prefix, `inspect.${leaf}`);
18884
19546
  }
18885
19547
  }
19548
+ function translateExperimentalPaths(map, prefix, out) {
19549
+ for (const [oldKey, leaf] of [
19550
+ ["experimental_lsp_ty", "lsp_ty"],
19551
+ ["experimental_bash_rewrite", "rewrite"],
19552
+ ["experimental_bash_compress", "compress"],
19553
+ ["experimental_bash_background", "background"]
19554
+ ]) {
19555
+ if (!Object.hasOwn(map, oldKey))
19556
+ continue;
19557
+ recordRetired(out, prefix, oldKey);
19558
+ const value = map[oldKey];
19559
+ delete map[oldKey];
19560
+ if (!isRecord2(map.experimental))
19561
+ map.experimental = {};
19562
+ const experimental = map.experimental;
19563
+ let destination = experimental;
19564
+ if (leaf !== "lsp_ty") {
19565
+ if (!isRecord2(experimental.bash))
19566
+ experimental.bash = {};
19567
+ destination = experimental.bash;
19568
+ }
19569
+ if (Object.hasOwn(destination, leaf)) {
19570
+ superseded(out, oldKey, `${oldKey} is ignored because experimental.${leaf === "lsp_ty" ? leaf : `bash.${leaf}`} is already set`);
19571
+ } else {
19572
+ destination[leaf] = value;
19573
+ }
19574
+ }
19575
+ if (!isRecord2(map.experimental) || !Object.hasOwn(map.experimental, "bash"))
19576
+ return;
19577
+ const experimental = map.experimental;
19578
+ const legacy = experimental.bash;
19579
+ if (isRecord2(legacy)) {
19580
+ if (!["rewrite", "compress", "background"].some((leaf) => Object.hasOwn(legacy, leaf)))
19581
+ return;
19582
+ for (const leaf of Object.keys(legacy))
19583
+ recordRetired(out, prefix, `experimental.bash.${leaf}`);
19584
+ if (Object.hasOwn(map, "bash")) {
19585
+ superseded(out, "experimental.bash", 'experimental.bash is ignored because top-level "bash" is already set');
19586
+ } else {
19587
+ const bash = {
19588
+ rewrite: legacy.rewrite === true,
19589
+ compress: legacy.compress === true,
19590
+ background: legacy.background === true
19591
+ };
19592
+ for (const leaf of ["long_running_reminder_enabled", "long_running_reminder_interval_ms"]) {
19593
+ if (Object.hasOwn(legacy, leaf))
19594
+ bash[leaf] = legacy[leaf];
19595
+ }
19596
+ map.bash = bash;
19597
+ }
19598
+ } else {
19599
+ recordRetired(out, prefix, "experimental.bash");
19600
+ }
19601
+ delete experimental.bash;
19602
+ if (Object.keys(experimental).length === 0)
19603
+ delete map.experimental;
19604
+ }
18886
19605
  function translateBlock(map, userBase, blockLabel, out) {
18887
19606
  const prefix = blockLabel === "base" ? "" : `${blockLabel}.`;
18888
19607
  translateGithubAliases(map, prefix, out);
18889
19608
  translateInspectLspPaths(map, prefix, out);
19609
+ translateExperimentalPaths(map, prefix, out);
18890
19610
  let explicitList;
18891
19611
  const rawList = map.disabled_tools;
18892
19612
  if (Array.isArray(rawList) && rawList.every((entry) => typeof entry === "string")) {
@@ -18899,6 +19619,8 @@ function translateBlock(map, userBase, blockLabel, out) {
18899
19619
  });
18900
19620
  }
18901
19621
  for (const [path] of RETIRED_PATHS) {
19622
+ if (path === "experimental.bash")
19623
+ continue;
18902
19624
  if (hasPath(map, path))
18903
19625
  recordRetired(out, prefix, path);
18904
19626
  }
@@ -19332,11 +20054,13 @@ class BridgePool {
19332
20054
  return alive;
19333
20055
  }
19334
20056
  getBridge(projectRoot) {
20057
+ return this.getBridgeForCanonicalRoot(asCanonicalRootPath(normalizeKey(projectRoot)));
20058
+ }
20059
+ getBridgeForCanonicalRoot(key) {
19335
20060
  if (this.shutdownCalled) {
19336
20061
  this.shutdownCalled = false;
19337
20062
  this.startCleanupTimer();
19338
20063
  }
19339
- const key = normalizeKey(projectRoot);
19340
20064
  const existing = this.bridges.get(key);
19341
20065
  if (existing) {
19342
20066
  existing.lastUsed = Date.now();
@@ -19542,7 +20266,9 @@ class RevivableTransportPool {
19542
20266
  this.activePool = initialPool;
19543
20267
  }
19544
20268
  getBridge(projectRoot) {
19545
- const key = canonicalizeProjectRoot(projectRoot);
20269
+ return this.getBridgeForCanonicalRoot(asCanonicalRootPath(canonicalizeProjectRoot(projectRoot)));
20270
+ }
20271
+ getBridgeForCanonicalRoot(key) {
19546
20272
  let transport = this.transports.get(key);
19547
20273
  if (!transport) {
19548
20274
  transport = new RevivableProjectTransport(this, key);
@@ -19577,7 +20303,7 @@ class RevivableTransportPool {
19577
20303
  return transport;
19578
20304
  }
19579
20305
  async toolCall(projectRoot, runtime, name, rawArgs = {}, options) {
19580
- return this.getBridge(projectRoot).toolCall(runtime.sessionID, name, rawArgs, options);
20306
+ return this.toolCallForCanonicalRoot(asCanonicalRootPath(canonicalizeProjectRoot(projectRoot)), runtime.sessionID, name, rawArgs, options);
19581
20307
  }
19582
20308
  setConfigureOverride(key, value) {
19583
20309
  if (key === "edit_slot_survives") {
@@ -19717,14 +20443,22 @@ class RevivableTransportPool {
19717
20443
  return this.activePool?.getActiveBridgeForRoot(projectRoot) ?? null;
19718
20444
  }
19719
20445
  async send(projectRoot, command, params, options) {
20446
+ return this.sendForCanonicalRoot(asCanonicalRootPath(canonicalizeProjectRoot(projectRoot)), command, params, options);
20447
+ }
20448
+ async sendForCanonicalRoot(projectRoot, command, params, options) {
20449
+ this.getBridgeForCanonicalRoot(projectRoot);
19720
20450
  const pool = await this.ensureActivePool();
19721
- const bridge = pool.getBridge(projectRoot);
20451
+ const bridge = pool.getBridgeForCanonicalRoot?.(projectRoot) ?? pool.getBridge(projectRoot);
19722
20452
  this.transports.get(projectRoot)?.refreshStatusSubscription(bridge);
19723
20453
  return bridge.send(command, params, options);
19724
20454
  }
19725
20455
  async toolCallOnProject(projectRoot, sessionId, name, rawArgs, options) {
20456
+ return this.toolCallForCanonicalRoot(asCanonicalRootPath(canonicalizeProjectRoot(projectRoot)), sessionId, name, rawArgs, options);
20457
+ }
20458
+ async toolCallForCanonicalRoot(projectRoot, sessionId, name, rawArgs, options) {
20459
+ this.getBridgeForCanonicalRoot(projectRoot);
19726
20460
  const pool = await this.ensureActivePool();
19727
- const bridge = pool.getBridge(projectRoot);
20461
+ const bridge = pool.getBridgeForCanonicalRoot?.(projectRoot) ?? pool.getBridge(projectRoot);
19728
20462
  this.transports.get(projectRoot)?.refreshStatusSubscription(bridge);
19729
20463
  return bridge.toolCall(sessionId, name, rawArgs, options);
19730
20464
  }
@@ -19802,7 +20536,7 @@ function satisfyLifecycleDemand(wrapper, projectRoot) {
19802
20536
  const activePool = wrapper.activePool;
19803
20537
  if (activePool.isShutdown() || !(activePool instanceof SubcTransportPool))
19804
20538
  return;
19805
- activePool.getBridge(projectRoot);
20539
+ activePool.getBridgeForCanonicalRoot(asCanonicalRootPath(projectRoot));
19806
20540
  }
19807
20541
  async function createAftTransportPool(opts) {
19808
20542
  let binaryPath = opts.binaryPath;
@@ -19837,8 +20571,8 @@ async function createAftTransportPool(opts) {
19837
20571
  });
19838
20572
  if (reapingEnabled) {
19839
20573
  const owner = wrapper;
19840
- const getBridge = owner.getBridge.bind(owner);
19841
- owner.getBridge = (projectRoot) => {
20574
+ const getBridge = owner.getBridgeForCanonicalRoot.bind(owner);
20575
+ owner.getBridgeForCanonicalRoot = (projectRoot) => {
19842
20576
  satisfyLifecycleDemand(owner, projectRoot);
19843
20577
  return getBridge(projectRoot);
19844
20578
  };
@@ -19864,6 +20598,7 @@ async function createConcreteAftTransportPool(opts) {
19864
20598
  throw new Error(missing);
19865
20599
  }
19866
20600
  return new SubcTransportPool({
20601
+ hostScopes: opts.harness === "opencode" || opts.harness === "pi",
19867
20602
  connectionFile,
19868
20603
  harness: opts.harness,
19869
20604
  consumerIdentity: opts.subcConsumerIdentity,
@@ -20125,7 +20860,7 @@ import { homedir as homedir9, tmpdir as tmpdir3 } from "node:os";
20125
20860
  import { basename as basename2, dirname as dirname6, join as join12, resolve as resolve8 } from "node:path";
20126
20861
 
20127
20862
  // ../aft-bridge/dist/paths.js
20128
- import { existsSync as existsSync5, readFileSync as readFileSync5, renameSync as renameSync3, writeFileSync as writeFileSync2 } from "node:fs";
20863
+ import { existsSync as existsSync5, readFileSync as readFileSync5, realpathSync as realpathSync3, renameSync as renameSync3, writeFileSync as writeFileSync2 } from "node:fs";
20129
20864
  import { homedir as homedir7 } from "node:os";
20130
20865
  import { dirname as dirname4, isAbsolute as isAbsolute5, join as join10, resolve as resolve7 } from "node:path";
20131
20866
  function homeDir3() {
@@ -20160,6 +20895,23 @@ function resolveCortexKitUserConfigPath() {
20160
20895
  function resolveCortexKitProjectConfigPath(projectDirectory) {
20161
20896
  return join10(projectDirectory, ".cortexkit", "aft.jsonc");
20162
20897
  }
20898
+ function resolveProjectConfigReadPath(projectDirectory, operatingHarness) {
20899
+ const canonical = resolveCortexKitProjectConfigPath(projectDirectory);
20900
+ if (existsSync5(canonical))
20901
+ return canonical;
20902
+ const sources = resolveLegacyAftConfigSources(projectDirectory).project;
20903
+ const ordered = operatingHarness ? [
20904
+ ...sources.filter((source) => source.harness === operatingHarness),
20905
+ ...sources.filter((source) => source.harness !== operatingHarness)
20906
+ ] : sources;
20907
+ return ordered.find((source) => existsSync5(source.path))?.path ?? canonical;
20908
+ }
20909
+ function legacyProjectConfigLocationNotice(projectDirectory, path) {
20910
+ const canonical = resolveCortexKitProjectConfigPath(projectDirectory);
20911
+ if (path === canonical)
20912
+ return null;
20913
+ return `Legacy project config ${path} is read in place; move it to ${canonical} yourself, and use aft doctor --fix to update retired keys.`;
20914
+ }
20163
20915
  function resolveCortexKitConfigPaths(projectDirectory) {
20164
20916
  return {
20165
20917
  userConfigPath: resolveCortexKitUserConfigPath(),
@@ -20167,15 +20919,27 @@ function resolveCortexKitConfigPaths(projectDirectory) {
20167
20919
  };
20168
20920
  }
20169
20921
  function resolveLegacyAftConfigSources(projectDirectory) {
20922
+ const project = [
20923
+ ...legacySources(join10(projectDirectory, ".opencode", "aft"), "OpenCode project", "opencode"),
20924
+ ...legacySources(join10(projectDirectory, ".pi", "aft"), "Pi project", "pi")
20925
+ ];
20926
+ const identity = (path) => {
20927
+ try {
20928
+ return realpathSync3(path);
20929
+ } catch {
20930
+ return resolve7(path);
20931
+ }
20932
+ };
20933
+ const protectedPaths = new Set([
20934
+ resolveCortexKitProjectConfigPath(projectDirectory),
20935
+ ...project.map((source) => source.path)
20936
+ ].map(identity));
20170
20937
  return {
20171
20938
  user: [
20172
20939
  ...legacySources(join10(legacyOpenCodeConfigDir(), "aft"), "OpenCode user", "opencode"),
20173
20940
  ...legacySources(join10(legacyPiAgentDir(), "aft"), "Pi user", "pi")
20174
- ],
20175
- project: [
20176
- ...legacySources(join10(projectDirectory, ".opencode", "aft"), "OpenCode project", "opencode"),
20177
- ...legacySources(join10(projectDirectory, ".pi", "aft"), "Pi project", "pi")
20178
- ]
20941
+ ].filter((source) => !protectedPaths.has(identity(source.path))),
20942
+ project
20179
20943
  };
20180
20944
  }
20181
20945
  function resolveHarnessStoragePath(storageRoot, harness, ...segments) {
@@ -21363,7 +22127,7 @@ function isNpmAvailable(deps = defaultDeps()) {
21363
22127
  return resolveNpm(deps) !== null;
21364
22128
  }
21365
22129
  // ../aft-bridge/dist/onnx-probe.js
21366
- import { closeSync as closeSync7, existsSync as existsSync8, openSync as openSync6, readSync as readSync5, realpathSync as realpathSync3 } from "node:fs";
22130
+ import { closeSync as closeSync7, existsSync as existsSync8, openSync as openSync6, readSync as readSync5, realpathSync as realpathSync4 } from "node:fs";
21367
22131
  var MH_MAGIC = 4277009102;
21368
22132
  var MH_MAGIC_64 = 4277009103;
21369
22133
  var FAT_MAGIC = 3405691582;
@@ -21450,7 +22214,7 @@ function isUncheckableDependency(path) {
21450
22214
  function probeOnnxRuntimeLoadable(libPath, arch = process.arch) {
21451
22215
  let fd;
21452
22216
  try {
21453
- fd = openSync6(realpathSync3(libPath), "r");
22217
+ fd = openSync6(realpathSync4(libPath), "r");
21454
22218
  } catch (err) {
21455
22219
  return {
21456
22220
  loadable: false,
@@ -21489,7 +22253,7 @@ function probeOnnxRuntimeLoadable(libPath, arch = process.arch) {
21489
22253
  }
21490
22254
  // ../aft-bridge/dist/onnx-runtime.js
21491
22255
  import { randomBytes as randomBytes2 } from "node:crypto";
21492
- 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";
22256
+ 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";
21493
22257
  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";
21494
22258
  import { Readable as Readable2 } from "node:stream";
21495
22259
  import { pipeline as pipeline2 } from "node:stream/promises";
@@ -21773,7 +22537,7 @@ function detectOnnxVersion(libDir, libName) {
21773
22537
  try {
21774
22538
  const entries = readdirSync3(libDir);
21775
22539
  try {
21776
- const resolved = parseOnnxVersionFromPath(realpathSync4(join16(libDir, libName)));
22540
+ const resolved = parseOnnxVersionFromPath(realpathSync5(join16(libDir, libName)));
21777
22541
  if (resolved && resolved !== INVALID_ORT_VERSION)
21778
22542
  return resolved;
21779
22543
  } catch {}
@@ -21790,7 +22554,7 @@ function detectOnnxVersion(libDir, libName) {
21790
22554
  const base = join16(libDir, libName);
21791
22555
  if (existsSync10(base)) {
21792
22556
  try {
21793
- const real = realpathSync4(base);
22557
+ const real = realpathSync5(base);
21794
22558
  const version = parseOnnxVersionFromPath(real) ?? parseOnnxVersionFromDirectoryPath(real);
21795
22559
  if (version)
21796
22560
  return version;
@@ -21982,7 +22746,7 @@ async function downloadFileWithCap(url, destPath) {
21982
22746
  }
21983
22747
  }
21984
22748
  function validateExtractedTree(stagingRoot) {
21985
- const realRoot = realpathSync4(stagingRoot);
22749
+ const realRoot = realpathSync5(stagingRoot);
21986
22750
  let totalBytes = 0;
21987
22751
  const walk = (dir) => {
21988
22752
  const entries = readdirSync3(dir);
@@ -22131,7 +22895,7 @@ function copyOnnxLibraries(info, extractedDir, targetDir, realFiles, symlinks, c
22131
22895
  log(`ORT extract: failed to copy optional ${libFile}: ${copyErr}`);
22132
22896
  }
22133
22897
  }
22134
- const targetRoot = realpathSync4(targetDir);
22898
+ const targetRoot = realpathSync5(targetDir);
22135
22899
  for (const link of symlinks) {
22136
22900
  const dst = join16(targetDir, link.name);
22137
22901
  try {
@@ -22951,7 +23715,7 @@ function formatSemanticIndexStatus(status, stage, error, backend) {
22951
23715
  return status;
22952
23716
  }
22953
23717
  // src/config.ts
22954
- import { existsSync as existsSync11, readFileSync as readFileSync10, renameSync as renameSync7, statSync as statSync10, unlinkSync as unlinkSync5, writeFileSync as writeFileSync6 } from "node:fs";
23718
+ import { existsSync as existsSync11, readFileSync as readFileSync10, statSync as statSync10 } from "node:fs";
22955
23719
  import { homedir as homedir12 } from "node:os";
22956
23720
  import { isAbsolute as isAbsolute9, parse as parsePath, resolve as resolvePath } from "node:path";
22957
23721
  var import_comment_json = __toESM(require_src2(), 1);
@@ -28173,155 +28937,6 @@ function resolveBashConfig(config) {
28173
28937
  }
28174
28938
  return { ...base, enabled: true, rewrite: true, compress: true, background: true };
28175
28939
  }
28176
- var CONFIG_MIGRATIONS = [
28177
- { oldKey: "experimental_lsp_ty", newPath: ["experimental", "lsp_ty"] },
28178
- { oldKey: "experimental_bash_rewrite", newPath: ["experimental", "bash", "rewrite"] },
28179
- { oldKey: "experimental_bash_compress", newPath: ["experimental", "bash", "compress"] },
28180
- { oldKey: "experimental_bash_background", newPath: ["experimental", "bash", "background"] }
28181
- ];
28182
- function isWritableMigrationError(errorValue) {
28183
- const code = errorValue?.code;
28184
- return code === "EROFS" || code === "EACCES" || code === "EPERM";
28185
- }
28186
- function extractCommentsForPreservation(content) {
28187
- const comments = [];
28188
- const linePattern = /\/\/[^\n]*/g;
28189
- for (const match of content.match(linePattern) ?? []) {
28190
- comments.push(match.trim());
28191
- }
28192
- const blockPattern = /\/\*[\s\S]*?\*\//g;
28193
- for (const match of content.match(blockPattern) ?? []) {
28194
- comments.push(match.replace(/\s+/g, " ").trim());
28195
- }
28196
- return comments;
28197
- }
28198
- function ensureRecordAtPath(root, path) {
28199
- let current = root;
28200
- for (const segment of path) {
28201
- const existing = current[segment];
28202
- if (!existing || typeof existing !== "object" || Array.isArray(existing)) {
28203
- current[segment] = {};
28204
- }
28205
- current = current[segment];
28206
- }
28207
- return current;
28208
- }
28209
- function hasPath2(root, path) {
28210
- let current = root;
28211
- for (const segment of path) {
28212
- if (!current || typeof current !== "object" || Array.isArray(current))
28213
- return false;
28214
- const record2 = current;
28215
- if (!Object.hasOwn(record2, segment))
28216
- return false;
28217
- current = record2[segment];
28218
- }
28219
- return true;
28220
- }
28221
- function setPath(root, path, value) {
28222
- const parent = ensureRecordAtPath(root, path.slice(0, -1));
28223
- parent[path[path.length - 1]] = value;
28224
- }
28225
- function migrateRawConfig(rawConfig, configPath, logger) {
28226
- const oldKeys = [];
28227
- for (const migration of CONFIG_MIGRATIONS) {
28228
- if (!Object.hasOwn(rawConfig, migration.oldKey))
28229
- continue;
28230
- if (hasPath2(rawConfig, migration.newPath)) {
28231
- logger?.warn(`Config migration conflict at ${configPath}: ${migration.oldKey} ignored because ${migration.newPath.join(".")} is already set`);
28232
- } else {
28233
- setPath(rawConfig, migration.newPath, rawConfig[migration.oldKey]);
28234
- }
28235
- delete rawConfig[migration.oldKey];
28236
- oldKeys.push(migration.oldKey);
28237
- }
28238
- oldKeys.push(...migrateExperimentalBashBlock(rawConfig, configPath, logger));
28239
- return oldKeys;
28240
- }
28241
- function migrateExperimentalBashBlock(rawConfig, configPath, logger) {
28242
- const experimental = rawConfig.experimental;
28243
- if (typeof experimental !== "object" || experimental === null || Array.isArray(experimental)) {
28244
- return [];
28245
- }
28246
- const expRecord = experimental;
28247
- if (!Object.hasOwn(expRecord, "bash"))
28248
- return [];
28249
- const legacyBash = expRecord.bash;
28250
- if (typeof legacyBash !== "object" || legacyBash === null || Array.isArray(legacyBash)) {
28251
- delete expRecord.bash;
28252
- if (Object.keys(expRecord).length === 0)
28253
- delete rawConfig.experimental;
28254
- return ["experimental.bash"];
28255
- }
28256
- const bashRecord = legacyBash;
28257
- const hasFeatureFlag = "rewrite" in bashRecord || "compress" in bashRecord || "background" in bashRecord;
28258
- if (!hasFeatureFlag)
28259
- return [];
28260
- const movedKeys = Object.keys(bashRecord).map((k) => `experimental.bash.${k}`);
28261
- if (Object.hasOwn(rawConfig, "bash")) {
28262
- logger?.warn(`Config migration conflict at ${configPath}: experimental.bash dropped because top-level "bash" is already set`);
28263
- } else {
28264
- const migrated = {
28265
- rewrite: bashRecord.rewrite === true,
28266
- compress: bashRecord.compress === true,
28267
- background: bashRecord.background === true
28268
- };
28269
- if (bashRecord.long_running_reminder_enabled !== undefined) {
28270
- migrated.long_running_reminder_enabled = bashRecord.long_running_reminder_enabled;
28271
- }
28272
- if (bashRecord.long_running_reminder_interval_ms !== undefined) {
28273
- migrated.long_running_reminder_interval_ms = bashRecord.long_running_reminder_interval_ms;
28274
- }
28275
- rawConfig.bash = migrated;
28276
- }
28277
- delete expRecord.bash;
28278
- if (Object.keys(expRecord).length === 0) {
28279
- delete rawConfig.experimental;
28280
- }
28281
- return movedKeys;
28282
- }
28283
- function migrateAftConfigFile2(configPath, logger = { log: log2, warn: warn2 }) {
28284
- if (!existsSync11(configPath)) {
28285
- return { migrated: false, oldKeys: [] };
28286
- }
28287
- let tmpPath = null;
28288
- let oldKeys = [];
28289
- try {
28290
- const content = readFileSync10(configPath, "utf-8");
28291
- const rawConfig = import_comment_json.parse(content);
28292
- if (!rawConfig || typeof rawConfig !== "object" || Array.isArray(rawConfig)) {
28293
- return { migrated: false, oldKeys: [] };
28294
- }
28295
- oldKeys = migrateRawConfig(rawConfig, configPath, logger);
28296
- if (oldKeys.length === 0) {
28297
- return { migrated: false, oldKeys: [] };
28298
- }
28299
- const serialized = `${import_comment_json.stringify(rawConfig, null, 2)}
28300
- `;
28301
- const originalComments = extractCommentsForPreservation(content);
28302
- const droppedComments = originalComments.filter((comment) => !serialized.includes(comment.trim()));
28303
- const nextContent = droppedComments.length > 0 ? `${droppedComments.join(`
28304
- `)}
28305
- ${serialized}` : serialized;
28306
- tmpPath = `${configPath}.tmp.${process.pid}`;
28307
- writeFileSync6(tmpPath, nextContent, "utf-8");
28308
- renameSync7(tmpPath, configPath);
28309
- logger.log(`Migrated config at ${configPath}: removed ${oldKeys.join(", ")}`);
28310
- return { migrated: true, oldKeys };
28311
- } catch (err) {
28312
- if (tmpPath) {
28313
- try {
28314
- unlinkSync5(tmpPath);
28315
- } catch {}
28316
- }
28317
- if (isWritableMigrationError(err)) {
28318
- const errorMsg = err instanceof Error ? err.message : String(err);
28319
- logger.warn(`Config migration could not write ${configPath} (${errorMsg}); using migrated config in memory`);
28320
- return { migrated: oldKeys.length > 0, oldKeys };
28321
- }
28322
- return { migrated: false, oldKeys: [] };
28323
- }
28324
- }
28325
28940
  function parseConfigPartially(rawConfig) {
28326
28941
  let configForParsing = rawConfig;
28327
28942
  if (Object.hasOwn(rawConfig, "edit_mode") && rawConfig.edit_mode !== "default" && rawConfig.edit_mode !== "hashline") {
@@ -28396,7 +29011,6 @@ function loadConfigFromPath(configPath, tier) {
28396
29011
  configLoadSources.push(configPath);
28397
29012
  configLoadTexts.set(configPath, content);
28398
29013
  const rawConfig = import_comment_json.parse(content);
28399
- migrateRawConfig(rawConfig, configPath, { log: log2, warn: warn2 });
28400
29014
  cleanConfig = stripJsoncSymbols(rawConfig);
28401
29015
  } catch (err) {
28402
29016
  const errorMsg = err instanceof Error ? err.message : String(err);
@@ -28756,21 +29370,14 @@ function migrateAftConfigLocations(projectDirectory, logger = { log: log2, warn:
28756
29370
  legacySources: legacy.user,
28757
29371
  operatingHarness: "opencode",
28758
29372
  logger
28759
- }),
28760
- migrateAftConfigFile({
28761
- scope: "project",
28762
- targetPath: paths.projectConfigPath,
28763
- legacySources: legacy.project,
28764
- operatingHarness: "opencode",
28765
- logger
28766
29373
  })
28767
29374
  ];
28768
29375
  }
28769
29376
  function resolveAftConfigPaths(projectDirectory) {
28770
- const paths = resolveCortexKitConfigPaths(projectDirectory);
28771
- migrateAftConfigFile2(paths.userConfigPath);
28772
- migrateAftConfigFile2(paths.projectConfigPath);
28773
- return paths;
29377
+ return {
29378
+ ...resolveCortexKitConfigPaths(projectDirectory),
29379
+ projectConfigPath: resolveProjectConfigReadPath(projectDirectory, ACTIVE_HARNESS)
29380
+ };
28774
29381
  }
28775
29382
  function resolveOpenCodeRegistrationRoot(directory, worktree) {
28776
29383
  if (!worktree)
@@ -28798,6 +29405,15 @@ function loadAftConfig(projectDirectory) {
28798
29405
  configLoadNotices = [];
28799
29406
  semanticInputSupplied = false;
28800
29407
  const { userConfigPath, projectConfigPath } = resolveAftConfigPaths(projectDirectory);
29408
+ const locationNotice = legacyProjectConfigLocationNotice(projectDirectory, projectConfigPath);
29409
+ if (locationNotice) {
29410
+ log2(locationNotice);
29411
+ configLoadNotices.push({
29412
+ configPath: projectConfigPath,
29413
+ digest: "legacy-project-config-location",
29414
+ message: locationNotice
29415
+ });
29416
+ }
28801
29417
  let config = loadConfigFromPath(userConfigPath, "user") ?? {
28802
29418
  disabled_tools: [...DEFAULT_DISABLED_TOOLS]
28803
29419
  };
@@ -40726,6 +41342,24 @@ function markExplicitControl(sessionID, taskId, trackOutstanding = true) {
40726
41342
  function unmarkExplicitControl(sessionID, taskId) {
40727
41343
  stateFor(sessionID).explicitControlTasks.delete(taskId);
40728
41344
  }
41345
+ function holdBgCompletion(sessionID, completion, stopped = false) {
41346
+ const state = stateFor(sessionID);
41347
+ if (!stopped && state.consumedTaskIds.has(completion.task_id))
41348
+ return;
41349
+ state.heldWorkerCompletions = true;
41350
+ state.explicitControlTasks.delete(completion.task_id);
41351
+ state.outstandingTaskIds.delete(completion.task_id);
41352
+ if (!state.pendingCompletions.some((pending) => pending.task_id === completion.task_id)) {
41353
+ state.pendingCompletions.push(completion);
41354
+ }
41355
+ }
41356
+ function hasHeldBgCompletions(sessionID) {
41357
+ const state = stateFor(sessionID);
41358
+ return state.heldWorkerCompletions === true && (state.pendingCompletions.length > 0 || state.pendingPatternMatches.length > 0);
41359
+ }
41360
+ function markHeldBgCompletions(sessionID) {
41361
+ stateFor(sessionID).heldWorkerCompletions = true;
41362
+ }
40729
41363
  function queuePendingPatternMatch(state, entry) {
40730
41364
  const normalized = entry.reason ? entry : { ...entry, reason: "pattern_match" };
40731
41365
  const existingIdx = state.pendingPatternMatches.findIndex((match) => match.task_id === normalized.task_id);
@@ -40811,7 +41445,7 @@ async function appendInTurnBgCompletions(drainContext, output) {
40811
41445
  return;
40812
41446
  const state = stateFor(drainContext.sessionID);
40813
41447
  if (state.outstandingTaskIds.size === 0 && state.pendingCompletions.length === 0 && state.pendingLongRunning.length === 0 && state.pendingPatternMatches.length === 0) {
40814
- if (!idleDrainMayFindWork(state))
41448
+ if (!idleDrainMayFindWork(state, drainContext))
40815
41449
  return;
40816
41450
  await drainCompletions(drainContext);
40817
41451
  if (state.outstandingTaskIds.size === 0 && state.pendingCompletions.length === 0 && state.pendingLongRunning.length === 0 && state.pendingPatternMatches.length === 0) {
@@ -40837,6 +41471,7 @@ async function appendInTurnBgCompletions(drainContext, output) {
40837
41471
  reminder_chars: reminder.length
40838
41472
  });
40839
41473
  state.pendingCompletions = [];
41474
+ state.heldWorkerCompletions = false;
40840
41475
  for (const completion of deliveredCompletions) {
40841
41476
  state.wakeDeferredTaskIds.delete(completion.task_id);
40842
41477
  }
@@ -41190,7 +41825,8 @@ function observeOpenCodeBgNotificationEvent(event) {
41190
41825
  }
41191
41826
  async function triggerWakeIfPending(drainContext, skipDrain, includeDeferredCompletions = true, forceDrain = false, deferWhileBusy = false) {
41192
41827
  const state = stateFor(drainContext.sessionID);
41193
- if (!skipDrain && (forceDrain || state.outstandingTaskIds.size > 0 || !state.forcedDrainCompleted)) {
41828
+ const hasTrackedOrPendingWork = state.outstandingTaskIds.size > 0 || state.pendingCompletions.length > 0 || state.pendingLongRunning.length > 0 || state.pendingPatternMatches.length > 0;
41829
+ if (!skipDrain && (forceDrain || state.outstandingTaskIds.size > 0 || !state.forcedDrainCompleted) && (forceDrain || hasTrackedOrPendingWork || idleDrainMayFindWork(state, drainContext))) {
41194
41830
  await drainCompletions(drainContext);
41195
41831
  }
41196
41832
  routeExplicitControlCompletions(state);
@@ -41425,8 +42061,13 @@ function bridgeForDrain(drainContext) {
41425
42061
  function isTerminalNudgeError(error) {
41426
42062
  return error instanceof Error && error.code === "root_generation_expired";
41427
42063
  }
41428
- function idleDrainMayFindWork(state) {
41429
- return !state.forcedDrainCompleted || state.untrackedCompletionSinceDrain || state.deliveredAwaitingAckTaskIds.size > 0;
42064
+ function idleDrainMayFindWork(state, drainContext) {
42065
+ if (state.untrackedCompletionSinceDrain || state.deliveredAwaitingAckTaskIds.size > 0) {
42066
+ return true;
42067
+ }
42068
+ if (state.forcedDrainCompleted)
42069
+ return false;
42070
+ return Boolean(drainContext.resolvedBridge ?? drainContext.ctx.pool.getActiveBridgeForRoot(drainContext.directory));
41430
42071
  }
41431
42072
  async function drainCompletions(drainContext) {
41432
42073
  const state = stateFor(drainContext.sessionID);
@@ -42226,7 +42867,7 @@ function resolvePathFromProjectRoot(projectRoot, target) {
42226
42867
  async function resolvePathArg(ctx, runtime, target) {
42227
42868
  return resolvePathFromProjectRoot(await resolveProjectRoot(ctx, runtime), target);
42228
42869
  }
42229
- function bridgeFor(ctx, runtime) {
42870
+ function checkedProjectRoot(ctx, runtime) {
42230
42871
  const projectRoot = projectRootFor(runtime);
42231
42872
  if (ctx.isProjectEnabled?.(projectRoot) === false) {
42232
42873
  throw new Error(`AFT disabled by config for ${projectRoot}`);
@@ -42234,7 +42875,7 @@ function bridgeFor(ctx, runtime) {
42234
42875
  if (!existsSync12(projectRoot)) {
42235
42876
  throw new Error(`project directory no longer exists: ${projectRoot} (stale restored session?)`);
42236
42877
  }
42237
- return ctx.pool.getBridge(projectRoot);
42878
+ return projectRoot;
42238
42879
  }
42239
42880
  async function isWorkerRuntime(ctx, runtime) {
42240
42881
  if (!runtime.sessionID)
@@ -42253,6 +42894,9 @@ async function callBridge(ctx, runtime, command, params = {}, options) {
42253
42894
  if (workerSession) {
42254
42895
  merged[WORKER_SESSION_FIELD] = true;
42255
42896
  }
42897
+ if (ctx.hostToolNames) {
42898
+ merged[HOST_TOOL_NAMES_FIELD] = ctx.hostToolNames;
42899
+ }
42256
42900
  const timeoutMs = timeoutForCommand(command);
42257
42901
  const sendOptions = {
42258
42902
  ...timeoutMs !== undefined ? { timeoutMs } : {},
@@ -42263,7 +42907,9 @@ async function callBridge(ctx, runtime, command, params = {}, options) {
42263
42907
  markBridgeStart();
42264
42908
  let response;
42265
42909
  try {
42266
- response = await bridgeFor(ctx, runtime).send(command, merged, Object.keys(sendOptions).length > 0 ? sendOptions : undefined);
42910
+ const root = asCanonicalRootPath(checkedProjectRoot(ctx, runtime));
42911
+ const opts = Object.keys(sendOptions).length > 0 ? sendOptions : undefined;
42912
+ response = ctx.pool.sendForCanonicalRoot ? await ctx.pool.sendForCanonicalRoot(root, command, merged, opts) : await ctx.pool.getBridge(root).send(command, merged, opts);
42267
42913
  } catch (error) {
42268
42914
  if (command === "bash" && isBashTransportDeadError(error))
42269
42915
  throw error;
@@ -42285,12 +42931,15 @@ async function callToolCall(ctx, runtime, name, rawArgs = {}, options) {
42285
42931
  configureWarningClient: ctx.client,
42286
42932
  ...runtime.effectAbort ? { abortSignal: runtime.effectAbort } : {},
42287
42933
  ...workerSession ? { workerSession: true } : {},
42934
+ ...ctx.hostToolNames ? { hostToolNames: ctx.hostToolNames } : {},
42288
42935
  ...options
42289
42936
  };
42290
42937
  markBridgeStart();
42291
42938
  let response;
42292
42939
  try {
42293
- response = await bridgeFor(ctx, runtime).toolCall(runtime.sessionID, name, rawArgs, Object.keys(sendOptions).length > 0 ? sendOptions : undefined);
42940
+ const root = asCanonicalRootPath(checkedProjectRoot(ctx, runtime));
42941
+ const opts = Object.keys(sendOptions).length > 0 ? sendOptions : undefined;
42942
+ 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);
42294
42943
  } catch (error) {
42295
42944
  throw adaptToolError(name, error);
42296
42945
  } finally {
@@ -42398,7 +43047,7 @@ async function signalBashWaitDetachForProject(pool, projectRoot, sessionID) {
42398
43047
  }
42399
43048
 
42400
43049
  // src/lsp-auto-install.ts
42401
- import { existsSync as existsSync14, mkdirSync as mkdirSync6, renameSync as renameSync9, rmSync as rmSync5, statSync as statSync12, writeFileSync as writeFileSync8 } from "node:fs";
43050
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6, renameSync as renameSync8, rmSync as rmSync5, statSync as statSync12, writeFileSync as writeFileSync7 } from "node:fs";
42402
43051
  import { join as join19 } from "node:path";
42403
43052
 
42404
43053
  // src/lsp-cache.ts
@@ -42408,11 +43057,11 @@ import {
42408
43057
  mkdirSync as mkdirSync5,
42409
43058
  openSync as openSync8,
42410
43059
  readFileSync as readFileSync11,
42411
- renameSync as renameSync8,
43060
+ renameSync as renameSync7,
42412
43061
  rmSync as rmSync4,
42413
43062
  statSync as statSync11,
42414
- unlinkSync as unlinkSync6,
42415
- writeFileSync as writeFileSync7
43063
+ unlinkSync as unlinkSync5,
43064
+ writeFileSync as writeFileSync6
42416
43065
  } from "node:fs";
42417
43066
  import { join as join17 } from "node:path";
42418
43067
  function aftCacheBase() {
@@ -42453,7 +43102,7 @@ function writeInstalledMetaIn(installDir, version, sha256) {
42453
43102
  installedAt: new Date().toISOString(),
42454
43103
  ...sha256 ? { sha256 } : {}
42455
43104
  };
42456
- writeFileSync7(join17(installDir, INSTALLED_META_FILE), JSON.stringify(meta), "utf8");
43105
+ writeFileSync6(join17(installDir, INSTALLED_META_FILE), JSON.stringify(meta), "utf8");
42457
43106
  } catch (err) {
42458
43107
  log2(`[lsp-cache] failed to write installed-meta in ${installDir}: ${err}`);
42459
43108
  }
@@ -42494,7 +43143,7 @@ function acquireInstallLock(lockKey) {
42494
43143
  try {
42495
43144
  const fd = openSync8(lock, "wx");
42496
43145
  try {
42497
- writeFileSync7(fd, `${process.pid}
43146
+ writeFileSync6(fd, `${process.pid}
42498
43147
  ${new Date().toISOString()}
42499
43148
  ${token}
42500
43149
  `);
@@ -42550,7 +43199,7 @@ ${token}
42550
43199
  }
42551
43200
  log2(`[lsp] reclaiming install lock for ${lockKey} (owner_pid=${owningPid ?? "unknown"}, alive=${ownerAlive}, age_ms=${age})`);
42552
43201
  try {
42553
- unlinkSync6(lock);
43202
+ unlinkSync5(lock);
42554
43203
  } catch {}
42555
43204
  return tryClaim();
42556
43205
  }
@@ -42589,7 +43238,7 @@ function releaseInstallLock(lockKey, acquisitionToken) {
42589
43238
  return;
42590
43239
  }
42591
43240
  try {
42592
- unlinkSync6(lock);
43241
+ unlinkSync5(lock);
42593
43242
  } catch (unlinkErr) {
42594
43243
  const code = unlinkErr.code;
42595
43244
  if (code !== "ENOENT") {
@@ -42664,7 +43313,7 @@ function claimLspAutoInstallPass() {
42664
43313
  try {
42665
43314
  const fd = openSync8(lock, "wx");
42666
43315
  try {
42667
- writeFileSync7(fd, JSON.stringify(owner));
43316
+ writeFileSync6(fd, JSON.stringify(owner));
42668
43317
  } finally {
42669
43318
  closeSync9(fd);
42670
43319
  }
@@ -42695,12 +43344,12 @@ function claimLspAutoInstallPass() {
42695
43344
  return null;
42696
43345
  }
42697
43346
  const tempPath = `${stampPath}.tmp-${process.pid}-${owner.token}`;
42698
- writeFileSync7(tempPath, `${JSON.stringify({ lastRunMs: Date.now() })}
43347
+ writeFileSync6(tempPath, `${JSON.stringify({ lastRunMs: Date.now() })}
42699
43348
  `, {
42700
43349
  encoding: "utf8",
42701
43350
  mode: 384
42702
43351
  });
42703
- renameSync8(tempPath, stampPath);
43352
+ renameSync7(tempPath, stampPath);
42704
43353
  return { release };
42705
43354
  } catch (err) {
42706
43355
  if (claimed) {
@@ -42737,7 +43386,7 @@ function writeVersionCheck(npmPackage, latest) {
42737
43386
  last_checked: new Date().toISOString(),
42738
43387
  latest_eligible: latest
42739
43388
  };
42740
- writeFileSync7(file, JSON.stringify(record, null, 2));
43389
+ writeFileSync6(file, JSON.stringify(record, null, 2));
42741
43390
  }
42742
43391
  function shouldRecheckVersion(record, weeklyCheckIntervalMs = 7 * 24 * 60 * 60 * 1000) {
42743
43392
  if (!record)
@@ -43118,7 +43767,7 @@ function ensureInstallAnchor(cwd) {
43118
43767
  try {
43119
43768
  const stub = join19(cwd, "package.json");
43120
43769
  if (!existsSync14(stub)) {
43121
- writeFileSync8(stub, `${JSON.stringify({ name: "aft-lsp-cache", version: "0.0.0", private: true })}
43770
+ writeFileSync7(stub, `${JSON.stringify({ name: "aft-lsp-cache", version: "0.0.0", private: true })}
43122
43771
  `);
43123
43772
  }
43124
43773
  } catch (err) {
@@ -43331,7 +43980,7 @@ function quarantineCachedNpmInstall(spec, reason) {
43331
43980
  try {
43332
43981
  mkdirSync6(join19(dest, ".."), { recursive: true });
43333
43982
  rmSync5(dest, { recursive: true, force: true });
43334
- renameSync9(packageDir, dest);
43983
+ renameSync8(packageDir, dest);
43335
43984
  } catch (err) {
43336
43985
  warn2(`[lsp] tofu_mismatch ${spec.npm}: failed to quarantine cache entry: ${err}`);
43337
43986
  }
@@ -43435,12 +44084,12 @@ import {
43435
44084
  readFileSync as readFileSync13,
43436
44085
  readlinkSync as readlinkSync2,
43437
44086
  readSync as readSync6,
43438
- realpathSync as realpathSync5,
43439
- renameSync as renameSync10,
44087
+ realpathSync as realpathSync6,
44088
+ renameSync as renameSync9,
43440
44089
  rmSync as rmSync6,
43441
44090
  statSync as statSync13,
43442
- unlinkSync as unlinkSync7,
43443
- writeFileSync as writeFileSync9
44091
+ unlinkSync as unlinkSync6,
44092
+ writeFileSync as writeFileSync8
43444
44093
  } from "node:fs";
43445
44094
  import { dirname as dirname10, join as join20, relative as relative5, resolve as resolve12 } from "node:path";
43446
44095
  import { Readable as Readable3 } from "node:stream";
@@ -43566,9 +44215,9 @@ function writeInstallFailureMarker(spec, release, sha256, installError) {
43566
44215
  const tempPath = `${path}.tmp-${process.pid}-${randomBytes4(6).toString("hex")}`;
43567
44216
  try {
43568
44217
  mkdirSync7(dirname10(path), { recursive: true });
43569
- writeFileSync9(tempPath, `${JSON.stringify({ release, sha256, error: markerError, at: new Date().toISOString() })}
44218
+ writeFileSync8(tempPath, `${JSON.stringify({ release, sha256, error: markerError, at: new Date().toISOString() })}
43570
44219
  `, { encoding: "utf8", mode: 384 });
43571
- renameSync10(tempPath, path);
44220
+ renameSync9(tempPath, path);
43572
44221
  } catch (err) {
43573
44222
  try {
43574
44223
  rmSync6(tempPath, { force: true });
@@ -43634,7 +44283,7 @@ function writeGithubInstalledMetaIn(installDir, version, binarySha256, archiveSh
43634
44283
  binarySha256,
43635
44284
  ...archiveSha256 ? { archiveSha256 } : {}
43636
44285
  };
43637
- writeFileSync9(join20(installDir, INSTALLED_META_FILE2), JSON.stringify(meta), "utf8");
44286
+ writeFileSync8(join20(installDir, INSTALLED_META_FILE2), JSON.stringify(meta), "utf8");
43638
44287
  } catch (err) {
43639
44288
  warn2(`[lsp] failed to write github installed metadata in ${installDir}: ${err}`);
43640
44289
  }
@@ -43874,7 +44523,7 @@ async function downloadFile(url, destPath, fetchImpl, assetSize, signal) {
43874
44523
  await pipeline3(nodeStream, createWriteStream3(destPath), { signal: timeout.signal });
43875
44524
  } catch (err) {
43876
44525
  try {
43877
- unlinkSync7(destPath);
44526
+ unlinkSync6(destPath);
43878
44527
  } catch {}
43879
44528
  throw err;
43880
44529
  } finally {
@@ -43903,7 +44552,7 @@ async function fetchFollowingRedirects(url, fetchImpl, signal) {
43903
44552
  throw new Error(`too many redirects (>${maxRedirects})`);
43904
44553
  }
43905
44554
  function validateExtraction(stagingRoot) {
43906
- const realStagingRoot = realpathSync5(stagingRoot);
44555
+ const realStagingRoot = realpathSync6(stagingRoot);
43907
44556
  let totalBytes = 0;
43908
44557
  const walk = (dir) => {
43909
44558
  let entries;
@@ -43929,7 +44578,7 @@ function validateExtraction(stagingRoot) {
43929
44578
  }
43930
44579
  let realFull;
43931
44580
  try {
43932
- realFull = realpathSync5(full);
44581
+ realFull = realpathSync6(full);
43933
44582
  } catch (err) {
43934
44583
  throw new Error(`failed to realpath ${full}: ${err}`);
43935
44584
  }
@@ -43957,14 +44606,30 @@ function assertArchiveEntryPath(entry) {
43957
44606
  throw new Error(`archive entry escapes archive root: ${entry}`);
43958
44607
  }
43959
44608
  }
44609
+ var ARCHIVE_LIST_TIMEOUT_MS = 30000;
43960
44610
  function runArchiveTool(command, args, timeout) {
43961
44611
  return new Promise((resolvePromise, reject) => {
43962
- execFile(command, args, { encoding: "utf8", ...timeout !== undefined ? { timeout } : {} }, (err, stdout) => {
43963
- if (err)
43964
- reject(err);
43965
- else
43966
- resolvePromise(stdout);
43967
- });
44612
+ let child;
44613
+ const deadline = setTimeout(() => {
44614
+ reject(new Error(`${command} ${args[0]} timed out after ${timeout} ms`));
44615
+ child?.kill("SIGKILL");
44616
+ child?.stdin?.destroy();
44617
+ child?.stdout?.destroy();
44618
+ child?.stderr?.destroy();
44619
+ }, timeout);
44620
+ try {
44621
+ child = execFile(command, args, { encoding: "utf8", timeout, killSignal: "SIGKILL" }, (err, stdout) => {
44622
+ clearTimeout(deadline);
44623
+ if (err)
44624
+ reject(err);
44625
+ else
44626
+ resolvePromise(stdout);
44627
+ });
44628
+ child.stdin?.end();
44629
+ } catch (error) {
44630
+ clearTimeout(deadline);
44631
+ reject(error);
44632
+ }
43968
44633
  });
43969
44634
  }
43970
44635
  async function runTarExtraction(args, timeout) {
@@ -43978,7 +44643,7 @@ async function runTarExtraction(args, timeout) {
43978
44643
  }
43979
44644
  async function precheckWithTar(archivePath, command) {
43980
44645
  let totalBytes = 0;
43981
- const verbose = await runArchiveTool(command, ["-tvf", archivePath]);
44646
+ const verbose = await runArchiveTool(command, ["-tvf", archivePath], ARCHIVE_LIST_TIMEOUT_MS);
43982
44647
  for (const line of verbose.split(`
43983
44648
  `)) {
43984
44649
  if (line.startsWith("h")) {
@@ -43991,7 +44656,7 @@ async function precheckWithTar(archivePath, command) {
43991
44656
  totalBytes += Math.max(...numeric);
43992
44657
  }
43993
44658
  }
43994
- const names = await runArchiveTool(command, ["-tf", archivePath]);
44659
+ const names = await runArchiveTool(command, ["-tf", archivePath], ARCHIVE_LIST_TIMEOUT_MS);
43995
44660
  for (const entry of names.split(`
43996
44661
  `)) {
43997
44662
  if (entry.length > 0)
@@ -44002,11 +44667,11 @@ async function precheckWithTar(archivePath, command) {
44002
44667
  async function precheckArchiveContents(archivePath, archiveType, zipListingMode = "platform") {
44003
44668
  let totalBytes = 0;
44004
44669
  if (archiveType === "zip" && zipListingMode === "platform" && process.platform !== "win32") {
44005
- const out = await runArchiveTool("unzip", ["-l", archivePath]);
44670
+ const out = await runArchiveTool("unzip", ["-l", archivePath], ARCHIVE_LIST_TIMEOUT_MS);
44006
44671
  const match = out.match(/^\s*(\d+)\s+\d+\s+files?\s*$/m);
44007
44672
  if (match)
44008
44673
  totalBytes = Number.parseInt(match[1] ?? "0", 10);
44009
- const names = await runArchiveTool("unzip", ["-Z1", archivePath]);
44674
+ const names = await runArchiveTool("unzip", ["-Z1", archivePath], ARCHIVE_LIST_TIMEOUT_MS);
44010
44675
  for (const entry of names.split(`
44011
44676
  `)) {
44012
44677
  if (entry.length > 0)
@@ -44033,7 +44698,7 @@ async function extractArchiveSafely(archivePath, destDir, archiveType) {
44033
44698
  try {
44034
44699
  rmSync6(destDir, { recursive: true, force: true });
44035
44700
  } catch {}
44036
- renameSync10(stagingDir, destDir);
44701
+ renameSync9(stagingDir, destDir);
44037
44702
  } catch (err) {
44038
44703
  try {
44039
44704
  rmSync6(stagingDir, { recursive: true, force: true });
@@ -44048,7 +44713,7 @@ function quarantineCachedGithubInstall(spec, reason) {
44048
44713
  try {
44049
44714
  mkdirSync7(dirname10(dest), { recursive: true });
44050
44715
  rmSync6(dest, { recursive: true, force: true });
44051
- renameSync10(packageDir, dest);
44716
+ renameSync9(packageDir, dest);
44052
44717
  } catch (err) {
44053
44718
  warn2(`[lsp] tofu_mismatch ${spec.id}: failed to quarantine cache entry: ${err}`);
44054
44719
  }
@@ -44140,7 +44805,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
44140
44805
  const message = `hash failed: ${err instanceof Error ? err.message : String(err)}`;
44141
44806
  error2(`[lsp] hash ${spec.id} failed: ${err}`);
44142
44807
  try {
44143
- unlinkSync7(archivePath);
44808
+ unlinkSync6(archivePath);
44144
44809
  } catch {}
44145
44810
  return { ok: false, sha256: "", error: message };
44146
44811
  }
@@ -44151,7 +44816,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
44151
44816
  const message = `TOFU sha256 mismatch: previously installed archive sha256=${previousArchiveSha256}, ` + `downloaded sha256=${archiveSha256}`;
44152
44817
  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.`);
44153
44818
  try {
44154
- unlinkSync7(archivePath);
44819
+ unlinkSync6(archivePath);
44155
44820
  } catch {}
44156
44821
  return { ok: false, sha256: archiveSha256, error: message };
44157
44822
  }
@@ -44163,7 +44828,7 @@ async function downloadAndInstall(spec, tag, assets, platform, arch, fetchImpl,
44163
44828
  return { ok: false, sha256: archiveSha256, error: message };
44164
44829
  } finally {
44165
44830
  try {
44166
- unlinkSync7(archivePath);
44831
+ unlinkSync6(archivePath);
44167
44832
  } catch {}
44168
44833
  }
44169
44834
  const innerBinaryPath = join20(extractDir, spec.binaryPathInArchive(platform, arch, version));
@@ -45060,6 +45725,190 @@ function astTools(ctx) {
45060
45725
  };
45061
45726
  }
45062
45727
 
45728
+ // src/v2-session-lifecycle.ts
45729
+ import { Effect as Effect4, Stream } from "effect";
45730
+ function noteV2ToolAbort(client, sessionID) {
45731
+ const state = stateFor2(client, sessionID);
45732
+ if (!state || state.settling)
45733
+ return;
45734
+ let resolve = () => {};
45735
+ const promise = new Promise((done) => {
45736
+ resolve = done;
45737
+ });
45738
+ state.settling = { promise, resolve };
45739
+ }
45740
+ async function awaitV2ExecutionSettlement(client, sessionID) {
45741
+ await stateFor2(client, sessionID)?.settling?.promise;
45742
+ }
45743
+ var locations = new WeakMap;
45744
+ function stateFor2(client, sessionID) {
45745
+ if (!client || typeof client !== "object")
45746
+ return;
45747
+ const sessions = locations.get(client);
45748
+ if (!sessions)
45749
+ return;
45750
+ let state = sessions.get(sessionID);
45751
+ if (!state) {
45752
+ state = {
45753
+ stopped: false,
45754
+ stopEpoch: 0,
45755
+ tasks: new Map,
45756
+ wakes: new Set,
45757
+ watches: new Map
45758
+ };
45759
+ sessions.set(sessionID, state);
45760
+ }
45761
+ return state;
45762
+ }
45763
+ function v2SubagentStopped(client, sessionID) {
45764
+ return stateFor2(client, sessionID)?.stopped === true;
45765
+ }
45766
+ function v2StopEpoch(client, sessionID) {
45767
+ return stateFor2(client, sessionID)?.stopEpoch ?? 0;
45768
+ }
45769
+ async function killBackgroundTask(ctx, runtime, taskId) {
45770
+ const result = await callBashBridge(ctx, runtime, "bash_kill", { task_id: taskId }, { abortSignal: undefined });
45771
+ if (result.success === false)
45772
+ throw new Error(String(result.message ?? "bash_kill failed"));
45773
+ markHeldBgCompletions(runtime.sessionID);
45774
+ }
45775
+ async function trackV2BackgroundTask(ctx, runtime, taskId, startedEpoch) {
45776
+ const state = stateFor2(ctx.client, runtime.sessionID);
45777
+ if (!state)
45778
+ return;
45779
+ const kill = () => killBackgroundTask(ctx, runtime, taskId);
45780
+ if (state.stopped || state.stopEpoch !== startedEpoch) {
45781
+ await kill();
45782
+ return;
45783
+ }
45784
+ state.tasks.set(taskId, kill);
45785
+ }
45786
+ async function killStoppedBackgroundTasks(client, sessionID, ctx, startedBeforeMs) {
45787
+ const kills = [];
45788
+ const visited = new Set;
45789
+ const dispatch = (taskId, kill) => {
45790
+ if (visited.has(taskId))
45791
+ return;
45792
+ visited.add(taskId);
45793
+ kills.push(kill().catch((error) => {
45794
+ warn2(`[bash] failed to kill stopped session ${sessionID}'s task ${taskId}: ${String(error)}`);
45795
+ }));
45796
+ };
45797
+ for (const [owner, state] of locations.get(client) ?? []) {
45798
+ if (owner !== sessionID)
45799
+ continue;
45800
+ const tasks = [...state.tasks];
45801
+ state.tasks.clear();
45802
+ for (const [taskId, kill] of tasks) {
45803
+ dispatch(taskId, kill);
45804
+ }
45805
+ }
45806
+ if (ctx) {
45807
+ const runtime = {
45808
+ sessionID,
45809
+ directory: client.location.directory,
45810
+ directoryIsSessionRoot: true
45811
+ };
45812
+ try {
45813
+ const result = await callBashBridge(ctx, runtime, "bash_owned_tasks", { started_before_ms: startedBeforeMs }, { abortSignal: undefined });
45814
+ if (result.success === false)
45815
+ throw new Error(String(result.message ?? "bash_owned_tasks failed"));
45816
+ for (const taskId of Array.isArray(result.task_ids) ? result.task_ids : []) {
45817
+ if (typeof taskId === "string")
45818
+ dispatch(taskId, () => killBackgroundTask(ctx, runtime, taskId));
45819
+ }
45820
+ } catch (error) {
45821
+ warn2(`[bash] failed to enumerate stopped session ${sessionID}'s tasks: ${String(error)}`);
45822
+ }
45823
+ }
45824
+ await Promise.all(kills);
45825
+ }
45826
+ function allowV2WatchWake(client, sessionID, taskId) {
45827
+ const state = stateFor2(client, sessionID);
45828
+ if (state && !state.stopped)
45829
+ state.wakes.add(taskId);
45830
+ }
45831
+ function v2WatchWakeAllowed(client, sessionID, taskId) {
45832
+ const state = stateFor2(client, sessionID);
45833
+ return state !== undefined && !state.stopped && state.wakes.has(taskId);
45834
+ }
45835
+ function forgetV2Watch(client, sessionID, taskId) {
45836
+ const state = stateFor2(client, sessionID);
45837
+ if (!state)
45838
+ return;
45839
+ state.wakes.delete(taskId);
45840
+ state.tasks.delete(taskId);
45841
+ for (const [watchId, watch] of state.watches) {
45842
+ if (watch.taskId === taskId)
45843
+ state.watches.delete(watchId);
45844
+ }
45845
+ }
45846
+ async function trackV2Watch(ctx, runtime, taskId, watchId) {
45847
+ const state = stateFor2(ctx.client, runtime.sessionID);
45848
+ if (!state || typeof watchId !== "string")
45849
+ return;
45850
+ const cancel = () => callBashBridge(ctx, runtime, "bash_unnotify", { task_id: taskId, watch_id: watchId }, { abortSignal: undefined });
45851
+ if (state.stopped) {
45852
+ await cancel();
45853
+ return;
45854
+ }
45855
+ state.wakes.add(taskId);
45856
+ state.watches.set(watchId, { taskId, cancel });
45857
+ }
45858
+ function registerV2SessionLifecycle(context, ctx) {
45859
+ const host = context;
45860
+ return Effect4.gen(function* () {
45861
+ if (!host.event?.subscribe || !context || typeof context !== "object")
45862
+ return;
45863
+ const sessions = new Map;
45864
+ locations.set(context, sessions);
45865
+ yield* Effect4.addFinalizer(() => Effect4.sync(() => {
45866
+ for (const state of sessions.values())
45867
+ state.settling?.resolve();
45868
+ locations.delete(context);
45869
+ }));
45870
+ const pull = yield* Stream.toPull(host.event.subscribe());
45871
+ yield* Effect4.forkScoped(Effect4.forever(pull.pipe(Effect4.flatMap((events) => Effect4.forEach(events, (event) => Effect4.promise(async () => {
45872
+ const sessionID = event.data.sessionID;
45873
+ if (!sessionID)
45874
+ return;
45875
+ const state = stateFor2(context, sessionID);
45876
+ if (!state)
45877
+ return;
45878
+ if (event.type === "session.execution.started") {
45879
+ state.stopped = false;
45880
+ return;
45881
+ }
45882
+ const deliberateStop = event.type === "session.execution.interrupted" && (event.data.reason === "user" || event.data.reason === "inactivity");
45883
+ if (!deliberateStop) {
45884
+ if (event.type === "session.execution.failed" || event.type === "session.execution.succeeded" || event.type === "session.execution.interrupted") {
45885
+ state.settling?.resolve();
45886
+ state.settling = undefined;
45887
+ }
45888
+ return;
45889
+ }
45890
+ if (!await resolveIsSubagent(context, sessionID)) {
45891
+ state.settling?.resolve();
45892
+ state.settling = undefined;
45893
+ return;
45894
+ }
45895
+ state.stopped = true;
45896
+ state.stopEpoch += 1;
45897
+ state.wakes.clear();
45898
+ state.settling?.resolve();
45899
+ state.settling = undefined;
45900
+ const watches = [...state.watches.values()];
45901
+ state.watches.clear();
45902
+ Promise.all([
45903
+ killStoppedBackgroundTasks(context, sessionID, ctx, event.created),
45904
+ ...watches.map((watch) => watch.cancel().catch((error) => {
45905
+ warn2(`[bash_watch] failed to cancel stopped session ${sessionID}'s watch on ${watch.taskId}: ${String(error)}`);
45906
+ }))
45907
+ ]);
45908
+ }), { discard: true })))), { startImmediately: true });
45909
+ });
45910
+ }
45911
+
45063
45912
  // src/tools/bash.ts
45064
45913
  var z3 = tool.schema;
45065
45914
  var METADATA_PREVIEW_LIMIT = 30 * 1024;
@@ -45195,7 +46044,7 @@ function bashToolDescription(aftSearchRegistered, compressionOn, backgroundOn, d
45195
46044
  ].join(", ");
45196
46045
  const searchSteer = `use ${steerTools}${zoomEnabled ? ", or aft_zoom" : ""} instead`;
45197
46046
  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." : "";
45198
- 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).";
46047
+ 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).";
45199
46048
  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." : "";
45200
46049
  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.";
45201
46050
  const remoteRuns = surface.remoteRuns ? ` ${BASH_RUNON_GUIDANCE}` : "";
@@ -45293,7 +46142,7 @@ function bashTimeoutDescription(backgroundOn, role = "primary") {
45293
46142
  if (!backgroundOn) {
45294
46143
  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.";
45295
46144
  }
45296
- 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";
46145
+ 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";
45297
46146
  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.`;
45298
46147
  }
45299
46148
  function createBashTool(ctx, aftSearchRegisteredOverride) {
@@ -45305,7 +46154,7 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
45305
46154
  ...bashCompanionRegistered(ctx.config, "bash_kill") ? ["bash_kill"] : []
45306
46155
  ];
45307
46156
  const waitArg = initialBashCfg.background ? {
45308
- 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.`)
46157
+ 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.`)
45309
46158
  } : {};
45310
46159
  const sandboxArg = nativeSandboxEnabled(ctx.config) ? {
45311
46160
  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.")
@@ -45360,6 +46209,7 @@ ${BASH_RUNON_GUIDANCE}`, "");
45360
46209
  },
45361
46210
  execute: async (args, context) => {
45362
46211
  const bashCfg = resolveBashConfig(ctx.config);
46212
+ const startedEpoch = v2StopEpoch(ctx.client, context.sessionID);
45363
46213
  const ctxAftSearchRegistered = ctx.aftSearchRegistered === true;
45364
46214
  const aftSearchRegistered = aftSearchRegisteredOverride ?? ctxAftSearchRegistered;
45365
46215
  let accumulatedOutput = "";
@@ -45389,7 +46239,7 @@ ${BASH_RUNON_GUIDANCE}`, "");
45389
46239
  const blockToCompletion = subagentForcedForeground || backgroundDisabled || requestedWait;
45390
46240
  const effectiveBackground = blockToCompletion ? false : requestedBackground;
45391
46241
  const rawTimeout = coerceOptionalInt(args.timeout, "timeout", 1, Number.MAX_SAFE_INTEGER);
45392
- const workerWaitMaxMs = isSubagent ? bashCfg.worker_wait_max_ms : undefined;
46242
+ const workerWaitMaxMs = bashCfg.worker_wait_max_ms;
45393
46243
  const ptyRows = coerceOptionalInt(args.ptyRows, "ptyRows", 1, 60);
45394
46244
  const ptyCols = coerceOptionalInt(args.ptyCols, "ptyCols", 1, 140);
45395
46245
  const compressed = coerceBoolean(args.compressed, true);
@@ -45473,6 +46323,7 @@ ${BASH_RUNON_GUIDANCE}`, "");
45473
46323
  if (data.status === "running" && typeof data.task_id === "string") {
45474
46324
  const taskId = data.task_id;
45475
46325
  trackBgTask(context.sessionID, taskId);
46326
+ await trackV2BackgroundTask(ctx, context, taskId, startedEpoch);
45476
46327
  let rendered = data.output ?? "";
45477
46328
  if (isSubagent) {
45478
46329
  rendered += workerBackgroundTaskNote(taskId);
@@ -45545,7 +46396,10 @@ async function formatBashStatusText(runtime, taskId, data, requestedOutputMode,
45545
46396
  if (typeof data.live_descendants_summary === "string") {
45546
46397
  text += ` · ${data.live_descendants_summary}`;
45547
46398
  }
45548
- if (data.output_incomplete === true) {
46399
+ if (typeof data.remote_phase_note === "string") {
46400
+ text += ` · ${data.remote_phase_note}`;
46401
+ }
46402
+ if (data.output_incomplete === true || status === "failed" && data.status_reason) {
45549
46403
  const reason = typeof data.status_reason === "string" && data.status_reason ? data.status_reason : "PTY output may be incomplete";
45550
46404
  text += `
45551
46405
  [${reason}]`;
@@ -45642,7 +46496,25 @@ function conflictTools(ctx) {
45642
46496
  }
45643
46497
 
45644
46498
  // src/tools/definitions/v2.ts
45645
- import { Effect as Effect4 } from "effect";
46499
+ import { Effect as Effect5 } from "effect";
46500
+
46501
+ // src/v2-host-tool-names.ts
46502
+ var V2_HOST_TOOL_NAMES = Object.freeze({
46503
+ bash: "shell",
46504
+ apply_patch: "patch"
46505
+ });
46506
+ var AFT_TOOL_FOR_V2_HOST = Object.freeze(Object.fromEntries(Object.entries(V2_HOST_TOOL_NAMES).map(([aft, host]) => [host, aft])));
46507
+ function v2HostToolName(aftName) {
46508
+ return V2_HOST_TOOL_NAMES[aftName] ?? aftName;
46509
+ }
46510
+ function aftToolNameForV2Host(hostName) {
46511
+ return AFT_TOOL_FOR_V2_HOST[hostName] ?? hostName;
46512
+ }
46513
+ function renameV2ToolReferences(text) {
46514
+ return text.replace(/(?<![\w./-])bash(?![\w-]|\.\w|\s+(?:itself|REPLs|syntax)\b)/g, "shell").replace(/(?<![\w./-])apply_patch(?![\w-]|\.\w)/g, "patch");
46515
+ }
46516
+
46517
+ // src/tools/definitions/v2.ts
45646
46518
  var V2_TOOL_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
45647
46519
  var V2_PATH_HEADER_TOOLS = new Set(["read", "write", "edit"]);
45648
46520
  var V2_BASH_TOOLS = new Set(["bash"]);
@@ -45727,12 +46599,23 @@ function bareToolName3(name) {
45727
46599
  return name.startsWith("aft_") ? name.slice(4) : name;
45728
46600
  }
45729
46601
  function projectArguments(name, definition) {
45730
- if (!V2_PATH_HEADER_TOOLS.has(bareToolName3(name)) || !("filePath" in definition.args)) {
45731
- return { ...definition.args };
46602
+ const args = renameArgumentDescriptions(definition.args);
46603
+ if (!V2_PATH_HEADER_TOOLS.has(bareToolName3(name)) || !("filePath" in args)) {
46604
+ return args;
45732
46605
  }
45733
- const { filePath, ...rest } = definition.args;
46606
+ const { filePath, ...rest } = args;
45734
46607
  return { path: filePath, ...rest };
45735
46608
  }
46609
+ function renameArgumentDescriptions(args) {
46610
+ const renamed = {};
46611
+ for (const [key, schema] of Object.entries(args)) {
46612
+ const described = schema;
46613
+ const description = described.description;
46614
+ const next = description === undefined ? undefined : renameV2ToolReferences(description);
46615
+ renamed[key] = next === undefined || next === description ? schema : described.describe(next);
46616
+ }
46617
+ return renamed;
46618
+ }
45736
46619
  function executionArguments(name, input) {
45737
46620
  if (!V2_PATH_HEADER_TOOLS.has(bareToolName3(name)) || !("path" in input))
45738
46621
  return input;
@@ -45791,7 +46674,7 @@ function runtimeFor(name, location, context, signal, consumers) {
45791
46674
  abort: signal,
45792
46675
  effectAbort: signal,
45793
46676
  metadata: (update) => {
45794
- Effect4.runPromise(context.progress(jsonSafeMetadata({
46677
+ Effect5.runPromise(context.progress(jsonSafeMetadata({
45795
46678
  ...update.metadata ?? {},
45796
46679
  ...update.title ? { title: update.title } : {}
45797
46680
  })));
@@ -45806,36 +46689,46 @@ function runtimeFor(name, location, context, signal, consumers) {
45806
46689
  };
45807
46690
  }
45808
46691
  function projectV2Tool(name, definition, location, consumers = {}) {
46692
+ const hostName = v2HostToolName(name);
45809
46693
  const input = tool.schema.object(projectArguments(name, definition));
45810
- assertV2Contract(name, input);
46694
+ assertV2Contract(hostName, input);
45811
46695
  const sharedOptions = definition.options;
45812
46696
  const permission = hostPermission(name);
45813
46697
  return {
45814
- name,
45815
- description: definition.description,
46698
+ name: hostName,
46699
+ description: renameV2ToolReferences(definition.description),
45816
46700
  input,
45817
46701
  options: {
45818
46702
  ...sharedOptions,
45819
46703
  ...permission ? { permission } : {},
45820
46704
  codemode: false
45821
46705
  },
45822
- execute: (rawInput, context) => Effect4.tryPromise({
46706
+ execute: (rawInput, context) => Effect5.tryPromise({
45823
46707
  try: async (signal) => {
45824
- if (name === "bash_status" || name === "bash_watch") {
45825
- consumers.registerSession?.(context.sessionID);
46708
+ const onAbort = () => {
46709
+ if (context.sessionID)
46710
+ consumers.onToolAbort?.(context.sessionID);
46711
+ };
46712
+ signal.addEventListener("abort", onAbort, { once: true });
46713
+ try {
46714
+ if (name === "bash_status" || name === "bash_watch") {
46715
+ consumers.registerSession?.(context.sessionID);
46716
+ }
46717
+ const runtime = runtimeFor(name, location, context, signal, consumers);
46718
+ const input = executionArguments(name, rawInput);
46719
+ const result = consumers.executeBash && V2_BASH_TOOLS.has(name) ? await consumers.executeBash({
46720
+ name,
46721
+ input,
46722
+ context: runtime,
46723
+ definition
46724
+ }) : await definition.execute(input, runtime);
46725
+ const refusal = failureEnvelopeMessage(result);
46726
+ if (refusal)
46727
+ throw new Error(refusal);
46728
+ return resultContent(result);
46729
+ } finally {
46730
+ signal.removeEventListener("abort", onAbort);
45826
46731
  }
45827
- const runtime = runtimeFor(name, location, context, signal, consumers);
45828
- const input = executionArguments(name, rawInput);
45829
- const result = consumers.executeBash && V2_BASH_TOOLS.has(name) ? await consumers.executeBash({
45830
- name,
45831
- input,
45832
- context: runtime,
45833
- definition
45834
- }) : await definition.execute(input, runtime);
45835
- const refusal = failureEnvelopeMessage(result);
45836
- if (refusal)
45837
- throw new Error(refusal);
45838
- return resultContent(result);
45839
46732
  },
45840
46733
  catch: failure
45841
46734
  })
@@ -45864,10 +46757,10 @@ function joinList(items) {
45864
46757
  }
45865
46758
 
45866
46759
  // src/shared/read-vision.ts
45867
- import { Effect as Effect5 } from "effect";
46760
+ import { Effect as Effect6 } from "effect";
45868
46761
  var VISION_HOST_TIMEOUT_MS = 1500;
45869
46762
  var states = new WeakMap;
45870
- function stateFor2(client) {
46763
+ function stateFor3(client) {
45871
46764
  let state = states.get(client);
45872
46765
  if (!state) {
45873
46766
  state = { models: new Map, history: new Map, capabilities: new Map };
@@ -45876,7 +46769,7 @@ function stateFor2(client) {
45876
46769
  return state;
45877
46770
  }
45878
46771
  function rememberReadModel(client, sessionID, model) {
45879
- stateFor2(client).models.set(sessionID, model);
46772
+ stateFor3(client).models.set(sessionID, model);
45880
46773
  }
45881
46774
  async function bounded(call) {
45882
46775
  let timer;
@@ -45899,7 +46792,7 @@ function record3(value) {
45899
46792
  async function currentSessionVisionCapability(client, sessionID) {
45900
46793
  if (!sessionID)
45901
46794
  return;
45902
- const state = stateFor2(client);
46795
+ const state = stateFor3(client);
45903
46796
  let model = state.models.get(sessionID);
45904
46797
  if (!model) {
45905
46798
  let pending = state.history.get(sessionID);
@@ -45923,7 +46816,7 @@ async function currentSessionVisionCapability(client, sessionID) {
45923
46816
  return;
45924
46817
  const list = async () => {
45925
46818
  const result = api.list?.();
45926
- return isV2PluginContext3(client) ? Effect5.runPromise(result) : await result;
46819
+ return isV2PluginContext3(client) ? Effect6.runPromise(result) : await result;
45927
46820
  };
45928
46821
  const listed = record3(await bounded(list));
45929
46822
  const catalog = record3(listed?.data) ?? listed;
@@ -46014,6 +46907,7 @@ function createBashWatchTool(ctx) {
46014
46907
  throw new Error(`invalid_request: ${message}`);
46015
46908
  throw new Error(`${code}: ${message}`);
46016
46909
  }
46910
+ await trackV2Watch(ctx, context, taskId, registered.watch_id);
46017
46911
  const metadata = context.metadata;
46018
46912
  metadata?.({ taskId, registered: true, watchId: registered.watch_id });
46019
46913
  return `Watch registered: ${registered.watch_id} on task ${taskId}
@@ -46023,6 +46917,11 @@ A notification will fire when the pattern matches or the task exits.`;
46023
46917
  const role = isSubagent ? "worker" : "primary";
46024
46918
  const effectiveWaitMs = subagentForcedSync ? bashCfg.worker_wait_max_ms : resolveWatchTimeoutMs(coerceConfiguredWatchTimeout(args.timeoutMs, role, syncWaitCap), role, syncWaitCap, bashCfg.worker_wait_max_ms);
46025
46919
  const data = await waitForBashStatus(ctx, context, taskId, undefined, waitFor, effectiveWaitMs, role);
46920
+ if (isSubagent && context.abort.aborted)
46921
+ await awaitV2ExecutionSettlement(ctx.client, context.sessionID);
46922
+ const stopped = isSubagent && v2SubagentStopped(ctx.client, context.sessionID);
46923
+ if (stopped && data.waited?.reason === "user_message")
46924
+ data.waited.reason = "aborted";
46026
46925
  const waited = data.waited;
46027
46926
  if (waited?.reason === "user_message") {
46028
46927
  const convertedText = await convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, coerceBoolean(args.once, true), waited.elapsed_ms, role);
@@ -46041,6 +46940,7 @@ async function convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, once, e
46041
46940
  const interrupted = `Sync watch for task ${taskId} was interrupted because you sent a message after ${elapsedMs}ms of waiting. `;
46042
46941
  const reminderFallback = interruptedWatchTail(role, `The task is still running in the background. A completion reminder will be ` + `delivered automatically when the task exits.`);
46043
46942
  if (!waitFor) {
46943
+ allowV2WatchWake(ctx.client, context.sessionID, taskId);
46044
46944
  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.`);
46045
46945
  }
46046
46946
  const notifyParams = {
@@ -46058,6 +46958,7 @@ async function convertToAsyncWatchOnAbort(ctx, context, taskId, waitFor, once, e
46058
46958
  unmarkExplicitControl(context.sessionID, taskId);
46059
46959
  return interrupted + `Auto-registering an async watch failed (${String(registered.message ?? "unknown error")}). ` + reminderFallback;
46060
46960
  }
46961
+ await trackV2Watch(ctx, context, taskId, registered.watch_id);
46061
46962
  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.`);
46062
46963
  } catch (err) {
46063
46964
  unmarkExplicitControl(context.sessionID, taskId);
@@ -46174,6 +47075,9 @@ async function waitForBashStatus(ctx, runtime, taskId, outputMode, waitFor, effe
46174
47075
  }
46175
47076
  busySince = undefined;
46176
47077
  lastData = data;
47078
+ if (role === "worker" && abortSignal?.aborted) {
47079
+ return withWaited(data, waited("aborted"));
47080
+ }
46177
47081
  const terminal = isTerminalStatus(data.status);
46178
47082
  if (waitFor) {
46179
47083
  const scan = await readNewTaskOutput(data, spillCursor);
@@ -46862,9 +47766,6 @@ function createHashlineEditTool(ctx) {
46862
47766
  });
46863
47767
  if (denial)
46864
47768
  return permissionDeniedResponse(denial);
46865
- const preview = await callToolCall(ctx, context, "edit", rawArgs, { preview: true });
46866
- if (preview.success === false)
46867
- throw toolErrorFromResponse("edit", preview);
46868
47769
  const data = await callToolCall(ctx, context, "edit", rawArgs);
46869
47770
  if (data.success === false)
46870
47771
  throw toolErrorFromResponse("edit", data);
@@ -47493,12 +48394,13 @@ function createInspectTier2IdleScheduler(options) {
47493
48394
  }
47494
48395
  function inspectTools(ctx) {
47495
48396
  const inspectTool = {
47496
- 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.
48397
+ 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.
47497
48398
 
47498
48399
  ` + "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.",
47499
48400
  args: {
47500
- 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.")),
47501
- 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.")),
48401
+ 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.")),
48402
+ 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'.")),
48403
+ 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.")),
47502
48404
  topK: arg(z9.number().int().positive().max(100).optional().describe("Max drill-down items per category. Default 20, max 100."))
47503
48405
  },
47504
48406
  execute: async (args, context) => {
@@ -47513,6 +48415,8 @@ function inspectTools(ctx) {
47513
48415
  rawArgs.scope = scoped.scope;
47514
48416
  if (args.topK !== undefined && args.topK !== null)
47515
48417
  rawArgs.topK = args.topK;
48418
+ if (args.offset !== undefined && args.offset !== null)
48419
+ rawArgs.offset = args.offset;
47516
48420
  const response = await callToolCall(ctx, context, "inspect", rawArgs, {
47517
48421
  transportTimeoutMs: resolveInspectDiagnosticsTimeoutMs(ctx.config) + INSPECT_TRANSPORT_HEADROOM_MS
47518
48422
  });
@@ -48392,11 +49296,11 @@ function describeBashForRegisteredSurface(allTools, config) {
48392
49296
  }
48393
49297
  function registerAftTools(context, location, definitions, consumers = {}) {
48394
49298
  return context.tool.transform((editor) => {
48395
- const projected = Object.entries(definitions).map(([name, definition]) => projectV2Tool(name, definition, location, consumers));
48396
- for (const definition of projected) {
48397
- if (V2_BUILTIN_REPLACEMENTS.has(definition.name))
48398
- editor.remove(definition.name);
48399
- editor.add(definition);
49299
+ for (const [name, definition] of Object.entries(definitions)) {
49300
+ const projected = projectV2Tool(name, definition, location, consumers);
49301
+ if (V2_BUILTIN_REPLACEMENTS.has(name))
49302
+ editor.remove(projected.name);
49303
+ editor.add(projected);
48400
49304
  }
48401
49305
  });
48402
49306
  }
@@ -48797,6 +49701,7 @@ function startOpenCodeLiveConfigReload(options) {
48797
49701
  const { userConfigPath, projectConfigPath } = resolveCortexKitConfigPaths(options.directory);
48798
49702
  return startLiveConfigReload({
48799
49703
  paths: [userConfigPath, projectConfigPath],
49704
+ projectPath: projectConfigPath,
48800
49705
  load: () => loadOpenCodeConfigForLiveReload(options.directory),
48801
49706
  initialSources: options.initialSources,
48802
49707
  initialProjectText: options.initialSourceTexts?.[projectConfigPath] ?? null,
@@ -49363,21 +50268,21 @@ import {
49363
50268
  mkdirSync as mkdirSync8,
49364
50269
  openSync as openSync11,
49365
50270
  readFileSync as readFileSync17,
49366
- renameSync as renameSync11,
50271
+ renameSync as renameSync10,
49367
50272
  rmSync as rmSync8,
49368
50273
  statSync as statSync16,
49369
- writeFileSync as writeFileSync12
50274
+ writeFileSync as writeFileSync11
49370
50275
  } from "node:fs";
49371
50276
  import { dirname as dirname16 } from "node:path";
49372
50277
 
49373
50278
  // src/hooks/auto-update-checker/cache.ts
49374
- import { cpSync, existsSync as existsSync19, mkdtempSync, readFileSync as readFileSync16, rmSync as rmSync7, writeFileSync as writeFileSync11 } from "node:fs";
50279
+ import { cpSync, existsSync as existsSync19, mkdtempSync, readFileSync as readFileSync16, rmSync as rmSync7, writeFileSync as writeFileSync10 } from "node:fs";
49375
50280
  import { tmpdir as tmpdir5 } from "node:os";
49376
50281
  import { basename as basename6, dirname as dirname15, join as join25 } from "node:path";
49377
50282
  var import_comment_json3 = __toESM(require_src2(), 1);
49378
50283
 
49379
50284
  // src/hooks/auto-update-checker/checker.ts
49380
- import { existsSync as existsSync18, readFileSync as readFileSync15, statSync as statSync15, writeFileSync as writeFileSync10 } from "node:fs";
50285
+ import { existsSync as existsSync18, readFileSync as readFileSync15, statSync as statSync15, writeFileSync as writeFileSync9 } from "node:fs";
49381
50286
  import { readFile } from "node:fs/promises";
49382
50287
  import { homedir as homedir15 } from "node:os";
49383
50288
  import { dirname as dirname14, isAbsolute as isAbsolute13, join as join24, resolve as resolve15 } from "node:path";
@@ -49765,11 +50670,11 @@ function restoreAutoUpdateSnapshot(snapshot) {
49765
50670
  if (snapshot.packageJson === null)
49766
50671
  rmSync7(snapshot.packageJsonPath, { force: true });
49767
50672
  else
49768
- writeFileSync11(snapshot.packageJsonPath, snapshot.packageJson);
50673
+ writeFileSync10(snapshot.packageJsonPath, snapshot.packageJson);
49769
50674
  if (snapshot.lockfile === null)
49770
50675
  rmSync7(snapshot.lockfilePath, { force: true });
49771
50676
  else
49772
- writeFileSync11(snapshot.lockfilePath, snapshot.lockfile);
50677
+ writeFileSync10(snapshot.lockfilePath, snapshot.lockfile);
49773
50678
  rmSync7(snapshot.packageDir, { recursive: true, force: true });
49774
50679
  if (snapshot.stagedPackageDir) {
49775
50680
  cpSync(snapshot.stagedPackageDir, snapshot.packageDir, { recursive: true });
@@ -49806,7 +50711,7 @@ function removeFromPackageLock(installDir, packageName) {
49806
50711
  modified = true;
49807
50712
  }
49808
50713
  if (modified) {
49809
- writeFileSync11(lockPath, JSON.stringify(lock, null, 2));
50714
+ writeFileSync10(lockPath, JSON.stringify(lock, null, 2));
49810
50715
  log2(`[auto-update-checker] Removed from package-lock.json: ${packageName}`);
49811
50716
  }
49812
50717
  return modified;
@@ -49828,7 +50733,7 @@ function ensureDependencyVersion(packageJsonPath, packageName, version) {
49828
50733
  return true;
49829
50734
  dependencies[packageName] = version;
49830
50735
  nextPackageJson.dependencies = dependencies;
49831
- writeFileSync11(packageJsonPath, JSON.stringify(nextPackageJson, null, 2));
50736
+ writeFileSync10(packageJsonPath, JSON.stringify(nextPackageJson, null, 2));
49832
50737
  log2(`[auto-update-checker] Updated dependency in package.json: ${packageName} → ${version}`);
49833
50738
  return true;
49834
50739
  } catch (err) {
@@ -50072,7 +50977,7 @@ function claimCheckSlot(storageDir, intervalMs) {
50072
50977
  let lockFd = null;
50073
50978
  try {
50074
50979
  lockFd = openSync11(lockPath, "wx");
50075
- writeFileSync12(lockFd, JSON.stringify(owner));
50980
+ writeFileSync11(lockFd, JSON.stringify(owner));
50076
50981
  closeSync12(lockFd);
50077
50982
  lockFd = null;
50078
50983
  } catch (err) {
@@ -50086,9 +50991,9 @@ function claimCheckSlot(storageDir, intervalMs) {
50086
50991
  if (!isOrphanedCheckLock(lockPath))
50087
50992
  return null;
50088
50993
  const replacementPath = `${lockPath}.tmp.${process.pid}`;
50089
- writeFileSync12(replacementPath, JSON.stringify(owner), { encoding: "utf-8", mode: 384 });
50994
+ writeFileSync11(replacementPath, JSON.stringify(owner), { encoding: "utf-8", mode: 384 });
50090
50995
  try {
50091
- renameSync11(replacementPath, lockPath);
50996
+ renameSync10(replacementPath, lockPath);
50092
50997
  } catch (replaceError) {
50093
50998
  rmSync8(replacementPath, { force: true });
50094
50999
  throw replaceError;
@@ -50168,8 +51073,8 @@ function hasRecentCheckTimestamp(file, intervalMs) {
50168
51073
  }
50169
51074
  function writeCheckTimestamp(file) {
50170
51075
  const tmp = `${file}.tmp.${process.pid}`;
50171
- writeFileSync12(tmp, JSON.stringify({ lastCheckedMs: Date.now() }), "utf-8");
50172
- renameSync11(tmp, file);
51076
+ writeFileSync11(tmp, JSON.stringify({ lastCheckedMs: Date.now() }), "utf-8");
51077
+ renameSync10(tmp, file);
50173
51078
  }
50174
51079
  function isNewerVersion(candidate, installed) {
50175
51080
  const semver = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
@@ -50373,9 +51278,9 @@ import {
50373
51278
  mkdirSync as mkdirSync9,
50374
51279
  readdirSync as readdirSync6,
50375
51280
  readFileSync as readFileSync18,
50376
- renameSync as renameSync12,
50377
- unlinkSync as unlinkSync8,
50378
- writeFileSync as writeFileSync13
51281
+ renameSync as renameSync11,
51282
+ unlinkSync as unlinkSync7,
51283
+ writeFileSync as writeFileSync12
50379
51284
  } from "node:fs";
50380
51285
  import { createServer } from "node:http";
50381
51286
  import { dirname as dirname17, join as join27 } from "node:path";
@@ -50531,13 +51436,13 @@ class AftRpcServer {
50531
51436
  const dir = dirname17(this.portFilePath);
50532
51437
  mkdirSync9(dir, { recursive: true, mode: 448 });
50533
51438
  const tmpPath = `${this.portFilePath}.tmp`;
50534
- writeFileSync13(tmpPath, JSON.stringify({
51439
+ writeFileSync12(tmpPath, JSON.stringify({
50535
51440
  port: this.port,
50536
51441
  token: this.token,
50537
51442
  pid: process.pid,
50538
51443
  started_at: Date.now()
50539
51444
  }), { encoding: "utf-8", mode: 384 });
50540
- renameSync12(tmpPath, this.portFilePath);
51445
+ renameSync11(tmpPath, this.portFilePath);
50541
51446
  }
50542
51447
  sweepDeadPortFiles() {
50543
51448
  let entries;
@@ -50555,11 +51460,11 @@ class AftRpcServer {
50555
51460
  try {
50556
51461
  const record = parseRpcPortRecord(readFileSync18(filePath, "utf-8"));
50557
51462
  if (record === null) {
50558
- unlinkSync8(filePath);
51463
+ unlinkSync7(filePath);
50559
51464
  continue;
50560
51465
  }
50561
51466
  if (record.pid !== undefined && !isPidAlive(record.pid)) {
50562
- unlinkSync8(filePath);
51467
+ unlinkSync7(filePath);
50563
51468
  }
50564
51469
  } catch {}
50565
51470
  }
@@ -50595,7 +51500,7 @@ class AftRpcServer {
50595
51500
  this.bunServer = null;
50596
51501
  }
50597
51502
  try {
50598
- unlinkSync8(this.portFilePath);
51503
+ unlinkSync7(this.portFilePath);
50599
51504
  } catch {}
50600
51505
  }
50601
51506
  async handleFetch(req, srv) {
@@ -50824,7 +51729,13 @@ function readStatusBar(value) {
50824
51729
  const duplicates = readOptionalNumber(bar.duplicates);
50825
51730
  const todos = readOptionalNumber(bar.todos);
50826
51731
  const noLanguageServer = bar.diagnostics === "no_language_server";
50827
- if (errors === null && warnings === null && deadCode === null && unusedExports === null && duplicates === null && todos === null && !noLanguageServer && disabled.length === 0) {
51732
+ const producerMissing = bar.diagnostics === "producer_missing";
51733
+ 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) => ({
51734
+ language: gap.language,
51735
+ producer: gap.producer,
51736
+ root: gap.root
51737
+ })) : [];
51738
+ if (errors === null && warnings === null && deadCode === null && unusedExports === null && duplicates === null && todos === null && !noLanguageServer && !producerMissing && disabled.length === 0) {
50828
51739
  return;
50829
51740
  }
50830
51741
  return {
@@ -50836,7 +51747,8 @@ function readStatusBar(value) {
50836
51747
  ...duplicates !== null ? { duplicates } : {},
50837
51748
  ...todos !== null ? { todos } : {},
50838
51749
  ...bar.tier2_stale === true ? { tier2_stale: true } : {},
50839
- ...noLanguageServer ? { diagnostics: "no_language_server" } : {}
51750
+ ...noLanguageServer ? { diagnostics: "no_language_server" } : {},
51751
+ ...producerMissing ? { diagnostics: "producer_missing", diagnostics_gaps: diagnosticsGaps } : {}
50840
51752
  };
50841
51753
  }
50842
51754
  function formatFlag(enabled) {
@@ -51288,11 +52200,11 @@ function throwSentinel(command) {
51288
52200
  throw sentinel;
51289
52201
  }
51290
52202
  var PLUGIN_VERSION = resolvePluginVersion(import.meta.url);
51291
- var ANNOUNCEMENT_VERSION = "0.59.0";
52203
+ var ANNOUNCEMENT_VERSION = "0.60.0";
51292
52204
  var ANNOUNCEMENT_FEATURES = [
51293
- "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.",
51294
- "New: live pull-request diffs with `read` on `pr://N/diff`, a `pattern` argument for `aft_search`, and per-category switches for `aft_inspect`.",
51295
- "Fixes for issues reported on OpenCode 2, Pi and OMP, and much less repeated indexing and disk work."
52205
+ "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.",
52206
+ "`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.",
52207
+ "OpenCode 2 gets native `shell` and `patch` tools, and stopped subagents are no longer woken by their own background commands."
51296
52208
  ];
51297
52209
  var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/DSa65w8wuf";
51298
52210
  var plugin = async (input) => initializePluginForDirectory(input);
@@ -51785,7 +52697,7 @@ var src_default = plugin;
51785
52697
  import { Effect as Effect14 } from "effect";
51786
52698
 
51787
52699
  // src/rpc/register.ts
51788
- import { Effect as Effect6 } from "effect";
52700
+ import { Effect as Effect7 } from "effect";
51789
52701
 
51790
52702
  // src/tui/aft-rpc.ts
51791
52703
  import { Schema } from "effect";
@@ -51885,7 +52797,7 @@ function restoreOwnProperty(target, key, descriptor) {
51885
52797
  Reflect.deleteProperty(target, key);
51886
52798
  }
51887
52799
  function registerAftRpc(context, location, pool) {
51888
- return Effect6.gen(function* () {
52800
+ return Effect7.gen(function* () {
51889
52801
  const root = locationRoot(location);
51890
52802
  const bridgeUnsubscribes = new Map;
51891
52803
  let disposed = false;
@@ -51893,7 +52805,7 @@ function registerAftRpc(context, location, pool) {
51893
52805
  const emit = async (name, payload) => {
51894
52806
  if (disposed)
51895
52807
  return;
51896
- await Effect6.runPromise(registration.events.emit(name, payload));
52808
+ await Effect7.runPromise(registration.events.emit(name, payload));
51897
52809
  };
51898
52810
  const publishStatus = (snapshot) => {
51899
52811
  const sessionID = sessionFromSnapshot(snapshot);
@@ -51907,21 +52819,21 @@ function registerAftRpc(context, location, pool) {
51907
52819
  return;
51908
52820
  bridgeUnsubscribes.set(bridge, bridge.subscribeStatus(publishStatus));
51909
52821
  };
51910
- const getStatus = (input) => Effect6.suspend(() => {
52822
+ const getStatus = (input) => Effect7.suspend(() => {
51911
52823
  const sessionID = input.sessionID || "rpc";
51912
52824
  const bridge = pool.getActiveBridgeForRoot(root);
51913
52825
  if (!bridge)
51914
- return Effect6.succeed(placeholderStatus());
52826
+ return Effect7.succeed(placeholderStatus());
51915
52827
  observeBridge(bridge);
51916
52828
  const cached = bridge.getCachedStatus();
51917
52829
  const cachedSessionID = cached ? sessionFromSnapshot(cached) : undefined;
51918
52830
  if (cached && cachedSessionID === sessionID) {
51919
- return Effect6.succeed({ success: true, ...cached });
52831
+ return Effect7.succeed({ success: true, ...cached });
51920
52832
  }
51921
- return Effect6.tryPromise({
52833
+ return Effect7.tryPromise({
51922
52834
  try: (signal) => bridge.send("status", { session_id: sessionID }, { abortSignal: signal }),
51923
52835
  catch: (error) => error
51924
- }).pipe(Effect6.tap((response) => Effect6.sync(() => {
52836
+ }).pipe(Effect7.tap((response) => Effect7.sync(() => {
51925
52837
  if (response.success !== false)
51926
52838
  bridge.cacheStatusSnapshot(response);
51927
52839
  })));
@@ -51983,21 +52895,21 @@ function registerAftRpc(context, location, pool) {
51983
52895
  bridgeUnsubscribes.clear();
51984
52896
  restoreOwnProperty(pool, "getBridge", ownGetBridge);
51985
52897
  restoreOwnProperty(pool, "toolCall", ownToolCall);
51986
- await Effect6.runPromise(registration.dispose);
52898
+ await Effect7.runPromise(registration.dispose);
51987
52899
  }
51988
52900
  };
51989
52901
  });
51990
52902
  }
51991
52903
  function registerAftConfigErrorRpc(context, message) {
51992
- return Effect6.gen(function* () {
52904
+ return Effect7.gen(function* () {
51993
52905
  let disposed = false;
51994
52906
  const registration = yield* context.rpc.register(AftRpc, {
51995
- getStatus: () => Effect6.succeed(configErrorStatusSnapshot(message))
52907
+ getStatus: () => Effect7.succeed(configErrorStatusSnapshot(message))
51996
52908
  });
51997
52909
  const emit = async (name, payload) => {
51998
52910
  if (disposed)
51999
52911
  return;
52000
- await Effect6.runPromise(registration.events.emit(name, payload));
52912
+ await Effect7.runPromise(registration.events.emit(name, payload));
52001
52913
  };
52002
52914
  return {
52003
52915
  emitStatusInvalidated: (payload = {}) => emit("statusInvalidated", payload),
@@ -52008,14 +52920,14 @@ function registerAftConfigErrorRpc(context, message) {
52008
52920
  if (disposed)
52009
52921
  return;
52010
52922
  disposed = true;
52011
- await Effect6.runPromise(registration.dispose);
52923
+ await Effect7.runPromise(registration.dispose);
52012
52924
  }
52013
52925
  };
52014
52926
  });
52015
52927
  }
52016
52928
 
52017
52929
  // src/permissions/v2.ts
52018
- import { Effect as Effect7 } from "effect";
52930
+ import { Effect as Effect8 } from "effect";
52019
52931
 
52020
52932
  class PermissionDeniedError extends Error {
52021
52933
  name = "PermissionDeniedError";
@@ -52071,7 +52983,7 @@ function detail(error) {
52071
52983
  return error instanceof Error ? error.message : String(error);
52072
52984
  }
52073
52985
  async function runHostEffect(effect) {
52074
- return await Effect7.runPromise(effect);
52986
+ return await Effect8.runPromise(effect);
52075
52987
  }
52076
52988
  async function sessionRecord(host, sessionID) {
52077
52989
  if (!sessionID)
@@ -52848,11 +53760,11 @@ function v2PromptChannelFor(settings) {
52848
53760
  return createV2PromptChannel(() => discoverPromptServer(settings));
52849
53761
  }
52850
53762
 
52851
- // src/v2-host-tool-overlap.ts
52852
- import { Effect as Effect9 } from "effect";
53763
+ // src/v2-prompt-detach.ts
53764
+ import { Effect as Effect10 } from "effect";
52853
53765
 
52854
53766
  // src/wakes/session-delivery.ts
52855
- import { Effect as Effect8, Exit } from "effect";
53767
+ import { Effect as Effect9, Exit } from "effect";
52856
53768
  var AFT_PROMPT_SOURCE = "aft";
52857
53769
  function isAftOriginatedPrompt(metadata) {
52858
53770
  return typeof metadata === "object" && metadata !== null && metadata.source === AFT_PROMPT_SOURCE;
@@ -52866,43 +53778,49 @@ class V2SessionDelivery {
52866
53778
  this.session = session;
52867
53779
  }
52868
53780
  completion(input) {
52869
- return Effect8.suspend(() => {
53781
+ return Effect9.suspend(() => {
52870
53782
  if (this.disposed)
52871
- return Effect8.succeed(false);
53783
+ return Effect9.succeed(false);
52872
53784
  const keys = [...new Set(input.taskIDs)].map((taskID) => `${input.sessionID}\x00${taskID}`);
52873
53785
  if (keys.length === 0 || keys.every((key) => this.admittedCompletions.has(key))) {
52874
- return Effect8.succeed(false);
53786
+ return Effect9.succeed(false);
52875
53787
  }
52876
53788
  const reserved = keys.filter((key) => !this.admittedCompletions.has(key));
52877
53789
  for (const key of reserved)
52878
53790
  this.admittedCompletions.add(key);
52879
- return Effect8.suspend(() => this.session.prompt({
52880
- sessionID: input.sessionID,
52881
- text: input.text,
52882
- delivery: "steer",
52883
- metadata: {
52884
- ...input.metadata,
52885
- source: AFT_PROMPT_SOURCE,
52886
- task_ids: [...new Set(input.taskIDs)]
52887
- }
52888
- })).pipe(Effect8.onExit((exit) => Effect8.sync(() => {
53791
+ return Effect9.suspend(() => this.session.get({ sessionID: input.sessionID }).pipe(Effect9.flatMap((session) => {
53792
+ if (session.revert) {
53793
+ return Effect9.fail(new Error("Background bash completion deferred: session has a staged revert"));
53794
+ }
53795
+ return this.session.synthetic({
53796
+ sessionID: input.sessionID,
53797
+ text: input.text,
53798
+ description: "Background bash completed",
53799
+ delivery: "steer",
53800
+ metadata: {
53801
+ ...input.metadata,
53802
+ source: AFT_PROMPT_SOURCE,
53803
+ task_ids: [...new Set(input.taskIDs)]
53804
+ }
53805
+ });
53806
+ }))).pipe(Effect9.onExit((exit) => Effect9.sync(() => {
52889
53807
  if (Exit.isFailure(exit))
52890
53808
  for (const key of reserved)
52891
53809
  this.admittedCompletions.delete(key);
52892
- })), Effect8.map(() => true));
53810
+ })), Effect9.map(() => true));
52893
53811
  });
52894
53812
  }
52895
53813
  status(input) {
52896
- return Effect8.suspend(() => {
53814
+ return Effect9.suspend(() => {
52897
53815
  if (this.disposed)
52898
- return Effect8.succeed(undefined);
53816
+ return Effect9.succeed(undefined);
52899
53817
  return this.session.synthetic({
52900
53818
  sessionID: input.sessionID,
52901
53819
  text: input.text,
52902
53820
  description: input.description,
52903
53821
  metadata: input.metadata,
52904
53822
  resume: false
52905
- }).pipe(Effect8.asVoid);
53823
+ }).pipe(Effect9.asVoid);
52906
53824
  });
52907
53825
  }
52908
53826
  dispose() {
@@ -52913,117 +53831,7 @@ class V2SessionDelivery {
52913
53831
  }
52914
53832
  }
52915
53833
 
52916
- // src/v2-host-tool-overlap.ts
52917
- var OVERLAPS = [
52918
- { aft: "apply_patch", host: "patch" },
52919
- { aft: "bash", host: "shell" }
52920
- ];
52921
- var FIX_COMMAND = "`npx @cortexkit/aft doctor --fix`";
52922
- function findHostToolOverlaps(hostToolNames, registeredAftTools) {
52923
- return OVERLAPS.filter((pair) => registeredAftTools.has(pair.aft) && hostToolNames.has(pair.host) && !registeredAftTools.has(pair.host));
52924
- }
52925
- function hostToolOverlapNotice(overlaps) {
52926
- const patch = overlaps.some((pair) => pair.host === "patch");
52927
- const shell = overlaps.some((pair) => pair.host === "shell");
52928
- if (patch && shell) {
52929
- return `OpenCode's built-in patch and shell tools are still enabled beside AFT's apply_patch and bash, so the model sees two editing tools and two shells. Run ${FIX_COMMAND} to turn the built-in ones off.`;
52930
- }
52931
- if (patch) {
52932
- return `OpenCode's built-in patch tool is still enabled beside AFT's apply_patch, so the model sees two editing tools. Run ${FIX_COMMAND} to turn the built-in one off.`;
52933
- }
52934
- if (shell) {
52935
- return `OpenCode's built-in shell tool is still enabled beside AFT's bash, so the model sees two shells. Run ${FIX_COMMAND} to turn the built-in one off.`;
52936
- }
52937
- return null;
52938
- }
52939
- var noticeClaimed = false;
52940
- var loggedUnavailableReasons = new Set;
52941
- function logDetectionUnavailable(reason) {
52942
- if (loggedUnavailableReasons.has(reason))
52943
- return;
52944
- loggedUnavailableReasons.add(reason);
52945
- debug2(`Built-in tool overlap check skipped: ${reason}; no notice shown`);
52946
- }
52947
- function reasonOf(error) {
52948
- return error instanceof Error ? error.message : String(error);
52949
- }
52950
- async function hostToolNames(context) {
52951
- const tool = context.tool;
52952
- const list = tool?.list;
52953
- if (typeof list !== "function") {
52954
- logDetectionUnavailable("the host context has no tool.list()");
52955
- return null;
52956
- }
52957
- let entries;
52958
- try {
52959
- entries = await Effect9.runPromise(list.call(tool));
52960
- } catch (error) {
52961
- logDetectionUnavailable(`tool.list() failed: ${reasonOf(error)}`);
52962
- return null;
52963
- }
52964
- if (!Array.isArray(entries)) {
52965
- logDetectionUnavailable("tool.list() did not return a list");
52966
- return null;
52967
- }
52968
- const names = new Set;
52969
- for (const entry of entries) {
52970
- const name = typeof entry?.id === "string" ? entry.id : entry?.name;
52971
- if (typeof name === "string")
52972
- names.add(name);
52973
- }
52974
- return names;
52975
- }
52976
- async function checkAndNotify(context, registeredAftTools, deliver) {
52977
- const names = await hostToolNames(context);
52978
- if (!names)
52979
- return;
52980
- const overlaps = findHostToolOverlaps(names, registeredAftTools);
52981
- const text = hostToolOverlapNotice(overlaps);
52982
- if (!text)
52983
- return;
52984
- if (noticeClaimed)
52985
- return;
52986
- noticeClaimed = true;
52987
- try {
52988
- await deliver(text);
52989
- debug2(`Built-in tool overlap notice sent to the TUI: ${overlaps.map((pair) => `${pair.host} beside ${pair.aft}`).join(", ")}`);
52990
- } catch (error) {
52991
- noticeClaimed = false;
52992
- warn2(`Built-in tool overlap notice could not be delivered: ${reasonOf(error)}`);
52993
- }
52994
- }
52995
- function createHostToolOverlapPromptHook(context, registeredAftTools, deliver, onChecked) {
52996
- const host = context;
52997
- let checked = false;
52998
- return (event) => Effect9.sync(() => {
52999
- if (checked || noticeClaimed)
53000
- return;
53001
- if (isAftOriginatedPrompt(event?.metadata))
53002
- return;
53003
- if (typeof event?.sessionID !== "string" || event.sessionID.length === 0)
53004
- return;
53005
- checked = true;
53006
- const done = checkAndNotify(host, registeredAftTools, deliver).catch((error) => {
53007
- warn2(`Built-in tool overlap check failed: ${reasonOf(error)}`);
53008
- });
53009
- onChecked?.(done);
53010
- });
53011
- }
53012
- function registerV2HostToolOverlapNotice(context, registeredAftTools, deliver, onChecked) {
53013
- if (findHostToolOverlaps(new Set(["patch", "shell"]), registeredAftTools).length === 0) {
53014
- return Effect9.succeed(false);
53015
- }
53016
- const session = context?.session;
53017
- const hook = session?.hook;
53018
- if (typeof hook !== "function") {
53019
- logDetectionUnavailable("the host context has no session prompt hook to check from");
53020
- return Effect9.succeed(false);
53021
- }
53022
- return hook.call(session, "prompt", createHostToolOverlapPromptHook(context, registeredAftTools, deliver, onChecked)).pipe(Effect9.as(true));
53023
- }
53024
-
53025
53834
  // src/v2-prompt-detach.ts
53026
- import { Effect as Effect10 } from "effect";
53027
53835
  function mapOffset(offset, edits) {
53028
53836
  let shift = 0;
53029
53837
  for (const edit of edits) {
@@ -53102,25 +53910,55 @@ function gateEditingTools(event, registeredTools) {
53102
53910
  const prefersPatch = id.includes("gpt-") && !id.includes("oss") && !id.includes("gpt-4");
53103
53911
  for (const name of prefersPatch ? ["edit", "write"] : ["apply_patch"]) {
53104
53912
  if (registeredTools.has(name))
53105
- delete event.tools[name];
53913
+ delete event.tools[v2HostToolName(name)];
53106
53914
  }
53107
53915
  }
53108
- function registerV2ToolHooks(context, runtime, registeredTools) {
53916
+ var HOST_SHELL_DESCRIPTION = /^Execute a shell command\b/;
53917
+ function restoreShellDescription(event, registeredTools, shellDescription) {
53918
+ if (shellDescription === undefined || !registeredTools.has("bash"))
53919
+ return;
53920
+ const shell = event.tools[v2HostToolName("bash")];
53921
+ if (!shell || shell.description === shellDescription)
53922
+ return;
53923
+ if (!HOST_SHELL_DESCRIPTION.test(shell.description))
53924
+ return;
53925
+ shell.description = shellDescription;
53926
+ }
53927
+ function registerV2ToolHooks(context, runtime, registeredTools, bashDescription) {
53109
53928
  const host = context;
53929
+ const shellDescription = bashDescription === undefined ? undefined : renameV2ToolReferences(bashDescription);
53110
53930
  return Effect11.gen(function* () {
53111
53931
  if (typeof host.tool?.hook === "function") {
53932
+ yield* host.tool.hook("execute.after", (event) => Effect11.promise(async () => {
53933
+ if (!runtime.pool || event.status !== "completed" || !hasHeldBgCompletions(event.sessionID))
53934
+ return;
53935
+ const content = event.result.content;
53936
+ const output = { output: "" };
53937
+ await appendInTurnBgCompletions({
53938
+ ctx: runtime,
53939
+ directory: context.location.directory,
53940
+ sessionID: event.sessionID
53941
+ }, output);
53942
+ if (!output.output)
53943
+ return;
53944
+ event.result = {
53945
+ ...event.result,
53946
+ content: typeof content === "string" ? content + output.output : [...content ?? [], { type: "text", text: output.output }]
53947
+ };
53948
+ }));
53112
53949
  yield* host.tool.hook("execute.before", (event) => Effect11.try({
53113
53950
  try: () => {
53114
- if (!registeredTools.has(event.tool))
53951
+ const name = aftToolNameForV2Host(event.tool);
53952
+ if (!registeredTools.has(name))
53115
53953
  return;
53116
- event.input = prepareOpenCodeArguments(event.tool, event.input, {
53954
+ event.input = prepareOpenCodeArguments(name, event.input, {
53117
53955
  hashlineEffective: runtime.hashlineEffective
53118
53956
  });
53119
53957
  },
53120
53958
  catch: (error) => new ArgumentError({ message: error instanceof Error ? error.message : String(error) })
53121
53959
  }));
53122
53960
  yield* host.tool.hook("execute.after", (event) => Effect11.sync(() => {
53123
- if (event.tool !== "bash" || event.status !== "completed" || !registeredTools.has("bash") || !registeredTools.has("aft_conflicts"))
53961
+ if (aftToolNameForV2Host(event.tool) !== "bash" || event.status !== "completed" || !registeredTools.has("bash") || !registeredTools.has("aft_conflicts"))
53124
53962
  return;
53125
53963
  const content = event.result.content;
53126
53964
  event.result = {
@@ -53131,10 +53969,14 @@ function registerV2ToolHooks(context, runtime, registeredTools) {
53131
53969
  }
53132
53970
  if (typeof host.session?.hook === "function") {
53133
53971
  for (const name of ["compaction", "generate"]) {
53134
- yield* host.session.hook(name, (event) => Effect11.sync(() => gateEditingTools(event, registeredTools)));
53972
+ yield* host.session.hook(name, (event) => Effect11.sync(() => {
53973
+ gateEditingTools(event, registeredTools);
53974
+ restoreShellDescription(event, registeredTools, shellDescription);
53975
+ }));
53135
53976
  }
53136
53977
  yield* host.session.hook("context", (event) => Effect11.sync(() => {
53137
53978
  gateEditingTools(event, registeredTools);
53979
+ restoreShellDescription(event, registeredTools, shellDescription);
53138
53980
  rememberReadModel(context, event.sessionID, {
53139
53981
  providerID: event.model.providerID,
53140
53982
  modelID: event.model.id
@@ -53279,15 +54121,33 @@ var sharedBridgeOptions = {
53279
54121
  onBashCompletion: routeCompletion,
53280
54122
  onBashLongRunning: routeLongRunning
53281
54123
  };
53282
- function createV2RuntimeConsumer(context) {
54124
+ function detachWaitsForCompletion(sessionID, text, bridge, config) {
54125
+ const onlyThisBridge = {
54126
+ getActiveBridgeForRoot: () => bridge,
54127
+ activeBridges: () => [bridge]
54128
+ };
54129
+ interruptBashWaitsForChatMessage(onlyThisBridge, config, "", sessionID, {
54130
+ parts: [{ type: "text", text, synthetic: true }]
54131
+ });
54132
+ }
54133
+ function createV2RuntimeConsumer(context, getConfig = () => ({})) {
53283
54134
  const delivery = new V2SessionDelivery(context.session);
53284
54135
  const sessions = new Set;
53285
54136
  const route = {
53286
54137
  completion: async (completion, bridge) => {
54138
+ if (await resolveIsSubagent(context, completion.session_id)) {
54139
+ await awaitV2ExecutionSettlement(context, completion.session_id);
54140
+ }
54141
+ if (await resolveIsSubagent(context, completion.session_id) && !v2WatchWakeAllowed(context, completion.session_id, completion.task_id)) {
54142
+ holdBgCompletion(completion.session_id, completion, v2SubagentStopped(context, completion.session_id));
54143
+ forgetV2Watch(context, completion.session_id, completion.task_id);
54144
+ return;
54145
+ }
54146
+ const text = formatSystemReminder([completion]);
53287
54147
  const admitted = await Effect13.runPromise(delivery.completion({
53288
54148
  sessionID: completion.session_id,
53289
54149
  taskIDs: [completion.task_id],
53290
- text: formatSystemReminder([completion]),
54150
+ text,
53291
54151
  metadata: {
53292
54152
  source: "aft",
53293
54153
  kind: "bash_completion",
@@ -53297,10 +54157,12 @@ function createV2RuntimeConsumer(context) {
53297
54157
  }));
53298
54158
  if (!admitted)
53299
54159
  return;
54160
+ detachWaitsForCompletion(completion.session_id, text, bridge, getConfig());
53300
54161
  await bridge.send("bash_ack_completions", {
53301
54162
  session_id: completion.session_id,
53302
54163
  task_ids: [completion.task_id]
53303
54164
  });
54165
+ forgetV2Watch(context, completion.session_id, completion.task_id);
53304
54166
  },
53305
54167
  longRunning: (reminder) => Effect13.runPromise(delivery.status({
53306
54168
  sessionID: reminder.session_id,
@@ -53324,6 +54186,7 @@ function createV2RuntimeConsumer(context) {
53324
54186
  return {
53325
54187
  bridgeOptions: sharedBridgeOptions,
53326
54188
  registerSession,
54189
+ onToolAbort: (sessionID) => noteV2ToolAbort(context, sessionID),
53327
54190
  executeBash: (execution) => {
53328
54191
  registerSession(execution.context.sessionID);
53329
54192
  return executeV2Bash(execution);
@@ -53350,10 +54213,9 @@ var defaults = {
53350
54213
  registerPromptHook: registerV2PromptDetachHook,
53351
54214
  registerToolHooks: registerV2ToolHooks,
53352
54215
  registerWorkflowHints: registerV2WorkflowHints,
53353
- registerHostToolOverlapNotice: registerV2HostToolOverlapNotice,
53354
- toolConsumers: (context, config) => ({
54216
+ toolConsumers: (context, config, getConfig = () => config) => ({
53355
54217
  ...hoistedV2ToolConsumers(context, v2PromptChannelFor(config?.opencode)),
53356
- ...createV2RuntimeConsumer(context)
54218
+ ...createV2RuntimeConsumer(context, getConfig)
53357
54219
  }),
53358
54220
  acquireBridge,
53359
54221
  releaseBridge,
@@ -53378,7 +54240,8 @@ async function bootLocation(context, location, dependencies) {
53378
54240
  const pluginVersion = dependencies.resolveVersion();
53379
54241
  const environment = await prepareBridgeEnvironment({ configRoot: directory, lspDirectory: directory, config, pluginVersion, notify }, dependencies);
53380
54242
  const canonicalDirectory = location.project?.canonical ?? directory;
53381
- const consumers = dependencies.toolConsumers(context, config);
54243
+ let liveConfig = config;
54244
+ const consumers = dependencies.toolConsumers(context, config, () => liveConfig);
53382
54245
  const isProjectEnabled = createProjectAcceptance(directory, dependencies);
53383
54246
  const pool = await dependencies.acquireBridge(canonicalDirectory, {
53384
54247
  harness: "opencode",
@@ -53403,6 +54266,7 @@ async function bootLocation(context, location, dependencies) {
53403
54266
  client: context,
53404
54267
  config,
53405
54268
  hashlineEffective: openCodeHashlineEffective(config),
54269
+ hostToolNames: V2_HOST_TOOL_NAMES,
53406
54270
  storageDir: environment.storageDir,
53407
54271
  isProjectEnabled
53408
54272
  };
@@ -53411,7 +54275,8 @@ async function bootLocation(context, location, dependencies) {
53411
54275
  const { hashlineEditRegistered } = applyToolSurfaceOverrides(pool, config, registeredTools);
53412
54276
  reportHashlineDowngrade(config, registeredTools, notify);
53413
54277
  toolContext.hashlineEffective = hashlineEditRegistered;
53414
- const hintsBlock = buildHintsForRegisteredTools(config, registeredTools, hashlineEditRegistered);
54278
+ const hints = buildHintsForRegisteredTools(config, registeredTools, hashlineEditRegistered);
54279
+ const hintsBlock = hints === null ? null : renameV2ToolReferences(hints);
53415
54280
  const liveConfigReload = dependencies.startLiveConfigReload({
53416
54281
  directory,
53417
54282
  initialSources: bootstrap.sources ?? [],
@@ -53419,6 +54284,7 @@ async function bootLocation(context, location, dependencies) {
53419
54284
  getConfig: () => toolContext.config,
53420
54285
  setConfig: (next) => {
53421
54286
  toolContext.config = next;
54287
+ liveConfig = next;
53422
54288
  },
53423
54289
  notify
53424
54290
  });
@@ -53470,6 +54336,7 @@ function makeServerEffect(overrides = {}) {
53470
54336
  }));
53471
54337
  const rpc = yield* dependencies.registerRpc(context, location, runtime.pool);
53472
54338
  yield* Effect14.addFinalizer(() => Effect14.promise(() => rpc.dispose()));
54339
+ yield* registerV2SessionLifecycle(context, runtime.toolContext);
53473
54340
  yield* dependencies.registerPromptHook(context, {
53474
54341
  pool: runtime.pool,
53475
54342
  projectRoot: runtime.directory,
@@ -53477,9 +54344,8 @@ function makeServerEffect(overrides = {}) {
53477
54344
  registerSession: runtime.consumers.registerSession
53478
54345
  });
53479
54346
  yield* dependencies.registerTools(context, location, runtime.tools, runtime.consumers);
53480
- yield* dependencies.registerToolHooks(context, runtime.toolContext, new Set(Object.keys(runtime.tools)));
54347
+ yield* dependencies.registerToolHooks(context, runtime.toolContext, new Set(Object.keys(runtime.tools)), runtime.tools.bash?.description);
53481
54348
  yield* dependencies.registerWorkflowHints(context, runtime.hintsBlock);
53482
- yield* dependencies.registerHostToolOverlapNotice(context, new Set(Object.keys(runtime.tools)), (message) => rpc.emitHostToolOverlap({ message }));
53483
54349
  });
53484
54350
  };
53485
54351
  }