@zqagent/core 0.2.4 → 0.2.6

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
@@ -388,8 +388,11 @@ function formatTokens(tokens) {
388
388
  return `${tokens}`;
389
389
  }
390
390
  var KEEP_RECENT_FRACTION = 0.25;
391
- var SPAN_TOOL_RESULT_CLIP = 400;
392
- var SPAN_BUDGET_CHARS = 4e5;
391
+ var SOFT_THRESHOLD_RATIO = 0.75;
392
+ var ELIDE_KEEP_FRACTION = 0.15;
393
+ var SUMMARY_MAX_TOKENS = 3e3;
394
+ var SPAN_TOOL_RESULT_CLIP = 200;
395
+ var SPAN_BUDGET_CHARS = 8e4;
393
396
  var USER_MESSAGE_CLIP = 600;
394
397
  var USER_MESSAGES_MAX = 40;
395
398
  var TRIM_FRACTION = 0.1;
@@ -412,9 +415,22 @@ function triggerTokens(contextWindow, thresholdPct = DEFAULT_THRESHOLD_PCT, capT
412
415
  }
413
416
  return base;
414
417
  }
418
+ function softTriggerTokens(contextWindow, thresholdPct = DEFAULT_THRESHOLD_PCT, capTokens, softThresholdPct) {
419
+ const hard = triggerTokens(contextWindow, thresholdPct, capTokens);
420
+ if (softThresholdPct != null && Number.isFinite(softThresholdPct) && softThresholdPct > 0) {
421
+ return Math.min(
422
+ triggerTokens(contextWindow, softThresholdPct, capTokens),
423
+ Math.floor(hard * 0.95)
424
+ );
425
+ }
426
+ return Math.floor(hard * SOFT_THRESHOLD_RATIO);
427
+ }
415
428
  function shouldCompact(signal, contextWindow, thresholdPct, capTokens) {
416
429
  return signal >= triggerTokens(contextWindow, thresholdPct, capTokens);
417
430
  }
