opencode-acp 1.14.4 → 1.14.5

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.
Files changed (47) hide show
  1. package/README.md +10 -23
  2. package/README.zh-CN.md +10 -23
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +144 -899
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/commands/context.d.ts.map +1 -1
  7. package/dist/lib/commands/help.d.ts.map +1 -1
  8. package/dist/lib/commands/index.d.ts +0 -1
  9. package/dist/lib/commands/index.d.ts.map +1 -1
  10. package/dist/lib/compress/index.d.ts +1 -2
  11. package/dist/lib/compress/index.d.ts.map +1 -1
  12. package/dist/lib/compress/pipeline.d.ts.map +1 -1
  13. package/dist/lib/config-validation.d.ts.map +1 -1
  14. package/dist/lib/config.d.ts +0 -14
  15. package/dist/lib/config.d.ts.map +1 -1
  16. package/dist/lib/hooks.d.ts.map +1 -1
  17. package/dist/lib/messages/inject/inject.d.ts.map +1 -1
  18. package/dist/lib/messages/prune.d.ts.map +1 -1
  19. package/dist/lib/messages/truncate-tools.d.ts +10 -0
  20. package/dist/lib/messages/truncate-tools.d.ts.map +1 -0
  21. package/dist/lib/prompts/extensions/nudge.d.ts +1 -2
  22. package/dist/lib/prompts/extensions/nudge.d.ts.map +1 -1
  23. package/dist/lib/prompts/system.d.ts +1 -1
  24. package/dist/lib/prompts/system.d.ts.map +1 -1
  25. package/dist/lib/state/persistence.d.ts.map +1 -1
  26. package/dist/lib/state/state.d.ts.map +1 -1
  27. package/dist/lib/state/types.d.ts +12 -1
  28. package/dist/lib/state/types.d.ts.map +1 -1
  29. package/dist/lib/state/utils.d.ts +0 -1
  30. package/dist/lib/state/utils.d.ts.map +1 -1
  31. package/dist/lib/ui/notification.d.ts +0 -2
  32. package/dist/lib/ui/notification.d.ts.map +1 -1
  33. package/dist/lib/ui/utils.d.ts +1 -3
  34. package/dist/lib/ui/utils.d.ts.map +1 -1
  35. package/package.json +2 -1
  36. package/dist/lib/commands/sweep.d.ts +0 -23
  37. package/dist/lib/commands/sweep.d.ts.map +0 -1
  38. package/dist/lib/compress/prune-tool.d.ts +0 -4
  39. package/dist/lib/compress/prune-tool.d.ts.map +0 -1
  40. package/dist/lib/gc/truncate.d.ts +0 -15
  41. package/dist/lib/gc/truncate.d.ts.map +0 -1
  42. package/dist/lib/strategies/deduplication.d.ts +0 -10
  43. package/dist/lib/strategies/deduplication.d.ts.map +0 -1
  44. package/dist/lib/strategies/index.d.ts +0 -3
  45. package/dist/lib/strategies/index.d.ts.map +0 -1
  46. package/dist/lib/strategies/purge-errors.d.ts +0 -13
  47. package/dist/lib/strategies/purge-errors.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -883,7 +883,6 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
883
883
  "commands.protectedTools",
884
884
  "manualMode",
885
885
  "manualMode.enabled",
886
- "manualMode.automaticStrategies",
887
886
  "compress",
888
887
  "compress.mode",
889
888
  "compress.permission",
@@ -923,14 +922,6 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
923
922
  "gc.batchCleanup.lowThreshold",
924
923
  "gc.batchCleanup.highThreshold",
925
924
  "gc.batchCleanup.forceThreshold",
926
- "strategies",
927
- "strategies.deduplication",
928
- "strategies.deduplication.enabled",
929
- "strategies.deduplication.protectedTools",
930
- "strategies.purgeErrors",
931
- "strategies.purgeErrors.enabled",
932
- "strategies.purgeErrors.turns",
933
- "strategies.purgeErrors.protectedTools",
934
925
  "qualityGate",
935
926
  "qualityGate.enabled",
936
927
  "qualityGate.algorithm",
@@ -1089,13 +1080,6 @@ function validateConfigTypes(config) {
1089
1080
  actual: typeof manualMode.enabled
1090
1081
  });
1091
1082
  }
1092
- if (manualMode.automaticStrategies !== void 0 && typeof manualMode.automaticStrategies !== "boolean") {
1093
- errors.push({
1094
- key: "manualMode.automaticStrategies",
1095
- expected: "boolean",
1096
- actual: typeof manualMode.automaticStrategies
1097
- });
1098
- }
1099
1083
  }
1100
1084
  }
1101
1085
  const compress = config.compress;
@@ -1470,81 +1454,6 @@ function validateConfigTypes(config) {
1470
1454
  }
1471
1455
  }
1472
1456
  }
1473
- const strategies = config.strategies;
1474
- if (strategies !== void 0) {
1475
- if (typeof strategies !== "object" || strategies === null || Array.isArray(strategies)) {
1476
- errors.push({
1477
- key: "strategies",
1478
- expected: "object",
1479
- actual: typeof strategies
1480
- });
1481
- } else {
1482
- const dedup = strategies.deduplication;
1483
- if (dedup !== void 0) {
1484
- if (typeof dedup !== "object" || dedup === null || Array.isArray(dedup)) {
1485
- errors.push({
1486
- key: "strategies.deduplication",
1487
- expected: "object",
1488
- actual: typeof dedup
1489
- });
1490
- } else {
1491
- if (dedup.enabled !== void 0 && typeof dedup.enabled !== "boolean") {
1492
- errors.push({
1493
- key: "strategies.deduplication.enabled",
1494
- expected: "boolean",
1495
- actual: typeof dedup.enabled
1496
- });
1497
- }
1498
- if (dedup.protectedTools !== void 0 && !Array.isArray(dedup.protectedTools)) {
1499
- errors.push({
1500
- key: "strategies.deduplication.protectedTools",
1501
- expected: "string[]",
1502
- actual: typeof dedup.protectedTools
1503
- });
1504
- }
1505
- }
1506
- }
1507
- const purge = strategies.purgeErrors;
1508
- if (purge !== void 0) {
1509
- if (typeof purge !== "object" || purge === null || Array.isArray(purge)) {
1510
- errors.push({
1511
- key: "strategies.purgeErrors",
1512
- expected: "object",
1513
- actual: typeof purge
1514
- });
1515
- } else {
1516
- if (purge.enabled !== void 0 && typeof purge.enabled !== "boolean") {
1517
- errors.push({
1518
- key: "strategies.purgeErrors.enabled",
1519
- expected: "boolean",
1520
- actual: typeof purge.enabled
1521
- });
1522
- }
1523
- if (purge.turns !== void 0 && typeof purge.turns !== "number") {
1524
- errors.push({
1525
- key: "strategies.purgeErrors.turns",
1526
- expected: "number",
1527
- actual: typeof purge.turns
1528
- });
1529
- }
1530
- if (typeof purge.turns === "number" && purge.turns < 1) {
1531
- errors.push({
1532
- key: "strategies.purgeErrors.turns",
1533
- expected: "positive number (>= 1)",
1534
- actual: `${purge.turns} (will be clamped to 1)`
1535
- });
1536
- }
1537
- if (purge.protectedTools !== void 0 && !Array.isArray(purge.protectedTools)) {
1538
- errors.push({
1539
- key: "strategies.purgeErrors.protectedTools",
1540
- expected: "string[]",
1541
- actual: typeof purge.protectedTools
1542
- });
1543
- }
1544
- }
1545
- }
1546
- }
1547
- }
1548
1457
  return errors;
1549
1458
  }
1550
1459
 
@@ -1614,8 +1523,7 @@ var defaultConfig = {
1614
1523
  protectedTools: [...DEFAULT_PROTECTED_TOOLS]
1615
1524
  },
1616
1525
  manualMode: {
1617
- enabled: false,
1618
- automaticStrategies: true
1526
+ enabled: false
1619
1527
  },
