@cortexkit/aft-opencode 0.18.2 → 0.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7813,7 +7813,8 @@ import * as fs from "fs";
7813
7813
  import * as os from "os";
7814
7814
  import * as path from "path";
7815
7815
  var TAG = "[aft-plugin]";
7816
- var logFile = path.join(os.tmpdir(), "aft-plugin.log");
7816
+ var isTestEnv = process.env.BUN_TEST === "1" || false;
7817
+ var logFile = path.join(os.tmpdir(), isTestEnv ? "aft-plugin-test.log" : "aft-plugin.log");
7817
7818
  var useStderr = process.env.AFT_LOG_STDERR === "1";
7818
7819
  var buffer = [];
7819
7820
  var flushTimer = null;
@@ -7871,6 +7872,9 @@ function warn(message, data) {
7871
7872
  function error(message, data) {
7872
7873
  write("ERROR", message, data);
7873
7874
  }
7875
+ function sessionLog(sessionId, message, data) {
7876
+ write("INFO", message, data, sessionId);
7877
+ }
7874
7878
  function sessionWarn(sessionId, message, data) {
7875
7879
  write("WARN", message, data, sessionId);
7876
7880
  }
@@ -7878,15 +7882,90 @@ function getLogFilePath() {
7878
7882
  return logFile;
7879
7883
  }
7880
7884
 
7885
+ // src/shared/last-user-model.ts
7886
+ var CACHE_TTL_MS = 5000;
7887
+ var CACHE_MAX_ENTRIES = 100;
7888
+ var cache = new Map;
7889
+ async function getLastUserModel(client, sessionId) {
7890
+ const now = Date.now();
7891
+ const cached = cache.get(sessionId);
7892
+ if (cached && cached.expiresAt > now) {
7893
+ cache.delete(sessionId);
7894
+ cache.set(sessionId, cached);
7895
+ return cached.model;
7896
+ }
7897
+ if (cached)
7898
+ cache.delete(sessionId);
7899
+ const c = client;
7900
+ const fetcher = c?.session?.messages;
7901
+ if (typeof fetcher !== "function") {
7902
+ return null;
7903
+ }
7904
+ let messages;
7905
+ try {
7906
+ const result = await fetcher({ path: { id: sessionId } });
7907
+ messages = result?.data ?? [];
7908
+ } catch {
7909
+ return null;
7910
+ }
7911
+ for (let i = messages.length - 1;i >= 0; i--) {
7912
+ const info = messages[i]?.info;
7913
+ if (info?.role !== "user")
7914
+ continue;
7915
+ if (isSyntheticIgnoredMessage(messages[i]))
7916
+ continue;
7917
+ const model = info.model;
7918
+ if (!model?.providerID || !model?.modelID)
7919
+ continue;
7920
+ const resolved = {
7921
+ providerID: model.providerID,
7922
+ modelID: model.modelID,
7923
+ ...model.variant ? { variant: model.variant } : {}
7924
+ };
7925
+ setCache(sessionId, resolved);
7926
+ return resolved;
7927
+ }
7928
+ return null;
7929
+ }
7930
+ function setCache(sessionId, model) {
7931
+ if (cache.has(sessionId))
7932
+ cache.delete(sessionId);
7933
+ cache.set(sessionId, { expiresAt: Date.now() + CACHE_TTL_MS, model });
7934
+ while (cache.size > CACHE_MAX_ENTRIES) {
7935
+ const oldest = cache.keys().next().value;
7936
+ if (typeof oldest !== "string")
7937
+ break;
7938
+ cache.delete(oldest);
7939
+ }
7940
+ }
7941
+ function isSyntheticIgnoredMessage(message) {
7942
+ const parts = message?.info?.parts ?? message?.parts;
7943
+ return Array.isArray(parts) && parts.length > 0 && parts.every((part) => part?.ignored === true);
7944
+ }
7945
+
7881
7946
  // src/bg-notifications.ts
7882
7947
  var sessionBgStates = new Map;
7883
7948
  var SESSION_BG_STATE_IDLE_TTL_MS = 60 * 60 * 1000;
7884
7949
  var DEBOUNCE_STEP_MS = 200;
7885
7950
  var DEBOUNCE_CAP_MS = 1000;
7951
+ var UNKNOWN_COMPLETION_TTL_MS = 5000;
7952
+ var UNKNOWN_COMPLETION_CAP = 32;
7886
7953
  var DEFAULT_SESSION_ID = "__default__";
7887
7954
  var LOG_PREFIX = "[aft-plugin] bg-notifications:";
7888
7955
  function trackBgTask(sessionID, taskId) {
7889
- stateFor(sessionID).outstandingTaskIds.add(taskId);
7956
+ const state = stateFor(sessionID);
7957
+ pruneUnknownCompletions(state, Date.now());
7958
+ const buffered = state.unknownCompletions.filter((entry) => entry.completion.task_id === taskId);
7959
+ state.unknownCompletions = state.unknownCompletions.filter((entry) => entry.completion.task_id !== taskId);
7960
+ if (buffered.length > 0) {
7961
+ for (const entry of buffered) {
7962
+ if (!state.pendingCompletions.some((pending) => pending.task_id === taskId)) {
7963
+ state.pendingCompletions.push(entry.completion);
7964
+ }
7965
+ }
7966
+ return;
7967
+ }
7968
+ state.outstandingTaskIds.add(taskId);
7890
7969
  }
7891
7970
  function ingestBgCompletions(sessionID, completions) {
7892
7971
  if (!Array.isArray(completions) || completions.length === 0)
@@ -7896,8 +7975,10 @@ function ingestBgCompletions(sessionID, completions) {
7896
7975
  for (const completion of completions) {
7897
7976
  if (!isBgCompletion(completion))
7898
7977
  continue;
7899
- if (!state.outstandingTaskIds.has(completion.task_id))
7978
+ if (!state.outstandingTaskIds.has(completion.task_id)) {
7979
+ bufferUnknownCompletion(state, completion);
7900
7980
  continue;
7981
+ }
7901
7982
  state.outstandingTaskIds.delete(completion.task_id);
7902
7983
  if (!state.pendingCompletions.some((pending) => pending.task_id === completion.task_id) && !accepted.some((pending) => pending.task_id === completion.task_id)) {
7903
7984
  accepted.push(completion);
@@ -7906,6 +7987,10 @@ function ingestBgCompletions(sessionID, completions) {
7906
7987
  state.pendingCompletions.push(...accepted);
7907
7988
  return accepted;
7908
7989
  }
7990
+ async function handlePushedBgCompletion(drainContext, completion) {
7991
+ ingestBgCompletions(drainContext.sessionID, [completion]);
7992
+ await triggerWakeIfPending(drainContext, true);
7993
+ }
7909
7994
  async function appendInTurnBgCompletions(drainContext, output) {
7910
7995
  if (!output)
7911
7996
  return;
@@ -7922,43 +8007,55 @@ async function appendInTurnBgCompletions(drainContext, output) {
7922
8007
  state.pendingCompletions = [];
7923
8008
  }
7924
8009
  async function handleIdleBgCompletions(drainContext) {
8010
+ await triggerWakeIfPending(drainContext, false);
8011
+ }
8012
+ async function triggerWakeIfPending(drainContext, skipDrain) {
7925
8013
  const state = stateFor(drainContext.sessionID);
7926
8014
  if (state.wakeFiredThisIdle)
7927
8015
  return;
7928
- if (state.outstandingTaskIds.size > 0) {
8016
+ if (drainContext.isActive?.())
8017
+ return;
8018
+ if (!skipDrain && state.outstandingTaskIds.size > 0) {
7929
8019
  await drainCompletions(drainContext);
7930
8020
  }
7931
8021
  if (state.pendingCompletions.length === 0)
7932
8022
  return;
7933
8023
  scheduleWake(state, async (reminder) => {
7934
- try {
7935
- const client = drainContext.client;
7936
- if (typeof client.session?.promptAsync !== "function") {
7937
- throw new Error("client.session.promptAsync is unavailable");
7938
- }
7939
- await client.session.promptAsync({
7940
- path: { id: drainContext.sessionID },
7941
- body: {
7942
- noReply: false,
7943
- parts: [{ type: "text", text: reminder }]
7944
- }
7945
- });
7946
- } catch (err) {
7947
- warn(`${LOG_PREFIX} wake send failed: ${err instanceof Error ? err.message : String(err)}`);
7948
- }
8024
+ const client = drainContext.client;
8025
+ if (typeof client.session?.promptAsync !== "function") {
8026
+ throw new Error("client.session.promptAsync is unavailable");
8027
+ }
8028
+ const lastModel = await getLastUserModel(client, drainContext.sessionID);
8029
+ const body = {
8030
+ noReply: false,
8031
+ parts: [{ type: "text", text: reminder }]
8032
+ };
8033
+ if (lastModel) {
8034
+ body.model = { providerID: lastModel.providerID, modelID: lastModel.modelID };
8035
+ if (lastModel.variant)
8036
+ body.variant = lastModel.variant;
8037
+ }
8038
+ await client.session.promptAsync({
8039
+ path: { id: drainContext.sessionID },
8040
+ body
8041
+ });
8042
+ }, (err) => {
8043
+ sessionWarn(drainContext.sessionID, `${LOG_PREFIX} wake send failed: ${err instanceof Error ? err.message : String(err)}`);
7949
8044
  });
7950
8045
  }
7951
8046
  function resetBgWake(sessionID) {
7952
8047
  stateFor(sessionID).wakeFiredThisIdle = false;
7953
8048
  }
7954
8049
  function formatSystemReminder(completions) {
7955
- const bullets = completions.map((completion) => `- ${formatCompletion(completion)}`).join(`
8050
+ const bullets = completions.map((completion) => formatCompletion(completion)).join(`
7956
8051
  `);
8052
+ const anyTruncated = completions.some((c) => c.output_truncated === true);
8053
+ const tail = anyTruncated ? `
8054
+
8055
+ For truncated tasks, use bash_status({ taskId: "..." }) to retrieve full output.` : "";
7957
8056
  return `<system-reminder>
7958
8057
  [BACKGROUND BASH COMPLETED]
7959
- ${bullets}
7960
-
7961
- Use bash_status({ task_id: "..." }) to retrieve full output.
8058
+ ${bullets}${tail}
7962
8059
  </system-reminder>`;
7963
8060
  }
7964
8061
  function extractSessionID(value) {
@@ -7984,15 +8081,15 @@ async function drainCompletions({ ctx, directory, sessionID }) {
7984
8081
  const bridge = ctx.pool.getAnyActiveBridge(directory) ?? ctx.pool.getBridge(directory);
7985
8082
  const response = await bridge.send("bash_drain_completions", { session_id: sessionID });
7986
8083
  if (response.success === false) {
7987
- warn(`${LOG_PREFIX} drain failed: ${String(response.message ?? "unknown error")}`);
8084
+ sessionWarn(sessionID, `${LOG_PREFIX} drain failed: ${String(response.message ?? "unknown error")}`);
7988
8085
  return;
7989
8086
  }
7990
8087
  ingestBgCompletions(sessionID, response.bg_completions);
7991
8088
  } catch (err) {
7992
- warn(`${LOG_PREFIX} drain failed: ${err instanceof Error ? err.message : String(err)}`);
8089
+ sessionWarn(sessionID, `${LOG_PREFIX} drain failed: ${err instanceof Error ? err.message : String(err)}`);
7993
8090
  }
7994
8091
  }
7995
- function scheduleWake(state, sendWake) {
8092
+ function scheduleWake(state, sendWake, onSendFailure) {
7996
8093
  const now = Date.now();
7997
8094
  if (state.debounceTimer && state.pendingCompletions.length <= state.scheduledCompletionCount) {
7998
8095
  return;
@@ -8007,16 +8104,24 @@ function scheduleWake(state, sendWake) {
8007
8104
  state.scheduledCompletionCount = state.pendingCompletions.length;
8008
8105
  if (state.debounceTimer)
8009
8106
  clearTimeout(state.debounceTimer);
8010
- const delay = Math.max(0, (state.scheduledFireAt ?? now) - now);
8107
+ const delay = state.retryDelayMs ?? Math.max(0, (state.scheduledFireAt ?? now) - now);
8011
8108
  state.debounceTimer = setTimeout(() => {
8012
- const reminder = formatSystemReminder(state.pendingCompletions);
8109
+ const pending = state.pendingCompletions;
8110
+ const reminder = formatSystemReminder(pending);
8013
8111
  state.pendingCompletions = [];
8014
8112
  state.debounceTimer = null;
8015
- state.wakeFiredThisIdle = true;
8016
8113
  state.firstCompletionAt = null;
8017
8114
  state.scheduledFireAt = null;
8018
8115
  state.scheduledCompletionCount = 0;
8019
- sendWake(reminder);
8116
+ sendWake(reminder).then(() => {
8117
+ state.retryDelayMs = null;
8118
+ state.wakeFiredThisIdle = true;
8119
+ }).catch((err) => {
8120
+ state.pendingCompletions = [...pending, ...state.pendingCompletions];
8121
+ state.retryDelayMs = Math.min((delay || DEBOUNCE_STEP_MS) * 2, DEBOUNCE_CAP_MS);
8122
+ onSendFailure(err);
8123
+ scheduleWake(state, sendWake, onSendFailure);
8124
+ });
8020
8125
  }, delay);
8021
8126
  state.debounceTimer.unref?.();
8022
8127
  }
@@ -8034,6 +8139,8 @@ function stateFor(sessionID) {
8034
8139
  firstCompletionAt: null,
8035
8140
  scheduledFireAt: null,
8036
8141
  scheduledCompletionCount: 0,
8142
+ retryDelayMs: null,
8143
+ unknownCompletions: [],
8037
8144
  lastSeenAt: now
8038
8145
  };
8039
8146
  sessionBgStates.set(key, state);
@@ -8054,6 +8161,18 @@ function cleanupIdleSessionStates(now) {
8054
8161
  sessionBgStates.delete(sessionID);
8055
8162
  }
8056
8163
  }
8164
+ function bufferUnknownCompletion(state, completion) {
8165
+ const now = Date.now();
8166
+ pruneUnknownCompletions(state, now);
8167
+ state.unknownCompletions = state.unknownCompletions.filter((entry) => entry.completion.task_id !== completion.task_id);
8168
+ state.unknownCompletions.push({ completion, receivedAt: now });
8169
+ if (state.unknownCompletions.length > UNKNOWN_COMPLETION_CAP) {
8170
+ state.unknownCompletions.splice(0, state.unknownCompletions.length - UNKNOWN_COMPLETION_CAP);
8171
+ }
8172
+ }
8173
+ function pruneUnknownCompletions(state, now) {
8174
+ state.unknownCompletions = state.unknownCompletions.filter((entry) => now - entry.receivedAt <= UNKNOWN_COMPLETION_TTL_MS);
8175
+ }
8057
8176
  function isBgCompletion(value) {
8058
8177
  if (!value || typeof value !== "object" || Array.isArray(value))
8059
8178
  return false;
@@ -8068,10 +8187,26 @@ ${reminder}` : reminder;
8068
8187
  function formatCompletion(completion) {
8069
8188
  const status = formatStatus(completion);
8070
8189
  const duration = formatDuration(completion);
8071
- return `task ${completion.task_id} (${status}${duration ? `, ${duration}` : ""}): ${completion.command}`;
8190
+ const header = `- task ${completion.task_id} (${status}${duration ? `, ${duration}` : ""})`;
8191
+ const previewBlock = formatOutputPreview(completion);
8192
+ return previewBlock ? `${header}
8193
+ ${previewBlock}` : header;
8194
+ }
8195
+ function formatOutputPreview(completion) {
8196
+ const ansiRegex = /\x1b\[[0-9;]*[a-zA-Z]/g;
8197
+ const raw = (completion.output_preview ?? "").replace(ansiRegex, "");
8198
+ if (!raw.trim())
8199
+ return "";
8200
+ const trimmed = raw.replace(/\n+$/, "");
8201
+ const ellipsis = completion.output_truncated ? "\u2026" : "";
8202
+ const indented = trimmed.split(`
8203
+ `).map((line) => ` ${line}`).join(`
8204
+ `);
8205
+ return ellipsis ? ` ${ellipsis}
8206
+ ${indented}` : indented;
8072
8207
  }
8073
8208
  function formatStatus(completion) {
8074
- if (completion.status === "timeout")
8209
+ if (completion.status === "timed_out" || completion.status === "timeout")
8075
8210
  return "timed out";
8076
8211
  if (completion.status === "killed")
8077
8212
  return "killed";
@@ -21693,6 +21828,7 @@ var ExperimentalConfigSchema = exports_external.object({
21693
21828
  });
21694
21829
  var AftConfigSchema = exports_external.object({
21695
21830
  format_on_edit: exports_external.boolean().optional(),
21831
+ formatter_timeout_secs: exports_external.number().int().min(1).max(600).optional(),
21696
21832
  validate_on_edit: exports_external.enum(["syntax", "full"]).optional(),
21697
21833
  formatter: exports_external.record(exports_external.string(), FormatterEnum).optional(),
21698
21834
  checker: exports_external.record(exports_external.string(), CheckerEnum).optional(),
@@ -24332,13 +24468,17 @@ async function getSessionMessages(client, sessionId) {
24332
24468
  async function sendIgnoredMessage(client, sessionId, text) {
24333
24469
  try {
24334
24470
  const c = client;
24335
- const promptInput = {
24336
- path: { id: sessionId },
24337
- body: {
24338
- noReply: true,
24339
- parts: [{ type: "text", text, ignored: true }]
24340
- }
24471
+ const lastModel = await getLastUserModel(client, sessionId);
24472
+ const body = {
24473
+ noReply: true,
24474
+ parts: [{ type: "text", text, ignored: true }]
24341
24475
  };
24476
+ if (lastModel) {
24477
+ body.model = { providerID: lastModel.providerID, modelID: lastModel.modelID };
24478
+ if (lastModel.variant)
24479
+ body.variant = lastModel.variant;
24480
+ }
24481
+ const promptInput = { path: { id: sessionId }, body };
24342
24482
  if (typeof c.session?.prompt === "function") {
24343
24483
  await Promise.resolve(c.session.prompt(promptInput));
24344
24484
  return true;
@@ -24348,7 +24488,7 @@ async function sendIgnoredMessage(client, sessionId, text) {
24348
24488
  return true;
24349
24489
  }
24350
24490
  } catch (err) {
24351
- log(`[aft-plugin] notification send failed: ${err instanceof Error ? err.message : String(err)}`);
24491
+ sessionLog(sessionId, `[aft-plugin] notification send failed: ${err instanceof Error ? err.message : String(err)}`);
24352
24492
  }
24353
24493
  return false;
24354
24494
  }
@@ -24374,7 +24514,7 @@ async function sendWarning(opts, message) {
24374
24514
  if (!sessionId)
24375
24515
  return;
24376
24516
  const text = `${WARNING_MARKER} ${message}`;
24377
- log(`[aft-plugin] sending warning to session ${sessionId}`);
24517
+ sessionLog(sessionId, `[aft-plugin] sending warning to session ${sessionId}`);
24378
24518
  await sendIgnoredMessage(opts.client, sessionId, text);
24379
24519
  }
24380
24520
  async function sendFeatureAnnouncement(opts, version2, features, storageDir) {
@@ -24397,7 +24537,7 @@ async function sendFeatureAnnouncement(opts, version2, features, storageDir) {
24397
24537
  return;
24398
24538
  const text = [`${FEATURE_MARKER} v${version2}:`, ...features.map((f) => ` \u2022 ${f}`)].join(`
24399
24539
  `);
24400
- log(`[aft-plugin] sending feature announcement for v${version2}`);
24540
+ sessionLog(sessionId, `[aft-plugin] sending feature announcement for v${version2}`);
24401
24541
  await sendIgnoredMessage(opts.client, sessionId, text);
24402
24542
  }
24403
24543
  if (storageDir) {
@@ -24515,7 +24655,7 @@ async function cleanupWarnings(opts) {
24515
24655
  }
24516
24656
  if (warningIds.length === 0)
24517
24657
  return;
24518
- log(`[aft-plugin] cleaning up ${warningIds.length} stale warning(s)`);
24658
+ sessionLog(sessionId, `[aft-plugin] cleaning up ${warningIds.length} stale warning(s)`);
24519
24659
  for (const id of warningIds) {
24520
24660
  await deleteMessage(effectiveServerUrl, sessionId, id);
24521
24661
  }
@@ -24968,6 +25108,9 @@ function isProcessAlive2(pid) {
24968
25108
  }
24969
25109
  }
24970
25110
 
25111
+ // src/pool.ts
25112
+ import { realpathSync as realpathSync3 } from "fs";
25113
+
24971
25114
  // src/bridge.ts
24972
25115
  import { spawn as spawn3 } from "child_process";
24973
25116
  import { homedir as homedir7 } from "os";
@@ -25060,6 +25203,7 @@ class BinaryBridge {
25060
25203
  minVersion;
25061
25204
  onVersionMismatch;
25062
25205
  onConfigureWarnings;
25206
+ onBashCompletion;
25063
25207
  restartResetTimer = null;
25064
25208
  constructor(binaryPath, cwd, options, configOverrides) {
25065
25209
  this.binaryPath = binaryPath;
@@ -25070,6 +25214,7 @@ class BinaryBridge {
25070
25214
  this.minVersion = options?.minVersion;
25071
25215
  this.onVersionMismatch = options?.onVersionMismatch;
25072
25216
  this.onConfigureWarnings = options?.onConfigureWarnings;
25217
+ this.onBashCompletion = options?.onBashCompletion;
25073
25218
  }
25074
25219
  get restartCount() {
25075
25220
  return this._restartCount;
@@ -25077,6 +25222,9 @@ class BinaryBridge {
25077
25222
  isAlive() {
25078
25223
  return this.process !== null && this.process.exitCode === null && !this.process.killed;
25079
25224
  }
25225
+ hasPendingRequests() {
25226
+ return this.pending.size > 0;
25227
+ }
25080
25228
  async send(command, params = {}, options) {
25081
25229
  if (this._shuttingDown) {
25082
25230
  throw new Error(`[aft-plugin] Bridge is shutting down, cannot send "${command}"`);
@@ -25130,17 +25278,21 @@ class BinaryBridge {
25130
25278
  `;
25131
25279
  const effectiveTimeoutMs = options?.transportTimeoutMs ?? options?.timeoutMs ?? this.timeoutMs;
25132
25280
  const requestSessionId = typeof params.session_id === "string" && params.session_id.length > 0 ? params.session_id : undefined;
25281
+ const keepBridgeOnTimeout = options?.keepBridgeOnTimeout === true;
25133
25282
  return new Promise((resolve4, reject) => {
25134
25283
  const timer = setTimeout(() => {
25135
25284
  this.pending.delete(id);
25136
- const timeoutMsg = `Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms \u2014 restarting bridge`;
25285
+ const restartSuffix = keepBridgeOnTimeout ? "" : " \u2014 restarting bridge";
25286
+ const timeoutMsg = `Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms${restartSuffix}`;
25137
25287
  if (requestSessionId) {
25138
25288
  sessionWarn(requestSessionId, timeoutMsg);
25139
25289
  } else {
25140
25290
  warn(timeoutMsg);
25141
25291
  }
25142
25292
  reject(new Error(`[aft-plugin] Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
25143
- this.handleTimeout();
25293
+ if (!keepBridgeOnTimeout) {
25294
+ this.handleTimeout();
25295
+ }
25144
25296
  }, effectiveTimeoutMs);
25145
25297
  this.pending.set(id, { resolve: resolve4, reject, timer, onProgress: options?.onProgress });
25146
25298
  if (!this.process?.stdin?.writable) {
@@ -25345,6 +25497,10 @@ class BinaryBridge {
25345
25497
  }
25346
25498
  continue;
25347
25499
  }
25500
+ if (response.type === "bash_completed") {
25501
+ this.onBashCompletion?.(response, this);
25502
+ continue;
25503
+ }
25348
25504
  const id = response.id;
25349
25505
  if (id && this.pending.has(id)) {
25350
25506
  const entry = this.pending.get(id);
@@ -25354,6 +25510,8 @@ class BinaryBridge {
25354
25510
  clearTimeout(entry.timer);
25355
25511
  this.scheduleRestartCountReset();
25356
25512
  entry.resolve(response);
25513
+ } else if (typeof response.type === "string") {
25514
+ log(`Ignoring unknown stdout push frame type: ${response.type}`);
25357
25515
  }
25358
25516
  } catch (_err) {
25359
25517
  warn(`Failed to parse stdout line: ${line}`);
@@ -25451,7 +25609,8 @@ class BridgePool {
25451
25609
  maxRestarts: options.maxRestarts,
25452
25610
  minVersion: options.minVersion,
25453
25611
  onVersionMismatch: options.onVersionMismatch,
25454
- onConfigureWarnings: options.onConfigureWarnings
25612
+ onConfigureWarnings: options.onConfigureWarnings,
25613
+ onBashCompletion: options.onBashCompletion
25455
25614
  };
25456
25615
  this.configOverrides = configOverrides;
25457
25616
  if (Number.isFinite(this.idleTimeoutMs)) {
@@ -25474,6 +25633,14 @@ class BridgePool {
25474
25633
  }
25475
25634
  return null;
25476
25635
  }
25636
+ getActiveBridgeForRoot(projectRoot) {
25637
+ const key = normalizeKey(projectRoot);
25638
+ const entry = this.bridges.get(key);
25639
+ if (!entry?.bridge.isAlive())
25640
+ return null;
25641
+ entry.lastUsed = Date.now();
25642
+ return entry.bridge;
25643
+ }
25477
25644
  getBridge(projectRoot) {
25478
25645
  const key = normalizeKey(projectRoot);
25479
25646
  const existing = this.bridges.get(key);
@@ -25533,7 +25700,12 @@ class BridgePool {
25533
25700
  }
25534
25701
  }
25535
25702
  function normalizeKey(projectRoot) {
25536
- return projectRoot.replace(/[/\\]+$/, "");
25703
+ const stripped = projectRoot.replace(/[/\\]+$/, "");
25704
+ try {
25705
+ return realpathSync3(stripped);
25706
+ } catch {
25707
+ return stripped;
25708
+ }
25537
25709
  }
25538
25710
 
25539
25711
  // src/resolver.ts
@@ -25771,9 +25943,7 @@ class AftRpcServer {
25771
25943
  return;
25772
25944
  }
25773
25945
  const { token: _token, ...handlerParams } = params;
25774
- log(`RPC call: ${method} params=${JSON.stringify(handlerParams).slice(0, 200)}`);
25775
25946
  handler(handlerParams).then((result) => {
25776
- log(`RPC result: ${method} => ${JSON.stringify(result).slice(0, 200)}`);
25777
25947
  res.writeHead(200, { "Content-Type": "application/json" });
25778
25948
  res.end(JSON.stringify(result));
25779
25949
  }).catch((err) => {
@@ -26025,8 +26195,9 @@ import {
26025
26195
  } from "fs";
26026
26196
  import { isIP } from "net";
26027
26197
  import { join as join17 } from "path";
26198
+ import { Agent, fetch as undiciFetch } from "undici";
26028
26199
  var MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
26029
- var CACHE_TTL_MS = 24 * 60 * 60 * 1000;
26200
+ var CACHE_TTL_MS2 = 24 * 60 * 60 * 1000;
26030
26201
  var FETCH_TIMEOUT_MS = 30000;
26031
26202
  var MAX_REDIRECTS = 5;
26032
26203
  function cacheDir(storageDir) {
@@ -26116,7 +26287,7 @@ function isPrivateIp(address) {
26116
26287
  }
26117
26288
  return true;
26118
26289
  }
26119
- async function assertPublicUrl(url2, allowPrivate) {
26290
+ async function assertPublicUrl(url2, allowPrivate, dnsLookup = lookup) {
26120
26291
  if (url2.protocol !== "http:" && url2.protocol !== "https:") {
26121
26292
  throw new Error(`Only http:// and https:// URLs are supported, got: ${url2.protocol}`);
26122
26293
  }
@@ -26127,14 +26298,27 @@ async function assertPublicUrl(url2, allowPrivate) {
26127
26298
  if (isPrivateIp(hostname3)) {
26128
26299
  throw new Error(`Blocked private URL host ${url2.hostname} (${hostname3})`);
26129
26300
  }
26130
- return;
26301
+ return hostname3;
26131
26302
  }
26132
- const addresses = await lookup(hostname3, { all: true, verbatim: true });
26303
+ const addresses = await dnsLookup(hostname3, { all: true, verbatim: true });
26133
26304
  for (const { address } of addresses) {
26134
26305
  if (isPrivateIp(address)) {
26135
26306
  throw new Error(`Blocked private URL host ${url2.hostname} (${address})`);
26136
26307
  }
26137
26308
  }
26309
+ if (addresses.length === 0) {
26310
+ throw new Error(`Failed to resolve URL host ${url2.hostname}`);
26311
+ }
26312
+ return addresses[0].address;
26313
+ }
26314
+ function createPinnedDispatcher(validatedIp) {
26315
+ return new Agent({
26316
+ connect: {
26317
+ lookup: (_hostname, _opts, callback) => {
26318
+ callback(null, validatedIp, validatedIp.includes(":") ? 6 : 4);
26319
+ }
26320
+ }
26321
+ });
26138
26322
  }
26139
26323
  function resolveRedirectUrl(currentUrl, location) {
26140
26324
  if (!location) {
@@ -26142,17 +26326,21 @@ function resolveRedirectUrl(currentUrl, location) {
26142
26326
  }
26143
26327
  return new URL(location, currentUrl);
26144
26328
  }
26145
- async function fetchWithRedirects(startUrl, allowPrivate) {
26329
+ async function fetchWithRedirects(startUrl, allowPrivate, deps = {}) {
26146
26330
  let currentUrl = startUrl;
26331
+ const fetchImpl = deps.fetchImpl ?? undiciFetch;
26332
+ const dnsLookup = deps.lookup ?? lookup;
26147
26333
  for (let redirectCount = 0;redirectCount <= MAX_REDIRECTS; redirectCount++) {
26148
- await assertPublicUrl(currentUrl, allowPrivate);
26334
+ const validatedIp = await assertPublicUrl(currentUrl, allowPrivate, dnsLookup);
26335
+ const dispatcher = validatedIp ? (deps.dispatcherFactory ?? createPinnedDispatcher)(validatedIp) : undefined;
26149
26336
  const controller = new AbortController;
26150
26337
  const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
26151
26338
  let response;
26152
26339
  try {
26153
- response = await fetch(currentUrl.href, {
26340
+ response = await fetchImpl(currentUrl.href, {
26154
26341
  signal: controller.signal,
26155
26342
  redirect: "manual",
26343
+ dispatcher,
26156
26344
  headers: {
26157
26345
  "user-agent": "aft-opencode-plugin",
26158
26346
  accept: "application/vnd.github.raw, text/markdown, text/x-markdown, text/html;q=0.9, text/plain;q=0.5"
@@ -26206,14 +26394,14 @@ async function fetchUrlToTempFile(url2, storageDir, options = {}) {
26206
26394
  const meta4 = JSON.parse(readFileSync8(metaFile, "utf8"));
26207
26395
  const age = Date.now() - meta4.fetchedAt;
26208
26396
  const cached2 = contentPath(storageDir, hash2, meta4.extension);
26209
- if (age < CACHE_TTL_MS && existsSync11(cached2)) {
26397
+ if (age < CACHE_TTL_MS2 && existsSync11(cached2)) {
26210
26398
  log(`URL cache hit: ${url2} (${Math.round(age / 1000)}s old)`);
26211
26399
  return cached2;
26212
26400
  }
26213
26401
  } catch {}
26214
26402
  }
26215
26403
  log(`Fetching URL: ${url2}`);
26216
- const response = await fetchWithRedirects(parsed, allowPrivate);
26404
+ const response = await fetchWithRedirects(parsed, allowPrivate, options);
26217
26405
  if (!response.ok) {
26218
26406
  throw new Error(`HTTP ${response.status} ${response.statusText} fetching ${url2}`);
26219
26407
  }
@@ -26279,7 +26467,7 @@ function cleanupUrlCache(storageDir) {
26279
26467
  try {
26280
26468
  const meta3 = JSON.parse(readFileSync8(metaFile, "utf8"));
26281
26469
  const age = Date.now() - meta3.fetchedAt;
26282
- if (age > CACHE_TTL_MS) {
26470
+ if (age > CACHE_TTL_MS2) {
26283
26471
  const hash2 = entry.slice(0, -".meta.json".length);
26284
26472
  const content = contentPath(storageDir, hash2, meta3.extension);
26285
26473
  if (existsSync11(content))
@@ -27079,6 +27267,7 @@ function createBashTool(ctx) {
27079
27267
  permissions_requested: true
27080
27268
  }, callBridge, {
27081
27269
  transportTimeoutMs: bashTransportTimeoutMs(args.timeout),
27270
+ keepBridgeOnTimeout: true,
27082
27271
  onProgress: ({ text }) => {
27083
27272
  accumulatedOutput = preview(accumulatedOutput + text);
27084
27273
  metadata?.({ output: accumulatedOutput, description });
@@ -27140,6 +27329,49 @@ function createBashTool(ctx) {
27140
27329
  }
27141
27330
  };
27142
27331
  }
27332
+ function createBashStatusTool(ctx) {
27333
+ return {
27334
+ description: "Check the status and captured output of a background bash task spawned with bash({ background: true }). Returns status (running | completed | failed | killed | timed_out), exit code, duration, and a preview of captured output.",
27335
+ args: {
27336
+ taskId: z3.string().describe("Background task ID returned by bash({ background: true }), e.g. bgb-6b454047.")
27337
+ },
27338
+ execute: async (args, context) => {
27339
+ const data = await callBridge(ctx, context, "bash_status", {
27340
+ task_id: args.taskId
27341
+ });
27342
+ if (data.success === false) {
27343
+ throw new Error(data.message ?? "bash_status failed");
27344
+ }
27345
+ const status = data.status;
27346
+ const exit = typeof data.exit_code === "number" ? ` (exit ${data.exit_code})` : "";
27347
+ const dur = typeof data.duration_ms === "number" ? ` ${Math.round(data.duration_ms / 1000)}s` : "";
27348
+ let text = `Task ${args.taskId}: ${status}${exit}${dur}`;
27349
+ const preview = data.output_preview;
27350
+ if (preview && status !== "running") {
27351
+ text += `
27352
+ ${preview.slice(0, 2000)}`;
27353
+ }
27354
+ return text;
27355
+ }
27356
+ };
27357
+ }
27358
+ function createBashKillTool(ctx) {
27359
+ return {
27360
+ description: "Terminate a running background bash task spawned with bash({ background: true }). Returns confirmation of kill or an error if the task already finished.",
27361
+ args: {
27362
+ taskId: z3.string().describe("Background task ID returned by bash({ background: true }), e.g. bgb-6b454047.")
27363
+ },
27364
+ execute: async (args, context) => {
27365
+ const data = await callBridge(ctx, context, "bash_kill", {
27366
+ task_id: args.taskId
27367
+ });
27368
+ if (data.success === false) {
27369
+ throw new Error(data.message ?? "bash_kill failed");
27370
+ }
27371
+ return `Task ${args.taskId}: ${String(data.status ?? "killed")}`;
27372
+ }
27373
+ };
27374
+ }
27143
27375
  function bashTransportTimeoutMs(timeout) {
27144
27376
  const bashTimeout = timeout ?? DEFAULT_BASH_TIMEOUT_MS;
27145
27377
  return Math.max(30000, bashTimeout + BASH_TRANSPORT_TIMEOUT_OVERHEAD_MS);
@@ -27770,6 +28002,11 @@ function createEditTool(ctx, writeToolName = "write") {
27770
28002
  }
27771
28003
  }
27772
28004
  let result = JSON.stringify(data);
28005
+ const globSkipNote = formatGlobSkipReasonsNote(data.format_skip_reasons);
28006
+ if (globSkipNote)
28007
+ result += `
28008
+
28009
+ ${globSkipNote}`;
27773
28010
  if (!args.dryRun) {
27774
28011
  const diags = data.lsp_diagnostics;
27775
28012
  if (diags && diags.length > 0) {
@@ -27800,6 +28037,14 @@ Note: LSP server(s) exited during this edit: ${exitedServers.join(", ")}. Their
27800
28037
  }
27801
28038
  };
27802
28039
  }
28040
+ function formatGlobSkipReasonsNote(reasons) {
28041
+ if (!Array.isArray(reasons))
28042
+ return;
28043
+ const actionable = reasons.filter((reason) => typeof reason === "string").filter((reason) => ["formatter_not_installed", "formatter_excluded_path", "timeout", "error"].includes(reason));
28044
+ if (actionable.length === 0)
28045
+ return;
28046
+ return `Note: formatter skipped some glob edit result file(s): ${[...new Set(actionable)].sort().join(", ")}. See per-file format_skipped_reason values for details.`;
28047
+ }
27803
28048
  var APPLY_PATCH_DESCRIPTION = `Use the \`apply_patch\` tool to edit files. Your patch language is a stripped\u2011down, file\u2011oriented diff format designed to be easy to parse and safe to apply. You can think of it as a high\u2011level envelope:
27804
28049
 
27805
28050
  *** Begin Patch
@@ -28051,8 +28296,8 @@ ${diagLines}`);
28051
28296
  const summary = partial2 ? `Patch partially applied \u2014 ${hunks.length - failures.length} of ${hunks.length} hunk(s) succeeded. Failed: ${failures.join(", ")}. Successful changes are kept; use \`aft_safety\` to revert if you want to abort.` : `Patch failed \u2014 none of the ${hunks.length} hunk(s) applied: ${failures.join(", ")}.`;
28052
28297
  results.push(summary);
28053
28298
  if (!partial2) {
28054
- return results.join(`
28055
- `);
28299
+ throw new Error(results.join(`
28300
+ `));
28056
28301
  }
28057
28302
  }
28058
28303
  const callID = getCallID2(context);
@@ -28158,6 +28403,8 @@ function createMoveTool(ctx) {
28158
28403
  function hoistedTools(ctx) {
28159
28404
  return {
28160
28405
  bash: createBashTool(ctx),
28406
+ bash_status: createBashStatusTool(ctx),
28407
+ bash_kill: createBashKillTool(ctx),
28161
28408
  read: createReadTool(ctx),
28162
28409
  write: createWriteTool(ctx, "edit"),
28163
28410
  edit: createEditTool(ctx, "write"),
@@ -28529,9 +28776,12 @@ Provide exactly ONE of 'filePath' or 'url'. Pass either 'symbol' for a single lo
28529
28776
  const params2 = { file: file2, symbol: sym };
28530
28777
  if (args.contextLines !== undefined)
28531
28778
  params2.context_lines = args.contextLines;
28532
- return callBridge(ctx, context, "zoom", params2);
28779
+ return callBridge(ctx, context, "zoom", params2).catch((err) => ({
28780
+ success: false,
28781
+ message: err instanceof Error ? err.message : String(err)
28782
+ }));
28533
28783
  }));
28534
- return JSON.stringify(results);
28784
+ return JSON.stringify(formatZoomBatchResult(args.symbols, results), null, 2);
28535
28785
  }
28536
28786
  const params = { file: file2 };
28537
28787
  if (typeof args.symbol === "string")
@@ -28547,6 +28797,40 @@ Provide exactly ONE of 'filePath' or 'url'. Pass either 'symbol' for a single lo
28547
28797
  }
28548
28798
  };
28549
28799
  }
28800
+ function formatZoomBatchResult(symbols, responses) {
28801
+ const entries = symbols.map((name, index) => {
28802
+ const response = responses[index] ?? { success: false, message: "missing zoom response" };
28803
+ if (response.success === false) {
28804
+ const message = typeof response.message === "string" && response.message.length > 0 ? response.message : "zoom failed";
28805
+ return { name, success: false, error: message };
28806
+ }
28807
+ return { name, success: true, content: zoomResponseContent(response) };
28808
+ });
28809
+ const complete = entries.every((entry) => entry.success);
28810
+ const lines = [];
28811
+ if (!complete) {
28812
+ lines.push("Incomplete zoom results: one or more symbols failed.");
28813
+ }
28814
+ for (const entry of entries) {
28815
+ if (entry.success) {
28816
+ lines.push(`Symbol "${entry.name}":
28817
+ ${entry.content ?? ""}`.trimEnd());
28818
+ } else {
28819
+ lines.push(`Symbol "${entry.name}" not found: ${entry.error ?? "zoom failed"}`);
28820
+ }
28821
+ }
28822
+ return { complete, symbols: entries, text: lines.join(`
28823
+
28824
+ `) };
28825
+ }
28826
+ function zoomResponseContent(response) {
28827
+ if (typeof response.content === "string")
28828
+ return response.content;
28829
+ if (typeof response.text === "string")
28830
+ return response.text;
28831
+ const { success: _success2, ...rest } = response;
28832
+ return JSON.stringify(rest, null, 2);
28833
+ }
28550
28834
  var SKIP_DIRS = new Set([
28551
28835
  "node_modules",
28552
28836
  ".git",
@@ -28622,7 +28906,7 @@ var LSP_SYMBOL_KIND_MAP = {
28622
28906
  12: "function",
28623
28907
  23: "struct"
28624
28908
  };
28625
- async function queryLspHints(client, symbolName, directory) {
28909
+ async function queryLspHints(client, symbolName, directory, sessionId) {
28626
28910
  try {
28627
28911
  const statusResult = await client.lsp.status();
28628
28912
  const servers = statusResult.data;
@@ -28664,7 +28948,7 @@ async function queryLspHints(client, symbolName, directory) {
28664
28948
  }
28665
28949
  return { symbols: hints };
28666
28950
  } catch (err) {
28667
- warn(`LSP query failed for "${symbolName}": ${err.message}`);
28951
+ sessionWarn(sessionId, `LSP query failed for "${symbolName}": ${err.message}`);
28668
28952
  return;
28669
28953
  }
28670
28954
  }
@@ -28753,7 +29037,7 @@ function refactoringTools(ctx) {
28753
29037
  params.call_site_line = Number(args.callSiteLine);
28754
29038
  break;
28755
29039
  }
28756
- const hints = await queryLspHints(ctx.client, args.symbol ?? args.name);
29040
+ const hints = await queryLspHints(ctx.client, args.symbol ?? args.name, undefined, context.sessionID);
28757
29041
  if (hints)
28758
29042
  params.lsp_hints = hints;
28759
29043
  const response = await callBridge(ctx, context, commandMap[op], params);
@@ -29109,6 +29393,58 @@ function structureTools(ctx) {
29109
29393
  };
29110
29394
  }
29111
29395
 
29396
+ // src/workflow-hints.ts
29397
+ var HEADING = "## Prefer AFT tools for token efficiency";
29398
+ function buildWorkflowHints(opts) {
29399
+ const sections = [];
29400
+ const grepName = opts.hoistBuiltins ? "grep" : "aft_grep";
29401
+ const bashName = opts.hoistBuiltins ? "bash" : "aft_bash";
29402
+ const bashStatusName = "bash_status";
29403
+ const hasOutline = !opts.disabledTools.has("aft_outline");
29404
+ const hasZoom = !opts.disabledTools.has("aft_zoom");
29405
+ const hasGrep = opts.toolSurface !== "minimal" && !opts.disabledTools.has(grepName);
29406
+ const hasSearch = opts.toolSurface !== "minimal" && opts.semanticEnabled && !opts.disabledTools.has("aft_search");
29407
+ const hasNavigate = opts.toolSurface === "all" && !opts.disabledTools.has("aft_navigate");
29408
+ const hasBgBash = opts.bashBackgroundEnabled && !opts.disabledTools.has(bashName) && !opts.disabledTools.has(bashStatusName);
29409
+ if (hasOutline && hasZoom) {
29410
+ sections.push(`**Web/URL access**: \`aft_outline({ url })\` first for structure, then \`aft_zoom({ url, symbol: "<heading>" })\` for the specific section.`);
29411
+ }
29412
+ if (hasOutline && hasZoom && (hasGrep || hasSearch)) {
29413
+ const locator = hasGrep && hasSearch ? `\`${grepName}\` or \`aft_search\`` : hasGrep ? `\`${grepName}\`` : "`aft_search`";
29414
+ sections.push(`**Code exploration**: ${locator} to locate \u2192 \`aft_outline\` for structure \u2192 \`aft_zoom\` for symbol(s).`);
29415
+ }
29416
+ if (hasNavigate) {
29417
+ sections.push([
29418
+ "Use `aft_navigate` instead of grep + read chains for relationship questions:",
29419
+ "- `callers` \u2014 find all call sites before changing a function signature",
29420
+ "- `impact` \u2014 blast radius (which functions/files will need updates)",
29421
+ "- `trace_to` \u2014 how execution reaches this code from entry points (routes, exports, main)",
29422
+ "- `trace_data` \u2014 follow a value through assignments and parameters across files"
29423
+ ].join(`
29424
+ `));
29425
+ }
29426
+ if (hasBgBash) {
29427
+ sections.push(`**Long-running commands** (builds, installs, full test suites): \`${bashName}({ background: true })\` returns immediately with a \`taskId\`. Check progress with \`${bashStatusName}({ taskId })\`.`);
29428
+ }
29429
+ if (sections.length === 0) {
29430
+ return null;
29431
+ }
29432
+ return `${HEADING}
29433
+
29434
+ ${sections.join(`
29435
+
29436
+ `)}`;
29437
+ }
29438
+ function buildHintsFromConfig(config2, disabledTools) {
29439
+ return buildWorkflowHints({
29440
+ toolSurface: config2.tool_surface ?? "recommended",
29441
+ hoistBuiltins: config2.hoist_builtin_tools !== false,
29442
+ semanticEnabled: config2.semantic_search === true,
29443
+ bashBackgroundEnabled: config2.experimental?.bash?.background === true,
29444
+ disabledTools
29445
+ });
29446
+ }
29447
+
29112
29448
  // src/index.ts
29113
29449
  var STATUS_COMMAND = "aft-status";
29114
29450
  var SENTINEL_PREFIX = "__AFT_STATUS_";
@@ -29129,13 +29465,17 @@ function coerceConfigureWarnings(warnings) {
29129
29465
  }
29130
29466
  async function sendIgnoredMessage2(client, sessionID, text) {
29131
29467
  const typedClient = client;
29132
- const promptInput = {
29133
- path: { id: sessionID },
29134
- body: {
29135
- noReply: true,
29136
- parts: [{ type: "text", text, ignored: true }]
29137
- }
29468
+ const lastModel = await getLastUserModel(client, sessionID);
29469
+ const body = {
29470
+ noReply: true,
29471
+ parts: [{ type: "text", text, ignored: true }]
29138
29472
  };
29473
+ if (lastModel) {
29474
+ body.model = { providerID: lastModel.providerID, modelID: lastModel.modelID };
29475
+ if (lastModel.variant)
29476
+ body.variant = lastModel.variant;
29477
+ }
29478
+ const promptInput = { path: { id: sessionID }, body };
29139
29479
  if (typeof typedClient.session?.prompt === "function") {
29140
29480
  await Promise.resolve(typedClient.session.prompt(promptInput));
29141
29481
  return;
@@ -29171,6 +29511,8 @@ var plugin = async (input) => {
29171
29511
  const configOverrides = {};
29172
29512
  if (aftConfig.format_on_edit !== undefined)
29173
29513
  configOverrides.format_on_edit = aftConfig.format_on_edit;
29514
+ if (aftConfig.formatter_timeout_secs !== undefined)
29515
+ configOverrides.formatter_timeout_secs = aftConfig.formatter_timeout_secs;
29174
29516
  if (aftConfig.validate_on_edit !== undefined)
29175
29517
  configOverrides.validate_on_edit = aftConfig.validate_on_edit;
29176
29518
  if (aftConfig.formatter !== undefined)
@@ -29306,6 +29648,15 @@ ${lines}
29306
29648
  pluginVersion: PLUGIN_VERSION,
29307
29649
  projectRoot
29308
29650
  }, validWarnings);
29651
+ },
29652
+ onBashCompletion: (completion, bridge) => {
29653
+ handlePushedBgCompletion({
29654
+ ctx,
29655
+ directory: input.directory,
29656
+ sessionID: completion.session_id,
29657
+ client: input.client,
29658
+ isActive: () => bridge.hasPendingRequests()
29659
+ }, completion);
29309
29660
  }
29310
29661
  }, configOverrides);
29311
29662
  const ctx = {
@@ -29326,7 +29677,7 @@ ${lines}
29326
29677
  });
29327
29678
  rpcServer.handle("status", async (params) => {
29328
29679
  const sessionID = params.sessionID || "rpc";
29329
- const bridge = pool.getAnyActiveBridge(input.directory) ?? pool.getBridge(input.directory);
29680
+ const bridge = pool.getActiveBridgeForRoot(input.directory) ?? pool.getBridge(input.directory);
29330
29681
  return await bridge.send("status", { session_id: sessionID });
29331
29682
  });
29332
29683
  const storageDir = configOverrides.storage_dir;
@@ -29436,8 +29787,34 @@ Install: ${getManualInstallHint()}`).catch(() => {});
29436
29787
  autoUpdate: aftConfig.auto_update ?? true,
29437
29788
  signal: autoUpdateAbort.signal
29438
29789
  });
29790
+ const HINTS_TOOL_NAMES = [
29791
+ "aft_outline",
29792
+ "aft_zoom",
29793
+ "aft_search",
29794
+ "aft_navigate",
29795
+ "grep",
29796
+ "aft_grep",
29797
+ "bash",
29798
+ "aft_bash",
29799
+ "bash_status"
29800
+ ];
29801
+ const registeredTools = new Set(Object.keys(allTools));
29802
+ const hintsAbsentTools = new Set;
29803
+ for (const name of HINTS_TOOL_NAMES) {
29804
+ if (!registeredTools.has(name))
29805
+ hintsAbsentTools.add(name);
29806
+ }
29807
+ const hintsBlock = buildHintsFromConfig(aftConfig, hintsAbsentTools);
29808
+ if (hintsBlock) {
29809
+ log(`Workflow hints injected (${hintsBlock.length} chars)`);
29810
+ }
29439
29811
  return {
29440
29812
  tool: allTools,
29813
+ "experimental.chat.system.transform": async (_input, output) => {
29814
+ if (hintsBlock) {
29815
+ output.system.push(hintsBlock);
29816
+ }
29817
+ },
29441
29818
  event: async (eventInput) => {
29442
29819
  await autoUpdateEventHook(eventInput);
29443
29820
  if (eventInput.event.type !== "session.idle")
@@ -29459,7 +29836,7 @@ Install: ${getManualInstallHint()}`).catch(() => {});
29459
29836
  if (isTuiMode2() || commandInput.command !== STATUS_COMMAND) {
29460
29837
  return;
29461
29838
  }
29462
- const bridge = ctx.pool.getAnyActiveBridge(input.directory) ?? ctx.pool.getBridge(input.directory);
29839
+ const bridge = ctx.pool.getActiveBridgeForRoot(input.directory) ?? ctx.pool.getBridge(input.directory);
29463
29840
  const response = await bridge.send("status", { session_id: commandInput.sessionID });
29464
29841
  if (response.success === false) {
29465
29842
  throw new Error(response.message || "status failed");