431
+ function elisionKeepChars(hardTriggerTokens, fraction = ELIDE_KEEP_FRACTION) {
432
+ return Math.max(4e3, Math.floor(hardTriggerTokens * fraction * 4));
433
+ }
418
434
  function turnStarts(messages, start) {
419
435
  const users = [];
420
436
  const assistants = [];
@@ -564,10 +580,11 @@ var SUMMARY_SYSTEM_PROMPT = `\u4F60\u6B63\u5728\u538B\u7F29\u4E00\u4E2A AI \u534
564
580
 
565
581
  \u89C4\u5219\uFF1A
566
582
  - \u4E0D\u8981\u5C06\u5B8C\u6574\u7684\u6587\u4EF6\u5185\u5BB9\u5F53\u4F5C\u771F\u7406\u3002\u53EA\u8BB0\u5F55\u67D0\u4E2A\u6587\u4EF6\u88AB\u8BFB\u53D6/\u7F16\u8F91\u8FC7\u8FD9\u4E00\u4E8B\u5B9E\uFF1B\u534F\u4F5C\u8005\u5982\u679C\u9700\u8981\u518D\u6B21\u83B7\u53D6\u5185\u5BB9\u4F1A\u91CD\u65B0\u8BFB\u53D6\u3002\u9648\u65E7\u7684\u6587\u4EF6\u8BB0\u5FC6\u6BD4\u6CA1\u6709\u8BB0\u5FC6\u66F4\u7CDF\u7CD5\u3002
583
+ - \u82E5\u63D0\u4F9B\u4E86\u4E0A\u4E00\u7248\u6458\u8981\uFF1A\u53EA\u628A\u5B83\u4ECD\u7136\u6709\u6548\u7684\u7ED3\u8BBA\u5E76\u5165\u5BF9\u5E94\u7AE0\u8282\uFF0C\u4E0D\u8981\u6574\u6BB5\u590D\u8FF0\u6216\u9010\u6761\u8F6C\u6284\u3002
567
584
  - \u8981\u5177\u4F53\uFF1A\u8DEF\u5F84\u3001\u540D\u79F0\u3001\u547D\u4EE4\u3001id\u2014\u2014\u800C\u4E0D\u662F\u6A21\u7CCA\u7684\u5F15\u7528\u3002
568
585
  - \u4EC5\u8F93\u51FA\u6458\u8981\u7AE0\u8282\uFF0C\u4E0D\u9644\u52A0\u524D\u8A00\u3002`;
569
586
  var CONTINUATION_CONTRACT = "\u4ECE\u4F60\u4E4B\u524D\u4E2D\u65AD\u7684\u5730\u65B9\u7EE7\u7EED\uFF1A\u5B8C\u5168\u6309\u7167\u63CF\u8FF0\u63A5\u7EED\u5F53\u524D\u5DE5\u4F5C\u548C\u4E0B\u4E00\u6B65\u3002\u4E0D\u8981\u91CD\u65B0\u8BE2\u95EE\u5DF2\u56DE\u7B54\u7684\u95EE\u9898\uFF0C\u4E0D\u8981\u56DE\u987E\uFF0C\u4E0D\u8981\u63D0\u53CA\u4E0A\u4E0B\u6587\u5DF2\u88AB\u538B\u7F29\u3002\u5982\u679C\u4F60\u9700\u8981\u4E0A\u8FF0\u63D0\u5230\u7684\u6587\u4EF6\u7684\u5185\u5BB9\uFF0C\u8BF7\u91CD\u65B0\u8BFB\u53D6\u5B83\u3002";
570
- function renderSpan(span, budgetChars = SPAN_BUDGET_CHARS) {
587
+ function renderSpan(span, budgetChars2 = SPAN_BUDGET_CHARS) {
571
588
  const lines = [];
572
589
  for (const m of span) {
573
590
  const role = m.role;
@@ -591,8 +608,8 @@ function renderSpan(span, budgetChars = SPAN_BUDGET_CHARS) {
591
608
  if (text) lines.push(`[${role}] ${text}`);
592
609
  }
593
610
  let rendered = lines.join("\n");
594
- if (rendered.length > budgetChars) {
595
- rendered = "(\u2026oldest turns elided\u2026)\n" + rendered.slice(-budgetChars);
611
+ if (rendered.length > budgetChars2) {
612
+ rendered = "(\u2026oldest turns elided\u2026)\n" + rendered.slice(-budgetChars2);
596
613
  }
597
614
  return rendered;
598
615
  }
@@ -606,9 +623,17 @@ function summarizerMessages(span, priorSummary = "") {
606
623
  { role: "user", content: body }
607
624
  ];
608
625
  }
626
+ function summarizerReasoningEffort() {
627
+ const v = process.env.COMPACTION_REASONING_EFFORT?.toLowerCase();
628
+ if (v === "low" || v === "medium" || v === "high") return v;
629
+ return "low";
630
+ }
609
631
  async function summarizeSpan(provider, model, span, priorSummary = "", opts) {
610
632
  const msgs = summarizerMessages(span, priorSummary);
611
- const chunks = await provider.stream(model, msgs, [], opts);
633
+ const chunks = await provider.stream(model, msgs, [], {
634
+ ...opts,
635
+ maxTokens: SUMMARY_MAX_TOKENS
636
+ });
612
637
  let text = "";
613
638
  for await (const chunk of chunks) {
614
639
  if (chunk.content) text += chunk.content;
@@ -719,6 +744,166 @@ function applyToOutbound(messages, state) {
719
744
  head.push({ role: "user", content: compactedBlock(state) });
720
745
  return [...head, ...messages.slice(boundary)];
721
746
  }
747
+ var ELIDE_EXEMPT_TOOLS = /* @__PURE__ */ new Set([
748
+ "explore",
749
+ "ask_user",
750
+ "propose_plan",
751
+ "request_directory",
752
+ "todo_write"
753
+ ]);
754
+ function elidedToolContent(m) {
755
+ const name = m.tool_call_name || "tool";
756
+ const size = m.content.length;
757
+ const saved = /full output saved to ([^\]]+)/.exec(m.content)?.[1]?.trim();
758
+ return saved ? `[old tool result elided: ${name}, ${size} chars; full output saved to ${saved}]` : `[old tool result elided: ${name}, ${size} chars; re-run the tool if its content is still needed]`;
759
+ }
760
+ function isPlaceholder(content) {
761
+ return content.startsWith("[old tool result elided") || content.startsWith("[superseded by a later read_file");
762
+ }
763
+ var ONE_SHOT_TOOLS = /* @__PURE__ */ new Set(["git_diff"]);
764
+ var ONE_SHOT_KEEP_RECENT = 1;
765
+ function applyOneShotElision(messages, keepRecent = ONE_SHOT_KEEP_RECENT, tools = ONE_SHOT_TOOLS) {
766
+ const out = [...messages];
767
+ let seen = 0;
768
+ let elided = 0;
769
+ let freed = 0;
770
+ for (let i = messages.length - 1; i >= 0; i--) {
771
+ const m = messages[i];
772
+ if (m.role !== "tool" || !tools.has(m.tool_call_name ?? "")) continue;
773
+ if (isPlaceholder(m.content)) continue;
774
+ seen++;
775
+ if (seen <= keepRecent) continue;
776
+ const placeholder = elidedToolContent(m);
777
+ freed += m.content.length - placeholder.length;
778
+ out[i] = { ...m, content: placeholder };
779
+ elided++;
780
+ }
781
+ return elided > 0 ? { messages: out, elided, freedChars: freed } : { messages, elided: 0, freedChars: 0 };
782
+ }
783
+ function applyToolResultElision(messages, keepChars, exempt = ELIDE_EXEMPT_TOOLS) {
784
+ let total = 0;
785
+ for (const m of messages) {
786
+ if (m.role === "tool" && !exempt.has(m.tool_call_name ?? "")) {
787
+ total += m.content.length;
788
+ }
789
+ }
790
+ if (total <= keepChars) return { messages, elided: 0, freedChars: 0 };
791
+ const out = [...messages];
792
+ let budget = 0;
793
+ let elided = 0;
794
+ let freed = 0;
795
+ for (let i = messages.length - 1; i >= 0; i--) {
796
+ const m = messages[i];
797
+ if (m.role !== "tool") continue;
798
+ if (exempt.has(m.tool_call_name ?? "")) continue;
799
+ if (elided === 0 && budget === 0 && m.content.length > keepChars) {
800
+ const kept = m.content.slice(0, keepChars) + "\n\u2026(truncated)";
801
+ freed += m.content.length - kept.length;
802
+ out[i] = { ...m, content: kept };
803
+ budget = keepChars;
804
+ continue;
805
+ }
806
+ if (budget + m.content.length <= keepChars) {
807
+ budget += m.content.length;
808
+ continue;
809
+ }
810
+ const placeholder = elidedToolContent(m);
811
+ freed += m.content.length - placeholder.length;
812
+ out[i] = { ...m, content: placeholder };
813
+ elided++;
814
+ }
815
+ return { messages: out, elided, freedChars: freed };
816
+ }
817
+ function normalizeToolPath(p) {
818
+ let s = p.trim().replace(/\\/g, "/");
819
+ while (s.startsWith("./")) s = s.slice(2);
820
+ return s.replace(/\/{2,}/g, "/");
821
+ }
822
+ var MUTATING_TOOLS = /* @__PURE__ */ new Set([
823
+ "write_file",
824
+ "replace_in_file",
825
+ "apply_patch",
826
+ "apply_unified_diff",
827
+ "run_shell"
828
+ ]);
829
+ function parseReadFileSpan(content) {
830
+ const body = content.replace(/^\[tool output truncated:[^\n]*\]\n\n/, "");
831
+ let path16;
832
+ let start;
833
+ let end;
834
+ try {
835
+ const obj = JSON.parse(body);
836
+ path16 = obj.path;
837
+ start = obj.start_line;
838
+ end = obj.end_line;
839
+ } catch {
840
+ const str2 = (key) => new RegExp(`"${key}"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)"`).exec(body)?.[1];
841
+ const num = (key) => {
842
+ const m = new RegExp(`"${key}"\\s*:\\s*(\\d+)`).exec(body);
843
+ return m ? Number(m[1]) : void 0;
844
+ };
845
+ path16 = str2("path");
846
+ start = num("start_line");
847
+ end = num("end_line");
848
+ }
849
+ if (typeof path16 !== "string" || !path16) return null;
850
+ if (typeof start !== "number" || typeof end !== "number") return null;
851
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
852
+ return { path: path16, start, end };
853
+ }
854
+ function addSpan(spans, start, end) {
855
+ spans.push([start, end]);
856
+ spans.sort((a, b) => a[0] - b[0]);
857
+ const merged = [];
858
+ for (const cur of spans) {
859
+ const last = merged[merged.length - 1];
860
+ if (last && cur[0] <= last[1] + 1) {
861
+ last[1] = Math.max(last[1], cur[1]);
862
+ } else {
863
+ merged.push([...cur]);
864
+ }
865
+ }
866
+ spans.length = 0;
867
+ spans.push(...merged);
868
+ }
869
+ function coversSpans(spans, start, end) {
870
+ return spans.some(([a, b]) => a <= start && b >= end);
871
+ }
872
+ function applyReadFileDedup(messages) {
873
+ let candidates = 0;
874
+ for (const m of messages) {
875
+ if (m.role === "tool" && m.tool_call_name === "read_file") candidates++;
876
+ }
877
+ if (candidates < 2) return { messages, elided: 0 };
878
+ const out = [...messages];
879
+ const covered = /* @__PURE__ */ new Map();
880
+ let elided = 0;
881
+ for (let i = messages.length - 1; i >= 0; i--) {
882
+ const m = messages[i];
883
+ if (m.role === "assistant") {
884
+ if (m.tool_calls?.some((tc) => MUTATING_TOOLS.has(tc.function.name))) {
885
+ covered.clear();
886
+ }
887
+ continue;
888
+ }
889
+ if (m.role !== "tool" || m.tool_call_name !== "read_file") continue;
890
+ const span = parseReadFileSpan(m.content);
891
+ if (!span) continue;
892
+ const key = normalizeToolPath(span.path);
893
+ const seen = covered.get(key) ?? [];
894
+ if (coversSpans(seen, span.start, span.end)) {
895
+ out[i] = {
896
+ ...m,
897
+ content: `[superseded by a later read_file of ${span.path} (lines ${span.start}-${span.end})]`
898
+ };
899
+ elided++;
900
+ continue;
901
+ }
902
+ addSpan(seen, span.start, span.end);
903
+ covered.set(key, seen);
904
+ }
905
+ return { messages: elided > 0 ? out : messages, elided };
906
+ }
722
907
  var OVERFLOW_MARKERS = [
723
908
  "context_length_exceeded",
724
909
  "maximum context length",
@@ -734,9 +919,108 @@ function isContextOverflow(exc) {
734
919
  return OVERFLOW_MARKERS.some((m) => text.includes(m));
735
920
  }
736
921
 
922
+ // src/engine/toolResultBudget.ts
923
+ import * as fs from "fs";
924
+ import * as path2 from "path";
925
+
926
+ // src/paths.ts
927
+ import * as os from "os";
928
+ import * as path from "path";
929
+ var APP_DIR_NAME = "zqagent";
930
+ var IS_WINDOWS = process.platform === "win32";
931
+ function homeDir() {
932
+ return process.env.HOME || process.env.USERPROFILE || os.homedir() || os.tmpdir();
933
+ }
934
+ function stateDir() {
935
+ const base = process.env.XDG_STATE_HOME || (IS_WINDOWS ? process.env.LOCALAPPDATA : void 0) || path.join(homeDir(), ".local", "state");
936
+ return path.join(base, APP_DIR_NAME);
937
+ }
938
+
939
+ // src/engine/toolResultBudget.ts
940
+ var DEFAULT_TOOL_RESULT_MAX_CHARS = 24e3;
941
+ var HEAD_RATIO = 0.6;
942
+ function budgetChars() {
943
+ const n = Number(process.env.TOOL_RESULT_MAX_CHARS);
944
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : DEFAULT_TOOL_RESULT_MAX_CHARS;
945
+ }
946
+ function toolResultDir() {
947
+ const env = process.env.TOOL_RESULT_DIR?.trim();
948
+ return env || path2.join(stateDir(), "tool-results");
949
+ }
950
+ function clipText(text, budget) {
951
+ if (text.length <= budget) return text;
952
+ const room = Math.max(
953
+ 0,
954
+ budget - `
955
+
956
+ \u2026 [${text.length} characters omitted] \u2026
957
+
958
+ `.length
959
+ );
960
+ const headLen = Math.floor(room * HEAD_RATIO);
961
+ const tailLen = room - headLen;
962
+ const omitted = text.length - headLen - tailLen;
963
+ return text.slice(0, headLen) + `
964
+
965
+ \u2026 [${omitted} characters omitted] \u2026
966
+
967
+ ` + text.slice(-tailLen);
968
+ }
969
+ function clipDeep(value, perField) {
970
+ if (typeof value === "string") {
971
+ return value.length <= perField ? value : clipText(value, perField);
972
+ }
973
+ if (Array.isArray(value)) {
974
+ return value.map((v) => clipDeep(v, perField));
975
+ }
976
+ if (value && typeof value === "object") {
977
+ const out = {};
978
+ for (const [k, v] of Object.entries(value)) out[k] = clipDeep(v, perField);
979
+ return out;
980
+ }
981
+ return value;
982
+ }
983
+ function saveRaw(tool, raw, dir) {
984
+ try {
985
+ fs.mkdirSync(dir, { recursive: true });
986
+ const safe = tool.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 40) || "tool";
987
+ const name = `${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}-${safe}.txt`;
988
+ const file = path2.join(dir, name);
989
+ fs.writeFileSync(file, raw, "utf-8");
990
+ return file;
991
+ } catch {
992
+ return void 0;
993
+ }
994
+ }
995
+ function applyToolResultBudget(tool, result, opts = {}) {
996
+ const budget = opts.maxChars ?? budgetChars();
997
+ const raw = typeof result === "string" ? result : JSON.stringify(result, null, 2) ?? String(result);
998
+ if (raw.length <= budget) {
999
+ return { content: raw, truncated: false };
1000
+ }
1001
+ let content = raw;
1002
+ if (typeof result === "object" && result !== null) {
1003
+ let perField = Math.floor(budget * 0.7);
1004
+ let clipped = JSON.stringify(clipDeep(result, perField), null, 2);
1005
+ for (let i = 0; i < 6 && clipped && clipped.length > budget && perField > 128; i++) {
1006
+ perField = Math.floor(perField / 2);
1007
+ clipped = JSON.stringify(clipDeep(result, perField), null, 2);
1008
+ }
1009
+ if (clipped && clipped.length <= budget) content = clipped;
1010
+ }
1011
+ if (content.length > budget) content = clipText(content, budget);
1012
+ const savedPath = saveRaw(tool, raw, opts.dir ?? toolResultDir());
1013
+ if (savedPath) {
1014
+ content = `[tool output truncated: ${raw.length} chars, full output saved to ${savedPath}]
1015
+
1016
+ ` + content;
1017
+ }
1018
+ return { content, truncated: true, savedPath };
1019
+ }
1020
+
737
1021
  // src/engine/TurnEngine.ts
738
1022
  import * as crypto from "crypto";
739
- import * as fs from "fs";
1023
+ import * as fs2 from "fs";
740
1024
  import * as _path from "path";
741
1025
 
742
1026
  // src/prompts/index.ts
@@ -863,13 +1147,17 @@ var INTERACTIVE_TOOLS = /* @__PURE__ */ new Set([
863
1147
  "propose_plan",
864
1148
  "request_directory"
865
1149
  ]);
866
- function toolResultMessage(tc, result, taskId) {
867
- const content = typeof result === "string" ? result : JSON.stringify(result, null, 2);
1150
+ var MAX_COMPACT_ROUNDS = 4;
1151
+ var OVERFLOW_TARGET_RATIO = 0.5;
1152
+ var OVERFLOW_TRIM_FRACTION = 0.5;
1153
+ var COMPACT_STALL_RETRY_DELTA = 20;
1154
+ function toolResultMessage(tc, result, taskId, toolResultDir2) {
1155
+ const budgeted = applyToolResultBudget(tc.name, result, { dir: toolResultDir2 });
868
1156
  return {
869
1157
  role: "tool",
870
1158
  tool_call_id: tc.id,
871
1159
  tool_call_name: tc.name,
872
- content,
1160
+ content: budgeted.content,
873
1161
  taskId
874
1162
  };
875
1163
  }
@@ -980,12 +1268,22 @@ var TurnEngine = class _TurnEngine {
980
1268
  // 上下文压缩
981
1269
  compactionState = null;
982
1270
  _lastContextTokens = null;
1271
+ /** 压缩无进展时的消息条数快照;-1 表示无冷却 */
1272
+ _compactStalledAt = -1;
983
1273
  /** 显式配置的上下文窗口大小(token 数),未设置时为 undefined(走自动检测) */
984
1274
  _contextWindow;
985
1275
  /** 压缩触发阈值比例(默认 0.8) */
986
1276
  _thresholdPct;
1277
+ /** 软阈值比例(undefined = 取硬阈值的 75%) */
1278
+ _softThresholdPct;
1279
+ /** 是否启用软阈值淘汰(COMPACTION_ELIDE=0 可关闭) */
1280
+ _elisionEnabled;
1281
+ /** 是否启用重复读取去重(COMPACTION_DEDUP=0 可关闭) */
1282
+ _dedupEnabled;
987
1283
  /** 压缩触发阈值绝对上限(null = 不限制) */
988
1284
  _capTokens;
1285
+ /** 超预算工具结果的落盘目录(undefined = 使用默认状态目录) */
1286
+ _toolResultDir;
989
1287
  // 产出文件追踪(本轮 Turn 中成功写入的文件)
990
1288
  _producedFiles = [];
991
1289
  _turnStartTime = 0;
@@ -1037,7 +1335,15 @@ var TurnEngine = class _TurnEngine {
1037
1335
  this._hook = opts.hook;
1038
1336
  this._contextWindow = opts.contextWindow;
1039
1337
  this._thresholdPct = opts.thresholdPct ?? DEFAULT_THRESHOLD_PCT;
1338
+ this._softThresholdPct = opts.softThresholdPct;
1339
+ this._elisionEnabled = !/^(0|false|off|no)$/i.test(
1340
+ (process.env.COMPACTION_ELIDE ?? "").trim()
1341
+ );
1342
+ this._dedupEnabled = !/^(0|false|off|no)$/i.test(
1343
+ (process.env.COMPACTION_DEDUP ?? "").trim()
1344
+ );
1040
1345
  this._capTokens = opts.capTokens ?? null;
1346
+ this._toolResultDir = opts.toolResultDir;
1041
1347
  this._reasoningEffort = opts.reasoningEffort;
1042
1348
  if (this.messages.length === 0 || this.messages[0].role !== "system") {
1043
1349
  this.messages.unshift({ role: "system", content: this.instructions });
@@ -1093,6 +1399,7 @@ var TurnEngine = class _TurnEngine {
1093
1399
  this._cancel = false;
1094
1400
  this._producedFiles = [];
1095
1401
  this._turnStartTime = Date.now();
1402
+ this._compactStalledAt = -1;
1096
1403
  this._emitHook((h) => h.onTurnStart?.(userInput));
1097
1404
  yield {
1098
1405
  type: "turn_start" /* TURN_START */,
@@ -1809,7 +2116,9 @@ var TurnEngine = class _TurnEngine {
1809
2116
  for (let i = 0; i < concurrent.length; i++) {
1810
2117
  const tc = concurrent[i];
1811
2118
  const { result, status } = results[i];
1812
- this.messages.push(toolResultMessage(tc, result, this.taskId));
2119
+ this.messages.push(
2120
+ toolResultMessage(tc, result, this.taskId, this._toolResultDir)
2121
+ );
1813
2122
  this._trackProducedFile(
1814
2123
  tc,
1815
2124
  result,
@@ -1872,7 +2181,9 @@ var TurnEngine = class _TurnEngine {
1872
2181
  };
1873
2182
  status = "error";
1874
2183
  }
1875
- this.messages.push(toolResultMessage(tc, result, this.taskId));
2184
+ this.messages.push(
2185
+ toolResultMessage(tc, result, this.taskId, this._toolResultDir)
2186
+ );
1876
2187
  this._trackProducedFile(
1877
2188
  tc,
1878
2189
  result,
@@ -1907,13 +2218,41 @@ var TurnEngine = class _TurnEngine {
1907
2218
  * 过滤 notice 消息;tool 消息只保留必要字段;
1908
2219
  * 其余消息剥离 display-only sidecar 字段(source/_display/ts/
1909
2220
  * reasoning/usage),避免污染模型上下文。
2221
+ *
2222
+ * 达到软阈值时先淘汰旧工具结果(零 LLM 成本),再产出最终视图。
1910
2223
  */
1911
2224
  _outboundMessages() {
1912
- const _STRIP = /* @__PURE__ */ new Set(["source", "_display", "ts", "reasoning", "usage"]);
1913
- const sourceMessages = applyToOutbound(
1914
- this.messages,
1915
- this.compactionState
2225
+ const base = applyToOutbound(this.messages, this.compactionState);
2226
+ const deduped = this._dedupEnabled ? applyReadFileDedup(base).messages : base;
2227
+ let view = this._finalizeOutbound(deduped);
2228
+ if (!this._elisionEnabled) return view;
2229
+ if (estimateTokens(view) < this._softTriggerTokens()) {
2230
+ return view;
2231
+ }
2232
+ const oneShot = applyOneShotElision(deduped);
2233
+ if (oneShot.freedChars > 0) {
2234
+ view = this._finalizeOutbound(oneShot.messages);
2235
+ if (estimateTokens(view) < this._softTriggerTokens()) {
2236
+ return view;
2237
+ }
2238
+ }
2239
+ const keepChars = elisionKeepChars(
2240
+ triggerTokens(
2241
+ this._getContextWindow(),
2242
+ this._thresholdPct,
2243
+ this._capTokens
2244
+ )
2245
+ );
2246
+ const elided = applyToolResultElision(
2247
+ oneShot.messages,
2248
+ keepChars
1916
2249
  );
2250
+ if (elided.freedChars === 0) return view;
2251
+ return this._finalizeOutbound(elided.messages);
2252
+ }
2253
+ /** 过滤 notice + 剥离 display-only 字段,得到最终发送视图 */
2254
+ _finalizeOutbound(sourceMessages) {
2255
+ const _STRIP = /* @__PURE__ */ new Set(["source", "_display", "ts", "reasoning", "usage"]);
1917
2256
  return sourceMessages.filter((m) => m.role !== "notice").map((m) => {
1918
2257
  if (m.role === "tool") {
1919
2258
  return {
@@ -1931,6 +2270,15 @@ var TurnEngine = class _TurnEngine {
1931
2270
  return stripped;
1932
2271
  });
1933
2272
  }
2273
+ /** 软阈值(token 数):达到它只淘汰旧工具结果 */
2274
+ _softTriggerTokens() {
2275
+ return softTriggerTokens(
2276
+ this._getContextWindow(),
2277
+ this._thresholdPct,
2278
+ this._capTokens,
2279
+ this._softThresholdPct
2280
+ );
2281
+ }
1934
2282
  /** 通知消息:仅用于日志/展示,不发给模型 */
1935
2283
  _appendNotice(kind, text) {
1936
2284
  this.messages.push({
@@ -1972,18 +2320,36 @@ var TurnEngine = class _TurnEngine {
1972
2320
  return false;
1973
2321
  }
1974
2322
  // ===================== 自动压缩 =====================
1975
- /** 当前上下文 token 用量:优先采信 API 返回的精确数量,否则用 chars/4 估算 */
2323
+ /**
2324
+ * 当前上下文 token 用量。
2325
+ *
2326
+ * API 返回的 `usage.input` 是精确值,但反映的是「上一次请求」——工具结果
2327
+ * 与淘汰/压缩都会让它过时。因此同时估算当前 outbound 视图,取较大者:
2328
+ * 宁可早一点压缩,也不要漏判导致溢出。
2329
+ */
1976
2330
  _compactionTokens() {
1977
- if (this._lastContextTokens != null) {
1978
- return { tokens: this._lastContextTokens, estimated: false };
2331
+ const estimated = estimateTokens(this._outboundMessages());
2332
+ if (this._lastContextTokens == null) {
2333
+ return { tokens: estimated, estimated: true };
1979
2334
  }
1980
- return {
1981
- tokens: estimateTokens(this._outboundMessages()),
1982
- estimated: true
1983
- };
2335
+ if (estimated > this._lastContextTokens) {
2336
+ return { tokens: estimated, estimated: true };
2337
+ }
2338
+ return { tokens: this._lastContextTokens, estimated: false };
2339
+ }
2340
+ /** 压缩后重置精确计数:后续按估算值判断 */
2341
+ _resetContextTokens() {
2342
+ this._lastContextTokens = null;
2343
+ }
2344
+ /** 当前 outbound 视图的估算 token 数 */
2345
+ _estimateContextTokens() {
2346
+ return estimateTokens(this._outboundMessages());
1984
2347
  }
1985
2348
  /** 检查是否需要压缩(每轮 LLM 调用前) */
1986
2349
  _compactionDue() {
2350
+ if (this._compactStalledAt >= 0 && this.messages.length < this._compactStalledAt + COMPACT_STALL_RETRY_DELTA) {
2351
+ return false;
2352
+ }
1987
2353
  const { tokens } = this._compactionTokens();
1988
2354
  return shouldCompact(
1989
2355
  tokens,
@@ -2030,63 +2396,82 @@ var TurnEngine = class _TurnEngine {
2030
2396
  async _compactNow(reqId, force = false) {
2031
2397
  const contextWindow = this._getContextWindow();
2032
2398
  const messagesBefore = this._outboundMessages().length;
2033
- const keep = Math.floor(
2034
- KEEP_RECENT_FRACTION * triggerTokens(
2035
- contextWindow,
2036
- this._thresholdPct,
2037
- this._capTokens
2038
- )
2399
+ const trigger = triggerTokens(
2400
+ contextWindow,
2401
+ this._thresholdPct,
2402
+ this._capTokens
2039
2403
  );
2040
- const model = this.model;
2041
- let state = null;
2042
- let failed = false;
2043
- for (let attempt = 0; attempt < 2; attempt++) {
2044
- try {
2045
- const logTime = (/* @__PURE__ */ new Date()).toISOString();
2046
- state = await buildState(
2404
+ const target = Math.floor(
2405
+ trigger * (force ? OVERFLOW_TARGET_RATIO : 0.9)
2406
+ );
2407
+ const model = process.env.COMPACTION_MODEL?.trim() || this.model;
2408
+ const reasoningEffort = summarizerReasoningEffort();
2409
+ let keep = Math.floor(KEEP_RECENT_FRACTION * trigger);
2410
+ let method = null;
2411
+ let lastState = null;
2412
+ let progressed = false;
2413
+ for (let round = 0; round < MAX_COMPACT_ROUNDS; round++) {
2414
+ if (round > 0 && this._estimateContextTokens() <= target) break;
2415
+ let state = null;
2416
+ for (let attempt = 0; attempt < 2 && !state; attempt++) {
2417
+ try {
2418
+ const logTime = (/* @__PURE__ */ new Date()).toISOString();
2419
+ state = await buildState(
2420
+ this.messages,
2421
+ this.provider,
2422
+ model,
2423
+ keep,
2424
+ this.compactionState,
2425
+ { reqId, logTime, reasoningEffort }
2426
+ );
2427
+ } catch {
2428
+ }
2429
+ }
2430
+ if (state) {
2431
+ this.compactionState = state;
2432
+ lastState = state;
2433
+ method = "summarized";
2434
+ progressed = true;
2435
+ } else {
2436
+ const trimmed = trimState(
2047
2437
  this.messages,
2048
- this.provider,
2049
- model,
2050
- keep,
2051
2438
  this.compactionState,
2052
- { reqId, logTime, reasoningEffort: this._reasoningEffort }
2439
+ force ? OVERFLOW_TRIM_FRACTION : 0.25
2053
2440
  );
2054
- failed = false;
2055
- break;
2056
- } catch {
2057
- failed = true;
2058
- }
2059
- }
2060
- if (state) {
2061
- this.compactionState = state;
2062
- this._lastContextTokens = null;
2441
+ if (!trimmed) break;
2442
+ this.compactionState = trimmed;
2443
+ lastState = trimmed;
2444
+ method = "trimmed";
2445
+ progressed = true;
2446
+ }
2447
+ this._resetContextTokens();
2448
+ if (this._estimateContextTokens() <= trigger) break;
2449
+ keep = Math.max(Math.floor(trigger * 0.05), Math.floor(keep / 2));
2450
+ }
2451
+ if (!progressed || !lastState) return { notice: null, method: null };
2452
+ this._compactStalledAt = this._estimateContextTokens() >= trigger ? this.messages.length : -1;
2453
+ const messagesAfter = this._outboundMessages().length;
2454
+ const common = {
2455
+ boundaryIndex: lastState.boundaryIndex,
2456
+ messagesBefore,
2457
+ messagesAfter
2458
+ };
2459
+ if (method === "summarized") {
2063
2460
  return {
2064
2461
  notice: "\u4E0A\u4E0B\u6587\u5DF2\u538B\u7F29 \u2014 \u65E9\u671F\u8F6E\u6B21\u5DF2\u6458\u8981",
2065
2462
  method: "summarized",
2066
- modelUsed: state.modelUsed,
2067
- boundaryIndex: state.boundaryIndex,
2068
- summaryPreview: state.summaryText.slice(0, 200),
2069
- keptUserMessages: state.userMessages.length,
2070
- droppedUserMessages: state.userMessagesDropped,
2071
- messagesBefore,
2072
- messagesAfter: this._outboundMessages().length
2463
+ modelUsed: lastState.modelUsed,
2464
+ summaryPreview: lastState.summaryText.slice(0, 200),
2465
+ keptUserMessages: lastState.userMessages.length,
2466
+ droppedUserMessages: lastState.userMessagesDropped,
2467
+ ...common
2073
2468
  };
2074
2469
  }
2075
- if (failed || force) {
2076
- const trimmed = trimState(this.messages, this.compactionState);
2077
- if (trimmed) {
2078
- this.compactionState = trimmed;
2079
- this._lastContextTokens = null;
2080
- return {
2081
- notice: "\u4E0A\u4E0B\u6587\u5DF2\u88C1\u526A \u2014 \u6700\u65E7\u7684\u8F6E\u6B21\u5DF2\u4E22\u5F03\uFF08\u6458\u8981\u4E0D\u53EF\u7528\uFF09",
2082
- method: "trimmed",
2083
- boundaryIndex: trimmed.boundaryIndex,
2084
- messagesBefore,
2085
- messagesAfter: this._outboundMessages().length
2086
- };
2087
- }
2088
- }
2089
- return { notice: null, method: null };
2470
+ return {
2471
+ notice: "\u4E0A\u4E0B\u6587\u5DF2\u88C1\u526A \u2014 \u6700\u65E7\u7684\u8F6E\u6B21\u5DF2\u4E22\u5F03\uFF08\u6458\u8981\u4E0D\u53EF\u7528\uFF09",
2472
+ method: "trimmed",
2473
+ ...common
2474
+ };
2090
2475
  }
2091
2476
  /** 追踪文件写入工具成功产生的文件 */
2092
2477
  _trackProducedFile(tc, result, status) {
@@ -2096,9 +2481,9 @@ var TurnEngine = class _TurnEngine {
2096
2481
  return;
2097
2482
  }
2098
2483
  if (result && typeof result === "object") {
2099
- const path14 = result.path;
2100
- if (path14) {
2101
- this._producedFiles.push(path14);
2484
+ const path16 = result.path;
2485
+ if (path16) {
2486
+ this._producedFiles.push(path16);
2102
2487
  return;
2103
2488
  }
2104
2489
  const changed = result.changed_files;
@@ -2122,7 +2507,7 @@ var TurnEngine = class _TurnEngine {
2122
2507
  _fileExists(f, ws) {
2123
2508
  if (!ws && !_path.isAbsolute(f)) return true;
2124
2509
  try {
2125
- return fs.existsSync(_path.isAbsolute(f) ? f : _path.join(ws, f));
2510
+ return fs2.existsSync(_path.isAbsolute(f) ? f : _path.join(ws, f));
2126
2511
  } catch {
2127
2512
  return false;
2128
2513
  }
@@ -2145,7 +2530,7 @@ var TurnEngine = class _TurnEngine {
2145
2530
  if (depth > 4 || results.length >= 50) return;
2146
2531
  let entries;
2147
2532
  try {
2148
- entries = fs.readdirSync(dir, { withFileTypes: true });
2533
+ entries = fs2.readdirSync(dir, { withFileTypes: true });
2149
2534
  } catch {
2150
2535
  return;
2151
2536
  }
@@ -2160,7 +2545,7 @@ var TurnEngine = class _TurnEngine {
2160
2545
  const ext = dotIdx !== -1 ? e.name.slice(dotIdx).toLowerCase() : "";
2161
2546
  if (!_TurnEngine.SCAN_SUFFIXES.has(ext)) continue;
2162
2547
  try {
2163
- const st = fs.statSync(full);
2548
+ const st = fs2.statSync(full);
2164
2549
  if (st.mtimeMs >= since) {
2165
2550
  results.push(
2166
2551
  _path.relative(_path.resolve(ws), _path.resolve(full))
@@ -2181,8 +2566,8 @@ var TurnEngine = class _TurnEngine {
2181
2566
  };
2182
2567
 
2183
2568
  // src/providers/OpenAIProvider.ts
2184
- import * as fs2 from "fs";
2185
- import * as path from "path";
2569
+ import * as fs3 from "fs";
2570
+ import * as path3 from "path";
2186
2571
  import OpenAI from "openai";
2187
2572
 
2188
2573
  // src/terminal.ts
@@ -2360,7 +2745,7 @@ var OpenAIProvider = class {
2360
2745
  });
2361
2746
  if (opts.logDir) {
2362
2747
  this._logDir = opts.logDir;
2363
- fs2.mkdirSync(opts.logDir, { recursive: true });
2748
+ fs3.mkdirSync(opts.logDir, { recursive: true });
2364
2749
  }
2365
2750
  }
2366
2751
  // =====================================================================
@@ -2513,6 +2898,8 @@ var OpenAIProvider = class {
2513
2898
  tools: this._toResponsesTools(tools, model),
2514
2899
  // 推理强度:DeepSeek 不支持,跳过
2515
2900
  ...opts.reasoningEffort && !this._isDeepSeek(model) ? { reasoning: { effort: opts.reasoningEffort } } : {},
2901
+ // 限制输出长度(摘要器等轻量请求)
2902
+ ...opts.maxTokens ? { max_output_tokens: opts.maxTokens } : {},
2516
2903
  stream: true
2517
2904
  }, requestOptions);
2518
2905
  return this._responsesStream(reqId, startTime, rawStream, {
@@ -2611,6 +2998,8 @@ var OpenAIProvider = class {
2611
2998
  })),
2612
2999
  // 推理强度:DeepSeek 不支持,跳过
2613
3000
  ...opts.reasoningEffort && !this._isDeepSeek(model) ? { reasoning_effort: opts.reasoningEffort } : {},
3001
+ // 限制输出长度:o 系列 / gpt-5 推理模型仅支持 max_completion_tokens
3002
+ ...opts.maxTokens ? isReasoningModel(model) ? { max_completion_tokens: opts.maxTokens } : { max_tokens: opts.maxTokens } : {},
2614
3003
  stream: true,
2615
3004
  // 要求返回 Token 用量统计(在流结束前的一个 chunk 中携带)
2616
3005
  stream_options: { include_usage: true }
@@ -2724,8 +3113,8 @@ var OpenAIProvider = class {
2724
3113
  if (!this._logDir) return;
2725
3114
  try {
2726
3115
  const today = logTime?.split("T")[0];
2727
- const dir = path.join(this._logDir, today);
2728
- fs2.mkdirSync(dir, { recursive: true });
3116
+ const dir = path3.join(this._logDir, today);
3117
+ fs3.mkdirSync(dir, { recursive: true });
2729
3118
  const entry = {
2730
3119
  reqId,
2731
3120
  ts: logTime,
@@ -2737,8 +3126,8 @@ var OpenAIProvider = class {
2737
3126
  toolNames: tools?.map((t) => t.function.name) ?? []
2738
3127
  }
2739
3128
  };
2740
- fs2.writeFileSync(
2741
- path.join(dir, `${reqId}.json`),
3129
+ fs3.writeFileSync(
3130
+ path3.join(dir, `${reqId}.json`),
2742
3131
  JSON.stringify(entry, null, 2),
2743
3132
  "utf-8"
2744
3133
  );
@@ -2757,9 +3146,9 @@ var OpenAIProvider = class {
2757
3146
  if (!this._logDir) return;
2758
3147
  try {
2759
3148
  const today = logTime?.split("T")[0];
2760
- const filePath = path.join(this._logDir, today, `${reqId}.json`);
2761
- if (!fs2.existsSync(filePath)) return;
2762
- const entry = JSON.parse(fs2.readFileSync(filePath, "utf-8"));
3149
+ const filePath = path3.join(this._logDir, today, `${reqId}.json`);
3150
+ if (!fs3.existsSync(filePath)) return;
3151
+ const entry = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
2763
3152
  entry.elapsedMs = Date.now() - startTime;
2764
3153
  if (data.response) {
2765
3154
  entry.response = data.response;
@@ -2767,7 +3156,7 @@ var OpenAIProvider = class {
2767
3156
  if (data.error) {
2768
3157
  entry.error = data.error;
2769
3158
  }
2770
- fs2.writeFileSync(filePath, JSON.stringify(entry, null, 2), "utf-8");
3159
+ fs3.writeFileSync(filePath, JSON.stringify(entry, null, 2), "utf-8");
2771
3160
  } catch (e) {
2772
3161
  console.error(e);
2773
3162
  }
@@ -3232,62 +3621,62 @@ var ToolRegistry = class {
3232
3621
  };
3233
3622
 
3234
3623
  // src/tools/pathSafety.ts
3235
- import * as fs3 from "fs";
3236
- import * as path2 from "path";
3624
+ import * as fs4 from "fs";
3625
+ import * as path4 from "path";
3237
3626
  function isInsideRelative(rel) {
3238
- return !path2.isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${path2.sep}`);
3627
+ return !path4.isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${path4.sep}`);
3239
3628
  }
3240
3629
  function isFilesystemRoot(p) {
3241
- return path2.parse(p).root === p;
3630
+ return path4.parse(p).root === p;
3242
3631
  }
3243
3632
  function resolveWithinRoot(workspace, target) {
3244
- const abs = path2.resolve(workspace, target);
3633
+ const abs = path4.resolve(workspace, target);
3245
3634
  let rootReal;
3246
3635
  try {
3247
- rootReal = fs3.realpathSync(workspace);
3636
+ rootReal = fs4.realpathSync(workspace);
3248
3637
  } catch {
3249
- const rootAbs = path2.resolve(workspace);
3250
- if (!isInsideRelative(path2.relative(rootAbs, abs))) return null;
3638
+ const rootAbs = path4.resolve(workspace);
3639
+ if (!isInsideRelative(path4.relative(rootAbs, abs))) return null;
3251
3640
  return { root: rootAbs, real: abs };
3252
3641
  }
3253
3642
  if (isFilesystemRoot(rootReal)) return null;
3254
3643
  let real;
3255
3644
  try {
3256
- real = fs3.realpathSync(abs);
3645
+ real = fs4.realpathSync(abs);
3257
3646
  } catch {
3258
3647
  let probe = abs;
3259
3648
  const tail = [];
3260
3649
  for (; ; ) {
3261
- const exists = fs3.existsSync(probe) || fs3.lstatSync(probe, { throwIfNoEntry: false }) !== void 0;
3650
+ const exists = fs4.existsSync(probe) || fs4.lstatSync(probe, { throwIfNoEntry: false }) !== void 0;
3262
3651
  if (exists) {
3263
3652
  try {
3264
- real = path2.join(fs3.realpathSync(probe), ...tail);
3653
+ real = path4.join(fs4.realpathSync(probe), ...tail);
3265
3654
  break;
3266
3655
  } catch {
3267
3656
  return null;
3268
3657
  }
3269
3658
  }
3270
- tail.unshift(path2.basename(probe));
3271
- const parent = path2.dirname(probe);
3659
+ tail.unshift(path4.basename(probe));
3660
+ const parent = path4.dirname(probe);
3272
3661
  if (parent === probe) return null;
3273
3662
  probe = parent;
3274
3663
  }
3275
3664
  }
3276
- if (!isInsideRelative(path2.relative(rootReal, real))) return null;
3665
+ if (!isInsideRelative(path4.relative(rootReal, real))) return null;
3277
3666
  return { root: rootReal, real };
3278
3667
  }
3279
3668
  function isWithinRoot(workspace, target) {
3280
3669
  return resolveWithinRoot(workspace, target) !== null;
3281
3670
  }
3282
3671
  function resolveEntryWithinRoot(workspace, target) {
3283
- const abs = path2.resolve(workspace, target);
3284
- const parent = resolveWithinRoot(workspace, path2.dirname(abs));
3672
+ const abs = path4.resolve(workspace, target);
3673
+ const parent = resolveWithinRoot(workspace, path4.dirname(abs));
3285
3674
  if (!parent) return null;
3286
- return { root: parent.root, real: path2.join(parent.real, path2.basename(abs)) };
3675
+ return { root: parent.root, real: path4.join(parent.real, path4.basename(abs)) };
3287
3676
  }
3288
3677
 
3289
3678
  // src/permissions/PermissionEngine.ts
3290
- import * as path3 from "path";
3679
+ import * as path5 from "path";
3291
3680
  var Mode = /* @__PURE__ */ ((Mode2) => {
3292
3681
  Mode2["DISCUSS"] = "discuss";
3293
3682
  Mode2["PLAN"] = "plan";
@@ -3344,7 +3733,7 @@ var PermissionEngine = class {
3344
3733
  /** 会话级「总是允许此命令」集合 */
3345
3734
  sessionAllowCommands;
3346
3735
  constructor(opts) {
3347
- this.workspaceRoot = path3.resolve(opts.workspaceRoot);
3736
+ this.workspaceRoot = path5.resolve(opts.workspaceRoot);
3348
3737
  this.mode = opts.mode;
3349
3738
  this.allowedCommands = Array.isArray(opts.allowedCommands) ? opts.allowedCommands : [];
3350
3739
  this.sessionAllowTools = /* @__PURE__ */ new Set();
@@ -4259,14 +4648,14 @@ function approvalToHtmlTable(input, maxLen = APPROVAL_VALUE_MAX) {
4259
4648
  }
4260
4649
 
4261
4650
  // src/engine/FileLogHook.ts
4262
- import * as fs4 from "fs";
4263
- import * as path4 from "path";
4651
+ import * as fs5 from "fs";
4652
+ import * as path6 from "path";
4264
4653
  var FileLogHook = class {
4265
- constructor(sessionId, stateDir) {
4654
+ constructor(sessionId, stateDir2) {
4266
4655
  this.sessionId = sessionId;
4267
- fs4.mkdirSync(stateDir, { recursive: true });
4268
- this._logPath = path4.join(stateDir, `${sessionId}.jsonl`);
4269
- this._statePath = path4.join(stateDir, `${sessionId}_state.json`);
4656
+ fs5.mkdirSync(stateDir2, { recursive: true });
4657
+ this._logPath = path6.join(stateDir2, `${sessionId}.jsonl`);
4658
+ this._statePath = path6.join(stateDir2, `${sessionId}_state.json`);
4270
4659
  }
4271
4660
  sessionId;
4272
4661
  _logPath;
@@ -4360,13 +4749,13 @@ var FileLogHook = class {
4360
4749
  ...meta?.nameSource !== void 0 ? { nameSource: meta.nameSource } : {},
4361
4750
  ...meta?.taskCount !== void 0 ? { taskCount: meta.taskCount } : {}
4362
4751
  };
4363
- fs4.writeFileSync(this._statePath, JSON.stringify(state, null, 2), "utf-8");
4752
+ fs5.writeFileSync(this._statePath, JSON.stringify(state, null, 2), "utf-8");
4364
4753
  }
4365
4754
  /** 读取保存的引擎状态 */
4366
- static loadState(sessionId, stateDir) {
4367
- const p = path4.join(stateDir, `${sessionId}_state.json`);
4755
+ static loadState(sessionId, stateDir2) {
4756
+ const p = path6.join(stateDir2, `${sessionId}_state.json`);
4368
4757
  try {
4369
- const raw = fs4.readFileSync(p, "utf-8");
4758
+ const raw = fs5.readFileSync(p, "utf-8");
4370
4759
  return JSON.parse(raw);
4371
4760
  } catch (e) {
4372
4761
  console.error(e);
@@ -4374,10 +4763,10 @@ var FileLogHook = class {
4374
4763
  }
4375
4764
  }
4376
4765
  /** 读取事件日志 */
4377
- static loadEvents(sessionId, stateDir) {
4378
- const p = path4.join(stateDir, `${sessionId}.jsonl`);
4766
+ static loadEvents(sessionId, stateDir2) {
4767
+ const p = path6.join(stateDir2, `${sessionId}.jsonl`);
4379
4768
  try {
4380
- const raw = fs4.readFileSync(p, "utf-8");
4769
+ const raw = fs5.readFileSync(p, "utf-8");
4381
4770
  return raw.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
4382
4771
  } catch (e) {
4383
4772
  console.error(e);
@@ -4388,7 +4777,7 @@ var FileLogHook = class {
4388
4777
  _append(entry) {
4389
4778
  entry["ts"] = (/* @__PURE__ */ new Date()).toISOString();
4390
4779
  try {
4391
- fs4.appendFileSync(this._logPath, JSON.stringify(entry) + "\n", "utf-8");
4780
+ fs5.appendFileSync(this._logPath, JSON.stringify(entry) + "\n", "utf-8");
4392
4781
  } catch (e) {
4393
4782
  console.error(e);
4394
4783
  }
@@ -4812,12 +5201,12 @@ var McpManager = class {
4812
5201
  };
4813
5202
 
4814
5203
  // src/tools/files.ts
4815
- import * as fs5 from "fs";
4816
- import * as path5 from "path";
5204
+ import * as fs6 from "fs";
5205
+ import * as path7 from "path";
4817
5206
  var DEFAULT_MAX_LINES = 2e3;
4818
5207
  var MAX_LINE_CHARS = 500;
4819
5208
  function makeReadFile(workspace) {
4820
- const root = path5.resolve(workspace);
5209
+ const root = path7.resolve(workspace);
4821
5210
  return function read_file(args) {
4822
5211
  const filePath = String(args.path ?? "");
4823
5212
  const startLine = Math.max(1, Number(args.start_line ?? 1));
@@ -4827,13 +5216,13 @@ function makeReadFile(workspace) {
4827
5216
  return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
4828
5217
  }
4829
5218
  const target = resolved.real;
4830
- if (!fs5.existsSync(target) || !fs5.statSync(target).isFile()) {
5219
+ if (!fs6.existsSync(target) || !fs6.statSync(target).isFile()) {
4831
5220
  return { error: `\u4E0D\u662F\u6587\u4EF6: ${filePath}` };
4832
5221
  }
4833
5222
  const selected = [];
4834
5223
  let total = 0;
4835
5224
  try {
4836
- const content = fs5.readFileSync(target, "utf-8");
5225
+ const content = fs6.readFileSync(target, "utf-8");
4837
5226
  const lines = content.split("\n");
4838
5227
  total = lines.length;
4839
5228
  for (let i = startLine - 1; i < Math.min(startLine - 1 + maxLines, lines.length); i++) {
@@ -4848,7 +5237,7 @@ function makeReadFile(workspace) {
4848
5237
  }
4849
5238
  const end = startLine + selected.length - 1;
4850
5239
  const result = {
4851
- path: path5.relative(resolved.root, target),
5240
+ path: path7.relative(resolved.root, target),
4852
5241
  start_line: startLine,
4853
5242
  end_line: end,
4854
5243
  total_lines: total,
@@ -4861,7 +5250,7 @@ function makeReadFile(workspace) {
4861
5250
  };
4862
5251
  }
4863
5252
  function makeWriteFile(workspace) {
4864
- const root = path5.resolve(workspace);
5253
+ const root = path7.resolve(workspace);
4865
5254
  return function write_file(args) {
4866
5255
  const filePath = String(args.path ?? "");
4867
5256
  const content = String(args.content ?? "");
@@ -4871,34 +5260,34 @@ function makeWriteFile(workspace) {
4871
5260
  return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
4872
5261
  }
4873
5262
  const target = resolved.real;
4874
- if (fs5.existsSync(target) && fs5.statSync(target).isDirectory()) {
5263
+ if (fs6.existsSync(target) && fs6.statSync(target).isDirectory()) {
4875
5264
  return { error: "\u8DEF\u5F84\u662F\u76EE\u5F55\uFF0C\u4E0D\u662F\u6587\u4EF6" };
4876
5265
  }
4877
- if (fs5.existsSync(target) && !overwrite) {
5266
+ if (fs6.existsSync(target) && !overwrite) {
4878
5267
  return { error: "\u6587\u4EF6\u5DF2\u5B58\u5728 (overwrite=false)" };
4879
5268
  }
4880
5269
  try {
4881
- fs5.mkdirSync(path5.dirname(target), { recursive: true });
4882
- fs5.writeFileSync(target, content, "utf-8");
5270
+ fs6.mkdirSync(path7.dirname(target), { recursive: true });
5271
+ fs6.writeFileSync(target, content, "utf-8");
4883
5272
  } catch (exc) {
4884
5273
  return { error: `\u5199\u5165\u5931\u8D25: ${String(exc)}` };
4885
5274
  }
4886
- return path5.relative(resolved.root, target);
5275
+ return path7.relative(resolved.root, target);
4887
5276
  };
4888
5277
  }
4889
5278
  function makeListFiles(workspace) {
4890
- const root = path5.resolve(workspace);
5279
+ const root = path7.resolve(workspace);
4891
5280
  return function list_files(args) {
4892
5281
  const dirPath = String(args.path ?? ".");
4893
5282
  const maxResults = Math.min(Number(args.max_results ?? 100), 100);
4894
5283
  const resolved = resolveWithinRoot(root, dirPath);
4895
5284
  if (!resolved) return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
4896
5285
  const target = resolved.real;
4897
- if (!fs5.existsSync(target) || !fs5.statSync(target).isDirectory()) {
5286
+ if (!fs6.existsSync(target) || !fs6.statSync(target).isDirectory()) {
4898
5287
  return { error: `\u4E0D\u662F\u76EE\u5F55: ${dirPath}` };
4899
5288
  }
4900
5289
  try {
4901
- const entries = fs5.readdirSync(target, { withFileTypes: true });
5290
+ const entries = fs6.readdirSync(target, { withFileTypes: true });
4902
5291
  const results = [];
4903
5292
  for (const e of entries) {
4904
5293
  if (results.length >= maxResults) break;
@@ -4907,14 +5296,14 @@ function makeListFiles(workspace) {
4907
5296
  results.push(e.isDirectory() ? `${e.name}/` : e.name);
4908
5297
  }
4909
5298
  results.sort();
4910
- return { path: path5.relative(resolved.root, target), entries: results.slice(0, maxResults) };
5299
+ return { path: path7.relative(resolved.root, target), entries: results.slice(0, maxResults) };
4911
5300
  } catch (exc) {
4912
5301
  return { error: `\u5217\u51FA\u6587\u4EF6\u5931\u8D25: ${String(exc)}` };
4913
5302
  }
4914
5303
  };
4915
5304
  }
4916
5305
  function makeReplaceInFile(workspace) {
4917
- const root = path5.resolve(workspace);
5306
+ const root = path7.resolve(workspace);
4918
5307
  return function replace_in_file(args) {
4919
5308
  const filePath = String(args.path ?? "");
4920
5309
  const oldStr = String(args.old ?? "");
@@ -4924,19 +5313,19 @@ function makeReplaceInFile(workspace) {
4924
5313
  const resolved = resolveWithinRoot(root, filePath);
4925
5314
  if (!resolved) return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
4926
5315
  const target = resolved.real;
4927
- if (!fs5.existsSync(target) || !fs5.statSync(target).isFile()) {
5316
+ if (!fs6.existsSync(target) || !fs6.statSync(target).isFile()) {
4928
5317
  return { error: `\u4E0D\u662F\u6587\u4EF6: ${filePath}` };
4929
5318
  }
4930
5319
  try {
4931
- const content = fs5.readFileSync(target, "utf-8");
5320
+ const content = fs6.readFileSync(target, "utf-8");
4932
5321
  const count = content.split(oldStr).length - 1;
4933
5322
  if (count !== expectedReplacements) {
4934
5323
  return { error: `\u671F\u671B ${expectedReplacements} \u5904\u66FF\u6362\uFF0C\u5B9E\u9645\u627E\u5230 ${count} \u5904: ${filePath}` };
4935
5324
  }
4936
5325
  const updated = content.replaceAll(oldStr, newStr);
4937
- fs5.writeFileSync(target, updated, "utf-8");
5326
+ fs6.writeFileSync(target, updated, "utf-8");
4938
5327
  return {
4939
- path: path5.relative(resolved.root, target),
5328
+ path: path7.relative(resolved.root, target),
4940
5329
  replacements: count,
4941
5330
  chars_before: content.length,
4942
5331
  chars_after: updated.length
@@ -5036,8 +5425,8 @@ function registerFileTools(registry, workspace) {
5036
5425
  }
5037
5426
 
5038
5427
  // src/tools/shell.ts
5039
- import * as fs7 from "fs";
5040
- import * as path7 from "path";
5428
+ import * as fs8 from "fs";
5429
+ import * as path9 from "path";
5041
5430
 
5042
5431
  // src/tools/execAsync.ts
5043
5432
  import { spawn } from "child_process";
@@ -5199,15 +5588,15 @@ function execAsync(file, argv, opts = {}) {
5199
5588
  }
5200
5589
 
5201
5590
  // src/tools/shellRuntime.ts
5202
- import * as fs6 from "fs";
5203
- import * as path6 from "path";
5591
+ import * as fs7 from "fs";
5592
+ import * as path8 from "path";
5204
5593
  var WINDOWS_SUFFIXES = [".exe", ".cmd", ".bat", ".com", ""];
5205
5594
  function pathApi(platform) {
5206
- return platform === "win32" ? path6.win32 : path6.posix;
5595
+ return platform === "win32" ? path8.win32 : path8.posix;
5207
5596
  }
5208
5597
  function defaultFileExists(p) {
5209
5598
  try {
5210
- return fs6.statSync(p).isFile();
5599
+ return fs7.statSync(p).isFile();
5211
5600
  } catch {
5212
5601
  return false;
5213
5602
  }
@@ -5238,7 +5627,7 @@ function kindFromBasename(base) {
5238
5627
  return "sh";
5239
5628
  }
5240
5629
  function busyboxArgs(file, command) {
5241
- const base = (file ? path6.win32.basename(file) : "busybox").toLowerCase().replace(/\.(exe|cmd|bat|com)$/, "");
5630
+ const base = (file ? path8.win32.basename(file) : "busybox").toLowerCase().replace(/\.(exe|cmd|bat|com)$/, "");
5242
5631
  return base === "busybox" ? ["sh", "-c", command] : ["-c", command];
5243
5632
  }
5244
5633
  function argsFor(kind, command) {
@@ -5504,7 +5893,7 @@ var MAX_STDERR = 5e3;
5504
5893
  var MAX_BUFFER = 1024 * 1024 * 5;
5505
5894
  var shellSessions = /* @__PURE__ */ new Map();
5506
5895
  function sessionFor(workspace) {
5507
- const key = path7.resolve(workspace);
5896
+ const key = path9.resolve(workspace);
5508
5897
  let s = shellSessions.get(key);
5509
5898
  if (!s) {
5510
5899
  s = { cwd: key };
@@ -5561,8 +5950,8 @@ function makeRunShell(workspace, spec, tasks, scope) {
5561
5950
  let toRun = command;
5562
5951
  if (target) {
5563
5952
  try {
5564
- const resolved = path7.resolve(session.cwd, target);
5565
- if (fs7.existsSync(resolved) && fs7.statSync(resolved).isDirectory()) {
5953
+ const resolved = path9.resolve(session.cwd, target);
5954
+ if (fs8.existsSync(resolved) && fs8.statSync(resolved).isDirectory()) {
5566
5955
  session.cwd = resolved;
5567
5956
  }
5568
5957
  } catch {
@@ -5687,8 +6076,8 @@ function registerShellTool(registry, workspace, scope) {
5687
6076
  }
5688
6077
 
5689
6078
  // src/tools/nodeGrep.ts
5690
- import * as fs8 from "fs";
5691
- import * as path8 from "path";
6079
+ import * as fs9 from "fs";
6080
+ import * as path10 from "path";
5692
6081
  var SKIP_DIRS = /* @__PURE__ */ new Set([
5693
6082
  ".git",
5694
6083
  "node_modules",
@@ -5713,27 +6102,27 @@ function nodeGrep(opts) {
5713
6102
  } catch (err) {
5714
6103
  throw new Error(`\u65E0\u6548\u7684\u6B63\u5219\u8868\u8FBE\u5F0F: ${opts.pattern}\uFF08${String(err)}\uFF09`);
5715
6104
  }
5716
- const target = path8.resolve(opts.workspace, opts.target);
5717
- if (!fs8.existsSync(target)) {
6105
+ const target = path10.resolve(opts.workspace, opts.target);
6106
+ if (!fs9.existsSync(target)) {
5718
6107
  throw new Error(`\u641C\u7D22\u8DEF\u5F84\u4E0D\u5B58\u5728: ${opts.target}`);
5719
6108
  }
5720
6109
  const matches = [];
5721
6110
  const scanFile = (file) => {
5722
6111
  let stat2;
5723
6112
  try {
5724
- stat2 = fs8.statSync(file);
6113
+ stat2 = fs9.statSync(file);
5725
6114
  } catch {
5726
6115
  return false;
5727
6116
  }
5728
6117
  if (!stat2.isFile() || stat2.size > maxFileBytes) return false;
5729
6118
  let buf;
5730
6119
  try {
5731
- buf = fs8.readFileSync(file);
6120
+ buf = fs9.readFileSync(file);
5732
6121
  } catch {
5733
6122
  return false;
5734
6123
  }
5735
6124
  if (looksBinary(buf)) return false;
5736
- const rel = path8.relative(opts.workspace, file) || path8.basename(file);
6125
+ const rel = path10.relative(opts.workspace, file) || path10.basename(file);
5737
6126
  const lines = buf.toString("utf-8").split("\n");
5738
6127
  for (let i = 0; i < lines.length; i++) {
5739
6128
  const raw = lines[i].replace(/\r$/, "");
@@ -5744,7 +6133,7 @@ function nodeGrep(opts) {
5744
6133
  }
5745
6134
  return false;
5746
6135
  };
5747
- const stat = fs8.statSync(target);
6136
+ const stat = fs9.statSync(target);
5748
6137
  if (stat.isFile()) {
5749
6138
  scanFile(target);
5750
6139
  return matches;
@@ -5754,12 +6143,12 @@ function nodeGrep(opts) {
5754
6143
  const dir = stack.pop();
5755
6144
  let entries;
5756
6145
  try {
5757
- entries = fs8.readdirSync(dir, { withFileTypes: true });
6146
+ entries = fs9.readdirSync(dir, { withFileTypes: true });
5758
6147
  } catch {
5759
6148
  continue;
5760
6149
  }
5761
6150
  for (const entry of entries) {
5762
- const full = path8.join(dir, entry.name);
6151
+ const full = path10.join(dir, entry.name);
5763
6152
  if (entry.isDirectory()) {
5764
6153
  if (!SKIP_DIRS.has(entry.name)) stack.push(full);
5765
6154
  } else if (entry.isFile()) {
@@ -5771,22 +6160,22 @@ function nodeGrep(opts) {
5771
6160
  }
5772
6161
 
5773
6162
  // src/tools/ripgrep.ts
5774
- import * as fs9 from "fs";
5775
- import * as os from "os";
5776
- import * as path9 from "path";
6163
+ import * as fs10 from "fs";
6164
+ import * as os2 from "os";
6165
+ import * as path11 from "path";
5777
6166
  import { createRequire } from "module";
5778
6167
  var cached2;
5779
6168
  function extractBundledRipgrep() {
5780
6169
  try {
5781
- const req = typeof __require === "function" ? __require : createRequire(path9.join(process.cwd(), "__zqagent__.js"));
6170
+ const req = typeof __require === "function" ? __require : createRequire(path11.join(process.cwd(), "__zqagent__.js"));
5782
6171
  const sea = req("node:sea");
5783
6172
  if (!sea?.isSea?.() || typeof sea.getAsset !== "function") return null;
5784
6173
  const asset = sea.getAsset("rg");
5785
6174
  if (!asset) return null;
5786
- const dir = fs9.mkdtempSync(path9.join(os.tmpdir(), "zqagent-rg-"));
5787
- const file = path9.join(dir, process.platform === "win32" ? "rg.exe" : "rg");
5788
- fs9.writeFileSync(file, Buffer.from(asset));
5789
- fs9.chmodSync(file, 493);
6175
+ const dir = fs10.mkdtempSync(path11.join(os2.tmpdir(), "zqagent-rg-"));
6176
+ const file = path11.join(dir, process.platform === "win32" ? "rg.exe" : "rg");
6177
+ fs10.writeFileSync(file, Buffer.from(asset));
6178
+ fs10.chmodSync(file, 493);
5790
6179
  return file;
5791
6180
  } catch {
5792
6181
  return null;
@@ -5795,7 +6184,7 @@ function extractBundledRipgrep() {
5795
6184
  function resolveRipgrep() {
5796
6185
  if (cached2 !== void 0) return cached2;
5797
6186
  const explicit = (process.env.ZQAGENT_RG ?? "").trim();
5798
- if (explicit && fs9.existsSync(explicit)) {
6187
+ if (explicit && fs10.existsSync(explicit)) {
5799
6188
  cached2 = explicit;
5800
6189
  return cached2;
5801
6190
  }
@@ -5809,7 +6198,7 @@ function resolveRipgrep() {
5809
6198
  env: process.env,
5810
6199
  fileExists: (p) => {
5811
6200
  try {
5812
- return fs9.statSync(p).isFile();
6201
+ return fs10.statSync(p).isFile();
5813
6202
  } catch {
5814
6203
  return false;
5815
6204
  }
@@ -6356,8 +6745,8 @@ function registerWebTools(registry) {
6356
6745
  }
6357
6746
 
6358
6747
  // src/tools/git.ts
6359
- import * as fs10 from "fs";
6360
- import * as path10 from "path";
6748
+ import * as fs11 from "fs";
6749
+ import * as path12 from "path";
6361
6750
  async function runGit(repoDir, args, timeout = 15e3) {
6362
6751
  const res = await execAsync("git", ["-c", "core.quotepath=false", ...args], {
6363
6752
  cwd: repoDir,
@@ -6374,9 +6763,9 @@ async function runGit(repoDir, args, timeout = 15e3) {
6374
6763
  async function resolveRepoRoot(workspace, pathArg) {
6375
6764
  let dir = workspace;
6376
6765
  if (pathArg) {
6377
- const abs = path10.resolve(workspace, pathArg);
6766
+ const abs = path12.resolve(workspace, pathArg);
6378
6767
  try {
6379
- dir = fs10.statSync(abs).isDirectory() ? abs : path10.dirname(abs);
6768
+ dir = fs11.statSync(abs).isDirectory() ? abs : path12.dirname(abs);
6380
6769
  } catch {
6381
6770
  dir = abs;
6382
6771
  }
@@ -6389,24 +6778,24 @@ function repoDirArg(workspace, raw) {
6389
6778
  if (typeof raw !== "string") return void 0;
6390
6779
  const v = raw.trim();
6391
6780
  if (!v) return void 0;
6392
- const base = path10.resolve(workspace);
6393
- const abs = path10.resolve(base, v);
6394
- const rel = path10.relative(base, abs);
6395
- if (rel.startsWith("..") || path10.isAbsolute(rel)) return void 0;
6781
+ const base = path12.resolve(workspace);
6782
+ const abs = path12.resolve(base, v);
6783
+ const rel = path12.relative(base, abs);
6784
+ if (rel.startsWith("..") || path12.isAbsolute(rel)) return void 0;
6396
6785
  return abs;
6397
6786
  }
6398
6787
  function toPathspec(repoRoot, workspace, pathArg) {
6399
6788
  if (!pathArg) return null;
6400
- const abs = path10.resolve(workspace, pathArg);
6401
- let rel = path10.relative(repoRoot, abs);
6789
+ const abs = path12.resolve(workspace, pathArg);
6790
+ let rel = path12.relative(repoRoot, abs);
6402
6791
  if (rel === "") return ".";
6403
- if (rel.startsWith("..") || path10.isAbsolute(rel)) {
6792
+ if (rel.startsWith("..") || path12.isAbsolute(rel)) {
6404
6793
  try {
6405
- const realRepo = fs10.realpathSync(repoRoot);
6406
- const realAbs = fs10.realpathSync(abs);
6407
- const rel2 = path10.relative(realRepo, realAbs);
6794
+ const realRepo = fs11.realpathSync(repoRoot);
6795
+ const realAbs = fs11.realpathSync(abs);
6796
+ const rel2 = path12.relative(realRepo, realAbs);
6408
6797
  if (rel2 === "") return ".";
6409
- if (!rel2.startsWith("..") && !path10.isAbsolute(rel2)) return rel2;
6798
+ if (!rel2.startsWith("..") && !path12.isAbsolute(rel2)) return rel2;
6410
6799
  } catch {
6411
6800
  }
6412
6801
  return null;
@@ -6551,10 +6940,10 @@ function registerGitTools(registry, workspace) {
6551
6940
  }
6552
6941
 
6553
6942
  // src/tools/diff.ts
6554
- import * as fs11 from "fs";
6555
- import * as path11 from "path";
6943
+ import * as fs12 from "fs";
6944
+ import * as path13 from "path";
6556
6945
  function makeApplyPatch(workspace) {
6557
- const root = path11.resolve(workspace);
6946
+ const root = path13.resolve(workspace);
6558
6947
  return function apply_patch(args) {
6559
6948
  const patch = String(args.patch ?? "");
6560
6949
  if (!patch.trim()) return { error: "\u8865\u4E01\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A" };
@@ -6593,12 +6982,12 @@ function makeApplyPatch(workspace) {
6593
6982
  contentLines.push(current.slice(1));
6594
6983
  i++;
6595
6984
  }
6596
- if (fs11.existsSync(target)) {
6985
+ if (fs12.existsSync(target)) {
6597
6986
  return { error: `\u6587\u4EF6\u5DF2\u5B58\u5728: ${filePath}` };
6598
6987
  }
6599
- fs11.mkdirSync(path11.dirname(target), { recursive: true });
6600
- fs11.writeFileSync(target, contentLines.join("\n") + "\n", "utf-8");
6601
- const rel = path11.relative(resolved.root, target);
6988
+ fs12.mkdirSync(path13.dirname(target), { recursive: true });
6989
+ fs12.writeFileSync(target, contentLines.join("\n") + "\n", "utf-8");
6990
+ const rel = path13.relative(resolved.root, target);
6602
6991
  changedFiles.push(rel);
6603
6992
  addedFiles.push(rel);
6604
6993
  fileCount++;
@@ -6611,11 +7000,11 @@ function makeApplyPatch(workspace) {
6611
7000
  return { error: `\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916: ${filePath}` };
6612
7001
  }
6613
7002
  const target = entry.real;
6614
- if (!fs11.existsSync(target)) {
7003
+ if (!fs12.existsSync(target)) {
6615
7004
  return { error: `\u6587\u4EF6\u4E0D\u5B58\u5728: ${filePath}` };
6616
7005
  }
6617
- fs11.unlinkSync(target);
6618
- deletedFiles.push(path11.relative(entry.root, target));
7006
+ fs12.unlinkSync(target);
7007
+ deletedFiles.push(path13.relative(entry.root, target));
6619
7008
  fileCount++;
6620
7009
  i++;
6621
7010
  continue;
@@ -6651,10 +7040,10 @@ function makeApplyPatch(workspace) {
6651
7040
  if (!chunkLines.length) {
6652
7041
  return { error: `\u8865\u4E01\u66F4\u65B0\u6CA1\u6709 hunks: ${filePath}` };
6653
7042
  }
6654
- if (!fs11.existsSync(target)) {
7043
+ if (!fs12.existsSync(target)) {
6655
7044
  return { error: `\u6587\u4EF6\u4E0D\u5B58\u5728: ${filePath}` };
6656
7045
  }
6657
- const oldLines = fs11.readFileSync(target, "utf-8").split("\n");
7046
+ const oldLines = fs12.readFileSync(target, "utf-8").split("\n");
6658
7047
  const newLines = applyPatchHunks(oldLines, chunkLines, filePath);
6659
7048
  let finalResolved = resolved;
6660
7049
  if (moveTo) {
@@ -6665,20 +7054,20 @@ function makeApplyPatch(workspace) {
6665
7054
  finalResolved = moved;
6666
7055
  }
6667
7056
  const finalTarget = finalResolved.real;
6668
- if (moveTo && fs11.existsSync(finalTarget) && finalTarget !== target) {
7057
+ if (moveTo && fs12.existsSync(finalTarget) && finalTarget !== target) {
6669
7058
  return { error: `\u6587\u4EF6\u5DF2\u5B58\u5728: ${moveTo}` };
6670
7059
  }
6671
- fs11.mkdirSync(path11.dirname(finalTarget), { recursive: true });
6672
- fs11.writeFileSync(finalTarget, newLines.join("\n"), "utf-8");
7060
+ fs12.mkdirSync(path13.dirname(finalTarget), { recursive: true });
7061
+ fs12.writeFileSync(finalTarget, newLines.join("\n"), "utf-8");
6673
7062
  if (moveTo && finalTarget !== target) {
6674
7063
  const entry = resolveEntryWithinRoot(root, filePath);
6675
7064
  if (!entry) {
6676
7065
  return { error: `\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916: ${filePath}` };
6677
7066
  }
6678
- fs11.unlinkSync(entry.real);
6679
- deletedFiles.push(path11.relative(resolved.root, target));
7067
+ fs12.unlinkSync(entry.real);
7068
+ deletedFiles.push(path13.relative(resolved.root, target));
6680
7069
  }
6681
- changedFiles.push(path11.relative(finalResolved.root, finalTarget));
7070
+ changedFiles.push(path13.relative(finalResolved.root, finalTarget));
6682
7071
  fileCount++;
6683
7072
  continue;
6684
7073
  }
@@ -6690,18 +7079,18 @@ function makeApplyPatch(workspace) {
6690
7079
  return { changed_files: changedFiles, added_files: addedFiles, deleted_files: deletedFiles, file_count: fileCount };
6691
7080
  };
6692
7081
  }
6693
- function applyPatchHunks(oldLines, chunks, path14) {
7082
+ function applyPatchHunks(oldLines, chunks, path16) {
6694
7083
  let newLines = [...oldLines];
6695
7084
  let cursor = 0;
6696
7085
  for (const chunk of chunks) {
6697
7086
  const oldBlock = chunk.filter((l) => l[0] === " " || l[0] === "-").map((l) => l.slice(1));
6698
7087
  const newBlock = chunk.filter((l) => l[0] === " " || l[0] === "+").map((l) => l.slice(1));
6699
7088
  if (!oldBlock.length) {
6700
- throw new Error(`\u8865\u4E01 hunk \u5FC5\u987B\u5305\u542B\u4E0A\u4E0B\u6587\u6216\u5220\u9664\u884C: ${path14}`);
7089
+ throw new Error(`\u8865\u4E01 hunk \u5FC5\u987B\u5305\u542B\u4E0A\u4E0B\u6587\u6216\u5220\u9664\u884C: ${path16}`);
6701
7090
  }
6702
7091
  const matchStart = findBlockMatch(newLines, oldBlock, cursor);
6703
7092
  if (matchStart === -1) {
6704
- throw new Error(`\u8865\u4E01\u4E0A\u4E0B\u6587\u4E0D\u5339\u914D: ${path14}`);
7093
+ throw new Error(`\u8865\u4E01\u4E0A\u4E0B\u6587\u4E0D\u5339\u914D: ${path16}`);
6705
7094
  }
6706
7095
  const matchEnd = matchStart + oldBlock.length;
6707
7096
  newLines.splice(matchStart, oldBlock.length, ...newBlock);
@@ -6718,7 +7107,7 @@ function findBlockMatch(lines, block, start) {
6718
7107
  return -1;
6719
7108
  }
6720
7109
  function makeApplyUnifiedDiff(workspace) {
6721
- const root = path11.resolve(workspace);
7110
+ const root = path13.resolve(workspace);
6722
7111
  return function apply_unified_diff(args) {
6723
7112
  const diff = String(args.diff ?? "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
6724
7113
  if (!diff.trim()) return { error: "diff \u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A" };
@@ -6737,26 +7126,26 @@ function makeApplyUnifiedDiff(workspace) {
6737
7126
  return { error: `\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916: ${targetPath}` };
6738
7127
  }
6739
7128
  const target = resolved.real;
6740
- const oldLines = oldIsDevNull ? [] : fs11.existsSync(target) ? fs11.readFileSync(target, "utf-8").split("\n") : [];
7129
+ const oldLines = oldIsDevNull ? [] : fs12.existsSync(target) ? fs12.readFileSync(target, "utf-8").split("\n") : [];
6741
7130
  const newLines = applyDiffHunks(oldLines, patch.hunks, targetPath);
6742
7131
  hunkCount += patch.hunks.length;
6743
7132
  if (newIsDevNull) {
6744
- if (fs11.existsSync(target)) {
6745
- if (!fs11.statSync(target).isFile()) {
7133
+ if (fs12.existsSync(target)) {
7134
+ if (!fs12.statSync(target).isFile()) {
6746
7135
  return { error: `\u8DEF\u5F84\u4E0D\u662F\u6587\u4EF6: ${targetPath}` };
6747
7136
  }
6748
7137
  const entry = resolveEntryWithinRoot(root, targetPath);
6749
7138
  if (!entry) {
6750
7139
  return { error: `\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916: ${targetPath}` };
6751
7140
  }
6752
- fs11.unlinkSync(entry.real);
7141
+ fs12.unlinkSync(entry.real);
6753
7142
  }
6754
- deletedFiles.push(path11.relative(resolved.root, target));
7143
+ deletedFiles.push(path13.relative(resolved.root, target));
6755
7144
  continue;
6756
7145
  }
6757
- fs11.mkdirSync(path11.dirname(target), { recursive: true });
6758
- fs11.writeFileSync(target, newLines.join("\n"), "utf-8");
6759
- const rel = path11.relative(resolved.root, target);
7146
+ fs12.mkdirSync(path13.dirname(target), { recursive: true });
7147
+ fs12.writeFileSync(target, newLines.join("\n"), "utf-8");
7148
+ const rel = path13.relative(resolved.root, target);
6760
7149
  changedFiles.push(rel);
6761
7150
  if (oldIsDevNull) addedFiles.push(rel);
6762
7151
  }
@@ -7001,7 +7390,8 @@ function buildExplorerEngine(opts) {
7001
7390
  model: opts.model,
7002
7391
  instructions: EXPLORER_INSTRUCTIONS,
7003
7392
  maxIterations: parseMaxIterations(process.env.EXPLORE_MAX_ITERATIONS, 30),
7004
- hook: opts.hook
7393
+ hook: opts.hook,
7394
+ toolResultDir: opts.toolResultDir
7005
7395
  });
7006
7396
  }
7007
7397
  function makeExploreTool(opts) {
@@ -7013,7 +7403,8 @@ function makeExploreTool(opts) {
7013
7403
  provider: opts.provider,
7014
7404
  model: opts.model,
7015
7405
  readonlyRegistrars: opts.readonlyRegistrars,
7016
- hook: opts.hookFactory?.()
7406
+ hook: opts.hookFactory?.(),
7407
+ toolResultDir: opts.toolResultDir
7017
7408
  });
7018
7409
  let report = "";
7019
7410
  let status = "unknown";
@@ -7073,13 +7464,13 @@ function registerExploreTool(registry, opts) {
7073
7464
  }
7074
7465
 
7075
7466
  // src/tools/media.ts
7076
- import * as fs13 from "fs";
7077
- import * as path13 from "path";
7467
+ import * as fs14 from "fs";
7468
+ import * as path15 from "path";
7078
7469
 
7079
7470
  // src/tools/ffmpegRuntime.ts
7080
- import * as fs12 from "fs";
7081
- import * as os2 from "os";
7082
- import * as path12 from "path";
7471
+ import * as fs13 from "fs";
7472
+ import * as os3 from "os";
7473
+ import * as path14 from "path";
7083
7474
  import { createRequire as createRequire2 } from "module";
7084
7475
  import { createHash } from "crypto";
7085
7476
  import { pipeline } from "stream/promises";
@@ -7102,29 +7493,29 @@ if (PROXY_ENV2) {
7102
7493
  }
7103
7494
  var DOWNLOAD_TIMEOUT_MS = 5 * 60 * 1e3;
7104
7495
  function agentBinDir(workspace) {
7105
- return path12.join(path12.resolve(workspace), ".agent", "bin");
7496
+ return path14.join(path14.resolve(workspace), ".agent", "bin");
7106
7497
  }
7107
7498
  function exeName(name, platform) {
7108
7499
  return platform === "win32" ? `${name}.exe` : name;
7109
7500
  }
7110
7501
  function defaultFileExists2(p) {
7111
7502
  try {
7112
- return fs12.statSync(p).isFile();
7503
+ return fs13.statSync(p).isFile();
7113
7504
  } catch {
7114
7505
  return false;
7115
7506
  }
7116
7507
  }
7117
7508
  function extractBundled(name, platform) {
7118
7509
  try {
7119
- const req = typeof __require === "function" ? __require : createRequire2(path12.join(process.cwd(), "__zqagent__.js"));
7510
+ const req = typeof __require === "function" ? __require : createRequire2(path14.join(process.cwd(), "__zqagent__.js"));
7120
7511
  const sea = req("node:sea");
7121
7512
  if (!sea?.isSea?.() || typeof sea.getAsset !== "function") return null;
7122
7513
  const asset = sea.getAsset(name);
7123
7514
  if (!asset) return null;
7124
- const dir = fs12.mkdtempSync(path12.join(os2.tmpdir(), `zqagent-${name}-`));
7125
- const file = path12.join(dir, exeName(name, platform));
7126
- fs12.writeFileSync(file, Buffer.from(asset));
7127
- fs12.chmodSync(file, 493);
7515
+ const dir = fs13.mkdtempSync(path14.join(os3.tmpdir(), `zqagent-${name}-`));
7516
+ const file = path14.join(dir, exeName(name, platform));
7517
+ fs13.writeFileSync(file, Buffer.from(asset));
7518
+ fs13.chmodSync(file, 493);
7128
7519
  return file;
7129
7520
  } catch {
7130
7521
  return null;
@@ -7134,7 +7525,7 @@ function resolveOne(name, workspace, o) {
7134
7525
  const envKey = name === "ffmpeg" ? ENV_FFMPEG : ENV_FFPROBE;
7135
7526
  const explicit = (o.env[envKey] ?? "").trim();
7136
7527
  if (explicit && o.fileExists(explicit)) return { path: explicit, source: "env" };
7137
- const local = path12.join(agentBinDir(workspace), exeName(name, o.platform));
7528
+ const local = path14.join(agentBinDir(workspace), exeName(name, o.platform));
7138
7529
  if (o.fileExists(local)) return { path: local, source: "agent-bin" };
7139
7530
  const bundled = extractBundled(name, o.platform);
7140
7531
  if (bundled) return { path: bundled, source: "bundled" };
@@ -7148,7 +7539,7 @@ function resolveOne(name, workspace, o) {
7148
7539
  var cache = /* @__PURE__ */ new Map();
7149
7540
  function resolveMedia(workspace, opts = {}) {
7150
7541
  const platform = opts.platform ?? process.platform;
7151
- const key = `${platform}:${path12.resolve(workspace)}`;
7542
+ const key = `${platform}:${path14.resolve(workspace)}`;
7152
7543
  const hit = cache.get(key);
7153
7544
  if (hit) return hit;
7154
7545
  const ctx = {
@@ -7254,7 +7645,7 @@ function parseChecksumFile(text, filename) {
7254
7645
  if (!m) continue;
7255
7646
  const hash = m[1].toLowerCase();
7256
7647
  const name = m[2]?.trim() ?? "";
7257
- if (!name || name === filename || path12.basename(name) === filename) return hash;
7648
+ if (!name || name === filename || path14.basename(name) === filename) return hash;
7258
7649
  }
7259
7650
  const single = lines[0]?.trim();
7260
7651
  if (lines.length === 1 && single && /^[0-9a-f]{32,128}$/i.test(single)) {
@@ -7264,7 +7655,7 @@ function parseChecksumFile(text, filename) {
7264
7655
  }
7265
7656
  async function computeChecksum(file, algo) {
7266
7657
  const hash = createHash(algo);
7267
- await pipeline(fs12.createReadStream(file), hash);
7658
+ await pipeline(fs13.createReadStream(file), hash);
7268
7659
  return hash.digest("hex");
7269
7660
  }
7270
7661
  function fetchInit() {
@@ -7279,7 +7670,7 @@ async function downloadToFile(url, dest) {
7279
7670
  const resp = await nodeFetch3(url, fetchInit());
7280
7671
  if (!resp.ok) throw new Error(`HTTP ${resp.status} ${resp.statusText}`);
7281
7672
  if (!resp.body) throw new Error("\u54CD\u5E94\u6CA1\u6709\u5185\u5BB9");
7282
- await pipeline(resp.body, fs12.createWriteStream(dest));
7673
+ await pipeline(resp.body, fs13.createWriteStream(dest));
7283
7674
  }
7284
7675
  async function resolveComponentUrl(c) {
7285
7676
  if (c.url) return c.url;
@@ -7296,12 +7687,12 @@ function findBinaryInTree(dir, names) {
7296
7687
  const cur = stack.pop();
7297
7688
  let entries;
7298
7689
  try {
7299
- entries = fs12.readdirSync(cur, { withFileTypes: true });
7690
+ entries = fs13.readdirSync(cur, { withFileTypes: true });
7300
7691
  } catch {
7301
7692
  continue;
7302
7693
  }
7303
7694
  for (const e of entries) {
7304
- const full = path12.join(cur, e.name);
7695
+ const full = path14.join(cur, e.name);
7305
7696
  if (e.isDirectory()) {
7306
7697
  stack.push(full);
7307
7698
  } else if (names.includes(e.name)) {
@@ -7312,7 +7703,7 @@ function findBinaryInTree(dir, names) {
7312
7703
  return null;
7313
7704
  }
7314
7705
  async function extractArchive(archive, dest, kind, platform) {
7315
- fs12.mkdirSync(dest, { recursive: true });
7706
+ fs13.mkdirSync(dest, { recursive: true });
7316
7707
  let res;
7317
7708
  if (kind === "tar.xz") {
7318
7709
  res = await execAsync("tar", ["-xJf", archive, "-C", dest]);
@@ -7348,7 +7739,7 @@ async function downloadFfmpeg(workspace, opts = {}) {
7348
7739
  error: `\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4E0B\u8F7D\u7684\u5E73\u53F0: ${platform}/${arch}`
7349
7740
  };
7350
7741
  }
7351
- const tmp = fs12.mkdtempSync(path12.join(os2.tmpdir(), "zqagent-ffmpeg-"));
7742
+ const tmp = fs13.mkdtempSync(path14.join(os3.tmpdir(), "zqagent-ffmpeg-"));
7352
7743
  const binDir = agentBinDir(workspace);
7353
7744
  const installed = {};
7354
7745
  try {
@@ -7362,7 +7753,7 @@ async function downloadFfmpeg(workspace, opts = {}) {
7362
7753
  }
7363
7754
  let archive = fetched.get(url);
7364
7755
  if (!archive) {
7365
- const archivePath = path12.join(tmp, path12.basename(new URL(url).pathname) || `${c.name}.${c.archive}`);
7756
+ const archivePath = path14.join(tmp, path14.basename(new URL(url).pathname) || `${c.name}.${c.archive}`);
7366
7757
  log(`\u4E0B\u8F7D ${url}`);
7367
7758
  try {
7368
7759
  await downloadToFile(url, archivePath);
@@ -7372,7 +7763,7 @@ async function downloadFfmpeg(workspace, opts = {}) {
7372
7763
  if (c.checksumUrl) {
7373
7764
  const algo = c.checksumAlgo ?? "sha256";
7374
7765
  try {
7375
- const expected = parseChecksumFile(await fetchText(c.checksumUrl), path12.basename(archivePath));
7766
+ const expected = parseChecksumFile(await fetchText(c.checksumUrl), path14.basename(archivePath));
7376
7767
  if (!expected) {
7377
7768
  notes.push(`\u672A\u80FD\u4ECE ${c.checksumUrl} \u89E3\u6790\u51FA\u6821\u9A8C\u548C\uFF0C\u5DF2\u8DF3\u8FC7\u6821\u9A8C`);
7378
7769
  } else {
@@ -7389,24 +7780,24 @@ async function downloadFfmpeg(workspace, opts = {}) {
7389
7780
  notes.push(`\u6821\u9A8C\u548C\u83B7\u53D6\u5931\u8D25 (${c.checksumUrl}): ${String(e instanceof Error ? e.message : e)}`);
7390
7781
  }
7391
7782
  } else {
7392
- notes.push(`${path12.basename(archivePath)} \u672A\u63D0\u4F9B\u6821\u9A8C\u548C\uFF08\u4EC5 HTTPS \u4F20\u8F93\u4FDD\u62A4\uFF09`);
7783
+ notes.push(`${path14.basename(archivePath)} \u672A\u63D0\u4F9B\u6821\u9A8C\u548C\uFF08\u4EC5 HTTPS \u4F20\u8F93\u4FDD\u62A4\uFF09`);
7393
7784
  }
7394
7785
  archive = { file: archivePath, kind: c.archive };
7395
7786
  fetched.set(url, archive);
7396
7787
  }
7397
- const extractDir = fs12.mkdtempSync(path12.join(tmp, "x-"));
7788
+ const extractDir = fs13.mkdtempSync(path14.join(tmp, "x-"));
7398
7789
  const err = await extractArchive(archive.file, extractDir, archive.kind, platform);
7399
7790
  if (err) return { ok: false, error: err };
7400
7791
  const found = findBinaryInTree(extractDir, [exeName(c.name, platform), c.name]);
7401
7792
  if (!found) {
7402
- return { ok: false, error: `\u5F52\u6863\u5185\u672A\u627E\u5230 ${c.name}: ${path12.basename(archive.file)}` };
7793
+ return { ok: false, error: `\u5F52\u6863\u5185\u672A\u627E\u5230 ${c.name}: ${path14.basename(archive.file)}` };
7403
7794
  }
7404
- fs12.mkdirSync(binDir, { recursive: true });
7405
- const target = path12.join(binDir, exeName(c.name, platform));
7795
+ fs13.mkdirSync(binDir, { recursive: true });
7796
+ const target = path14.join(binDir, exeName(c.name, platform));
7406
7797
  const staging = `${target}.tmp-${process.pid}`;
7407
- fs12.copyFileSync(found, staging);
7408
- fs12.chmodSync(staging, 493);
7409
- fs12.renameSync(staging, target);
7798
+ fs13.copyFileSync(found, staging);
7799
+ fs13.chmodSync(staging, 493);
7800
+ fs13.renameSync(staging, target);
7410
7801
  installed[c.name] = target;
7411
7802
  }
7412
7803
  resetFfmpegCache();
@@ -7415,7 +7806,7 @@ async function downloadFfmpeg(workspace, opts = {}) {
7415
7806
  } catch (e) {
7416
7807
  return { ok: false, error: `\u5B89\u88C5\u5931\u8D25: ${String(e instanceof Error ? e.message : e)}` };
7417
7808
  } finally {
7418
- fs12.rmSync(tmp, { recursive: true, force: true });
7809
+ fs13.rmSync(tmp, { recursive: true, force: true });
7419
7810
  }
7420
7811
  }
7421
7812
  function canAutoDownload(platform, arch) {
@@ -7680,13 +8071,13 @@ function resolveTarget(workspace, value) {
7680
8071
  return { abs: resolved.real, root: resolved.root };
7681
8072
  }
7682
8073
  function relOut(workspace, abs) {
7683
- return path13.relative(workspace, abs) || abs;
8074
+ return path15.relative(workspace, abs) || abs;
7684
8075
  }
7685
8076
  function registerMediaTool(registry, workspace, opts = {}) {
7686
8077
  const platform = opts.platform ?? process.platform;
7687
8078
  const arch = process.arch;
7688
8079
  const media = async (args) => {
7689
- const root = path13.resolve(workspace);
8080
+ const root = path15.resolve(workspace);
7690
8081
  const op = str(args["op"]);
7691
8082
  if (!op) {
7692
8083
  return { error: `\u7F3A\u5C11\u53C2\u6570 op\u3002\u53EF\u9009\u503C\uFF1A${OPS.join(" / ")}` };
@@ -7731,7 +8122,7 @@ function registerMediaTool(registry, workspace, opts = {}) {
7731
8122
  if (!input) return { error: `\u64CD\u4F5C ${op} \u9700\u8981\u53C2\u6570 input\uFF08\u5DE5\u4F5C\u533A\u5185\u76F8\u5BF9\u8DEF\u5F84\uFF09` };
7732
8123
  const r = resolveTarget(root, input);
7733
8124
  if ("error" in r) return { error: r.error };
7734
- if (!fs13.existsSync(r.abs) || !fs13.statSync(r.abs).isFile()) {
8125
+ if (!fs14.existsSync(r.abs) || !fs14.statSync(r.abs).isFile()) {
7735
8126
  return { error: `\u8F93\u5165\u6587\u4EF6\u4E0D\u5B58\u5728: ${input}` };
7736
8127
  }
7737
8128
  inputAbs = r.abs;
@@ -7743,7 +8134,7 @@ function registerMediaTool(registry, workspace, opts = {}) {
7743
8134
  if (!output) return { error: `\u64CD\u4F5C ${op} \u9700\u8981\u53C2\u6570 output\uFF08\u5DE5\u4F5C\u533A\u5185\u76F8\u5BF9\u8DEF\u5F84\uFF09` };
7744
8135
  const r = resolveTarget(root, output);
7745
8136
  if ("error" in r) return { error: r.error };
7746
- fs13.mkdirSync(path13.dirname(r.abs), { recursive: true });
8137
+ fs14.mkdirSync(path15.dirname(r.abs), { recursive: true });
7747
8138
  outputAbs = r.abs;
7748
8139
  relRoot = r.root;
7749
8140
  }
@@ -7793,7 +8184,7 @@ function registerMediaTool(registry, workspace, opts = {}) {
7793
8184
  };
7794
8185
  if (outputAbs) {
7795
8186
  try {
7796
- result["size_bytes"] = fs13.statSync(outputAbs).size;
8187
+ result["size_bytes"] = fs14.statSync(outputAbs).size;
7797
8188
  } catch {
7798
8189
  }
7799
8190
  }
@@ -8033,7 +8424,9 @@ function createDefaultEngine(opts) {
8033
8424
  subagentHookFactory,
8034
8425
  contextWindow,
8035
8426
  thresholdPct,
8427
+ softThresholdPct,
8036
8428
  capTokens,
8429
+ toolResultDir: toolResultDir2,
8037
8430
  logDir
8038
8431
  } = opts;
8039
8432
  const envEffort = process.env.OPENAI_REASONING_EFFORT?.toLowerCase();
@@ -8056,7 +8449,8 @@ function createDefaultEngine(opts) {
8056
8449
  workspace,
8057
8450
  provider: effectiveProvider,
8058
8451
  model,
8059
- hookFactory: subagentHookFactory
8452
+ hookFactory: subagentHookFactory,
8453
+ toolResultDir: toolResultDir2
8060
8454
  });
8061
8455
  const permissions = new PermissionEngine({
8062
8456
  workspaceRoot: workspace,
@@ -8078,7 +8472,9 @@ function createDefaultEngine(opts) {
8078
8472
  hook,
8079
8473
  contextWindow,
8080
8474
  thresholdPct,
8081
- capTokens
8475
+ softThresholdPct,
8476
+ capTokens,
8477
+ toolResultDir: toolResultDir2
8082
8478
  });
8083
8479
  return engine;
8084
8480
  }