@cortexkit/aft-pi 0.45.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -11860,11 +11860,11 @@ class BridgePool {
11860
11860
  function normalizeKey(projectRoot) {
11861
11861
  return canonicalizeProjectRoot(projectRoot);
11862
11862
  }
11863
- // ../../node_modules/.bun/@cortexkit+subc-client@0.3.0/node_modules/@cortexkit/subc-client/src/client.ts
11863
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.3.4/node_modules/@cortexkit/subc-client/dist/client.js
11864
11864
  import { promises as fs2 } from "node:fs";
11865
11865
  import { debuglog } from "node:util";
11866
11866
 
11867
- // ../../node_modules/.bun/@cortexkit+subc-client@0.3.0/node_modules/@cortexkit/subc-client/src/auth.ts
11867
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.3.4/node_modules/@cortexkit/subc-client/dist/auth.js
11868
11868
  import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
11869
11869
  var NONCE_LEN = 32;
11870
11870
  var MAX_AUTH_MESSAGE_LEN = 4096;
@@ -11928,7 +11928,7 @@ async function authenticateClient(sock, conn, deadlineMs) {
11928
11928
  await writeMessage(sock, { client_auth: Array.from(clientAuth) }, deadlineMs);
11929
11929
  }
11930
11930
 
11931
- // ../../node_modules/.bun/@cortexkit+subc-client@0.3.0/node_modules/@cortexkit/subc-client/src/connection-file.ts
11931
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.3.4/node_modules/@cortexkit/subc-client/dist/connection-file.js
11932
11932
  import { promises as fs } from "node:fs";
11933
11933
  var SCHEMA_VERSION = 1;
11934
11934
  var MIN_KEY_LEN = 32;
@@ -11997,13 +11997,13 @@ async function readConnectionFile(path2) {
11997
11997
  return info;
11998
11998
  }
11999
11999
 
12000
- // ../../node_modules/.bun/@cortexkit+subc-client@0.3.0/node_modules/@cortexkit/subc-client/src/envelope.ts
12000
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.3.4/node_modules/@cortexkit/subc-client/dist/envelope.js
12001
12001
  var PROTOCOL_VERSION = 1;
12002
12002
  var HEADER_LEN = 17;
12003
12003
  var FROZEN_PREFIX_LEN = 5;
12004
12004
  var MAX_FRAME_BODY_LEN = 64 * 1024 * 1024;
12005
12005
  var FrameType;
12006
- ((FrameType2) => {
12006
+ (function(FrameType2) {
12007
12007
  FrameType2[FrameType2["Request"] = 0] = "Request";
12008
12008
  FrameType2[FrameType2["Response"] = 1] = "Response";
12009
12009
  FrameType2[FrameType2["Push"] = 2] = "Push";
@@ -12016,11 +12016,17 @@ var FrameType;
12016
12016
  FrameType2[FrameType2["Hello"] = 9] = "Hello";
12017
12017
  FrameType2[FrameType2["HelloAck"] = 10] = "HelloAck";
12018
12018
  FrameType2[FrameType2["Goodbye"] = 11] = "Goodbye";
12019
- })(FrameType ||= {});
12020
- var FRAME_TYPE_MAX = 11 /* Goodbye */;
12019
+ })(FrameType || (FrameType = {}));
12020
+ var FRAME_TYPE_MAX = FrameType.Goodbye;
12021
12021
  function isPureHeader(ty) {
12022
- return ty === 6 /* Cancel */ || ty === 7 /* Ping */ || ty === 8 /* Pong */ || ty === 11 /* Goodbye */;
12023
- }
12022
+ return ty === FrameType.Cancel || ty === FrameType.Ping || ty === FrameType.Pong || ty === FrameType.Goodbye;
12023
+ }
12024
+ var Priority;
12025
+ (function(Priority2) {
12026
+ Priority2[Priority2["Passive"] = 0] = "Passive";
12027
+ Priority2[Priority2["Interactive"] = 1] = "Interactive";
12028
+ Priority2[Priority2["Background"] = 2] = "Background";
12029
+ })(Priority || (Priority = {}));
12024
12030
  var FLAG_BINARY = 1;
12025
12031
  var FLAG_PRIORITY_MASK = 6;
12026
12032
  var FLAG_PRIORITY_SHIFT = 1;
@@ -12104,7 +12110,7 @@ function encodeFrame(frame) {
12104
12110
  return out;
12105
12111
  }
12106
12112
 
12107
- // ../../node_modules/.bun/@cortexkit+subc-client@0.3.0/node_modules/@cortexkit/subc-client/src/socket.ts
12113
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.3.4/node_modules/@cortexkit/subc-client/dist/socket.js
12108
12114
  import net from "node:net";
12109
12115
 
12110
12116
  class SocketClosedError extends Error {
@@ -12296,14 +12302,14 @@ class SubcSocket {
12296
12302
  }
12297
12303
  }
12298
12304
 
12299
- // ../../node_modules/.bun/@cortexkit+subc-client@0.3.0/node_modules/@cortexkit/subc-client/src/client.ts
12305
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.3.4/node_modules/@cortexkit/subc-client/dist/client.js
12300
12306
  var debug = debuglog("subc-client");
12301
12307
  var DEFAULT_HANDSHAKE_TIMEOUT_MS = 1e4;
12302
12308
  var DEFAULT_REQUEST_TIMEOUT_MS = 30000;
12303
12309
  var TIMEOUT_ARBITRATION_GRACE_MS = 50;
12304
12310
  var REQUEST_DEADLINE_MARKER = "request_deadline";
12305
12311
  var DEADLINE_NO_DROP_CODE = "deadline_exceeded_no_drop_observed";
12306
- var ROUTE_OPEN_RETRY_DEADLINE_MS = 1e4;
12312
+ var ROUTE_OPEN_RETRY_DEADLINE_MS = 30000;
12307
12313
  var BODY_READ_TIMEOUT_MS = 30000;
12308
12314
  var EMPTY_BODY = new Uint8Array(0);
12309
12315
  var DEFAULT_MANAGED_TARGET_KIND = "management_surface";
@@ -12385,12 +12391,13 @@ class SubcClient {
12385
12391
  }
12386
12392
  async request(routeChannel, body, opts = {}) {
12387
12393
  const bytes = body instanceof Uint8Array ? body : this.encode(body);
12388
- const priority = opts.priority ?? 1 /* Interactive */;
12394
+ const priority = opts.priority ?? Priority.Interactive;
12389
12395
  const reply = await this.send(routeChannel, bytes, priority, opts.timeoutMs, opts.onProgress);
12390
12396
  return this.parseJson(reply);
12391
12397
  }
