@lexwdex-org/opencode-dcp 3.4.8 → 3.4.10

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 (82) hide show
  1. package/README.en.md +2 -2
  2. package/README.md +8 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +870 -357
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/commands/context.d.ts +2 -1
  7. package/dist/lib/commands/context.d.ts.map +1 -1
  8. package/dist/lib/commands/decompress.d.ts +2 -1
  9. package/dist/lib/commands/decompress.d.ts.map +1 -1
  10. package/dist/lib/commands/help.d.ts +2 -1
  11. package/dist/lib/commands/help.d.ts.map +1 -1
  12. package/dist/lib/commands/manual.d.ts +2 -1
  13. package/dist/lib/commands/manual.d.ts.map +1 -1
  14. package/dist/lib/commands/recompress.d.ts +2 -1
  15. package/dist/lib/commands/recompress.d.ts.map +1 -1
  16. package/dist/lib/commands/stats.d.ts +2 -1
  17. package/dist/lib/commands/stats.d.ts.map +1 -1
  18. package/dist/lib/commands/sweep.d.ts +2 -1
  19. package/dist/lib/commands/sweep.d.ts.map +1 -1
  20. package/dist/lib/compress/external-content.d.ts +4 -0
  21. package/dist/lib/compress/external-content.d.ts.map +1 -0
  22. package/dist/lib/compress/external-inference.d.ts.map +1 -1
  23. package/dist/lib/compress/message.d.ts.map +1 -1
  24. package/dist/lib/compress/pipeline.d.ts +3 -2
  25. package/dist/lib/compress/pipeline.d.ts.map +1 -1
  26. package/dist/lib/compress/protected-content.d.ts +2 -1
  27. package/dist/lib/compress/protected-content.d.ts.map +1 -1
  28. package/dist/lib/compress/range.d.ts.map +1 -1
  29. package/dist/lib/compress/search.d.ts +2 -1
  30. package/dist/lib/compress/search.d.ts.map +1 -1
  31. package/dist/lib/compress/timing.d.ts +2 -0
  32. package/dist/lib/compress/timing.d.ts.map +1 -1
  33. package/dist/lib/compress/types.d.ts +4 -3
  34. package/dist/lib/compress/types.d.ts.map +1 -1
  35. package/dist/lib/compress-permission.d.ts.map +1 -1
  36. package/dist/lib/concurrency.d.ts +2 -0
  37. package/dist/lib/concurrency.d.ts.map +1 -0
  38. package/dist/lib/config.d.ts +1 -0
  39. package/dist/lib/config.d.ts.map +1 -1
  40. package/dist/lib/hooks.d.ts +6 -5
  41. package/dist/lib/hooks.d.ts.map +1 -1
  42. package/dist/lib/messages/inject/inject.d.ts.map +1 -1
  43. package/dist/lib/messages/inject/subagent-results.d.ts +2 -1
  44. package/dist/lib/messages/inject/subagent-results.d.ts.map +1 -1
  45. package/dist/lib/messages/inject/utils.d.ts +2 -2
  46. package/dist/lib/messages/inject/utils.d.ts.map +1 -1
  47. package/dist/lib/messages/utils.d.ts.map +1 -1
  48. package/dist/lib/opencode-client.d.ts +3 -0
  49. package/dist/lib/opencode-client.d.ts.map +1 -0
  50. package/dist/lib/prompts/context-limit-nudge.d.ts +1 -1
  51. package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -1
  52. package/dist/lib/prompts/iteration-nudge.d.ts +1 -1
  53. package/dist/lib/prompts/iteration-nudge.d.ts.map +1 -1
  54. package/dist/lib/prompts/store.d.ts +1 -0
  55. package/dist/lib/prompts/store.d.ts.map +1 -1
  56. package/dist/lib/prompts/system.d.ts +1 -1
  57. package/dist/lib/prompts/system.d.ts.map +1 -1
  58. package/dist/lib/prompts/turn-nudge.d.ts +1 -1
  59. package/dist/lib/prompts/turn-nudge.d.ts.map +1 -1
  60. package/dist/lib/state/index.d.ts +1 -0
  61. package/dist/lib/state/index.d.ts.map +1 -1
  62. package/dist/lib/state/persistence.d.ts +1 -0
  63. package/dist/lib/state/persistence.d.ts.map +1 -1
  64. package/dist/lib/state/state.d.ts +5 -2
  65. package/dist/lib/state/state.d.ts.map +1 -1
  66. package/dist/lib/state/store.d.ts +38 -0
  67. package/dist/lib/state/store.d.ts.map +1 -0
  68. package/dist/lib/state/tool-cache.d.ts.map +1 -1
  69. package/dist/lib/state/types.d.ts +2 -1
  70. package/dist/lib/state/types.d.ts.map +1 -1
  71. package/dist/lib/state/utils.d.ts +4 -1
  72. package/dist/lib/state/utils.d.ts.map +1 -1
  73. package/dist/lib/strategies/deduplication.d.ts.map +1 -1
  74. package/dist/lib/token-utils.d.ts +0 -1
  75. package/dist/lib/token-utils.d.ts.map +1 -1
  76. package/dist/lib/ui/notification.d.ts +4 -3
  77. package/dist/lib/ui/notification.d.ts.map +1 -1
  78. package/dist/lib/update.d.ts +3 -9
  79. package/dist/lib/update.d.ts.map +1 -1
  80. package/package.json +2 -2
  81. package/dist/lib/auth.d.ts +0 -4
  82. package/dist/lib/auth.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -912,7 +912,6 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
912
912
  "enabled",
913
913
  "autoUpdate",
914
914
  "debug",
915
- "showUpdateToasts",
916
915
  "pruneNotification",
917
916
  "pruneNotificationType",
918
917
  "turnProtection",
@@ -935,6 +934,7 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
935
934
  "compress.summaryBuffer",
936
935
  "compress.maxContextLimit",
937
936
  "compress.minContextLimit",
937
+ "compress.boundaryNudge",
938
938
  "compress.modelMaxLimits",
939
939
  "compress.modelMinLimits",
940
940
  "compress.nudgeFrequency",
@@ -944,6 +944,11 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
944
944
  "compress.protectTags",
945
945
  "compress.protectUserMessages",
946
946
  "compress.externalModel",
947
+ "compress.externalModel.url",
948
+ "compress.externalModel.model",
949
+ "compress.externalModel.apiKey",
950
+ "compress.externalModel.timeout",
951
+ "compress.externalModel.retries",
947
952
  "strategies",
948
953
  "strategies.deduplication",
949
954
  "strategies.deduplication.enabled",
@@ -1138,6 +1143,13 @@ function validateConfigTypes(config) {
1138
1143
  actual: typeof compress.summaryBuffer
1139
1144
  });
1140
1145
  }
1146
+ if (compress.boundaryNudge !== void 0 && typeof compress.boundaryNudge !== "boolean") {
1147
+ errors.push({
1148
+ key: "compress.boundaryNudge",
1149
+ expected: "boolean",
1150
+ actual: typeof compress.boundaryNudge
1151
+ });
1152
+ }
1141
1153
  if (compress.nudgeFrequency !== void 0 && typeof compress.nudgeFrequency !== "number") {
1142
1154
  errors.push({
1143
1155
  key: "compress.nudgeFrequency",
@@ -1259,6 +1271,12 @@ function validateConfigTypes(config) {
1259
1271
  expected: "string",
1260
1272
  actual: typeof compress.externalModel.url
1261
1273
  });
1274
+ } else if (compress.externalModel.url.trim().length === 0) {
1275
+ errors.push({
1276
+ key: "compress.externalModel.url",
1277
+ expected: "non-empty string",
1278
+ actual: "empty string"
1279
+ });
1262
1280
  }
1263
1281
  if (typeof compress.externalModel.model !== "string") {
1264
1282
  errors.push({
@@ -1266,6 +1284,33 @@ function validateConfigTypes(config) {
1266
1284
  expected: "string",
1267
1285
  actual: typeof compress.externalModel.model
1268
1286
  });
1287
+ } else if (compress.externalModel.model.trim().length === 0) {
1288
+ errors.push({
1289
+ key: "compress.externalModel.model",
1290
+ expected: "non-empty string",
1291
+ actual: "empty string"
1292
+ });
1293
+ }
1294
+ if (compress.externalModel.apiKey !== void 0 && typeof compress.externalModel.apiKey !== "string") {
1295
+ errors.push({
1296
+ key: "compress.externalModel.apiKey",
1297
+ expected: "string",
1298
+ actual: typeof compress.externalModel.apiKey
1299
+ });
1300
+ }
1301
+ if (compress.externalModel.timeout !== void 0 && (typeof compress.externalModel.timeout !== "number" || !Number.isFinite(compress.externalModel.timeout) || compress.externalModel.timeout <= 0)) {
1302
+ errors.push({
1303
+ key: "compress.externalModel.timeout",
1304
+ expected: "positive finite number",
1305
+ actual: JSON.stringify(compress.externalModel.timeout)
1306
+ });
1307
+ }
1308
+ if (compress.externalModel.retries !== void 0 && (!Number.isInteger(compress.externalModel.retries) || compress.externalModel.retries < 0)) {
1309
+ errors.push({
1310
+ key: "compress.externalModel.retries",
1311
+ expected: "non-negative integer",
1312
+ actual: JSON.stringify(compress.externalModel.retries)
1313
+ });
1269
1314
  }
1270
1315
  }
1271
1316
  }
