@cortexkit/aft-opencode 0.49.4 → 0.50.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8334,6 +8334,8 @@ var DEFAULT_BRIDGE_TIMEOUT_MS = 30000;
8334
8334
  var BRIDGE_HANG_TIMEOUT_THRESHOLD = 2;
8335
8335
  var MAX_STDOUT_BUFFER = 64 * 1024 * 1024;
8336
8336
  var STDOUT_BUFFER_COMPACT_THRESHOLD = 64 * 1024;
8337
+ var HASHLINE_REGISTRATION_LOG_INTERVAL_MS = 60000;
8338
+ var HASHLINE_REGISTRATION_LOG_STATE_LIMIT = 256;
8337
8339
  var TERMINAL_BASH_STATUSES = new Set([
8338
8340
  "completed",
8339
8341
  "failed",
@@ -8483,6 +8485,9 @@ class BinaryBridge {
8483
8485
  configured = false;
8484
8486
  _configurePromise = null;
8485
8487
  configOverrides;
8488
+ editSlotSurvives;
8489
+ editSlotSurvivesCaptured = false;
8490
+ hashlineRegistrationLogState = new Map;
8486
8491
  minVersion;
8487
8492
  onVersionMismatch;
8488
8493
  onConfigureWarnings;
@@ -8499,20 +8504,33 @@ class BinaryBridge {
8499
8504
  errorPrefix;
8500
8505
  logger;
8501
8506
  childEnv;
8502
- constructor(binaryPath, cwd, options, configOverrides) {
8507
+ constructor(binaryPath, cwd, options, configOverrides, editSlotSurvives) {
8503
8508
  this.binaryPath = binaryPath;
8504
8509
  this.cwd = cwd;
8505
8510
  this.timeoutMs = options?.timeoutMs ?? DEFAULT_BRIDGE_TIMEOUT_MS;
8506
8511
  this.hangThreshold = options?.hangThreshold ?? BRIDGE_HANG_TIMEOUT_THRESHOLD;
8507
8512
  this.maxRestarts = options?.maxRestarts ?? 3;
8508
- this.configOverrides = configOverrides ?? {};
8513
+ this.errorPrefix = options?.errorPrefix ?? "[aft-bridge]";
8514
+ this.configOverrides = { ...configOverrides ?? {} };
8515
+ const legacyEditSlotSurvives = this.configOverrides.edit_slot_survives;
8516
+ delete this.configOverrides.edit_slot_survives;
8517
+ if (legacyEditSlotSurvives !== undefined && typeof legacyEditSlotSurvives !== "boolean") {
8518
+ throw new Error(`${this.errorPrefix} edit_slot_survives must be a boolean`);
8519
+ }
8520
+ if (editSlotSurvives !== undefined && legacyEditSlotSurvives !== undefined && editSlotSurvives !== legacyEditSlotSurvives) {
8521
+ throw new Error(`${this.errorPrefix} conflicting edit_slot_survives construction values`);
8522
+ }
8523
+ const capturedEditSlotSurvives = editSlotSurvives ?? legacyEditSlotSurvives;
8524
+ if (typeof capturedEditSlotSurvives === "boolean") {
8525
+ this.editSlotSurvives = capturedEditSlotSurvives;
8526
+ this.editSlotSurvivesCaptured = true;
8527
+ }
8509
8528
  this.minVersion = options?.minVersion;
8510
8529
  this.onVersionMismatch = options?.onVersionMismatch;
8511
8530
  this.onConfigureWarnings = options?.onConfigureWarnings;
8512
8531
  this.onBashCompletion = options?.onBashCompletion;
8513
8532
  this.onBashLongRunning = options?.onBashLongRunning;
8514
8533
  this.onBashPatternMatch = options?.onBashPatternMatch;
8515
- this.errorPrefix = options?.errorPrefix ?? "[aft-bridge]";
8516
8534
  this.logger = options?.logger;
8517
8535
  this.childEnv = options?.childEnv;
8518
8536
  }
@@ -8623,13 +8641,54 @@ class BinaryBridge {
8623
8641
  cacheStatusSnapshot(snapshot) {
8624
8642
  this.cachedStatus = snapshot;
8625
8643
  }
8644
+ setEditSlotSurvives(value) {
8645
+ if (this.editSlotSurvivesCaptured) {
8646
+ throw new Error(`${this.errorPrefix} edit_slot_survives is write-once and was already captured`);
8647
+ }
8648
+ this.editSlotSurvives = value;
8649
+ this.editSlotSurvivesCaptured = true;
8650
+ }
8651
+ logHashlineRegistrationCarrier(phase, sessionId, editSlotSurvives) {
8652
+ const session = sessionId && sessionId.length > 0 ? sessionId : "__default__";
8653
+ const key = `${phase}\x00${session}\x00${String(editSlotSurvives)}`;
8654
+ const now = Date.now();
8655
+ const state = this.hashlineRegistrationLogState.get(key);
8656
+ if (state && now - state.lastEmittedAt < HASHLINE_REGISTRATION_LOG_INTERVAL_MS) {
8657
+ state.suppressed += 1;
8658
+ return;
8659
+ }
8660
+ const repeated = state && state.suppressed > 0 ? ` repeated=${state.suppressed + 1}` : "";
8661
+ if (!state && this.hashlineRegistrationLogState.size >= HASHLINE_REGISTRATION_LOG_STATE_LIMIT) {
8662
+ const oldest = this.hashlineRegistrationLogState.keys().next().value;
8663
+ if (oldest !== undefined)
8664
+ this.hashlineRegistrationLogState.delete(oldest);
8665
+ }
8666
+ this.hashlineRegistrationLogState.set(key, { lastEmittedAt: now, suppressed: 0 });
8667
+ this.sessionLogVia(sessionId, `hashline registration carrier transport=ndjson phase=${phase} edit_slot_survives=${editSlotSurvives}${repeated}`);
8668
+ }
8626
8669
  async send(command, params = {}, options) {
8627
- return this.sendWithVersionMismatchRetry(command, params, options, true);
8670
+ let dispatchParams = params;
8671
+ if (command === "configure") {
8672
+ dispatchParams = { ...params };
8673
+ delete dispatchParams.edit_slot_survives;
8674
+ const editSlotSurvives = this.editSlotSurvives;
8675
+ if (this.editSlotSurvivesCaptured && typeof editSlotSurvives === "boolean") {
8676
+ dispatchParams.edit_slot_survives = editSlotSurvives;
8677
+ const sessionId = typeof dispatchParams.session_id === "string" ? dispatchParams.session_id : undefined;
8678
+ this.logHashlineRegistrationCarrier("configure", sessionId, editSlotSurvives);
8679
+ }
8680
+ }
8681
+ return this.sendWithVersionMismatchRetry(command, dispatchParams, options, true);
8628
8682
  }
8629
8683
  async toolCall(sessionId, name, rawArgs = {}, options) {
8630
8684
  const params = { name, arguments: rawArgs };
8631
8685
  if (sessionId)
8632
8686
  params.session_id = sessionId;
8687
+ const editSlotSurvives = this.editSlotSurvives;
8688
+ if (this.editSlotSurvivesCaptured && typeof editSlotSurvives === "boolean") {
8689
+ params.edit_slot_survives = editSlotSurvives;
8690
+ this.logHashlineRegistrationCarrier("tool_call", sessionId, editSlotSurvives);
8691
+ }
8633
8692
  const { preview, ...sendOptions } = options ?? {};
8634
8693
  if (preview === true)
8635
8694
  params.preview = true;
@@ -9433,7 +9492,8 @@ function formatDroppedKeyWarnings(dropped) {
9433
9492
  // ../aft-bridge/dist/downloader.js
9434
9493
  import { spawnSync } from "node:child_process";
9435
9494
  import { createHash as createHash2, randomUUID } from "node:crypto";
9436
- import { chmodSync, closeSync, copyFileSync, createWriteStream, existsSync as existsSync2, mkdirSync, openSync, readFileSync as readFileSync3, renameSync, rmSync, statSync as statSync2, unlinkSync, writeSync } from "node:fs";
9495
+ import { chmodSync, closeSync, copyFileSync, createWriteStream, existsSync as existsSync2, mkdirSync, openSync, readdirSync, readFileSync as readFileSync3, renameSync, rmSync, statSync as statSync2, unlinkSync, writeSync } from "node:fs";
9496
+ import { hostname } from "node:os";
9437
9497
  import { join as join4 } from "node:path";
9438
9498
  import { Readable } from "node:stream";
9439
9499
  import { pipeline } from "node:stream/promises";
@@ -9458,8 +9518,8 @@ var REPO = "cortexkit/aft";
9458
9518
  var DOWNLOAD_TIMEOUT_MS = 300000;
9459
9519
  var LATEST_TAG_TIMEOUT_MS = 30000;
9460
9520
  var MAX_DOWNLOAD_BYTES = 200 * 1024 * 1024;
9461
- var DOWNLOAD_LOCK_TIMEOUT_MS = 120000;
9462
9521
  var DOWNLOAD_LOCK_STALE_MS = 10 * 60000;
9522
+ var DOWNLOAD_LOCK_TIMEOUT_MS = DOWNLOAD_LOCK_STALE_MS + 30000;
9463
9523
  function readBinaryVersion(binaryPath) {
9464
9524
  try {
9465
9525
  const result = spawnSync(binaryPath, ["--version"], {
@@ -9527,11 +9587,37 @@ async function downloadBinary(version) {
9527
9587
  let binaryTimeout = null;
9528
9588
  let checksumTimeout = null;
9529
9589
  const tmpPath = `${binaryPath}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
9590
+ const cleanUpPartialDownload = () => {
9591
+ try {
9592
+ if (existsSync2(tmpPath))
9593
+ unlinkSync(tmpPath);
9594
+ } catch {}
9595
+ };
9596
+ let interrupted = false;
9597
+ const cleanUpInterruptedDownload = () => {
9598
+ if (interrupted)
9599
+ return;
9600
+ interrupted = true;
9601
+ binaryController?.abort();
9602
+ checksumController?.abort();
9603
+ cleanUpPartialDownload();
9604
+ releaseLock?.();
9605
+ releaseLock = null;
9606
+ };
9607
+ const handleSigint = () => {
9608
+ cleanUpInterruptedDownload();
9609
+ process.off("SIGINT", handleSigint);
9610
+ process.kill(process.pid, "SIGINT");
9611
+ };
9612
+ const handleExit = () => cleanUpInterruptedDownload();
9613
+ process.once("SIGINT", handleSigint);
9614
+ process.once("exit", handleExit);
9530
9615
  try {
9531
9616
  if (!existsSync2(versionedCacheDir)) {
9532
9617
  mkdirSync(versionedCacheDir, { recursive: true });
9533
9618
  }
9534
9619
  releaseLock = await acquireDownloadLock(lockPath);
9620
+ sweepStaleDownloadTemps(versionedCacheDir, binaryName);
9535
9621
  if (existsSync2(binaryPath) && isExpectedCachedBinary(binaryPath, tag)) {
9536
9622
  return binaryPath;
9537
9623
  }
@@ -9607,13 +9693,11 @@ async function downloadBinary(version) {
9607
9693
  } catch (err) {
9608
9694
  const msg = err instanceof Error ? err.message : String(err);
9609
9695
  error(`Failed to download AFT binary: ${msg}`);
9610
- if (existsSync2(tmpPath)) {
9611
- try {
9612
- unlinkSync(tmpPath);
9613
- } catch {}
9614
- }
9696
+ cleanUpPartialDownload();
9615
9697
  return null;
9616
9698
  } finally {
9699
+ process.off("SIGINT", handleSigint);
9700
+ process.off("exit", handleExit);
9617
9701
  if (binaryTimeout) {
9618
9702
  binaryController?.abort();
9619
9703
  clearTimeout(binaryTimeout);
@@ -9639,17 +9723,85 @@ async function ensureBinary(version) {
9639
9723
  log("No cached binary found, downloading latest...");
9640
9724
  return downloadBinary();
9641
9725
  }
9642
- async function acquireDownloadLock(lockPath) {
9726
+ function createDownloadLockOwner() {
9727
+ return JSON.stringify({
9728
+ pid: process.pid,
9729
+ hostname: hostname(),
9730
+ createdAt: Date.now(),
9731
+ token: randomUUID()
9732
+ });
9733
+ }
9734
+ function parseDownloadLockOwner(raw) {
9735
+ try {
9736
+ const parsed = JSON.parse(raw);
9737
+ if (typeof parsed === "object" && parsed !== null) {
9738
+ const { pid, hostname: ownerHostname } = parsed;
9739
+ return {
9740
+ pid: typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0 ? pid : null,
9741
+ hostname: typeof ownerHostname === "string" && ownerHostname ? ownerHostname : null
9742
+ };
9743
+ }
9744
+ } catch {}
9745
+ const legacyPid = Number(raw.split(":", 1)[0]);
9746
+ return {
9747
+ pid: Number.isSafeInteger(legacyPid) && legacyPid > 0 ? legacyPid : null,
9748
+ hostname: null
9749
+ };
9750
+ }
9751
+ function isProcessAlive(pid) {
9752
+ try {
9753
+ process.kill(pid, 0);
9754
+ return true;
9755
+ } catch (err) {
9756
+ return err.code !== "ESRCH";
9757
+ }
9758
+ }
9759
+ function isReclaimableDownloadLock(owner, ageMs, staleMs) {
9760
+ if (Math.abs(ageMs) > staleMs)
9761
+ return true;
9762
+ return (owner.hostname === null || owner.hostname === hostname()) && owner.pid !== null && !isProcessAlive(owner.pid);
9763
+ }
9764
+ function reclaimDownloadLock(lockPath, expectedOwner) {
9765
+ try {
9766
+ if (readFileSync3(lockPath, "utf-8") !== expectedOwner)
9767
+ return false;
9768
+ rmSync(lockPath, { force: true });
9769
+ return true;
9770
+ } catch (err) {
9771
+ if (err.code === "ENOENT")
9772
+ return true;
9773
+ throw err;
9774
+ }
9775
+ }
9776
+ function sweepStaleDownloadTemps(versionedCacheDir, binaryName) {
9777
+ const tempPrefix = `${binaryName}.`;
9778
+ try {
9779
+ for (const entry of readdirSync(versionedCacheDir, { withFileTypes: true })) {
9780
+ if (!entry.isFile() || !entry.name.startsWith(tempPrefix) || !entry.name.endsWith(".tmp")) {
9781
+ continue;
9782
+ }
9783
+ const tempPath = join4(versionedCacheDir, entry.name);
9784
+ const ageMs = Date.now() - statSync2(tempPath).mtimeMs;
9785
+ if (Math.abs(ageMs) > DOWNLOAD_LOCK_STALE_MS)
9786
+ unlinkSync(tempPath);
9787
+ }
9788
+ } catch {}
9789
+ }
9790
+ async function acquireDownloadLock(lockPath, timing = {}) {
9791
+ const timeoutMs = timing.timeoutMs ?? DOWNLOAD_LOCK_TIMEOUT_MS;
9792
+ const staleMs = timing.staleMs ?? DOWNLOAD_LOCK_STALE_MS;
9793
+ const pollIntervalMs = timing.pollIntervalMs ?? 100;
9643
9794
  const startedAt = Date.now();
9644
9795
  while (true) {
9645
9796
  try {
9646
- const owner = `${process.pid}:${Date.now()}:${randomUUID()}`;
9797
+ const owner = createDownloadLockOwner();
9647
9798
  const fd = openSync(lockPath, "wx");
9648
- writeSync(fd, owner);
9799
+ try {
9800
+ writeSync(fd, owner);
9801
+ } finally {
9802
+ closeSync(fd);
9803
+ }
9649
9804
  return () => {
9650
- try {
9651
- closeSync(fd);
9652
- } catch {}
9653
9805
  try {
9654
9806
  if (readFileSync3(lockPath, "utf-8") === owner) {
9655
9807
  rmSync(lockPath, { force: true });
@@ -9660,19 +9812,24 @@ async function acquireDownloadLock(lockPath) {
9660
9812
  const code = err.code;
9661
9813
  if (code !== "EEXIST")
9662
9814
  throw err;
9815
+ let existingOwner;
9816
+ let ageMs;
9663
9817
  try {
9664
- const ageMs = Date.now() - statSync2(lockPath).mtimeMs;
9665
- if (ageMs > DOWNLOAD_LOCK_STALE_MS) {
9666
- rmSync(lockPath, { force: true });
9818
+ existingOwner = readFileSync3(lockPath, "utf-8");
9819
+ ageMs = Date.now() - statSync2(lockPath).mtimeMs;
9820
+ } catch (readErr) {
9821
+ if (readErr.code === "ENOENT")
9667
9822
  continue;
9668
- }
9669
- } catch {
9670
- continue;
9823
+ throw readErr;
9671
9824
  }
9672
- if (Date.now() - startedAt > DOWNLOAD_LOCK_TIMEOUT_MS) {
9825
+ if (isReclaimableDownloadLock(parseDownloadLockOwner(existingOwner), ageMs, staleMs)) {
9826
+ if (reclaimDownloadLock(lockPath, existingOwner))
9827
+ continue;
9828
+ }
9829
+ if (Date.now() - startedAt > timeoutMs) {
9673
9830
  throw new Error(`Timed out waiting for download lock: ${lockPath}`);
9674
9831
  }
9675
- await new Promise((resolve3) => setTimeout(resolve3, 100));
9832
+ await new Promise((resolve3) => setTimeout(resolve3, pollIntervalMs));
9676
9833
  }
9677
9834
  }
9678
9835
  }
@@ -12483,6 +12640,8 @@ function projectRootKeyHash(dir) {
12483
12640
  var AFT_MODULE_ID = "aft";
12484
12641
  var MAX_CONSECUTIVE_TRANSPORT_FAILURES = 3;
12485
12642
  var BG_STABLE_MS = 5000;
12643
+ var BG_LIFECYCLE_LOG_INTERVAL_MS = 60000;
12644
+ var BG_DISPATCH_PROBE_INTERVAL_MS = 60000;
12486
12645
  var DEFAULT_SESSION_ID = "__default__";
12487
12646
  var LOCALLY_SATISFIED_COMMANDS = new Set(["configure"]);
12488
12647
 
@@ -12545,12 +12704,15 @@ class BgSubscription {
12545
12704
  canAttach;
12546
12705
  onRootAttachFailure;
12547
12706
  onDormant;
12707
+ dispatchProbeIntervalMs;
12548
12708
  nudgeRef;
12549
12709
  isCurrent;
12550
12710
  stopped = false;
12551
12711
  current = null;
12552
12712
  loop;
12553
- constructor(identity, acquireClient, dropClient, consumerIdentity, onNudge, sleep2, canAttach, onRootAttachFailure, onDormant, nudgeRef, isCurrent = () => true) {
12713
+ lifecycleLogState = new Map;
12714
+ nudgeReceiptLogState = null;
12715
+ constructor(identity, acquireClient, dropClient, consumerIdentity, onNudge, sleep2, canAttach, onRootAttachFailure, onDormant, dispatchProbeIntervalMs, nudgeRef, isCurrent = () => true) {
12554
12716
  this.identity = identity;
12555
12717
  this.acquireClient = acquireClient;
12556
12718
  this.dropClient = dropClient;
@@ -12560,6 +12722,7 @@ class BgSubscription {
12560
12722
  this.canAttach = canAttach;
12561
12723
  this.onRootAttachFailure = onRootAttachFailure;
12562
12724
  this.onDormant = onDormant;
12725
+ this.dispatchProbeIntervalMs = dispatchProbeIntervalMs;
12563
12726
  this.nudgeRef = nudgeRef;
12564
12727
  this.isCurrent = isCurrent;
12565
12728
  this.loop = this.run();
@@ -12576,25 +12739,106 @@ class BgSubscription {
12576
12739
  return;
12577
12740
  });
12578
12741
  }
12742
+ info(kind, message) {
12743
+ const now = Date.now();
12744
+ const state = this.lifecycleLogState.get(kind);
12745
+ if (state && now - state.lastEmittedAt < BG_LIFECYCLE_LOG_INTERVAL_MS) {
12746
+ state.suppressed += 1;
12747
+ return;
12748
+ }
12749
+ const suppressed = state?.suppressed ?? 0;
12750
+ this.lifecycleLogState.set(kind, { lastEmittedAt: now, suppressed: 0 });
12751
+ const suffix = suppressed > 0 ? ` suppressed=${suppressed}` : "";
12752
+ log(`subc bg_events: ${message}${suffix}`, { sessionId: this.identity.session });
12753
+ }
12754
+ routeId(route) {
12755
+ return `${route.channel}@${route.epoch}`;
12756
+ }
12757
+ recordNudgeReceipt(routeId) {
12758
+ const now = Date.now();
12759
+ const state = this.nudgeReceiptLogState;
12760
+ if (state && now - state.lastEmittedAt < BG_LIFECYCLE_LOG_INTERVAL_MS) {
12761
+ state.count += 1;
12762
+ return;
12763
+ }
12764
+ const count = (state?.count ?? 0) + 1;
12765
+ this.nudgeReceiptLogState = { lastEmittedAt: now, count: 0 };
12766
+ log(`subc bg_events: nudge received channel=${routeId} count=${count}`, {
12767
+ sessionId: this.identity.session
12768
+ });
12769
+ }
12770
+ errorText(error2) {
12771
+ return error2 instanceof Error ? `${error2.name}: ${error2.message}` : String(error2);
12772
+ }
12773
+ startDispatchProbe(client, routeId) {
12774
+ const initial = client.droppedIngressFrames;
12775
+ if (typeof initial !== "number")
12776
+ return () => {
12777
+ return;
12778
+ };
12779
+ let previous = initial;
12780
+ const timer = setInterval(() => {
12781
+ const total = client.droppedIngressFrames;
12782
+ if (typeof total !== "number" || total <= previous)
12783
+ return;
12784
+ const delta = total - previous;
12785
+ previous = total;
12786
+ this.info("dispatch-epoch-drop", `client ingress epoch drops scope=client observed_while_channel=${routeId} delta=${delta} total=${total}`);
12787
+ }, this.dispatchProbeIntervalMs);
12788
+ timer.unref?.();
12789
+ return () => clearInterval(timer);
12790
+ }
12579
12791
  async run() {
12580
- let attempt = 0;
12792
+ let backoffAttempt = 0;
12793
+ let reconnectAttempt = 0;
12794
+ let reconnecting = false;
12795
+ const beginReconnect = () => {
12796
+ reconnecting = true;
12797
+ reconnectAttempt = reconnectAttempt === 0 ? 1 : reconnectAttempt + 1;
12798
+ };
12799
+ const giveUp = (reason) => {
12800
+ this.info("reconnect-gave-up", `reconnect gave-up attempt=${reconnectAttempt} reason=${reason}`);
12801
+ };
12581
12802
  while (!this.stopped) {
12582
- if (!this.isCurrent())
12803
+ if (!this.isCurrent()) {
12804
+ if (reconnecting)
12805
+ giveUp("stale-session");
12583
12806
  return;
12807
+ }
12584
12808
  if (!this.canAttach()) {
12809
+ if (reconnecting)
12810
+ giveUp("root-dormant");
12585
12811
  this.onDormant();
12586
12812
  return;
12587
12813
  }
12814
+ if (reconnecting) {
12815
+ this.info("reconnect-attempt", `reconnect attempt=${reconnectAttempt}`);
12816
+ }
12588
12817
  let client;
12589
12818
  try {
12590
12819
  client = await this.acquireClient();
12591
- } catch {
12592
- await this.backoff(attempt++);
12820
+ } catch (err) {
12821
+ if (!reconnecting)
12822
+ beginReconnect();
12823
+ this.info("reconnect-error", `reconnect error attempt=${reconnectAttempt} error=${this.errorText(err)}`);
12824
+ await this.backoff(backoffAttempt++);
12825
+ if (reconnecting)
12826
+ reconnectAttempt += 1;
12593
12827
  continue;
12594
12828
  }
12595
- if (this.stopped || !this.isCurrent())
12829
+ if (this.stopped) {
12830
+ if (reconnecting)
12831
+ giveUp("stopped");
12596
12832
  return;
12833
+ }
12834
+ if (!this.isCurrent()) {
12835
+ if (reconnecting)
12836
+ giveUp("stale-session");
12837
+ return;
12838
+ }
12597
12839
  if (!this.canAttach()) {
12840
+ if (reconnecting)
12841
+ giveUp("root-dormant");
12598
12842
  this.onDormant();
12599
12843
  return;
12600
12844
  }
@@ -12603,44 +12847,84 @@ class BgSubscription {
12603
12847
  route = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, this.identity, { consumerIdentity: this.consumerIdentity });
12604
12848
  } catch (err) {
12605
12849
  if (this.isCurrent() && this.onRootAttachFailure(err)) {
12850
+ if (reconnecting)
12851
+ giveUp("root-dormant");
12606
12852
  this.onDormant();
12607
12853
  return;
12608
12854
  }
12609
12855
  if (isConsumerReconnectTransient(err))
12610
12856
  this.dropClient(client);
12611
- await this.backoff(attempt++);
12857
+ if (!reconnecting)
12858
+ beginReconnect();
12859
+ this.info("reconnect-error", `reconnect error attempt=${reconnectAttempt} error=${this.errorText(err)}`);
12860
+ await this.backoff(backoffAttempt++);
12861
+ if (reconnecting)
12862
+ reconnectAttempt += 1;
12612
12863
  continue;
12613
12864
  }
12614
12865
  if (this.stopped || !this.isCurrent()) {
12615
12866
  safeCloseRoute(client, route);
12867
+ if (reconnecting)
12868
+ giveUp(this.stopped ? "stopped" : "stale-session");
12616
12869
  return;
12617
12870
  }
12618
12871
  const subscribedAt = Date.now();
12872
+ const routeId = this.routeId(route);
12873
+ let stopDispatchProbe = () => {
12874
+ return;
12875
+ };
12619
12876
  try {
12620
12877
  const sub = client.subscribe(route, { op: "bg_events" }, () => {
12621
- if (!this.stopped && this.isCurrent())
12622
- this.onNudge();
12878
+ if (this.stopped) {
12879
+ this.info("nudge-drop-stopped", `nudge dropped cause=subscription-stopped channel=${routeId}`);
12880
+ return;
12881
+ }
12882
+ this.recordNudgeReceipt(routeId);
12883
+ if (!this.isCurrent()) {
12884
+ this.info("nudge-stale-carrier", `nudge carried by stale subscription; checking current session channel=${routeId}`);
12885
+ }
12886
+ this.onNudge();
12623
12887
  });
12624
12888
  this.current = sub;
12889
+ stopDispatchProbe = this.startDispatchProbe(client, routeId);
12890
+ this.info("subscription-open", `subscription open channel=${routeId}`);
12891
+ if (reconnecting) {
12892
+ this.info("reconnect-success", `reconnect success attempt=${reconnectAttempt} channel=${routeId}`);
12893
+ reconnecting = false;
12894
+ reconnectAttempt = 0;
12895
+ }
12625
12896
  if (this.stopped)
12626
12897
  sub.unsubscribe();
12627
12898
  if (!this.stopped && this.isCurrent())
12628
12899
  this.onNudge();
12629
12900
  await sub.closed;
12630
- return;
12901
+ this.info("stream-end", `stream ended channel=${routeId}`);
12902
+ if (this.stopped) {
12903
+ giveUp("stopped");
12904
+ return;
12905
+ }
12906
+ beginReconnect();
12631
12907
  } catch (err) {
12632
- if (this.stopped)
12908
+ const routeId2 = this.routeId(route);
12909
+ this.info("stream-error", `stream error channel=${routeId2} error=${this.errorText(err)}`);
12910
+ if (this.stopped) {
12911
+ giveUp("stopped");
12633
12912
  return;
12913
+ }
12634
12914
  if (isConsumerReconnectTransient(err))
12635
12915
  this.dropClient(client);
12636
12916
  if (Date.now() - subscribedAt >= BG_STABLE_MS)
12637
- attempt = 0;
12917
+ backoffAttempt = 0;
12918
+ beginReconnect();
12638
12919
  } finally {
12920
+ stopDispatchProbe();
12639
12921
  this.current = null;
12640
12922
  safeCloseRoute(client, route);
12641
12923
  }
12642
- await this.backoff(attempt++);
12924
+ await this.backoff(backoffAttempt++);
12643
12925
  }
12926
+ if (reconnecting)
12927
+ giveUp("stopped");
12644
12928
  }
12645
12929
  async backoff(attempt) {
12646
12930
  const ms = Math.min(100 * 2 ** Math.min(attempt, 6), 2000);
@@ -12738,6 +13022,9 @@ class SubcTransport {
12738
13022
  this.assertCurrent();
12739
13023
  const { preview, timeoutMs, onProgress } = this.splitOptions(options);
12740
13024
  const body = { name, arguments: rawArgs };
13025
+ const editSlotSurvives = this.pool.getEditSlotSurvives();
13026
+ if (editSlotSurvives !== undefined)
13027
+ body.edit_slot_survives = editSlotSurvives;
12741
13028
  if (preview === true)
12742
13029
  body.preview = true;
12743
13030
  const reply = await this.pool.routeRequest(this.identityFor(sessionId), body, timeoutMs, onProgress, this.generation);
@@ -12752,7 +13039,11 @@ class SubcTransport {
12752
13039
  }
12753
13040
  const { timeoutMs, onProgress } = this.splitOptions(options);
12754
13041
  const session = typeof params.session_id === "string" ? params.session_id : undefined;
12755
- const reply = await this.pool.routeRequest(this.identityFor(session), { name: command, arguments: params }, timeoutMs, onProgress, this.generation);
13042
+ const body = { name: command, arguments: params };
13043
+ const editSlotSurvives = this.pool.getEditSlotSurvives();
13044
+ if (editSlotSurvives !== undefined)
13045
+ body.edit_slot_survives = editSlotSurvives;
13046
+ const reply = await this.pool.routeRequest(this.identityFor(session), body, timeoutMs, onProgress, this.generation);
12756
13047
  const response = reliftReply(reply);
12757
13048
  this.captureStatusBar(response);
12758
13049
  return response;
@@ -12776,6 +13067,7 @@ class SubcTransportPool {
12776
13067
  onBgEventsNudge;
12777
13068
  onBgEventsNudgeRef;
12778
13069
  bgBackoffSleep;
13070
+ bgDispatchProbeIntervalMs;
12779
13071
  lifecycleDemandCheck;
12780
13072
  onLifecycleEvent;
12781
13073
  onBgNudgeRejected;
@@ -12791,8 +13083,11 @@ class SubcTransportPool {
12791
13083
  transportFailures = 0;
12792
13084
  transports = new Map;
12793
13085
  generationRejections = new Set;
13086
+ nudgeDeliveryLogState = new Map;
12794
13087
  pendingRootCleanups = new Set;
12795
13088
  shuttingDown = false;
13089
+ editSlotSurvives;
13090
+ editSlotSurvivesCaptured = false;
12796
13091
  constructor(options) {
12797
13092
  this.connectionFile = options.connectionFile;
12798
13093
  this.harness = options.harness;
@@ -12802,6 +13097,7 @@ class SubcTransportPool {
12802
13097
  this.onBgEventsNudge = options.onBgEventsNudge;
12803
13098
  this.onBgEventsNudgeRef = options.onBgEventsNudgeRef;
12804
13099
  this.bgBackoffSleep = options.bgBackoffSleep ?? ((ms) => new Promise((resolve4) => setTimeout(resolve4, ms)));
13100
+ this.bgDispatchProbeIntervalMs = options.bgDispatchProbeIntervalMs ?? BG_DISPATCH_PROBE_INTERVAL_MS;
12805
13101
  const lifecycle = options.lifecycle;
12806
13102
  const demandCheck = options.lifecycleDemandCheck ?? options.demandCheck ?? lifecycle?.demandCheck;
12807
13103
  this.lifecycleDemandCheck = demandCheck;
@@ -13118,6 +13414,35 @@ class SubcTransportPool {
13118
13414
  isCurrentSession(key, record) {
13119
13415
  return this.sessions.get(key) === record && !record.closed;
13120
13416
  }
13417
+ currentSessionForNudge(identity) {
13418
+ const current = this.sessions.get(identityKey(identity));
13419
+ return current && !current.closed ? current : null;
13420
+ }
13421
+ nudgeRefFor(record) {
13422
+ const poolId = this.currentPoolId();
13423
+ const generation = record.generation;
13424
+ if (poolId === undefined || generation === undefined)
13425
+ return;
13426
+ return {
13427
+ canonicalRoot: record.canonicalRoot,
13428
+ session: record.identity.session,
13429
+ concretePoolId: poolId,
13430
+ generation
13431
+ };
13432
+ }
13433
+ logNudgeDelivery(kind, record, message) {
13434
+ const key = `${kind}\x00${record.identityKey}`;
13435
+ const now = Date.now();
13436
+ const state = this.nudgeDeliveryLogState.get(key);
13437
+ if (state && now - state.lastEmittedAt < BG_LIFECYCLE_LOG_INTERVAL_MS) {
13438
+ state.suppressed += 1;
13439
+ return;
13440
+ }
13441
+ const suppressed = state?.suppressed ?? 0;
13442
+ this.nudgeDeliveryLogState.set(key, { lastEmittedAt: now, suppressed: 0 });
13443
+ const suffix = suppressed > 0 ? ` suppressed=${suppressed}` : "";
13444
+ log(`subc bg_events: ${message}${suffix}`, { sessionId: record.identity.session });
13445
+ }
13121
13446
  removeIndexMembership(record) {
13122
13447
  const keys = this.rootIndex.get(record.canonicalRoot);
13123
13448
  if (!keys)
@@ -13371,20 +13696,32 @@ class SubcTransportPool {
13371
13696
  return;
13372
13697
  if (record.bgSub)
13373
13698
  return;
13374
- const poolId = this.currentPoolId();
13375
- const generation = record.generation;
13376
- const nudgeRef = poolId !== undefined && generation !== undefined ? {
13377
- canonicalRoot: record.canonicalRoot,
13378
- session: identity.session,
13379
- concretePoolId: poolId,
13380
- generation
13381
- } : undefined;
13699
+ const nudgeRef = this.nudgeRefFor(record);
13382
13700
  const onNudge = () => {
13383
- if (!this.isCurrentSession(record.identityKey, record))
13701
+ const currentRecord = this.currentSessionForNudge(identity);
13702
+ if (!currentRecord) {
13703
+ this.logNudgeDelivery("drop-no-current-session", record, `nudge dropped cause=no-current-session root=${record.canonicalRoot}`);
13704
+ return;
13705
+ }
13706
+ if (currentRecord !== record) {
13707
+ this.logNudgeDelivery("forward-superseded-carrier", currentRecord, `nudge forwarding cause=superseded-carrying-record root=${currentRecord.canonicalRoot}`);
13708
+ }
13709
+ const currentRef = this.nudgeRefFor(currentRecord);
13710
+ let delivered = false;
13711
+ if (currentRef && this.onBgEventsNudgeRef) {
13712
+ this.onBgEventsNudgeRef(currentRef);
13713
+ delivered = true;
13714
+ }
13715
+ if (this.onBgEventsNudge) {
13716
+ if (!currentRef && this.onBgEventsNudgeRef) {
13717
+ this.logNudgeDelivery("fallback-missing-generation", currentRecord, `nudge dispatch fallback=root-session-handler cause=generation-provenance-unavailable root=${currentRecord.canonicalRoot}`);
13718
+ }
13719
+ this.onBgEventsNudge(currentRecord.identity.project_root, currentRecord.identity.session);
13720
+ delivered = true;
13721
+ }
13722
+ if (delivered)
13384
13723
  return;
13385
- this.onBgEventsNudge?.(identity.project_root, identity.session);
13386
- if (nudgeRef)
13387
- this.onBgEventsNudgeRef?.(nudgeRef);
13724
+ this.logNudgeDelivery("drop-no-compatible-handler", currentRecord, `nudge dropped cause=generation-provenance-unavailable-and-root-session-handler-unwired root=${currentRecord.canonicalRoot}`);
13388
13725
  };
13389
13726
  let sub = null;
13390
13727
  const clearDormantSubscription = () => {
@@ -13396,7 +13733,7 @@ class SubcTransportPool {
13396
13733
  return false;
13397
13734
  this.markRootDormant(record.canonicalRoot, error2);
13398
13735
  return true;
13399
- }, clearDormantSubscription, nudgeRef, () => this.isCurrentSession(record.identityKey, record) && this.isCurrentLiveGeneration(record.canonicalRoot, record.generation));
13736
+ }, clearDormantSubscription, this.bgDispatchProbeIntervalMs, nudgeRef, () => this.isCurrentSession(record.identityKey, record) && this.isCurrentLiveGeneration(record.canonicalRoot, record.generation));
13400
13737
  record.bgSub = sub;
13401
13738
  if (!this.rootCanAttach(record.canonicalRoot)) {
13402
13739
  record.bgSub = null;
@@ -13458,7 +13795,21 @@ class SubcTransportPool {
13458
13795
  return Promise.resolve();
13459
13796
  return registry.requestProjectRootClose(registration.concretePoolId, root, generation, cause);
13460
13797
  }
13461
- setConfigureOverride(_key, _value) {}
13798
+ setConfigureOverride(key, value) {
13799
+ if (key !== "edit_slot_survives")
13800
+ return;
13801
+ if (typeof value !== "boolean") {
13802
+ throw new Error("edit_slot_survives must be set once to a boolean");
13803
+ }
13804
+ if (this.editSlotSurvivesCaptured) {
13805
+ throw new Error("edit_slot_survives is write-once and was already captured");
13806
+ }
13807
+ this.editSlotSurvives = value;
13808
+ this.editSlotSurvivesCaptured = true;
13809
+ }
13810
+ getEditSlotSurvives() {
13811
+ return this.editSlotSurvives;
13812
+ }
13462
13813
  async reconfigure(_projectRoot, _overrides) {}
13463
13814
  async replaceBinary(path2) {
13464
13815
  return path2;
@@ -13536,14 +13887,29 @@ function toolErrorFromResponse(command, response) {
13536
13887
  return new AftToolError(message, code, response);
13537
13888
  }
13538
13889
  var BASH_TRANSPORT_DISPOSITION = "The transport to the AFT daemon was interrupted; no background task was created for this command and no task ID exists. Re-run the command. Do not poll bash_status for it.";
13890
+ 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.";
13891
+ function isRouteGoodbyeError(error2) {
13892
+ if (!(error2 instanceof SubcError))
13893
+ return false;
13894
+ if (error2.code === undefined) {
13895
+ return error2.message.includes("route closed by subc");
13896
+ }
13897
+ return error2.code === "route_closed" && error2.message.includes("route closed by subc");
13898
+ }
13539
13899
  function isTransportClassError(error2) {
13540
13900
  return isBridgeTransportTimeout(error2) || isConsumerReconnectTransient(error2) || error2 instanceof StaleRouteHandleError || error2 instanceof SubcRootGenerationExpiredError || error2 instanceof SubcRootReapedError;
13541
13901
  }
13542
13902
  function adaptToolError(command, error2) {
13543
- if (command !== "bash" || !isTransportClassError(error2))
13544
- return error2;
13545
13903
  if (!(error2 instanceof Error))
13546
13904
  return error2;
13905
+ if (isRouteGoodbyeError(error2)) {
13906
+ if (error2.message.includes(SUBC_MODULE_RESTART_DISPOSITION))
13907
+ return error2;
13908
+ error2.message = error2.message ? `${error2.message} ${SUBC_MODULE_RESTART_DISPOSITION}` : SUBC_MODULE_RESTART_DISPOSITION;
13909
+ return error2;
13910
+ }
13911
+ if (command !== "bash" || !isTransportClassError(error2))
13912
+ return error2;
13547
13913
  if (error2.message.includes(BASH_TRANSPORT_DISPOSITION))
13548
13914
  return error2;
13549
13915
  error2.message = error2.message ? `${error2.message} ${BASH_TRANSPORT_DISPOSITION}` : BASH_TRANSPORT_DISPOSITION;
@@ -14343,7 +14709,7 @@ async function ensureStorageMigrated(opts) {
14343
14709
  throw new Error(`AFT storage migration failed (${detail}). ` + `Harness: ${opts.harness}. Legacy: ${legacyRoot}. Target: ${newRoot}. ` + `See log: ${logPath}. ` + `Plugin load aborted to prevent legacy/new state divergence.` + (stderrTail ? ` Stderr tail: ${stderrTail}` : "") + (stdoutTail ? ` Stdout tail: ${stdoutTail}` : ""));
14344
14710
  }
14345
14711
  // ../aft-bridge/dist/npm-resolver.js
14346
- import { readdirSync, statSync as statSync5 } from "node:fs";
14712
+ import { readdirSync as readdirSync2, statSync as statSync5 } from "node:fs";
14347
14713
  import { homedir as homedir8 } from "node:os";
14348
14714
  import { delimiter, dirname as dirname4, isAbsolute as isAbsolute3, join as join9 } from "node:path";
14349
14715
  function defaultDeps() {
@@ -14383,7 +14749,7 @@ function npmAdjacentToNode(deps) {
14383
14749
  function highestVersionedNodeBin(installsDir, name) {
14384
14750
  let entries;
14385
14751
  try {
14386
- entries = readdirSync(installsDir);
14752
+ entries = readdirSync2(installsDir);
14387
14753
  } catch {
14388
14754
  return null;
14389
14755
  }
@@ -14462,7 +14828,7 @@ function isNpmAvailable(deps = defaultDeps()) {
14462
14828
  // ../aft-bridge/dist/onnx-runtime.js
14463
14829
  import { execFileSync } from "node:child_process";
14464
14830
  import { createHash as createHash4 } from "node:crypto";
14465
- import { chmodSync as chmodSync3, closeSync as closeSync4, copyFileSync as copyFileSync3, createWriteStream as createWriteStream2, existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync5, openSync as openSync4, readdirSync as readdirSync2, readFileSync as readFileSync6, readlinkSync, realpathSync as realpathSync2, rmSync as rmSync3, statSync as statSync6, symlinkSync, unlinkSync as unlinkSync4, writeFileSync as writeFileSync3 } from "node:fs";
14831
+ import { chmodSync as chmodSync3, closeSync as closeSync4, copyFileSync as copyFileSync3, createWriteStream as createWriteStream2, existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync5, openSync as openSync4, readdirSync as readdirSync3, readFileSync as readFileSync6, readlinkSync, realpathSync as realpathSync2, rmSync as rmSync3, statSync as statSync6, symlinkSync, unlinkSync as unlinkSync4, writeFileSync as writeFileSync3 } from "node:fs";
14466
14832
  import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute4, join as join10, relative as relative2, resolve as resolve6, win32 } from "node:path";
14467
14833
  import { Readable as Readable2 } from "node:stream";
14468
14834
  import { pipeline as pipeline2 } from "node:stream/promises";
@@ -14579,7 +14945,7 @@ async function ensureOnnxRuntime(storageDir) {
14579
14945
  }
14580
14946
  function cleanupAbandonedStagingDirs(onnxBaseDir) {
14581
14947
  try {
14582
- const entries = readdirSync2(onnxBaseDir);
14948
+ const entries = readdirSync3(onnxBaseDir);
14583
14949
  for (const entry of entries) {
14584
14950
  if (!entry.startsWith(`${ORT_VERSION}.tmp.`))
14585
14951
  continue;
@@ -14590,7 +14956,7 @@ function cleanupAbandonedStagingDirs(onnxBaseDir) {
14590
14956
  let abandoned = false;
14591
14957
  if (Number.isFinite(pid) && pid > 0) {
14592
14958
  if (process.platform === "win32") {
14593
- const ownerAlive = isProcessAlive(pid);
14959
+ const ownerAlive = isProcessAlive2(pid);
14594
14960
  if (!ownerAlive) {
14595
14961
  abandoned = true;
14596
14962
  } else {
@@ -14602,7 +14968,7 @@ function cleanupAbandonedStagingDirs(onnxBaseDir) {
14602
14968
  }
14603
14969
  }
14604
14970
  } else {
14605
- abandoned = !isProcessAlive(pid);
14971
+ abandoned = !isProcessAlive2(pid);
14606
14972
  }
14607
14973
  } else {
14608
14974
  abandoned = true;
@@ -14653,7 +15019,7 @@ function isPathInsideRoot(root, candidate) {
14653
15019
  }
14654
15020
  function detectOnnxVersion(libDir, libName) {
14655
15021
  try {
14656
- const entries = readdirSync2(libDir);
15022
+ const entries = readdirSync3(libDir);
14657
15023
  const barePrefix = libName.replace(/\.(so|dylib|dll)$/, "");
14658
15024
  const expectedPrefix = process.platform === "win32" ? barePrefix.toLowerCase() : barePrefix;
14659
15025
  for (const entry of entries) {
@@ -14716,7 +15082,7 @@ function isWindowsSystem32Directory(dir) {
14716
15082
  }
14717
15083
  function directoryContainsLibrary(dir, libName) {
14718
15084
  try {
14719
- const entries = readdirSync2(dir);
15085
+ const entries = readdirSync3(dir);
14720
15086
  if (process.platform === "win32") {
14721
15087
  const expected = libName.toLowerCase();
14722
15088
  return entries.some((entry) => entry.toLowerCase() === expected);
@@ -14754,7 +15120,7 @@ function findSystemOnnxRuntime(libName) {
14754
15120
  if (!existsSync7(nugetPackageDir))
14755
15121
  return nugetPaths;
14756
15122
  try {
14757
- for (const entry of readdirSync2(nugetPackageDir, { withFileTypes: true })) {
15123
+ for (const entry of readdirSync3(nugetPackageDir, { withFileTypes: true })) {
14758
15124
  if (!entry.isDirectory())
14759
15125
  continue;
14760
15126
  if (entry.name === "__globalPackagesFolder" || entry.name.startsWith("."))
@@ -14849,7 +15215,7 @@ function validateExtractedTree(stagingRoot) {
14849
15215
  const realRoot = realpathSync2(stagingRoot);
14850
15216
  let totalBytes = 0;
14851
15217
  const walk = (dir) => {
14852
- const entries = readdirSync2(dir);
15218
+ const entries = readdirSync3(dir);
14853
15219
  for (const entry of entries) {
14854
15220
  const fullPath = join10(dir, entry);
14855
15221
  const lst = lstatSync(fullPath);
@@ -14907,7 +15273,7 @@ async function downloadOnnxRuntime(info, targetDir) {
14907
15273
  throw new Error(`Expected directory not found: ${extractedDir}`);
14908
15274
  }
14909
15275
  mkdirSync5(targetDir, { recursive: true });
14910
- const libFiles = readdirSync2(extractedDir).filter((f) => f.startsWith("libonnxruntime") || f.startsWith("onnxruntime"));
15276
+ const libFiles = readdirSync3(extractedDir).filter((f) => f.startsWith("libonnxruntime") || f.startsWith("onnxruntime"));
14911
15277
  const realFiles = [];
14912
15278
  const symlinks = [];
14913
15279
  for (const libFile of libFiles) {
@@ -15085,7 +15451,7 @@ ${new Date().toISOString()}
15085
15451
  }
15086
15452
  const age = Date.now() - lockMtimeMs;
15087
15453
  const ageWithinFresh = Math.abs(age) < STALE_LOCK_MS;
15088
- const ownerAlive = owningPid !== null && isProcessAlive(owningPid);
15454
+ const ownerAlive = owningPid !== null && isProcessAlive2(owningPid);
15089
15455
  if (ownerAlive && ageWithinFresh) {
15090
15456
  return false;
15091
15457
  }
@@ -15149,7 +15515,7 @@ function isWindowsProcessAlive(pid) {
15149
15515
  return false;
15150
15516
  }
15151
15517
  }
15152
- function isProcessAlive(pid) {
15518
+ function isProcessAlive2(pid) {
15153
15519
  if (process.platform === "win32")
15154
15520
  return isWindowsProcessAlive(pid);
15155
15521
  try {
@@ -15524,6 +15890,19 @@ function parseEditArray(value) {
15524
15890
  }
15525
15891
  return value;
15526
15892
  }
15893
+ function stripLineRangeSentinels(item) {
15894
+ const hasRangeField = ["startLine", "endLine", "content"].some((key) => hasOwn(item, key));
15895
+ if (!hasRangeField)
15896
+ return;
15897
+ if (item.oldString === "")
15898
+ delete item.oldString;
15899
+ if (item.newString === "")
15900
+ delete item.newString;
15901
+ if (item.replaceAll === false)
15902
+ delete item.replaceAll;
15903
+ if (item.occurrence === 1)
15904
+ delete item.occurrence;
15905
+ }
15527
15906
  function normalizeEditItem(value, index) {
15528
15907
  if (!value || typeof value !== "object" || Array.isArray(value)) {
15529
15908
  throw new InvalidRequestError(`edit: edits[${index}] must be an object`);
@@ -15532,6 +15911,7 @@ function normalizeEditItem(value, index) {
15532
15911
  const item = copyOwnProperties(source);
15533
15912
  normalizeItemAlias(item, "oldString", "oldText");
15534
15913
  normalizeItemAlias(item, "newString", "newText");
15914
+ stripLineRangeSentinels(item);
15535
15915
  const hasFindField = ["oldString", "newString", "replaceAll", "occurrence"].some((key) => hasOwn(item, key));
15536
15916
  const hasRangeField = ["startLine", "endLine", "content"].some((key) => hasOwn(item, key));
15537
15917
  if (hasFindField && hasRangeField) {
@@ -15656,6 +16036,8 @@ class BridgePool {
15656
16036
  idleTimeoutMs;
15657
16037
  bridgeOptions;
15658
16038
  configOverrides;
16039
+ editSlotSurvives;
16040
+ editSlotSurvivesCaptured = false;
15659
16041
  projectConfigLoader;
15660
16042
  logger;
15661
16043
  cleanupTimer = null;
@@ -15680,7 +16062,16 @@ class BridgePool {
15680
16062
  logger: options.logger,
15681
16063
  childEnv: options.childEnv
15682
16064
  };
15683
- this.configOverrides = configOverrides;
16065
+ this.configOverrides = { ...configOverrides };
16066
+ const initialEditSlotSurvives = this.configOverrides.edit_slot_survives;
16067
+ delete this.configOverrides.edit_slot_survives;
16068
+ if (initialEditSlotSurvives !== undefined) {
16069
+ if (typeof initialEditSlotSurvives !== "boolean") {
16070
+ throw new Error("edit_slot_survives must be a boolean");
16071
+ }
16072
+ this.editSlotSurvives = initialEditSlotSurvives;
16073
+ this.editSlotSurvivesCaptured = true;
16074
+ }
15684
16075
  this.startCleanupTimer();
15685
16076
  }
15686
16077
  getActiveBridgeForRoot(projectRoot) {
@@ -15719,7 +16110,8 @@ class BridgePool {
15719
16110
  }
15720
16111
  const projectOverrides = this.loadProjectOverrides(key);
15721
16112
  const mergedOverrides = { ...this.configOverrides, ...projectOverrides };
15722
- const bridge = new BinaryBridge(this.binaryPath, key, this.bridgeOptions, mergedOverrides);
16113
+ delete mergedOverrides.edit_slot_survives;
16114
+ const bridge = new BinaryBridge(this.binaryPath, key, this.bridgeOptions, mergedOverrides, this.editSlotSurvivesCaptured ? this.editSlotSurvives : undefined);
15723
16115
  this.bridges.set(key, { bridge, lastUsed: Date.now() });
15724
16116
  return bridge;
15725
16117
  }
@@ -15823,6 +16215,23 @@ class BridgePool {
15823
16215
  error(message, meta);
15824
16216
  }
15825
16217
  setConfigureOverride(key, value) {
16218
+ if (key === "edit_slot_survives") {
16219
+ if (typeof value !== "boolean") {
16220
+ throw new Error("edit_slot_survives must be set once to a boolean");
16221
+ }
16222
+ if (this.editSlotSurvivesCaptured) {
16223
+ throw new Error("edit_slot_survives is write-once and was already captured");
16224
+ }
16225
+ this.editSlotSurvives = value;
16226
+ this.editSlotSurvivesCaptured = true;
16227
+ for (const entry of this.bridges.values()) {
16228
+ entry.bridge.setEditSlotSurvives(value);
16229
+ }
16230
+ for (const bridge of this.staleBridges) {
16231
+ bridge.setEditSlotSurvives(value);
16232
+ }
16233
+ return;
16234
+ }
15826
16235
  if (value === undefined) {
15827
16236
  delete this.configOverrides[key];
15828
16237
  } else {
@@ -15880,6 +16289,7 @@ class RevivableTransportPool {
15880
16289
  revival = null;
15881
16290
  transports = new Map;
15882
16291
  configureOverrides = new Map;
16292
+ editSlotSurvivesCaptured = false;
15883
16293
  constructor(initialPool, createPool, onBinaryReplaced) {
15884
16294
  this.createPool = createPool;
15885
16295
  this.onBinaryReplaced = onBinaryReplaced;
@@ -15910,6 +16320,18 @@ class RevivableTransportPool {
15910
16320
  return this.getBridge(projectRoot).toolCall(runtime.sessionID, name, rawArgs, options);
15911
16321
  }
15912
16322
  setConfigureOverride(key, value) {
16323
+ if (key === "edit_slot_survives") {
16324
+ if (typeof value !== "boolean") {
16325
+ throw new Error("edit_slot_survives must be set once to a boolean");
16326
+ }
16327
+ if (this.editSlotSurvivesCaptured) {
16328
+ throw new Error("edit_slot_survives is write-once and was already captured");
16329
+ }
16330
+ this.activePool.setConfigureOverride(key, value);
16331
+ this.editSlotSurvivesCaptured = true;
16332
+ this.configureOverrides.set(key, value);
16333
+ return;
16334
+ }
15913
16335
  if (value === undefined)
15914
16336
  this.configureOverrides.delete(key);
15915
16337
  else
@@ -15917,14 +16339,20 @@ class RevivableTransportPool {
15917
16339
  this.activePool.setConfigureOverride(key, value);
15918
16340
  }
15919
16341
  async reconfigure(projectRoot, overrides) {
16342
+ const pool = await this.ensureActivePool();
16343
+ const runtimeOverrides = {};
15920
16344
  for (const [key, value] of Object.entries(overrides)) {
16345
+ if (key === "edit_slot_survives") {
16346
+ this.setConfigureOverride(key, value);
16347
+ continue;
16348
+ }
15921
16349
  if (value === undefined)
15922
16350
  this.configureOverrides.delete(key);
15923
16351
  else
15924
16352
  this.configureOverrides.set(key, value);
16353
+ runtimeOverrides[key] = value;
15925
16354
  }
15926
- const pool = await this.ensureActivePool();
15927
- await pool.reconfigure(projectRoot, overrides);
16355
+ await pool.reconfigure(projectRoot, runtimeOverrides);
15928
16356
  }
15929
16357
  async replaceBinary(path2) {
15930
16358
  const replaced = await this.activePool.replaceBinary(path2);
@@ -16336,6 +16764,9 @@ var UNKNOWN_COMPLETION_TTL_MS = 5000;
16336
16764
  var UNKNOWN_COMPLETION_CAP = 32;
16337
16765
  var DEFAULT_SESSION_ID2 = "__default__";
16338
16766
  var LOG_PREFIX = "[aft-plugin] bg-notifications:";
16767
+ var SUBC_NUDGE_LOG_INTERVAL_MS = 60000;
16768
+ var subcNudgesInFlight = new Map;
16769
+ var subcNudgeLogState = new Map;
16339
16770
  function consumeBgCompletion(sessionID, taskId) {
16340
16771
  const state = stateFor(sessionID);
16341
16772
  state.pendingCompletions = state.pendingCompletions.filter((c) => c.task_id !== taskId);
@@ -16542,11 +16973,45 @@ async function handleIdleBgCompletions(drainContext) {
16542
16973
  stateFor(drainContext.sessionID).wakeDeferredTaskIds.clear();
16543
16974
  await triggerWakeIfPending(drainContext, false, true);
16544
16975
  }
16545
- async function handleSubcBgEventsNudge(drainContext) {
16976
+ function handleSubcBgEventsNudge(drainContext) {
16977
+ const key = `${drainContext.directory}\x00${drainContext.sessionID}`;
16978
+ const inFlight = subcNudgesInFlight.get(key);
16979
+ if (inFlight) {
16980
+ logSubcNudgeLifecycle(drainContext, "coalesced-in-flight", "nudge coalesced cause=handler-already-in-flight");
16981
+ return inFlight;
16982
+ }
16983
+ logSubcNudgeLifecycle(drainContext, "handler-entry", "nudge handler entered");
16984
+ const handling = handleSubcBgEventsNudgeOnce(drainContext);
16985
+ subcNudgesInFlight.set(key, handling);
16986
+ const clear = () => {
16987
+ if (subcNudgesInFlight.get(key) === handling)
16988
+ subcNudgesInFlight.delete(key);
16989
+ };
16990
+ handling.then(clear, clear);
16991
+ return handling;
16992
+ }
16993
+ async function handleSubcBgEventsNudgeOnce(drainContext) {
16546
16994
  bridgeForDrain(drainContext);
16547
16995
  stateFor(drainContext.sessionID).wakeDeferredTaskIds.clear();
16548
16996
  await triggerWakeIfPending(drainContext, false, true, true);
16549
16997
  }
16998
+ function logSubcNudgeLifecycle(drainContext, kind, message) {
16999
+ const key = `${kind}\x00${drainContext.directory}\x00${drainContext.sessionID}`;
17000
+ const now = Date.now();
17001
+ const state = subcNudgeLogState.get(key);
17002
+ if (state && now - state.lastEmittedAt < SUBC_NUDGE_LOG_INTERVAL_MS) {
17003
+ state.suppressed += 1;
17004
+ return;
17005
+ }
17006
+ const suppressed = state?.suppressed ?? 0;
17007
+ subcNudgeLogState.set(key, { lastEmittedAt: now, suppressed: 0 });
17008
+ sessionLog(drainContext.sessionID, `${LOG_PREFIX} ${message}`, {
17009
+ event: "subc_bg_nudge_delivery",
17010
+ cause: kind,
17011
+ canonical_root: drainContext.directory,
17012
+ suppressed
17013
+ });
17014
+ }
16550
17015
  async function triggerWakeIfPending(drainContext, skipDrain, includeDeferredCompletions = true, forceDrain = false) {
16551
17016
  const state = stateFor(drainContext.sessionID);
16552
17017
  if (!skipDrain && (forceDrain || state.outstandingTaskIds.size > 0 || !state.forcedDrainCompleted)) {
@@ -17412,6 +17877,7 @@ async function signalBashWaitDetachForProject(pool, projectRoot, sessionID) {
17412
17877
 
17413
17878
  // src/config.ts
17414
17879
  import { existsSync as existsSync10, readFileSync as readFileSync7, renameSync as renameSync5, unlinkSync as unlinkSync5, writeFileSync as writeFileSync4 } from "node:fs";
17880
+ import { parse as parsePath, resolve as resolvePath } from "node:path";
17415
17881
  var import_comment_json = __toESM(require_src2(), 1);
17416
17882
 
17417
17883
  // ../../node_modules/.bun/zod@4.4.3/node_modules/zod/v4/classic/external.js
@@ -17524,7 +17990,7 @@ __export(exports_external, {
17524
17990
  instanceof: () => _instanceof,
17525
17991
  includes: () => _includes,
17526
17992
  httpUrl: () => httpUrl,
17527
- hostname: () => hostname2,
17993
+ hostname: () => hostname3,
17528
17994
  hex: () => hex2,
17529
17995
  hash: () => hash,
17530
17996
  guid: () => guid2,
@@ -18975,7 +19441,7 @@ __export(exports_regexes, {
18975
19441
  idnEmail: () => idnEmail,
18976
19442
  httpProtocol: () => httpProtocol,
18977
19443
  html5Email: () => html5Email,
18978
- hostname: () => hostname,
19444
+ hostname: () => hostname2,
18979
19445
  hex: () => hex,
18980
19446
  guid: () => guid,
18981
19447
  extendedDuration: () => extendedDuration,
@@ -19033,7 +19499,7 @@ var cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]
19033
19499
  var cidrv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/;
19034
19500
  var base64 = /^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$/;
19035
19501
  var base64url = /^[A-Za-z0-9_-]*$/;
19036
- var hostname = /^(?=.{1,253}\.?$)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[-0-9a-zA-Z]{0,61}[0-9a-zA-Z])?)*\.?$/;
19502
+ var hostname2 = /^(?=.{1,253}\.?$)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[-0-9a-zA-Z]{0,61}[0-9a-zA-Z])?)*\.?$/;
19037
19503
  var domain = /^([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
19038
19504
  var httpProtocol = /^https?$/;
19039
19505
  var e164 = /^\+[1-9]\d{6,14}$/;
@@ -29657,7 +30123,7 @@ __export(exports_schemas2, {
29657
30123
  int: () => int,
29658
30124
  instanceof: () => _instanceof,
29659
30125
  httpUrl: () => httpUrl,
29660
- hostname: () => hostname2,
30126
+ hostname: () => hostname3,
29661
30127
  hex: () => hex2,
29662
30128
  hash: () => hash,
29663
30129
  guid: () => guid2,
@@ -30309,7 +30775,7 @@ var ZodCustomStringFormat = /* @__PURE__ */ $constructor("ZodCustomStringFormat"
30309
30775
  function stringFormat(format, fnOrRegex, _params = {}) {
30310
30776
  return _stringFormat(ZodCustomStringFormat, format, fnOrRegex, _params);
30311
30777
  }
30312
- function hostname2(_params) {
30778
+ function hostname3(_params) {
30313
30779
  return _stringFormat(ZodCustomStringFormat, "hostname", exports_regexes.hostname, _params);
30314
30780
  }
30315
30781
  function hex2(_params) {
@@ -31798,6 +32264,7 @@ var BackupConfigSchema = exports_external.object({
31798
32264
  var AftConfigSchema = exports_external.preprocess((value) => stripHarnessSpecificConfigKeys(value, PI_ONLY_KEYS), exports_external.object({
31799
32265
  $schema: exports_external.string().optional(),
31800
32266
  enabled: exports_external.boolean().optional(),
32267
+ edit_mode: exports_external.enum(["default", "hashline"]).optional(),
31801
32268
  format_on_edit: exports_external.boolean().optional(),
31802
32269
  formatter_timeout_secs: exports_external.number().int().min(1).max(600).optional(),
31803
32270
  validate_on_edit: exports_external.enum(["syntax", "full"]).optional(),
@@ -32035,14 +32502,19 @@ ${serialized}` : serialized;
32035
32502
  }
32036
32503
  }
32037
32504
  function parseConfigPartially(rawConfig) {
32038
- const fullResult = AftConfigSchema.safeParse(rawConfig);
32505
+ let configForParsing = rawConfig;
32506
+ if (Object.hasOwn(rawConfig, "edit_mode") && rawConfig.edit_mode !== "default" && rawConfig.edit_mode !== "hashline") {
32507
+ warn2(`Unknown edit_mode value ${JSON.stringify(rawConfig.edit_mode)}; falling back to "default"`);
32508
+ configForParsing = { ...rawConfig, edit_mode: "default" };
32509
+ }
32510
+ const fullResult = AftConfigSchema.safeParse(configForParsing);
32039
32511
  if (fullResult.success) {
32040
32512
  return fullResult.data;
32041
32513
  }
32042
32514
  const partialConfig = {};
32043
32515
  const invalidSections = [];
32044
- for (const key of Object.keys(rawConfig)) {
32045
- const sectionResult = AftConfigSchema.safeParse({ [key]: rawConfig[key] });
32516
+ for (const key of Object.keys(configForParsing)) {
32517
+ const sectionResult = AftConfigSchema.safeParse({ [key]: configForParsing[key] });
32046
32518
  if (sectionResult.success) {
32047
32519
  const parsed = sectionResult.data;
32048
32520
  if (parsed[key] !== undefined) {
@@ -32211,6 +32683,7 @@ function getProjectLspStrippedKeys(lsp) {
32211
32683
  }
32212
32684
  var PROJECT_SAFE_TOP_LEVEL_FIELDS = new Set([
32213
32685
  "enabled",
32686
+ "edit_mode",
32214
32687
  "tool_surface",
32215
32688
  "hoist_builtin_tools",
32216
32689
  "format_on_edit",
@@ -32326,6 +32799,12 @@ function resolveAftConfigPaths(projectDirectory) {
32326
32799
  migrateAftConfigFile2(paths.projectConfigPath);
32327
32800
  return paths;
32328
32801
  }
32802
+ function resolveOpenCodeRegistrationRoot(directory, worktree) {
32803
+ if (!worktree)
32804
+ return directory;
32805
+ const resolvedWorktree = resolvePath(worktree);
32806
+ return parsePath(resolvedWorktree).root === resolvedWorktree ? directory : resolvedWorktree;
32807
+ }
32329
32808
  function buildConfigTierConfigureParams(projectDirectory, processState = {}) {
32330
32809
  const paths = resolveAftConfigPaths(projectDirectory);
32331
32810
  const initialPluginState = Object.keys(processState).length > 0;
@@ -33784,7 +34263,7 @@ ${new Date().toISOString()}
33784
34263
  const age = Date.now() - lockMtimeMs;
33785
34264
  const ageWithinFresh = Math.abs(age) < STALE_LOCK_MS2;
33786
34265
  const skipLiveness = process.platform === "win32";
33787
- const ownerAlive = !skipLiveness && owningPid !== null && isProcessAlive2(owningPid);
34266
+ const ownerAlive = !skipLiveness && owningPid !== null && isProcessAlive3(owningPid);
33788
34267
  if (skipLiveness ? ageWithinFresh : ownerAlive && ageWithinFresh) {
33789
34268
  return false;
33790
34269
  }
@@ -33794,7 +34273,7 @@ ${new Date().toISOString()}
33794
34273
  } catch {}
33795
34274
  return tryClaim();
33796
34275
  }
33797
- function isProcessAlive2(pid) {
34276
+ function isProcessAlive3(pid) {
33798
34277
  try {
33799
34278
  process.kill(pid, 0);
33800
34279
  return true;
@@ -34028,7 +34507,7 @@ var NPM_LSP_TABLE = [
34028
34507
  ];
34029
34508
 
34030
34509
  // src/lsp-project-relevance.ts
34031
- import { existsSync as existsSync15, readdirSync as readdirSync3, readFileSync as readFileSync13 } from "node:fs";
34510
+ import { existsSync as existsSync15, readdirSync as readdirSync4, readFileSync as readFileSync13 } from "node:fs";
34032
34511
  import { join as join17 } from "node:path";
34033
34512
  var MAX_WALK_DIRS = 200;
34034
34513
  var MAX_WALK_DEPTH = 4;
@@ -34093,7 +34572,7 @@ function relevantExtensionsInProject(projectRoot, extToServer) {
34093
34572
  visitedDirs += 1;
34094
34573
  let entries;
34095
34574
  try {
34096
- entries = readdirSync3(current.dir, { withFileTypes: true });
34575
+ entries = readdirSync4(current.dir, { withFileTypes: true });
34097
34576
  } catch {
34098
34577
  continue;
34099
34578
  }
@@ -34514,7 +34993,7 @@ import {
34514
34993
  existsSync as existsSync17,
34515
34994
  lstatSync as lstatSync2,
34516
34995
  mkdirSync as mkdirSync9,
34517
- readdirSync as readdirSync4,
34996
+ readdirSync as readdirSync5,
34518
34997
  readFileSync as readFileSync15,
34519
34998
  readlinkSync as readlinkSync2,
34520
34999
  realpathSync as realpathSync3,
@@ -34917,7 +35396,7 @@ function validateExtraction(stagingRoot) {
34917
35396
  const walk = (dir) => {
34918
35397
  let entries;
34919
35398
  try {
34920
- entries = readdirSync4(dir);
35399
+ entries = readdirSync5(dir);
34921
35400
  } catch (err) {
34922
35401
  throw new Error(`failed to read staging dir ${dir}: ${err}`);
34923
35402
  }
@@ -35359,14 +35838,18 @@ function normalizeToolArgSchemas(toolDefinition) {
35359
35838
  function bareToolName2(toolName) {
35360
35839
  return toolName.startsWith("aft_") ? toolName.slice(4) : toolName;
35361
35840
  }
35362
- function prepareOpenCodeArguments(toolName, rawArguments) {
35841
+ function prepareOpenCodeArguments(toolName, rawArguments, preparation = {}) {
35363
35842
  const bare = bareToolName2(toolName);
35843
+ if (bare === "edit" && preparation.hashlineEffective === true) {
35844
+ return rawArguments;
35845
+ }
35364
35846
  if (bare === "edit") {
35365
35847
  return prepareCanonicalEditArguments(toolName, rawArguments);
35366
35848
  }
35367
35849
  return prepareCanonicalPathArguments(toolName, rawArguments);
35368
35850
  }
35369
35851
  var DISPLAY_FILE_PATH_TOOLS = new Set(["read", "write", "edit"]);
35852
+ var PREPARED_EXECUTORS = new WeakSet;
35370
35853
  function preserveDisplayFilePathAlias(toolName, rawArguments, prepared) {
35371
35854
  if (!DISPLAY_FILE_PATH_TOOLS.has(toolName))
35372
35855
  return;
@@ -35377,21 +35860,25 @@ function preserveDisplayFilePathAlias(toolName, rawArguments, prepared) {
35377
35860
  raw.filePath = prepared.path;
35378
35861
  }
35379
35862
  }
35380
- function prepareToolMap(tools) {
35863
+ function prepareToolMap(tools, preparation = {}) {
35381
35864
  for (const [toolName, def] of Object.entries(tools)) {
35865
+ if (PREPARED_EXECUTORS.has(def.execute))
35866
+ continue;
35382
35867
  const execute = def.execute;
35383
- def.execute = async (args, context) => {
35384
- const prepared = prepareOpenCodeArguments(toolName, args);
35868
+ const preparedExecute = async (args, context) => {
35869
+ const prepared = prepareOpenCodeArguments(toolName, args, preparation);
35385
35870
  preserveDisplayFilePathAlias(toolName, args, prepared);
35386
35871
  return execute(prepared, context);
35387
35872
  };
35873
+ PREPARED_EXECUTORS.add(preparedExecute);
35874
+ def.execute = preparedExecute;
35388
35875
  }
35389
35876
  return tools;
35390
35877
  }
35391
- function normalizeToolMap(tools) {
35878
+ function normalizeToolMap(tools, preparation = {}) {
35392
35879
  for (const def of Object.values(tools))
35393
35880
  normalizeToolArgSchemas(def);
35394
- return prepareToolMap(tools);
35881
+ return prepareToolMap(tools, preparation);
35395
35882
  }
35396
35883
  // src/shared/ignored-message.ts
35397
35884
  async function sendIgnoredMessage2(client, sessionID, text) {
@@ -35522,7 +36009,7 @@ import { randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from
35522
36009
  import {
35523
36010
  existsSync as existsSync18,
35524
36011
  mkdirSync as mkdirSync10,
35525
- readdirSync as readdirSync5,
36012
+ readdirSync as readdirSync6,
35526
36013
  readFileSync as readFileSync16,
35527
36014
  renameSync as renameSync9,
35528
36015
  unlinkSync as unlinkSync8,
@@ -35692,7 +36179,7 @@ class AftRpcServer {
35692
36179
  sweepDeadPortFiles() {
35693
36180
  let entries;
35694
36181
  try {
35695
- entries = readdirSync5(this.portsDir);
36182
+ entries = readdirSync6(this.portsDir);
35696
36183
  } catch {
35697
36184
  return;
35698
36185
  }
@@ -37901,7 +38388,56 @@ ${backupBehavior}
37901
38388
  - Symbol replace includes decorators, attributes, and doc comments in range
37902
38389
  - Response is a compact server-rendered summary; before/after diff details are attached as UI metadata when available.`;
37903
38390
  }
38391
+ function createHashlineEditTool(ctx) {
38392
+ return {
38393
+ description: "Apply a hashline patch. Arguments are exactly `{patch}` where `patch` is a non-empty string of one or more `[path#TAG]` sections with PUT/CUT/REM/MV operations. Paths and tags come from section headers; obtain tags from tagged reads. Server-owned preview control is outside this schema.",
38394
+ args: {
38395
+ patch: z8.string().min(1).describe("Hashline patch text with one or more [path#TAG] sections and PUT/CUT/REM/MV operations")
38396
+ },
38397
+ execute: async (args, context) => {
38398
+ const patch = args.patch;
38399
+ const projectRoot = await resolveProjectRoot(ctx, context);
38400
+ const rawArgs = { patch };
38401
+ const preflight = await callToolCall(ctx, context, "hashline_preflight", rawArgs);
38402
+ if (preflight.success === false)
38403
+ throw toolErrorFromResponse("edit", preflight);
38404
+ const permissionPatterns = [
38405
+ ...coerceStringArray(preflight.affected_paths),
38406
+ ...coerceStringArray(preflight.affected_rel_paths),
38407
+ ...coerceStringArray(preflight.mv_destinations)
38408
+ ].filter((value, index, all) => all.indexOf(value) === index);
38409
+ for (const target of [
38410
+ ...coerceStringArray(preflight.affected_paths),
38411
+ ...coerceStringArray(preflight.mv_destinations)
38412
+ ]) {
38413
+ const absolute = resolvePathFromProjectRoot(projectRoot, target);
38414
+ const denial2 = await assertExternalDirectoryPermission(ctx, context, absolute);
38415
+ if (denial2)
38416
+ return permissionDeniedResponse(denial2);
38417
+ }
38418
+ const denial = await askEditPermission(context, permissionPatterns, {
38419
+ surface: "hashline"
38420
+ });
38421
+ if (denial)
38422
+ return permissionDeniedResponse(denial);
38423
+ const preview2 = await callToolCall(ctx, context, "edit", rawArgs, { preview: true });
38424
+ if (preview2.success === false)
38425
+ throw toolErrorFromResponse("edit", preview2);
38426
+ const data = await callToolCall(ctx, context, "edit", rawArgs);
38427
+ if (data.success === false)
38428
+ throw toolErrorFromResponse("edit", data);
38429
+ const firstPath = typeof data.filePath === "string" ? resolvePathFromProjectRoot(projectRoot, data.filePath) : "";
38430
+ return {
38431
+ output: data.text,
38432
+ title: firstPath ? relativeToWorktree(firstPath, projectRoot) : "edit",
38433
+ metadata: data.metadata && typeof data.metadata === "object" ? data.metadata : {}
38434
+ };
38435
+ }
38436
+ };
38437
+ }
37904
38438
  function createEditTool(ctx, writeToolName = "write") {
38439
+ if (ctx.hashlineEffective === true)
38440
+ return createHashlineEditTool(ctx);
37905
38441
  return {
37906
38442
  description: getEditDescription(ctx, writeToolName),
37907
38443
  args: {
@@ -38209,7 +38745,7 @@ function hoistedTools(ctx) {
38209
38745
  tools.bash_kill = createBashKillTool(ctx);
38210
38746
  }
38211
38747
  }
38212
- return prepareToolMap(tools);
38748
+ return prepareToolMap(tools, { hashlineEffective: ctx.hashlineEffective });
38213
38749
  }
38214
38750
  function aftPrefixedTools(ctx) {
38215
38751
  const aftEditTool = createEditTool(ctx, "aft_write");
@@ -38231,7 +38767,7 @@ function aftPrefixedTools(ctx) {
38231
38767
  tools.bash_kill = createBashKillTool(ctx);
38232
38768
  }
38233
38769
  }
38234
- return prepareToolMap(tools);
38770
+ return prepareToolMap(tools, { hashlineEffective: ctx.hashlineEffective });
38235
38771
  }
38236
38772
 
38237
38773
  // src/tools/imports.ts
@@ -39240,6 +39776,15 @@ function semanticTools(ctx) {
39240
39776
 
39241
39777
  // src/tool-registration.ts
39242
39778
  var ALL_ONLY_TOOLS = ["aft_callgraph", "aft_delete", "aft_move", "aft_refactor"];
39779
+ function openCodeEditSlotSurvives(config2) {
39780
+ return (config2.tool_surface ?? "recommended") !== "minimal" && config2.hoist_builtin_tools !== false && !(config2.disabled_tools ?? []).includes("edit");
39781
+ }
39782
+ function openCodeHashlineEffective(config2) {
39783
+ return config2.edit_mode === "hashline" && openCodeEditSlotSurvives(config2);
39784
+ }
39785
+ function openCodeHashlineEditRegistered(config2, registeredTools) {
39786
+ return openCodeHashlineEffective(config2) && registeredTools.has("edit");
39787
+ }
39243
39788
  function buildOpenCodeToolMap(ctx, config2, onUnknownDisabled) {
39244
39789
  const surface = config2.tool_surface ?? "recommended";
39245
39790
  const allTools = normalizeToolMap({
@@ -39254,7 +39799,7 @@ function buildOpenCodeToolMap(ctx, config2, onUnknownDisabled) {
39254
39799
  ...surface !== "minimal" && config2.search_index === true && searchTools(ctx),
39255
39800
  ...refactoringTools(ctx),
39256
39801
  ...surface !== "minimal" && conflictTools(ctx)
39257
- });
39802
+ }, { hashlineEffective: ctx.hashlineEffective });
39258
39803
  if (surface !== "all") {
39259
39804
  for (const name of ALL_ONLY_TOOLS)
39260
39805
  delete allTools[name];
@@ -39395,7 +39940,7 @@ var PLUGIN_VERSION = (() => {
39395
39940
  return "0.0.0";
39396
39941
  }
39397
39942
  })();
39398
- var ANNOUNCEMENT_VERSION = "0.49.4";
39943
+ var ANNOUNCEMENT_VERSION = "0.50.1";
39399
39944
  var ANNOUNCEMENT_FEATURES = [
39400
39945
  "Callgraph stays consistent under load: edits arriving mid-rebuild can no longer be silently lost from navigation results.",
39401
39946
  "aft_zoom handles real-world .jsonc: files with comment banners above the opening brace now resolve, and not-found errors point at the segment that actually failed (thanks @iceteaSA).",
@@ -39411,22 +39956,23 @@ async function initializePluginForDirectory(input) {
39411
39956
  });
39412
39957
  }
39413
39958
  };
39414
- let aftConfig = loadAftConfig(input.directory);
39959
+ const registrationRoot = resolveOpenCodeRegistrationRoot(input.directory, input.worktree);
39960
+ let aftConfig = loadAftConfig(registrationRoot);
39415
39961
  if (aftConfig.enabled === false) {
39416
- log2(`AFT disabled by config for ${input.directory}`);
39962
+ log2(`AFT disabled by config for ${registrationRoot}`);
39417
39963
  return { tool: {} };
39418
39964
  }
39419
- deliverConfigMigrationWarnings(input.directory, migrateAftConfigLocations(input.directory, bridgeLogger).flatMap((result) => result.warnings));
39420
- aftConfig = loadAftConfig(input.directory);
39421
- enqueueConfigParseWarnings(input.directory, getConfigLoadErrors());
39965
+ deliverConfigMigrationWarnings(registrationRoot, migrateAftConfigLocations(registrationRoot, bridgeLogger).flatMap((result) => result.warnings));
39966
+ aftConfig = loadAftConfig(registrationRoot);
39967
+ enqueueConfigParseWarnings(registrationRoot, getConfigLoadErrors());
39422
39968
  if (aftConfig.enabled === false) {
39423
- log2(`AFT disabled by config for ${input.directory}`);
39969
+ log2(`AFT disabled by config for ${registrationRoot}`);
39424
39970
  return { tool: {} };
39425
39971
  }
39426
39972
  const binaryPath = await findBinary(PLUGIN_VERSION);
39427
39973
  await ensureStorageMigrated({ harness: "opencode", binaryPath, logger: bridgeLogger });
39428
39974
  const autoUpdateAbort = new AbortController;
39429
- const projectEnabledCache = new Map([[input.directory, true]]);
39975
+ const projectEnabledCache = new Map([[registrationRoot, true]]);
39430
39976
  const loggedDisabledProjects = new Set;
39431
39977
  const isProjectEnabled = (projectRoot) => {
39432
39978
  const cached2 = projectEnabledCache.get(projectRoot);
@@ -39442,10 +39988,11 @@ async function initializePluginForDirectory(input) {
39442
39988
  return enabled;
39443
39989
  };
39444
39990
  const storageDir = resolveCortexKitStorageRoot();
39445
- const configOverrides = buildConfigTierConfigureParams(input.directory, {
39991
+ const configOverrides = buildConfigTierConfigureParams(registrationRoot, {
39446
39992
  bash_permissions: true,
39447
39993
  storage_dir: storageDir
39448
39994
  });
39995
+ const hashlineEffective = openCodeHashlineEffective(aftConfig);
39449
39996
  let lspInstallCompletion = null;
39450
39997
  const isFastembedSemanticBackend = (aftConfig.semantic?.backend ?? "fastembed") === "fastembed";
39451
39998
  let onnxRuntimePromise = null;
@@ -39635,6 +40182,16 @@ ${lines}
39635
40182
  poolOptions,
39636
40183
  configOverrides,
39637
40184
  subcConnectionFile: aftConfig.subc?.connection_file,
40185
+ onBgEventsNudge: (directory, sessionID) => {
40186
+ handleSubcBgEventsNudge({
40187
+ ctx,
40188
+ directory,
40189
+ sessionID,
40190
+ client: input.client
40191
+ }).catch((err) => {
40192
+ warn2(`[aft-plugin] bg nudge rejected: ${err instanceof Error ? err.message : String(err)}`);
40193
+ });
40194
+ },
39638
40195
  onBgEventsNudgeRef: (ref) => {
39639
40196
  handleSubcBgEventsNudge({
39640
40197
  ctx,
@@ -39664,6 +40221,7 @@ ${lines}
39664
40221
  client: input.client,
39665
40222
  plugin: input.plugin,
39666
40223
  config: aftConfig,
40224
+ hashlineEffective,
39667
40225
  storageDir: configOverrides.storage_dir,
39668
40226
  isProjectEnabled
39669
40227
  };
@@ -39865,6 +40423,10 @@ Install: ${getManualInstallHint()}`).catch(() => {});
39865
40423
  "bash_status"
39866
40424
  ];
39867
40425
  const registeredTools = new Set(Object.keys(allTools));
40426
+ const hashlineEditRegistered = openCodeHashlineEditRegistered(aftConfig, registeredTools);
40427
+ ctx.hashlineEffective = hashlineEditRegistered;
40428
+ pool.setConfigureOverride("edit_slot_survives", hashlineEditRegistered);
40429
+ log2(`hashline activation decision requested=${aftConfig.edit_mode === "hashline"} ` + `edit_slot_survives=${hashlineEditRegistered} effective=${ctx.hashlineEffective}`);
39868
40430
  const aftSearchRegistered = registeredTools.has("aft_search");
39869
40431
  pool.setConfigureOverride("aft_search_registered", aftSearchRegistered);
39870
40432
  ctx.aftSearchRegistered = aftSearchRegistered;
@@ -39959,7 +40521,9 @@ Install: ${getManualInstallHint()}`).catch(() => {});
39959
40521
  signalBashWaitDetachForProject(pool, sessionDir, sid);
39960
40522
  },
39961
40523
  "tool.execute.before": async (toolInput, output) => {
39962
- output.args = prepareOpenCodeArguments(toolInput.tool, output.args);
40524
+ output.args = prepareOpenCodeArguments(toolInput.tool, output.args, {
40525
+ hashlineEffective: ctx.hashlineEffective
40526
+ });
39963
40527
  if (toolInput.sessionID)
39964
40528
  inspectTier2Idle.clear(toolInput.sessionID);
39965
40529
  },