12392
12398
  async call(moduleId, method, params, opts = {}) {
12393
12399
  const body = params === undefined ? { method } : { method, params };
12400
+ let retriedUnknownChannel = false;
12394
12401
  for (;; ) {
12395
12402
  const routeChannel = await this.cachedRouteChannel(moduleId, opts);
12396
12403
  try {
@@ -12398,6 +12405,11 @@ class SubcClient {
12398
12405
  } catch (err) {
12399
12406
  if (!(err instanceof SubcCallError))
12400
12407
  throw this.terminalCallError("managed call failed", err);
12408
+ if (err.code === "unknown_channel" && !retriedUnknownChannel && !this.closeStarted) {
12409
+ retriedUnknownChannel = true;
12410
+ this.evictRouteChannel(routeChannel);
12411
+ continue;
12412
+ }
12401
12413
  if (err.kind === "not_sent") {
12402
12414
  try {
12403
12415
  await this.reconnectAfterDrop(err);
@@ -12415,7 +12427,7 @@ class SubcClient {
12415
12427
  }
12416
12428
  subscribe(routeChannel, body, onEvent, opts = {}) {
12417
12429
  const bytes = body instanceof Uint8Array ? body : this.encode(body);
12418
- const priority = opts.priority ?? 1 /* Interactive */;
12430
+ const priority = opts.priority ?? Priority.Interactive;
12419
12431
  const corr = this.nextCorr++;
12420
12432
  const key = `${routeChannel}:${corr}`;
12421
12433
  const closed = new Promise((resolve7, reject) => {
@@ -12431,7 +12443,7 @@ class SubcClient {
12431
12443
  timer: null,
12432
12444
  subscription: true
12433
12445
  });
12434
- const frame = buildFrame(0 /* Request */, buildFlags(false, priority, false), routeChannel, corr, bytes);
12446
+ const frame = buildFrame(FrameType.Request, buildFlags(false, priority, false), routeChannel, corr, bytes);
12435
12447
  this.sock.write(encodeFrame(frame), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS).catch((err) => {
12436
12448
  const p = this.pending.get(key);
12437
12449
  if (p)
@@ -12443,7 +12455,7 @@ class SubcClient {
12443
12455
  if (cancelled)
12444
12456
  return;
12445
12457
  cancelled = true;
12446
- const cancel = buildFrame(6 /* Cancel */, buildFlags(false, priority, false), routeChannel, corr, EMPTY_BODY);
12458
+ const cancel = buildFrame(FrameType.Cancel, buildFlags(false, priority, false), routeChannel, corr, EMPTY_BODY);
12447
12459
  this.sock.write(encodeFrame(cancel), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS).catch(() => {});
12448
12460
  };
12449
12461
  return { unsubscribe, closed };
@@ -12494,7 +12506,7 @@ class SubcClient {
12494
12506
  sendRouteGoodbye(channel) {
12495
12507
  if (this.closedErr)
12496
12508
  return;
12497
- const goodbye = buildFrame(11 /* Goodbye */, buildFlags(false, 1 /* Interactive */, false), channel, 0n, EMPTY_BODY);
12509
+ const goodbye = buildFrame(FrameType.Goodbye, buildFlags(false, Priority.Interactive, false), channel, 0n, EMPTY_BODY);
12498
12510
  this.sock.write(encodeFrame(goodbye), Date.now() + DEFAULT_REQUEST_TIMEOUT_MS).catch(() => {});
12499
12511
  }
12500
12512
  static async openConnection(opts) {
@@ -12511,14 +12523,14 @@ class SubcClient {
12511
12523
  return { sock, conn };
12512
12524
  }
12513
12525
  async controlRpc(body) {
12514
- return this.send(0, body, 1 /* Interactive */, undefined, undefined);
12526
+ return this.send(0, body, Priority.Interactive, undefined, undefined);
12515
12527
  }
12516
12528
  send(channel, body, priority, timeoutMs, onProgress) {
12517
12529
  if (this.closedErr)
12518
12530
  return Promise.reject(this.closedErr);
12519
12531
  const corr = this.nextCorr++;
12520
12532
  const key = `${channel}:${corr}`;
12521
- const frame = buildFrame(0 /* Request */, buildFlags(false, priority, false), channel, corr, body);
12533
+ const frame = buildFrame(FrameType.Request, buildFlags(false, priority, false), channel, corr, body);
12522
12534
  return new Promise((resolve7, reject) => {
12523
12535
  const ms = timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
12524
12536
  const pending = {
@@ -12558,7 +12570,7 @@ class SubcClient {
12558
12570
  }
12559
12571
  async managedRequest(routeChannel, body, opts) {
12560
12572
  const bytes = body instanceof Uint8Array ? body : this.encode(body);
12561
- const priority = opts.priority ?? 1 /* Interactive */;
12573
+ const priority = opts.priority ?? Priority.Interactive;
12562
12574
  try {
12563
12575
  const reply = await this.sendManaged(routeChannel, bytes, priority, opts.timeoutMs, opts.onProgress);
12564
12576
  return this.parseJson(reply);
@@ -12574,7 +12586,7 @@ class SubcClient {
12574
12586
  }
12575
12587
  const corr = this.nextCorr++;
12576
12588
  const key = `${channel}:${corr}`;
12577
- const frame = buildFrame(0 /* Request */, buildFlags(false, priority, false), channel, corr, body);
12589
+ const frame = buildFrame(FrameType.Request, buildFlags(false, priority, false), channel, corr, body);
12578
12590
  let handedToSocket = false;
12579
12591
  const classifyFailure = (err) => {
12580
12592
  if (!handedToSocket) {
@@ -12733,6 +12745,9 @@ class SubcClient {
12733
12745
  return;
12734
12746
  } catch (err) {
12735
12747
  if (!isConsumerReconnectTransient(err) || attempt >= this.opts.reconnectBackoff.maxAttempts) {
12748
+ if (err instanceof AuthError && attempt > 1) {
12749
+ throw new AuthError(`reconnect gave up after ${attempt} attempts: ${err.message} — the connection file and the daemon's key disagree persistently ` + `(daemon restarting in a loop, split connection-file paths, or a genuinely foreign daemon on this port); ` + `check the daemon, then restart this host app`);
12750
+ }
12736
12751
  throw err;
12737
12752
  }
12738
12753
  await this.opts.sleep(delay);
@@ -12801,26 +12816,27 @@ class SubcClient {
12801
12816
  const pending = this.pending.get(key);
12802
12817
  if (pending) {
12803
12818
  switch (frame.header.ty) {
12804
- case 2 /* Push */:
12805
- case 3 /* StreamData */:
12819
+ case FrameType.Push:
12820
+ case FrameType.StreamData:
12806
12821
  pending.onProgress?.(frame.body);
12807
12822
  return;
12808
- case 1 /* Response */:
12809
- case 4 /* StreamEnd */:
12823
+ case FrameType.Response:
12824
+ case FrameType.StreamEnd:
12810
12825
  this.settle(key, pending, () => pending.resolve(frame));
12811
12826
  return;
12812
- case 5 /* Error */:
12827
+ case FrameType.Error:
12813
12828
  this.settle(key, pending, () => pending.reject(this.errorFromFrame(frame)));
12814
12829
  return;
12815
12830
  default:
12816
12831
  return;
12817
12832
  }
12818
12833
  }
12819
- if (frame.header.ty === 11 /* Goodbye */) {
12834
+ if (frame.header.ty === FrameType.Goodbye) {
12820
12835
  this.failChannel(frame.header.channel, new SubcError("route closed by subc (GOODBYE)"));
12836
+ this.evictRouteChannel(frame.header.channel);
12821
12837
  return;
12822
12838
  }
12823
- if (frame.header.ty === 1 /* Response */ || frame.header.ty === 5 /* Error */ || frame.header.ty === 4 /* StreamEnd */) {
12839
+ if (frame.header.ty === FrameType.Response || frame.header.ty === FrameType.Error || frame.header.ty === FrameType.StreamEnd) {
12824
12840
  debug("dropped terminal frame with no waiter: type=%d channel=%d corr=%s port=%s", frame.header.ty, frame.header.channel, frame.header.corr, this.sock.localPort() ?? "?");
12825
12841
  return;
12826
12842
  }
@@ -12846,6 +12862,13 @@ class SubcClient {
12846
12862
  return new SubcError(Buffer.from(frame.body).toString("utf8") || "subc error");
12847
12863
  }
12848
12864
  }
12865
+ evictRouteChannel(channel) {
12866
+ for (const cached of this.routes.values()) {
12867
+ if (cached.channel === channel && cached.generation === this.generation) {
12868
+ cached.channel = null;
12869
+ }
12870
+ }
12871
+ }
12849
12872
  failChannel(channel, err) {
12850
12873
  for (const [key, pending] of this.pending) {
12851
12874
  if (pending.channel === channel) {
@@ -12892,7 +12915,9 @@ function isConsumerReconnectTransient(err) {
12892
12915
  return true;
12893
12916
  if (err instanceof SubcCallError)
12894
12917
  return err.kind === "not_sent" || err.kind === "outcome_unknown";
12895
- if (err instanceof SubcError || err instanceof ConnectionFileError || err instanceof AuthError)
12918
+ if (err instanceof AuthError)
12919
+ return true;
12920
+ if (err instanceof SubcError || err instanceof ConnectionFileError)
12896
12921
  return false;
12897
12922
  const code = errorCode(err);
12898
12923
  return code === "ECONNREFUSED" || code === "ECONNRESET" || code === "EPIPE" || code === "ETIMEDOUT" || code === "ENOENT";
@@ -12945,14 +12970,51 @@ function causeMessage(cause) {
12945
12970
  return "";
12946
12971
  return `: ${cause instanceof Error ? cause.message : String(cause)}`;
12947
12972
  }
12948
- // ../../node_modules/.bun/@cortexkit+subc-client@0.3.0/node_modules/@cortexkit/subc-client/src/provider.ts
12973
+ // ../../node_modules/.bun/@cortexkit+subc-client@0.3.4/node_modules/@cortexkit/subc-client/dist/provider.js
12949
12974
  import { Buffer as Buffer2 } from "node:buffer";
12950
12975
  var DEFAULT_HANDSHAKE_TIMEOUT_MS2 = 1e4;
12951
12976
  var BODY_READ_TIMEOUT_MS2 = 30000;
12952
12977
  var WRITE_TIMEOUT_MS = 30000;
12953
12978
  var DEFAULT_RESTORED_DEBOUNCE_MS = 250;
12979
+ var DEFAULT_PROVIDER_HANDLER_CAPACITY = 64;
12980
+ var HEALTH_CHECK_OP = "health.check";
12954
12981
  var HELLO_CORR = 1n;
12955
12982
 
12983
+ class AsyncPermitPool {
12984
+ available;
12985
+ waiters = [];
12986
+ constructor(capacity) {
12987
+ if (!Number.isInteger(capacity) || capacity <= 0) {
12988
+ throw new SubcProviderError("provider handler capacity must be a positive integer", "invalid_handler_capacity");
12989
+ }
12990
+ this.available = capacity;
12991
+ }
12992
+ async acquire() {
12993
+ if (this.available > 0) {
12994
+ this.available -= 1;
12995
+ return this.releaseOnce();
12996
+ }
12997
+ await new Promise((resolve7) => {
12998
+ this.waiters.push(resolve7);
12999
+ });
13000
+ return this.releaseOnce();
13001
+ }
13002
+ releaseOnce() {
13003
+ let released = false;
13004
+ return () => {
13005
+ if (released)
13006
+ return;
13007
+ released = true;
13008
+ const next = this.waiters.shift();
13009
+ if (next) {
13010
+ next();
13011
+ } else {
13012
+ this.available += 1;
13013
+ }
13014
+ };
13015
+ }
13016
+ }
13017
+
12956
13018
  class SubcProviderError extends Error {
12957
13019
  code;
12958
13020
  constructor(message, code) {
@@ -12971,6 +13033,7 @@ class SubcProvider {
12971
13033
  closeStarted = false;
12972
13034
  closedErr = null;
12973
13035
  inflight = new Map;
13036
+ requestGate = new AsyncPermitPool(DEFAULT_PROVIDER_HANDLER_CAPACITY);
12974
13037
  reconnecting = null;
12975
13038
  generation = 1;
12976
13039
  connectionEpoch = 1;
@@ -13009,7 +13072,7 @@ class SubcProvider {
13009
13072
  this.cancelRestoredDebounce();
13010
13073
  const sock = this.sock;
13011
13074
  try {
13012
- await sendFrame(sock, buildFrame(11 /* Goodbye */, controlFlags(), 0, 0n, new Uint8Array(0)));
13075
+ await sendFrame(sock, buildFrame(FrameType.Goodbye, controlFlags(), 0, 0n, new Uint8Array(0)));
13013
13076
  } catch {} finally {
13014
13077
  sock.close();
13015
13078
  this.finishClosed();
@@ -13060,21 +13123,21 @@ class SubcProvider {
13060
13123
  }
13061
13124
  async dispatch(frame, sock, generation) {
13062
13125
  switch (frame.header.ty) {
13063
- case 7 /* Ping */:
13126
+ case FrameType.Ping:
13064
13127
  if (frame.header.channel === 0) {
13065
- await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, 8 /* Pong */, frame.header.flags, 0, frame.header.corr, new Uint8Array(0)));
13128
+ await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, FrameType.Pong, frame.header.flags, 0, frame.header.corr, new Uint8Array(0)));
13066
13129
  }
13067
13130
  return true;
13068
- case 11 /* Goodbye */:
13131
+ case FrameType.Goodbye:
13069
13132
  if (frame.header.channel === 0)
13070
13133
  return false;
13071
13134
  this.abortChannel(generation, frame.header.channel);
13072
13135
  await this.opts.onRouteGone?.(frame.header.channel);
13073
13136
  return true;
13074
- case 6 /* Cancel */:
13137
+ case FrameType.Cancel:
13075
13138
  this.inflight.get(routeKey(generation, frame.header.channel, frame.header.corr))?.abort();
13076
13139
  return true;
13077
- case 0 /* Request */:
13140
+ case FrameType.Request:
13078
13141
  if (frame.header.channel === 0) {
13079
13142
  await this.handleControlRequest(frame, sock, generation);
13080
13143
  } else {
@@ -13109,6 +13172,14 @@ class SubcProvider {
13109
13172
  }
13110
13173
  async handleControlRequest(frame, sock, generation) {
13111
13174
  const request = parseJson(frame.body);
13175
+ if (request.op === HEALTH_CHECK_OP) {
13176
+ this.handleHealthRequest(frame, sock, generation).catch((err) => {
13177
+ if (!this.closeStarted && this.sock === sock && this.generation === generation) {
13178
+ console.warn("SubcProvider health handler failed after its request was dispatched", err);
13179
+ }
13180
+ });
13181
+ return;
13182
+ }
13112
13183
  if (request.op !== "route.bind") {
13113
13184
  throw new SubcProviderError(`unsupported module control request ${request.op ?? "<missing op>"}`);
13114
13185
  }
@@ -13124,41 +13195,67 @@ class SubcProvider {
13124
13195
  await this.sendError(frame, rejection.code, rejection.message, controlFlags(), sock, generation);
13125
13196
  return;
13126
13197
  }
13127
- await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, 1 /* Response */, controlFlags(), 0, frame.header.corr, encodeJson({ op: "route.bind" })));
13198
+ await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, FrameType.Response, controlFlags(), 0, frame.header.corr, encodeJson({ op: "route.bind" })));
13128
13199
  }
13129
13200
  async handleDataRequest(frame, sock, generation) {
13130
13201
  const { channel, corr, ver } = frame.header;
13131
13202
  const key = routeKey(generation, channel, corr);
13132
13203
  const controller = new AbortController;
13133
13204
  this.inflight.set(key, controller);
13134
- const dataFlags = buildFlags(false, 1 /* Interactive */, false);
13205
+ const dataFlags = buildFlags(false, Priority.Interactive, false);
13135
13206
  const ctx = {
13136
13207
  signal: controller.signal,
13137
13208
  currentEpoch: () => this.connectionEpoch,
13138
13209
  emit: async (eventBody) => {
13139
13210
  if (controller.signal.aborted)
13140
13211
  return;
13141
- await this.sendOn(sock, generation, buildFrameWithVersion(ver, 3 /* StreamData */, dataFlags, channel, corr, eventBody));
13212
+ await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.StreamData, dataFlags, channel, corr, eventBody));
13142
13213
  }
13143
13214
  };
13215
+ const releasePermit = await (this.requestGate ?? new AsyncPermitPool(DEFAULT_PROVIDER_HANDLER_CAPACITY)).acquire();
13144
13216
  try {
13145
13217
  const body = await this.opts.handler(channel, frame.body, ctx);
13146
13218
  if (body === undefined) {
13147
- await this.sendOn(sock, generation, buildFrameWithVersion(ver, 4 /* StreamEnd */, dataFlags, channel, corr, new Uint8Array(0)));
13219
+ await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.StreamEnd, dataFlags, channel, corr, new Uint8Array(0)));
13148
13220
  } else if (body instanceof Uint8Array) {
13149
- await this.sendOn(sock, generation, buildFrameWithVersion(ver, 1 /* Response */, dataFlags, channel, corr, body));
13221
+ await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.Response, dataFlags, channel, corr, body));
13150
13222
  } else {
13151
13223
  throw new SubcProviderError("provider handler must return a Uint8Array or void", "invalid_handler_response");
13152
13224
  }
13153
13225
  } catch (err) {
13154
13226
  await this.sendError(frame, err instanceof SubcProviderError && err.code ? err.code : "handler_error", err instanceof Error ? err.message : String(err), dataFlags, sock, generation);
13155
13227
  } finally {
13228
+ releasePermit();
13229
+ if (this.inflight.get(key) === controller)
13230
+ this.inflight.delete(key);
13231
+ }
13232
+ }
13233
+ async handleHealthRequest(frame, sock, generation) {
13234
+ const { channel, corr, ver } = frame.header;
13235
+ const key = routeKey(generation, channel, corr);
13236
+ const controller = new AbortController;
13237
+ this.inflight.set(key, controller);
13238
+ const releasePermit = await (this.requestGate ?? new AsyncPermitPool(DEFAULT_PROVIDER_HANDLER_CAPACITY)).acquire();
13239
+ try {
13240
+ if (controller.signal.aborted)
13241
+ return;
13242
+ const report = await this.opts.health();
13243
+ await this.sendOn(sock, generation, buildFrameWithVersion(ver, FrameType.Response, controlFlags(), channel, corr, encodeJson({
13244
+ op: HEALTH_CHECK_OP,
13245
+ status: report.status,
13246
+ ...report.detail === undefined ? {} : { detail: report.detail },
13247
+ ...report.metrics === undefined ? {} : { metrics: report.metrics }
13248
+ })));
13249
+ } catch (err) {
13250
+ await this.sendError(frame, err instanceof SubcProviderError && err.code ? err.code : "health_error", err instanceof Error ? err.message : String(err), controlFlags(), sock, generation);
13251
+ } finally {
13252
+ releasePermit();
13156
13253
  if (this.inflight.get(key) === controller)
13157
13254
  this.inflight.delete(key);
13158
13255
  }
13159
13256
  }
13160
13257
  async sendError(frame, code, message, flags, sock, generation) {
13161
- await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, 5 /* Error */, flags, frame.header.channel, frame.header.corr, encodeJson({ code, message })));
13258
+ await this.sendOn(sock, generation, buildFrameWithVersion(frame.header.ver, FrameType.Error, flags, frame.header.channel, frame.header.corr, encodeJson({ code, message })));
13162
13259
  }
13163
13260
  async sendOn(sock, generation, frame) {
13164
13261
  if (this.sock !== sock || this.generation !== generation || this.closeStarted || this.closedErr)
@@ -13298,6 +13395,7 @@ function normalizeProviderConnectOptions(opts) {
13298
13395
  connectionFile: opts.connectionFile,
13299
13396
  manifest: opts.manifest,
13300
13397
  handler: opts.handler,
13398
+ health: opts.health ?? (() => ({ status: "ok" })),
13301
13399
  handshakeTimeoutMs: opts.handshakeTimeoutMs,
13302
13400
  controlOps: opts.controlOps,
13303
13401
  onBind: opts.onBind,
@@ -13309,12 +13407,19 @@ function normalizeProviderConnectOptions(opts) {
13309
13407
  launchNonce: opts.launchNonce
13310
13408
  };
13311
13409
  }
13410
+ function normalizedControlOps(controlOps) {
13411
+ if (controlOps === null)
13412
+ return null;
13413
+ const merged = new Set(controlOps ?? []);
13414
+ merged.add(HEALTH_CHECK_OP);
13415
+ return [...merged];
13416
+ }
13312
13417
  function buildHelloFrame(opts) {
13313
13418
  const nonce = launchNonce(opts);
13314
- return buildFrame(9 /* Hello */, controlFlags(), 0, HELLO_CORR, encodeJson({
13419
+ return buildFrame(FrameType.Hello, controlFlags(), 0, HELLO_CORR, encodeJson({
13315
13420
  manifest: normalizeManifest(opts.manifest),
13316
13421
  protocol_ver: PROTOCOL_VERSION,
13317
- control_ops: opts.controlOps === undefined ? null : opts.controlOps,
13422
+ control_ops: normalizedControlOps(opts.controlOps),
13318
13423
  ...nonce ? { launch_nonce: nonce } : {}
13319
13424
  }));
13320
13425
  }
@@ -13325,7 +13430,9 @@ function isProviderReconnectTransient(err) {
13325
13430
  return true;
13326
13431
  if (err instanceof SocketWriteNotQueuedError || err instanceof SocketWriteQueuedError)
13327
13432
  return true;
13328
- if (err instanceof ConnectionFileError || err instanceof AuthError)
13433
+ if (err instanceof AuthError)
13434
+ return true;
13435
+ if (err instanceof ConnectionFileError)
13329
13436
  return false;
13330
13437
  const code = errorCode2(err);
13331
13438
  return code === "ECONNREFUSED" || code === "ECONNRESET" || code === "EPIPE" || code === "ETIMEDOUT" || code === "ENOENT";
@@ -13342,7 +13449,7 @@ async function pauseBeforeStateRetry() {
13342
13449
  await new Promise((resolve7) => setTimeout(resolve7, 0));
13343
13450
  }
13344
13451
  function controlFlags() {
13345
- return buildFlags(false, 0 /* Passive */, false);
13452
+ return buildFlags(false, Priority.Passive, false);
13346
13453
  }
13347
13454
  async function sendFrame(sock, frame) {
13348
13455
  await sock.write(encodeFrame(frame), Date.now() + WRITE_TIMEOUT_MS);
@@ -13352,9 +13459,9 @@ async function expectHelloAck(sock, deadline) {
13352
13459
  const body = header.len === 0 ? new Uint8Array(0) : await sock.readExact(header.len, deadline);
13353
13460
  const frame = { header, body };
13354
13461
  switch (header.ty) {
13355
- case 10 /* HelloAck */:
13462
+ case FrameType.HelloAck:
13356
13463
  return parseJson(body);
13357
- case 5 /* Error */: {
13464
+ case FrameType.Error: {
13358
13465
  const error2 = parseJson(body);
13359
13466
  throw new SubcProviderError(`subc rejected HELLO: ${error2.code ?? "unknown"} — ${error2.message ?? "subc error"}`, error2.code);
13360
13467
  }
@@ -13420,6 +13527,7 @@ function normalizeProviderRole(role) {
13420
13527
  role: "tool_provider",
13421
13528
  tools: role.tools.map((tool) => ({
13422
13529
  name: tool.name,
13530
+ ...tool.description === undefined ? {} : { description: tool.description },
13423
13531
  execution_mode: tool.execution_mode,
13424
13532
  schema: tool.schema
13425
13533
  })),
@@ -13594,6 +13702,9 @@ class BgSubscription {
13594
13702
  function isRecord(value) {
13595
13703
  return typeof value === "object" && value !== null && !Array.isArray(value);
13596
13704
  }
13705
+ function isUnknownChannelError(err) {
13706
+ return err instanceof SubcError && err.code === "unknown_channel";
13707
+ }
13597
13708
  function safeCloseRoute(client, channel) {
13598
13709
  try {
13599
13710
  client.closeRouteChannel(channel).catch(() => {
@@ -13673,7 +13784,7 @@ class SubcTransport {
13673
13784
  if (!options)
13674
13785
  return {};
13675
13786
  const preview = options.preview;
13676
- const timeoutMs = options.timeoutMs;
13787
+ const timeoutMs = options.transportTimeoutMs ?? options.timeoutMs;
13677
13788
  const onProgress = options.onProgress;
13678
13789
  return { preview, timeoutMs, onProgress };
13679
13790
  }
@@ -13743,45 +13854,66 @@ class SubcTransportPool {
13743
13854
  record.inflight += 1;
13744
13855
  try {
13745
13856
  const client = await this.ensureClient();
13746
- if (!this.isCurrentSession(key, record)) {
13747
- throw new RouteTornDownError("subc session closed");
13748
- }
13749
- let channel;
13750
- let entry;
13751
- try {
13752
- ({ channel, entry } = await this.routeChannel(client, identity, record));
13857
+ const openRoute = async () => {
13753
13858
  if (!this.isCurrentSession(key, record)) {
13754
13859
  throw new RouteTornDownError("subc session closed");
13755
13860
  }
13756
- } catch (err) {
13757
- if (err instanceof RouteTornDownError)
13861
+ try {
13862
+ const opened = await this.routeChannel(client, identity, record);
13863
+ if (!this.isCurrentSession(key, record)) {
13864
+ throw new RouteTornDownError("subc session closed");
13865
+ }
13866
+ return opened;
13867
+ } catch (err) {
13868
+ if (err instanceof RouteTornDownError)
13869
+ throw err;
13870
+ if (isConsumerReconnectTransient(err) && this.isCurrentSession(key, record) && this.client === client) {
13871
+ this.dropClient(client);
13872
+ }
13758
13873
  throw err;
13759
- if (isConsumerReconnectTransient(err) && this.isCurrentSession(key, record) && this.client === client) {
13760
- this.dropClient(client);
13761
13874
  }
13762
- throw err;
13763
- }
13764
- try {
13765
- const reply = await client.request(channel, body, { timeoutMs, onProgress });
13766
- if (this.isCurrentSession(key, record) && this.client === client) {
13767
- this.transportFailures = 0;
13768
- }
13769
- this.ensureBgSubscription(identity, record);
13770
- return reply;
13771
- } catch (err) {
13772
- if (record.routeEntry === entry) {
13773
- entry.closed = true;
13875
+ };
13876
+ const clearRouteEntry = (entry2) => {
13877
+ if (record.routeEntry === entry2) {
13878
+ entry2.closed = true;
13774
13879
  record.routeEntry = null;
13775
13880
  }
13881
+ };
13882
+ const handleRequestFailure = (err, entry2) => {
13883
+ clearRouteEntry(entry2);
13776
13884
  if (this.isCurrentSession(key, record) && this.client === client) {
13777
13885
  if (isConsumerReconnectTransient(err)) {
13778
13886
  this.transportFailures = 0;
13779
13887
  this.dropClient(client);
13780
- } else if (++this.transportFailures >= MAX_CONSECUTIVE_TRANSPORT_FAILURES) {
13888
+ } else if (!isUnknownChannelError(err) && ++this.transportFailures >= MAX_CONSECUTIVE_TRANSPORT_FAILURES) {
13781
13889
  this.transportFailures = 0;
13782
13890
  this.dropClient(client);
13783
13891
  }
13784
13892
  }
13893
+ };
13894
+ const requestOnRoute = async (channel2) => {
13895
+ const reply = await client.request(channel2, body, { timeoutMs, onProgress });
13896
+ if (this.isCurrentSession(key, record) && this.client === client) {
13897
+ this.transportFailures = 0;
13898
+ }
13899
+ this.ensureBgSubscription(identity, record);
13900
+ return reply;
13901
+ };
13902
+ let { channel, entry } = await openRoute();
13903
+ try {
13904
+ return await requestOnRoute(channel);
13905
+ } catch (err) {
13906
+ if (isUnknownChannelError(err) && this.isCurrentSession(key, record) && this.client === client) {
13907
+ clearRouteEntry(entry);
13908
+ ({ channel, entry } = await openRoute());
13909
+ try {
13910
+ return await requestOnRoute(channel);
13911
+ } catch (retryErr) {
13912
+ handleRequestFailure(retryErr, entry);
13913
+ throw retryErr;
13914
+ }
13915
+ }
13916
+ handleRequestFailure(err, entry);
13785
13917
  throw err;
13786
13918
  }
13787
13919
  } finally {
@@ -14778,7 +14910,7 @@ import {
14778
14910
  // package.json
14779
14911
  var package_default = {
14780
14912
  name: "@cortexkit/aft-pi",
14781
- version: "0.45.0",
14913
+ version: "0.46.0",
14782
14914
  type: "module",
14783
14915
  description: "Pi coding agent extension for Agent File Tools (AFT) — tree-sitter and LSP-powered code analysis",
14784
14916
  main: "dist/index.js",
@@ -14801,19 +14933,19 @@ var package_default = {
14801
14933
  "test:unit": "bun test src/__tests__ --path-ignore-patterns 'src/__tests__/e2e/**'"
14802
14934
  },
14803
14935
  dependencies: {
14804
- "@cortexkit/aft-bridge": "0.45.0",
14936
+ "@cortexkit/aft-bridge": "0.46.0",
14805
14937
  "comment-json": "^5.0.0",
14806
14938
  diff: "^8.0.4",
14807
14939
  typebox: "1.1.38",
14808
14940
  zod: "^4.4.3"
14809
14941
  },
14810
14942
  optionalDependencies: {
14811
- "@cortexkit/aft-darwin-arm64": "0.45.0",
14812
- "@cortexkit/aft-darwin-x64": "0.45.0",
14813
- "@cortexkit/aft-linux-arm64": "0.45.0",
14814
- "@cortexkit/aft-linux-x64": "0.45.0",
14815
- "@cortexkit/aft-win32-arm64": "0.45.0",
14816
- "@cortexkit/aft-win32-x64": "0.45.0"
14943
+ "@cortexkit/aft-darwin-arm64": "0.46.0",
14944
+ "@cortexkit/aft-darwin-x64": "0.46.0",
14945
+ "@cortexkit/aft-linux-arm64": "0.46.0",
14946
+ "@cortexkit/aft-linux-x64": "0.46.0",
14947
+ "@cortexkit/aft-win32-arm64": "0.46.0",
14948
+ "@cortexkit/aft-win32-x64": "0.46.0"
14817
14949
  },
14818
14950
  devDependencies: {
14819
14951
  "@earendil-works/pi-coding-agent": "^0.80.2",
@@ -29776,9 +29908,6 @@ var LspServerEntrySchema = exports_external.object({
29776
29908
  env: exports_external.record(exports_external.string().min(1), exports_external.string()).optional(),
29777
29909
  initialization_options: exports_external.unknown().optional()
29778
29910
  });
29779
- var LspServerSchema = LspServerEntrySchema.extend({
29780
- id: exports_external.string().trim().min(1)
29781
- });
29782
29911
  var LspConfigSchema = exports_external.object({
29783
29912
  servers: exports_external.record(exports_external.string().trim().min(1), LspServerEntrySchema).optional(),
29784
29913
  disabled: exports_external.array(exports_external.string().trim().min(1)).optional(),
@@ -29821,16 +29950,7 @@ var InspectConfigSchema = exports_external.object({
29821
29950
  tier2_soft_deadline_ms: exports_external.number().int().positive().optional(),
29822
29951
  max_drill_down_items: exports_external.number().int().positive().max(100).optional(),
29823
29952
  duplicates: exports_external.object({
29824
- lower_bound: exports_external.number().int().positive().optional(),
29825
- discard_cost: exports_external.number().int().min(0).optional(),
29826
- expected_mirrors: exports_external.array(exports_external.tuple([exports_external.string().trim().min(1), exports_external.string().trim().min(1)])).optional(),
29827
- anonymize: exports_external.object({
29828
- variables: exports_external.boolean().optional(),
29829
- fields: exports_external.boolean().optional(),
29830
- methods: exports_external.boolean().optional(),
29831
- types: exports_external.boolean().optional(),
29832
- literals: exports_external.boolean().optional()
29833
- }).optional()
29953
+ expected_mirrors: exports_external.array(exports_external.tuple([exports_external.string().trim().min(1), exports_external.string().trim().min(1)])).optional()
29834
29954
  }).optional()
29835
29955
  });
29836
29956
  var BackupConfigSchema = exports_external.object({
@@ -30112,16 +30232,9 @@ function mergeInspectConfig(baseInspect, overrideInspect) {
30112
30232
  ...overrideInspect,
30113
30233
  duplicates: baseInspect?.duplicates || overrideInspect?.duplicates ? {
30114
30234
  ...baseInspect?.duplicates,
30115
- ...overrideInspect?.duplicates,
30116
- anonymize: baseInspect?.duplicates?.anonymize || overrideInspect?.duplicates?.anonymize ? {
30117
- ...baseInspect?.duplicates?.anonymize,
30118
- ...overrideInspect?.duplicates?.anonymize
30119
- } : undefined
30235
+ ...overrideInspect?.duplicates
30120
30236
  } : undefined
30121
30237
  };
30122
- if (inspect.duplicates && inspect.duplicates.anonymize === undefined) {
30123
- delete inspect.duplicates.anonymize;
30124
- }
30125
30238
  if (Object.values(inspect).every((value) => value === undefined)) {
30126
30239
  return;
30127
30240
  }
@@ -32445,80 +32558,52 @@ async function assertExternalDirectoryPermission(extCtx, target, options = {}) {
32445
32558
  return;
32446
32559
  throw new Error(`Blocked: '${absoluteTarget}' is outside the project root and restrict_to_project_root is ` + "enabled (AFT full isolation). Not overridable per-call; set restrict_to_project_root: false " + "in aft.jsonc to allow external paths.");
32447
32560
  }
32448
- var ReadParams = Type2.Union([
32449
- Type2.Object({
32450
- path: Type2.String({
32451
- description: "Path to the file to read (absolute or relative to project root)"
32452
- }),
32453
- filePath: Type2.Optional(Type2.String({
32454
- description: "Compatibility alias for `path`. Used only when `path` is absent."
32455
- })),
32456
- offset: optionalInt(1, Number.MAX_SAFE_INTEGER),
32457
- limit: optionalInt(1, Number.MAX_SAFE_INTEGER)
32458
- }),
32459
- Type2.Object({
32460
- path: Type2.Optional(Type2.String({
32461
- description: "Path to the file to read (absolute or relative to project root)"
32462
- })),
32463
- filePath: Type2.String({
32464
- description: "Compatibility alias for `path`. Used only when `path` is absent."
32465
- }),
32466
- offset: optionalInt(1, Number.MAX_SAFE_INTEGER),
32467
- limit: optionalInt(1, Number.MAX_SAFE_INTEGER)
32468
- })
32469
- ]);
32470
- var WriteParams = Type2.Union([
32471
- Type2.Object({
32472
- filePath: Type2.String({
32473
- description: "Path to the file to write (absolute or relative to project root)"
32474
- }),
32475
- path: Type2.Optional(Type2.String({
32476
- description: "Compatibility alias for `filePath`. Used only when `filePath` is absent."
32477
- })),
32478
- content: Type2.String({ description: "Full file contents to write" })
32479
- }),
32480
- Type2.Object({
32481
- filePath: Type2.Optional(Type2.String({
32482
- description: "Path to the file to write (absolute or relative to project root)"
32483
- })),
32484
- path: Type2.String({
32485
- description: "Compatibility alias for `filePath`. Used only when `filePath` is absent."
32486
- }),
32487
- content: Type2.String({ description: "Full file contents to write" })
32488
- })
32489
- ]);
32490
- var EditParams = Type2.Union([
32491
- Type2.Object({
32492
- filePath: Type2.String({
32493
- description: "Path to the file to edit (absolute or relative to project root)"
32494
- }),
32495
- path: Type2.Optional(Type2.String({
32496
- description: "Compatibility alias for `filePath`. Used only when `filePath` is absent."
32497
- })),
32498
- oldString: Type2.Optional(Type2.String({ description: "Text to find (exact match, fuzzy fallback)" })),
32499
- newString: Type2.Optional(Type2.String({ description: "Replacement text (omit to delete match)" })),
32500
- replaceAll: Type2.Optional(Type2.Boolean({ description: "Replace every occurrence" })),
32501
- occurrence: optionalInt(0, Number.MAX_SAFE_INTEGER),
32502
- appendContent: Type2.Optional(Type2.String({
32503
- description: "Append text to the end of the file (creates the file if missing, parent dirs auto-created). When set, oldString/newString are ignored."
32504
- }))
32505
- }),
32506
- Type2.Object({
32507
- filePath: Type2.Optional(Type2.String({
32508
- description: "Path to the file to edit (absolute or relative to project root)"
32509
- })),
32510
- path: Type2.String({
32511
- description: "Compatibility alias for `filePath`. Used only when `filePath` is absent."
32512
- }),
32513
- oldString: Type2.Optional(Type2.String({ description: "Text to find (exact match, fuzzy fallback)" })),
32514
- newString: Type2.Optional(Type2.String({ description: "Replacement text (omit to delete match)" })),
32515
- replaceAll: Type2.Optional(Type2.Boolean({ description: "Replace every occurrence" })),
32516
- occurrence: optionalInt(0, Number.MAX_SAFE_INTEGER),
32517
- appendContent: Type2.Optional(Type2.String({
32518
- description: "Append text to the end of the file (creates the file if missing, parent dirs auto-created). When set, oldString/newString are ignored."
32519
- }))
32520
- })
32521
- ]);
32561
+ var ReadParams = Type2.Object({
32562
+ path: Type2.Optional(Type2.String({
32563
+ description: "Path to the file to read (absolute or relative to project root)"
32564
+ })),
32565
+ filePath: Type2.Optional(Type2.String({
32566
+ description: "Alias for `path` — provide one of the two."
32567
+ })),
32568
+ offset: optionalInt(1, Number.MAX_SAFE_INTEGER),
32569
+ limit: optionalInt(1, Number.MAX_SAFE_INTEGER)
32570
+ });
32571
+ var WriteParams = Type2.Object({
32572
+ filePath: Type2.Optional(Type2.String({
32573
+ description: "Path to the file to write (absolute or relative to project root)"
32574
+ })),
32575
+ path: Type2.Optional(Type2.String({
32576
+ description: "Alias for `filePath` — provide one of the two."
32577
+ })),
32578
+ content: Type2.String({ description: "Full file contents to write" })
32579
+ });
32580
+ var BatchEditParams = Type2.Object({
32581
+ oldString: Type2.Optional(Type2.String({ description: "Text to find for a batch find/replace edit" })),
32582
+ newString: Type2.Optional(Type2.String({ description: "Replacement text for a batch find/replace edit" })),
32583
+ startLine: Type2.Optional(Type2.Any({ description: "1-based start line for a batch line-range edit" })),
32584
+ endLine: Type2.Optional(Type2.Any({ description: "1-based end line for a batch line-range edit" })),
32585
+ content: Type2.Optional(Type2.String({
32586
+ description: "Replacement text for a batch line-range edit (empty string deletes the lines)"
32587
+ }))
32588
+ });
32589
+ var EditParams = Type2.Object({
32590
+ filePath: Type2.Optional(Type2.String({
32591
+ description: "Path to the file to edit (absolute or relative to project root)"
32592
+ })),
32593
+ path: Type2.Optional(Type2.String({
32594
+ description: "Alias for `filePath` — provide one of the two."
32595
+ })),
32596
+ oldString: Type2.Optional(Type2.String({ description: "Text to find (exact match, fuzzy fallback)" })),
32597
+ newString: Type2.Optional(Type2.String({ description: "Replacement text (omit to delete match)" })),
32598
+ replaceAll: Type2.Optional(Type2.Boolean({ description: "Replace every occurrence" })),
32599
+ occurrence: optionalInt(0, Number.MAX_SAFE_INTEGER),
32600
+ appendContent: Type2.Optional(Type2.String({
32601
+ description: "Append text to the end of the file (creates the file if missing, parent dirs auto-created). When set, edits/oldString/newString are ignored."
32602
+ })),
32603
+ edits: Type2.Optional(Type2.Array(BatchEditParams, {
32604
+ description: "Batch edits — array of { oldString, newString } or { startLine, endLine, content } objects applied atomically to one file."
32605
+ }))
32606
+ });
32522
32607
  var GrepParams = Type2.Object({
32523
32608
  pattern: Type2.String({ description: "Regex pattern to search for" }),
32524
32609
  path: Type2.Optional(Type2.String({
@@ -32533,6 +32618,47 @@ function readPathArg(args) {
32533
32618
  function mutationFilePathArg(args) {
32534
32619
  return coerceAliasedStringParam(args.filePath, args.path);
32535
32620
  }
32621
+ function hasOwn(record2, key) {
32622
+ return Object.hasOwn(record2, key);
32623
+ }
32624
+ function validateBatchEdit(edit, index) {
32625
+ if (!edit || typeof edit !== "object" || Array.isArray(edit)) {
32626
+ throw new Error(`batch: edit[${index}] must be an object`);
32627
+ }
32628
+ const record2 = edit;
32629
+ if (typeof record2.oldString === "string") {
32630
+ return;
32631
+ }
32632
+ if (hasOwn(record2, "startLine")) {
32633
+ if (typeof record2.startLine !== "number" || !Number.isInteger(record2.startLine) || record2.startLine < 0) {
32634
+ throw new Error(`batch: edit[${index}] 'startLine' must be a positive integer (1-based)`);
32635
+ }
32636
+ if (record2.startLine === 0) {
32637
+ throw new Error(`batch: edit[${index}] 'startLine' must be >= 1 (1-based)`);
32638
+ }
32639
+ if (typeof record2.endLine !== "number" || !Number.isInteger(record2.endLine) || record2.endLine < 0) {
32640
+ throw new Error(`batch: edit[${index}] 'endLine' must be a positive integer (1-based)`);
32641
+ }
32642
+ if (record2.endLine === 0) {
32643
+ throw new Error(`batch: edit[${index}] 'endLine' must be >= 1 (1-based)`);
32644
+ }
32645
+ return;
32646
+ }
32647
+ throw new Error(`batch: edit[${index}] must have either 'oldString' or 'startLine'/'endLine'`);
32648
+ }
32649
+ function validateBatchEdits(edits) {
32650
+ if (edits === undefined)
32651
+ return;
32652
+ if (!Array.isArray(edits)) {
32653
+ throw new Error("batch: missing required param 'edits' (expected array)");
32654
+ }
32655
+ if (edits.length === 0) {
32656
+ throw new Error("batch: 'edits' array must not be empty");
32657
+ }
32658
+ edits.forEach((edit, index) => {
32659
+ validateBatchEdit(edit, index);
32660
+ });
32661
+ }
32536
32662
  function renderReadCall(args, theme, context) {
32537
32663
  const text = reuseText(context.lastComponent);
32538
32664
  const filePath = args ? readPathArg(args) : undefined;
@@ -32553,7 +32679,7 @@ function registerHoistedTools(pi, ctx, surface) {
32553
32679
  const bridge = bridgeFor(ctx, extCtx.cwd);
32554
32680
  const pathArg = readPathArg(params);
32555
32681
  if (typeof pathArg !== "string") {
32556
- throw new Error("read: missing required `path`");
32682
+ throw new Error("read: missing required parameter `path`");
32557
32683
  }
32558
32684
  const offset = coerceOptionalInt(params.offset, "offset", 1, Number.MAX_SAFE_INTEGER);
32559
32685
  const limit = coerceOptionalInt(params.limit, "limit", 1, Number.MAX_SAFE_INTEGER);
@@ -32611,7 +32737,7 @@ PDFs aren't supported on the Pi harness yet.`, response);
32611
32737
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
32612
32738
  const filePathArg = mutationFilePathArg(params);
32613
32739
  if (typeof filePathArg !== "string") {
32614
- throw new Error("write: missing required `filePath`");
32740
+ throw new Error("write: missing required parameter `filePath`");
32615
32741
  }
32616
32742
  const filePath = await resolvePathArg(extCtx.cwd, filePathArg);
32617
32743
  await assertExternalDirectoryPermission(extCtx, filePath, {
@@ -32640,28 +32766,35 @@ PDFs aren't supported on the Pi harness yet.`, response);
32640
32766
  pi.registerTool({
32641
32767
  name: "edit",
32642
32768
  label: "edit",
32643
- description: "Find-and-replace edit with progressive fuzzy matching (handles whitespace and Unicode drift). Uses `filePath`, `oldString`, `newString`. Errors on multiple matches — use `occurrence` to pick one, or `replaceAll: true`.",
32644
- promptSnippet: "Targeted find-and-replace (uses filePath/oldString/newString; occurrence or replaceAll for disambiguation; fuzzy whitespace matching). Pass appendContent to append to a file (creates if missing).",
32769
+ description: "Edit part of a file via `appendContent`, batch `edits[]`, or `oldString`/`newString` find-and-replace. Mode priority: appendContent > edits > oldString. Find/replace errors on multiple matches — use `occurrence` or `replaceAll: true`.",
32770
+ promptSnippet: "Partial file edits via appendContent, edits[], or oldString/newString (mode priority: appendContent > edits > oldString).",
32645
32771
  promptGuidelines: [
32646
32772
  "Prefer edit over write when changing part of an existing file.",
32647
- "Include enough surrounding context in oldString to make the match unique, or set replaceAll/occurrence explicitly.",
32648
- "Use appendContent (instead of read+write) when adding text to the end of a file."
32773
+ "Use appendContent when adding text to the end of a file.",
32774
+ "Use edits[] for multiple atomic changes in one file.",
32775
+ "Include enough surrounding context in oldString to make the match unique, or set replaceAll/occurrence explicitly."
32649
32776
  ],
32650
32777
  parameters: EditParams,
32651
32778
  async execute(_toolCallId, params, _signal, _onUpdate, extCtx) {
32779
+ const argsRecord = params;
32780
+ if (argsRecord.startLine !== undefined || argsRecord.endLine !== undefined) {
32781
+ throw new Error("edit: 'startLine'/'endLine' are not top-level parameters. " + "For line-range edits, nest them inside the `edits` array: " + '`edits: [{ startLine: N, endLine: M, content: "..." }]`. ' + "For find/replace, use `oldString`/`newString` instead.");
32782
+ }
32652
32783
  const filePathArg = mutationFilePathArg(params);
32653
32784
  if (typeof filePathArg !== "string") {
32654
- throw new Error("edit: missing required `filePath`");
32785
+ throw new Error("edit: missing required parameter `filePath`");
32655
32786
  }
32787
+ if (params.appendContent === undefined)
32788
+ validateBatchEdits(params.edits);
32656
32789
  const filePath = await resolvePathArg(extCtx.cwd, filePathArg);
32657
32790
  await assertExternalDirectoryPermission(extCtx, filePath, {
32658
32791
  restrictToProjectRoot: surface.restrictToProjectRoot
32659
32792
  });
32660
32793
  const bridge = bridgeFor(ctx, extCtx.cwd);
32661
32794
  const rawArgs = { filePath: filePathArg };
32662
- for (const key of ["appendContent", "oldString", "newString"]) {
32663
- if (params[key] !== undefined)
32664
- rawArgs[key] = params[key];
32795
+ for (const key of ["appendContent", "edits", "oldString", "newString"]) {
32796
+ if (argsRecord[key] !== undefined)
32797
+ rawArgs[key] = argsRecord[key];
32665
32798
  }
32666
32799
  if (params.replaceAll !== undefined)
32667
32800
  rawArgs.replaceAll = coerceBoolean(params.replaceAll);
@@ -32725,6 +32858,7 @@ function buildMutationResult(response) {
32725
32858
  const additions = diffObj?.additions ?? 0;
32726
32859
  const deletions = diffObj?.deletions ?? 0;
32727
32860
  const replacements = response.replacements;
32861
+ const editsApplied = response.edits_applied;
32728
32862
  const diagnostics = response.lsp_diagnostics;
32729
32863
  const truncated = diffObj?.truncated === true;
32730
32864
  const noOp = response.no_op === true;
@@ -32771,6 +32905,7 @@ ${formatDiagnosticsText(diagnostics)}`;
32771
32905
  additions,
32772
32906
  deletions,
32773
32907
  replacements,
32908
+ editsApplied,
32774
32909
  diagnostics,
32775
32910
  truncated: truncated || undefined,
32776
32911
  formatted,
@@ -32845,7 +32980,8 @@ ${theme.fg("error", errorText || "edit failed")}`);
32845
32980
  const additions = details?.additions ?? 0;
32846
32981
  const deletions = details?.deletions ?? 0;
32847
32982
  const text = reuseText(context.lastComponent);
32848
- const summary = theme.fg("success", `+${additions}/-${deletions}`);
32983
+ const countDetail = typeof details?.editsApplied === "number" && details.editsApplied > 1 ? `, ${details.editsApplied} edits` : typeof details?.replacements === "number" && details.replacements > 1 ? `, ${details.replacements} replacements` : "";
32984
+ const summary = theme.fg("success", `+${additions}/-${deletions}${countDetail}`);
32849
32985
  let suffix = "";
32850
32986
  if (details?.truncated) {
32851
32987
  suffix = ` ${theme.fg("muted", "(diff truncated)")}`;
@@ -35420,6 +35556,9 @@ var SearchParams2 = Type13.Object({
35420
35556
  includeTests: Type13.Optional(Type13.Boolean({
35421
35557
  default: false,
35422
35558
  description: "Include test files (*.test.*, *_test.rs, __tests__/, …) plus test-support, fixture, mock, snapshot, and corpus files. Defaults to false."
35559
+ })),
35560
+ path: Type13.Optional(Type13.String({
35561
+ description: "Search a different project root (absolute or ~ path). Requires that project to have been indexed by AFT."
35423
35562
  }))
35424
35563
  });
35425
35564
  function buildSemanticSections(args, payload, theme) {
@@ -35527,6 +35666,8 @@ function registerSemanticTool(pi, ctx) {
35527
35666
  req.hint = params.hint;
35528
35667
  if (params.includeTests !== undefined)
35529
35668
  req.includeTests = params.includeTests;
35669
+ if (params.path !== undefined)
35670
+ req.path = params.path;
35530
35671
  const response = await callToolCall(bridge, "search", req, extCtx);
35531
35672
  if (response.success === false) {
35532
35673
  throw new Error(response.text || response.message || "search failed");
@@ -35701,13 +35842,12 @@ var PLUGIN_VERSION = (() => {
35701
35842
  return "0.0.0";
35702
35843
  }
35703
35844
  })();
35704
- var ANNOUNCEMENT_VERSION = "0.45.0";
35845
+ var ANNOUNCEMENT_VERSION = "0.46.0";
35705
35846
  var ANNOUNCEMENT_FEATURES = [
35706
- "Code Health verified against 12 real repositories: dead code is only reported for languages we can actually analyze (Java/Kotlin now say 'not analyzed' instead of guessing), and generated code (gen/ trees, *_pb.ts, DO NOT EDIT banners) moves out of the headline counts into its own bucket.",
35707
- "Config files (playwright/eslint/tsup/vitest *.config.ts), Storybook stories, Mocha hooks, and SvelteKit hooks/$lib imports are no longer flagged as dead code.",
35708
- "Rust module aliases, inline-module calls, and cross-crate pub use re-exports now resolve; Go interface methods (sort.Interface, Bubble Tea) are no longer flagged dead.",
35709
- "Fixed npx @cortexkit/aft doctor --fix and setup failing to download the binary on fresh installs.",
35710
- "Bridges now detect an updated aft binary on disk and respawn onto it automatically."
35847
+ "Search index changes persist on clean shutdown, so restarts and cross-worktree searches see current results.",
35848
+ "Cross-project search: sessions can search sibling checkouts read-only, serving borrowed indexes with drift warnings instead of failing.",
35849
+ "Duplicate detection requires a 10-line minimum span, ending flags on 3-4 line idiomatic blocks.",
35850
+ "Pi edit tool supports batch edits[] arrays, matching OpenCode."
35711
35851
  ];
35712
35852
  var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/DSa65w8wuf";
35713
35853
  var ALL_ONLY_TOOLS = new Set(["aft_callgraph", "aft_delete", "aft_move", "aft_refactor"]);