@@ -1393,6 +1438,7 @@ var defaultConfig = {
1393
1438
  summaryBuffer: true,
1394
1439
  maxContextLimit: "85%",
1395
1440
  minContextLimit: "50%",
1441
+ boundaryNudge: true,
1396
1442
  nudgeFrequency: 2,
1397
1443
  iterationNudgeThreshold: 15,
1398
1444
  nudgeForce: "strong",
@@ -1515,6 +1561,7 @@ function mergeCompress(base, override) {
1515
1561
  summaryBuffer: override.summaryBuffer ?? base.summaryBuffer,
1516
1562
  maxContextLimit: override.maxContextLimit ?? base.maxContextLimit,
1517
1563
  minContextLimit: override.minContextLimit ?? base.minContextLimit,
1564
+ boundaryNudge: override.boundaryNudge ?? base.boundaryNudge,
1518
1565
  modelMaxLimits: override.modelMaxLimits ?? base.modelMaxLimits,
1519
1566
  modelMinLimits: override.modelMinLimits ?? base.modelMinLimits,
1520
1567
  nudgeFrequency: override.nudgeFrequency ?? base.nudgeFrequency,
@@ -1746,6 +1793,8 @@ function isProtectedUserMessage(config, message) {
1746
1793
  // lib/token-utils.ts
1747
1794
  var anthropicCountTokens = _anthropicTokenizer.countTokens ?? _anthropicTokenizer.default?.countTokens;
1748
1795
  function getCurrentTokenUsage(state, messages) {
1796
+ let reportedTokens = 0;
1797
+ let lastReportedIndex = -1;
1749
1798
  for (let i = messages.length - 1; i >= 0; i--) {
1750
1799
  const msg = messages[i];
1751
1800
  if (msg.info.role !== "assistant") {
@@ -1756,16 +1805,23 @@ function getCurrentTokenUsage(state, messages) {
1756
1805
  continue;
1757
1806
  }
1758
1807
  if (state.lastCompaction > 0 && (msg.info.time.created < state.lastCompaction || msg.info.summary === true && msg.info.time.created === state.lastCompaction)) {
1759
- return 0;
1808
+ lastReportedIndex = i;
1809
+ break;
1760
1810
  }
1761
1811
  const input = assistantInfo.tokens?.input || 0;
1762
1812
  const output = assistantInfo.tokens?.output || 0;
1763
1813
  const reasoning = assistantInfo.tokens?.reasoning || 0;
1764
1814
  const cacheRead = assistantInfo.tokens?.cache?.read || 0;
1765
1815
  const cacheWrite = assistantInfo.tokens?.cache?.write || 0;
1766
- return input + output + reasoning + cacheRead + cacheWrite;
1816
+ reportedTokens = input + output + reasoning + cacheRead + cacheWrite;
1817
+ lastReportedIndex = i;
1818
+ break;
1767
1819
  }
1768
- return 0;
1820
+ let unreportedTokens = 0;
1821
+ for (let i = lastReportedIndex + 1; i < messages.length; i++) {
1822
+ unreportedTokens += countAllMessageTokens(messages[i]);
1823
+ }
1824
+ return reportedTokens + unreportedTokens;
1769
1825
  }
1770
1826
  function getCurrentParams(state, messages, logger) {
1771
1827
  const userMsg = getLastUserMessage(messages);
@@ -1774,16 +1830,14 @@ function getCurrentParams(state, messages, logger) {
1774
1830
  return {
1775
1831
  providerId: void 0,
1776
1832
  modelId: void 0,
1777
- agent: void 0,
1778
- variant: void 0
1833
+ agent: void 0
1779
1834
  };
1780
1835
  }
1781
1836
  const userInfo = userMsg.info;
1782
1837
  const agent = userInfo.agent;
1783
1838
  const providerId = userInfo.model.providerID;
1784
1839
  const modelId = userInfo.model.modelID;
1785
- const variant = userInfo.model.variant;
1786
- return { providerId, modelId, agent, variant };
1840
+ return { providerId, modelId, agent };
1787
1841
  }
1788
1842
  function countTokens2(text) {
1789
1843
  if (!text) return 0;
@@ -1828,7 +1882,8 @@ function extractToolContent(part) {
1828
1882
  }
1829
1883
  function countToolTokens(part) {
1830
1884
  const contents = extractToolContent(part);
1831
- return estimateTokensBatch(contents);
1885
+ if (contents.length === 0) return 0;
1886
+ return Math.ceil(Buffer.byteLength(contents.join(" "), "utf-8") / 4);
1832
1887
  }
1833
1888
  function getTotalToolTokens(state, toolIds) {
1834
1889
  let total = 0;
@@ -1910,7 +1965,7 @@ var MESSAGE_REF_MAX_INDEX = 9999;
1910
1965
  function formatMessageRef(index) {
1911
1966
  if (!Number.isInteger(index) || index < MESSAGE_REF_MIN_INDEX || index > MESSAGE_REF_MAX_INDEX) {
1912
1967
  throw new Error(
1913
- `Message ID index out of bounds: ${index}. Supported range is 0-${MESSAGE_REF_MAX_INDEX}.`
1968
+ `Message ID index out of bounds: ${index}. Supported range is ${MESSAGE_REF_MIN_INDEX}-${MESSAGE_REF_MAX_INDEX}.`
1914
1969
  );
1915
1970
  }
1916
1971
  return `m${index.toString().padStart(MESSAGE_REF_WIDTH, "0")}`;
@@ -2011,14 +2066,16 @@ function assignMessageRefs(state, messages) {
2011
2066
  return assigned;
2012
2067
  }
2013
2068
  function allocateNextMessageRef(state) {
2014
- let candidate = Number.isInteger(state.messageIds.nextRef) ? Math.max(MESSAGE_REF_MIN_INDEX, state.messageIds.nextRef) : MESSAGE_REF_MIN_INDEX;
2015
- while (candidate <= MESSAGE_REF_MAX_INDEX) {
2069
+ const capacity = MESSAGE_REF_MAX_INDEX - MESSAGE_REF_MIN_INDEX + 1;
2070
+ const requested = Number.isInteger(state.messageIds.nextRef) ? state.messageIds.nextRef : MESSAGE_REF_MIN_INDEX;
2071
+ const start = requested >= MESSAGE_REF_MIN_INDEX && requested <= MESSAGE_REF_MAX_INDEX ? requested : MESSAGE_REF_MIN_INDEX;
2072
+ for (let offset = 0; offset < capacity; offset++) {
2073
+ const candidate = MESSAGE_REF_MIN_INDEX + (start - MESSAGE_REF_MIN_INDEX + offset) % capacity;
2016
2074
  const ref = formatMessageRef(candidate);
2017
2075
  if (!state.messageIds.byRef.has(ref)) {
2018
- state.messageIds.nextRef = candidate + 1;
2076
+ state.messageIds.nextRef = candidate === MESSAGE_REF_MAX_INDEX ? MESSAGE_REF_MIN_INDEX : candidate + 1;
2019
2077
  return ref;
2020
2078
  }
2021
- candidate++;
2022
2079
  }
2023
2080
  throw new Error(
2024
2081
  `Message ID alias capacity exceeded. Cannot allocate more than ${formatMessageRef(MESSAGE_REF_MAX_INDEX)} aliases in this session.`
@@ -2694,6 +2751,7 @@ function resolveMessage(entry, searchContext, state, config) {
2694
2751
  // lib/state/persistence.ts
2695
2752
  import * as fs from "fs/promises";
2696
2753
  import { existsSync as existsSync2 } from "fs";
2754
+ import { randomUUID } from "crypto";
2697
2755
  import { homedir as homedir2 } from "os";
2698
2756
  import { join as join2 } from "path";
2699
2757
 
@@ -2932,78 +2990,161 @@ function getActiveSummaryTokenUsage(state) {
2932
2990
  }
2933
2991
  return total;
2934
2992
  }
2993
+ var MAX_SUBAGENT_CACHE_ENTRIES = 64;
2994
+ var MAX_SUBAGENT_CACHE_CHARS = 5e5;
2995
+ var MAX_SUBAGENT_CACHE_TOTAL_CHARS = 2e6;
2996
+ function getCachedSubAgentResult(state, callId) {
2997
+ const cached = state.subAgentResultCache.get(callId);
2998
+ if (cached === void 0) return void 0;
2999
+ state.subAgentResultCache.delete(callId);
3000
+ state.subAgentResultCache.set(callId, cached);
3001
+ return cached;
3002
+ }
3003
+ function cacheSubAgentResult(state, callId, result) {
3004
+ const cached = result.length <= MAX_SUBAGENT_CACHE_CHARS ? result : result.slice(0, MAX_SUBAGENT_CACHE_CHARS) + "\n[\u5B50\u4EE3\u7406\u7ED3\u679C\u56E0\u957F\u5EA6\u9650\u5236\u88AB\u622A\u65AD]";
3005
+ state.subAgentResultCache.delete(callId);
3006
+ state.subAgentResultCache.set(callId, cached);
3007
+ let totalChars = Array.from(state.subAgentResultCache.values()).reduce(
3008
+ (total, value) => total + value.length,
3009
+ 0
3010
+ );
3011
+ while (state.subAgentResultCache.size > MAX_SUBAGENT_CACHE_ENTRIES || totalChars > MAX_SUBAGENT_CACHE_TOTAL_CHARS) {
3012
+ const oldest = state.subAgentResultCache.keys().next().value;
3013
+ if (typeof oldest !== "string") break;
3014
+ totalChars -= state.subAgentResultCache.get(oldest)?.length ?? 0;
3015
+ state.subAgentResultCache.delete(oldest);
3016
+ }
3017
+ return cached;
3018
+ }
2935
3019
  function resetOnCompaction(state, messages = []) {
2936
3020
  state.toolParameters.clear();
2937
3021
  if (messages.length === 0) {
2938
3022
  return;
2939
3023
  }
2940
3024
  const liveMessageIds = new Set(messages.map((msg) => msg.info.id));
3025
+ const liveToolIds = new Set(
3026
+ messages.flatMap(
3027
+ (message) => message.parts.flatMap(
3028
+ (part) => part.type === "tool" && part.callID ? [part.callID] : []
3029
+ )
3030
+ )
3031
+ );
2941
3032
  for (const [ref, rawId] of state.messageIds.byRef) {
2942
3033
  if (!liveMessageIds.has(rawId)) {
2943
3034
  state.messageIds.byRef.delete(ref);
2944
3035
  state.messageIds.byRawId.delete(rawId);
2945
3036
  }
2946
3037
  }
3038
+ for (const callId of state.subAgentResultCache.keys()) {
3039
+ if (!liveToolIds.has(callId)) state.subAgentResultCache.delete(callId);
3040
+ }
3041
+ const anchorSets = [
3042
+ state.nudges.contextLimitAnchors,
3043
+ state.nudges.turnNudgeAnchors,
3044
+ state.nudges.iterationNudgeAnchors
3045
+ ];
3046
+ for (const anchorSet of anchorSets) {
3047
+ for (const anchorId of anchorSet) {
3048
+ if (!liveMessageIds.has(anchorId)) {
3049
+ anchorSet.delete(anchorId);
3050
+ }
3051
+ }
3052
+ }
3053
+ }
3054
+
3055
+ // lib/concurrency.ts
3056
+ async function runWithConcurrency(items, concurrency, task) {
3057
+ let nextIndex = 0;
3058
+ const workerCount = Math.min(items.length, Math.max(1, Math.floor(concurrency)));
3059
+ const workers = Array.from({ length: workerCount }, async () => {
3060
+ for (; ; ) {
3061
+ const index = nextIndex++;
3062
+ if (index >= items.length) return;
3063
+ await task(items[index]);
3064
+ }
3065
+ });
3066
+ await Promise.all(workers);
2947
3067
  }
2948
3068
 
2949
3069
  // lib/state/persistence.ts
2950
- var STORAGE_DIR = join2(
2951
- process.env.XDG_DATA_HOME || join2(homedir2(), ".local", "share"),
2952
- "opencode",
2953
- "storage",
2954
- "plugin",
2955
- "dcp"
2956
- );
3070
+ var saveQueues = /* @__PURE__ */ new Map();
3071
+ function getStorageDir() {
3072
+ return join2(
3073
+ process.env.XDG_DATA_HOME || join2(homedir2(), ".local", "share"),
3074
+ "opencode",
3075
+ "storage",
3076
+ "plugin",
3077
+ "dcp"
3078
+ );
3079
+ }
2957
3080
  async function ensureStorageDir() {
2958
- if (!existsSync2(STORAGE_DIR)) {
2959
- await fs.mkdir(STORAGE_DIR, { recursive: true });
3081
+ const storageDir = getStorageDir();
3082
+ if (!existsSync2(storageDir)) {
3083
+ await fs.mkdir(storageDir, { recursive: true });
2960
3084
  }
2961
3085
  }
2962
3086
  function getSessionFilePath(sessionId) {
2963
- return join2(STORAGE_DIR, `${sessionId}.json`);
3087
+ return join2(getStorageDir(), `${sessionId}.json`);
2964
3088
  }
2965
3089
  async function writePersistedSessionState(sessionId, state, logger) {
2966
3090
  await ensureStorageDir();
2967
3091
  const filePath = getSessionFilePath(sessionId);
3092
+ const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
2968
3093
  const content = JSON.stringify(state, null, 2);
2969
- await fs.writeFile(filePath, content, "utf-8");
3094
+ try {
3095
+ await fs.writeFile(temporaryPath, content, "utf-8");
3096
+ await fs.rename(temporaryPath, filePath);
3097
+ } catch (error) {
3098
+ await fs.unlink(temporaryPath).catch(() => void 0);
3099
+ throw error;
3100
+ }
2970
3101
  logger.info("Saved session state to disk", {
2971
3102
  sessionId,
2972
3103
  totalTokensSaved: state.stats.totalPruneTokens
2973
3104
  });
2974
3105
  }
2975
3106
  async function saveSessionState(sessionState, logger, sessionName) {
2976
- try {
2977
- if (!sessionState.sessionId) {
2978
- return;
3107
+ if (!sessionState.sessionId) {
3108
+ return;
3109
+ }
3110
+ const sessionId = sessionState.sessionId;
3111
+ const state = {
3112
+ sessionName,
3113
+ manualMode: !!sessionState.manualMode,
3114
+ prune: {
3115
+ tools: Object.fromEntries(sessionState.prune.tools),
3116
+ messages: serializePruneMessagesState(sessionState.prune.messages)
3117
+ },
3118
+ nudges: {
3119
+ contextLimitAnchors: Array.from(sessionState.nudges.contextLimitAnchors),
3120
+ turnNudgeAnchors: Array.from(sessionState.nudges.turnNudgeAnchors),
3121
+ iterationNudgeAnchors: Array.from(sessionState.nudges.iterationNudgeAnchors),
3122
+ boundaryPending: sessionState.nudges.boundaryPending === true
3123
+ },
3124
+ stats: { ...sessionState.stats },
3125
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
3126
+ lastCompaction: sessionState.lastCompaction,
3127
+ messageIds: {
3128
+ byRawId: Object.fromEntries(sessionState.messageIds.byRawId),
3129
+ byRef: Object.fromEntries(sessionState.messageIds.byRef),
3130
+ nextRef: sessionState.messageIds.nextRef
2979
3131
  }
2980
- const state = {
2981
- sessionName,
2982
- manualMode: !!sessionState.manualMode,
2983
- prune: {
2984
- tools: Object.fromEntries(sessionState.prune.tools),
2985
- messages: serializePruneMessagesState(sessionState.prune.messages)
2986
- },
2987
- nudges: {
2988
- contextLimitAnchors: Array.from(sessionState.nudges.contextLimitAnchors),
2989
- turnNudgeAnchors: Array.from(sessionState.nudges.turnNudgeAnchors),
2990
- iterationNudgeAnchors: Array.from(sessionState.nudges.iterationNudgeAnchors)
2991
- },
2992
- stats: sessionState.stats,
2993
- lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
2994
- lastCompaction: sessionState.lastCompaction,
2995
- messageIds: {
2996
- byRawId: Object.fromEntries(sessionState.messageIds.byRawId),
2997
- byRef: Object.fromEntries(sessionState.messageIds.byRef),
2998
- nextRef: sessionState.messageIds.nextRef
2999
- }
3000
- };
3001
- await writePersistedSessionState(sessionState.sessionId, state, logger);
3132
+ };
3133
+ const previous = saveQueues.get(sessionId) ?? Promise.resolve();
3134
+ const current = previous.catch(() => void 0).then(() => writePersistedSessionState(sessionId, state, logger));
3135
+ saveQueues.set(sessionId, current);
3136
+ try {
3137
+ await current;
3002
3138
  } catch (error) {
3003
3139
  logger.error("Failed to save session state", {
3004
- sessionId: sessionState.sessionId,
3140
+ sessionId,
3005
3141
  error: error?.message
3006
3142
  });
3143
+ throw error;
3144
+ } finally {
3145
+ if (saveQueues.get(sessionId) === current) {
3146
+ saveQueues.delete(sessionId);
3147
+ }
3007
3148
  }
3008
3149
  }
3009
3150
  async function loadSessionState(sessionId, logger) {
@@ -3062,6 +3203,7 @@ async function loadSessionState(sessionId, logger) {
3062
3203
  });
3063
3204
  }
3064
3205
  state.nudges.iterationNudgeAnchors = dedupedIterationAnchors;
3206
+ state.nudges.boundaryPending = state.nudges.boundaryPending === true;
3065
3207
  logger.info("Loaded session state from disk", {
3066
3208
  sessionId
3067
3209
  });
@@ -3082,14 +3224,15 @@ async function loadAllSessionStats(logger) {
3082
3224
  sessionCount: 0
3083
3225
  };
3084
3226
  try {
3085
- if (!existsSync2(STORAGE_DIR)) {
3227
+ const storageDir = getStorageDir();
3228
+ if (!existsSync2(storageDir)) {
3086
3229
  return result;
3087
3230
  }
3088
- const files = await fs.readdir(STORAGE_DIR);
3231
+ const files = await fs.readdir(storageDir);
3089
3232
  const jsonFiles = files.filter((f) => f.endsWith(".json"));
3090
- for (const file of jsonFiles) {
3233
+ await runWithConcurrency(jsonFiles, 16, async (file) => {
3091
3234
  try {
3092
- const filePath = join2(STORAGE_DIR, file);
3235
+ const filePath = join2(storageDir, file);
3093
3236
  const content = await fs.readFile(filePath, "utf-8");
3094
3237
  const state = JSON.parse(content);
3095
3238
  if (state?.stats?.totalPruneTokens && state?.prune) {
@@ -3100,7 +3243,7 @@ async function loadAllSessionStats(logger) {
3100
3243
  }
3101
3244
  } catch {
3102
3245
  }
3103
- }
3246
+ });
3104
3247
  logger.debug("Loaded all-time stats", result);
3105
3248
  } catch (error) {
3106
3249
  logger.warn("Failed to load all-time stats", { error: error?.message });
@@ -3109,6 +3252,8 @@ async function loadAllSessionStats(logger) {
3109
3252
  }
3110
3253
 
3111
3254
  // lib/compress/timing.ts
3255
+ var MAX_TIMING_ENTRIES = 2048;
3256
+ var TIMING_TTL_MS = 30 * 60 * 1e3;
3112
3257
  function buildCompressionTimingKey(messageId, callId) {
3113
3258
  return `${messageId}:${callId}`;
3114
3259
  }
@@ -3116,8 +3261,26 @@ function consumeCompressionStart(state, messageId, callId) {
3116
3261
  const key = buildCompressionTimingKey(messageId, callId);
3117
3262
  const start = state.compressionTiming.startsByCallId.get(key);
3118
3263
  state.compressionTiming.startsByCallId.delete(key);
3264
+ state.compressionTiming.recordedAtByCallId.delete(key);
3119
3265
  return start;
3120
3266
  }
3267
+ function pruneCompressionTiming(state, now = Date.now()) {
3268
+ const timing = state.compressionTiming;
3269
+ const cutoff = now - TIMING_TTL_MS;
3270
+ for (const [key, recordedAt] of timing.recordedAtByCallId) {
3271
+ if (recordedAt >= cutoff) continue;
3272
+ timing.recordedAtByCallId.delete(key);
3273
+ timing.startsByCallId.delete(key);
3274
+ timing.pendingByCallId.delete(key);
3275
+ }
3276
+ while (timing.recordedAtByCallId.size > MAX_TIMING_ENTRIES) {
3277
+ const oldest = timing.recordedAtByCallId.keys().next().value;
3278
+ if (typeof oldest !== "string") break;
3279
+ timing.recordedAtByCallId.delete(oldest);
3280
+ timing.startsByCallId.delete(oldest);
3281
+ timing.pendingByCallId.delete(oldest);
3282
+ }
3283
+ }
3121
3284
  function resolveCompressionDuration(startedAt, eventTime, partTime) {
3122
3285
  const runningAt = typeof partTime?.start === "number" && Number.isFinite(partTime.start) ? partTime.start : eventTime;
3123
3286
  const pendingToRunningMs = typeof startedAt === "number" && typeof runningAt === "number" ? Math.max(0, runningAt - startedAt) : void 0;
@@ -3141,24 +3304,170 @@ function applyPendingCompressionDurations(state) {
3141
3304
  if (applied > 0) {
3142
3305
  updates += applied;
3143
3306
  state.compressionTiming.pendingByCallId.delete(key);
3307
+ state.compressionTiming.recordedAtByCallId.delete(key);
3144
3308
  }
3145
3309
  }
3146
3310
  return updates;
3147
3311
  }
3148
3312
 
3313
+ // lib/state/store.ts
3314
+ var MAX_RESIDENT_SESSIONS = 64;
3315
+ var MAX_MESSAGE_OWNERS = 2e4;
3316
+ var MAX_TIMING_ENTRIES2 = 2048;
3317
+ var TIMING_TTL_MS2 = 30 * 60 * 1e3;
3318
+ var SessionStateStore = class {
3319
+ constructor(createState) {
3320
+ this.createState = createState;
3321
+ }
3322
+ states = /* @__PURE__ */ new Map();
3323
+ initializationQueues = /* @__PURE__ */ new Map();
3324
+ messageOwners = /* @__PURE__ */ new Map();
3325
+ orphanStarts = /* @__PURE__ */ new Map();
3326
+ orphanPending = /* @__PURE__ */ new Map();
3327
+ get(sessionId) {
3328
+ const existing = this.states.get(sessionId);
3329
+ if (existing) {
3330
+ this.touch(sessionId, existing);
3331
+ return existing;
3332
+ }
3333
+ const state = this.createState();
3334
+ this.states.set(sessionId, state);
3335
+ this.trimSessions();
3336
+ return state;
3337
+ }
3338
+ peek(sessionId) {
3339
+ return this.states.get(sessionId);
3340
+ }
3341
+ forEach(callback) {
3342
+ for (const [sessionId, state] of this.states) {
3343
+ callback(state, sessionId);
3344
+ }
3345
+ }
3346
+ async initialize(sessionId, initializer) {
3347
+ const state = this.get(sessionId);
3348
+ const previous = this.initializationQueues.get(sessionId) ?? Promise.resolve();
3349
+ const current = previous.catch(() => void 0).then(() => initializer(state));
3350
+ this.initializationQueues.set(sessionId, current);
3351
+ try {
3352
+ await current;
3353
+ } finally {
3354
+ if (this.initializationQueues.get(sessionId) === current) {
3355
+ this.initializationQueues.delete(sessionId);
3356
+ }
3357
+ }
3358
+ return state;
3359
+ }
3360
+ registerMessages(sessionId, messages) {
3361
+ const state = this.get(sessionId);
3362
+ for (const message of messages) {
3363
+ const messageId = message.info?.id;
3364
+ if (typeof messageId !== "string") continue;
3365
+ this.messageOwners.delete(messageId);
3366
+ this.messageOwners.set(messageId, sessionId);
3367
+ this.moveOrphanTiming(messageId, state.compressionTiming);
3368
+ }
3369
+ while (this.messageOwners.size > MAX_MESSAGE_OWNERS) {
3370
+ const oldest = this.messageOwners.keys().next().value;
3371
+ if (typeof oldest !== "string") break;
3372
+ this.messageOwners.delete(oldest);
3373
+ }
3374
+ return state;
3375
+ }
3376
+ resolveEventState(sessionId, messageId) {
3377
+ const resolvedSessionId = sessionId ?? this.messageOwners.get(messageId);
3378
+ if (!resolvedSessionId) return void 0;
3379
+ this.messageOwners.set(messageId, resolvedSessionId);
3380
+ return this.get(resolvedSessionId);
3381
+ }
3382
+ recordOrphanStart(key, startedAt) {
3383
+ this.pruneOrphanTiming();
3384
+ if (this.orphanStarts.has(key)) return;
3385
+ this.orphanStarts.set(key, { startedAt, recordedAt: Date.now() });
3386
+ this.trimMap(this.orphanStarts);
3387
+ }
3388
+ consumeOrphanStart(key) {
3389
+ const entry = this.orphanStarts.get(key);
3390
+ this.orphanStarts.delete(key);
3391
+ return entry?.startedAt;
3392
+ }
3393
+ recordOrphanPending(key, entry) {
3394
+ this.pruneOrphanTiming();
3395
+ this.orphanPending.set(key, { ...entry, recordedAt: Date.now() });
3396
+ this.trimMap(this.orphanPending);
3397
+ }
3398
+ deleteOrphanStart(key) {
3399
+ this.orphanStarts.delete(key);
3400
+ }
3401
+ moveOrphanTiming(messageId, timing) {
3402
+ const prefix = `${messageId}:`;
3403
+ for (const [key, entry] of this.orphanStarts) {
3404
+ if (!key.startsWith(prefix)) continue;
3405
+ if (!timing.startsByCallId.has(key)) {
3406
+ timing.startsByCallId.set(key, entry.startedAt);
3407
+ }
3408
+ timing.recordedAtByCallId.set(key, entry.recordedAt);
3409
+ this.orphanStarts.delete(key);
3410
+ }
3411
+ for (const [key, entry] of this.orphanPending) {
3412
+ if (!key.startsWith(prefix)) continue;
3413
+ timing.pendingByCallId.set(key, {
3414
+ messageId: entry.messageId,
3415
+ callId: entry.callId,
3416
+ durationMs: entry.durationMs
3417
+ });
3418
+ timing.recordedAtByCallId.set(key, entry.recordedAt);
3419
+ this.orphanPending.delete(key);
3420
+ }
3421
+ }
3422
+ touch(sessionId, state) {
3423
+ this.states.delete(sessionId);
3424
+ this.states.set(sessionId, state);
3425
+ }
3426
+ trimSessions() {
3427
+ while (this.states.size > MAX_RESIDENT_SESSIONS) {
3428
+ const oldest = Array.from(this.states.keys()).find(
3429
+ (sessionId) => !this.initializationQueues.has(sessionId)
3430
+ );
3431
+ if (!oldest) return;
3432
+ this.states.delete(oldest);
3433
+ for (const [messageId, owner] of this.messageOwners) {
3434
+ if (owner === oldest) this.messageOwners.delete(messageId);
3435
+ }
3436
+ }
3437
+ }
3438
+ pruneOrphanTiming() {
3439
+ const cutoff = Date.now() - TIMING_TTL_MS2;
3440
+ for (const [key, entry] of this.orphanStarts) {
3441
+ if (entry.recordedAt < cutoff) this.orphanStarts.delete(key);
3442
+ }
3443
+ for (const [key, entry] of this.orphanPending) {
3444
+ if (entry.recordedAt < cutoff) this.orphanPending.delete(key);
3445
+ }
3446
+ }
3447
+ trimMap(map) {
3448
+ while (map.size > MAX_TIMING_ENTRIES2) {
3449
+ const oldest = map.keys().next().value;
3450
+ if (typeof oldest !== "string") return;
3451
+ map.delete(oldest);
3452
+ }
3453
+ }
3454
+ };
3455
+ function resolveSessionState(target, sessionId) {
3456
+ return target instanceof SessionStateStore ? target.get(sessionId) : target;
3457
+ }
3458
+
3149
3459
  // lib/state/state.ts
3150
- var checkSession = async (client, state, logger, messages, manualModeDefault) => {
3460
+ var checkSession = async (client, target, logger, messages, manualModeDefault) => {
3151
3461
  const lastUserMessage = getLastUserMessage(messages);
3152
- if (!lastUserMessage) {
3153
- return;
3154
- }
3155
- const lastSessionId = lastUserMessage.info.sessionID;
3462
+ const lastSessionId = lastUserMessage?.info.sessionID ?? messages.findLast((message) => typeof message.info?.sessionID === "string")?.info.sessionID;
3463
+ if (!lastSessionId) return target instanceof SessionStateStore ? void 0 : target;
3464
+ const state = target instanceof SessionStateStore ? target.registerMessages(lastSessionId, messages) : resolveSessionState(target, lastSessionId);
3156
3465
  if (state.sessionId === null || state.sessionId !== lastSessionId) {
3157
3466
  logger.info(`Session changed: ${state.sessionId} -> ${lastSessionId}`);
3158
3467
  try {
3159
- await ensureSessionInitialized(
3468
+ await initializeSessionState(
3160
3469
  client,
3161
- state,
3470
+ target,
3162
3471
  lastSessionId,
3163
3472
  logger,
3164
3473
  messages,
@@ -3182,6 +3491,7 @@ var checkSession = async (client, state, logger, messages, manualModeDefault) =>
3182
3491
  });
3183
3492
  }
3184
3493
  state.currentTurn = countTurns(state, messages);
3494
+ return state;
3185
3495
  };
3186
3496
  function createSessionState() {
3187
3497
  return {
@@ -3197,7 +3507,8 @@ function createSessionState() {
3197
3507
  nudges: {
3198
3508
  contextLimitAnchors: /* @__PURE__ */ new Set(),
3199
3509
  turnNudgeAnchors: /* @__PURE__ */ new Set(),
3200
- iterationNudgeAnchors: /* @__PURE__ */ new Set()
3510
+ iterationNudgeAnchors: /* @__PURE__ */ new Set(),
3511
+ boundaryPending: false
3201
3512
  },
3202
3513
  stats: {
3203
3514
  pruneTokenCounter: 0,
@@ -3205,7 +3516,8 @@ function createSessionState() {
3205
3516
  },
3206
3517
  compressionTiming: {
3207
3518
  startsByCallId: /* @__PURE__ */ new Map(),
3208
- pendingByCallId: /* @__PURE__ */ new Map()
3519
+ pendingByCallId: /* @__PURE__ */ new Map(),
3520
+ recordedAtByCallId: /* @__PURE__ */ new Map()
3209
3521
  },
3210
3522
  toolParameters: /* @__PURE__ */ new Map(),
3211
3523
  subAgentResultCache: /* @__PURE__ */ new Map(),
@@ -3222,6 +3534,7 @@ function createSessionState() {
3222
3534
  };
3223
3535
  }
3224
3536
  function resetSessionState(state) {
3537
+ const modelContextLimit = state.modelContextLimit;
3225
3538
  state.sessionId = null;
3226
3539
  state.isSubAgent = false;
3227
3540
  state.manualMode = false;
@@ -3234,7 +3547,8 @@ function resetSessionState(state) {
3234
3547
  state.nudges = {
3235
3548
  contextLimitAnchors: /* @__PURE__ */ new Set(),
3236
3549
  turnNudgeAnchors: /* @__PURE__ */ new Set(),
3237
- iterationNudgeAnchors: /* @__PURE__ */ new Set()
3550
+ iterationNudgeAnchors: /* @__PURE__ */ new Set(),
3551
+ boundaryPending: false
3238
3552
  };
3239
3553
  state.stats = {
3240
3554
  pruneTokenCounter: 0,
@@ -3250,9 +3564,20 @@ function resetSessionState(state) {
3250
3564
  };
3251
3565
  state.lastCompaction = 0;
3252
3566
  state.currentTurn = 0;
3253
- state.modelContextLimit = void 0;
3567
+ state.modelContextLimit = modelContextLimit;
3254
3568
  state.systemPromptTokens = void 0;
3255
3569
  }
3570
+ async function initializeSessionState(client, target, sessionId, logger, messages, manualModeEnabled) {
3571
+ if (target instanceof SessionStateStore) {
3572
+ target.registerMessages(sessionId, messages);
3573
+ return target.initialize(
3574
+ sessionId,
3575
+ (state) => ensureSessionInitialized(client, state, sessionId, logger, messages, manualModeEnabled)
3576
+ );
3577
+ }
3578
+ await ensureSessionInitialized(client, target, sessionId, logger, messages, manualModeEnabled);
3579
+ return target;
3580
+ }
3256
3581
  async function ensureSessionInitialized(client, state, sessionId, logger, messages, manualModeEnabled) {
3257
3582
  if (state.sessionId === sessionId) {
3258
3583
  return;
@@ -3282,6 +3607,7 @@ async function ensureSessionInitialized(client, state, sessionId, logger, messag
3282
3607
  state.nudges.iterationNudgeAnchors = new Set(
3283
3608
  persisted.nudges.iterationNudgeAnchors || []
3284
3609
  );
3610
+ state.nudges.boundaryPending = persisted.nudges.boundaryPending === true;
3285
3611
  state.stats = {
3286
3612
  pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0,
3287
3613
  totalPruneTokens: persisted.stats?.totalPruneTokens || 0
@@ -3316,6 +3642,7 @@ function syncToolCache(state, config, logger, messages) {
3316
3642
  try {
3317
3643
  logger.info("Syncing tool parameters from OpenCode messages");
3318
3644
  let turnCounter = 0;
3645
+ const candidates = /* @__PURE__ */ new Map();
3319
3646
  for (const msg of messages) {
3320
3647
  if (isMessageCompacted(state, msg)) {
3321
3648
  continue;
@@ -3329,26 +3656,35 @@ function syncToolCache(state, config, logger, messages) {
3329
3656
  if (part.type !== "tool" || !part.callID) {
3330
3657
  continue;
3331
3658
  }
3332
- const turnProtectionEnabled = config.turnProtection.enabled;
3333
- const turnProtectionTurns = config.turnProtection.turns;
3334
- const isProtectedByTurn = turnProtectionEnabled && turnProtectionTurns > 0 && state.currentTurn - turnCounter < turnProtectionTurns;
3335
- if (state.toolParameters.has(part.callID)) {
3336
- continue;
3337
- }
3338
- if (isProtectedByTurn) {
3339
- continue;
3340
- }
3341
- const tokenCount = countToolTokens(part);
3342
- state.toolParameters.set(part.callID, {
3343
- tool: part.tool,
3344
- parameters: part.state?.input ?? {},
3345
- status: part.state.status,
3346
- error: part.state.status === "error" ? part.state.error : void 0,
3347
- turn: turnCounter,
3348
- tokenCount
3349
- });
3659
+ candidates.delete(part.callID);
3660
+ candidates.set(part.callID, { part, turn: turnCounter });
3661
+ }
3662
+ }
3663
+ const recentCandidates = Array.from(candidates.entries()).filter(([, candidate]) => {
3664
+ const turnProtectionEnabled = config.turnProtection.enabled;
3665
+ const turnProtectionTurns = config.turnProtection.turns;
3666
+ return !(turnProtectionEnabled && turnProtectionTurns > 0 && state.currentTurn - candidate.turn < turnProtectionTurns);
3667
+ }).slice(-MAX_TOOL_CACHE_SIZE);
3668
+ const retainedIds = new Set(recentCandidates.map(([callId]) => callId));
3669
+ for (const callId of state.toolParameters.keys()) {
3670
+ if (!retainedIds.has(callId)) state.toolParameters.delete(callId);
3671
+ }
3672
+ for (const [callId, { part, turn }] of recentCandidates) {
3673
+ const status = part.state.status;
3674
+ const cached = state.toolParameters.get(callId);
3675
+ if (cached?.status === status) continue;
3676
+ const tokenCount = countToolTokens(part);
3677
+ state.toolParameters.set(callId, {
3678
+ tool: part.tool,
3679
+ parameters: part.state?.input ?? {},
3680
+ status,
3681
+ error: part.state.status === "error" ? part.state.error : void 0,
3682
+ turn,
3683
+ tokenCount
3684
+ });
3685
+ if (!cached) {
3350
3686
  logger.info(
3351
- `Cached tool id: ${part.callID} (turn ${turnCounter}${tokenCount !== void 0 ? `, ${tokenCount} tokens` : ""})`
3687
+ `Cached tool id: ${callId} (turn ${turn}${tokenCount !== void 0 ? `, ${tokenCount} tokens` : ""})`
3352
3688
  );
3353
3689
  }
3354
3690
  }
@@ -3494,6 +3830,9 @@ var deduplicate = (state, logger, config, messages) => {
3494
3830
  if (!metadata) {
3495
3831
  continue;
3496
3832
  }
3833
+ if (metadata.status !== void 0 && metadata.status !== "completed" && metadata.status !== "success") {
3834
+ continue;
3835
+ }
3497
3836
  if (isToolNameProtected(metadata.tool, protectedTools)) {
3498
3837
  continue;
3499
3838
  }
@@ -3991,7 +4330,6 @@ ${progressBar}`;
3991
4330
  }
3992
4331
  async function sendIgnoredMessage(client, sessionID, text, params, logger) {
3993
4332
  const agent = params.agent || void 0;
3994
- const variant = params.variant || void 0;
3995
4333
  const model = params.providerId && params.modelId ? {
3996
4334
  providerID: params.providerId,
3997
4335
  modelID: params.modelId
@@ -4005,7 +4343,6 @@ async function sendIgnoredMessage(client, sessionID, text, params, logger) {
4005
4343
  noReply: true,
4006
4344
  agent,
4007
4345
  model,
4008
- variant,
4009
4346
  parts: [
4010
4347
  {
4011
4348
  type: "text",
@@ -4022,7 +4359,18 @@ async function sendIgnoredMessage(client, sessionID, text, params, logger) {
4022
4359
 
4023
4360
  // lib/compress/pipeline.ts
4024
4361
  async function prepareSession(ctx, toolCtx, title) {
4025
- if (ctx.state.manualMode && ctx.state.manualMode !== "compress-pending") {
4362
+ const state = resolveSessionState(ctx.state, toolCtx.sessionID);
4363
+ toolCtx.metadata({ title });
4364
+ const rawMessages = await fetchSessionMessages(ctx.client, toolCtx.sessionID);
4365
+ await initializeSessionState(
4366
+ ctx.client,
4367
+ ctx.state,
4368
+ toolCtx.sessionID,
4369
+ ctx.logger,
4370
+ rawMessages,
4371
+ ctx.config.manualMode.enabled
4372
+ );
4373
+ if (state.manualMode && state.manualMode !== "compress-pending") {
4026
4374
  throw new Error(
4027
4375
  "Manual mode: compress blocked. Do not retry until `<compress triggered manually>` appears in user context."
4028
4376
  );
@@ -4033,35 +4381,26 @@ async function prepareSession(ctx, toolCtx, title) {
4033
4381
  always: ["*"],
4034
4382
  metadata: {}
4035
4383
  });
4036
- toolCtx.metadata({ title });
4037
- const rawMessages = await fetchSessionMessages(ctx.client, toolCtx.sessionID);
4038
- await ensureSessionInitialized(
4039
- ctx.client,
4040
- ctx.state,
4041
- toolCtx.sessionID,
4042
- ctx.logger,
4043
- rawMessages,
4044
- ctx.config.manualMode.enabled
4045
- );
4046
- assignMessageRefs(ctx.state, rawMessages);
4047
- deduplicate(ctx.state, ctx.logger, ctx.config, rawMessages);
4048
- purgeErrors(ctx.state, ctx.logger, ctx.config, rawMessages);
4384
+ assignMessageRefs(state, rawMessages);
4385
+ deduplicate(state, ctx.logger, ctx.config, rawMessages);
4386
+ purgeErrors(state, ctx.logger, ctx.config, rawMessages);
4049
4387
  return {
4388
+ state,
4050
4389
  rawMessages,
4051
- searchContext: buildSearchContext(ctx.state, rawMessages)
4390
+ searchContext: buildSearchContext(state, rawMessages)
4052
4391
  };
4053
4392
  }
4054
- async function finalizeSession(ctx, toolCtx, rawMessages, entries, batchTopic) {
4055
- ctx.state.manualMode = ctx.state.manualMode ? "active" : false;
4056
- applyPendingCompressionDurations(ctx.state);
4057
- await saveSessionState(ctx.state, ctx.logger);
4058
- const params = getCurrentParams(ctx.state, rawMessages, ctx.logger);
4393
+ async function finalizeSession(ctx, state, toolCtx, rawMessages, entries, batchTopic) {
4394
+ state.manualMode = state.manualMode ? "active" : false;
4395
+ applyPendingCompressionDurations(state);
4396
+ await saveSessionState(state, ctx.logger);
4397
+ const params = getCurrentParams(state, rawMessages, ctx.logger);
4059
4398
  const sessionMessageIds = rawMessages.filter((msg) => !isIgnoredUserMessage(msg)).map((msg) => msg.info.id);
4060
4399
  await sendCompressNotification(
4061
4400
  ctx.client,
4062
4401
  ctx.logger,
4063
4402
  ctx.config,
4064
- ctx.state,
4403
+ state,
4065
4404
  toolCtx.sessionID,
4066
4405
  entries,
4067
4406
  batchTopic,
@@ -4220,7 +4559,7 @@ async function appendProtectedTools(client, state, allowSubAgents, summary, sele
4220
4559
  output = typeof part.state.output === "string" ? part.state.output : JSON.stringify(part.state.output);
4221
4560
  }
4222
4561
  if (allowSubAgents && part.tool === "task" && part.state?.status === "completed" && typeof part.state?.output === "string") {
4223
- const cachedSubAgentResult = state.subAgentResultCache.get(part.callID);
4562
+ const cachedSubAgentResult = getCachedSubAgentResult(state, part.callID);
4224
4563
  if (cachedSubAgentResult !== void 0) {
4225
4564
  if (cachedSubAgentResult) {
4226
4565
  output = mergeSubagentResult(
@@ -4242,7 +4581,11 @@ async function appendProtectedTools(client, state, allowSubAgents, summary, sele
4242
4581
  subAgentResultText = "";
4243
4582
  }
4244
4583
  if (subAgentResultText) {
4245
- state.subAgentResultCache.set(part.callID, subAgentResultText);
4584
+ subAgentResultText = cacheSubAgentResult(
4585
+ state,
4586
+ part.callID,
4587
+ subAgentResultText
4588
+ );
4246
4589
  output = mergeSubagentResult(
4247
4590
  part.state.output,
4248
4591
  subAgentResultText
@@ -4350,6 +4693,21 @@ async function callExternalModel(endpoint, cfg, req) {
4350
4693
  return content;
4351
4694
  }
4352
4695
  async function generateSummaryViaExternal(cfg, req) {
4696
+ if (typeof cfg.url !== "string" || cfg.url.trim().length === 0) {
4697
+ throw new Error("\u5916\u90E8\u538B\u7F29\u6A21\u578B url \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
4698
+ }
4699
+ if (typeof cfg.model !== "string" || cfg.model.trim().length === 0) {
4700
+ throw new Error("\u5916\u90E8\u538B\u7F29\u6A21\u578B model \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
4701
+ }
4702
+ if (cfg.apiKey !== void 0 && typeof cfg.apiKey !== "string") {
4703
+ throw new Error("\u5916\u90E8\u538B\u7F29\u6A21\u578B apiKey \u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
4704
+ }
4705
+ if (cfg.timeout !== void 0 && (!Number.isFinite(cfg.timeout) || cfg.timeout <= 0)) {
4706
+ throw new Error("\u5916\u90E8\u538B\u7F29\u6A21\u578B timeout \u5FC5\u987B\u662F\u6B63\u6570");
4707
+ }
4708
+ if (cfg.retries !== void 0 && (!Number.isInteger(cfg.retries) || cfg.retries < 0)) {
4709
+ throw new Error("\u5916\u90E8\u538B\u7F29\u6A21\u578B retries \u5FC5\u987B\u662F\u975E\u8D1F\u6574\u6570");
4710
+ }
4353
4711
  const endpoint = resolveCompressBaseUrl(cfg.url);
4354
4712
  const maxRetries = cfg.retries ?? 1;
4355
4713
  let lastError;
@@ -4363,18 +4721,57 @@ async function generateSummaryViaExternal(cfg, req) {
4363
4721
  throw lastError;
4364
4722
  }
4365
4723
 
4366
- // lib/compress/message.ts
4367
- function extractMessageText(messageId, searchContext) {
4368
- const msg = searchContext.rawMessagesById.get(messageId);
4369
- if (!msg) return "";
4370
- const parts = [];
4371
- for (const part of msg.parts) {
4372
- if (part.type === "text" && typeof part.text === "string") {
4373
- parts.push(part.text);
4724
+ // lib/compress/external-content.ts
4725
+ var MAX_PART_CHARS = 1e5;
4726
+ var MAX_TOTAL_CHARS = 1e6;
4727
+ var TRUNCATED_MARKER = "\n[\u5185\u5BB9\u56E0\u957F\u5EA6\u9650\u5236\u88AB\u622A\u65AD]";
4728
+ function stringify(value) {
4729
+ if (typeof value === "string") return value;
4730
+ try {
4731
+ return JSON.stringify(value, null, 2) ?? String(value);
4732
+ } catch {
4733
+ return String(value);
4734
+ }
4735
+ }
4736
+ function truncate2(value, limit = MAX_PART_CHARS) {
4737
+ if (value.length <= limit) return value;
4738
+ return value.slice(0, limit) + TRUNCATED_MARKER;
4739
+ }
4740
+ function serializePart(part) {
4741
+ if (part.type === "text" && typeof part.text === "string") {
4742
+ return [truncate2(part.text)];
4743
+ }
4744
+ if (part.type !== "tool") return [];
4745
+ const chunks = [`[\u5DE5\u5177\uFF1A${part.tool}\uFF1B\u72B6\u6001\uFF1A${part.state.status}]`];
4746
+ if (part.state.input !== void 0) {
4747
+ chunks.push("\u8F93\u5165\uFF1A\n" + truncate2(stringify(part.state.input)));
4748
+ }
4749
+ if (part.state.status === "completed" && part.state.output !== void 0) {
4750
+ chunks.push("\u8F93\u51FA\uFF1A\n" + truncate2(stringify(part.state.output)));
4751
+ } else if (part.state.status === "error" && part.state.error !== void 0) {
4752
+ chunks.push("\u9519\u8BEF\uFF1A\n" + truncate2(stringify(part.state.error)));
4753
+ }
4754
+ return chunks;
4755
+ }
4756
+ function serializeMessagesForExternalSummary(messages) {
4757
+ const chunks = [];
4758
+ let length = 0;
4759
+ for (const message of messages) {
4760
+ const header = `[\u6D88\u606F\uFF1A${message.info.id}\uFF1B\u89D2\u8272\uFF1A${message.info.role}]`;
4761
+ const content = [header, ...message.parts.flatMap(serializePart)].join("\n");
4762
+ if (length + content.length > MAX_TOTAL_CHARS) {
4763
+ const remaining = Math.max(0, MAX_TOTAL_CHARS - length);
4764
+ if (remaining > 0) chunks.push(content.slice(0, remaining));
4765
+ chunks.push(TRUNCATED_MARKER.trim());
4766
+ break;
4374
4767
  }
4768
+ chunks.push(content);
4769
+ length += content.length;
4375
4770
  }
4376
- return parts.join("\n\n");
4771
+ return chunks.join("\n\n");
4377
4772
  }
4773
+
4774
+ // lib/compress/message.ts
4378
4775
  function buildSchema(externalModelEnabled) {
4379
4776
  const summaryField = externalModelEnabled ? tool.schema.string().optional().describe(
4380
4777
  "Complete technical summary replacing that one message (optional when external model is configured)"
@@ -4403,7 +4800,7 @@ function createCompressMessageTool(ctx) {
4403
4800
  const input = args;
4404
4801
  validateArgs(input);
4405
4802
  const callId = typeof toolCtx.callID === "string" ? toolCtx.callID : void 0;
4406
- const { rawMessages, searchContext } = await prepareSession(
4803
+ const { state, rawMessages, searchContext } = await prepareSession(
4407
4804
  ctx,
4408
4805
  toolCtx,
4409
4806
  `Compress Message: ${input.topic}`
@@ -4411,26 +4808,27 @@ function createCompressMessageTool(ctx) {
4411
4808
  const { plans, skippedIssues, skippedCount } = resolveMessages(
4412
4809
  input,
4413
4810
  searchContext,
4414
- ctx.state,
4811
+ state,
4415
4812
  ctx.config
4416
4813
  );
4417
4814
  if (plans.length === 0 && skippedCount > 0) {
4418
4815
  throw new Error(formatIssues(skippedIssues, skippedCount));
4419
4816
  }
4420
- if (ctx.config.compress.externalModel) {
4421
- for (const plan of plans) {
4817
+ const externalModel = ctx.config.compress.externalModel;
4818
+ if (externalModel) {
4819
+ await runWithConcurrency(plans, 3, async (plan) => {
4422
4820
  if (plan.entry.summary === void 0) {
4423
- const userContent = extractMessageText(plan.entry.messageId, searchContext);
4424
- const generated = await generateSummaryViaExternal(
4425
- ctx.config.compress.externalModel,
4426
- {
4427
- systemPrompt: runtimePrompts.compressMessage,
4428
- userContent
4429
- }
4821
+ const message = searchContext.rawMessagesById.get(plan.entry.messageId);
4822
+ const userContent = serializeMessagesForExternalSummary(
4823
+ message ? [message] : []
4430
4824
  );
4825
+ const generated = await generateSummaryViaExternal(externalModel, {
4826
+ systemPrompt: runtimePrompts.compressMessage,
4827
+ userContent
4828
+ });
4431
4829
  plan.entry.summary = generated;
4432
4830
  }
4433
- }
4831
+ });
4434
4832
  }
4435
4833
  const notifications = [];
4436
4834
  const preparedPlans = [];
@@ -4444,12 +4842,12 @@ function createCompressMessageTool(ctx) {
4444
4842
  plan.entry.summary,
4445
4843
  plan.selection,
4446
4844
  searchContext,
4447
- ctx.state,
4845
+ state,
4448
4846
  ctx.config.compress.protectTags
4449
4847
  );
4450
4848
  const summaryWithTools = await appendProtectedTools(
4451
4849
  ctx.client,
4452
- ctx.state,
4850
+ state,
4453
4851
  ctx.config.experimental.allowSubAgents,
4454
4852
  summaryWithPromptInfo,
4455
4853
  plan.selection,
@@ -4462,13 +4860,13 @@ function createCompressMessageTool(ctx) {
4462
4860
  summaryWithTools
4463
4861
  });
4464
4862
  }
4465
- const runId = allocateRunId(ctx.state);
4863
+ const runId = allocateRunId(state);
4466
4864
  for (const { plan, summaryWithTools } of preparedPlans) {
4467
- const blockId = allocateBlockId(ctx.state);
4865
+ const blockId = allocateBlockId(state);
4468
4866
  const storedSummary = wrapCompressedSummary(blockId, summaryWithTools);
4469
4867
  const summaryTokens = countTokens2(storedSummary);
4470
4868
  applyCompressionState(
4471
- ctx.state,
4869
+ state,
4472
4870
  {
4473
4871
  topic: plan.entry.topic,
4474
4872
  batchTopic: input.topic,
@@ -4493,7 +4891,7 @@ function createCompressMessageTool(ctx) {
4493
4891
  summaryTokens
4494
4892
  });
4495
4893
  }
4496
- await finalizeSession(ctx, toolCtx, rawMessages, notifications, input.topic);
4894
+ await finalizeSession(ctx, state, toolCtx, rawMessages, notifications, input.topic);
4497
4895
  return formatResult(plans.length, skippedIssues, skippedCount);
4498
4896
  }
4499
4897
  });
@@ -4729,19 +5127,6 @@ ${right}`;
4729
5127
  }
4730
5128
 
4731
5129
  // lib/compress/range.ts
4732
- function extractRangeText(selection, searchContext) {
4733
- const parts = [];
4734
- for (const messageId of selection.messageIds) {
4735
- const msg = searchContext.rawMessagesById.get(messageId);
4736
- if (!msg) continue;
4737
- for (const part of msg.parts) {
4738
- if (part.type === "text" && typeof part.text === "string") {
4739
- parts.push(part.text);
4740
- }
4741
- }
4742
- }
4743
- return parts.join("\n\n");
4744
- }
4745
5130
  function buildSchema2(externalModelEnabled) {
4746
5131
  const summaryField = externalModelEnabled ? tool2.schema.string().optional().describe(
4747
5132
  "Complete technical summary replacing all content in range (optional when external model is configured)"
@@ -4774,27 +5159,29 @@ function createCompressRangeTool(ctx) {
4774
5159
  const input = args;
4775
5160
  validateArgs2(input);
4776
5161
  const callId = typeof toolCtx.callID === "string" ? toolCtx.callID : void 0;
4777
- const { rawMessages, searchContext } = await prepareSession(
5162
+ const { state, rawMessages, searchContext } = await prepareSession(
4778
5163
  ctx,
4779
5164
  toolCtx,
4780
5165
  `Compress Range: ${input.topic}`
4781
5166
  );
4782
- const resolvedPlans = resolveRanges(input, searchContext, ctx.state);
5167
+ const resolvedPlans = resolveRanges(input, searchContext, state);
4783
5168
  validateNonOverlapping(resolvedPlans);
4784
- if (ctx.config.compress.externalModel) {
4785
- for (const plan of resolvedPlans) {
5169
+ const externalModel = ctx.config.compress.externalModel;
5170
+ if (externalModel) {
5171
+ await runWithConcurrency(resolvedPlans, 3, async (plan) => {
4786
5172
  if (plan.entry.summary === void 0) {
4787
- const userContent = extractRangeText(plan.selection, searchContext);
4788
- const generated = await generateSummaryViaExternal(
4789
- ctx.config.compress.externalModel,
4790
- {
4791
- systemPrompt: runtimePrompts.compressRange,
4792
- userContent
4793
- }
4794
- );
5173
+ const selectedMessages = plan.selection.messageIds.flatMap((messageId) => {
5174
+ const message = searchContext.rawMessagesById.get(messageId);
5175
+ return message ? [message] : [];
5176
+ });
5177
+ const userContent = serializeMessagesForExternalSummary(selectedMessages);
5178
+ const generated = await generateSummaryViaExternal(externalModel, {
5179
+ systemPrompt: runtimePrompts.compressRange,
5180
+ userContent
5181
+ });
4795
5182
  plan.entry.summary = generated;
4796
5183
  }
4797
- }
5184
+ });
4798
5185
  }
4799
5186
  const notifications = [];
4800
5187
  const preparedPlans = [];
@@ -4824,19 +5211,19 @@ function createCompressRangeTool(ctx) {
4824
5211
  injected.expandedSummary,
4825
5212
  plan.selection,
4826
5213
  searchContext,
4827
- ctx.state,
5214
+ state,
4828
5215
  ctx.config.compress.protectUserMessages
4829
5216
  );
4830
5217
  const summaryWithPromptInfo = appendProtectedPromptInfo(
4831
5218
  summaryWithUsers,
4832
5219
  plan.selection,
4833
5220
  searchContext,
4834
- ctx.state,
5221
+ state,
4835
5222
  ctx.config.compress.protectTags
4836
5223
  );
4837
5224
  const summaryWithTools = await appendProtectedTools(
4838
5225
  ctx.client,
4839
- ctx.state,
5226
+ state,
4840
5227
  ctx.config.experimental.allowSubAgents,
4841
5228
  summaryWithPromptInfo,
4842
5229
  plan.selection,
@@ -4858,13 +5245,13 @@ function createCompressRangeTool(ctx) {
4858
5245
  consumedBlockIds: completedSummary.consumedBlockIds
4859
5246
  });
4860
5247
  }
4861
- const runId = allocateRunId(ctx.state);
5248
+ const runId = allocateRunId(state);
4862
5249
  for (const preparedPlan of preparedPlans) {
4863
- const blockId = allocateBlockId(ctx.state);
5250
+ const blockId = allocateBlockId(state);
4864
5251
  const storedSummary = wrapCompressedSummary(blockId, preparedPlan.finalSummary);
4865
5252
  const summaryTokens = countTokens2(storedSummary);
4866
5253
  const applied = applyCompressionState(
4867
- ctx.state,
5254
+ state,
4868
5255
  {
4869
5256
  topic: input.topic,
4870
5257
  batchTopic: input.topic,
@@ -4890,7 +5277,7 @@ function createCompressRangeTool(ctx) {
4890
5277
  summaryTokens
4891
5278
  });
4892
5279
  }
4893
- await finalizeSession(ctx, toolCtx, rawMessages, notifications, input.topic);
5280
+ await finalizeSession(ctx, state, toolCtx, rawMessages, notifications, input.topic);
4894
5281
  return `Compressed ${totalCompressedMessages} messages into ${COMPRESSED_BLOCK_HEADER}.`;
4895
5282
  }
4896
5283
  });
@@ -5156,51 +5543,51 @@ import { homedir as homedir4 } from "os";
5156
5543
 
5157
5544
  // lib/prompts/system.ts
5158
5545
  var SYSTEM = `
5159
- You run in a context-limited environment. Manage context actively to avoid accumulation and keep retrieval quality high.
5546
+ \u4F60\u5728\u4E0A\u4E0B\u6587\u957F\u5EA6\u53D7\u9650\u7684\u73AF\u5883\u4E2D\u8FD0\u884C\u3002\u4E3B\u52A8\u7BA1\u7406\u4E0A\u4E0B\u6587\uFF0C\u907F\u514D\u65E0\u6548\u7D2F\u79EF\u5E76\u4FDD\u6301\u8F83\u9AD8\u7684\u68C0\u7D22\u8D28\u91CF\u3002
5160
5547
 
5161
- Your only context-management tool is \`compress\`. It replaces older conversation content with a technical summary you generate.
5548
+ \u552F\u4E00\u7684\u4E0A\u4E0B\u6587\u7BA1\u7406\u5DE5\u5177\u662F \`compress\`\u3002\u5B83\u4F1A\u7528\u4F60\u751F\u6210\u7684\u6280\u672F\u6458\u8981\u66FF\u6362\u8F83\u65E9\u7684\u5BF9\u8BDD\u5185\u5BB9\u3002
5162
5549
 
5163
- Hard constraints
5164
- - Context management is done ONLY by calling \`compress\`. Never produce summaries in plain text.
5165
- - NEVER output \`<summary>\` or \`<analysis>\` XML tags in text responses \u2014 they cause system errors.
5166
- - If context needs compression, call \`compress\`. Do not write summaries inline.
5167
- - Use Markdown headings (e.g. \`## Analysis\`, \`## Summary\`) to organize information. Never use XML tags.
5550
+ \u786C\u6027\u7EA6\u675F
5551
+ - \u53EA\u80FD\u901A\u8FC7\u8C03\u7528 \`compress\` \u7BA1\u7406\u4E0A\u4E0B\u6587\uFF0C\u7EDD\u4E0D\u80FD\u5728\u666E\u901A\u6587\u672C\u4E2D\u76F4\u63A5\u751F\u6210\u538B\u7F29\u6458\u8981\u3002
5552
+ - \u6587\u672C\u56DE\u590D\u4E2D\u7EDD\u4E0D\u80FD\u8F93\u51FA \`<summary>\` \u6216 \`<analysis>\` XML \u6807\u7B7E\uFF0C\u5B83\u4EEC\u4F1A\u5BFC\u81F4\u7CFB\u7EDF\u9519\u8BEF\u3002
5553
+ - \u9700\u8981\u538B\u7F29\u65F6\u8C03\u7528 \`compress\`\uFF0C\u4E0D\u8981\u5728\u56DE\u590D\u4E2D\u5185\u8054\u7F16\u5199\u6458\u8981\u3002
5554
+ - \u4F7F\u7528 Markdown \u6807\u9898\uFF08\u4F8B\u5982 \`## \u5206\u6790\`\u3001\`## \u6458\u8981\`\uFF09\u7EC4\u7EC7\u4FE1\u606F\uFF0C\u7EDD\u4E0D\u80FD\u4F7F\u7528 XML \u6807\u7B7E\u3002
5168
5555
 
5169
- \`<dcp-message-id>\` and \`<dcp-system-reminder>\` are environment-injected metadata. Do not output them.
5556
+ \`<dcp-message-id>\` \u548C \`<dcp-system-reminder>\` \u662F\u73AF\u5883\u6CE8\u5165\u7684\u5143\u6570\u636E\uFF0C\u4E0D\u8981\u8F93\u51FA\u5B83\u4EEC\u3002
5170
5557
 
5171
- When to compress
5558
+ \u4F55\u65F6\u538B\u7F29
5172
5559
 
5173
- HIGHEST-PRIORITY HARD TRIGGER \u2014 call \`compress\` immediately whenever the conversation TOPIC changes. Compress the previous topic's finished context into a summary.
5560
+ \u6700\u9AD8\u4F18\u5148\u7EA7\u7684\u5F3A\u5236\u89E6\u53D1\u6761\u4EF6\uFF1A\u53EA\u8981\u5BF9\u8BDD\u4E3B\u9898\u53D1\u751F\u53D8\u5316\uFF0C\u7ACB\u5373\u8C03\u7528 \`compress\`\uFF0C\u628A\u4E0A\u4E00\u4E3B\u9898\u5DF2\u7ECF\u5B8C\u6210\u7684\u4E0A\u4E0B\u6587\u538B\u7F29\u4E3A\u6458\u8981\u3002
5174
5561
 
5175
- A topic change is an objective, low-judgment signal, not a subjective call. Any of these counts:
5562
+ \u4E3B\u9898\u53D8\u5316\u662F\u5BA2\u89C2\u4E14\u4E0D\u9700\u8981\u590D\u6742\u5224\u65AD\u7684\u4FE1\u53F7\u3002\u4EE5\u4E0B\u4EFB\u4E00\u60C5\u51B5\u90FD\u7B97\u4E3B\u9898\u53D8\u5316\uFF1A
5176
5563
 
5177
- - The user raises a task or question unrelated to the previous one
5178
- - The focus shifts from one module/file/feature to another
5179
- - The goal shifts from explore/research \u2192 implement, or implement \u2192 debug/verify
5180
- - A distinct problem is resolved and you move to the next
5564
+ - \u7528\u6237\u63D0\u51FA\u4E0E\u4E0A\u4E00\u4EFB\u52A1\u65E0\u5173\u7684\u95EE\u9898
5565
+ - \u5DE5\u4F5C\u91CD\u70B9\u4ECE\u4E00\u4E2A\u6A21\u5757\u3001\u6587\u4EF6\u6216\u529F\u80FD\u8F6C\u79FB\u5230\u53E6\u4E00\u4E2A
5566
+ - \u76EE\u6807\u4ECE\u63A2\u7D22/\u7814\u7A76\u8F6C\u4E3A\u5B9E\u73B0\uFF0C\u6216\u4ECE\u5B9E\u73B0\u8F6C\u4E3A\u8C03\u8BD5/\u9A8C\u8BC1
5567
+ - \u4E00\u4E2A\u72EC\u7ACB\u95EE\u9898\u5DF2\u7ECF\u89E3\u51B3\uFF0C\u5F00\u59CB\u5904\u7406\u4E0B\u4E00\u4E2A\u95EE\u9898
5181
5568
 
5182
- When any occurs, compress the finished range of the previous topic at once. Do not wait. This is the single most effective way to keep context clear.
5569
+ \u51FA\u73B0\u4EFB\u4E00\u60C5\u51B5\u65F6\uFF0C\u7ACB\u5373\u538B\u7F29\u4E0A\u4E00\u4E3B\u9898\u5DF2\u7ECF\u5B8C\u6210\u7684\u8303\u56F4\uFF0C\u4E0D\u8981\u7B49\u5F85\u3002\u8FD9\u662F\u4FDD\u6301\u4E0A\u4E0B\u6587\u6E05\u6670\u6700\u6709\u6548\u7684\u65B9\u6CD5\u3002
5183
5570
 
5184
- Also compress (when a section genuinely closes even without a topic change):
5571
+ \u5373\u4F7F\u4E3B\u9898\u6CA1\u6709\u53D8\u5316\uFF0C\u5728\u4EE5\u4E0B\u9636\u6BB5\u786E\u5B9E\u7ED3\u675F\u65F6\u4E5F\u5E94\u538B\u7F29\uFF1A
5185
5572
 
5186
- - Research is done and findings are settled
5187
- - Implementation is complete and verified
5188
- - Exploration is exhausted and patterns understood
5189
- - Dead-end noise can be dropped without waiting for the whole section to close
5573
+ - \u7814\u7A76\u5B8C\u6210\uFF0C\u7ED3\u8BBA\u5DF2\u7ECF\u786E\u5B9A
5574
+ - \u5B9E\u73B0\u5B8C\u6210\u5E76\u901A\u8FC7\u9A8C\u8BC1
5575
+ - \u63A2\u7D22\u5DF2\u7ECF\u5145\u5206\uFF0C\u76F8\u5173\u89C4\u5F8B\u5DF2\u7ECF\u660E\u786E
5576
+ - \u5DF2\u786E\u8BA4\u65E0\u6548\u7684\u65B9\u5411\u53EF\u4EE5\u4E22\u5F03\uFF0C\u65E0\u9700\u7B49\u5F85\u6574\u4E2A\u9636\u6BB5\u7ED3\u675F
5190
5577
 
5191
- Do NOT compress when:
5578
+ \u4EE5\u4E0B\u60C5\u51B5\u4E0D\u8981\u538B\u7F29\uFF1A
5192
5579
 
5193
- - Raw context is still needed for editing or exact reference
5194
- - The target work is actively in progress
5195
- - You may need the exact code, error message, or file content in the next steps
5580
+ - \u7F16\u8F91\u6216\u7CBE\u786E\u5F15\u7528\u4ECD\u9700\u8981\u539F\u59CB\u4E0A\u4E0B\u6587
5581
+ - \u76EE\u6807\u5DE5\u4F5C\u4ECD\u5728\u8FDB\u884C
5582
+ - \u540E\u7EED\u6B65\u9AA4\u53EF\u80FD\u9700\u8981\u7CBE\u786E\u7684\u4EE3\u7801\u3001\u9519\u8BEF\u4FE1\u606F\u6216\u6587\u4EF6\u5185\u5BB9
5196
5583
 
5197
- Ask yourself before compressing: _"Is this section closed enough to be summary-only?"_
5584
+ \u538B\u7F29\u524D\u5148\u95EE\u81EA\u5DF1\uFF1A_\u201C\u8FD9\u4E00\u90E8\u5206\u662F\u5426\u5DF2\u7ECF\u7ED3\u675F\uFF0C\u53EF\u4EE5\u53EA\u4FDD\u7559\u6458\u8981\uFF1F\u201D_
5198
5585
 
5199
- If the answer is "the topic switched, the old content is no longer needed" \u2014 the answer is always yes.
5586
+ \u5982\u679C\u60C5\u51B5\u662F\u201C\u4E3B\u9898\u5DF2\u7ECF\u5207\u6362\uFF0C\u4E0D\u518D\u9700\u8981\u65E7\u5185\u5BB9\u201D\uFF0C\u7B54\u6848\u59CB\u7EC8\u662F\u201C\u53EF\u4EE5\u201D\u3002
5200
5587
 
5201
- Periodically assess the conversation's signal-to-noise ratio. Use \`compress\` deliberately and provide high-quality summaries. Prioritize stale content to maintain a high-signal context window.
5588
+ \u5B9A\u671F\u8BC4\u4F30\u5BF9\u8BDD\u7684\u6709\u6548\u4FE1\u606F\u6BD4\u4F8B\u3002\u8C28\u614E\u4F7F\u7528 \`compress\` \u5E76\u751F\u6210\u9AD8\u8D28\u91CF\u6458\u8981\uFF0C\u4F18\u5148\u5904\u7406\u8FC7\u671F\u5185\u5BB9\uFF0C\u4FDD\u6301\u9AD8\u8D28\u91CF\u4E0A\u4E0B\u6587\u3002
5202
5589
 
5203
- Keeping a sharp, high-quality context window for best performance is your responsibility.
5590
+ \u4F60\u6709\u8D23\u4EFB\u7EF4\u62A4\u7CBE\u786E\u3001\u9AD8\u8D28\u91CF\u7684\u4E0A\u4E0B\u6587\uFF0C\u4EE5\u83B7\u5F97\u6700\u4F73\u8868\u73B0\u3002
5204
5591
  `;
5205
5592
 
5206
5593
  // lib/prompts/compress-range.ts
@@ -5337,44 +5724,44 @@ var COMPRESS_MESSAGE = `\u5C06\u5BF9\u8BDD\u4E2D\u9009\u5B9A\u7684\u5355\u6761\u
5337
5724
 
5338
5725
  // lib/prompts/context-limit-nudge.ts
5339
5726
  var CONTEXT_LIMIT_NUDGE = `<dcp-system-reminder>
5340
- CRITICAL: max context limit reached.
5727
+ \u7D27\u6025\uFF1A\u5DF2\u8FBE\u5230\u4E0A\u4E0B\u6587\u4E0A\u9650\u3002
5341
5728
 
5342
- You are at or beyond the configured max context threshold. This is an emergency context-recovery moment.
5729
+ \u5F53\u524D\u4E0A\u4E0B\u6587\u5DF2\u8FBE\u5230\u6216\u8D85\u8FC7\u914D\u7F6E\u7684\u6700\u5927\u9608\u503C\uFF0C\u5FC5\u987B\u7ACB\u5373\u56DE\u6536\u4E0A\u4E0B\u6587\u3002
5343
5730
 
5344
- You MUST call \`compress\` now. Do not continue normal exploration until compression is done.
5731
+ \u4F60\u5FC5\u987B\u73B0\u5728\u8C03\u7528 \`compress\`\u3002\u538B\u7F29\u5B8C\u6210\u524D\u4E0D\u8981\u7EE7\u7EED\u5E38\u89C4\u63A2\u7D22\u3002
5345
5732
 
5346
- If mid-atomic-step, finish that one step, then compress immediately.
5733
+ \u5982\u679C\u6B63\u5728\u6267\u884C\u4E00\u4E2A\u4E0D\u53EF\u4E2D\u65AD\u7684\u6B65\u9AA4\uFF0C\u53EA\u5B8C\u6210\u8BE5\u6B65\u9AA4\uFF0C\u968F\u540E\u7ACB\u523B\u538B\u7F29\u3002
5347
5734
 
5348
- Selection
5349
- Start from older, already-finished history and capture as much stale context as possible in one pass.
5350
- Do not select the latest working messages unless they are clearly concluded.
5735
+ \u9009\u62E9\u8303\u56F4
5736
+ \u4ECE\u8F83\u65E9\u4E14\u5DF2\u7ECF\u5B8C\u6210\u7684\u5386\u53F2\u5F00\u59CB\uFF0C\u4E00\u6B21\u8986\u76D6\u5C3D\u53EF\u80FD\u591A\u7684\u8FC7\u671F\u4E0A\u4E0B\u6587\u3002
5737
+ \u9664\u975E\u6700\u65B0\u5DE5\u4F5C\u5DF2\u7ECF\u660E\u786E\u7ED3\u675F\uFF0C\u5426\u5219\u4E0D\u8981\u9009\u62E9\u6700\u8FD1\u7684\u5DE5\u4F5C\u6D88\u606F\u3002
5351
5738
 
5352
- Summary requirements
5353
- Your summary must cover all key details in the selected messages so work can continue.
5354
- If the range contains user messages, preserve their intent fully. For short user messages, prefer direct quotes to avoid semantic drift.
5739
+ \u6458\u8981\u8981\u6C42
5740
+ \u6458\u8981\u5FC5\u987B\u8986\u76D6\u6240\u9009\u6D88\u606F\u4E2D\u7684\u6240\u6709\u5173\u952E\u4FE1\u606F\uFF0C\u786E\u4FDD\u540E\u7EED\u5DE5\u4F5C\u53EF\u4EE5\u7EE7\u7EED\u3002
5741
+ \u5982\u679C\u8303\u56F4\u5305\u542B\u7528\u6237\u6D88\u606F\uFF0C\u5FC5\u987B\u5B8C\u6574\u4FDD\u7559\u7528\u6237\u610F\u56FE\u3002\u5BF9\u4E8E\u8F83\u77ED\u7684\u7528\u6237\u6D88\u606F\uFF0C\u4F18\u5148\u76F4\u63A5\u5F15\u7528\uFF0C\u907F\u514D\u8BED\u4E49\u504F\u79FB\u3002
5355
5742
  </dcp-system-reminder>
5356
5743
  `;
5357
5744
 
5358
5745
  // lib/prompts/turn-nudge.ts
5359
5746
  var TURN_NUDGE = `
5360
- New user turn. This is the moment topic changes are most likely.
5747
+ \u7528\u6237\u5F00\u59CB\u4E86\u65B0\u4E00\u8F6E\u5BF9\u8BDD\uFF0C\u6B64\u65F6\u6700\u5BB9\u6613\u53D1\u751F\u4E3B\u9898\u53D8\u5316\u3002
5361
5748
 
5362
- Is this turn's request a different topic/task from the previous one?
5363
- - New or independent topic: call \`compress\` now to fold the previous topic's finished context into a summary. Its details no longer need verbatim retention.
5364
- - Continuation of the same topic: you may keep it, but still watch for stale accumulation.
5749
+ \u672C\u8F6E\u8BF7\u6C42\u662F\u5426\u4E0E\u4E0A\u4E00\u8F6E\u5C5E\u4E8E\u4E0D\u540C\u4E3B\u9898\u6216\u4EFB\u52A1\uFF1F
5750
+ - \u65B0\u4E3B\u9898\u6216\u72EC\u7ACB\u4EFB\u52A1\uFF1A\u73B0\u5728\u8C03\u7528 \`compress\`\uFF0C\u628A\u4E0A\u4E00\u4E3B\u9898\u5DF2\u7ECF\u5B8C\u6210\u7684\u4E0A\u4E0B\u6587\u538B\u7F29\u4E3A\u6458\u8981\uFF0C\u4E0D\u518D\u9010\u5B57\u4FDD\u7559\u7EC6\u8282\u3002
5751
+ - \u540C\u4E00\u4E3B\u9898\u7684\u5EF6\u7EED\uFF1A\u53EF\u4EE5\u4FDD\u7559\uFF0C\u4F46\u4ECD\u9700\u6CE8\u610F\u8FC7\u671F\u5185\u5BB9\u7684\u7D2F\u79EF\u3002
5365
5752
 
5366
- Topic change is the clearest, most objective compression trigger \u2014 do not miss it.
5753
+ \u4E3B\u9898\u53D8\u5316\u662F\u6700\u660E\u786E\u3001\u6700\u5BA2\u89C2\u7684\u538B\u7F29\u89E6\u53D1\u6761\u4EF6\uFF0C\u4E0D\u8981\u9057\u6F0F\u3002
5367
5754
  `;
5368
5755
 
5369
5756
  // lib/prompts/iteration-nudge.ts
5370
5757
  var ITERATION_NUDGE = `
5371
- Many iterations elapsed without new user input.
5758
+ \u5728\u6CA1\u6709\u65B0\u7528\u6237\u8F93\u5165\u7684\u60C5\u51B5\u4E0B\u5DF2\u7ECF\u6267\u884C\u4E86\u591A\u8F6E\u3002
5372
5759
 
5373
- Has the current iteration's task effectively finished, or has its topic drifted away from earlier steps?
5374
- - If earlier steps belong to a resolved problem or a superseded exploration, call \`compress\` now to fold them into a summary.
5375
- - If the iteration still continues the same unfinished goal and early context is still needed, keep it.
5760
+ \u5F53\u524D\u4EFB\u52A1\u662F\u5426\u5DF2\u7ECF\u5B8C\u6210\uFF0C\u6216\u8005\u5F53\u524D\u4E3B\u9898\u662F\u5426\u5DF2\u7ECF\u504F\u79BB\u65E9\u671F\u6B65\u9AA4\uFF1F
5761
+ - \u5982\u679C\u65E9\u671F\u6B65\u9AA4\u5C5E\u4E8E\u5DF2\u7ECF\u89E3\u51B3\u7684\u95EE\u9898\u6216\u5DF2\u88AB\u66FF\u4EE3\u7684\u63A2\u7D22\uFF0C\u73B0\u5728\u8C03\u7528 \`compress\` \u5C06\u5B83\u4EEC\u538B\u7F29\u4E3A\u6458\u8981\u3002
5762
+ - \u5982\u679C\u4ECD\u5728\u7EE7\u7EED\u540C\u4E00\u4E2A\u672A\u5B8C\u6210\u76EE\u6807\uFF0C\u800C\u4E14\u8FD8\u9700\u8981\u65E9\u671F\u4E0A\u4E0B\u6587\uFF0C\u5219\u4FDD\u7559\u8FD9\u4E9B\u5185\u5BB9\u3002
5376
5763
 
5377
- Do not let finished exploration directions keep occupying context. Topic drift is the best moment to compress.
5764
+ \u4E0D\u8981\u8BA9\u5DF2\u7ECF\u7ED3\u675F\u7684\u63A2\u7D22\u65B9\u5411\u7EE7\u7EED\u5360\u7528\u4E0A\u4E0B\u6587\u3002\u4E3B\u9898\u53D1\u751F\u504F\u79FB\u65F6\u6700\u9002\u5408\u538B\u7F29\u3002
5378
5765
  `;
5379
5766
 
5380
5767
  // lib/prompts/extensions/system.ts
@@ -5613,6 +6000,7 @@ var PromptStore = class {
5613
6000
  paths;
5614
6001
  customPromptsEnabled;
5615
6002
  runtimePrompts;
6003
+ lastReloadAt = 0;
5616
6004
  constructor(logger, workingDirectory, customPromptsEnabled = false) {
5617
6005
  this.logger = logger;
5618
6006
  this.paths = resolvePromptPaths(workingDirectory);
@@ -5627,6 +6015,9 @@ var PromptStore = class {
5627
6015
  return { ...this.runtimePrompts };
5628
6016
  }
5629
6017
  reload() {
6018
+ const now = Date.now();
6019
+ if (now - this.lastReloadAt < 1e3) return;
6020
+ this.lastReloadAt = now;
5630
6021
  const nextPrompts = createBundledRuntimePrompts();
5631
6022
  if (!this.customPromptsEnabled) {
5632
6023
  this.runtimePrompts = nextPrompts;
@@ -5865,13 +6256,13 @@ var stripHallucinationsFromString = (text) => {
5865
6256
  };
5866
6257
  var stripHallucinations = (messages) => {
5867
6258
  for (const message of messages) {
6259
+ if (message.info.role !== "assistant") {
6260
+ continue;
6261
+ }
5868
6262
  for (const part of message.parts) {
5869
6263
  if (part.type === "text" && typeof part.text === "string") {
5870
6264
  part.text = stripHallucinationsFromString(part.text);
5871
6265
  }
5872
- if (part.type === "tool" && part.state?.status === "completed" && typeof part.state.output === "string") {
5873
- part.state.output = stripHallucinationsFromString(part.state.output);
5874
- }
5875
6266
  }
5876
6267
  }
5877
6268
  };
@@ -5966,7 +6357,8 @@ var filterCompressedRanges = (state, logger, config, messages) => {
5966
6357
  return;
5967
6358
  }
5968
6359
  const result = [];
5969
- for (const msg of messages) {
6360
+ for (let msgIndex = 0; msgIndex < messages.length; msgIndex++) {
6361
+ const msg = messages[msgIndex];
5970
6362
  const msgId = msg.info.id;
5971
6363
  const blockId = state.prune.messages.activeByAnchorMessageId.get(msgId);
5972
6364
  const summary = blockId !== void 0 ? state.prune.messages.blocksById.get(blockId) : void 0;
@@ -5978,7 +6370,6 @@ var filterCompressedRanges = (state, logger, config, messages) => {
5978
6370
  blockId: summary.blockId
5979
6371
  });
5980
6372
  } else {
5981
- const msgIndex = messages.indexOf(msg);
5982
6373
  const userMessage = getLastUserMessage(messages, msgIndex);
5983
6374
  if (userMessage) {
5984
6375
  const userInfo = userMessage.info;
@@ -6105,7 +6496,7 @@ var compressPermission = (state, config) => {
6105
6496
  return state.compressPermission ?? config.compress.permission;
6106
6497
  };
6107
6498
  var syncCompressPermissionState = (state, config, hostPermissions, messages) => {
6108
- const activeAgent = getLastUserMessage(messages)?.info.agent;
6499
+ const activeAgent = getLastUserMessage(messages)?.info?.agent;
6109
6500
  state.compressPermission = resolveEffectiveCompressPermission(
6110
6501
  config.compress.permission,
6111
6502
  hostPermissions,
@@ -6243,11 +6634,13 @@ function listPriorityRefsBeforeIndex(messages, priorities, anchorIndex, priority
6243
6634
 
6244
6635
  // lib/messages/inject/utils.ts
6245
6636
  var MESSAGE_MODE_NUDGE_PRIORITY = "high";
6246
- function getNudgeFrequency(_config) {
6247
- return 1;
6637
+ function getNudgeFrequency(config) {
6638
+ const value = config.compress.nudgeFrequency;
6639
+ return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
6248
6640
  }
6249
- function getIterationNudgeThreshold(_config) {
6250
- return 1;
6641
+ function getIterationNudgeThreshold(config) {
6642
+ const value = config.compress.iterationNudgeThreshold;
6643
+ return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
6251
6644
  }
6252
6645
  function findLastNonIgnoredMessage(messages) {
6253
6646
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -6360,10 +6753,28 @@ function buildMessagePriorityGuidance(messages, compressionPriorities, anchorInd
6360
6753
  const priorityLabel = `${priority[0].toUpperCase()}${priority.slice(1)}`;
6361
6754
  return renderMessagePriorityGuidance(priorityLabel, refs);
6362
6755
  }
6363
- function injectAnchoredNudge(message, nudgeText) {
6756
+ function normalizeNudgeSignature(baseNudgeText) {
6757
+ return baseNudgeText.replace(/<\/dcp-system-reminder>\s*$/, "").replace(/^\n+/, "").trim();
6758
+ }
6759
+ function messageContainsNudgeSignature(message, baseNudgeText) {
6760
+ const signature = normalizeNudgeSignature(baseNudgeText);
6761
+ if (!signature) {
6762
+ return false;
6763
+ }
6764
+ for (const part of message.parts) {
6765
+ if (part.type === "text" && typeof part.text === "string" && part.text.includes(signature)) {
6766
+ return true;
6767
+ }
6768
+ }
6769
+ return false;
6770
+ }
6771
+ function injectAnchoredNudge(message, nudgeText, baseNudgeText) {
6364
6772
  if (!nudgeText.trim()) {
6365
6773
  return;
6366
6774
  }
6775
+ if (messageContainsNudgeSignature(message, baseNudgeText)) {
6776
+ return;
6777
+ }
6367
6778
  if (message.info.role === "user") {
6368
6779
  if (appendToLastTextPart(message, nudgeText)) {
6369
6780
  return;
@@ -6424,7 +6835,7 @@ function applyRangeModeAnchoredNudge(anchorMessageIds, messages, baseNudgeText,
6424
6835
  return;
6425
6836
  }
6426
6837
  for (const { message } of collectAnchoredMessages(anchorMessageIds, messages)) {
6427
- injectAnchoredNudge(message, nudgeText);
6838
+ injectAnchoredNudge(message, nudgeText, baseNudgeText);
6428
6839
  }
6429
6840
  }
6430
6841
  function applyMessageModeAnchoredNudge(anchorMessageIds, messages, baseNudgeText, availableMessageIdGuidance, compressionPriorities) {
@@ -6437,7 +6848,7 @@ function applyMessageModeAnchoredNudge(anchorMessageIds, messages, baseNudgeText
6437
6848
  );
6438
6849
  const combinedGuidance = [availableMessageIdGuidance, priorityGuidance].filter((g) => g.trim().length > 0).join("\n\n");
6439
6850
  const nudgeText = appendGuidanceToDcpTag(baseNudgeText, combinedGuidance);
6440
- injectAnchoredNudge(message, nudgeText);
6851
+ injectAnchoredNudge(message, nudgeText, baseNudgeText);
6441
6852
  }
6442
6853
  }
6443
6854
  function applyAnchoredNudges(state, config, messages, prompts, compressionPriorities) {
@@ -6493,11 +6904,44 @@ function applyAnchoredNudges(state, config, messages, prompts, compressionPriori
6493
6904
  }
6494
6905
 
6495
6906
  // lib/messages/inject/inject.ts
6907
+ function userMessagesAfterLastAnchor(state, messages) {
6908
+ let latestAnchorIndex = -1;
6909
+ for (let index = messages.length - 1; index >= 0; index--) {
6910
+ if (state.nudges.turnNudgeAnchors.has(messages[index].info.id)) {
6911
+ latestAnchorIndex = index;
6912
+ break;
6913
+ }
6914
+ }
6915
+ let userCount = 0;
6916
+ for (let index = latestAnchorIndex + 1; index < messages.length; index++) {
6917
+ const message = messages[index];
6918
+ if (message.info.role === "user" && !isIgnoredUserMessage(message)) {
6919
+ userCount++;
6920
+ }
6921
+ }
6922
+ return userCount;
6923
+ }
6496
6924
  var injectCompressNudges = (state, config, logger, messages, prompts, compressionPriorities) => {
6925
+ const clearPendingBoundary = () => {
6926
+ if (!state.nudges.boundaryPending) {
6927
+ return;
6928
+ }
6929
+ state.nudges.boundaryPending = false;
6930
+ if (!state.sessionId) {
6931
+ return;
6932
+ }
6933
+ void saveSessionState(state, logger).catch(
6934
+ (error) => logger.warn("Failed to persist boundary nudge cleanup", {
6935
+ error: error instanceof Error ? error.message : String(error)
6936
+ })
6937
+ );
6938
+ };
6497
6939
  if (compressPermission(state, config) === "deny") {
6940
+ clearPendingBoundary();
6498
6941
  return;
6499
6942
  }
6500
6943
  if (state.manualMode) {
6944
+ clearPendingBoundary();
6501
6945
  return;
6502
6946
  }
6503
6947
  const lastMessage = findLastNonIgnoredMessage(messages);
@@ -6523,10 +6967,16 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
6523
6967
  state.nudges.contextLimitAnchors.clear();
6524
6968
  state.nudges.turnNudgeAnchors.clear();
6525
6969
  state.nudges.iterationNudgeAnchors.clear();
6526
- void saveSessionState(state, logger);
6970
+ state.nudges.boundaryPending = false;
6971
+ void saveSessionState(state, logger).catch(
6972
+ (error) => logger.warn("Failed to persist context-limit nudge", {
6973
+ error: error instanceof Error ? error.message : String(error)
6974
+ })
6975
+ );
6527
6976
  return;
6528
6977
  }
6529
6978
  const { providerId, modelId } = getModelInfo(messages);
6979
+ const boundaryNudgeEnabled = config.compress.boundaryNudge !== false;
6530
6980
  let anchorsChanged = false;
6531
6981
  const { overMaxLimit, overMinLimit } = isContextOverLimits(
6532
6982
  config,
@@ -6536,13 +6986,18 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
6536
6986
  messages
6537
6987
  );
6538
6988
  if (!overMinLimit) {
6539
- const hadTurnAnchors = state.nudges.turnNudgeAnchors.size > 0;
6540
6989
  const hadIterationAnchors = state.nudges.iterationNudgeAnchors.size > 0;
6541
- if (hadTurnAnchors || hadIterationAnchors) {
6542
- state.nudges.turnNudgeAnchors.clear();
6990
+ if (hadIterationAnchors) {
6543
6991
  state.nudges.iterationNudgeAnchors.clear();
6544
6992
  anchorsChanged = true;
6545
6993
  }
6994
+ if (!boundaryNudgeEnabled) {
6995
+ const hadTurnAnchors = state.nudges.turnNudgeAnchors.size > 0;
6996
+ if (hadTurnAnchors) {
6997
+ state.nudges.turnNudgeAnchors.clear();
6998
+ anchorsChanged = true;
6999
+ }
7000
+ }
6546
7001
  }
6547
7002
  if (overMaxLimit) {
6548
7003
  if (lastMessage) {
@@ -6560,7 +7015,7 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
6560
7015
  }
6561
7016
  } else if (overMinLimit) {
6562
7017
  const isLastMessageUser = lastMessage?.message.info.role === "user";
6563
- if (isLastMessageUser && lastAssistantMessage) {
7018
+ if (!boundaryNudgeEnabled && isLastMessageUser && lastAssistantMessage) {
6564
7019
  const previousSize = state.nudges.turnNudgeAnchors.size;
6565
7020
  state.nudges.turnNudgeAnchors.add(lastMessage.message.info.id);
6566
7021
  state.nudges.turnNudgeAnchors.add(lastAssistantMessage.info.id);
@@ -6592,9 +7047,40 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
6592
7047
  }
6593
7048
  }
6594
7049
  }
7050
+ if (boundaryNudgeEnabled && !overMaxLimit) {
7051
+ const lastUserMessage = getLastUserMessage(messages);
7052
+ const isLastMessageUser = lastMessage?.message.info.role === "user";
7053
+ const hasPendingBoundary = state.nudges.boundaryPending;
7054
+ if (lastUserMessage && lastAssistantMessage) {
7055
+ const lastUserMessageIndex = messages.findIndex(
7056
+ (message) => message.info.id === lastUserMessage.info.id
7057
+ );
7058
+ const lastAssistantMessageIndex = messages.findIndex(
7059
+ (message) => message.info.id === lastAssistantMessage.info.id
7060
+ );
7061
+ const alreadyAnchored = state.nudges.turnNudgeAnchors.has(lastUserMessage.info.id) && state.nudges.turnNudgeAnchors.has(lastAssistantMessage.info.id);
7062
+ if (!alreadyAnchored && lastUserMessageIndex >= 0 && lastAssistantMessageIndex >= 0) {
7063
+ const interval = getNudgeFrequency(config);
7064
+ const shouldAnchor = hasPendingBoundary || isLastMessageUser && userMessagesAfterLastAnchor(state, messages) >= interval;
7065
+ if (shouldAnchor) {
7066
+ state.nudges.turnNudgeAnchors.add(lastAssistantMessage.info.id);
7067
+ state.nudges.turnNudgeAnchors.add(lastUserMessage.info.id);
7068
+ anchorsChanged = true;
7069
+ }
7070
+ }
7071
+ if (hasPendingBoundary) {
7072
+ state.nudges.boundaryPending = false;
7073
+ anchorsChanged = true;
7074
+ }
7075
+ }
7076
+ }
6595
7077
  applyAnchoredNudges(state, config, messages, prompts, compressionPriorities);
6596
7078
  if (anchorsChanged) {
6597
- void saveSessionState(state, logger);
7079
+ void saveSessionState(state, logger).catch(
7080
+ (error) => logger.warn("Failed to persist compression nudge", {
7081
+ error: error instanceof Error ? error.message : String(error)
7082
+ })
7083
+ );
6598
7084
  }
6599
7085
  };
6600
7086
  var injectMessageIds = (state, config, messages, compressionPriorities) => {
@@ -6661,6 +7147,7 @@ var injectExtendedSubAgentResults = async (client, state, logger, messages, allo
6661
7147
  if (!allowSubAgents) {
6662
7148
  return;
6663
7149
  }
7150
+ const tasks = [];
6664
7151
  for (const message of messages) {
6665
7152
  const parts = Array.isArray(message.parts) ? message.parts : [];
6666
7153
  for (const part of parts) {
@@ -6673,40 +7160,43 @@ var injectExtendedSubAgentResults = async (client, state, logger, messages, allo
6673
7160
  if (part.state?.status !== "completed" || typeof part.state.output !== "string") {
6674
7161
  continue;
6675
7162
  }
6676
- const cachedResult = state.subAgentResultCache.get(part.callID);
6677
- if (cachedResult !== void 0) {
6678
- if (cachedResult) {
6679
- part.state.output = stripHallucinationsFromString(
6680
- mergeSubagentResult(part.state.output, cachedResult)
6681
- );
6682
- }
6683
- continue;
6684
- }
6685
- const subAgentSessionId = getSubAgentId(part);
6686
- if (!subAgentSessionId) {
6687
- continue;
6688
- }
6689
- let subAgentMessages = [];
6690
- try {
6691
- subAgentMessages = await fetchSubAgentMessages(client, subAgentSessionId);
6692
- } catch (error) {
6693
- logger.warn("Failed to fetch subagent session for output expansion", {
6694
- subAgentSessionId,
6695
- callID: part.callID,
6696
- error: error instanceof Error ? error.message : String(error)
6697
- });
6698
- continue;
6699
- }
6700
- const subAgentResultText = buildSubagentResultText(subAgentMessages);
6701
- if (!subAgentResultText) {
6702
- continue;
6703
- }
6704
- state.subAgentResultCache.set(part.callID, subAgentResultText);
6705
- part.state.output = stripHallucinationsFromString(
6706
- mergeSubagentResult(part.state.output, subAgentResultText)
6707
- );
7163
+ tasks.push({ part });
6708
7164
  }
6709
7165
  }
7166
+ await runWithConcurrency(tasks, 4, async ({ part }) => {
7167
+ const cachedResult = getCachedSubAgentResult(state, part.callID);
7168
+ if (cachedResult !== void 0) {
7169
+ if (cachedResult) {
7170
+ part.state.output = stripHallucinationsFromString(
7171
+ mergeSubagentResult(part.state.output, cachedResult)
7172
+ );
7173
+ }
7174
+ return;
7175
+ }
7176
+ const subAgentSessionId = getSubAgentId(part);
7177
+ if (!subAgentSessionId) {
7178
+ return;
7179
+ }
7180
+ let subAgentMessages = [];
7181
+ try {
7182
+ subAgentMessages = await fetchSubAgentMessages(client, subAgentSessionId);
7183
+ } catch (error) {
7184
+ logger.warn("Failed to fetch subagent session for output expansion", {
7185
+ subAgentSessionId,
7186
+ callID: part.callID,
7187
+ error: error instanceof Error ? error.message : String(error)
7188
+ });
7189
+ return;
7190
+ }
7191
+ const subAgentResultText = buildSubagentResultText(subAgentMessages);
7192
+ if (!subAgentResultText) {
7193
+ return;
7194
+ }
7195
+ const cached = cacheSubAgentResult(state, part.callID, subAgentResultText);
7196
+ part.state.output = stripHallucinationsFromString(
7197
+ mergeSubagentResult(part.state.output, cached)
7198
+ );
7199
+ });
6710
7200
  };
6711
7201
 
6712
7202
  // lib/messages/reasoning-strip.ts
@@ -7294,11 +7784,7 @@ var COMPRESS_TRIGGER_PROMPT = [
7294
7784
  function getTriggerPrompt(tool3, state, config, messages, userFocus) {
7295
7785
  const base = COMPRESS_TRIGGER_PROMPT;
7296
7786
  const compressedBlockGuidance = config.compress.mode === "message" ? "" : buildCompressedBlockGuidance(state);
7297
- const availableMessageIdGuidance = buildAvailableMessageIdGuidance(
7298
- state,
7299
- config,
7300
- messages
7301
- );
7787
+ const availableMessageIdGuidance = buildAvailableMessageIdGuidance(state, config, messages);
7302
7788
  const sections = [base, availableMessageIdGuidance, compressedBlockGuidance];
7303
7789
  if (userFocus && userFocus.trim().length > 0) {
7304
7790
  sections.push(`Additional user focus:
@@ -7811,13 +8297,14 @@ var INTERNAL_AGENT_SIGNATURES = [
7811
8297
  "You are an anchored context summarization assistant for coding sessions",
7812
8298
  "Summarize what was done in this conversation"
7813
8299
  ];
7814
- function createSystemPromptHandler(state, logger, config, prompts) {
8300
+ function createSystemPromptHandler(target, logger, config, prompts) {
7815
8301
  return async (input, output) => {
7816
- if (input.model?.limit?.context) {
8302
+ const state = input.sessionID ? resolveSessionState(target, input.sessionID) : target instanceof SessionStateStore ? void 0 : target;
8303
+ if (state && input.model?.limit?.context) {
7817
8304
  state.modelContextLimit = input.model.limit.context;
7818
8305
  logger.debug("Cached model context limit", { limit: state.modelContextLimit });
7819
8306
  }
7820
- if (state.isSubAgent && !config.experimental.allowSubAgents) {
8307
+ if (state?.isSubAgent && !config.experimental.allowSubAgents) {
7821
8308
  return;
7822
8309
  }
7823
8310
  const systemText = output.system.join("\n");
@@ -7825,7 +8312,7 @@ function createSystemPromptHandler(state, logger, config, prompts) {
7825
8312
  logger.info("Skipping DCP system prompt injection for internal agent");
7826
8313
  return;
7827
8314
  }
7828
- const effectivePermission = input.sessionID && state.sessionId === input.sessionID ? compressPermission(state, config) : config.compress.permission;
8315
+ const effectivePermission = input.sessionID && state?.sessionId === input.sessionID ? compressPermission(state, config) : config.compress.permission;
7829
8316
  if (effectivePermission === "deny") {
7830
8317
  return;
7831
8318
  }
@@ -7834,8 +8321,8 @@ function createSystemPromptHandler(state, logger, config, prompts) {
7834
8321
  const newPrompt = renderSystemPrompt(
7835
8322
  runtimePrompts,
7836
8323
  buildProtectedToolsExtension(config.compress.protectedTools),
7837
- !!state.manualMode,
7838
- state.isSubAgent && config.experimental.allowSubAgents
8324
+ !!state?.manualMode,
8325
+ !!state?.isSubAgent && config.experimental.allowSubAgents
7839
8326
  );
7840
8327
  if (output.system.length > 0) {
7841
8328
  output.system[output.system.length - 1] += "\n\n" + newPrompt;
@@ -7844,7 +8331,7 @@ function createSystemPromptHandler(state, logger, config, prompts) {
7844
8331
  }
7845
8332
  };
7846
8333
  }
7847
- function createChatMessageTransformHandler(client, state, logger, config, prompts, hostPermissions) {
8334
+ function createChatMessageTransformHandler(client, target, logger, config, prompts, hostPermissions) {
7848
8335
  return async (input, output) => {
7849
8336
  const receivedMessages = Array.isArray(output.messages) ? output.messages.length : 0;
7850
8337
  const messages = filterMessagesInPlace(output.messages);
@@ -7854,7 +8341,14 @@ function createChatMessageTransformHandler(client, state, logger, config, prompt
7854
8341
  usable: messages.length
7855
8342
  });
7856
8343
  }
7857
- await checkSession(client, state, logger, output.messages, config.manualMode.enabled);
8344
+ const state = await checkSession(
8345
+ client,
8346
+ target,
8347
+ logger,
8348
+ output.messages,
8349
+ config.manualMode.enabled
8350
+ );
8351
+ if (!state) return;
7858
8352
  syncCompressPermissionState(state, config, hostPermissions, output.messages);
7859
8353
  if (state.isSubAgent && !config.experimental.allowSubAgents) {
7860
8354
  return;
@@ -7900,7 +8394,7 @@ function createChatMessageTransformHandler(client, state, logger, config, prompt
7900
8394
  }
7901
8395
  };
7902
8396
  }
7903
- function createCommandExecuteHandler(client, state, logger, config, workingDirectory, hostPermissions) {
8397
+ function createCommandExecuteHandler(client, target, logger, config, workingDirectory, hostPermissions) {
7904
8398
  return async (input, output) => {
7905
8399
  if (!config.commands.enabled) {
7906
8400
  return;
@@ -7910,9 +8404,9 @@ function createCommandExecuteHandler(client, state, logger, config, workingDirec
7910
8404
  path: { id: input.sessionID }
7911
8405
  });
7912
8406
  const messages = filterMessages(messagesResponse.data || messagesResponse);
7913
- await ensureSessionInitialized(
8407
+ const state = await initializeSessionState(
7914
8408
  client,
7915
- state,
8409
+ target,
7916
8410
  input.sessionID,
7917
8411
  logger,
7918
8412
  messages,
@@ -7997,8 +8491,50 @@ function createTextCompleteHandler() {
7997
8491
  output.text = stripHallucinationsFromString(output.text);
7998
8492
  };
7999
8493
  }
8000
- function createEventHandler(state, logger) {
8494
+ function createEventHandler(target, logger, config) {
8495
+ const markBoundaryPending = async (state) => {
8496
+ if (!state) return;
8497
+ if (config.compress.boundaryNudge === false) return;
8498
+ if (state.manualMode) return;
8499
+ if (state.isSubAgent && !config.experimental.allowSubAgents) return;
8500
+ if (compressPermission(state, config) === "deny") return;
8501
+ if (!state.sessionId) return;
8502
+ if (state.nudges.boundaryPending) return;
8503
+ state.nudges.boundaryPending = true;
8504
+ await saveSessionState(state, logger);
8505
+ logger.debug("Marked boundary nudge pending", { sessionId: state.sessionId });
8506
+ };
8001
8507
  return async (input) => {
8508
+ if (input.event.type === "session.idle") {
8509
+ const sessionId = input.event.properties?.sessionID;
8510
+ if (typeof sessionId === "string") {
8511
+ const state2 = target instanceof SessionStateStore ? target.peek(sessionId) : target.sessionId === sessionId ? target : void 0;
8512
+ await markBoundaryPending(state2).catch(
8513
+ (error) => logger.warn("Failed to persist boundary nudge", {
8514
+ error: error instanceof Error ? error.message : String(error)
8515
+ })
8516
+ );
8517
+ }
8518
+ return;
8519
+ }
8520
+ if (input.event.type === "vcs.branch.updated") {
8521
+ if (target instanceof SessionStateStore) {
8522
+ const sessions = [];
8523
+ target.forEach((state2) => sessions.push(state2));
8524
+ await Promise.all(
8525
+ sessions.map(
8526
+ (state2) => markBoundaryPending(state2).catch(
8527
+ (error) => logger.warn("Failed to persist boundary nudge", {
8528
+ error: error instanceof Error ? error.message : String(error)
8529
+ })
8530
+ )
8531
+ )
8532
+ );
8533
+ } else {
8534
+ await markBoundaryPending(target);
8535
+ }
8536
+ return;
8537
+ }
8002
8538
  const eventTime = typeof input.event?.time === "number" && Number.isFinite(input.event.time) ? input.event.time : typeof input.event?.properties?.time === "number" && Number.isFinite(input.event.properties.time) ? input.event.properties.time : void 0;
8003
8539
  if (input.event.type !== "message.part.updated") {
8004
8540
  return;
@@ -8007,16 +8543,25 @@ function createEventHandler(state, logger) {
8007
8543
  if (part?.type !== "tool" || part.tool !== "compress") {
8008
8544
  return;
8009
8545
  }
8546
+ if (typeof part.callID !== "string" || typeof part.messageID !== "string") {
8547
+ return;
8548
+ }
8549
+ const eventSessionId = typeof input.event.properties?.sessionID === "string" ? input.event.properties.sessionID : typeof part.sessionID === "string" ? part.sessionID : void 0;
8550
+ const state = target instanceof SessionStateStore ? target.resolveEventState(eventSessionId, part.messageID) : target;
8551
+ const key = buildCompressionTimingKey(part.messageID, part.callID);
8552
+ if (state) pruneCompressionTiming(state);
8010
8553
  if (part.state.status === "pending") {
8011
- if (typeof part.callID !== "string" || typeof part.messageID !== "string") {
8554
+ const startedAt = eventTime ?? Date.now();
8555
+ if (!state && target instanceof SessionStateStore) {
8556
+ target.recordOrphanStart(key, startedAt);
8012
8557
  return;
8013
8558
  }
8014
- const startedAt = eventTime ?? Date.now();
8015
- const key = buildCompressionTimingKey(part.messageID, part.callID);
8559
+ if (!state) return;
8016
8560
  if (state.compressionTiming.startsByCallId.has(key)) {
8017
8561
  return;
8018
8562
  }
8019
8563
  state.compressionTiming.startsByCallId.set(key, startedAt);
8564
+ state.compressionTiming.recordedAtByCallId.set(key, Date.now());
8020
8565
  logger.debug("Recorded compression start", {
8021
8566
  messageID: part.messageID,
8022
8567
  callID: part.callID,
@@ -8025,20 +8570,26 @@ function createEventHandler(state, logger) {
8025
8570
  return;
8026
8571
  }
8027
8572
  if (part.state.status === "completed") {
8028
- if (typeof part.callID !== "string" || typeof part.messageID !== "string") {
8029
- return;
8030
- }
8031
- const key = buildCompressionTimingKey(part.messageID, part.callID);
8032
- const start = consumeCompressionStart(state, part.messageID, part.callID);
8573
+ const start = state ? consumeCompressionStart(state, part.messageID, part.callID) : target instanceof SessionStateStore ? target.consumeOrphanStart(key) : void 0;
8033
8574
  const durationMs = resolveCompressionDuration(start, eventTime, part.state.time);
8034
8575
  if (typeof durationMs !== "number") {
8035
8576
  return;
8036
8577
  }
8578
+ if (!state && target instanceof SessionStateStore) {
8579
+ target.recordOrphanPending(key, {
8580
+ messageId: part.messageID,
8581
+ callId: part.callID,
8582
+ durationMs
8583
+ });
8584
+ return;
8585
+ }
8586
+ if (!state) return;
8037
8587
  state.compressionTiming.pendingByCallId.set(key, {
8038
8588
  messageId: part.messageID,
8039
8589
  callId: part.callID,
8040
8590
  durationMs
8041
8591
  });
8592
+ state.compressionTiming.recordedAtByCallId.set(key, Date.now());
8042
8593
  const updates = applyPendingCompressionDurations(state);
8043
8594
  if (updates === 0) {
8044
8595
  return;
@@ -8055,44 +8606,19 @@ function createEventHandler(state, logger) {
8055
8606
  if (part.state.status === "running") {
8056
8607
  return;
8057
8608
  }
8058
- if (typeof part.callID === "string" && typeof part.messageID === "string") {
8609
+ if (state) {
8059
8610
  state.compressionTiming.startsByCallId.delete(
8060
8611
  buildCompressionTimingKey(part.messageID, part.callID)
8061
8612
  );
8613
+ state.compressionTiming.recordedAtByCallId.delete(key);
8614
+ } else if (target instanceof SessionStateStore) {
8615
+ target.deleteOrphanStart(key);
8062
8616
  }
8063
8617
  };
8064
8618
  }
8065
8619
 
8066
- // lib/auth.ts
8067
- function isSecureMode() {
8068
- return !!process.env.OPENCODE_SERVER_PASSWORD;
8069
- }
8070
- function getAuthorizationHeader() {
8071
- const password = process.env.OPENCODE_SERVER_PASSWORD;
8072
- if (!password) return void 0;
8073
- const username = process.env.OPENCODE_SERVER_USERNAME ?? "opencode";
8074
- const credentials = Buffer.from(`${username}:${password}`).toString("base64");
8075
- return `Basic ${credentials}`;
8076
- }
8077
- function configureClientAuth(client) {
8078
- const authHeader = getAuthorizationHeader();
8079
- if (!authHeader) {
8080
- return client;
8081
- }
8082
- const innerClient = client._client || client.client;
8083
- if (innerClient?.interceptors?.request) {
8084
- innerClient.interceptors.request.use((request) => {
8085
- if (!request.headers.has("Authorization")) {
8086
- request.headers.set("Authorization", authHeader);
8087
- }
8088
- return request;
8089
- });
8090
- }
8091
- return client;
8092
- }
8093
-
8094
8620
  // lib/update.ts
8095
- import { readFile as readFile2, rm } from "fs/promises";
8621
+ import { readFile as readFile2 } from "fs/promises";
8096
8622
  import { basename, dirname as dirname3, join as join5 } from "path";
8097
8623
  import { fileURLToPath } from "url";
8098
8624
  var PACKAGE_NAME = "@lexwdex-org/opencode-dcp";
@@ -8101,15 +8627,16 @@ function startAutoUpdate(ctx, enabled) {
8101
8627
  const controller = new AbortController();
8102
8628
  const timeout = setTimeout(() => controller.abort(), 1e4);
8103
8629
  void checkAutoUpdate(controller.signal).then((result) => {
8104
- if (!result.updated) return;
8630
+ if (!result.available) return;
8105
8631
  setTimeout(() => {
8106
- ctx.client.tui.showToast({
8632
+ void ctx.client.tui.showToast({
8107
8633
  body: {
8108
- title: "DCP update ready",
8109
- message: `Updated ${result.name} from ${result.current} to ${result.latest}. Restart OpenCode to finish.`,
8634
+ title: "DCP update available",
8635
+ message: `${result.name} ${result.latest} is available (current ${result.current}). Reinstall the plugin to update.`,
8110
8636
  variant: "info",
8111
8637
  duration: 7e3
8112
8638
  }
8639
+ }).catch(() => {
8113
8640
  });
8114
8641
  }, 5e3);
8115
8642
  }).catch(() => {
@@ -8117,25 +8644,14 @@ function startAutoUpdate(ctx, enabled) {
8117
8644
  }
8118
8645
  async function checkAutoUpdate(signal) {
8119
8646
  const packageDir = await findPackageDir(PACKAGE_NAME);
8120
- if (!packageDir) return { updated: false };
8647
+ if (!packageDir) return { available: false };
8121
8648
  const pkg = await readPackageJson(join5(packageDir, "package.json"));
8122
- if (!pkg?.name || !pkg.version) return { updated: false };
8649
+ if (!pkg?.name || !pkg.version) return { available: false };
8123
8650
  const latest = await fetchLatestVersion(pkg.name, signal);
8124
- if (!latest || !isVersionNewer(latest, pkg.version)) return { updated: false };
8125
- const removeDir = await updateRemoveDir(packageDir, pkg.name);
8126
- if (!removeDir) return { updated: false };
8127
- try {
8128
- await rm(removeDir, { recursive: true, force: true });
8129
- } catch {
8130
- return {
8131
- updated: false,
8132
- error: "remove_failed",
8133
- name: pkg.name,
8134
- current: pkg.version,
8135
- latest
8136
- };
8137
- }
8138
- return { updated: true, name: pkg.name, current: pkg.version, latest };
8651
+ if (!latest || !isVersionNewer(latest, pkg.version)) return { available: false };
8652
+ const wrapperDir = await findAutoUpdateWrapperDir(packageDir, pkg.name);
8653
+ if (!wrapperDir) return { available: false };
8654
+ return { available: true, name: pkg.name, current: pkg.version, latest };
8139
8655
  }
8140
8656
  async function findPackageDir(name) {
8141
8657
  let dir = dirname3(fileURLToPath(import.meta.url));
@@ -8147,7 +8663,7 @@ async function findPackageDir(name) {
8147
8663
  dir = parent;
8148
8664
  }
8149
8665
  }
8150
- async function updateRemoveDir(packageDir, name) {
8666
+ async function findAutoUpdateWrapperDir(packageDir, name) {
8151
8667
  const packageParent = dirname3(packageDir);
8152
8668
  const nodeModulesDir = basename(packageParent).startsWith("@") ? dirname3(packageParent) : packageParent;
8153
8669
  if (basename(nodeModulesDir) !== "node_modules") return void 0;
@@ -8243,36 +8759,33 @@ var server = (async (ctx) => {
8243
8759
  return {};
8244
8760
  }
8245
8761
  const logger = new Logger(config.debug);
8246
- const state = createSessionState();
8762
+ const states = new SessionStateStore(createSessionState);
8247
8763
  const prompts = new PromptStore(logger, ctx.directory, config.experimental.customPrompts);
8248
8764
  const hostPermissions = {
8249
8765
  global: void 0,
8250
8766
  agents: {}
8251
8767
  };
8252
- if (isSecureMode()) {
8253
- configureClientAuth(ctx.client);
8254
- }
8255
8768
  logger.info("DCP initialized", {
8256
8769
  strategies: config.strategies
8257
8770
  });
8258
8771
  startAutoUpdate(ctx, config.autoUpdate);
8259
8772
  const compressToolContext = {
8260
8773
  client: ctx.client,
8261
- state,
8774
+ state: states,
8262
8775
  logger,
8263
8776
  config,
8264
8777
  prompts
8265
8778
  };
8266
8779
  return {
8267
8780
  "experimental.chat.system.transform": createSystemPromptHandler(
8268
- state,
8781
+ states,
8269
8782
  logger,
8270
8783
  config,
8271
8784
  prompts
8272
8785
  ),
8273
8786
  "experimental.chat.messages.transform": createChatMessageTransformHandler(
8274
8787
  ctx.client,
8275
- state,
8788
+ states,
8276
8789
  logger,
8277
8790
  config,
8278
8791
  prompts,
@@ -8281,13 +8794,13 @@ var server = (async (ctx) => {
8281
8794
  "experimental.text.complete": createTextCompleteHandler(),
8282
8795
  "command.execute.before": createCommandExecuteHandler(
8283
8796
  ctx.client,
8284
- state,
8797
+ states,
8285
8798
  logger,
8286
8799
  config,
8287
8800
  ctx.directory,
8288
8801
  hostPermissions
8289
8802
  ),
8290
- event: createEventHandler(state, logger),
8803
+ event: createEventHandler(states, logger, config),
8291
8804
  tool: {
8292
8805
  ...config.compress.permission !== "deny" && {
8293
8806
  compress: config.compress.mode === "message" ? createCompressMessageTool(compressToolContext) : createCompressRangeTool(compressToolContext)