@cortexkit/aft-opencode 0.61.0 → 0.62.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.
@@ -9402,7 +9402,7 @@ function resolvePluginLogPath(context = {}) {
9402
9402
  var BASH_HOST_FALLBACK_BANNER = "[AFT host fallback - module transport down; no rewrites/compression/background]";
9403
9403
  var BASH_HOST_FALLBACK_MAX_OUTPUT_BYTES = 100 * 1024;
9404
9404
  var BASH_HOST_FALLBACK_MAX_TIMEOUT_MS = 10 * 60 * 1000;
9405
- var BASH_HOST_FALLBACK_REFUSAL = "AFT transport is down; only foreground execution is available in host fallback";
9405
+ var BASH_HOST_FALLBACK_REFUSAL = "AFT module connection is down";
9406
9406
  var BASH_RUNON_DESCRIPTION = 'Run on a remote build server: `linux` (2 vCPUs), or `linux,Nc` for N vCPUs such as `linux,8c`; add ",net" for outbound internet (offline by default). `windows` runs the command in Git Bash on a Windows build machine (no vCPU or network options).';
9407
9407
  var BASH_RUNON_GUIDANCE = 'When remote runs are available, put `runon: "linux"` on build and test lines (cargo, bun test), including chains and pipes. Keep git, gh, interactive and file-editing commands local, and keep a line local if it needs macOS (Seatbelt, codesign, launchd, TCC, AppKit) or runs binaries built on this machine: a remote build leaves no binaries or target/ output in the local worktree. Add `,Nc` to request N vCPUs (`linux,4c`); without it the job gets 2 vCPUs. The job sees exactly the CPUs it gets, so ask for what the command uses: 2c (the default) for a single test binary or script, 4c for `cargo check`/clippy on one crate, 8c for a workspace test suite, and 16c for a large release build.';
9408
9408
  function pathKeyForPlatform(env, platform) {
@@ -11226,7 +11226,7 @@ function isEmptyParam(value) {
11226
11226
  return false;
11227
11227
  }
11228
11228
  function coerceJsonCollectionParam(value, field) {
11229
- const shape = field === "targets" ? "an array of {path, symbol}" : "a string or array of strings";
11229
+ const shape = field !== "sections" ? "an array of {target, symbols}" : "a string or array of strings";
11230
11230
  if (typeof value === "string") {
11231
11231
  const trimmed = value.trim();
11232
11232
  if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
@@ -11235,13 +11235,13 @@ function coerceJsonCollectionParam(value, field) {
11235
11235
  } catch {
11236
11236
  throw new Error(`${field} must be ${shape} (got a string that is not valid JSON)`);
11237
11237
  }
11238
- } else if (field === "targets") {
11238
+ } else if (field !== "sections") {
11239
11239
  throw new Error(`${field} must be ${shape} (got a string that is not valid JSON)`);
11240
11240
  }
11241
11241
  }
11242
11242
  if (value === undefined || value === null)
11243
11243
  return value;
11244
- if (field === "targets") {
11244
+ if (field !== "sections") {
11245
11245
  if (Array.isArray(value) || typeof value === "object")
11246
11246
  return value;
11247
11247
  } else if (typeof value === "string" || Array.isArray(value) && value.every((entry) => typeof entry === "string")) {
@@ -11264,7 +11264,7 @@ function usableZoomTargets(targets) {
11264
11264
  if (typeof entry !== "object")
11265
11265
  return true;
11266
11266
  const record = entry;
11267
- return !(isBlankParam(record.path) && isBlankParam(record.filePath) && isBlankParam(record.symbol));
11267
+ return !(isBlankParam(record.target) && isBlankParam(record.url) && isBlankParam(record.symbols) && isBlankParam(record.path) && isBlankParam(record.filePath) && isBlankParam(record.symbol));
11268
11268
  });
11269
11269
  }
11270
11270
  function isFindReplaceOnlyEdit(args) {
@@ -11448,6 +11448,7 @@ function aftLiveConfigKeys(resolveBash) {
11448
11448
  return [
11449
11449
  pathKey("configure_warnings_delivery"),
11450
11450
  pathKey("restrict_to_project_root"),
11451
+ pathKey("revision_reads"),
11451
11452
  pathKey("inspect.diagnostics_timeout_ms"),
11452
11453
  pathKey("inspect.tier2_idle_minutes"),
11453
11454
  pathKey("lsp.idle_minutes"),
@@ -11465,7 +11466,8 @@ function aftLiveConfigKeys(resolveBash) {
11465
11466
  bashKey("runon_enabled", resolveBash),
11466
11467
  bashKey("subagent_background", resolveBash),
11467
11468
  bashKey("watch_sync_max_ms", resolveBash),
11468
- bashKey("worker_wait_max_ms", resolveBash)
11469
+ bashKey("worker_wait_max_ms", resolveBash),
11470
+ bashKey("env", resolveBash)
11469
11471
  ];
11470
11472
  }
11471
11473
  function sameValue(a, b) {
@@ -15342,6 +15344,47 @@ function isSocketDeathError(error, depth = 0) {
15342
15344
  }
15343
15345
  return isConsumerReconnectTransient(error) || isSubcClientClosedError(error);
15344
15346
  }
15347
+ var REQUEST_CONNECTION_LOSS_CODES = new Set(["ECONNABORTED", "ECONNRESET", "EPIPE"]);
15348
+ function hasRequestConnectionLossCode(error) {
15349
+ if (typeof error !== "object" || error === null || !("code" in error))
15350
+ return false;
15351
+ return typeof error.code === "string" && REQUEST_CONNECTION_LOSS_CODES.has(error.code);
15352
+ }
15353
+ function hasRequestConnectionLossEvidence(error, depth = 0) {
15354
+ if (error instanceof SocketClosedError)
15355
+ return true;
15356
+ if (error instanceof SubcError && error.closeReason === "connection_lost")
15357
+ return true;
15358
+ if (hasRequestConnectionLossCode(error))
15359
+ return true;
15360
+ if (!(error instanceof SubcCallError) || error.kind !== "outcome_unknown" || depth >= 4 || error.cause === undefined || error.cause === error) {
15361
+ return false;
15362
+ }
15363
+ return hasRequestConnectionLossEvidence(error.cause, depth + 1);
15364
+ }
15365
+ function connectionClosedAfterDispatch(error) {
15366
+ if (error instanceof SubcCallError && error.kind === "not_sent")
15367
+ return error;
15368
+ if (typeof error === "object" && error !== null && "kind" in error && error.kind === "not_sent") {
15369
+ return error;
15370
+ }
15371
+ if (!hasRequestConnectionLossEvidence(error))
15372
+ return error;
15373
+ if (error instanceof SubcCallError)
15374
+ return error;
15375
+ if (!(error instanceof Error))
15376
+ return error;
15377
+ return Object.assign(error, { kind: "outcome_unknown" });
15378
+ }
15379
+ var dispatchedRequestDeadlines = new WeakSet;
15380
+ function noteDispatchedRequestDeadline(error) {
15381
+ if (error instanceof SubcError && error.code === REQUEST_DEADLINE_CODE) {
15382
+ dispatchedRequestDeadlines.add(error);
15383
+ }
15384
+ }
15385
+ function isDispatchedRequestDeadlineError(error) {
15386
+ return typeof error === "object" && error !== null && dispatchedRequestDeadlines.has(error);
15387
+ }
15345
15388
  function isUnansweredCallError(error) {
15346
15389
  if (isSocketDeathError(error))
15347
15390
  return false;
@@ -15948,7 +15991,24 @@ class SubcTransport {
15948
15991
  return {};
15949
15992
  const preview = options.preview;
15950
15993
  const timeoutMs = options.transportTimeoutMs ?? options.timeoutMs;
15951
- const onProgress = options.onProgress;
15994
+ const callback = options.onProgress;
15995
+ const onProgress = callback ? (body) => {
15996
+ let frame;
15997
+ try {
15998
+ frame = JSON.parse(new TextDecoder().decode(body));
15999
+ } catch {
16000
+ return;
16001
+ }
16002
+ if (typeof frame !== "object" || frame === null)
16003
+ return;
16004
+ const progress = frame;
16005
+ if (progress.type !== "progress" || typeof progress.chunk !== "string")
16006
+ return;
16007
+ callback({
16008
+ kind: progress.kind === "stderr" ? "stderr" : "stdout",
16009
+ text: progress.chunk
16010
+ });
16011
+ } : undefined;
15952
16012
  return { preview, timeoutMs, onProgress, abortSignal: options.abortSignal };
15953
16013
  }
15954
16014
  }
@@ -15960,7 +16020,7 @@ class SubcTransportPool {
15960
16020
  consumerIdentity;
15961
16021
  hostScopes;
15962
16022
  hostScopeCache = new Map;
15963
- hostScopeRefusals = new Set;
16023
+ hostScopeRefusals = new Map;
15964
16024
  hostScopeOutcomes = new Map;
15965
16025
  connectFn;
15966
16026
  onBgEventsNudge;
@@ -16847,7 +16907,7 @@ class SubcTransportPool {
16847
16907
  this.resetRouteReopenBackoff();
16848
16908
  return reply;
16849
16909
  } catch (requestError) {
16850
- const error = frameReadTimeoutError(routeClosedMidCallError(requestError, routeAndEntry.entry, record));
16910
+ const error = frameReadTimeoutError(routeClosedMidCallError(connectionClosedAfterDispatch(requestError), routeAndEntry.entry, record));
16851
16911
  if (this.isReapInduced(record))
16852
16912
  throw this.annotateReapError(error, record);
16853
16913
  const ownsRoute = this.isCurrentSession(key, record) && this.client === client;
@@ -16881,6 +16941,7 @@ class SubcTransportPool {
16881
16941
  continue;
16882
16942
  }
16883
16943
  handleRequestFailure(error, routeAndEntry.entry);
16944
+ noteDispatchedRequestDeadline(error);
16884
16945
  throw error;
16885
16946
  }
16886
16947
  }
@@ -17014,20 +17075,38 @@ class SubcTransportPool {
17014
17075
  return { route: await opening, entry };
17015
17076
  }
17016
17077
  hostScope(client, identity) {
17017
- if (this.hostScopeRefusals.has(identity.session))
17018
- return Promise.resolve(undefined);
17078
+ let lookupReason = "cache_miss";
17079
+ const refused = this.hostScopeRefusals.get(identity.session);
17080
+ if (refused) {
17081
+ const previous = this.hostScopeCache.get(identity.session);
17082
+ if (previous)
17083
+ this.cancelHostScopeRetry(previous);
17084
+ this.hostScopeCache.delete(identity.session);
17085
+ this.hostScopeRefusals.delete(identity.session);
17086
+ lookupReason = `previous_bind_refusal code=${refused}`;
17087
+ }
17019
17088
  let state = this.hostScopeCache.get(identity.session);
17020
17089
  if (!state) {
17021
17090
  state = { retryAt: 0, retryMs: HOST_SCOPE_RETRY_FLOOR_MS };
17022
17091
  this.hostScopeCache.set(identity.session, state);
17023
17092
  }
17024
17093
  if (state.answer) {
17025
- return Promise.resolve(state.answer.result === "scope" && !state.scopeBindPending ? state.answer.scope : undefined);
17094
+ const reason = state.scopeBindPending ? "scope_sync_pending" : `cached_${state.answer.result}`;
17095
+ log(`subc head scope: session=${identity.session} lookup=skipped reason=${reason}`);
17096
+ return Promise.resolve({
17097
+ scope: state.answer.result === "scope" && !state.scopeBindPending ? state.answer.scope : undefined,
17098
+ reason
17099
+ });
17026
17100
  }
17027
- if (state.pending)
17101
+ if (state.pending) {
17102
+ log(`subc head scope: session=${identity.session} lookup=skipped reason=inflight_lookup`);
17028
17103
  return state.pending;
17029
- if (Date.now() < state.retryAt)
17030
- return Promise.resolve(undefined);
17104
+ }
17105
+ if (Date.now() < state.retryAt) {
17106
+ log(`subc head scope: session=${identity.session} lookup=skipped reason=backoff retry_in=${state.retryAt - Date.now()}ms`);
17107
+ return Promise.resolve({ reason: state.unscopedReason ?? "lookup_backoff" });
17108
+ }
17109
+ log(`subc head scope: session=${identity.session} lookup=started reason=${lookupReason}`);
17031
17110
  const current = state;
17032
17111
  clearTimeout(current.retryTimer);
17033
17112
  current.retryTimer = undefined;
@@ -17035,24 +17114,32 @@ class SubcTransportPool {
17035
17114
  current.lookupAbort = abort;
17036
17115
  const lookup = this.readHostScope(client, identity, abort.signal).then((outcome) => {
17037
17116
  if (this.hostScopeCache.get(identity.session) !== current)
17038
- return;
17117
+ return { reason: "lookup_invalidated" };
17039
17118
  current.pending = undefined;
17040
17119
  current.lookupAbort = undefined;
17041
17120
  if (abort.signal.aborted)
17042
- return;
17121
+ return { reason: "lookup_cancelled" };
17043
17122
  this.logHostScopeOutcome(identity.session, outcome);
17123
+ const reason = outcome.result === "error" ? `lookup_error code=${outcome.code}` : `lookup_${outcome.result}`;
17044
17124
  if (outcome.result === "scope" || outcome.result === "no_scope") {
17045
17125
  current.answer = outcome;
17046
17126
  current.retryAt = 0;
17047
17127
  current.retryMs = HOST_SCOPE_RETRY_FLOOR_MS;
17048
- if (outcome.result === "scope")
17049
- this.promoteHostScope(identity.session);
17128
+ if (outcome.result === "scope") {
17129
+ current.promoteAfterBind = Boolean(refused);
17130
+ if (!current.promoteAfterBind)
17131
+ this.promoteHostScope(identity.session);
17132
+ }
17050
17133
  } else {
17134
+ current.unscopedReason = reason;
17051
17135
  current.retryAt = Date.now() + current.retryMs;
17052
17136
  current.retryMs = Math.min(current.retryMs * 2, HOST_SCOPE_RETRY_CAP_MS);
17053
17137
  this.scheduleHostScopeRetry(identity.session, current);
17054
17138
  }
17055
- return outcome.result === "scope" ? outcome.scope : undefined;
17139
+ return {
17140
+ scope: outcome.result === "scope" ? outcome.scope : undefined,
17141
+ reason
17142
+ };
17056
17143
  });
17057
17144
  current.pending = lookup;
17058
17145
  return lookup;
@@ -17099,7 +17186,7 @@ class SubcTransportPool {
17099
17186
  retry = this.hostScope(client, record.identity);
17100
17187
  }
17101
17188
  retry.catch((error) => {
17102
- warn(`subc head scope: background retry failed code=${error instanceof Error ? error.name : "Error"}`);
17189
+ warn(`subc head scope: session=${session} background retry failed code=${error instanceof Error ? error.name : "Error"}`);
17103
17190
  });
17104
17191
  }, Math.max(0, state.retryAt - Date.now()));
17105
17192
  state.retryTimer.unref();
@@ -17115,8 +17202,8 @@ class SubcTransportPool {
17115
17202
  }
17116
17203
  refuseHostScopeBind(session, state, code) {
17117
17204
  if (!this.hostScopeRefusals.has(session)) {
17118
- this.hostScopeRefusals.add(session);
17119
- warn(`subc head scope: session=${session} scoped bind refused code=${code}; binding unscoped; retry stopped`);
17205
+ this.hostScopeRefusals.set(session, code);
17206
+ warn(`subc head scope: session=${session} scoped bind refused code=${code}; retry stopped until next bind lookup`);
17120
17207
  }
17121
17208
  if (state) {
17122
17209
  state.scopeBindPending = false;
@@ -17125,11 +17212,12 @@ class SubcTransportPool {
17125
17212
  }
17126
17213
  async retryHostScopeBind(client, identity, state, scope) {
17127
17214
  try {
17128
- const route = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, { consumerIdentity: this.consumerIdentity, scope });
17215
+ const route = await this.bindHostToolRoute(client, identity, "scope_probe", "scope_sync_retry", scope);
17129
17216
  safeCloseRoute(client, route);
17130
17217
  if (this.hostScopeCache.get(identity.session) !== state || !state.scopeBindPending || this.shuttingDown || client !== this.client || !this.unscopedHostRecord(identity.session))
17131
17218
  return;
17132
17219
  state.scopeBindPending = false;
17220
+ state.promoteAfterBind = false;
17133
17221
  state.retryAt = 0;
17134
17222
  state.retryMs = HOST_SCOPE_RETRY_FLOOR_MS;
17135
17223
  log(`subc head scope: session=${identity.session} scoped bind recovered ref=${scope.ref.slice(0, 8)} epoch=${scope.scopeEpoch}`);
@@ -17255,24 +17343,39 @@ class SubcTransportPool {
17255
17343
  signal.removeEventListener("abort", cancel);
17256
17344
  }
17257
17345
  }
17258
- openHostToolRoute(client, identity, record, entry) {
17346
+ openHostToolRoute(client, identity, record, entry, channel = "tool") {
17259
17347
  if (!this.hostScopes || !["opencode", "pi"].includes(identity.harness) || identity.session === DEFAULT_SESSION_ID) {
17260
- return client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, {
17261
- consumerIdentity: this.consumerIdentity
17262
- });
17348
+ const reason = !this.hostScopes ? "host_scopes_disabled" : identity.session === DEFAULT_SESSION_ID ? "default_session" : "unsupported_harness";
17349
+ log(`subc head scope: session=${identity.session} lookup=skipped reason=${reason}`);
17350
+ return this.bindHostToolRoute(client, identity, channel, reason);
17263
17351
  }
17264
- return this.openScopedHostToolRoute(client, identity, record, entry);
17352
+ return this.openScopedHostToolRoute(client, identity, record, entry, channel);
17265
17353
  }
17266
- async openScopedHostToolRoute(client, identity, record, entry) {
17267
- const scope = await this.hostScope(client, identity);
17354
+ bindHostToolRoute(client, identity, channel, reason, scope) {
17355
+ log(`subc head scope: session=${identity.session} bind=${scope ? "scoped" : "unscoped"} channel=${channel} root=${identity.project_root} reason=${reason}` + (scope ? ` ref=${scope.ref.slice(0, 8)} epoch=${scope.scopeEpoch}` : ""));
17356
+ return client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, {
17357
+ consumerIdentity: this.consumerIdentity,
17358
+ ...scope ? { scope } : {}
17359
+ });
17360
+ }
17361
+ async openScopedHostToolRoute(client, identity, record, entry, channel = "tool") {
17362
+ const decision = await this.hostScope(client, identity);
17363
+ const scope = decision.scope;
17364
+ let reason = decision.reason;
17268
17365
  this.assertRecordLive(record);
17269
17366
  if (scope) {
17270
17367
  try {
17271
- const route = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, { consumerIdentity: this.consumerIdentity, scope });
17368
+ const route = await this.bindHostToolRoute(client, identity, channel, reason, scope);
17272
17369
  if (entry)
17273
17370
  entry.hostScope = scope;
17371
+ const state = this.hostScopeCache.get(identity.session);
17372
+ if (state?.promoteAfterBind && state.answer?.result === "scope" && state.answer.scope === scope) {
17373
+ state.promoteAfterBind = false;
17374
+ this.promoteHostScope(identity.session);
17375
+ }
17274
17376
  return route;
17275
17377
  } catch (error) {
17378
+ log(`subc head scope: session=${identity.session} bind=failed channel=${channel} reason=scoped_open_error code=${error instanceof SubcError ? error.code ?? error.name : "Error"}`);
17276
17379
  if (!(error instanceof SubcError) || !error.code || isSocketDeathError(error))
17277
17380
  throw error;
17278
17381
  if (isRouteOpenReloadWindowError(error))
@@ -17281,18 +17384,21 @@ class SubcTransportPool {
17281
17384
  const state = this.hostScopeCache.get(identity.session);
17282
17385
  if (state && isHostScopeSyncRefusal(error.code)) {
17283
17386
  this.deferHostScopeBind(identity.session, state, error.code);
17387
+ reason = `scope_sync_pending code=${error.code}`;
17284
17388
  } else {
17285
17389
  this.refuseHostScopeBind(identity.session, state, error.code);
17390
+ if (decision.reason === "cached_scope") {
17391
+ return this.openScopedHostToolRoute(client, identity, record, entry, channel);
17392
+ }
17393
+ reason = `scoped_bind_refused code=${error.code}`;
17286
17394
  }
17287
17395
  }
17288
17396
  }
17289
17397
  this.assertRecordLive(record);
17290
- const route = await client.routeOpen({ kind: "tool_provider", module_id: AFT_MODULE_ID }, identity, {
17291
- consumerIdentity: this.consumerIdentity
17292
- });
17398
+ const route = await this.bindHostToolRoute(client, identity, channel, reason);
17293
17399
  if (this.hostScopeCache.get(identity.session)?.answer?.result === "scope" && !this.hostScopeCache.get(identity.session)?.scopeBindPending && !this.hostScopeRefusals.has(identity.session)) {
17294
17400
  safeCloseRoute(client, route);
17295
- return this.openScopedHostToolRoute(client, identity, record, entry);
17401
+ return this.openScopedHostToolRoute(client, identity, record, entry, channel);
17296
17402
  }
17297
17403
  return route;
17298
17404
  }
@@ -17389,7 +17495,7 @@ class SubcTransportPool {
17389
17495
  };
17390
17496
  sub = new BgSubscription(identity, () => this.ensureClient(), (client, cause) => this.dropClient(client, cause), (client) => {
17391
17497
  binding.hostScope = undefined;
17392
- return this.openHostToolRoute(client, identity, record, binding);
17498
+ return this.openHostToolRoute(client, identity, record, binding, "bg_events");
17393
17499
  }, onNudge, this.bgBackoffSleep, () => this.rootCanAttach(record.canonicalRoot), (error) => {
17394
17500
  if (!isAbsentRootRouteError(error))
17395
17501
  return false;
@@ -17682,11 +17788,54 @@ function shellTimeoutDisposition(outcome) {
17682
17788
  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.";
17683
17789
  var BRIDGE_TRANSPORT_UNKNOWN_OUTCOME_DISPOSITION = "The standalone AFT transport failed after this call may have been sent, so its outcome is UNKNOWN: it may or may not have executed. Verify actual state before re-running, and never blind-retry a mutation.";
17684
17790
  var SUBC_ROUTE_CLOSED_MID_CALL_DISPOSITION = "The plugin closed this call's route while the call was in flight, so its outcome is UNKNOWN: it may or may not have executed. Verify actual state before re-running, and never blind-retry a mutation.";
17791
+ var SUBC_CONNECTION_CLOSED_MID_CALL_DISPOSITION = "The AFT module connection closed while this request was in flight, so its outcome is UNKNOWN: it may or may not have executed. Verify actual state before retrying, and never blindly retry a mutation.";
17792
+ function bashConnectionClosedDisposition(taskId) {
17793
+ const taskFollowup = taskId ? `Use bash_status or bash_watch with task ID ${taskId} to check it.` : "If a task ID is available, use it with bash_status or bash_watch; otherwise wait for the completion notice.";
17794
+ return `The command may have run or may still be running. AFT keeps commands running when the connection drops; a completion notice will follow if it ran. ${taskFollowup} Do not rerun it.`;
17795
+ }
17796
+
17797
+ class AftModuleUnavailableError extends SubcCallError {
17798
+ constructor(message, cause) {
17799
+ super("not_sent", message, "aft_module_unavailable", cause);
17800
+ this.name = "AftModuleUnavailableError";
17801
+ }
17802
+ }
17803
+ function refuseAftModuleOptionWithoutFallback(error, option, refusalPrefix) {
17804
+ return new AftModuleUnavailableError(`${refusalPrefix}; ${option} has no host fallback. Nothing was started, so it is safe to retry after the AFT module connection is restored.`, error);
17805
+ }
17685
17806
  var BASH_FRAME_READ_TIMEOUT_DISPOSITION = "The command's state is UNKNOWN: the transport timed out while reading the response frame, so it may or may not have executed. Check bash_status if a task ID could have been assigned, and do not re-run side-effecting commands blindly.";
17686
17807
  var SUBC_FRAME_READ_TIMEOUT_DISPOSITION = "The transport timed out while reading the response frame for this call, so its outcome is UNKNOWN: it may or may not have executed. Verify actual state before re-running, and never blind-retry a mutation.";
17687
17808
  function isRouteClosedMidCallError(error) {
17688
17809
  return error instanceof SubcRouteClosedMidCallError || isLocalRouteCloseRejection(error);
17689
17810
  }
17811
+ function isSubcConnectionClosedMidCallError(error) {
17812
+ if (!(error instanceof Error))
17813
+ return false;
17814
+ const fields = error;
17815
+ if (fields.kind !== "outcome_unknown")
17816
+ return false;
17817
+ if (error instanceof SocketClosedError)
17818
+ return true;
17819
+ if (error instanceof SubcError)
17820
+ return error.closeReason === "connection_lost";
17821
+ if (error instanceof SubcCallError) {
17822
+ return error.closeReason === "connection_lost" || hasConnectionLossCause(error);
17823
+ }
17824
+ return hasConnectionLossCause(error);
17825
+ }
17826
+ var CONNECTION_LOSS_ERROR_CODES = new Set(["ECONNABORTED", "ECONNRESET", "EPIPE"]);
17827
+ function hasConnectionLossCause(error, depth = 0) {
17828
+ if (error instanceof SocketClosedError)
17829
+ return true;
17830
+ if (error instanceof SubcError && error.closeReason === "connection_lost")
17831
+ return true;
17832
+ if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" && CONNECTION_LOSS_ERROR_CODES.has(error.code)) {
17833
+ return true;
17834
+ }
17835
+ if (!(error instanceof Error) || depth >= 4 || error.cause === undefined)
17836
+ return false;
17837
+ return hasConnectionLossCause(error.cause, depth + 1);
17838
+ }
17690
17839
  function isRouteGoodbyeError(error) {
17691
17840
  if (!(error instanceof SubcError))
17692
17841
  return false;
@@ -17741,6 +17890,8 @@ function classifyBashHostFallbackError(error) {
17741
17890
  if (!(error instanceof Error) || hasEngineResponse(error) || isRouteGoodbyeError(error)) {
17742
17891
  return;
17743
17892
  }
17893
+ if (isSubcConnectionClosedMidCallError(error))
17894
+ return;
17744
17895
  if (error instanceof BridgeTransportUnknownOutcomeError)
17745
17896
  return;
17746
17897
  if (isRouteClosedMidCallError(error))
@@ -17767,6 +17918,8 @@ function isBashTransportDeadError(error) {
17767
17918
  function adaptToolError(command, error) {
17768
17919
  if (!(error instanceof Error))
17769
17920
  return error;
17921
+ if (error instanceof AftModuleUnavailableError)
17922
+ return error;
17770
17923
  if (isSubcFrameReadTimeoutError(error)) {
17771
17924
  const typedError = error instanceof SubcFrameReadTimeoutError ? error : new SubcFrameReadTimeoutError(error);
17772
17925
  const disposition = command === "bash" ? BASH_FRAME_READ_TIMEOUT_DISPOSITION : SUBC_FRAME_READ_TIMEOUT_DISPOSITION;
@@ -17781,6 +17934,13 @@ function adaptToolError(command, error) {
17781
17934
  error.message = error.message ? `${error.message} ${BRIDGE_TRANSPORT_UNKNOWN_OUTCOME_DISPOSITION}` : BRIDGE_TRANSPORT_UNKNOWN_OUTCOME_DISPOSITION;
17782
17935
  return error;
17783
17936
  }
17937
+ if (isSubcConnectionClosedMidCallError(error)) {
17938
+ const disposition = command === "bash" ? bashConnectionClosedDisposition(taskIdFromError(error)) : SUBC_CONNECTION_CLOSED_MID_CALL_DISPOSITION;
17939
+ if (error.message.includes(disposition))
17940
+ return error;
17941
+ error.message = error.message ? `${error.message} ${disposition}` : disposition;
17942
+ return error;
17943
+ }
17784
17944
  if (isRouteGoodbyeError(error)) {
17785
17945
  if (error.message.includes(SUBC_MODULE_RESTART_DISPOSITION))
17786
17946
  return error;
@@ -17793,6 +17953,13 @@ function adaptToolError(command, error) {
17793
17953
  error.message = error.message ? `${error.message} ${SUBC_ROUTE_CLOSED_MID_CALL_DISPOSITION}` : SUBC_ROUTE_CLOSED_MID_CALL_DISPOSITION;
17794
17954
  return error;
17795
17955
  }
17956
+ if (command === "bash" && isDispatchedRequestDeadlineError(error)) {
17957
+ const disposition = bashConnectionClosedDisposition(taskIdFromError(error));
17958
+ if (error.message.includes(disposition))
17959
+ return error;
17960
+ error.message = error.message ? `${error.message} ${disposition}` : disposition;
17961
+ return error;
17962
+ }
17796
17963
  if ((command === "bash" || command === "powershell") && isBridgeTransportTimeout(error)) {
17797
17964
  const outcome = error.shellOutcome;
17798
17965
  const disposition = shellTimeoutDisposition(outcome);
@@ -17801,6 +17968,9 @@ function adaptToolError(command, error) {
17801
17968
  error.message = error.message ? `${error.message} ${disposition}` : disposition;
17802
17969
  return error;
17803
17970
  }
17971
+ if (command !== "bash" && isBashTransportDeadError(error)) {
17972
+ return new AftModuleUnavailableError("The AFT module connection is down or unavailable; this AFT-backed option has no host fallback. Nothing was started, so it is safe to retry after the connection is restored.", error);
17973
+ }
17804
17974
  if (command !== "bash" || !isBashTransportDeadError(error))
17805
17975
  return error;
17806
17976
  if (error.message.includes(BASH_TRANSPORT_DISPOSITION))
@@ -17808,6 +17978,15 @@ function adaptToolError(command, error) {
17808
17978
  error.message = error.message ? `${error.message} ${BASH_TRANSPORT_DISPOSITION}` : BASH_TRANSPORT_DISPOSITION;
17809
17979
  return error;
17810
17980
  }
17981
+ function taskIdFromError(error) {
17982
+ for (let current = error, depth = 0;current instanceof Error && depth < 5; current = current.cause, depth += 1) {
17983
+ const fields = current;
17984
+ const taskId = fields.taskId ?? fields.task_id;
17985
+ if (typeof taskId === "string" && taskId.length > 0)
17986
+ return taskId;
17987
+ }
17988
+ return;
17989
+ }
17811
17990
  // ../aft-bridge/dist/feature-config.js
17812
17991
  import { createHash as createHash4 } from "node:crypto";
17813
17992
  var CANONICAL_TOOLS = [
@@ -22993,39 +23172,70 @@ function validateOptionalCanonicalPath(record, property) {
22993
23172
  }
22994
23173
  pathValue(record, property);
22995
23174
  }
22996
- function normalizeZoomTargets(record) {
22997
- if (!hasOwn(record, "targets"))
22998
- return;
22999
- const targets = record.targets;
23000
- const normalizeTarget = (target, index) => {
23001
- if (!target || typeof target !== "object" || Array.isArray(target)) {
23002
- throw new InvalidRequestError(`'targets[${index}].path' must be a non-empty string`);
23003
- }
23004
- const source = target;
23005
- const blank = (value) => value === undefined || value === null || typeof value === "string" && !value.trim();
23006
- const emptyTarget = blank(source.symbol) && blank(source.path) && blank(source.filePath);
23007
- if (emptyTarget)
23008
- return { ...source };
23009
- const normalized = { ...source };
23010
- try {
23011
- normalizeAliasPair(normalized, "path", "filePath", false);
23012
- } catch (error) {
23013
- if (error instanceof InvalidRequestError) {
23014
- throw new InvalidRequestError(error.message.replace("'filePath'", `'targets[${index}].filePath'`).replace("'path'", `'targets[${index}].path'`));
23015
- }
23016
- throw error;
23175
+ function normalizeZoomLocation(record, prefix = "") {
23176
+ let first;
23177
+ for (const key of ["target", "path", "filePath", "url"]) {
23178
+ const value = record[key];
23179
+ if (value === undefined || value === null || typeof value === "string" && !value.trim()) {
23180
+ delete record[key];
23181
+ continue;
23017
23182
  }
23018
- return normalized;
23019
- };
23020
- if (Array.isArray(targets)) {
23021
- if (targets.length === 0)
23022
- return;
23023
- record.targets = targets.map(normalizeTarget);
23024
- return;
23025
- }
23026
- if (targets && typeof targets === "object") {
23027
- record.targets = normalizeTarget(targets, 0);
23183
+ const location = pathValue(record, key);
23184
+ if (first && first.value !== location) {
23185
+ throw new InvalidRequestError(`Conflicting '${prefix}${first.key}' location '${first.value}' and '${prefix}${key}' '${location}': provide the same location or only one target`);
23186
+ }
23187
+ first ??= { key, value: location };
23028
23188
  }
23189
+ for (const alias of ["path", "filePath", "url"])
23190
+ delete record[alias];
23191
+ if (first)
23192
+ record.target = first.value;
23193
+ }
23194
+ function normalizeZoomTargets(record) {
23195
+ const normalizeEntry = (value, prefix) => {
23196
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
23197
+ throw new InvalidRequestError(`'${prefix}target' must be a non-empty string`);
23198
+ }
23199
+ const entry = { ...value };
23200
+ normalizeZoomLocation(entry, prefix);
23201
+ if (hasOwn(entry, "symbol")) {
23202
+ if (!isBlankParam(entry.symbols) && JSON.stringify(entry.symbols) !== JSON.stringify(entry.symbol)) {
23203
+ throw new InvalidRequestError(`Conflicting '${prefix}symbols' and '${prefix}symbol': provide the same symbols or only one spelling`);
23204
+ }
23205
+ if (isBlankParam(entry.symbols))
23206
+ entry.symbols = entry.symbol;
23207
+ delete entry.symbol;
23208
+ }
23209
+ entry.target ??= "";
23210
+ entry.symbols ??= "";
23211
+ return entry;
23212
+ };
23213
+ const batch = [];
23214
+ if (hasOwn(record, "batch")) {
23215
+ const entries = coerceJsonCollectionParam(record.batch, "batch");
23216
+ if (!Array.isArray(entries) || entries.length === 0) {
23217
+ throw new InvalidRequestError("'batch' must be a non-empty array of {target, symbols}");
23218
+ }
23219
+ batch.push(...entries.map((entry, index) => normalizeEntry(entry, `batch[${index}].`)));
23220
+ }
23221
+ normalizeZoomLocation(record);
23222
+ if (!isBlankParam(record.target) || !isBlankParam(record.symbols)) {
23223
+ const entry = {};
23224
+ if (record.target !== undefined)
23225
+ entry.target = record.target;
23226
+ if (record.symbols !== undefined)
23227
+ entry.symbols = record.symbols;
23228
+ batch.unshift(normalizeEntry(entry, ""));
23229
+ }
23230
+ if (hasOwn(record, "targets")) {
23231
+ const targets = coerceJsonCollectionParam(record.targets, "targets");
23232
+ batch.push(...usableZoomTargets(targets).map((entry, index) => normalizeEntry(entry, `targets[${index}].`)));
23233
+ }
23234
+ for (const key of ["target", "symbols", "targets"])
23235
+ delete record[key];
23236
+ if (!batch.length)
23237
+ throw new InvalidRequestError("'batch' must be a non-empty array of {target, symbols}");
23238
+ record.batch = batch;
23029
23239
  }
23030
23240
  function bareToolName(toolName) {
23031
23241
  const bare = toolName.startsWith("aft_") ? toolName.slice(4) : toolName;
@@ -23052,7 +23262,6 @@ function prepareCanonicalPathArguments(toolName, rawArguments) {
23052
23262
  normalizeAliasPair(record, "path", "filePath", true);
23053
23263
  break;
23054
23264
  case "zoom":
23055
- normalizeAliasPair(record, "path", "filePath", false);
23056
23265
  normalizeZoomTargets(record);
23057
23266
  break;
23058
23267
  case "callgraph":
@@ -23473,6 +23682,19 @@ function copyOwnProperties(source) {
23473
23682
  copy[key] = source[key];
23474
23683
  return copy;
23475
23684
  }
23685
+ // ../aft-bridge/dist/revision-reads.js
23686
+ var REVISION_REF_DESCRIPTION = "Git revision to read the file at instead of the working tree: a branch, tag, full or short commit SHA, or a form like HEAD~2. Content comes from git's object store; the working tree, index and HEAD are not touched.";
23687
+ var REVISION_REF_TOOL_NOTE = "Pass `ref` to read the file as it is at a git revision (branch, tag, SHA, HEAD~2); the reply's first line names the commit it resolved to.";
23688
+ function revisionReadsOffered(config) {
23689
+ return config.revision_reads === true;
23690
+ }
23691
+ function revisionRefArg(value) {
23692
+ if (value === undefined || value === null)
23693
+ return;
23694
+ if (typeof value === "string" && value.trim().length === 0)
23695
+ return;
23696
+ return value;
23697
+ }
23476
23698
  // ../aft-bridge/dist/semantic-status.js
23477
23699
  var MISSING_ONNX_RUNTIME_MARKER = "ONNX Runtime not found.";
23478
23700
  var MISSING_ONNX_RUNTIME_LABEL = "unavailable — ONNX Runtime missing (npx @cortexkit/aft doctor --fix)";
@@ -28495,6 +28717,41 @@ var ExperimentalConfigSchema = object({
28495
28717
  }).optional(),
28496
28718
  lsp_ty: boolean2().optional()
28497
28719
  });
28720
+ var BLOCKED_BASH_ENV_NAMES = new Set([
28721
+ "LD_PRELOAD",
28722
+ "LD_LIBRARY_PATH",
28723
+ "LD_AUDIT",
28724
+ "DYLD_INSERT_LIBRARIES",
28725
+ "DYLD_LIBRARY_PATH",
28726
+ "DYLD_FALLBACK_LIBRARY_PATH",
28727
+ "BASH_ENV",
28728
+ "ENV",
28729
+ "IFS",
28730
+ "PATH",
28731
+ "SHELLOPTS",
28732
+ "BASHOPTS",
28733
+ "PROMPT_COMMAND",
28734
+ "PS4",
28735
+ "PYTHONSTARTUP",
28736
+ "PYTHONPATH",
28737
+ "PYTHONHOME",
28738
+ "PERL5OPT",
28739
+ "PERL5LIB",
28740
+ "PERLLIB",
28741
+ "PERL5DB",
28742
+ "RUBYOPT",
28743
+ "RUBYLIB",
28744
+ "NODE_OPTIONS",
28745
+ "GCONV_PATH",
28746
+ "LOCPATH",
28747
+ "NLSPATH",
28748
+ "HOSTALIASES",
28749
+ "RESOLV_HOST_CONF"
28750
+ ]);
28751
+ function isBlockedBashEnvKey(key) {
28752
+ const normalized = process.platform === "win32" ? key.toUpperCase() : key;
28753
+ return BLOCKED_BASH_ENV_NAMES.has(normalized) || normalized.startsWith("LD_") || normalized.startsWith("DYLD_") || normalized.startsWith("BASH_FUNC_");
28754
+ }
28498
28755
  var BashFeaturesSchema = object({
28499
28756
  enabled: boolean2().optional(),
28500
28757
  rewrite: boolean2().optional(),
@@ -28514,7 +28771,18 @@ var BashFeaturesSchema = object({
28514
28771
  }).optional(),
28515
28772
  linux_scope: boolean2().optional(),
28516
28773
  disclaim_privacy: boolean2().optional(),
28517
- powershell_tool: boolean2().optional()
28774
+ powershell_tool: boolean2().optional(),
28775
+ env: record(string2(), string2()).superRefine((env, context) => {
28776
+ for (const key of Object.keys(env)) {
28777
+ if (isBlockedBashEnvKey(key)) {
28778
+ context.addIssue({
28779
+ code: "custom",
28780
+ path: [key],
28781
+ message: `bash.env key ${key} is blocked because it can alter startup or protected process state`
28782
+ });
28783
+ }
28784
+ }
28785
+ }).optional()
28518
28786
  });
28519
28787
  var BashConfigSchema = union([boolean2(), BashFeaturesSchema]);
28520
28788
  var SandboxConfigSchema = object({
@@ -28603,6 +28871,7 @@ var AftConfigFieldsSchema = object({
28603
28871
  $schema: string2().optional(),
28604
28872
  edit_mode: _enum(["default", "hashline"]).optional(),
28605
28873
  format_on_edit: boolean2().optional(),
28874
+ revision_reads: boolean2().optional(),
28606
28875
  formatter_timeout_secs: number2().int().min(1).max(600).optional(),
28607
28876
  validate_on_edit: _enum(["syntax", "full"]).optional(),
28608
28877
  formatter: record(string2(), FormatterEnum).optional(),
@@ -28727,7 +28996,8 @@ function resolveBashConfig(config) {
28727
28996
  foreground_wait_window_ms: foregroundWaitWindowMs,
28728
28997
  watch_sync_max_ms: watchSyncMaxMs,
28729
28998
  worker_wait_max_ms: (typeof top === "object" && top !== null ? top.worker_wait_max_ms : undefined) ?? DEFAULT_WORKER_WAIT_MAX_MS,
28730
- powershell_tool: typeof top === "object" && top !== null ? top.powershell_tool ?? false : false
28999
+ powershell_tool: typeof top === "object" && top !== null ? top.powershell_tool ?? false : false,
29000
+ env: typeof top === "object" && top !== null ? { ...top.env ?? {} } : {}
28731
29001
  };
28732
29002
  if (top === false) {
28733
29003
  return base;
@@ -29057,6 +29327,7 @@ function getProjectLspStrippedKeys(lsp) {
29057
29327
  var PROJECT_SAFE_TOP_LEVEL_FIELDS = new Set([
29058
29328
  "edit_mode",
29059
29329
  "format_on_edit",
29330
+ "revision_reads",
29060
29331
  "validate_on_edit",
29061
29332
  "configure_warnings_delivery",
29062
29333
  "views",
@@ -29111,6 +29382,8 @@ function getStrippedTopLevelKeys(override, base) {
29111
29382
  }
29112
29383
  if (typeof override.bash === "object" && override.bash.disclaim_privacy === false)
29113
29384
  stripped.push("bash.disclaim_privacy");
29385
+ if (typeof override.bash === "object" && override.bash.env !== undefined)
29386
+ stripped.push("bash.env");
29114
29387
  if (override.sandbox?.write_allow !== undefined)
29115
29388
  stripped.push("sandbox.write_allow");
29116
29389
  if (override.subc !== undefined)
@@ -29158,8 +29431,10 @@ function mergeConfigs(base, override) {
29158
29431
  const projectBash = typeof override.bash === "object" ? { ...override.bash } : override.bash;
29159
29432
  if (typeof projectBash === "object" && projectBash.disclaim_privacy !== true)
29160
29433
  delete projectBash.disclaim_privacy;
29161
- if (typeof projectBash === "object")
29434
+ if (typeof projectBash === "object") {
29162
29435
  delete projectBash.runon_enabled;
29436
+ delete projectBash.env;
29437
+ }
29163
29438
  const bash = mergeBashConfig(base.bash, projectBash);
29164
29439
  const inspect = mergeInspectConfig(base.inspect, override.inspect);
29165
29440
  const worktree = mergeWorktreeConfig(base.worktree, override.worktree);
@@ -29699,7 +29974,7 @@ var wakeConfirmationWindowMs = DEFAULT_WAKE_CONFIRMATION_WINDOW_MS;
29699
29974
  var busyWakeDeferCeilingMs = BUSY_WAKE_DEFER_CEILING_MS;
29700
29975
  var lastWakeMessageTimestamp = 0;
29701
29976
  var wakeMessageCounter = 0;
29702
- var subcNudgesInFlight = new Map;
29977
+ var subcNudgesInFlight = new WeakMap;
29703
29978
  var subcNudgeLogState = new Map;
29704
29979
  var deferredBusyWakes = new Map;
29705
29980
  function consumeBgCompletion(sessionID, taskId) {
@@ -29781,7 +30056,7 @@ function markExplicitControl(sessionID, taskId, trackOutstanding = true) {
29781
30056
  if (idx >= 0) {
29782
30057
  const completion = state.pendingCompletions[idx];
29783
30058
  state.pendingCompletions.splice(idx, 1);
29784
- queuePendingPatternMatch(state, completionToExitPattern(completion, true));
30059
+ queuePendingPatternMatch(state, completionToExitPattern(completion, sessionID, true));
29785
30060
  state.wakeDeferredTaskIds.delete(taskId);
29786
30061
  }
29787
30062
  }
@@ -29818,7 +30093,7 @@ function queuePendingPatternMatch(state, entry) {
29818
30093
  }
29819
30094
  state.pendingPatternMatches.push(normalized);
29820
30095
  }
29821
- function routeExplicitControlCompletions(state) {
30096
+ function routeExplicitControlCompletions(state, sessionID) {
29822
30097
  if (state.pendingCompletions.length === 0)
29823
30098
  return;
29824
30099
  const remaining = [];
@@ -29827,7 +30102,7 @@ function routeExplicitControlCompletions(state) {
29827
30102
  state.outstandingTaskIds.delete(completion.task_id);
29828
30103
  state.explicitControlTasks.delete(completion.task_id);
29829
30104
  state.wakeDeferredTaskIds.delete(completion.task_id);
29830
- queuePendingPatternMatch(state, completionToExitPattern(completion, true));
30105
+ queuePendingPatternMatch(state, completionToExitPattern(completion, sessionID, true));
29831
30106
  } else {
29832
30107
  remaining.push(completion);
29833
30108
  }
@@ -29859,7 +30134,7 @@ function ingestBgCompletions(sessionID, completions) {
29859
30134
  if (state.explicitControlTasks.has(completion.task_id)) {
29860
30135
  state.outstandingTaskIds.delete(completion.task_id);
29861
30136
  state.explicitControlTasks.delete(completion.task_id);
29862
- queuePendingPatternMatch(state, completionToExitPattern(completion, true));
30137
+ queuePendingPatternMatch(state, completionToExitPattern(completion, sessionID, true));
29863
30138
  continue;
29864
30139
  }
29865
30140
  if (isDeliveringOrAwaitingAck(state, completion.task_id)) {
@@ -29901,7 +30176,7 @@ async function appendInTurnBgCompletions(drainContext, output) {
29901
30176
  if (state.outstandingTaskIds.size > 0 || !state.forcedDrainCompleted) {
29902
30177
  await drainCompletions(drainContext);
29903
30178
  }
29904
- routeExplicitControlCompletions(state);
30179
+ routeExplicitControlCompletions(state, drainContext.sessionID);
29905
30180
  if (state.pendingCompletions.length === 0 && state.pendingLongRunning.length === 0 && state.pendingPatternMatches.length === 0)
29906
30181
  return;
29907
30182
  const deliveredCompletions = [...state.pendingCompletions];
@@ -29917,6 +30192,9 @@ async function appendInTurnBgCompletions(drainContext, output) {
29917
30192
  reminder_chars: reminder.length
29918
30193
  });
29919
30194
  state.pendingCompletions = [];
30195
+ for (const completion of deliveredCompletions) {
30196
+ consumeBgCompletion(drainContext.sessionID, completion.task_id);
30197
+ }
29920
30198
  state.heldWorkerCompletions = false;
29921
30199
  for (const completion of deliveredCompletions) {
29922
30200
  state.wakeDeferredTaskIds.delete(completion.task_id);
@@ -29940,17 +30218,22 @@ async function handleIdleBgCompletions(drainContext) {
29940
30218
  }
29941
30219
  function handleSubcBgEventsNudge(drainContext) {
29942
30220
  const key = `${drainContext.directory}\x00${drainContext.sessionID}`;
29943
- const inFlight = subcNudgesInFlight.get(key);
30221
+ let poolNudges = subcNudgesInFlight.get(drainContext.ctx.pool);
30222
+ if (!poolNudges) {
30223
+ poolNudges = new Map;
30224
+ subcNudgesInFlight.set(drainContext.ctx.pool, poolNudges);
30225
+ }
30226
+ const inFlight = poolNudges.get(key);
29944
30227
  if (inFlight) {
29945
30228
  logSubcNudgeLifecycle(drainContext, "coalesced-in-flight", "nudge coalesced cause=handler-already-in-flight");
29946
30229
  return inFlight;
29947
30230
  }
29948
30231
  logSubcNudgeLifecycle(drainContext, "handler-entry", "nudge handler entered");
29949
30232
  const handling = handleSubcBgEventsNudgeOnce(drainContext);
29950
- subcNudgesInFlight.set(key, handling);
30233
+ poolNudges.set(key, handling);
29951
30234
  const clear = () => {
29952
- if (subcNudgesInFlight.get(key) === handling)
29953
- subcNudgesInFlight.delete(key);
30235
+ if (poolNudges.get(key) === handling)
30236
+ poolNudges.delete(key);
29954
30237
  };
29955
30238
  handling.then(clear, clear);
29956
30239
  return handling;
@@ -29959,9 +30242,62 @@ async function handleSubcBgEventsNudgeOnce(drainContext) {
29959
30242
  bridgeForDrain(drainContext);
29960
30243
  const state = stateFor(drainContext.sessionID);
29961
30244
  state.wakeDeferredTaskIds.clear();
30245
+ if (drainContext.delivery) {
30246
+ await drainCompletions(drainContext);
30247
+ await deliverDrainedNotifications(drainContext, drainContext.delivery);
30248
+ return;
30249
+ }
29962
30250
  rearmHardStoppedWake(state, drainContext);
29963
30251
  await triggerWakeIfPending(drainContext, false, true, true, true);
29964
30252
  }
30253
+ async function deliverDrainedNotifications(drainContext, delivery) {
30254
+ const state = stateFor(drainContext.sessionID);
30255
+ routeExplicitControlCompletions(state, drainContext.sessionID);
30256
+ const completions = state.pendingCompletions.splice(0);
30257
+ const matches = state.pendingPatternMatches.splice(0);
30258
+ const taskIds = [...new Set([...completions, ...matches].map((item) => item.task_id))];
30259
+ markDelivering(state, taskIds);
30260
+ const bridge = bridgeForDrain(drainContext);
30261
+ const items = [...completions, ...matches];
30262
+ for (const [index, item] of items.entries()) {
30263
+ const isMatch = isPatternMatchEntry(item);
30264
+ try {
30265
+ if (!isMatch && state.consumedTaskIds.has(item.task_id))
30266
+ continue;
30267
+ const admitted = isMatch ? await delivery.patternMatch(item, bridge) : await delivery.completion(item, bridge);
30268
+ if (!admitted) {
30269
+ if (isMatch)
30270
+ queuePendingPatternMatch(state, item);
30271
+ else if (!state.consumedTaskIds.has(item.task_id))
30272
+ state.pendingCompletions.push(item);
30273
+ continue;
30274
+ }
30275
+ if (!isMatch)
30276
+ consumeBgCompletion(drainContext.sessionID, item.task_id);
30277
+ markDeliveredAwaitingAck(state, [item.task_id]);
30278
+ const acked = await ackCompletions(drainContext, [
30279
+ { task_id: item.task_id, status: "unknown", exit_code: null, command: "" }
30280
+ ]);
30281
+ if (acked)
30282
+ confirmAcked(state, [item.task_id]);
30283
+ } catch (error) {
30284
+ if (isMatch)
30285
+ queuePendingPatternMatch(state, item);
30286
+ else
30287
+ state.pendingCompletions.push(item);
30288
+ for (const remaining of items.slice(index + 1)) {
30289
+ if (isPatternMatchEntry(remaining))
30290
+ queuePendingPatternMatch(state, remaining);
30291
+ else
30292
+ state.pendingCompletions.push(remaining);
30293
+ }
30294
+ clearDelivering(state, taskIds);
30295
+ throw error;
30296
+ } finally {
30297
+ clearDelivering(state, [item.task_id]);
30298
+ }
30299
+ }
30300
+ }
29965
30301
  function logSubcNudgeLifecycle(drainContext, kind, message) {
29966
30302
  logDeliveryHop(drainContext, kind, message, {
29967
30303
  event: "subc_bg_nudge_delivery",
@@ -30275,7 +30611,7 @@ async function triggerWakeIfPending(drainContext, skipDrain, includeDeferredComp
30275
30611
  if (!skipDrain && (forceDrain || state.outstandingTaskIds.size > 0 || !state.forcedDrainCompleted) && (forceDrain || hasTrackedOrPendingWork || idleDrainMayFindWork(state, drainContext))) {
30276
30612
  await drainCompletions(drainContext);
30277
30613
  }
30278
- routeExplicitControlCompletions(state);
30614
+ routeExplicitControlCompletions(state, drainContext.sessionID);
30279
30615
  if (!hasWakeEligiblePending(state, includeDeferredCompletions))
30280
30616
  return;
30281
30617
  if (deferWhileBusy && state.hostStatus === "busy") {
@@ -30768,7 +31104,7 @@ function ingestDrainedBgCompletions(sessionID, completions) {
30768
31104
  state.outstandingTaskIds.delete(completion.task_id);
30769
31105
  if (state.explicitControlTasks.has(completion.task_id)) {
30770
31106
  state.explicitControlTasks.delete(completion.task_id);
30771
- queuePendingPatternMatch(state, completionToExitPattern(completion, true));
31107
+ queuePendingPatternMatch(state, completionToExitPattern(completion, sessionID, true));
30772
31108
  continue;
30773
31109
  }
30774
31110
  if (state.consumedTaskIds.has(completion.task_id))
@@ -30809,12 +31145,12 @@ function bufferUnknownCompletion(state, completion) {
30809
31145
  function pruneUnknownCompletions(state, now) {
30810
31146
  state.unknownCompletions = state.unknownCompletions.filter((entry) => now - entry.receivedAt <= UNKNOWN_COMPLETION_TTL_MS);
30811
31147
  }
30812
- function completionToExitPattern(completion, ackCompletionOnDelivery = false) {
31148
+ function completionToExitPattern(completion, sessionID, ackCompletionOnDelivery = false) {
30813
31149
  const status = formatStatus(completion);
30814
31150
  const preview = formatOutputPreview(completion).replace(/^ {4}/gm, "").slice(-300);
30815
31151
  const entry = {
30816
31152
  task_id: completion.task_id,
30817
- session_id: "",
31153
+ session_id: sessionID || DEFAULT_SESSION_ID2,
30818
31154
  watch_id: "exit",
30819
31155
  match_text: "",
30820
31156
  match_offset: 0,
@@ -44086,6 +44422,10 @@ async function callToolCall(ctx, runtime, name, rawArgs = {}, options) {
44086
44422
  const opts = Object.keys(sendOptions).length > 0 ? sendOptions : undefined;
44087
44423
  response = ctx.pool.toolCallForCanonicalRoot ? await ctx.pool.toolCallForCanonicalRoot(root, runtime.sessionID, name, rawArgs, opts) : await ctx.pool.getBridge(root).toolCall(runtime.sessionID, name, rawArgs, opts);
44088
44424
  } catch (error) {
44425
+ const noFallbackOption = name === "bash" ? rawArgs.runon !== undefined ? "runon" : rawArgs.pty === true ? "pty:true" : rawArgs.background === true ? "background:true" : undefined : undefined;
44426
+ if (noFallbackOption && isBashTransportDeadError(error)) {
44427
+ throw refuseAftModuleOptionWithoutFallback(error, noFallbackOption, BASH_HOST_FALLBACK_REFUSAL);
44428
+ }
44089
44429
  throw adaptToolError(name, error);
44090
44430
  } finally {
44091
44431
  markBridgeEnd();
@@ -45343,17 +45683,19 @@ ${BASH_RUNON_GUIDANCE}`, "");
45343
45683
  });
45344
45684
  } catch (error) {
45345
45685
  const fallbackCause = classifyBashHostFallbackError(error);
45346
- if (!bashCfg.host_fallback || fallbackCause === undefined)
45686
+ if (fallbackCause === undefined)
45347
45687
  throw error;
45348
45688
  if (!backgroundDisabled && rawRequestedBackground) {
45349
- throw new Error(`${BASH_HOST_FALLBACK_REFUSAL}; background:true is unsupported.`);
45689
+ throw refuseAftModuleOptionWithoutFallback(error, "background:true", BASH_HOST_FALLBACK_REFUSAL);
45350
45690
  }
45351
45691
  if (requestedPty) {
45352
- throw new Error(`${BASH_HOST_FALLBACK_REFUSAL}; pty:true is unsupported.`);
45692
+ throw refuseAftModuleOptionWithoutFallback(error, "pty:true", BASH_HOST_FALLBACK_REFUSAL);
45353
45693
  }
45354
45694
  if (runon !== undefined) {
45355
- throw new Error(`${BASH_HOST_FALLBACK_REFUSAL}; runon is unsupported, and the command was not run locally.`);
45695
+ throw refuseAftModuleOptionWithoutFallback(error, "runon", BASH_HOST_FALLBACK_REFUSAL);
45356
45696
  }
45697
+ if (!bashCfg.host_fallback)
45698
+ throw error;
45357
45699
  const projectRoot = projectRootFor(context);
45358
45700
  const pattern = bashHostFallbackAskPattern(command, projectRoot, fallbackCause);
45359
45701
  await runAsk(context.ask({
@@ -45761,14 +46103,22 @@ function runtimeFor(name, location, context, signal, consumers, onStepAbort) {
45761
46103
  }
45762
46104
  function projectV2Tool(name, definition, location, consumers = {}) {
45763
46105
  const hostName = v2HostToolName(name);
45764
- const input = tool.schema.object(projectArguments(name, definition));
45765
- assertV2Contract(hostName, input);
46106
+ const currentInput = () => {
46107
+ const input = tool.schema.object(projectArguments(name, definition));
46108
+ assertV2Contract(hostName, input);
46109
+ return input;
46110
+ };
46111
+ currentInput();
45766
46112
  const sharedOptions = definition.options;
45767
46113
  const permission = hostPermission(name);
45768
46114
  return {
45769
46115
  name: hostName,
45770
- description: renameV2ToolReferences(definition.description),
45771
- input,
46116
+ get description() {
46117
+ return renameV2ToolReferences(["read", "write", "edit"].includes(name) ? definition.description.replaceAll("filePath", "path") : definition.description);
46118
+ },
46119
+ get input() {
46120
+ return currentInput();
46121
+ },
45772
46122
  options: {
45773
46123
  ...sharedOptions,
45774
46124
  ...permission ? { permission } : {},
@@ -46369,14 +46719,12 @@ function buildUnifiedDiff(fp, before, after) {
46369
46719
  `);
46370
46720
  const afterLines = after.split(`
46371
46721
  `);
46372
- const LINE_CAP = 5000;
46373
- if (beforeLines.length > LINE_CAP || afterLines.length > LINE_CAP) {
46374
- const limit = Math.max(beforeLines.length, afterLines.length);
46722
+ const ops = diffLines(beforeLines, afterLines);
46723
+ if (ops === null) {
46375
46724
  return `Index: ${fp}
46376
- (diff skipped: file has ${limit} lines, above ${LINE_CAP}-line diff cap)
46725
+ (diff skipped: changed region exceeds the diff-work budget)
46377
46726
  `;
46378
46727
  }
46379
- const ops = diffLines(beforeLines, afterLines);
46380
46728
  if (ops.every((op) => op.tag === "eq")) {
46381
46729
  return `Index: ${fp}
46382
46730
  ===================================================================
@@ -46402,6 +46750,9 @@ function buildUnifiedDiff(fp, before, after) {
46402
46750
  }
46403
46751
  return diff;
46404
46752
  }
46753
+ var LCS_TABLE_CELL_BUDGET = 25010001;
46754
+ var MYERS_TRACE_CELL_BUDGET = 1500000;
46755
+ var MYERS_WORK_BUDGET = 25000000;
46405
46756
  function diffLines(a, b) {
46406
46757
  const n = a.length;
46407
46758
  const m = b.length;
@@ -46417,6 +46768,26 @@ function diffLines(a, b) {
46417
46768
  const innerM = m - prefix - suffix;
46418
46769
  const w = innerM + 1;
46419
46770
  const cells = (innerN + 1) * w;
46771
+ if (cells > LCS_TABLE_CELL_BUDGET) {
46772
+ const middle = myersDiff(a.slice(prefix, n - suffix), b.slice(prefix, m - suffix), prefix);
46773
+ if (middle === null)
46774
+ return null;
46775
+ const ops = [];
46776
+ for (let i = 0;i < prefix; i++) {
46777
+ ops.push({ tag: "eq", beforeIdx: i, afterIdx: i, line: a[i] });
46778
+ }
46779
+ for (const op of middle)
46780
+ ops.push(op);
46781
+ for (let i = 0;i < suffix; i++) {
46782
+ ops.push({
46783
+ tag: "eq",
46784
+ beforeIdx: n - suffix + i,
46785
+ afterIdx: m - suffix + i,
46786
+ line: a[n - suffix + i]
46787
+ });
46788
+ }
46789
+ return ops;
46790
+ }
46420
46791
  diffTableCells += cells;
46421
46792
  const inner = Math.min(innerN, innerM) < 65535 ? new Uint16Array(cells) : new Uint32Array(cells);
46422
46793
  for (let i = 1;i <= innerN; i++) {
@@ -46459,6 +46830,103 @@ function diffLines(a, b) {
46459
46830
  ops.reverse();
46460
46831
  return ops;
46461
46832
  }
46833
+ function myersDiff(a, b, baseIndex) {
46834
+ const n = a.length;
46835
+ const m = b.length;
46836
+ const max = n + m;
46837
+ const maxDepth = Math.min(max, Math.floor((Math.sqrt(1 + 8 * MYERS_TRACE_CELL_BUDGET) - 1) / 2));
46838
+ const offset = maxDepth + 1;
46839
+ const frontier = new Int32Array(2 * maxDepth + 3);
46840
+ frontier[offset + 1] = 0;
46841
+ const trace = [];
46842
+ let traceCells = 0;
46843
+ let work = 0;
46844
+ for (let d = 0;d <= maxDepth; d++) {
46845
+ let reachedEnd = false;
46846
+ for (let k = -d;k <= d; k += 2) {
46847
+ if (++work > MYERS_WORK_BUDGET)
46848
+ return null;
46849
+ let x;
46850
+ if (k === -d || k !== d && frontier[offset + k - 1] < frontier[offset + k + 1]) {
46851
+ x = frontier[offset + k + 1];
46852
+ } else {
46853
+ x = frontier[offset + k - 1] + 1;
46854
+ }
46855
+ let y = x - k;
46856
+ while (x < n && y < m && a[x] === b[y]) {
46857
+ if (++work > MYERS_WORK_BUDGET)
46858
+ return null;
46859
+ x++;
46860
+ y++;
46861
+ }
46862
+ frontier[offset + k] = x;
46863
+ if (x >= n && y >= m) {
46864
+ reachedEnd = true;
46865
+ break;
46866
+ }
46867
+ }
46868
+ if (reachedEnd) {
46869
+ const reversed = [];
46870
+ let x = n;
46871
+ let y = m;
46872
+ for (let depth = d;depth > 0; depth--) {
46873
+ const previous = trace[depth - 1];
46874
+ const k = x - y;
46875
+ const insert = k === -depth || k !== depth && previous[(k - 1 + depth - 1) / 2] < previous[(k + 1 + depth - 1) / 2];
46876
+ const previousK = insert ? k + 1 : k - 1;
46877
+ const previousX = previous[(previousK + depth - 1) / 2];
46878
+ const previousY = previousX - previousK;
46879
+ while (x > previousX && y > previousY) {
46880
+ reversed.push({
46881
+ tag: "eq",
46882
+ beforeIdx: baseIndex + x - 1,
46883
+ afterIdx: baseIndex + y - 1,
46884
+ line: a[x - 1]
46885
+ });
46886
+ x--;
46887
+ y--;
46888
+ }
46889
+ if (x === previousX) {
46890
+ reversed.push({ tag: "ins", afterIdx: baseIndex + y - 1, line: b[y - 1] });
46891
+ y--;
46892
+ } else {
46893
+ reversed.push({ tag: "del", beforeIdx: baseIndex + x - 1, line: a[x - 1] });
46894
+ x--;
46895
+ }
46896
+ }
46897
+ while (x > 0 && y > 0 && a[x - 1] === b[y - 1]) {
46898
+ reversed.push({
46899
+ tag: "eq",
46900
+ beforeIdx: baseIndex + x - 1,
46901
+ afterIdx: baseIndex + y - 1,
46902
+ line: a[x - 1]
46903
+ });
46904
+ x--;
46905
+ y--;
46906
+ }
46907
+ while (x > 0) {
46908
+ reversed.push({ tag: "del", beforeIdx: baseIndex + x - 1, line: a[x - 1] });
46909
+ x--;
46910
+ }
46911
+ while (y > 0) {
46912
+ reversed.push({ tag: "ins", afterIdx: baseIndex + y - 1, line: b[y - 1] });
46913
+ y--;
46914
+ }
46915
+ reversed.reverse();
46916
+ return reversed;
46917
+ }
46918
+ const snapshotSize = d + 1;
46919
+ if (traceCells + snapshotSize > MYERS_TRACE_CELL_BUDGET)
46920
+ return null;
46921
+ const snapshot = new Array(snapshotSize);
46922
+ let index = 0;
46923
+ for (let k = -d;k <= d; k += 2)
46924
+ snapshot[index++] = frontier[offset + k];
46925
+ trace.push(snapshot);
46926
+ traceCells += snapshotSize;
46927
+ }
46928
+ return null;
46929
+ }
46462
46930
  function groupIntoHunks(ops, context, gap, beforeLen, afterLen) {
46463
46931
  const changeIdx = [];
46464
46932
  for (let k = 0;k < ops.length; k++) {
@@ -46559,6 +47027,9 @@ Examples:
46559
47027
  Read 30 lines from line 200: { "path": "src/app.ts", "offset": 200, "limit": 30 }
46560
47028
  List directory: { "path": "src/" }
46561
47029
  `;
47030
+ function v1FileParameterDescription(description) {
47031
+ return description.replaceAll("`path`", "`filePath`").replaceAll('"path":', '"filePath":');
47032
+ }
46562
47033
  function readDescription(ghReadEnabled) {
46563
47034
  const githubDescription = whenGhReadEnabled(ghReadEnabled, ISSUE_AND_PR_READ_DESCRIPTION);
46564
47035
  if (!githubDescription)
@@ -46569,16 +47040,38 @@ ${githubDescription}
46569
47040
 
46570
47041
  Examples:`);
46571
47042
  }
47043
+ function withRevisionNote(description) {
47044
+ if (description.includes(`
47045
+ Examples:`)) {
47046
+ return description.replace(`
47047
+ Examples:`, `
47048
+ ${REVISION_REF_TOOL_NOTE}
47049
+
47050
+ Examples:`);
47051
+ }
47052
+ return `${description}
47053
+
47054
+ ${REVISION_REF_TOOL_NOTE}`;
47055
+ }
47056
+ var revisionRefArgSchema = {
47057
+ ref: z7.string().optional().describe(REVISION_REF_DESCRIPTION)
47058
+ };
46572
47059
  function createReadTool(ctx) {
47060
+ const baseDescription = v1FileParameterDescription(readDescription(resolveGithubConfig(ctx.config).read));
47061
+ const baseArgs = {
47062
+ filePath: z7.string().describe("Path to file or directory (absolute or relative to project root)"),
47063
+ startLine: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based line or directory entry to start reading from"),
47064
+ endLine: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based line or directory entry to stop reading at (inclusive)"),
47065
+ limit: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("Max lines (default: 2000) or directory entries (default and cap: 1000) to return"),
47066
+ offset: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based line or directory entry to start reading from (use with limit). Ignored if startLine is provided")
47067
+ };
46573
47068
  return prepareToolMap({
46574
47069
  read: {
46575
- description: readDescription(resolveGithubConfig(ctx.config).read),
46576
- args: {
46577
- filePath: z7.string().describe("Path to file or directory (absolute or relative to project root)"),
46578
- startLine: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based line or directory entry to start reading from"),
46579
- endLine: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based line or directory entry to stop reading at (inclusive)"),
46580
- limit: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("Max lines (default: 2000) or directory entries (default and cap: 1000) to return"),
46581
- offset: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based line or directory entry to start reading from (use with limit). Ignored if startLine is provided")
47070
+ get description() {
47071
+ return revisionReadsOffered(ctx.config) ? withRevisionNote(baseDescription) : baseDescription;
47072
+ },
47073
+ get args() {
47074
+ return revisionReadsOffered(ctx.config) ? { ...baseArgs, ...revisionRefArgSchema } : baseArgs;
46582
47075
  },
46583
47076
  execute: async (args, context) => {
46584
47077
  const file = args.path;
@@ -46616,6 +47109,9 @@ function createReadTool(ctx) {
46616
47109
  rawArgs.offset = offset;
46617
47110
  if (limit !== undefined)
46618
47111
  rawArgs.limit = limit;
47112
+ const ref = revisionRefArg(args.ref);
47113
+ if (ref !== undefined)
47114
+ rawArgs.ref = ref;
46619
47115
  const mediaCandidate = isGithubResourcePath(file) || /\.(png|jpe?g|gif|webp|pdf)$/i.test(filePath) && await stat2(filePath).then((info) => info.isFile(), () => false);
46620
47116
  const visionCapability = mediaCandidate ? await measurePreStage("capability", () => currentSessionVisionCapability(ctx.client, context.sessionID)) : undefined;
46621
47117
  if (visionCapability !== undefined)
@@ -46664,11 +47160,11 @@ function githubWriteDisabledError(tool2, address) {
46664
47160
  function getWriteDescription(ctx, editToolName) {
46665
47161
  const backupText = ctx.config.backup?.enabled === false ? "Backup capture is disabled by user config." : "Existing files are backed up before overwriting (undo via aft_safety).";
46666
47162
  const githubText = resolveGithubConfig(ctx.config).write ? ' When enabled, `write("issue://N", content)` or `write("pr://N", content)` publishes a new comment.' : "";
46667
- return `Write content to a file, creating it and parent directories automatically. ${backupText} Auto-formats when the project has a formatter configured. Use it to create files or replace whole contents; for partial edits, use the \`${editToolName}\` tool. Write very large new files in parts: a single call can exceed the model's output limit and is then aborted with empty arguments.${githubText}`;
47163
+ return `Write content to a file, creating it and parent directories automatically. Use \`path\` for the file location. ${backupText} Auto-formats when the project has a formatter configured. Use it to create files or replace whole contents; for partial edits, use the \`${editToolName}\` tool. Write very large new files in parts: a single call can exceed the model's output limit and is then aborted with empty arguments.${githubText}`;
46668
47164
  }
46669
47165
  function createWriteTool(ctx, editToolName = "edit") {
46670
47166
  return {
46671
- description: getWriteDescription(ctx, editToolName),
47167
+ description: v1FileParameterDescription(getWriteDescription(ctx, editToolName)),
46672
47168
  args: {
46673
47169
  filePath: z7.string().describe("Path to the file to write (absolute or relative to project root)"),
46674
47170
  content: z7.string().describe("The full content to write to the file; for issue://N / pr://N, the comment body to post")
@@ -46862,7 +47358,7 @@ function createEditTool(ctx, writeToolName = "write") {
46862
47358
  if (ctx.hashlineEffective === true)
46863
47359
  return createHashlineEditTool(ctx);
46864
47360
  return {
46865
- description: getEditDescription(ctx, writeToolName),
47361
+ description: v1FileParameterDescription(getEditDescription(ctx, writeToolName)),
46866
47362
  args: {
46867
47363
  filePath: z7.string().describe("Path to the file to edit (absolute or relative to project root)"),
46868
47364
  symbol: z7.string().optional().describe("Named symbol to replace; only meaningful together with `content`. A call carrying one without the other is incomplete."),
@@ -47623,28 +48119,14 @@ var MAX_OUTLINE_READ_ROUNDS = 8;
47623
48119
  function isGithubPathArg(value) {
47624
48120
  return typeof value === "string" && (value.startsWith("issue://") || value.startsWith("pr://"));
47625
48121
  }
48122
+ function isUrl(value) {
48123
+ return value.startsWith("http://") || value.startsWith("https://");
48124
+ }
47626
48125
  function buildZoomTitle(args) {
47627
- const targets = args.targets;
47628
- if (!isEmptyParam(targets)) {
47629
- if (Array.isArray(targets)) {
47630
- if (targets.length === 1) {
47631
- return `${targets[0].path}#${targets[0].symbol}`;
47632
- }
47633
- return `${targets.length} targets across files`;
47634
- }
47635
- if (targets && typeof targets === "object") {
47636
- return `${targets.path}#${targets.symbol}`;
47637
- }
47638
- }
47639
- const path = args.path ?? args.url ?? "";
47640
- if (typeof args.symbols === "string")
47641
- return path ? `${path}#${args.symbols}` : args.symbols;
47642
- if (Array.isArray(args.symbols) && args.symbols.length > 0) {
47643
- if (args.symbols.length === 1)
47644
- return path ? `${path}#${args.symbols[0]}` : args.symbols[0];
47645
- return path ? `${path} (${args.symbols.length} symbols)` : `${args.symbols.length} symbols`;
47646
- }
47647
- return path || "(no target)";
48126
+ if (args.batch.length !== 1)
48127
+ return `${args.batch.length} locations`;
48128
+ const entry = args.batch[0];
48129
+ return `${entry.target}#${Array.isArray(entry.symbols) ? entry.symbols.join(", ") : entry.symbols}`;
47648
48130
  }
47649
48131
  function outlineFocusSteer(config) {
47650
48132
  const zoomEnabled = toolEnabled(config, "aft_zoom");
@@ -47657,31 +48139,53 @@ function readingTools(ctx) {
47657
48139
  const zoomEnabled = toolEnabled(ctx.config, "aft_zoom");
47658
48140
  const ghReadEnabled = resolveGithubConfig(ctx.config).read;
47659
48141
  const githubOutlineDescription = whenGhReadEnabled(ghReadEnabled, "GitHub issues and pull requests can be outlined with `issue://NUMBER` or `pr://NUMBER` (including `OWNER/REPO` forms).");
47660
- const githubZoomDescription = whenGhReadEnabled(ghReadEnabled, "GitHub issue and pull-request discussion ordinals can be zoomed with `path: issue://…` or `path: pr://…` and `symbols`.");
47661
- const tools = prepareToolMap({
47662
- aft_outline: {
47663
- description: "Structural outline of source code, documentation files, or remote URLs. For code, returns symbols (functions, classes, types) with line ranges. For Markdown and HTML, returns heading hierarchy. Use this to explore structure before reading specific sections with " + (zoomEnabled ? "aft_zoom" : "read") + ". With `files: true`, the outline is breadth-first with directory rollups; drill in by outlining a subdirectory. Rows show language, symbol count, and line count.\n\n" + outlineFocusSteer(ctx.config) + `
48142
+ const githubZoomDescription = whenGhReadEnabled(ghReadEnabled, "Batch targets may use `issue://NUMBER` or `pr://NUMBER` (including `OWNER/REPO` forms) to inspect GitHub discussion ordinals.");
48143
+ const outlineDescription = "Structural outline of source code, documentation files, or remote URLs. For code, returns symbols (functions, classes, types) with line ranges. For Markdown and HTML, returns heading hierarchy. Use this to explore structure before reading specific sections with " + (zoomEnabled ? "aft_zoom" : "read") + ". With `files: true`, the outline is breadth-first with directory rollups; drill in by outlining a subdirectory. Rows show language, symbol count, and line count.\n\n" + outlineFocusSteer(ctx.config) + `
47664
48144
 
47665
48145
  ` + "Pass a single `target`:\n" + ` • file path → outline that file (with signatures)
47666
48146
  ` + ` • directory path → outline source files under it
47667
48147
  ` + ` • URL (http:// or https://) → fetch and outline a remote HTML/Markdown document
47668
48148
  ` + " • array of paths → outline multiple files in one call; with files:true, every path must be a directory" + (githubOutlineDescription ? `
47669
48149
 
47670
- ${githubOutlineDescription}` : "") + "\n\nWhen a list is cut, the reply ends with `shown N of M <unit> (<reason>) · narrow: <knobs>`; absence of that line means the list is complete.",
47671
- args: {
47672
- target: z11.union([z11.string(), z11.array(z11.string())]).describe("What to outline: a file path, directory path, URL, or array of paths. The mode is auto-detected: URLs by `http://`/`https://` prefix, directories by stat, arrays as multi-file."),
47673
- files: z11.boolean().optional().describe("Directory-only mode: when true, target must be a directory or array of directories and the result is a breadth-first file tree with directory rollups plus language, symbol, and line counts."),
47674
- includeTests: z11.boolean().optional().describe("Directory outline only: include test files. Defaults to false; tests are hidden.")
48150
+ ${githubOutlineDescription}` : "") + "\n\nWhen a list is cut, the reply ends with `shown N of M <unit> (<reason>) · narrow: <knobs>`; absence of that line means the list is complete.";
48151
+ const outlineArgs = {
48152
+ target: z11.union([z11.string(), z11.array(z11.string())]).describe("What to outline: a file path, directory path, URL, or array of paths. The mode is auto-detected: URLs by `http://`/`https://` prefix, directories by stat, arrays as multi-file."),
48153
+ files: z11.boolean().optional().describe("Directory-only mode: when true, target must be a directory or array of directories and the result is a breadth-first file tree with directory rollups plus language, symbol, and line counts."),
48154
+ includeTests: z11.boolean().optional().describe("Directory outline only: include test files. Defaults to false; tests are hidden.")
48155
+ };
48156
+ const zoomDescription = 'Inspect code symbols or documentation sections. Pass lookups in `batch: [{ target, symbols }]`; a single lookup is a one-entry batch. Mix files and URLs in one batch, for example `batch: [{ target: "src/app.ts", symbols: "run" }, { target: "https://example.com/docs", symbols: "Overview" }]`. Each target is auto-detected as a file path or an HTTP/HTTPS URL. `symbols` is a string or string array. Markdown and HTML lookups return the section under the heading. Top-level `contextLines` and `callgraph` apply to every entry; callgraph adds calls-out and called-by annotations. Each failed lookup reports its own error line.' + (githubZoomDescription ? `
48157
+
48158
+ ${githubZoomDescription}` : "");
48159
+ const zoomArgs = {
48160
+ batch: z11.array(z11.object({
48161
+ target: z11.string().describe("File path (absolute or project-relative) or HTTP/HTTPS URL, auto-detected; issue://N or pr://N when GitHub reads are enabled"),
48162
+ symbols: z11.union([z11.string(), z11.array(z11.string())]).describe("Symbol name for code or heading text for Markdown/HTML; a string or array of strings")
48163
+ })).min(1).describe("One or more lookups; entries may mix file paths and URLs"),
48164
+ contextLines: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("Lines of context before/after each symbol (default: 3)"),
48165
+ callgraph: z11.boolean().optional().describe("Include call-graph annotations for every entry (default: false)")
48166
+ };
48167
+ const offered = () => revisionReadsOffered(ctx.config);
48168
+ const tools = prepareToolMap({
48169
+ aft_outline: {
48170
+ get description() {
48171
+ return offered() ? `${outlineDescription}
48172
+
48173
+ ${REVISION_REF_TOOL_NOTE} Only a single file can be outlined at a revision.` : outlineDescription;
48174
+ },
48175
+ get args() {
48176
+ return offered() ? { ...outlineArgs, ...revisionRefArgSchema } : outlineArgs;
47675
48177
  },
47676
48178
  execute: async (args, context) => {
47677
48179
  const target = coerceTargetParam(args.target);
47678
48180
  const filesMode = coerceBoolean(args.files);
47679
48181
  const hasIncludeTests = !isEmptyParam(args.includeTests);
47680
48182
  const includeTests = coerceBoolean(args.includeTests);
48183
+ const ref = revisionRefArg(args.ref);
47681
48184
  const rawArgs = {
47682
48185
  target,
47683
48186
  ...filesMode ? { files: true } : {},
47684
- ...hasIncludeTests ? { includeTests } : {}
48187
+ ...hasIncludeTests ? { includeTests } : {},
48188
+ ...ref !== undefined ? { ref } : {}
47685
48189
  };
47686
48190
  const checkedReads = new Map;
47687
48191
  let directoryOutline = filesMode;
@@ -47753,152 +48257,73 @@ ${excluded.size} files left out by read rules.` : "");
47753
48257
  }
47754
48258
  },
47755
48259
  aft_zoom: {
47756
- description: "Inspect code symbols or documentation sections. For code, returns the full source of a symbol. Pass `callgraph: true` to also include call-graph annotations (calls-out / called-by within the same file). For Markdown and HTML, returns the section content under the given heading.\n\nUse `{ path, symbols }` or `{ url, symbols }` for one file/URL. `symbols` can be a string or array (one or many lookups in the same file/URL). Use `targets` for cross-file batches: `{ path, symbol }` or an array of them. Sending both merges them into one batch; a lookup that fails reports its own error line." + (githubZoomDescription ? `
48260
+ get description() {
48261
+ return offered() ? `${zoomDescription}
47757
48262
 
47758
- ${githubZoomDescription}` : ""),
47759
- args: {
47760
- path: z11.string().optional().describe("Path to file (absolute or relative to project root)"),
47761
- url: z11.string().optional().describe("HTTP/HTTPS URL of an HTML or Markdown document to fetch and zoom into"),
47762
- symbols: z11.union([z11.string(), z11.array(z11.string())]).optional().describe("Symbol name for code, or heading text for Markdown/HTML. Pass a string for one lookup or an array for batched lookups in the same file/URL."),
47763
- targets: z11.union([
47764
- z11.object({
47765
- path: z11.string().describe("Path to file (absolute or relative to project root)"),
47766
- symbol: z11.string().describe("Symbol name in that file")
47767
- }),
47768
- z11.array(z11.object({
47769
- path: z11.string().describe("Path to file (absolute or relative to project root)"),
47770
- symbol: z11.string().describe("Symbol name in that file")
47771
- }))
47772
- ]).optional().describe("Cross-file batch: `{ path, symbol }` or an array of them. May be combined with path/url + symbols; all lookups are answered in one batch."),
47773
- contextLines: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("Lines of context before/after the symbol (default: 3)"),
47774
- callgraph: z11.boolean().optional().describe("Include call-graph annotations (calls-out / called-by within the same file). Default false; off keeps zoom output minimal.")
48263
+ ${REVISION_REF_TOOL_NOTE} Every file of the call is read at that revision; URLs do not have revisions.` : zoomDescription;
48264
+ },
48265
+ get args() {
48266
+ return offered() ? { ...zoomArgs, ...revisionRefArgSchema } : zoomArgs;
47775
48267
  },
47776
48268
  execute: async (args, context) => {
47777
- const targetsInput = coerceJsonCollectionParam(args.targets, "targets");
47778
- const targetEntries = usableZoomTargets(targetsInput);
47779
- const hasFilePath = !isBlankParam(args.path);
47780
- const hasUrl = !isBlankParam(args.url);
47781
- const hasTargets = targetEntries.length > 0;
47782
- const hasSymbols = !isBlankParam(args.symbols);
47783
- const wantCallgraph = coerceBoolean(args.callgraph);
47784
- const contextLines = coerceOptionalInt(args.contextLines, "contextLines", 1, Number.MAX_SAFE_INTEGER);
47785
- const zoomTitle = buildZoomTitle({
47786
- ...args,
47787
- targets: hasTargets ? targetEntries : undefined
48269
+ if (!Array.isArray(args.batch) || args.batch.length === 0)
48270
+ throw new Error("'batch' must be a non-empty array of {target, symbols}");
48271
+ const ref = revisionRefArg(args.ref);
48272
+ if (ref !== undefined && args.batch.some((entry) => isUrl(entry.target)))
48273
+ throw new Error("'ref' cannot be used with a URL target: a URL has no revision");
48274
+ const localPaths = args.batch.map((entry) => entry.target).filter((target) => target && !isGithubPathArg(target) && !isUrl(target));
48275
+ const resolved = await Promise.all(localPaths.map((target) => resolvePathArg(ctx, context, target)));
48276
+ const resolvedByInput = new Map(localPaths.map((input, index) => [input, resolved[index]]));
48277
+ const deniedPaths = new Map;
48278
+ const distinctFiles = [...new Set(resolved)];
48279
+ if (context.hostVersion === 2) {
48280
+ const outcomes = await checkReadPaths(context, distinctFiles, (file, batchContext) => assertPathReadPermissions(ctx, batchContext, file));
48281
+ for (const [file, denial] of outcomes)
48282
+ if (denial)
48283
+ deniedPaths.set(file, denial);
48284
+ } else {
48285
+ for (const file of distinctFiles) {
48286
+ const denial = await assertPathReadPermissions(ctx, context, file);
48287
+ if (denial)
48288
+ return permissionDeniedResponse(denial);
48289
+ }
48290
+ }
48291
+ const allowed = args.batch.filter((entry) => {
48292
+ const file = resolvedByInput.get(entry.target);
48293
+ return file === undefined || !deniedPaths.has(file);
47788
48294
  });
47789
- const zoomDisplay = { title: zoomTitle };
47790
- if (hasFilePath)
47791
- zoomDisplay.path = args.path;
47792
- if (hasUrl)
47793
- zoomDisplay.url = args.url;
47794
- if (hasSymbols) {
47795
- zoomDisplay.symbols = typeof args.symbols === "string" ? args.symbols : JSON.stringify(args.symbols);
47796
- }
47797
- if (hasTargets)
47798
- zoomDisplay.targets = JSON.stringify(targetEntries);
48295
+ const deniedText = args.batch.flatMap((entry) => {
48296
+ const file = resolvedByInput.get(entry.target);
48297
+ const denial = file === undefined ? undefined : deniedPaths.get(file);
48298
+ return denial ? [`${entry.target}: ${denial}`] : [];
48299
+ }).join(`
48300
+ `);
48301
+ const rawArgs = { batch: allowed };
48302
+ if (ref !== undefined)
48303
+ rawArgs.ref = ref;
48304
+ const contextLines = coerceOptionalInt(args.contextLines, "contextLines", 1, Number.MAX_SAFE_INTEGER);
47799
48305
  if (contextLines !== undefined)
47800
- zoomDisplay.contextLines = contextLines;
47801
- if (wantCallgraph)
47802
- zoomDisplay.callgraph = true;
48306
+ rawArgs.contextLines = contextLines;
48307
+ if (coerceBoolean(args.callgraph))
48308
+ rawArgs.callgraph = true;
48309
+ const title = buildZoomTitle({ batch: args.batch });
47803
48310
  const withMeta = (output) => ({
47804
48311
  output,
47805
- title: zoomTitle,
47806
- metadata: zoomDisplay
48312
+ title,
48313
+ metadata: { title, ...rawArgs, batch: JSON.stringify(args.batch) }
47807
48314
  });
47808
- if (hasTargets) {
47809
- if (hasFilePath && hasUrl) {
47810
- throw new Error("Provide exactly ONE of 'path' or 'url' — not both");
47811
- }
47812
- const targets = targetEntries.map((entry, i) => {
47813
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
47814
- throw new Error(`targets[${i}].path must be a non-empty string`);
47815
- }
47816
- const record = entry;
47817
- const targetPath = isBlankParam(record.path) ? record.filePath : record.path;
47818
- return {
47819
- path: typeof targetPath === "string" ? targetPath : "",
47820
- symbol: typeof record.symbol === "string" ? record.symbol : ""
47821
- };
47822
- });
47823
- const localPaths = targets.map((target) => target.path).filter((path) => path.trim().length > 0 && !isGithubPathArg(path));
47824
- if (hasFilePath && !isGithubPathArg(args.path))
47825
- localPaths.push(args.path);
47826
- const resolvedTargets = await Promise.all(localPaths.map((path) => resolvePathArg(ctx, context, path)));
47827
- const deniedPaths = new Map;
47828
- const resolvedByInput = new Map(localPaths.map((input, index) => [input, resolvedTargets[index]]));
47829
- const isDenied = (input) => {
47830
- const resolved = resolvedByInput.get(input);
47831
- return resolved !== undefined && deniedPaths.has(resolved);
47832
- };
47833
- const distinctFiles = [...new Set(resolvedTargets)];
47834
- if (context.hostVersion === 2) {
47835
- const outcomes = await checkReadPaths(context, distinctFiles, (file, batchContext) => assertPathReadPermissions(ctx, batchContext, file));
47836
- for (const [file, denial] of outcomes)
47837
- if (denial)
47838
- deniedPaths.set(file, denial);
47839
- } else {
47840
- for (const file of distinctFiles) {
47841
- const denial = await assertPathReadPermissions(ctx, context, file);
47842
- if (denial)
47843
- return permissionDeniedResponse(denial);
47844
- }
47845
- }
47846
- const rawArgs = {
47847
- targets: targets.filter((target) => !isDenied(target.path)).map((target) => ({
47848
- filePath: target.path,
47849
- symbol: target.symbol
47850
- }))
47851
- };
47852
- const deniedPrimary = hasFilePath && isDenied(args.path);
47853
- if (!deniedPrimary) {
47854
- if (hasFilePath)
47855
- rawArgs.filePath = args.path;
47856
- else if (hasUrl)
47857
- rawArgs.url = args.url;
47858
- }
47859
- if (hasSymbols && !deniedPrimary)
47860
- rawArgs.symbols = args.symbols;
47861
- if (contextLines !== undefined)
47862
- rawArgs.contextLines = contextLines;
47863
- if (wantCallgraph)
47864
- rawArgs.callgraph = true;
47865
- const deniedText = [...deniedPaths].map(([file, denial]) => `${file}: ${denial}`).join(`
47866
- `);
47867
- if (!rawArgs.targets.length && !(hasFilePath && !deniedPrimary) && !hasUrl)
47868
- return withMeta(deniedText);
47869
- const response = await callToolCall(ctx, context, "zoom", rawArgs);
47870
- if (response.success === false) {
47871
- throw new Error(response.text || response.message || "zoom failed");
47872
- }
47873
- return withMeta([response.text, deniedText].filter(Boolean).join(`
47874
-
47875
- `));
47876
- }
47877
- if (!hasFilePath && !hasUrl) {
47878
- throw new Error("Provide exactly one of 'path', 'url', or 'targets'");
47879
- }
47880
- if (hasFilePath && hasUrl) {
47881
- throw new Error("Provide exactly ONE of 'path' or 'url' — not both");
47882
- }
47883
- const githubPath = hasFilePath && isGithubPathArg(args.path) || hasUrl && isGithubPathArg(args.url);
47884
- if (!hasUrl && !githubPath) {
47885
- const file = await resolvePathArg(ctx, context, args.path);
47886
- const permissionDenied = await assertPathReadPermissions(ctx, context, file);
47887
- if (permissionDenied)
47888
- return permissionDeniedResponse(permissionDenied);
48315
+ if (!allowed.length) {
48316
+ const denial = deniedPaths.values().next().value;
48317
+ if (args.batch.length === 1 && denial)
48318
+ return permissionDeniedResponse(denial);
48319
+ return withMeta(deniedText);
47889
48320
  }
47890
- const rawArgs = hasUrl ? { url: args.url } : { filePath: args.path };
47891
- if (hasSymbols)
47892
- rawArgs.symbols = args.symbols;
47893
- if (contextLines !== undefined)
47894
- rawArgs.contextLines = contextLines;
47895
- if (wantCallgraph)
47896
- rawArgs.callgraph = true;
47897
48321
  const response = await callToolCall(ctx, context, "zoom", rawArgs);
47898
- if (response.success === false) {
48322
+ if (response.success === false)
47899
48323
  throw new Error(response.text || response.message || "zoom failed");
47900
- }
47901
- return withMeta(response.text);
48324
+ return withMeta([response.text, deniedText].filter(Boolean).join(`
48325
+
48326
+ `));
47902
48327
  }
47903
48328
  }
47904
48329
  });
@@ -47984,6 +48409,7 @@ async function assertPathReadPermissions(ctx, context, target, kind = "file", ch
47984
48409
  }
47985
48410
 
47986
48411
  // src/tools/safety.ts
48412
+ import * as fs5 from "node:fs";
47987
48413
  import * as path5 from "node:path";
47988
48414
  var z12 = tool.schema;
47989
48415
  function responsePaths(response) {
@@ -48015,22 +48441,23 @@ function safetyTools(ctx) {
48015
48441
  ` + `Ops:
48016
48442
  ` + `- 'undo': Undo the entire last tool call when 'path' is omitted (typical), or undo the last edit to one file when 'path' is provided. Note: pops from the undo stack (irreversible, no redo). Use 'history' to inspect per-file history before undoing.
48017
48443
  ` + `- 'history': List all edit snapshots for a file. Requires 'path'.
48018
- ` + `- 'checkpoint': Save a named snapshot. Explicit 'files' may be untracked or gitignored; omit them to snapshot backup-tracked files. Checkpoints are session-scoped and lost on bridge or daemon restart. Requires 'name'.
48444
+ ` + `- 'checkpoint': Save a named snapshot (on disk across restarts; expires in 14 days; may contain secrets). Explicit files may be untracked/gitignored; omit for backup-tracked files. Requires 'name'.
48019
48445
  ` + `- 'restore': Restore files to a previously saved checkpoint. Optional 'path'/'files' scopes the restore; omitted scope restores the whole checkpoint. Requires 'name'.
48020
48446
  ` + `- 'list': List all available named checkpoints. No extra params needed.
48447
+ ` + `- 'purge': Permanently delete this session's undo backups and checkpoint copies of 'path' (a file, or everything under a directory); cannot be undone.
48021
48448
 
48022
48449
  ` + `Each op requires specific parameters — see parameter descriptions for requirements.
48023
48450
 
48024
48451
  ` + "Use checkpoint before risky multi-file changes. Use undo for quick single-file rollback.",
48025
48452
  args: {
48026
- op: z12.enum(["undo", "history", "checkpoint", "restore", "list"]).describe("Safety operation"),
48027
- path: z12.string().optional().describe("File path (required for history, optional for undo, optional for checkpoint — snapshots only that file when files is omitted, optional for restore — restores only that file). Absolute or relative to project root"),
48453
+ op: z12.enum(["undo", "history", "checkpoint", "restore", "list", "purge"]).describe("Safety operation"),
48454
+ path: z12.string().optional().describe("File path (required for history, optional for undo, optional for checkpoint — snapshots only that file when files is omitted, optional for restore — restores only that file, required for purge — a file or directory). Absolute or relative to project root"),
48028
48455
  name: z12.string().optional().describe("Checkpoint name (required for checkpoint, restore)"),
48029
48456
  files: z12.array(z12.string()).optional().describe("Specific files to include in checkpoint, or to restore from a checkpoint (optional; restore without path/files restores the whole checkpoint; explicit checkpoint files may be untracked or gitignored)")
48030
48457
  },
48031
48458
  execute: async (args, context) => {
48032
48459
  const op = args.op;
48033
- if (op === "history" && typeof args.path !== "string") {
48460
+ if ((op === "history" || op === "purge") && typeof args.path !== "string") {
48034
48461
  throw new Error(`'path' is required for '${op}' op`);
48035
48462
  }
48036
48463
  if ((op === "checkpoint" || op === "restore") && typeof args.name !== "string") {
@@ -48100,6 +48527,42 @@ function safetyTools(ctx) {
48100
48527
  if (permissionError)
48101
48528
  return permissionDeniedResponse(permissionError);
48102
48529
  }
48530
+ if (op === "purge") {
48531
+ const absolutePath = resolveAbsolutePath(context, args.path);
48532
+ let isDirectory = false;
48533
+ try {
48534
+ isDirectory = fs5.statSync(absolutePath).isDirectory();
48535
+ } catch {}
48536
+ const denial = await assertExternalDirectoryPermission(ctx, context, absolutePath, {
48537
+ kind: isDirectory ? "directory" : "file"
48538
+ });
48539
+ if (denial)
48540
+ return permissionDeniedResponse(denial);
48541
+ const preview = await callBridge(ctx, context, "safety_purge_paths", {
48542
+ file: expandTilde2(args.path)
48543
+ });
48544
+ if (preview.success === false) {
48545
+ throw new Error(bridgeErrorMessage(preview, "purge path preview failed"));
48546
+ }
48547
+ const previewPaths = Array.from(new Set(responsePaths(preview)));
48548
+ for (const savedPath of previewPaths) {
48549
+ const previewDenial = await assertExternalDirectoryPermission(ctx, context, savedPath);
48550
+ if (previewDenial)
48551
+ return permissionDeniedResponse(previewDenial);
48552
+ }
48553
+ const pattern = resolveRelativePatternFromAbsolute(context, absolutePath);
48554
+ const patterns = isDirectory ? [pattern, path5.join(pattern, "*")] : [pattern];
48555
+ for (const previewPattern of relativePatternsFromPaths(context, previewPaths)) {
48556
+ if (!patterns.includes(previewPattern))
48557
+ patterns.push(previewPattern);
48558
+ }
48559
+ const permissionError = await askEditPermission(context, patterns, {
48560
+ filepath: absolutePath,
48561
+ operation: "purge"
48562
+ });
48563
+ if (permissionError)
48564
+ return permissionDeniedResponse(permissionError);
48565
+ }
48103
48566
  const rawArgs = { op };
48104
48567
  if (args.name !== undefined)
48105
48568
  rawArgs.name = args.name;
@@ -48120,7 +48583,7 @@ function safetyTools(ctx) {
48120
48583
  }
48121
48584
 
48122
48585
  // src/tools/search.ts
48123
- import * as fs5 from "node:fs";
48586
+ import * as fs6 from "node:fs";
48124
48587
  import * as path6 from "node:path";
48125
48588
  function arg2(schema) {
48126
48589
  return schema;
@@ -48129,7 +48592,7 @@ function absoluteSearchPath(projectRoot, target) {
48129
48592
  return resolvePathFromProjectRoot(projectRoot, expandTilde2(target));
48130
48593
  }
48131
48594
  function searchPathExists(projectRoot, target) {
48132
- return fs5.existsSync(absoluteSearchPath(projectRoot, target));
48595
+ return fs6.existsSync(absoluteSearchPath(projectRoot, target));
48133
48596
  }
48134
48597
  function splitSearchPathArg(projectRoot, raw) {
48135
48598
  if (searchPathExists(projectRoot, raw) || !/\s/.test(raw)) {
@@ -48172,7 +48635,7 @@ ${note}` : note;
48172
48635
  }
48173
48636
  function searchPathKind(projectRoot, target, defaultKind) {
48174
48637
  try {
48175
- const stat = fs5.lstatSync(absoluteSearchPath(projectRoot, target));
48638
+ const stat = fs6.lstatSync(absoluteSearchPath(projectRoot, target));
48176
48639
  if (defaultKind === "file") {
48177
48640
  return stat.isDirectory() ? "directory" : "file";
48178
48641
  }
@@ -48379,7 +48842,7 @@ function semanticTools(ctx) {
48379
48842
  topK: arg3(optionalInt(1, 50).describe("Number of results (default: 10, max: 50)")),
48380
48843
  offset: arg3(optionalInt(0, 1e5).describe("Zero-based result offset (default: 0, max: 100000).")),
48381
48844
  includeTests: arg3(z13.boolean().optional().describe("Include test files (*.test.*, *_test.rs, __tests__/, …) plus test-support, fixture, mock, snapshot, and corpus files. Defaults to false.")),
48382
- path: arg3(z13.string().optional().describe("Only set this to search a different Git project (absolute or ~ path). Omit it for the current configured workspace, including non-Git workspace roots; this is not a subdirectory filter. Unindexed external projects use a bounded lexical fallback."))
48845
+ path: arg3(z13.string().optional().describe("Only set this to search a different project or plain folder (absolute or ~ path). Omit it for the current configured workspace, including non-Git workspace roots; this is not a subdirectory filter. Unindexed external folders use a bounded lexical fallback; filesystem roots, home and its ancestors are refused."))
48383
48846
  },
48384
48847
  execute: async (args, context) => {
48385
48848
  const hasQuery = typeof args.query === "string" && args.query.trim().length > 0;
@@ -48974,7 +49437,7 @@ function startOpenCodeLiveConfigReload(options) {
48974
49437
  // src/notifications.ts
48975
49438
  import { existsSync as existsSync17, readFileSync as readFileSync14 } from "node:fs";
48976
49439
  import { homedir as homedir14, platform } from "node:os";
48977
- import { join as join22 } from "node:path";
49440
+ import { join as join23 } from "node:path";
48978
49441
  function isTuiMode() {
48979
49442
  return process.env.OPENCODE_CLIENT === "cli";
48980
49443
  }
@@ -48996,15 +49459,15 @@ function getDesktopStatePath() {
48996
49459
  const os = platform();
48997
49460
  const home = homedir14();
48998
49461
  if (os === "darwin") {
48999
- return join22(home, "Library", "Application Support", "ai.opencode.desktop", "opencode.global.dat");
49462
+ return join23(home, "Library", "Application Support", "ai.opencode.desktop", "opencode.global.dat");
49000
49463
  }
49001
49464
  if (os === "linux") {
49002
- const xdgConfig = process.env.XDG_CONFIG_HOME || join22(home, ".config");
49003
- return join22(xdgConfig, "ai.opencode.desktop", "opencode.global.dat");
49465
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join23(home, ".config");
49466
+ return join23(xdgConfig, "ai.opencode.desktop", "opencode.global.dat");
49004
49467
  }
49005
49468
  if (os === "win32") {
49006
- const appData = process.env.APPDATA || join22(home, "AppData", "Roaming");
49007
- return join22(appData, "ai.opencode.desktop", "opencode.global.dat");
49469
+ const appData = process.env.APPDATA || join23(home, "AppData", "Roaming");
49470
+ return join23(appData, "ai.opencode.desktop", "opencode.global.dat");
49008
49471
  }
49009
49472
  return null;
49010
49473
  }
@@ -49522,7 +49985,7 @@ import {
49522
49985
  readFileSync as readFileSync17,
49523
49986
  renameSync as renameSync10,
49524
49987
  rmSync as rmSync8,
49525
- statSync as statSync16,
49988
+ statSync as statSync17,
49526
49989
  writeFileSync as writeFileSync11
49527
49990
  } from "node:fs";
49528
49991
  import { dirname as dirname16 } from "node:path";
@@ -49530,30 +49993,30 @@ import { dirname as dirname16 } from "node:path";
49530
49993
  // src/hooks/auto-update-checker/cache.ts
49531
49994
  import { cpSync, existsSync as existsSync19, mkdtempSync, readFileSync as readFileSync16, rmSync as rmSync7, writeFileSync as writeFileSync10 } from "node:fs";
49532
49995
  import { tmpdir as tmpdir5 } from "node:os";
49533
- import { basename as basename6, dirname as dirname15, join as join25 } from "node:path";
49996
+ import { basename as basename6, dirname as dirname15, join as join26 } from "node:path";
49534
49997
  var import_comment_json3 = __toESM(require_src2(), 1);
49535
49998
 
49536
49999
  // src/hooks/auto-update-checker/checker.ts
49537
- import { existsSync as existsSync18, readFileSync as readFileSync15, statSync as statSync15, writeFileSync as writeFileSync9 } from "node:fs";
50000
+ import { existsSync as existsSync18, readFileSync as readFileSync15, statSync as statSync16, writeFileSync as writeFileSync9 } from "node:fs";
49538
50001
  import { readFile } from "node:fs/promises";
49539
50002
  import { homedir as homedir15 } from "node:os";
49540
- import { dirname as dirname14, isAbsolute as isAbsolute13, join as join24, resolve as resolve15 } from "node:path";
50003
+ import { dirname as dirname14, isAbsolute as isAbsolute13, join as join25, resolve as resolve15 } from "node:path";
49541
50004
  import { fileURLToPath } from "node:url";
49542
50005
  var import_comment_json2 = __toESM(require_src2(), 1);
49543
50006
 
49544
50007
  // src/hooks/auto-update-checker/constants.ts
49545
- import { join as join23 } from "node:path";
50008
+ import { join as join24 } from "node:path";
49546
50009
  var PACKAGE_NAME = "@cortexkit/aft-opencode";
49547
50010
  var NPM_REGISTRY_URL = "https://registry.npmjs.org";
49548
50011
  var NPM_FETCH_TIMEOUT = 1e4;
49549
50012
  function cacheDir() {
49550
- return join23(getOpenCodeCacheRoot(), "packages");
50013
+ return join24(getOpenCodeCacheRoot(), "packages");
49551
50014
  }
49552
50015
  function userOpenCodeConfig() {
49553
- return join23(getOpenCodeConfigRoot(), "opencode.json");
50016
+ return join24(getOpenCodeConfigRoot(), "opencode.json");
49554
50017
  }
49555
50018
  function userOpenCodeConfigJsonc() {
49556
- return join23(getOpenCodeConfigRoot(), "opencode.jsonc");
50019
+ return join24(getOpenCodeConfigRoot(), "opencode.jsonc");
49557
50020
  }
49558
50021
 
49559
50022
  // src/hooks/auto-update-checker/types.ts
@@ -49612,8 +50075,8 @@ function extractChannel(version) {
49612
50075
  }
49613
50076
  function getConfigPaths(directory) {
49614
50077
  return [
49615
- join24(directory, ".opencode", "opencode.json"),
49616
- join24(directory, ".opencode", "opencode.jsonc"),
50078
+ join25(directory, ".opencode", "opencode.json"),
50079
+ join25(directory, ".opencode", "opencode.jsonc"),
49617
50080
  userOpenCodeConfig(),
49618
50081
  userOpenCodeConfigJsonc()
49619
50082
  ];
@@ -49656,10 +50119,10 @@ function getLocalDevPath(directory) {
49656
50119
  }
49657
50120
  function findPackageJsonUp(startPath) {
49658
50121
  try {
49659
- const stat = statSync15(startPath);
50122
+ const stat = statSync16(startPath);
49660
50123
  let dir = stat.isDirectory() ? startPath : dirname14(startPath);
49661
50124
  for (let i = 0;i < 10; i++) {
49662
- const pkgPath = join24(dir, "package.json");
50125
+ const pkgPath = join25(dir, "package.json");
49663
50126
  if (existsSync18(pkgPath)) {
49664
50127
  try {
49665
50128
  const pkg = PackageJsonSchema.safeParse(JSON.parse(readFileSync15(pkgPath, "utf-8")));
@@ -49725,7 +50188,7 @@ function findPluginEntry(directory) {
49725
50188
  }
49726
50189
  var cachedPackageVersion = null;
49727
50190
  function getSpecCachePackageJsonPath(spec) {
49728
- return join24(cacheDir(), spec, "node_modules", PACKAGE_NAME, "package.json");
50191
+ return join25(cacheDir(), spec, "node_modules", PACKAGE_NAME, "package.json");
49729
50192
  }
49730
50193
  function getCachedVersion(spec) {
49731
50194
  if (!spec && cachedPackageVersion)
@@ -49734,7 +50197,7 @@ function getCachedVersion(spec) {
49734
50197
  getCurrentRuntimePackageJsonPath(),
49735
50198
  spec ? getSpecCachePackageJsonPath(spec) : null,
49736
50199
  getSpecCachePackageJsonPath(`${PACKAGE_NAME}@latest`),
49737
- join24(getOpenCodeCacheRoot(), "node_modules", PACKAGE_NAME, "package.json")
50200
+ join25(getOpenCodeCacheRoot(), "node_modules", PACKAGE_NAME, "package.json")
49738
50201
  ].filter(isString);
49739
50202
  for (const packageJsonPath of candidates) {
49740
50203
  try {
@@ -49805,7 +50268,7 @@ async function registryWithoutNpm(directory) {
49805
50268
  for (let current = directory;; current = dirname14(current)) {
49806
50269
  if (scoped)
49807
50270
  break;
49808
- const path = join24(current, ".npmrc");
50271
+ const path = join25(current, ".npmrc");
49809
50272
  visited.add(path);
49810
50273
  const settings = await readRegistrySettings(path);
49811
50274
  scoped ??= settings["@cortexkit:registry"];
@@ -49815,7 +50278,7 @@ async function registryWithoutNpm(directory) {
49815
50278
  }
49816
50279
  const explicitUserConfig = process.env.npm_config_userconfig ?? process.env.NPM_CONFIG_USERCONFIG;
49817
50280
  const home = (process.platform === "win32" ? process.env.USERPROFILE : process.env.HOME) ?? homedir15();
49818
- const userConfig = explicitUserConfig ?? join24(home, ".npmrc");
50281
+ const userConfig = explicitUserConfig ?? join25(home, ".npmrc");
49819
50282
  if (!scoped && !visited.has(userConfig)) {
49820
50283
  const settings = await readRegistrySettings(userConfig, explicitUserConfig !== undefined);
49821
50284
  scoped = settings["@cortexkit:registry"];
@@ -49901,10 +50364,10 @@ async function getLatestVersion(channel = "latest", options = {}) {
49901
50364
  var pendingSnapshots = new Map;
49902
50365
  var quarantinedInstallDirs = new Set;
49903
50366
  function createAutoUpdateSnapshot(installDir, packageJsonPath, packageName) {
49904
- const packageDir = join25(installDir, "node_modules", packageName);
49905
- const lockfilePath = join25(installDir, "package-lock.json");
49906
- const tempDir = mkdtempSync(join25(tmpdir5(), "aft-auto-update-"));
49907
- const stagedPackageDir = existsSync19(packageDir) ? join25(tempDir, "package") : null;
50367
+ const packageDir = join26(installDir, "node_modules", packageName);
50368
+ const lockfilePath = join26(installDir, "package-lock.json");
50369
+ const tempDir = mkdtempSync(join26(tmpdir5(), "aft-auto-update-"));
50370
+ const stagedPackageDir = existsSync19(packageDir) ? join26(tempDir, "package") : null;
49908
50371
  if (stagedPackageDir)
49909
50372
  cpSync(packageDir, stagedPackageDir, { recursive: true });
49910
50373
  return {
@@ -49945,7 +50408,7 @@ function stripPackageNameFromPath(pathValue, packageName) {
49945
50408
  return current;
49946
50409
  }
49947
50410
  function removeFromPackageLock(installDir, packageName) {
49948
- const lockPath = join25(installDir, "package-lock.json");
50411
+ const lockPath = join26(installDir, "package-lock.json");
49949
50412
  if (!existsSync19(lockPath))
49950
50413
  return false;
49951
50414
  try {
@@ -50001,7 +50464,7 @@ function restorePendingSnapshot(installDir) {
50001
50464
  restoreAutoUpdateSnapshot(snapshot);
50002
50465
  }
50003
50466
  function removeInstalledPackage(installDir, packageName) {
50004
- const packageDir = join25(installDir, "node_modules", packageName);
50467
+ const packageDir = join26(installDir, "node_modules", packageName);
50005
50468
  if (!existsSync19(packageDir))
50006
50469
  return false;
50007
50470
  rmSync7(packageDir, { recursive: true, force: true });
@@ -50014,14 +50477,14 @@ function resolveInstallContext(runtimePackageJsonPath = getCurrentRuntimePackage
50014
50477
  const nodeModulesDir = stripPackageNameFromPath(packageDir, PACKAGE_NAME);
50015
50478
  if (nodeModulesDir && basename6(nodeModulesDir) === "node_modules") {
50016
50479
  const installDir = dirname15(nodeModulesDir);
50017
- const packageJsonPath = join25(installDir, "package.json");
50480
+ const packageJsonPath = join26(installDir, "package.json");
50018
50481
  if (existsSync19(packageJsonPath))
50019
50482
  return { installDir, packageJsonPath };
50020
50483
  }
50021
50484
  return null;
50022
50485
  }
50023
50486
  const cacheRoot = dirname15(cacheDir());
50024
- const legacyPackageJsonPath = join25(cacheRoot, "package.json");
50487
+ const legacyPackageJsonPath = join26(cacheRoot, "package.json");
50025
50488
  if (existsSync19(legacyPackageJsonPath)) {
50026
50489
  return { installDir: cacheRoot, packageJsonPath: legacyPackageJsonPath };
50027
50490
  }
@@ -50294,7 +50757,7 @@ function isOrphanedCheckLock(lockPath) {
50294
50757
  }
50295
50758
  }
50296
50759
  function readCheckLockState(lockPath) {
50297
- const fallbackStartedMs = statSync16(lockPath).mtimeMs;
50760
+ const fallbackStartedMs = statSync17(lockPath).mtimeMs;
50298
50761
  try {
50299
50762
  const parsed = JSON.parse(readFileSync17(lockPath, "utf-8"));
50300
50763
  const pid = Number.isSafeInteger(parsed.pid) && (parsed.pid ?? 0) > 0 ? parsed.pid ?? null : null;
@@ -50535,16 +50998,16 @@ import {
50535
50998
  writeFileSync as writeFileSync12
50536
50999
  } from "node:fs";
50537
51000
  import { createServer } from "node:http";
50538
- import { dirname as dirname17, join as join27 } from "node:path";
51001
+ import { dirname as dirname17, join as join28 } from "node:path";
50539
51002
 
50540
51003
  // src/shared/rpc-utils.ts
50541
- import { join as join26 } from "node:path";
51004
+ import { join as join27 } from "node:path";
50542
51005
  function projectHash(directory) {
50543
51006
  return projectRootKeyHash(directory);
50544
51007
  }
50545
51008
  function rpcPortFileDir(storageDir, directory) {
50546
51009
  const hash = projectHash(directory);
50547
- return join26(storageDir, "rpc", hash, "ports");
51010
+ return join27(storageDir, "rpc", hash, "ports");
50548
51011
  }
50549
51012
  function isPidAlive(pid) {
50550
51013
  if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
@@ -50618,7 +51081,7 @@ class AftRpcServer {
50618
51081
  constructor(storageDir, directory) {
50619
51082
  this.portsDir = rpcPortFileDir(storageDir, directory);
50620
51083
  this.instanceId = randomBytes5(8).toString("hex");
50621
- this.portFilePath = join27(this.portsDir, `${this.instanceId}.json`);
51084
+ this.portFilePath = join28(this.portsDir, `${this.instanceId}.json`);
50622
51085
  }
50623
51086
  handle(method, handler) {
50624
51087
  this.handlers.set(method, handler);
@@ -50706,7 +51169,7 @@ class AftRpcServer {
50706
51169
  for (const entry of entries) {
50707
51170
  if (!entry.endsWith(".json"))
50708
51171
  continue;
50709
- const filePath = join27(this.portsDir, entry);
51172
+ const filePath = join28(this.portsDir, entry);
50710
51173
  if (filePath === this.portFilePath)
50711
51174
  continue;
50712
51175
  try {
@@ -51335,7 +51798,7 @@ function buildWorkflowHints(opts) {
51335
51798
  `));
51336
51799
  }
51337
51800
  if (hasOutline && hasZoom) {
51338
- sections.push(`**Web/URL access**: \`aft_outline({ target: url })\` first for structure, then \`aft_zoom({ url, symbols: "<heading>" })\` for the specific section.`);
51801
+ sections.push(`**Web/URL access**: \`aft_outline({ target: url })\` first for structure, then \`aft_zoom({ batch: [{ target: url, symbols: "<heading>" }] })\` for the specific section.`);
51339
51802
  }
51340
51803
  if (hasOutline && (hasGrep || hasSearch) && (hasZoom || hasRead)) {
51341
51804
  const searchName = hasSearch ? "aft_search" : grepName;
@@ -51452,13 +51915,13 @@ function throwSentinel(command) {
51452
51915
  throw sentinel;
51453
51916
  }
51454
51917
  var PLUGIN_VERSION = resolvePluginVersion(import.meta.url);
51455
- var ANNOUNCEMENT_VERSION = "0.61.0";
51918
+ var ANNOUNCEMENT_VERSION = "0.62.0";
51456
51919
  var ANNOUNCEMENT_FEATURES = [
51457
- "OpenCode 2 now hands every AFT permission decision to OpenCode, so your permission rules and plugins apply to AFT tools too.",
51458
- "Shell sandboxing has per-role profiles: `sandbox.profile` (`off`, `relaxed`, `strict`) replaces `sandbox.enabled`, and `npx @cortexkit/aft doctor --fix` migrates your config.",
51459
- "Edits keep files' permissions and ownership, replacing them atomically when that is safe, and `apply_patch` rejects malformed or ambiguous patches before writing anything.",
51460
- "Much lighter background work: less CPU, disk and memory use, and no indexing of your home folder or drive roots.",
51461
- "Long shell waits keep their replies, remote `runon` jobs survive build-server restarts, and many reported issues are fixed."
51920
+ "Linux dev containers work again: a project at a bind-mount target such as `/workspace` is no longer refused as too broad.",
51921
+ "Read a file as it is at any git revision with the opt-in `ref` argument on `read`, `aft_outline` and `aft_zoom` (set `revision_reads: true`).",
51922
+ "`aft_safety` has a `purge` op that permanently removes a file from this session's undo backups and checkpoints, such as a secret written by mistake.",
51923
+ "`aft_zoom` takes one `batch` of lookups mixing files and URLs, and OpenCode shows diffs for edits to files over 5,000 lines.",
51924
+ "Idle OpenCode 2 sessions wake again when a background command finishes, and many reported issues are fixed."
51462
51925
  ];
51463
51926
  var ANNOUNCEMENT_FOOTER = "Join us on Discord: https://discord.gg/DSa65w8wuf";
51464
51927
  var plugin = async (input) => initializePluginForDirectory(input);
@@ -51951,7 +52414,7 @@ Install: ${getManualInstallHint()}`);
51951
52414
  var src_default = plugin;
51952
52415
 
51953
52416
  // src/entry/server-runtime.mjs
51954
- import { Effect as Effect14 } from "effect";
52417
+ import { Effect as Effect15 } from "effect";
51955
52418
 
51956
52419
  // src/rpc/register.ts
51957
52420
  import { Effect as Effect8 } from "effect";
@@ -52220,7 +52683,7 @@ class V2SessionDelivery {
52220
52683
  return Effect9.suspend(() => {
52221
52684
  if (this.disposed)
52222
52685
  return Effect9.succeed(false);
52223
- const keys = [...new Set(input.taskIDs)].map((taskID) => `${input.sessionID}\x00${taskID}`);
52686
+ const keys = [...new Set(input.taskIDs)].map((taskID) => `${input.sessionID}\x00${taskID}\x00${input.notificationID ?? "completion"}`);
52224
52687
  if (keys.length === 0 || keys.every((key) => this.admittedCompletions.has(key))) {
52225
52688
  return Effect9.succeed(false);
52226
52689
  }
@@ -52379,6 +52842,11 @@ function registerV2ToolHooks(context, runtime, registeredTools, bashDescription)
52379
52842
  return Effect11.gen(function* () {
52380
52843
  if (typeof host.tool?.hook === "function") {
52381
52844
  yield* host.tool.hook("execute.after", (event) => Effect11.promise(async () => {
52845
+ if (runtime.drainBgNotifications && event.status === "completed" && !hasHeldBgCompletions(event.sessionID)) {
52846
+ await runtime.drainBgNotifications(event.sessionID);
52847
+ if (!hasHeldBgCompletions(event.sessionID))
52848
+ return;
52849
+ }
52382
52850
  if (!runtime.pool || event.status !== "completed" || !hasHeldBgCompletions(event.sessionID))
52383
52851
  return;
52384
52852
  const content = event.result.content;
@@ -52527,6 +52995,17 @@ function holdUnrouted(completion, bridge) {
52527
52995
  tasks.set(completion.task_id, { completion, bridge });
52528
52996
  unrouted.set(completion.session_id, tasks);
52529
52997
  }
52998
+ function forgetUnroutedCompletion(sessionID, taskID) {
52999
+ const state = wakeRouteState();
53000
+ const held = state.unrouted.get(sessionID);
53001
+ if (!held)
53002
+ return;
53003
+ held.delete(taskID);
53004
+ if (held.size === 0) {
53005
+ state.unrouted.delete(sessionID);
53006
+ state.warnedUnrouted.delete(sessionID);
53007
+ }
53008
+ }
52530
53009
  function warnUnroutedOnce(sessionID, message) {
52531
53010
  const { warnedUnrouted } = wakeRouteState();
52532
53011
  if (warnedUnrouted.has(sessionID))
@@ -52534,20 +53013,25 @@ function warnUnroutedOnce(sessionID, message) {
52534
53013
  warnedUnrouted.add(sessionID);
52535
53014
  warn2(message);
52536
53015
  }
52537
- async function routeCompletion(completion, bridge) {
53016
+ async function routeCompletion(completion, bridge, acknowledge = true) {
52538
53017
  const route = wakeRouteState().routes.get(completion.session_id);
52539
53018
  if (route) {
52540
53019
  try {
52541
- await route.completion(completion, bridge);
52542
- return;
53020
+ const admitted = await route.completion(completion, bridge, acknowledge) === true;
53021
+ if (admitted)
53022
+ forgetUnroutedCompletion(completion.session_id, completion.task_id);
53023
+ return admitted;
52543
53024
  } catch (error) {
52544
- holdUnrouted(completion, bridge);
53025
+ if (acknowledge)
53026
+ holdUnrouted(completion, bridge);
52545
53027
  warn2(`[bash_completion] delivery of task ${completion.task_id} to session ${completion.session_id} failed: ${error instanceof Error ? error.message : String(error)}`);
52546
- return;
53028
+ return false;
52547
53029
  }
52548
53030
  }
52549
- holdUnrouted(completion, bridge);
53031
+ if (acknowledge)
53032
+ holdUnrouted(completion, bridge);
52550
53033
  warnUnroutedOnce(completion.session_id, `[bash_completion] no OpenCode Location has session ${completion.session_id} registered; completion of task ${completion.task_id} is held unacknowledged until the session registers`);
53034
+ return false;
52551
53035
  }
52552
53036
  async function routeLongRunning(reminder, _bridge) {
52553
53037
  const route = wakeRouteState().routes.get(reminder.session_id);
@@ -52555,7 +53039,7 @@ async function routeLongRunning(reminder, _bridge) {
52555
53039
  return route.longRunning(reminder);
52556
53040
  warnUnroutedOnce(reminder.session_id, `[bash_long_running] no OpenCode Location has session ${reminder.session_id} registered; long-running reminder for task ${reminder.task_id} was not delivered`);
52557
53041
  }
52558
- function deliverUnrouted(sessionID) {
53042
+ function deliverUnrouted(sessionID, replacementBridge) {
52559
53043
  const state = wakeRouteState();
52560
53044
  state.warnedUnrouted.delete(sessionID);
52561
53045
  const held = state.unrouted.get(sessionID);
@@ -52563,11 +53047,15 @@ function deliverUnrouted(sessionID) {
52563
53047
  return;
52564
53048
  state.unrouted.delete(sessionID);
52565
53049
  for (const { completion, bridge } of held.values()) {
52566
- routeCompletion(completion, bridge);
53050
+ if (replacementBridge && bridge.getCwd() !== replacementBridge.getCwd())
53051
+ continue;
53052
+ routeCompletion(completion, replacementBridge ?? bridge);
52567
53053
  }
52568
53054
  }
52569
53055
  var sharedBridgeOptions = {
52570
- onBashCompletion: routeCompletion,
53056
+ onBashCompletion: async (completion, bridge) => {
53057
+ await routeCompletion(completion, bridge);
53058
+ },
52571
53059
  onBashLongRunning: routeLongRunning
52572
53060
  };
52573
53061
  function detachWaitsForCompletion(sessionID, text, bridge, config) {
@@ -52582,15 +53070,17 @@ function detachWaitsForCompletion(sessionID, text, bridge, config) {
52582
53070
  function createV2RuntimeConsumer(context, getConfig = () => ({})) {
52583
53071
  const delivery = new V2SessionDelivery(context.session);
52584
53072
  const sessions = new Set;
53073
+ let disposed = false;
53074
+ let subcPool;
52585
53075
  const route = {
52586
- completion: async (completion, bridge) => {
53076
+ completion: async (completion, bridge, acknowledge = true) => {
52587
53077
  if (await resolveIsSubagent(context, completion.session_id)) {
52588
53078
  await awaitV2ExecutionSettlement(context, completion.session_id);
52589
53079
  }
52590
53080
  if (await resolveIsSubagent(context, completion.session_id) && !v2WatchWakeAllowed(context, completion.session_id, completion.task_id)) {
52591
53081
  holdBgCompletion(completion.session_id, completion, v2SubagentStopped(context, completion.session_id));
52592
53082
  forgetV2Watch(context, completion.session_id, completion.task_id);
52593
- return;
53083
+ return false;
52594
53084
  }
52595
53085
  const text = formatSystemReminder([completion]);
52596
53086
  const admitted = await Effect13.runPromise(delivery.completion({
@@ -52605,13 +53095,52 @@ function createV2RuntimeConsumer(context, getConfig = () => ({})) {
52605
53095
  }
52606
53096
  }));
52607
53097
  if (!admitted)
52608
- return;
53098
+ return false;
52609
53099
  detachWaitsForCompletion(completion.session_id, text, bridge, getConfig());
52610
- await bridge.send("bash_ack_completions", {
52611
- session_id: completion.session_id,
52612
- task_ids: [completion.task_id]
52613
- });
53100
+ if (acknowledge) {
53101
+ if (subcPool) {
53102
+ consumeBgCompletion(completion.session_id, completion.task_id);
53103
+ await markBgCompletionDelivered({
53104
+ ctx: { pool: subcPool() },
53105
+ directory: bridge.getCwd(),
53106
+ sessionID: completion.session_id,
53107
+ resolvedBridge: bridge
53108
+ }, completion.task_id);
53109
+ } else {
53110
+ await bridge.send("bash_ack_completions", {
53111
+ session_id: completion.session_id,
53112
+ task_ids: [completion.task_id]
53113
+ });
53114
+ }
53115
+ }
52614
53116
  forgetV2Watch(context, completion.session_id, completion.task_id);
53117
+ return true;
53118
+ },
53119
+ patternMatch: async (match, bridge) => {
53120
+ if (await resolveIsSubagent(context, match.session_id)) {
53121
+ await awaitV2ExecutionSettlement(context, match.session_id);
53122
+ if (!v2WatchWakeAllowed(context, match.session_id, match.task_id))
53123
+ return false;
53124
+ }
53125
+ const text = formatPatternMatchReminder([match]);
53126
+ const admitted = await Effect13.runPromise(delivery.completion({
53127
+ sessionID: match.session_id,
53128
+ taskIDs: [match.task_id],
53129
+ notificationID: `match\x00${match.watch_id}\x00${match.match_offset}`,
53130
+ text,
53131
+ metadata: {
53132
+ source: "aft",
53133
+ kind: "bash_pattern_match",
53134
+ task_ids: [match.task_id],
53135
+ watch_id: match.watch_id
53136
+ }
53137
+ }));
53138
+ if (admitted) {
53139
+ detachWaitsForCompletion(match.session_id, text, bridge, getConfig());
53140
+ if (match.reason === "task_exit")
53141
+ forgetUnroutedCompletion(match.session_id, match.task_id);
53142
+ }
53143
+ return admitted;
52615
53144
  },
52616
53145
  longRunning: (reminder) => Effect13.runPromise(delivery.status({
52617
53146
  sessionID: reminder.session_id,
@@ -52624,23 +53153,74 @@ function createV2RuntimeConsumer(context, getConfig = () => ({})) {
52624
53153
  }
52625
53154
  }))
52626
53155
  };
52627
- const registerSession = (sessionID) => {
52628
- if (!sessionID)
53156
+ const registerSession = (sessionID, bridge) => {
53157
+ if (!sessionID || disposed)
52629
53158
  return;
52630
53159
  sessions.add(sessionID);
52631
53160
  const { routes } = wakeRouteState();
52632
53161
  routes.set(sessionID, route);
52633
- deliverUnrouted(sessionID);
53162
+ deliverUnrouted(sessionID, bridge);
52634
53163
  };
52635
53164
  return {
52636
53165
  bridgeOptions: sharedBridgeOptions,
52637
53166
  registerSession,
53167
+ subcWakes: (getPool, directory) => {
53168
+ subcPool = getPool;
53169
+ const root = canonicalizeProjectRoot(directory);
53170
+ const drain = (sessionID, nudgeRef, resolvedBridge) => {
53171
+ if (disposed)
53172
+ return Promise.resolve();
53173
+ return handleSubcBgEventsNudge({
53174
+ ctx: { pool: getPool() },
53175
+ directory: root,
53176
+ sessionID,
53177
+ client: context,
53178
+ nudgeRef,
53179
+ resolvedBridge,
53180
+ delivery: {
53181
+ completion: (completion, bridge) => routeCompletion({ ...completion, type: "bash_completed", session_id: sessionID }, bridge, false),
53182
+ patternMatch: async (match, bridge) => {
53183
+ if (match.session_id !== sessionID)
53184
+ return false;
53185
+ return await wakeRouteState().routes.get(sessionID)?.patternMatch?.(match, bridge) ?? false;
53186
+ }
53187
+ }
53188
+ });
53189
+ };
53190
+ return {
53191
+ handlers: {
53192
+ onBgEventsNudge: (projectRoot, sessionID) => {
53193
+ if (canonicalizeProjectRoot(projectRoot) === root)
53194
+ return drain(sessionID);
53195
+ },
53196
+ onBgEventsNudgeRef: (ref) => {
53197
+ if (ref.canonicalRoot === root)
53198
+ return drain(ref.session, ref);
53199
+ }
53200
+ },
53201
+ drainSession: (sessionID) => {
53202
+ if (disposed)
53203
+ return Promise.resolve();
53204
+ const bridge = getPool().getBridge(root);
53205
+ registerSession(sessionID, bridge);
53206
+ return drain(sessionID, undefined, bridge);
53207
+ },
53208
+ restoreSession: async (sessionID) => {
53209
+ if (disposed)
53210
+ return;
53211
+ const bridge = getPool().getBridge(root);
53212
+ registerSession(sessionID, bridge);
53213
+ await drain(sessionID, undefined, bridge);
53214
+ }
53215
+ };
53216
+ },
52638
53217
  onToolAbort: (sessionID) => noteV2ToolAbort(context, sessionID),
52639
53218
  executeBash: (execution) => {
52640
53219
  registerSession(execution.context.sessionID);
52641
53220
  return executeV2Bash(execution);
52642
53221
  },
52643
53222
  dispose: () => {
53223
+ disposed = true;
52644
53224
  delivery.dispose();
52645
53225
  const { routes } = wakeRouteState();
52646
53226
  for (const sessionID of sessions) {
@@ -52652,6 +53232,34 @@ function createV2RuntimeConsumer(context, getConfig = () => ({})) {
52652
53232
  };
52653
53233
  }
52654
53234
 
53235
+ // src/wakes/subc-recovery.ts
53236
+ import { DateTime, Effect as Effect14 } from "effect";
53237
+ var RECENT_SESSION_LIMIT = 20;
53238
+ var RECENT_SESSION_WINDOW_MS = 24 * 60 * 60 * 1000;
53239
+ function recoverV2SubcSessions(context, directory, restoreSession) {
53240
+ const host = context;
53241
+ return Effect14.gen(function* () {
53242
+ if (typeof host.session?.list !== "function")
53243
+ return;
53244
+ yield* Effect14.sleep("10 millis");
53245
+ const sessions = yield* host.session.list({
53246
+ directory,
53247
+ order: "desc",
53248
+ limit: RECENT_SESSION_LIMIT
53249
+ });
53250
+ const cutoff = Date.now() - RECENT_SESSION_WINDOW_MS;
53251
+ for (const session of sessions.data.slice(0, RECENT_SESSION_LIMIT)) {
53252
+ if (session.location.directory !== directory || DateTime.toEpochMillis(session.time.updated) < cutoff)
53253
+ continue;
53254
+ yield* Effect14.tryPromise({
53255
+ try: () => restoreSession(session.id),
53256
+ catch: (error) => error
53257
+ }).pipe(Effect14.catch((error) => Effect14.sync(() => warn2(`[bash_completion] session recovery failed: ${String(error)}`))));
53258
+ yield* Effect14.sleep("10 millis");
53259
+ }
53260
+ }).pipe(Effect14.timeout("30 seconds"), Effect14.catch((error) => Effect14.sync(() => warn2(`[bash_completion] recent session discovery failed: ${String(error)}`))));
53261
+ }
53262
+
52655
53263
  // src/entry/server-runtime.mjs
52656
53264
  var defaults = {
52657
53265
  ...defaultBridgeBootstrapDependencies,
@@ -52692,6 +53300,7 @@ async function bootLocation(context, location, dependencies) {
52692
53300
  let liveConfig = config;
52693
53301
  const consumers = dependencies.toolConsumers(context, config, () => liveConfig);
52694
53302
  const isProjectEnabled = createProjectAcceptance(directory, dependencies);
53303
+ const subcWakes = config.subc?.connection_file?.trim() ? consumers.subcWakes?.(() => pool, directory) : undefined;
52695
53304
  const pool = await dependencies.acquireBridge(canonicalDirectory, {
52696
53305
  harness: "opencode",
52697
53306
  binaryPath: environment.binaryPath,
@@ -52707,7 +53316,8 @@ async function bootLocation(context, location, dependencies) {
52707
53316
  ...consumers.bridgeOptions
52708
53317
  },
52709
53318
  configOverrides: environment.configOverrides,
52710
- subcConnectionFile: config.subc?.connection_file
53319
+ subcConnectionFile: config.subc?.connection_file,
53320
+ ...subcWakes?.handlers
52711
53321
  });
52712
53322
  environment.attach(pool);
52713
53323
  const toolContext = {
@@ -52717,7 +53327,8 @@ async function bootLocation(context, location, dependencies) {
52717
53327
  hashlineEffective: openCodeHashlineEffective(config),
52718
53328
  hostToolNames: V2_HOST_TOOL_NAMES,
52719
53329
  storageDir: environment.storageDir,
52720
- isProjectEnabled
53330
+ isProjectEnabled,
53331
+ drainBgNotifications: subcWakes?.drainSession
52721
53332
  };
52722
53333
  const tools = dependencies.buildToolMap(toolContext, config, unknownDisabledToolsReporter(notify));
52723
53334
  const registeredTools = new Set(Object.keys(tools));
@@ -52737,7 +53348,7 @@ async function bootLocation(context, location, dependencies) {
52737
53348
  },
52738
53349
  notify
52739
53350
  });
52740
- return { consumers, pool, tools, liveConfigReload, toolContext, directory, hintsBlock };
53351
+ return { consumers, pool, tools, liveConfigReload, toolContext, directory, hintsBlock, subcWakes };
52741
53352
  }
52742
53353
  var runningByDirectory = new Map;
52743
53354
  var startsInProcess = 0;
@@ -52764,30 +53375,30 @@ function makeServerEffect(overrides = {}) {
52764
53375
  const location = context.location;
52765
53376
  if (typeof location?.directory !== "string") {
52766
53377
  debug2("V2 effect skipped: host context has no location");
52767
- return Effect14.void;
53378
+ return Effect15.void;
52768
53379
  }
52769
53380
  if (typeof context.permission?.assert !== "function") {
52770
53381
  throw new Error("AFT requires OpenCode 2.0.29 or newer: this V2 host has no permission.assert. Upgrade OpenCode before loading AFT.");
52771
53382
  }
52772
- return Effect14.gen(function* () {
53383
+ return Effect15.gen(function* () {
52773
53384
  const logRuntimeStop = logRuntimeStart(location);
52774
- yield* Effect14.addFinalizer(() => Effect14.sync(logRuntimeStop));
52775
- const runtime = yield* Effect14.promise(() => bootLocation(context, location, dependencies));
53385
+ yield* Effect15.addFinalizer(() => Effect15.sync(logRuntimeStop));
53386
+ const runtime = yield* Effect15.promise(() => bootLocation(context, location, dependencies));
52776
53387
  if (!runtime)
52777
53388
  return;
52778
53389
  if (runtime.configError !== undefined) {
52779
53390
  const rpc = yield* dependencies.registerConfigErrorRpc(context, runtime.configError);
52780
- yield* Effect14.addFinalizer(() => Effect14.promise(() => rpc.dispose()));
53391
+ yield* Effect15.addFinalizer(() => Effect15.promise(() => rpc.dispose()));
52781
53392
  yield* dependencies.registerTools(context, location, runtime.tools, runtime.consumers);
52782
53393
  return;
52783
53394
  }
52784
- yield* Effect14.addFinalizer(() => Effect14.promise(async () => {
53395
+ yield* Effect15.addFinalizer(() => Effect15.promise(async () => {
52785
53396
  runtime.liveConfigReload?.stop();
52786
53397
  runtime.consumers.dispose?.();
52787
53398
  await dependencies.releaseBridge(runtime.pool);
52788
53399
  }));
52789
53400
  const rpc = yield* dependencies.registerRpc(context, location, runtime.pool);
52790
- yield* Effect14.addFinalizer(() => Effect14.promise(() => rpc.dispose()));
53401
+ yield* Effect15.addFinalizer(() => Effect15.promise(() => rpc.dispose()));
52791
53402
  yield* registerV2SessionLifecycle(context, runtime.toolContext);
52792
53403
  yield* dependencies.registerPromptHook(context, {
52793
53404
  pool: runtime.pool,
@@ -52798,6 +53409,9 @@ function makeServerEffect(overrides = {}) {
52798
53409
  yield* dependencies.registerTools(context, location, runtime.tools, runtime.consumers);
52799
53410
  yield* dependencies.registerToolHooks(context, runtime.toolContext, new Set(Object.keys(runtime.tools)), runtime.tools.bash?.description);
52800
53411
  yield* dependencies.registerWorkflowHints(context, runtime.hintsBlock);
53412
+ if (runtime.subcWakes) {
53413
+ yield* Effect15.forkScoped(recoverV2SubcSessions(context, runtime.directory, runtime.subcWakes.restoreSession));
53414
+ }
52801
53415
  });
52802
53416
  };
52803
53417
  }