1620
1528
  turnProtection: {
1621
1529
  enabled: false,
@@ -1652,17 +1560,6 @@ var defaultConfig = {
1652
1560
  preserveRecentTokens: 5e3,
1653
1561
  preserveLastUserMessage: true
1654
1562
  },
1655
- strategies: {
1656
- deduplication: {
1657
- enabled: true,
1658
- protectedTools: []
1659
- },
1660
- purgeErrors: {
1661
- enabled: true,
1662
- turns: 4,
1663
- protectedTools: []
1664
- }
1665
- },
1666
1563
  gc: {
1667
1564
  algorithm: "truncate",
1668
1565
  promotionThreshold: 5,
@@ -1770,32 +1667,6 @@ function loadConfigFile(configPath) {
1770
1667
  return { data: null, parseError: error.message || "Failed to parse config" };
1771
1668
  }
1772
1669
  }
1773
- function mergeStrategies(base, override) {
1774
- if (!override) {
1775
- return base;
1776
- }
1777
- return {
1778
- deduplication: {
1779
- enabled: override.deduplication?.enabled ?? base.deduplication.enabled,
1780
- protectedTools: [
1781
- .../* @__PURE__ */ new Set([
1782
- ...base.deduplication.protectedTools,
1783
- ...override.deduplication?.protectedTools ?? []
1784
- ])
1785
- ]
1786
- },
1787
- purgeErrors: {
1788
- enabled: override.purgeErrors?.enabled ?? base.purgeErrors.enabled,
1789
- turns: override.purgeErrors?.turns ?? base.purgeErrors.turns,
1790
- protectedTools: [
1791
- .../* @__PURE__ */ new Set([
1792
- ...base.purgeErrors.protectedTools,
1793
- ...override.purgeErrors?.protectedTools ?? []
1794
- ])
1795
- ]
1796
- }
1797
- };
1798
- }
1799
1670
  function mergeCompress(base, override) {
1800
1671
  if (!override) {
1801
1672
  return base;
@@ -1843,8 +1714,7 @@ function mergeCommands(base, override) {
1843
1714
  function mergeManualMode(base, override) {
1844
1715
  if (override === void 0) return base;
1845
1716
  return {
1846
- enabled: override.enabled ?? base.enabled,
1847
- automaticStrategies: override.automaticStrategies ?? base.automaticStrategies
1717
+ enabled: override.enabled ?? base.enabled
1848
1718
  };
1849
1719
  }
1850
1720
  function mergeExperimental(base, override) {
@@ -1862,8 +1732,7 @@ function deepCloneConfig(config) {
1862
1732
  protectedTools: [...config.commands.protectedTools]
1863
1733
  },
1864
1734
  manualMode: {
1865
- enabled: config.manualMode.enabled,
1866
- automaticStrategies: config.manualMode.automaticStrategies
1735
+ enabled: config.manualMode.enabled
1867
1736
  },
1868
1737
  turnProtection: { ...config.turnProtection },
1869
1738
  experimental: { ...config.experimental },
@@ -1874,16 +1743,6 @@ function deepCloneConfig(config) {
1874
1743
  modelMinLimits: { ...config.compress.modelMinLimits },
1875
1744
  protectedTools: [...config.compress.protectedTools]
1876
1745
  },
1877
- strategies: {
1878
- deduplication: {
1879
- ...config.strategies.deduplication,
1880
- protectedTools: [...config.strategies.deduplication.protectedTools]
1881
- },
1882
- purgeErrors: {
1883
- ...config.strategies.purgeErrors,
1884
- protectedTools: [...config.strategies.purgeErrors.protectedTools]
1885
- }
1886
- },
1887
1746
  gc: {
1888
1747
  ...config.gc,
1889
1748
  batchCleanup: { ...config.gc.batchCleanup }
@@ -1932,7 +1791,6 @@ function mergeLayer(config, data) {
1932
1791
  ],
1933
1792
  compress: mergeCompress(config.compress, data.compress),
1934
1793
  gc: mergeGC(config.gc, data.gc),
1935
- strategies: mergeStrategies(config.strategies, data.strategies),
1936
1794
  qualityGate: mergeQualityGate(config.qualityGate, data.qualityGate)
1937
1795
  };
1938
1796
  }
@@ -2000,6 +1858,9 @@ Using previous/default values`
2000
1858
  return config;
2001
1859
  }
2002
1860
 
1861
+ // lib/compress/message.ts
1862
+ import { tool as tool2 } from "@opencode-ai/plugin";
1863
+
2003
1864
  // lib/compress/types.ts
2004
1865
  function resolveToolContext(factoryCtx, sessionID) {
2005
1866
  const state = factoryCtx.registry.get(sessionID);
@@ -2017,9 +1878,6 @@ function resolveToolContext(factoryCtx, sessionID) {
2017
1878
  };
2018
1879
  }
2019
1880
 
2020
- // lib/compress/message.ts
2021
- import { tool as tool2 } from "@opencode-ai/plugin";
2022
-
2023
1881
  // lib/token-utils.ts
2024
1882
  import * as _anthropicTokenizer from "@anthropic-ai/tokenizer";
2025
1883
 
@@ -2207,14 +2065,6 @@ function extractToolContent(part) {
2207
2065
  }
2208
2066
  return contents;
2209
2067
  }
2210
- function getTotalToolTokens(state, toolIds) {
2211
- let total = 0;
2212
- for (const id of toolIds) {
2213
- const entry = state.toolParameters.get(id);
2214
- total += entry?.tokenCount ?? 0;
2215
- }
2216
- return total;
2217
- }
2218
2068
  function countAllMessageTokens(msg) {
2219
2069
  const parts = Array.isArray(msg.parts) ? msg.parts : [];
2220
2070
  const texts = [];
@@ -2860,20 +2710,20 @@ function matchesGlob(inputPath, pattern) {
2860
2710
  regex += "$";
2861
2711
  return new RegExp(regex).test(input);
2862
2712
  }
2863
- function getFilePathsFromParameters(tool8, parameters) {
2713
+ function getFilePathsFromParameters(tool7, parameters) {
2864
2714
  if (typeof parameters !== "object" || parameters === null) {
2865
2715
  return [];
2866
2716
  }
2867
2717
  const paths = [];
2868
2718
  const params = parameters;
2869
- if (tool8 === "apply_patch" && typeof params.patchText === "string") {
2719
+ if (tool7 === "apply_patch" && typeof params.patchText === "string") {
2870
2720
  const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g;
2871
2721
  let match;
2872
2722
  while ((match = pathRegex.exec(params.patchText)) !== null) {
2873
2723
  paths.push(match[1].trim());
2874
2724
  }
2875
2725
  }
2876
- if (tool8 === "multiedit") {
2726
+ if (tool7 === "multiedit") {
2877
2727
  if (typeof params.filePath === "string") {
2878
2728
  paths.push(params.filePath);
2879
2729
  }
@@ -3696,15 +3546,6 @@ function countTurns(state, messages) {
3696
3546
  }
3697
3547
  return turnCount;
3698
3548
  }
3699
- function loadPruneMap(obj) {
3700
- if (!obj || typeof obj !== "object") {
3701
- return /* @__PURE__ */ new Map();
3702
- }
3703
- const entries = Object.entries(obj).filter(
3704
- (entry) => typeof entry[0] === "string" && typeof entry[1] === "number"
3705
- );
3706
- return new Map(entries);
3707
- }
3708
3549
  function createPruneMessagesState() {
3709
3550
  return {
3710
3551
  byMessageId: /* @__PURE__ */ new Map(),
@@ -3892,7 +3733,6 @@ function getTierTokenUsage(state) {
3892
3733
  }
3893
3734
  function resetOnCompaction(state) {
3894
3735
  state.toolParameters.clear();
3895
- state.prune.tools = /* @__PURE__ */ new Map();
3896
3736
  state.nudges = {
3897
3737
  contextLimitAnchors: /* @__PURE__ */ new Set(),
3898
3738
  turnNudgeAnchors: /* @__PURE__ */ new Set(),
@@ -3904,7 +3744,8 @@ function resetOnCompaction(state) {
3904
3744
  lastTier2NudgeTokens: void 0,
3905
3745
  lastTier3NudgeTokens: void 0,
3906
3746
  shouldInjectThisTurn: void 0,
3907
- compressBaselineSet: false
3747
+ compressBaselineSet: false,
3748
+ lastProcessedCompressMessageId: void 0
3908
3749
  };
3909
3750
  state.messageIds = {
3910
3751
  byRawId: /* @__PURE__ */ new Map(),
@@ -3968,7 +3809,6 @@ async function saveSessionState(sessionState, logger, sessionName) {
3968
3809
  const state = {
3969
3810
  sessionName,
3970
3811
  prune: {
3971
- tools: Object.fromEntries(sessionState.prune.tools),
3972
3812
  messages: serializePruneMessagesState(sessionState.prune.messages)
3973
3813
  },
3974
3814
  nudges: {
@@ -4003,10 +3843,9 @@ async function loadSessionState(sessionId, logger) {
4003
3843
  }
4004
3844
  const content = await fs.readFile(filePath, "utf-8");
4005
3845
  const state = JSON.parse(content);
4006
- const hasPruneTools = state?.prune?.tools && typeof state.prune.tools === "object";
4007
3846
  const hasPruneMessages = state?.prune?.messages && typeof state.prune.messages === "object";
4008
3847
  const hasNudgeFormat = state?.nudges && typeof state.nudges === "object";
4009
- if (!state || !state.prune || !hasPruneTools || !hasPruneMessages || !state.stats || !hasNudgeFormat) {
3848
+ if (!state || !state.prune || !hasPruneMessages || !state.stats || !hasNudgeFormat) {
4010
3849
  logger.warn("Invalid session state file, ignoring", {
4011
3850
  sessionId
4012
3851
  });
@@ -4094,7 +3933,8 @@ async function loadAllSessionStats(logger) {
4094
3933
  const state = JSON.parse(content);
4095
3934
  if (state?.stats?.totalPruneTokens && state?.prune) {
4096
3935
  result.totalTokens += state.stats.totalPruneTokens;
4097
- result.totalTools += state.prune.tools ? Object.keys(state.prune.tools).length : 0;
3936
+ const legacy = state.prune.tools;
3937
+ result.totalTools += legacy ? Object.keys(legacy).length : 0;
4098
3938
  result.totalMessages += state.prune.messages?.byMessageId ? Object.keys(state.prune.messages.byMessageId).length : 0;
4099
3939
  result.sessionCount++;
4100
3940
  }
@@ -4582,7 +4422,6 @@ function createSessionState() {
4582
4422
  compressPermission: void 0,
4583
4423
  pendingManualTrigger: null,
4584
4424
  prune: {
4585
- tools: /* @__PURE__ */ new Map(),
4586
4425
  messages: createPruneMessagesState()
4587
4426
  },
4588
4427
  nudges: {
@@ -4596,7 +4435,8 @@ function createSessionState() {
4596
4435
  lastTier2NudgeTokens: void 0,
4597
4436
  lastTier3NudgeTokens: void 0,
4598
4437
  shouldInjectThisTurn: void 0,
4599
- compressBaselineSet: false
4438
+ compressBaselineSet: false,
4439
+ lastProcessedCompressMessageId: void 0
4600
4440
  },
4601
4441
  stats: {
4602
4442
  pruneTokenCounter: 0,
@@ -4627,7 +4467,6 @@ function resetSessionState(state) {
4627
4467
  state.compressPermission = void 0;
4628
4468
  state.pendingManualTrigger = null;
4629
4469
  state.prune = {
4630
- tools: /* @__PURE__ */ new Map(),
4631
4470
  messages: createPruneMessagesState()
4632
4471
  };
4633
4472
  state.nudges = {
@@ -4641,7 +4480,8 @@ function resetSessionState(state) {
4641
4480
  lastTier2NudgeTokens: void 0,
4642
4481
  lastTier3NudgeTokens: void 0,
4643
4482
  shouldInjectThisTurn: void 0,
4644
- compressBaselineSet: false
4483
+ compressBaselineSet: false,
4484
+ lastProcessedCompressMessageId: void 0
4645
4485
  };
4646
4486
  state.stats = {
4647
4487
  pruneTokenCounter: 0,
@@ -4682,7 +4522,6 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
4682
4522
  }
4683
4523
  return;
4684
4524
  }
4685
- state.prune.tools = loadPruneMap(persisted.prune.tools);
4686
4525
  state.prune.messages = loadPruneMessagesState(persisted.prune.messages);
4687
4526
  state.nudges.contextLimitAnchors = new Set(persisted.nudges.contextLimitAnchors || []);
4688
4527
  state.nudges.turnNudgeAnchors = /* @__PURE__ */ new Set([
@@ -4808,255 +4647,7 @@ function trimToolParametersCache(state) {
4808
4647
  }
4809
4648
  }
4810
4649
 
4811
- // lib/strategies/deduplication.ts
4812
- var deduplicate = (state, logger, config, messages) => {
4813
- if (state.manualMode && !config.manualMode.automaticStrategies) {
4814
- return;
4815
- }
4816
- if (!config.strategies.deduplication.enabled) {
4817
- return;
4818
- }
4819
- const allToolIds = state.toolIdList;
4820
- if (allToolIds.length === 0) {
4821
- return;
4822
- }
4823
- const unprunedIds = allToolIds.filter((id) => !state.prune.tools.has(id));
4824
- if (unprunedIds.length === 0) {
4825
- return;
4826
- }
4827
- const protectedTools = config.strategies.deduplication.protectedTools;
4828
- const signatureMap = /* @__PURE__ */ new Map();
4829
- for (const id of unprunedIds) {
4830
- const metadata = state.toolParameters.get(id);
4831
- if (!metadata) {
4832
- continue;
4833
- }
4834
- if (isToolNameProtected(metadata.tool, protectedTools)) {
4835
- continue;
4836
- }
4837
- const filePaths = getFilePathsFromParameters(metadata.tool, metadata.parameters);
4838
- if (isFilePathProtected(filePaths, config.protectedFilePatterns)) {
4839
- continue;
4840
- }
4841
- const signature = createToolSignature(metadata.tool, metadata.parameters);
4842
- if (!signatureMap.has(signature)) {
4843
- signatureMap.set(signature, []);
4844
- }
4845
- const ids = signatureMap.get(signature);
4846
- if (ids) {
4847
- ids.push(id);
4848
- }
4849
- }
4850
- const newPruneIds = [];
4851
- for (const [, ids] of signatureMap.entries()) {
4852
- if (ids.length > 1) {
4853
- const idsToRemove = ids.slice(0, -1);
4854
- newPruneIds.push(...idsToRemove);
4855
- }
4856
- }
4857
- state.stats.totalPruneTokens += getTotalToolTokens(state, newPruneIds);
4858
- if (newPruneIds.length > 0) {
4859
- for (const id of newPruneIds) {
4860
- const entry = state.toolParameters.get(id);
4861
- state.prune.tools.set(id, entry?.tokenCount ?? 0);
4862
- }
4863
- logger.debug(`Marked ${newPruneIds.length} duplicate tool calls for pruning`);
4864
- }
4865
- };
4866
- function createToolSignature(tool8, parameters) {
4867
- if (!parameters) {
4868
- return tool8;
4869
- }
4870
- const normalized = normalizeParameters(parameters);
4871
- const sorted = sortObjectKeys(normalized);
4872
- return `${tool8}::${JSON.stringify(sorted)}`;
4873
- }
4874
- function normalizeParameters(params) {
4875
- if (typeof params !== "object" || params === null) return params;
4876
- if (Array.isArray(params)) return params;
4877
- const normalized = {};
4878
- for (const [key, value] of Object.entries(params)) {
4879
- if (value !== void 0 && value !== null) {
4880
- normalized[key] = value;
4881
- }
4882
- }
4883
- return normalized;
4884
- }
4885
- function sortObjectKeys(obj) {
4886
- if (typeof obj !== "object" || obj === null) return obj;
4887
- if (Array.isArray(obj)) return obj.map(sortObjectKeys);
4888
- const sorted = {};
4889
- for (const key of Object.keys(obj).sort()) {
4890
- sorted[key] = sortObjectKeys(obj[key]);
4891
- }
4892
- return sorted;
4893
- }
4894
-
4895
- // lib/strategies/purge-errors.ts
4896
- var purgeErrors = (state, logger, config, messages) => {
4897
- if (state.manualMode && !config.manualMode.automaticStrategies) {
4898
- return;
4899
- }
4900
- if (!config.strategies.purgeErrors.enabled) {
4901
- return;
4902
- }
4903
- const allToolIds = state.toolIdList;
4904
- if (allToolIds.length === 0) {
4905
- return;
4906
- }
4907
- const unprunedIds = allToolIds.filter((id) => !state.prune.tools.has(id));
4908
- if (unprunedIds.length === 0) {
4909
- return;
4910
- }
4911
- const protectedTools = config.strategies.purgeErrors.protectedTools;
4912
- const turnThreshold = Math.max(1, config.strategies.purgeErrors.turns);
4913
- const newPruneIds = [];
4914
- for (const id of unprunedIds) {
4915
- const metadata = state.toolParameters.get(id);
4916
- if (!metadata) {
4917
- continue;
4918
- }
4919
- if (isToolNameProtected(metadata.tool, protectedTools)) {
4920
- continue;
4921
- }
4922
- const filePaths = getFilePathsFromParameters(metadata.tool, metadata.parameters);
4923
- if (isFilePathProtected(filePaths, config.protectedFilePatterns)) {
4924
- continue;
4925
- }
4926
- if (metadata.status !== "error") {
4927
- continue;
4928
- }
4929
- const turnAge = state.currentTurn - metadata.turn;
4930
- if (turnAge >= turnThreshold) {
4931
- newPruneIds.push(id);
4932
- }
4933
- }
4934
- if (newPruneIds.length > 0) {
4935
- state.stats.totalPruneTokens += getTotalToolTokens(state, newPruneIds);
4936
- for (const id of newPruneIds) {
4937
- const entry = state.toolParameters.get(id);
4938
- state.prune.tools.set(id, entry?.tokenCount ?? 0);
4939
- }
4940
- logger.debug(
4941
- `Marked ${newPruneIds.length} error tool calls for pruning (older than ${turnThreshold} turns)`
4942
- );
4943
- }
4944
- };
4945
-
4946
4650
  // lib/ui/utils.ts
4947
- function extractParameterKey(tool8, parameters) {
4948
- if (!parameters) return "";
4949
- if (tool8 === "read" && parameters.filePath) {
4950
- const offset = parameters.offset;
4951
- const limit = parameters.limit;
4952
- if (offset !== void 0 && limit !== void 0) {
4953
- return `${parameters.filePath} (lines ${offset}-${offset + limit})`;
4954
- }
4955
- if (offset !== void 0) {
4956
- return `${parameters.filePath} (lines ${offset}+)`;
4957
- }
4958
- if (limit !== void 0) {
4959
- return `${parameters.filePath} (lines 0-${limit})`;
4960
- }
4961
- return parameters.filePath;
4962
- }
4963
- if ((tool8 === "write" || tool8 === "edit" || tool8 === "multiedit") && parameters.filePath) {
4964
- return parameters.filePath;
4965
- }
4966
- if (tool8 === "apply_patch" && typeof parameters.patchText === "string") {
4967
- const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g;
4968
- const paths = [];
4969
- let match;
4970
- while ((match = pathRegex.exec(parameters.patchText)) !== null) {
4971
- paths.push(match[1].trim());
4972
- }
4973
- if (paths.length > 0) {
4974
- const uniquePaths = [...new Set(paths)];
4975
- const count = uniquePaths.length;
4976
- const plural = count > 1 ? "s" : "";
4977
- if (count === 1) return uniquePaths[0];
4978
- if (count === 2) return uniquePaths.join(", ");
4979
- return `${count} file${plural}: ${uniquePaths[0]}, ${uniquePaths[1]}...`;
4980
- }
4981
- return "patch";
4982
- }
4983
- if (tool8 === "list") {
4984
- return parameters.path || "(current directory)";
4985
- }
4986
- if (tool8 === "glob") {
4987
- if (parameters.pattern) {
4988
- const pathInfo = parameters.path ? ` in ${parameters.path}` : "";
4989
- return `"${parameters.pattern}"${pathInfo}`;
4990
- }
4991
- return "(unknown pattern)";
4992
- }
4993
- if (tool8 === "grep") {
4994
- if (parameters.pattern) {
4995
- const pathInfo = parameters.path ? ` in ${parameters.path}` : "";
4996
- return `"${parameters.pattern}"${pathInfo}`;
4997
- }
4998
- return "(unknown pattern)";
4999
- }
5000
- if (tool8 === "bash") {
5001
- if (parameters.description) return parameters.description;
5002
- if (parameters.command) {
5003
- return parameters.command.length > 50 ? parameters.command.substring(0, 50) + "..." : parameters.command;
5004
- }
5005
- }
5006
- if (tool8 === "webfetch" && parameters.url) {
5007
- return parameters.url;
5008
- }
5009
- if (tool8 === "websearch" && parameters.query) {
5010
- return `"${parameters.query}"`;
5011
- }
5012
- if (tool8 === "codesearch" && parameters.query) {
5013
- return `"${parameters.query}"`;
5014
- }
5015
- if (tool8 === "todowrite") {
5016
- return `${parameters.todos?.length || 0} todos`;
5017
- }
5018
- if (tool8 === "todoread") {
5019
- return "read todo list";
5020
- }
5021
- if (tool8 === "task" && parameters.description) {
5022
- return parameters.description;
5023
- }
5024
- if (tool8 === "skill" && parameters.name) {
5025
- return parameters.name;
5026
- }
5027
- if (tool8 === "lsp") {
5028
- const op = parameters.operation || "lsp";
5029
- const path = parameters.filePath || "";
5030
- const line = parameters.line;
5031
- const char = parameters.character;
5032
- if (path && line !== void 0 && char !== void 0) {
5033
- return `${op} ${path}:${line}:${char}`;
5034
- }
5035
- if (path) {
5036
- return `${op} ${path}`;
5037
- }
5038
- return op;
5039
- }
5040
- if (tool8 === "question") {
5041
- const questions = parameters.questions;
5042
- if (Array.isArray(questions) && questions.length > 0) {
5043
- const headers = questions.map((q) => q.header || "").filter(Boolean).slice(0, 3);
5044
- const count = questions.length;
5045
- const plural = count > 1 ? "s" : "";
5046
- if (headers.length > 0) {
5047
- const suffix = count > 3 ? ` (+${count - 3} more)` : "";
5048
- return `${count} question${plural}: ${headers.join(", ")}${suffix}`;
5049
- }
5050
- return `${count} question${plural}`;
5051
- }
5052
- return "question";
5053
- }
5054
- const paramStr = JSON.stringify(parameters);
5055
- if (paramStr === "{}" || paramStr === "[]" || paramStr === "null") {
5056
- return "";
5057
- }
5058
- return paramStr.substring(0, 50);
5059
- }
5060
4651
  function formatAge(createdAt) {
5061
4652
  const elapsed = Date.now() - createdAt;
5062
4653
  if (elapsed < 6e4) return "just now";
@@ -5071,10 +4662,6 @@ function formatTokenCount(tokens, compact) {
5071
4662
  }
5072
4663
  return tokens.toString() + suffix;
5073
4664
  }
5074
- function truncate(str, maxLen = 60) {
5075
- if (str.length <= maxLen) return str;
5076
- return str.slice(0, maxLen - 3) + "...";
5077
- }
5078
4665
  function formatProgressBar(messageIds, prunedMessages, recentMessageIds, width = 50) {
5079
4666
  const ACTIVE = "\u2588";
5080
4667
  const PRUNED = "\u2591";
@@ -5134,48 +4721,6 @@ function cacheSystemPromptTokens(state, messages) {
5134
4721
  const estimatedSystemTokens = Math.max(0, firstInputTokens - countTokens2(firstUserText));
5135
4722
  state.systemPromptTokens = estimatedSystemTokens > 0 ? estimatedSystemTokens : void 0;
5136
4723
  }
5137
- function shortenPath(input, workingDirectory) {
5138
- const inPathMatch = input.match(/^(.+) in (.+)$/);
5139
- if (inPathMatch) {
5140
- const prefix = inPathMatch[1];
5141
- const pathPart = inPathMatch[2];
5142
- const shortenedPath = shortenSinglePath(pathPart, workingDirectory);
5143
- return `${prefix} in ${shortenedPath}`;
5144
- }
5145
- return shortenSinglePath(input, workingDirectory);
5146
- }
5147
- function shortenSinglePath(path, workingDirectory) {
5148
- if (workingDirectory) {
5149
- if (path.startsWith(workingDirectory + "/")) {
5150
- return path.slice(workingDirectory.length + 1);
5151
- }
5152
- if (path === workingDirectory) {
5153
- return ".";
5154
- }
5155
- }
5156
- return path;
5157
- }
5158
- function formatPrunedItemsList(pruneToolIds, toolMetadata, workingDirectory) {
5159
- const lines = [];
5160
- for (const id of pruneToolIds) {
5161
- const metadata = toolMetadata.get(id);
5162
- if (metadata) {
5163
- const paramKey = extractParameterKey(metadata.tool, metadata.parameters);
5164
- if (paramKey) {
5165
- const displayKey = truncate(shortenPath(paramKey, workingDirectory), 60);
5166
- lines.push(`\u2192 ${metadata.tool}: ${displayKey}`);
5167
- } else {
5168
- lines.push(`\u2192 ${metadata.tool}`);
5169
- }
5170
- }
5171
- }
5172
- const knownCount = pruneToolIds.filter((id) => toolMetadata.has(id)).length;
5173
- const unknownCount = pruneToolIds.length - knownCount;
5174
- if (unknownCount > 0) {
5175
- lines.push(`\u2192 (${unknownCount} tool${unknownCount > 1 ? "s" : ""} with unknown metadata)`);
5176
- }
5177
- return lines;
5178
- }
5179
4724
 
5180
4725
  // lib/ui/notification.ts
5181
4726
  var TOAST_BODY_MAX_LINES = 12;
@@ -6413,8 +5958,6 @@ async function prepareSession(ctx, toolCtx, title) {
6413
5958
  ctx.config
6414
5959
  );
6415
5960
  assignMessageRefs(ctx.state, rawMessages);
6416
- deduplicate(ctx.state, ctx.logger, ctx.config, rawMessages);
6417
- purgeErrors(ctx.state, ctx.logger, ctx.config, rawMessages);
6418
5961
  return {
6419
5962
  rawMessages,
6420
5963
  searchContext: buildSearchContext(ctx.state, rawMessages)
@@ -6537,7 +6080,7 @@ function normalizeRef(ref) {
6537
6080
  }
6538
6081
  function formatKeptMessage(msg, ref, maxChars) {
6539
6082
  const formatted = formatByType(msg);
6540
- const truncated = truncate2(formatted, maxChars);
6083
+ const truncated = truncate(formatted, maxChars);
6541
6084
  return `
6542
6085
  --- [${ref}: ${labelForMessage(msg)}] ---
6543
6086
  ${truncated}
@@ -6550,11 +6093,11 @@ function formatByType(msg) {
6550
6093
  return part.text;
6551
6094
  }
6552
6095
  if (part.type === "tool") {
6553
- const tool8 = part.tool || "unknown";
6096
+ const tool7 = part.tool || "unknown";
6554
6097
  const state = part.state || {};
6555
6098
  const input = state.input || {};
6556
6099
  const output = state.output || "";
6557
- switch (tool8) {
6100
+ switch (tool7) {
6558
6101
  case "bash":
6559
6102
  case "interactive_bash": {
6560
6103
  const cmd = typeof input === "string" ? input : input.command || JSON.stringify(input);
@@ -6583,7 +6126,7 @@ ${content}`;
6583
6126
  if (output && typeof output === "string" && output.length > 0) {
6584
6127
  return output;
6585
6128
  }
6586
- const compact = JSON.stringify({ tool: tool8, input }, null, 0);
6129
+ const compact = JSON.stringify({ tool: tool7, input }, null, 0);
6587
6130
  return compact.length > 500 ? compact.slice(0, 500) + "..." : compact;
6588
6131
  }
6589
6132
  }
@@ -6594,15 +6137,15 @@ ${content}`;
6594
6137
  function labelForMessage(msg) {
6595
6138
  for (const part of msg.parts || []) {
6596
6139
  if (part.type === "tool") {
6597
- const tool8 = part.tool || "unknown";
6140
+ const tool7 = part.tool || "unknown";
6598
6141
  const input = part.state?.input || {};
6599
6142
  const fp = input.filePath || input.path || input.command || "";
6600
- return fp ? `${tool8}: ${String(fp).slice(0, 60)}` : tool8;
6143
+ return fp ? `${tool7}: ${String(fp).slice(0, 60)}` : tool7;
6601
6144
  }
6602
6145
  }
6603
6146
  return msg.info.role === "user" ? "user" : "text";
6604
6147
  }
6605
- function truncate2(text, maxChars) {
6148
+ function truncate(text, maxChars) {
6606
6149
  if (text.length <= maxChars) return text;
6607
6150
  return text.slice(0, maxChars) + `
6608
6151
  ... [truncated, ${text.length} chars total]`;
@@ -7317,8 +6860,8 @@ var resolveEffectiveCompressPermission = (basePermission, hostPermissions, agent
7317
6860
  agentName ? hostPermissions.agents[agentName] : void 0
7318
6861
  ) ? "deny" : basePermission;
7319
6862
  };
7320
- var hasExplicitToolPermission = (permissionConfig, tool8) => {
7321
- return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig, tool8) : false;
6863
+ var hasExplicitToolPermission = (permissionConfig, tool7) => {
6864
+ return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig, tool7) : false;
7322
6865
  };
7323
6866
 
7324
6867
  // lib/compress-permission.ts
@@ -7509,7 +7052,7 @@ var dropEmptyMessages = (messages) => {
7509
7052
  };
7510
7053
 
7511
7054
  // lib/prompts/extensions/nudge.ts
7512
- function buildCompressedBlockGuidance(state, gcConfig, context) {
7055
+ function buildCompressedBlockGuidance(state, context) {
7513
7056
  const activeBlockIds = Array.from(state.prune.messages.activeBlockIds).filter((id) => Number.isInteger(id) && id > 0).sort((a, b) => a - b);
7514
7057
  const blockCount = activeBlockIds.length;
7515
7058
  const blocksForStats = activeBlockIds.map((id) => state.prune.messages.blocksById.get(id)).filter((b) => b !== void 0 && b.active);
@@ -7550,32 +7093,6 @@ function buildCompressedBlockGuidance(state, gcConfig, context) {
7550
7093
  }
7551
7094
  }
7552
7095
  }
7553
- const usageRatio = context?.currentTokens && context?.modelContextLimit ? context.currentTokens / context.modelContextLimit : 0;
7554
- if (gcConfig && usageRatio > 0.9) {
7555
- const promotionThreshold = gcConfig.promotionThreshold;
7556
- const agingBlocks = [];
7557
- for (const blockId of activeBlockIds) {
7558
- const block = state.prune.messages.blocksById.get(blockId);
7559
- if (!block) continue;
7560
- const survived = block.survivedCount ?? 0;
7561
- const gen = block.generation ?? "young";
7562
- const sizeK = (block.summary.length / 1e3).toFixed(1);
7563
- const preview = block.summary.slice(0, 120).replace(/\n/g, " ");
7564
- if (gen === "old" || survived >= promotionThreshold - 2) {
7565
- agingBlocks.push(
7566
- ` b${blockId}: age=${survived}/${promotionThreshold}, gen=${gen}, size=${sizeK}K chars \u2014 ${preview}...`
7567
- );
7568
- }
7569
- }
7570
- if (agingBlocks.length > 0) {
7571
- lines.push("");
7572
- lines.push("\u26A0\uFE0F Block aging warning \u2014 context near limit, these blocks may be truncated by last-resort GC:");
7573
- lines.push(...agingBlocks);
7574
- lines.push(
7575
- "Re-summarize these blocks into concise new ones to preserve key facts. At 100% context, oversized blocks are auto-truncated as a last resort."
7576
- );
7577
- }
7578
- }
7579
7096
  return lines.join("\n");
7580
7097
  }
7581
7098
  function renderMessagePriorityGuidance(priorityLabel, refs) {
@@ -8151,7 +7668,7 @@ function estimateContextComposition(messages, state, protectedTools = [], protec
8151
7668
  perTool.sort((a, b) => b.tokens - a.tokens);
8152
7669
  perCode.sort((a, b) => b.tokens - a.tokens);
8153
7670
  perText.sort((a, b) => b.tokens - a.tokens);
8154
- const toolTypeBreakdown = Array.from(toolTypeMap.entries()).map(([tool8, tokens]) => ({ tool: tool8, tokens })).sort((a, b) => b.tokens - a.tokens);
7671
+ const toolTypeBreakdown = Array.from(toolTypeMap.entries()).map(([tool7, tokens]) => ({ tool: tool7, tokens })).sort((a, b) => b.tokens - a.tokens);
8155
7672
  return {
8156
7673
  toolTokens,
8157
7674
  codeTokens,
@@ -8508,38 +8025,45 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
8508
8025
  const currentTurnStart = lastUserIdx >= 0 ? lastUserIdx + 1 : 0;
8509
8026
  const currentTurnHasCompress = messages.slice(currentTurnStart).some((m) => m.info.role === "assistant" && messageHasCompressAttempt(m));
8510
8027
  if (currentTurnHasCompress) {
8511
- const wasNudgeTriggered = state.nudges.lastNudgeShownTokens !== void 0;
8512
- state.nudges.contextLimitAnchors.clear();
8513
- state.nudges.turnNudgeAnchors.clear();
8514
- state.nudges.iterationNudgeAnchors.clear();
8515
- state.nudges.lastNudgeShownTokens = void 0;
8516
- state.nudges.lastToolOutputNudgeTokens = void 0;
8517
- state.nudges.lastTier2NudgeTokens = void 0;
8518
- state.nudges.lastTier3NudgeTokens = void 0;
8519
- const currentTurnHasSuccessfulCompress = messages.slice(currentTurnStart).some((m) => m.info.role === "assistant" && messageHasCompress(m));
8520
- if (currentTurnHasSuccessfulCompress && wasNudgeTriggered && !state.nudges.compressBaselineSet) {
8521
- const baseline = state.nudges.lastPerMessageNudgeTokens;
8522
- const postCompress = currentTokens;
8523
- const preCompress = preCompressTokens;
8524
- if (baseline !== void 0 && postCompress !== void 0 && preCompress !== void 0 && preCompress > postCompress) {
8525
- const growth = preCompress - baseline;
8526
- const compressed = preCompress - postCompress;
8527
- if (growth > 0 && compressed > 0) {
8528
- const ratio = Math.min(1, compressed / growth);
8529
- const adjustment = Math.min(1, ratio * 2);
8530
- const newBaseline = baseline + Math.round((postCompress - baseline) * adjustment);
8531
- state.nudges.lastPerMessageNudgeTokens = newBaseline;
8028
+ const lastCompressMsg = messages.slice(currentTurnStart).findLast((m) => m.info.role === "assistant" && messageHasCompressAttempt(m));
8029
+ const lastCompressMsgId = lastCompressMsg?.info?.id;
8030
+ if (lastCompressMsgId !== state.nudges.lastProcessedCompressMessageId) {
8031
+ state.nudges.lastProcessedCompressMessageId = lastCompressMsgId;
8032
+ const wasNudgeTriggered = state.nudges.lastNudgeShownTokens !== void 0;
8033
+ state.nudges.contextLimitAnchors.clear();
8034
+ state.nudges.turnNudgeAnchors.clear();
8035
+ state.nudges.iterationNudgeAnchors.clear();
8036
+ state.nudges.lastNudgeShownTokens = void 0;
8037
+ state.nudges.lastToolOutputNudgeTokens = void 0;
8038
+ state.nudges.lastTier2NudgeTokens = void 0;
8039
+ state.nudges.lastTier3NudgeTokens = void 0;
8040
+ const currentTurnHasSuccessfulCompress = messages.slice(currentTurnStart).some((m) => m.info.role === "assistant" && messageHasCompress(m));
8041
+ if (currentTurnHasSuccessfulCompress && wasNudgeTriggered && !state.nudges.compressBaselineSet) {
8042
+ const baseline = state.nudges.lastPerMessageNudgeTokens;
8043
+ const postCompress = currentTokens;
8044
+ const preCompress = preCompressTokens;
8045
+ if (baseline !== void 0 && postCompress !== void 0 && preCompress !== void 0 && preCompress > postCompress) {
8046
+ const growth = preCompress - baseline;
8047
+ const compressed = preCompress - postCompress;
8048
+ if (growth > 0 && compressed > 0) {
8049
+ const ratio = Math.min(1, compressed / growth);
8050
+ const adjustment = Math.min(1, ratio * 2);
8051
+ state.nudges.lastPerMessageNudgeTokens = baseline + Math.round((postCompress - baseline) * adjustment);
8052
+ } else {
8053
+ state.nudges.lastPerMessageNudgeTokens = postCompress;
8054
+ }
8532
8055
  } else {
8533
8056
  state.nudges.lastPerMessageNudgeTokens = postCompress;
8534
8057
  }
8535
- } else {
8536
- state.nudges.lastPerMessageNudgeTokens = postCompress;
8058
+ state.nudges.compressBaselineSet = true;
8537
8059
  }
8538
- state.nudges.compressBaselineSet = true;
8060
+ state.nudges.shouldInjectThisTurn = false;
8061
+ saveSessionState(state, logger).catch(() => {
8062
+ });
8063
+ return;
8539
8064
  }
8540
- saveSessionState(state, logger).catch(() => {
8541
- });
8542
- return;
8065
+ } else {
8066
+ state.nudges.lastProcessedCompressMessageId = void 0;
8543
8067
  }
8544
8068
  state.nudges.compressBaselineSet = false;
8545
8069
  let anchorsChanged = false;
@@ -8822,7 +8346,7 @@ ${HOW_TO_COMPRESS_RULES}`;
8822
8346
  const visibleMessageIds = new Set(
8823
8347
  messages.map((message) => message.info.id)
8824
8348
  );
8825
- const blockGuidance = buildCompressedBlockGuidance(state, config.gc, {
8349
+ const blockGuidance = buildCompressedBlockGuidance(state, {
8826
8350
  currentTokens,
8827
8351
  modelContextLimit,
8828
8352
  includeHint: tipsText !== null,
@@ -9642,7 +9166,7 @@ function renderOverview(visibleMessages, summaryTokens, blocks, fetchFailed, raw
9642
9166
  lines.push(
9643
9167
  ` ${formatTokens(total)} total | ${formatTokens(totalTool)} tool (${toolPct}%) | ${formatTokens(totalText)} text (${textPct}%) | ${formatTokens(summaryTokens)} summaries (${summaryPct}%)`
9644
9168
  );
9645
- const topTypes = Array.from(toolTypeMap.entries()).map(([tool8, tokens]) => ({ tool: tool8, tokens })).sort((a, b) => b.tokens - a.tokens).slice(0, 3);
9169
+ const topTypes = Array.from(toolTypeMap.entries()).map(([tool7, tokens]) => ({ tool: tool7, tokens })).sort((a, b) => b.tokens - a.tokens).slice(0, 3);
9646
9170
  if (topTypes.length > 0) {
9647
9171
  lines.push(
9648
9172
  ` Top tools: ${topTypes.map((t) => `${t.tool} (${pct(t.tokens, total)}%)`).join(", ")}`
@@ -9967,69 +9491,6 @@ Call with blockId to get the full summary: acp_context_recap({ blockId: N })`);
9967
9491
  });
9968
9492
  }
9969
9493
 
9970
- // lib/compress/prune-tool.ts
9971
- import { tool as tool7 } from "@opencode-ai/plugin";
9972
- var PRUNE_TOOL_DESCRIPTION = `Remove old tool outputs by tool type \u2014 frees context without compression.
9973
-
9974
- Unlike compress (which creates summaries), prune directly strips tool call outputs from context. Use for disposable tool outputs where the content is no longer needed: old todowrite states, edit success echoes, repeated status checks.
9975
-
9976
- Args:
9977
- - toolType: tool name to prune (e.g., "todowrite", "bash", "edit")
9978
- - keepLatest: how many recent calls to keep visible (default 3)`;
9979
- function createPruneTool(factoryCtx) {
9980
- factoryCtx.prompts.reload();
9981
- return tool7({
9982
- description: PRUNE_TOOL_DESCRIPTION,
9983
- args: {
9984
- toolType: tool7.schema.string().describe('Tool name to prune (e.g., "todowrite", "bash", "edit")'),
9985
- keepLatest: tool7.schema.number().optional().describe("How many recent calls to keep visible (default 3)")
9986
- },
9987
- async execute(args, toolCtx) {
9988
- const ctx = resolveToolContext(factoryCtx, toolCtx.sessionID);
9989
- const keepLatest = args.keepLatest ?? 3;
9990
- const { rawMessages } = await prepareSession(
9991
- ctx,
9992
- toolCtx,
9993
- `Prune: ${args.toolType}`
9994
- );
9995
- const matchingCalls = [];
9996
- for (let i = 0; i < rawMessages.length; i++) {
9997
- const msg = rawMessages[i];
9998
- if (!msg) continue;
9999
- for (const part of msg.parts || []) {
10000
- if (part.type !== "tool") continue;
10001
- const partTool = part?.tool || "";
10002
- if (partTool !== args.toolType) continue;
10003
- const callId = part?.callID;
10004
- if (!callId || typeof callId !== "string") continue;
10005
- if (ctx.state.prune.tools.has(callId)) continue;
10006
- const tokens = Math.round(JSON.stringify(part).length / 4);
10007
- matchingCalls.push({ callId, index: i, tokens });
10008
- }
10009
- }
10010
- if (matchingCalls.length <= keepLatest) {
10011
- return `Nothing to prune \u2014 only ${matchingCalls.length} ${args.toolType} calls visible (keepLatest=${keepLatest}).`;
10012
- }
10013
- matchingCalls.sort((a, b) => a.index - b.index);
10014
- const toPrune = matchingCalls.slice(0, matchingCalls.length - keepLatest);
10015
- let totalTokens = 0;
10016
- for (const item of toPrune) {
10017
- ctx.state.prune.tools.set(item.callId, item.tokens);
10018
- totalTokens += item.tokens;
10019
- }
10020
- await finalizeSession(
10021
- ctx,
10022
- toolCtx,
10023
- rawMessages,
10024
- [],
10025
- `Prune ${args.toolType}`
10026
- );
10027
- return `Pruned ${toPrune.length} ${args.toolType} calls (~${totalTokens} tokens). Kept latest ${keepLatest}. Outputs will be stripped on next context refresh.
10028
- IMPORTANT: This was an automatic context pruning. You MUST continue your previous task exactly where you left off. Do NOT ask the user what to do next.`;
10029
- }
10030
- });
10031
- }
10032
-
10033
9494
  // lib/compress/hide-consumed.ts
10034
9495
  function hideConsumedCompressCalls(state, messages) {
10035
9496
  if (state.prune.messages.blocksById.size === 0) {
@@ -10082,7 +9543,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
10082
9543
  import { join as join3 } from "path";
10083
9544
  import { existsSync as existsSync3 } from "fs";
10084
9545
  import { homedir as homedir3 } from "os";
10085
- var LOG_VERSION = true ? "1.14.4" : "dev";
9546
+ var LOG_VERSION = true ? "1.14.5" : "dev";
10086
9547
  var Logger = class {
10087
9548
  logDir;
10088
9549
  enabled;
@@ -10312,7 +9773,6 @@ You have five context-management tools:
10312
9773
  - \`compress\` \u2014 Replace a contiguous range of older conversation with a single detailed summary you write. Use when content is genuinely consumed (no longer needed for the current task step). Single range: \`compress({ topic: "API exploration", content: [{ startId: "m00150", endId: "m00220", summary: "..." }] })\`. Batch (multiple unrelated ranges, each with its own topic): \`compress({ content: [{ topic: "Auth", startId: "m00150", endId: "m00220", summary: "..." }, { topic: "Deploy", startId: "m00300", endId: "m00350", summary: "..." }] })\`.
10313
9774
  - \`decompress\` \u2014 Restore a previously compressed block's content. By default restores one tier up (T2\u2192T1 summaries, not raw messages). Use \`full: true\` to restore all the way to original messages. Use \`toFile\` to write to file instead of inflating context. Example: \`decompress({ blockId: "b5" })\` or \`decompress({ blockId: "b5", toFile: "path" })\` or \`decompress({ blockId: "b5", full: true })\`.
10314
9775
  - \`search_context\` \u2014 Search compressed block summaries (and optionally visible messages) by keyword. Use BEFORE decompressing to find the right block. Example: \`search_context({ query: "auth token refresh" })\`.
10315
- - \`prune\` \u2014 Remove old tool outputs by tool type, keeping only recent calls. Unlike compress (which creates summaries), prune directly strips outputs. Use for disposable outputs like old todowrite states or edit echoes. Example: \`prune({ toolType: "todowrite", keepLatest: 3 })\`.
10316
9776
  - \`acp_status\` \u2014 Context status with compressible ranges. No args = overview + ranges. \`scope:"uncompressed"\` for range view; add \`view:"messages"\` for per-message listing with \`tool\`/\`sort\` filters. \`scope:"compressed"\` for block details.
10317
9777
 
10318
9778
  COMPRESSION PHILOSOPHY
@@ -10967,6 +10427,73 @@ function renderSystemPrompt(prompts, protectedToolsExtension, manual, subagent)
10967
10427
  return [prompts.system.trim(), ...extensions].filter(Boolean).join("\n\n").replace(/\n([ \t]*\n)+/g, "\n\n").trim();
10968
10428
  }
10969
10429
 
10430
+ // lib/messages/truncate-tools.ts
10431
+ var TRUNCATION_MARKER = "[truncated for context space";
10432
+ var MIN_OUTPUT_TOKENS = 1e3;
10433
+ var KEEP_PREFIX_CHARS = 2e3;
10434
+ var KEEP_SUFFIX_CHARS = 2e3;
10435
+ var PROTECT_RECENT_MESSAGES = 3;
10436
+ function parseGcThreshold(threshold, modelContextLimit) {
10437
+ if (typeof threshold === "number") return threshold;
10438
+ const str = threshold ?? "100%";
10439
+ const match = /^(\d+(?:\.\d+)?)%$/.exec(str);
10440
+ if (match) return Math.round(Number(match[1]) / 100 * modelContextLimit);
10441
+ return modelContextLimit;
10442
+ }
10443
+ function truncateLargeToolOutputs(state, config, logger, messages) {
10444
+ if (!state.modelContextLimit) return;
10445
+ const currentTokens = getCurrentTokenUsage(state, messages);
10446
+ if (currentTokens === 0) return;
10447
+ const threshold = parseGcThreshold(config.gc?.majorGcThresholdPercent, state.modelContextLimit);
10448
+ if (currentTokens < threshold) return;
10449
+ const protectedIndex = messages.length - PROTECT_RECENT_MESSAGES;
10450
+ const candidates = [];
10451
+ for (let mi = 0; mi < messages.length; mi++) {
10452
+ if (mi >= protectedIndex) break;
10453
+ const msg = messages[mi];
10454
+ const parts = Array.isArray(msg.parts) ? msg.parts : [];
10455
+ for (let pi = 0; pi < parts.length; pi++) {
10456
+ const part = parts[pi];
10457
+ if (part?.type !== "tool") continue;
10458
+ if (part.state?.status !== "completed") continue;
10459
+ const content = extractCompletedToolOutput(part);
10460
+ if (content === void 0) continue;
10461
+ if (content === "[Old tool result content cleared]") continue;
10462
+ if (content.includes(TRUNCATION_MARKER)) continue;
10463
+ const tokens = countTokens2(content);
10464
+ if (tokens < MIN_OUTPUT_TOKENS) continue;
10465
+ candidates.push({ part, content, tokens });
10466
+ }
10467
+ }
10468
+ if (candidates.length === 0) return;
10469
+ candidates.sort((a, b) => b.tokens - a.tokens);
10470
+ const targetTokens = threshold * 0.9;
10471
+ let savedTokens = 0;
10472
+ let truncatedCount = 0;
10473
+ for (const { part, content, tokens } of candidates) {
10474
+ if (currentTokens - savedTokens <= targetTokens) break;
10475
+ if (content.length <= KEEP_PREFIX_CHARS + KEEP_SUFFIX_CHARS) continue;
10476
+ const prefix = content.slice(0, KEEP_PREFIX_CHARS);
10477
+ const suffix = content.slice(-KEEP_SUFFIX_CHARS);
10478
+ const truncated = prefix + `
10479
+
10480
+ ...${TRUNCATION_MARKER} \u2014 original ~${tokens} tokens]...
10481
+
10482
+ ` + suffix;
10483
+ part.state.output = truncated;
10484
+ savedTokens += tokens - countTokens2(truncated);
10485
+ truncatedCount++;
10486
+ }
10487
+ if (truncatedCount > 0) {
10488
+ logger.info("Emergency tool output truncation", {
10489
+ truncatedCount,
10490
+ estimatedSavedTokens: Math.round(savedTokens),
10491
+ currentTokens,
10492
+ threshold
10493
+ });
10494
+ }
10495
+ }
10496
+
10970
10497
  // lib/commands/context.ts
10971
10498
  function analyzeTokens(state, messages) {
10972
10499
  const breakdown = {
@@ -11015,7 +10542,7 @@ function analyzeTokens(state, messages) {
11015
10542
  let foundFirstUser = false;
11016
10543
  const allToolIds = /* @__PURE__ */ new Set();
11017
10544
  const activeToolIds = /* @__PURE__ */ new Set();
11018
- const prunedByMessageToolIds = /* @__PURE__ */ new Set();
10545
+ const prunedToolIds = /* @__PURE__ */ new Set();
11019
10546
  const allMessageIds = /* @__PURE__ */ new Set();
11020
10547
  for (const msg of messages) {
11021
10548
  allMessageIds.add(msg.info.id);
@@ -11033,11 +10560,10 @@ function analyzeTokens(state, messages) {
11033
10560
  activeToolIds.add(toolPart.callID);
11034
10561
  }
11035
10562
  if (isMessagePruned) {
11036
- prunedByMessageToolIds.add(toolPart.callID);
10563
+ prunedToolIds.add(toolPart.callID);
11037
10564
  }
11038
10565
  }
11039
- const isPruned = toolPart.callID && state.prune.tools.has(toolPart.callID);
11040
- if (!isCompacted && !isPruned) {
10566
+ if (!isCompacted) {
11041
10567
  if (toolPart.state?.input) {
11042
10568
  const inputStr = typeof toolPart.state.input === "string" ? toolPart.state.input : JSON.stringify(toolPart.state.input);
11043
10569
  toolInputParts.push(inputStr);
@@ -11060,14 +10586,7 @@ function analyzeTokens(state, messages) {
11060
10586
  foundFirstUser = true;
11061
10587
  }
11062
10588
  }
11063
- const prunedByToolIds = /* @__PURE__ */ new Set();
11064
- for (const id of allToolIds) {
11065
- if (state.prune.tools.has(id)) {
11066
- prunedByToolIds.add(id);
11067
- }
11068
- }
11069
- const prunedToolIds = /* @__PURE__ */ new Set([...prunedByToolIds, ...prunedByMessageToolIds]);
11070
- const toolsInContextCount = [...activeToolIds].filter((id) => !prunedByToolIds.has(id)).length;
10589
+ const toolsInContextCount = [...activeToolIds].filter((id) => !prunedToolIds.has(id)).length;
11071
10590
  let prunedMessageCount = 0;
11072
10591
  for (const [id, entry] of state.prune.messages.byMessageId) {
11073
10592
  if (allMessageIds.has(id) && entry.activeBlockIds.length > 0) {
@@ -11291,7 +10810,6 @@ async function handleDecompressCommand(ctx) {
11291
10810
  var BASE_COMMANDS = [
11292
10811
  ["/acp context", "Show token usage breakdown for current session"],
11293
10812
  ["/acp stats", "Show ACP pruning statistics"],
11294
- ["/acp sweep [n]", "Prune tools since last user message, or last n tools"],
11295
10813
  ["/acp manual [on|off]", "Toggle manual mode or set explicit state"]
11296
10814
  ];
11297
10815
  var TOOL_COMMANDS = {
@@ -11343,9 +10861,9 @@ var COMPRESS_TRIGGER_PROMPT = [
11343
10861
  "Follow the active compress mode, preserve all critical implementation details, and choose safe targets.",
11344
10862
  "Return after compress with a brief explanation of what content was compressed."
11345
10863
  ].join("\n\n");
11346
- function getTriggerPrompt(tool8, state, config, userFocus) {
10864
+ function getTriggerPrompt(tool7, state, config, userFocus) {
11347
10865
  const base = COMPRESS_TRIGGER_PROMPT;
11348
- const compressedBlockGuidance = config.compress.mode === "message" ? "" : buildCompressedBlockGuidance(state, config.gc);
10866
+ const compressedBlockGuidance = config.compress.mode === "message" ? "" : buildCompressedBlockGuidance(state);
11349
10867
  const sections = [base, compressedBlockGuidance];
11350
10868
  if (userFocus && userFocus.trim().length > 0) {
11351
10869
  sections.push(`Additional user focus:
@@ -11372,8 +10890,8 @@ async function handleManualToggleCommand(ctx, modeArg) {
11372
10890
  );
11373
10891
  logger.info("Manual mode toggled", { manualMode: state.manualMode });
11374
10892
  }
11375
- async function handleManualTriggerCommand(ctx, tool8, userFocus) {
11376
- return getTriggerPrompt(tool8, ctx.state, ctx.config, userFocus);
10893
+ async function handleManualTriggerCommand(ctx, tool7, userFocus) {
10894
+ return getTriggerPrompt(tool7, ctx.state, ctx.config, userFocus);
11377
10895
  }
11378
10896
  function applyPendingManualTrigger(state, messages, logger) {
11379
10897
  const pending = state.pendingManualTrigger;
@@ -11641,7 +11159,7 @@ async function handleStatsCommand(ctx) {
11641
11159
  (total, target) => total + target.durationMs,
11642
11160
  0
11643
11161
  );
11644
- const prunedToolIds = new Set(state.prune.tools.keys());
11162
+ const prunedToolIds = /* @__PURE__ */ new Set();
11645
11163
  for (const block of state.prune.messages.blocksById.values()) {
11646
11164
  if (block.active) {
11647
11165
  for (const toolId of block.effectiveToolIds) {
@@ -11679,243 +11197,6 @@ async function handleStatsCommand(ctx) {
11679
11197
  });
11680
11198
  }
11681
11199
 
11682
- // lib/commands/sweep.ts
11683
- function findLastUserMessageIndex(messages) {
11684
- for (let i = messages.length - 1; i >= 0; i--) {
11685
- const msg = messages[i];
11686
- if (msg.info.role === "user" && !isIgnoredUserMessage(msg)) {
11687
- return i;
11688
- }
11689
- }
11690
- return -1;
11691
- }
11692
- function collectToolIdsAfterIndex(state, messages, afterIndex) {
11693
- const toolIds = [];
11694
- for (let i = afterIndex + 1; i < messages.length; i++) {
11695
- const msg = messages[i];
11696
- if (isMessageCompacted(state, msg)) {
11697
- continue;
11698
- }
11699
- const parts = Array.isArray(msg.parts) ? msg.parts : [];
11700
- if (parts.length > 0) {
11701
- for (const part of parts) {
11702
- if (part.type === "tool" && part.callID && part.tool) {
11703
- toolIds.push(part.callID);
11704
- }
11705
- }
11706
- }
11707
- }
11708
- return toolIds;
11709
- }
11710
- function formatNoUserMessage() {
11711
- const lines = [];
11712
- lines.push("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E");
11713
- lines.push("\u2502 ACP Sweep \u2502");
11714
- lines.push("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F");
11715
- lines.push("");
11716
- lines.push("Nothing swept: no user message found.");
11717
- return lines.join("\n");
11718
- }
11719
- function formatSweepMessage(toolCount, tokensSaved, mode, toolIds, toolMetadata, workingDirectory, skippedProtected) {
11720
- const lines = [];
11721
- lines.push("\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E");
11722
- lines.push("\u2502 ACP Sweep \u2502");
11723
- lines.push("\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F");
11724
- lines.push("");
11725
- if (toolCount === 0) {
11726
- if (mode === "since-user") {
11727
- lines.push("No tools found since the previous user message.");
11728
- } else {
11729
- lines.push(`No tools found to sweep.`);
11730
- }
11731
- if (skippedProtected && skippedProtected > 0) {
11732
- lines.push(`(${skippedProtected} protected tool(s) skipped)`);
11733
- }
11734
- } else {
11735
- if (mode === "since-user") {
11736
- lines.push(`Swept ${toolCount} tool(s) since the previous user message.`);
11737
- } else {
11738
- lines.push(`Swept the last ${toolCount} tool(s).`);
11739
- }
11740
- lines.push(`Tokens saved: ~${tokensSaved.toLocaleString()}`);
11741
- if (skippedProtected && skippedProtected > 0) {
11742
- lines.push(`(${skippedProtected} protected tool(s) skipped)`);
11743
- }
11744
- lines.push("");
11745
- const itemLines = formatPrunedItemsList(toolIds, toolMetadata, workingDirectory);
11746
- lines.push(...itemLines);
11747
- }
11748
- return lines.join("\n");
11749
- }
11750
- async function handleSweepCommand(ctx) {
11751
- const { client, state, config, logger, sessionId, messages, args, workingDirectory } = ctx;
11752
- const params = getCurrentParams(state, messages, logger);
11753
- const protectedTools = config.commands.protectedTools;
11754
- syncToolCache(state, config, logger, messages);
11755
- buildToolIdList(state, messages);
11756
- const numArg = args[0] ? parseInt(args[0], 10) : null;
11757
- const isLastNMode = numArg !== null && !isNaN(numArg) && numArg > 0;
11758
- let toolIdsToSweep;
11759
- let mode;
11760
- if (isLastNMode) {
11761
- mode = "last-n";
11762
- const startIndex = Math.max(0, state.toolIdList.length - numArg);
11763
- toolIdsToSweep = state.toolIdList.slice(startIndex);
11764
- logger.info(`Sweep command: last ${numArg} mode, found ${toolIdsToSweep.length} tools`);
11765
- } else {
11766
- mode = "since-user";
11767
- const lastUserMsgIndex = findLastUserMessageIndex(messages);
11768
- if (lastUserMsgIndex === -1) {
11769
- const message2 = formatNoUserMessage();
11770
- await sendIgnoredMessage(client, sessionId, message2, params, logger);
11771
- logger.info("Sweep command: no user message found");
11772
- return;
11773
- } else {
11774
- toolIdsToSweep = collectToolIdsAfterIndex(state, messages, lastUserMsgIndex);
11775
- logger.info(
11776
- `Sweep command: found last user at index ${lastUserMsgIndex}, sweeping ${toolIdsToSweep.length} tools`
11777
- );
11778
- }
11779
- }
11780
- const newToolIds = toolIdsToSweep.filter((id) => {
11781
- if (state.prune.tools.has(id)) {
11782
- return false;
11783
- }
11784
- const entry = state.toolParameters.get(id);
11785
- if (!entry) {
11786
- return true;
11787
- }
11788
- if (isToolNameProtected(entry.tool, protectedTools)) {
11789
- logger.debug(`Sweep: skipping protected tool ${entry.tool} (${id})`);
11790
- return false;
11791
- }
11792
- const filePaths = getFilePathsFromParameters(entry.tool, entry.parameters);
11793
- if (isFilePathProtected(filePaths, config.protectedFilePatterns)) {
11794
- logger.debug(`Sweep: skipping protected file path(s) ${filePaths.join(", ")} (${id})`);
11795
- return false;
11796
- }
11797
- return true;
11798
- });
11799
- const skippedProtected = toolIdsToSweep.filter((id) => {
11800
- const entry = state.toolParameters.get(id);
11801
- if (!entry) {
11802
- return false;
11803
- }
11804
- if (isToolNameProtected(entry.tool, protectedTools)) {
11805
- return true;
11806
- }
11807
- const filePaths = getFilePathsFromParameters(entry.tool, entry.parameters);
11808
- if (isFilePathProtected(filePaths, config.protectedFilePatterns)) {
11809
- return true;
11810
- }
11811
- return false;
11812
- }).length;
11813
- if (newToolIds.length === 0) {
11814
- const message2 = formatSweepMessage(
11815
- 0,
11816
- 0,
11817
- mode,
11818
- [],
11819
- /* @__PURE__ */ new Map(),
11820
- workingDirectory,
11821
- skippedProtected
11822
- );
11823
- await sendIgnoredMessage(client, sessionId, message2, params, logger);
11824
- logger.info("Sweep command: no new tools to sweep", { skippedProtected });
11825
- return;
11826
- }
11827
- const tokensSaved = getTotalToolTokens(state, newToolIds);
11828
- for (const id of newToolIds) {
11829
- const entry = state.toolParameters.get(id);
11830
- state.prune.tools.set(id, entry?.tokenCount ?? 0);
11831
- }
11832
- state.stats.pruneTokenCounter += tokensSaved;
11833
- state.stats.totalPruneTokens += state.stats.pruneTokenCounter;
11834
- state.stats.pruneTokenCounter = 0;
11835
- const toolMetadata = /* @__PURE__ */ new Map();
11836
- for (const id of newToolIds) {
11837
- const entry = state.toolParameters.get(id);
11838
- if (entry) {
11839
- toolMetadata.set(id, entry);
11840
- }
11841
- }
11842
- saveSessionState(state, logger).catch(
11843
- (err) => logger.error("Failed to persist state after sweep", { error: err.message })
11844
- );
11845
- const message = formatSweepMessage(
11846
- newToolIds.length,
11847
- tokensSaved,
11848
- mode,
11849
- newToolIds,
11850
- toolMetadata,
11851
- workingDirectory,
11852
- skippedProtected
11853
- );
11854
- await sendIgnoredMessage(client, sessionId, message, params, logger);
11855
- logger.info("Sweep command completed", {
11856
- toolsSwept: newToolIds.length,
11857
- tokensSaved,
11858
- skippedProtected,
11859
- mode,
11860
- tools: Array.from(toolMetadata.entries()).map(([id, entry]) => ({
11861
- id,
11862
- tool: entry.tool
11863
- }))
11864
- });
11865
- }
11866
-
11867
- // lib/gc/truncate.ts
11868
- function runTruncateGC(blocks, params) {
11869
- let compactedBlocks = 0;
11870
- let savedTokens = 0;
11871
- for (const block of blocks) {
11872
- if (!block.active) continue;
11873
- if (block.summary.length <= params.maxOldGenSummaryLength) continue;
11874
- const originalLength = block.summary.length;
11875
- const truncated = truncateSummary(block.summary, params.maxOldGenSummaryLength, block.blockId);
11876
- const savedChars = originalLength - truncated.length;
11877
- if (savedChars > 0) {
11878
- block.summary = truncated;
11879
- block.summaryTokens = Math.round(truncated.length / 4);
11880
- compactedBlocks++;
11881
- savedTokens += Math.round(savedChars / 4);
11882
- }
11883
- }
11884
- return { compactedBlocks, savedTokens };
11885
- }
11886
- function truncateSummary(summary, maxLength, _blockId) {
11887
- if (summary.length <= maxLength) return summary;
11888
- const headerEnd = summary.indexOf("\n");
11889
- if (headerEnd === -1) return summary.slice(0, maxLength) + "\n...\n[GC truncated]";
11890
- const header = summary.slice(0, headerEnd + 1);
11891
- const footerStart = summary.lastIndexOf("\n\n");
11892
- const footer = footerStart > headerEnd ? summary.slice(footerStart) : "";
11893
- const availableForContent = maxLength - header.length - footer.length - 20;
11894
- if (availableForContent < 100) {
11895
- return header + "...\n[GC truncated]" + footer;
11896
- }
11897
- const content = summary.slice(headerEnd + 1, headerEnd + 1 + availableForContent);
11898
- return header + content + "\n...\n[GC truncated]" + footer;
11899
- }
11900
- function shouldRunMajorGC(currentTokens, modelContextLimit, gcConfig) {
11901
- if (!modelContextLimit || modelContextLimit === 0) return false;
11902
- const threshold = parseGcThreshold(gcConfig.majorGcThresholdPercent, modelContextLimit);
11903
- return currentTokens >= threshold;
11904
- }
11905
- function getGCParams(gcConfig, modelContextLimit, currentTokens) {
11906
- return {
11907
- maxOldGenSummaryLength: gcConfig.maxOldGenSummaryLength,
11908
- modelContextLimit,
11909
- currentTokens
11910
- };
11911
- }
11912
- function parseGcThreshold(limit, modelContextLimit) {
11913
- if (typeof limit === "number") return limit;
11914
- const percent = parseFloat(limit.slice(0, -1));
11915
- if (isNaN(percent)) return modelContextLimit;
11916
- return Math.round(Math.max(0, Math.min(100, Math.round(percent))) / 100 * modelContextLimit);
11917
- }
11918
-
11919
11200
  // lib/gc/merge.ts
11920
11201
  function collectActiveOldGenBlocks(state, maxOldGenSummaryLength) {
11921
11202
  const blocks = [];
@@ -12136,31 +11417,6 @@ function createSystemPromptHandler(registry3, logger, config, prompts) {
12136
11417
  }
12137
11418
  };
12138
11419
  }
12139
- function runMajorGC(state, config, logger, messages) {
12140
- if (!state.modelContextLimit) return;
12141
- const currentTokens = getCurrentTokenUsage(state, messages);
12142
- if (!shouldRunMajorGC(currentTokens, state.modelContextLimit, config.gc)) return;
12143
- const oldBlocks = [];
12144
- for (const [blockId, block] of state.prune.messages.blocksById) {
12145
- if (!block.active) continue;
12146
- if (block.generation === "old" || block.generation === void 0 || block.summary.length > config.gc.maxOldGenSummaryLength) {
12147
- oldBlocks.push(block);
12148
- }
12149
- }
12150
- if (oldBlocks.length === 0) return;
12151
- const params = getGCParams(config.gc, state.modelContextLimit, currentTokens);
12152
- const result = runTruncateGC(oldBlocks, params);
12153
- if (result.compactedBlocks > 0) {
12154
- logger.info("Major GC: truncated old-gen blocks", {
12155
- compactedBlocks: result.compactedBlocks,
12156
- savedTokens: result.savedTokens,
12157
- currentTokens,
12158
- threshold: config.gc.majorGcThresholdPercent
12159
- });
12160
- saveSessionState(state, logger).catch(() => {
12161
- });
12162
- }
12163
- }
12164
11420
  function createChatMessageTransformHandler(client, registry3, logger, config, prompts, hostPermissions) {
12165
11421
  return async (input, output) => {
12166
11422
  const receivedMessages = Array.isArray(output.messages) ? output.messages.length : 0;
@@ -12204,7 +11460,6 @@ function createChatMessageTransformHandler(client, registry3, logger, config, pr
12204
11460
  }
12205
11461
  syncToolCache(state, config, logger, output.messages);
12206
11462
  buildToolIdList(state, output.messages);
12207
- runMajorGC(state, config, logger, output.messages);
12208
11463
  const batchResult = runBatchCleanup(state, config, logger, output.messages);
12209
11464
  if (batchResult.mergedCount > 0) {
12210
11465
  saveSessionState(state, logger).catch(() => {
@@ -12212,6 +11467,7 @@ function createChatMessageTransformHandler(client, registry3, logger, config, pr
12212
11467
  }
12213
11468
  const prePruneTokens = getCurrentTokenUsage(state, output.messages);
12214
11469
  prune(state, logger, config, output.messages);
11470
+ truncateLargeToolOutputs(state, config, logger, output.messages);
12215
11471
  hideConsumedCompressCalls(state, output.messages);
12216
11472
  assignMessageRefs(state, output.messages);
12217
11473
  const compressionPriorities = buildPriorityMap(config, state, output.messages);
@@ -12288,14 +11544,6 @@ function createCommandExecuteHandler(client, registry3, logger, config, workingD
12288
11544
  await handleStatsCommand(commandCtx);
12289
11545
  throw new Error("__DCP_STATS_HANDLED__");
12290
11546
  }
12291
- if (subcommand === "sweep") {
12292
- await handleSweepCommand({
12293
- ...commandCtx,
12294
- args: subArgs,
12295
- workingDirectory
12296
- });
12297
- throw new Error("__DCP_SWEEP_HANDLED__");
12298
- }
12299
11547
  if (subcommand === "manual") {
12300
11548
  await handleManualToggleCommand(commandCtx, subArgs[0]?.toLowerCase());
12301
11549
  throw new Error("__DCP_MANUAL_HANDLED__");
@@ -12601,9 +11849,7 @@ var server = (async (ctx) => {
12601
11849
  if (isSecureMode()) {
12602
11850
  configureClientAuth(ctx.client);
12603
11851
  }
12604
- logger.info("DCP initialized", {
12605
- strategies: config.strategies
12606
- });
11852
+ logger.info("DCP initialized");
12607
11853
  startAutoUpdate(ctx, config.autoUpdate);
12608
11854
  const compressToolContext = {
12609
11855
  client: ctx.client,
@@ -12641,7 +11887,6 @@ var server = (async (ctx) => {
12641
11887
  ...config.compress.permission !== "deny" && {
12642
11888
  compress: config.compress.mode === "message" ? createCompressMessageTool(compressToolContext) : createCompressRangeTool(compressToolContext),
12643
11889
  decompress: createDecompressTool(compressToolContext),
12644
- prune: createPruneTool(compressToolContext),
12645
11890
  search_context: createSearchContextTool(compressToolContext),
12646
11891
  acp_status: createAcpStatusTool(compressToolContext),
12647
11892
  acp_context_recap: createAcpContextRecapTool(compressToolContext)