@lexwdex-org/opencode-dcp 3.4.8 → 3.4.9

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 (80) hide show
  1. package/README.en.md +2 -2
  2. package/README.md +2 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +702 -348
  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.map +1 -1
  39. package/dist/lib/hooks.d.ts +6 -5
  40. package/dist/lib/hooks.d.ts.map +1 -1
  41. package/dist/lib/messages/inject/inject.d.ts.map +1 -1
  42. package/dist/lib/messages/inject/subagent-results.d.ts +2 -1
  43. package/dist/lib/messages/inject/subagent-results.d.ts.map +1 -1
  44. package/dist/lib/messages/inject/utils.d.ts +2 -2
  45. package/dist/lib/messages/inject/utils.d.ts.map +1 -1
  46. package/dist/lib/messages/utils.d.ts.map +1 -1
  47. package/dist/lib/opencode-client.d.ts +3 -0
  48. package/dist/lib/opencode-client.d.ts.map +1 -0
  49. package/dist/lib/prompts/context-limit-nudge.d.ts +1 -1
  50. package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -1
  51. package/dist/lib/prompts/iteration-nudge.d.ts +1 -1
  52. package/dist/lib/prompts/iteration-nudge.d.ts.map +1 -1
  53. package/dist/lib/prompts/store.d.ts +1 -0
  54. package/dist/lib/prompts/store.d.ts.map +1 -1
  55. package/dist/lib/prompts/system.d.ts +1 -1
  56. package/dist/lib/prompts/system.d.ts.map +1 -1
  57. package/dist/lib/prompts/turn-nudge.d.ts +1 -1
  58. package/dist/lib/prompts/turn-nudge.d.ts.map +1 -1
  59. package/dist/lib/state/index.d.ts +1 -0
  60. package/dist/lib/state/index.d.ts.map +1 -1
  61. package/dist/lib/state/persistence.d.ts.map +1 -1
  62. package/dist/lib/state/state.d.ts +5 -2
  63. package/dist/lib/state/state.d.ts.map +1 -1
  64. package/dist/lib/state/store.d.ts +37 -0
  65. package/dist/lib/state/store.d.ts.map +1 -0
  66. package/dist/lib/state/tool-cache.d.ts.map +1 -1
  67. package/dist/lib/state/types.d.ts +1 -1
  68. package/dist/lib/state/types.d.ts.map +1 -1
  69. package/dist/lib/state/utils.d.ts +4 -1
  70. package/dist/lib/state/utils.d.ts.map +1 -1
  71. package/dist/lib/strategies/deduplication.d.ts.map +1 -1
  72. package/dist/lib/token-utils.d.ts +0 -1
  73. package/dist/lib/token-utils.d.ts.map +1 -1
  74. package/dist/lib/ui/notification.d.ts +4 -3
  75. package/dist/lib/ui/notification.d.ts.map +1 -1
  76. package/dist/lib/update.d.ts +3 -9
  77. package/dist/lib/update.d.ts.map +1 -1
  78. package/package.json +2 -2
  79. package/dist/lib/auth.d.ts +0 -4
  80. 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",
@@ -944,6 +943,11 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
944
943
  "compress.protectTags",
945
944
  "compress.protectUserMessages",
946
945
  "compress.externalModel",
946
+ "compress.externalModel.url",
947
+ "compress.externalModel.model",
948
+ "compress.externalModel.apiKey",
949
+ "compress.externalModel.timeout",
950
+ "compress.externalModel.retries",
947
951
  "strategies",
948
952
  "strategies.deduplication",
949
953
  "strategies.deduplication.enabled",
@@ -1259,6 +1263,12 @@ function validateConfigTypes(config) {
1259
1263
  expected: "string",
1260
1264
  actual: typeof compress.externalModel.url
1261
1265
  });
1266
+ } else if (compress.externalModel.url.trim().length === 0) {
1267
+ errors.push({
1268
+ key: "compress.externalModel.url",
1269
+ expected: "non-empty string",
1270
+ actual: "empty string"
1271
+ });
1262
1272
  }
1263
1273
  if (typeof compress.externalModel.model !== "string") {
1264
1274
  errors.push({
@@ -1266,6 +1276,33 @@ function validateConfigTypes(config) {
1266
1276
  expected: "string",
1267
1277
  actual: typeof compress.externalModel.model
1268
1278
  });
1279
+ } else if (compress.externalModel.model.trim().length === 0) {
1280
+ errors.push({
1281
+ key: "compress.externalModel.model",
1282
+ expected: "non-empty string",
1283
+ actual: "empty string"
1284
+ });
1285
+ }
1286
+ if (compress.externalModel.apiKey !== void 0 && typeof compress.externalModel.apiKey !== "string") {
1287
+ errors.push({
1288
+ key: "compress.externalModel.apiKey",
1289
+ expected: "string",
1290
+ actual: typeof compress.externalModel.apiKey
1291
+ });
1292
+ }
1293
+ if (compress.externalModel.timeout !== void 0 && (typeof compress.externalModel.timeout !== "number" || !Number.isFinite(compress.externalModel.timeout) || compress.externalModel.timeout <= 0)) {
1294
+ errors.push({
1295
+ key: "compress.externalModel.timeout",
1296
+ expected: "positive finite number",
1297
+ actual: JSON.stringify(compress.externalModel.timeout)
1298
+ });
1299
+ }
1300
+ if (compress.externalModel.retries !== void 0 && (!Number.isInteger(compress.externalModel.retries) || compress.externalModel.retries < 0)) {
1301
+ errors.push({
1302
+ key: "compress.externalModel.retries",
1303
+ expected: "non-negative integer",
1304
+ actual: JSON.stringify(compress.externalModel.retries)
1305
+ });
1269
1306
  }
1270
1307
  }
1271
1308
  }
@@ -1746,6 +1783,8 @@ function isProtectedUserMessage(config, message) {
1746
1783
  // lib/token-utils.ts
1747
1784
  var anthropicCountTokens = _anthropicTokenizer.countTokens ?? _anthropicTokenizer.default?.countTokens;
1748
1785
  function getCurrentTokenUsage(state, messages) {
1786
+ let reportedTokens = 0;
1787
+ let lastReportedIndex = -1;
1749
1788
  for (let i = messages.length - 1; i >= 0; i--) {
1750
1789
  const msg = messages[i];
1751
1790
  if (msg.info.role !== "assistant") {
@@ -1756,16 +1795,23 @@ function getCurrentTokenUsage(state, messages) {
1756
1795
  continue;
1757
1796
  }
1758
1797
  if (state.lastCompaction > 0 && (msg.info.time.created < state.lastCompaction || msg.info.summary === true && msg.info.time.created === state.lastCompaction)) {
1759
- return 0;
1798
+ lastReportedIndex = i;
1799
+ break;
1760
1800
  }
1761
1801
  const input = assistantInfo.tokens?.input || 0;
1762
1802
  const output = assistantInfo.tokens?.output || 0;
1763
1803
  const reasoning = assistantInfo.tokens?.reasoning || 0;
1764
1804
  const cacheRead = assistantInfo.tokens?.cache?.read || 0;
1765
1805
  const cacheWrite = assistantInfo.tokens?.cache?.write || 0;
1766
- return input + output + reasoning + cacheRead + cacheWrite;
1806
+ reportedTokens = input + output + reasoning + cacheRead + cacheWrite;
1807
+ lastReportedIndex = i;
1808
+ break;
1767
1809
  }
1768
- return 0;
1810
+ let unreportedTokens = 0;
1811
+ for (let i = lastReportedIndex + 1; i < messages.length; i++) {
1812
+ unreportedTokens += countAllMessageTokens(messages[i]);
1813
+ }
1814
+ return reportedTokens + unreportedTokens;
1769
1815
  }
1770
1816
  function getCurrentParams(state, messages, logger) {
1771
1817
  const userMsg = getLastUserMessage(messages);
@@ -1774,16 +1820,14 @@ function getCurrentParams(state, messages, logger) {
1774
1820
  return {
1775
1821
  providerId: void 0,
1776
1822
  modelId: void 0,
1777
- agent: void 0,
1778
- variant: void 0
1823
+ agent: void 0
1779
1824
  };
1780
1825
  }
1781
1826
  const userInfo = userMsg.info;
1782
1827
  const agent = userInfo.agent;
1783
1828
  const providerId = userInfo.model.providerID;
1784
1829
  const modelId = userInfo.model.modelID;
1785
- const variant = userInfo.model.variant;
1786
- return { providerId, modelId, agent, variant };
1830
+ return { providerId, modelId, agent };
1787
1831
  }
1788
1832
  function countTokens2(text) {
1789
1833
  if (!text) return 0;
@@ -1828,7 +1872,8 @@ function extractToolContent(part) {
1828
1872
  }
1829
1873
  function countToolTokens(part) {
1830
1874
  const contents = extractToolContent(part);
1831
- return estimateTokensBatch(contents);
1875
+ if (contents.length === 0) return 0;
1876
+ return Math.ceil(Buffer.byteLength(contents.join(" "), "utf-8") / 4);
1832
1877
  }
1833
1878
  function getTotalToolTokens(state, toolIds) {
1834
1879
  let total = 0;
@@ -1910,7 +1955,7 @@ var MESSAGE_REF_MAX_INDEX = 9999;
1910
1955
  function formatMessageRef(index) {
1911
1956
  if (!Number.isInteger(index) || index < MESSAGE_REF_MIN_INDEX || index > MESSAGE_REF_MAX_INDEX) {
1912
1957
  throw new Error(
1913
- `Message ID index out of bounds: ${index}. Supported range is 0-${MESSAGE_REF_MAX_INDEX}.`
1958
+ `Message ID index out of bounds: ${index}. Supported range is ${MESSAGE_REF_MIN_INDEX}-${MESSAGE_REF_MAX_INDEX}.`
1914
1959
  );
1915
1960
  }
1916
1961
  return `m${index.toString().padStart(MESSAGE_REF_WIDTH, "0")}`;
@@ -2011,14 +2056,16 @@ function assignMessageRefs(state, messages) {
2011
2056
  return assigned;
2012
2057
  }
2013
2058
  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) {
2059
+ const capacity = MESSAGE_REF_MAX_INDEX - MESSAGE_REF_MIN_INDEX + 1;
2060
+ const requested = Number.isInteger(state.messageIds.nextRef) ? state.messageIds.nextRef : MESSAGE_REF_MIN_INDEX;
2061
+ const start = requested >= MESSAGE_REF_MIN_INDEX && requested <= MESSAGE_REF_MAX_INDEX ? requested : MESSAGE_REF_MIN_INDEX;
2062
+ for (let offset = 0; offset < capacity; offset++) {
2063
+ const candidate = MESSAGE_REF_MIN_INDEX + (start - MESSAGE_REF_MIN_INDEX + offset) % capacity;
2016
2064
  const ref = formatMessageRef(candidate);
2017
2065
  if (!state.messageIds.byRef.has(ref)) {
2018
- state.messageIds.nextRef = candidate + 1;
2066
+ state.messageIds.nextRef = candidate === MESSAGE_REF_MAX_INDEX ? MESSAGE_REF_MIN_INDEX : candidate + 1;
2019
2067
  return ref;
2020
2068
  }
2021
- candidate++;
2022
2069
  }
2023
2070
  throw new Error(
2024
2071
  `Message ID alias capacity exceeded. Cannot allocate more than ${formatMessageRef(MESSAGE_REF_MAX_INDEX)} aliases in this session.`
@@ -2694,6 +2741,7 @@ function resolveMessage(entry, searchContext, state, config) {
2694
2741
  // lib/state/persistence.ts
2695
2742
  import * as fs from "fs/promises";
2696
2743
  import { existsSync as existsSync2 } from "fs";
2744
+ import { randomUUID } from "crypto";
2697
2745
  import { homedir as homedir2 } from "os";
2698
2746
  import { join as join2 } from "path";
2699
2747
 
@@ -2932,78 +2980,148 @@ function getActiveSummaryTokenUsage(state) {
2932
2980
  }
2933
2981
  return total;
2934
2982
  }
2983
+ var MAX_SUBAGENT_CACHE_ENTRIES = 64;
2984
+ var MAX_SUBAGENT_CACHE_CHARS = 5e5;
2985
+ var MAX_SUBAGENT_CACHE_TOTAL_CHARS = 2e6;
2986
+ function getCachedSubAgentResult(state, callId) {
2987
+ const cached = state.subAgentResultCache.get(callId);
2988
+ if (cached === void 0) return void 0;
2989
+ state.subAgentResultCache.delete(callId);
2990
+ state.subAgentResultCache.set(callId, cached);
2991
+ return cached;
2992
+ }
2993
+ function cacheSubAgentResult(state, callId, result) {
2994
+ 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]";
2995
+ state.subAgentResultCache.delete(callId);
2996
+ state.subAgentResultCache.set(callId, cached);
2997
+ let totalChars = Array.from(state.subAgentResultCache.values()).reduce(
2998
+ (total, value) => total + value.length,
2999
+ 0
3000
+ );
3001
+ while (state.subAgentResultCache.size > MAX_SUBAGENT_CACHE_ENTRIES || totalChars > MAX_SUBAGENT_CACHE_TOTAL_CHARS) {
3002
+ const oldest = state.subAgentResultCache.keys().next().value;
3003
+ if (typeof oldest !== "string") break;
3004
+ totalChars -= state.subAgentResultCache.get(oldest)?.length ?? 0;
3005
+ state.subAgentResultCache.delete(oldest);
3006
+ }
3007
+ return cached;
3008
+ }
2935
3009
  function resetOnCompaction(state, messages = []) {
2936
3010
  state.toolParameters.clear();
2937
3011
  if (messages.length === 0) {
2938
3012
  return;
2939
3013
  }
2940
3014
  const liveMessageIds = new Set(messages.map((msg) => msg.info.id));
3015
+ const liveToolIds = new Set(
3016
+ messages.flatMap(
3017
+ (message) => message.parts.flatMap(
3018
+ (part) => part.type === "tool" && part.callID ? [part.callID] : []
3019
+ )
3020
+ )
3021
+ );
2941
3022
  for (const [ref, rawId] of state.messageIds.byRef) {
2942
3023
  if (!liveMessageIds.has(rawId)) {
2943
3024
  state.messageIds.byRef.delete(ref);
2944
3025
  state.messageIds.byRawId.delete(rawId);
2945
3026
  }
2946
3027
  }
3028
+ for (const callId of state.subAgentResultCache.keys()) {
3029
+ if (!liveToolIds.has(callId)) state.subAgentResultCache.delete(callId);
3030
+ }
3031
+ }
3032
+
3033
+ // lib/concurrency.ts
3034
+ async function runWithConcurrency(items, concurrency, task) {
3035
+ let nextIndex = 0;
3036
+ const workerCount = Math.min(items.length, Math.max(1, Math.floor(concurrency)));
3037
+ const workers = Array.from({ length: workerCount }, async () => {
3038
+ for (; ; ) {
3039
+ const index = nextIndex++;
3040
+ if (index >= items.length) return;
3041
+ await task(items[index]);
3042
+ }
3043
+ });
3044
+ await Promise.all(workers);
2947
3045
  }
2948
3046
 
2949
3047
  // 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
- );
3048
+ var saveQueues = /* @__PURE__ */ new Map();
3049
+ function getStorageDir() {
3050
+ return join2(
3051
+ process.env.XDG_DATA_HOME || join2(homedir2(), ".local", "share"),
3052
+ "opencode",
3053
+ "storage",
3054
+ "plugin",
3055
+ "dcp"
3056
+ );
3057
+ }
2957
3058
  async function ensureStorageDir() {
2958
- if (!existsSync2(STORAGE_DIR)) {
2959
- await fs.mkdir(STORAGE_DIR, { recursive: true });
3059
+ const storageDir = getStorageDir();
3060
+ if (!existsSync2(storageDir)) {
3061
+ await fs.mkdir(storageDir, { recursive: true });
2960
3062
  }
2961
3063
  }
2962
3064
  function getSessionFilePath(sessionId) {
2963
- return join2(STORAGE_DIR, `${sessionId}.json`);
3065
+ return join2(getStorageDir(), `${sessionId}.json`);
2964
3066
  }
2965
3067
  async function writePersistedSessionState(sessionId, state, logger) {
2966
3068
  await ensureStorageDir();
2967
3069
  const filePath = getSessionFilePath(sessionId);
3070
+ const temporaryPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
2968
3071
  const content = JSON.stringify(state, null, 2);
2969
- await fs.writeFile(filePath, content, "utf-8");
3072
+ try {
3073
+ await fs.writeFile(temporaryPath, content, "utf-8");
3074
+ await fs.rename(temporaryPath, filePath);
3075
+ } catch (error) {
3076
+ await fs.unlink(temporaryPath).catch(() => void 0);
3077
+ throw error;
3078
+ }
2970
3079
  logger.info("Saved session state to disk", {
2971
3080
  sessionId,
2972
3081
  totalTokensSaved: state.stats.totalPruneTokens
2973
3082
  });
2974
3083
  }
2975
3084
  async function saveSessionState(sessionState, logger, sessionName) {
2976
- try {
2977
- if (!sessionState.sessionId) {
2978
- return;
3085
+ if (!sessionState.sessionId) {
3086
+ return;
3087
+ }
3088
+ const sessionId = sessionState.sessionId;
3089
+ const state = {
3090
+ sessionName,
3091
+ manualMode: !!sessionState.manualMode,
3092
+ prune: {
3093
+ tools: Object.fromEntries(sessionState.prune.tools),
3094
+ messages: serializePruneMessagesState(sessionState.prune.messages)
3095
+ },
3096
+ nudges: {
3097
+ contextLimitAnchors: Array.from(sessionState.nudges.contextLimitAnchors),
3098
+ turnNudgeAnchors: Array.from(sessionState.nudges.turnNudgeAnchors),
3099
+ iterationNudgeAnchors: Array.from(sessionState.nudges.iterationNudgeAnchors)
3100
+ },
3101
+ stats: { ...sessionState.stats },
3102
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
3103
+ lastCompaction: sessionState.lastCompaction,
3104
+ messageIds: {
3105
+ byRawId: Object.fromEntries(sessionState.messageIds.byRawId),
3106
+ byRef: Object.fromEntries(sessionState.messageIds.byRef),
3107
+ nextRef: sessionState.messageIds.nextRef
2979
3108
  }
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);
3109
+ };
3110
+ const previous = saveQueues.get(sessionId) ?? Promise.resolve();
3111
+ const current = previous.catch(() => void 0).then(() => writePersistedSessionState(sessionId, state, logger));
3112
+ saveQueues.set(sessionId, current);
3113
+ try {
3114
+ await current;
3002
3115
  } catch (error) {
3003
3116
  logger.error("Failed to save session state", {
3004
- sessionId: sessionState.sessionId,
3117
+ sessionId,
3005
3118
  error: error?.message
3006
3119
  });
3120
+ throw error;
3121
+ } finally {
3122
+ if (saveQueues.get(sessionId) === current) {
3123
+ saveQueues.delete(sessionId);
3124
+ }
3007
3125
  }
3008
3126
  }
3009
3127
  async function loadSessionState(sessionId, logger) {
@@ -3082,14 +3200,15 @@ async function loadAllSessionStats(logger) {
3082
3200
  sessionCount: 0
3083
3201
  };
3084
3202
  try {
3085
- if (!existsSync2(STORAGE_DIR)) {
3203
+ const storageDir = getStorageDir();
3204
+ if (!existsSync2(storageDir)) {
3086
3205
  return result;
3087
3206
  }
3088
- const files = await fs.readdir(STORAGE_DIR);
3207
+ const files = await fs.readdir(storageDir);
3089
3208
  const jsonFiles = files.filter((f) => f.endsWith(".json"));
3090
- for (const file of jsonFiles) {
3209
+ await runWithConcurrency(jsonFiles, 16, async (file) => {
3091
3210
  try {
3092
- const filePath = join2(STORAGE_DIR, file);
3211
+ const filePath = join2(storageDir, file);
3093
3212
  const content = await fs.readFile(filePath, "utf-8");
3094
3213
  const state = JSON.parse(content);
3095
3214
  if (state?.stats?.totalPruneTokens && state?.prune) {
@@ -3100,7 +3219,7 @@ async function loadAllSessionStats(logger) {
3100
3219
  }
3101
3220
  } catch {
3102
3221
  }
3103
- }
3222
+ });
3104
3223
  logger.debug("Loaded all-time stats", result);
3105
3224
  } catch (error) {
3106
3225
  logger.warn("Failed to load all-time stats", { error: error?.message });
@@ -3109,6 +3228,8 @@ async function loadAllSessionStats(logger) {
3109
3228
  }
3110
3229
 
3111
3230
  // lib/compress/timing.ts
3231
+ var MAX_TIMING_ENTRIES = 2048;
3232
+ var TIMING_TTL_MS = 30 * 60 * 1e3;
3112
3233
  function buildCompressionTimingKey(messageId, callId) {
3113
3234
  return `${messageId}:${callId}`;
3114
3235
  }
@@ -3116,8 +3237,26 @@ function consumeCompressionStart(state, messageId, callId) {
3116
3237
  const key = buildCompressionTimingKey(messageId, callId);
3117
3238
  const start = state.compressionTiming.startsByCallId.get(key);
3118
3239
  state.compressionTiming.startsByCallId.delete(key);
3240
+ state.compressionTiming.recordedAtByCallId.delete(key);
3119
3241
  return start;
3120
3242
  }
3243
+ function pruneCompressionTiming(state, now = Date.now()) {
3244
+ const timing = state.compressionTiming;
3245
+ const cutoff = now - TIMING_TTL_MS;
3246
+ for (const [key, recordedAt] of timing.recordedAtByCallId) {
3247
+ if (recordedAt >= cutoff) continue;
3248
+ timing.recordedAtByCallId.delete(key);
3249
+ timing.startsByCallId.delete(key);
3250
+ timing.pendingByCallId.delete(key);
3251
+ }
3252
+ while (timing.recordedAtByCallId.size > MAX_TIMING_ENTRIES) {
3253
+ const oldest = timing.recordedAtByCallId.keys().next().value;
3254
+ if (typeof oldest !== "string") break;
3255
+ timing.recordedAtByCallId.delete(oldest);
3256
+ timing.startsByCallId.delete(oldest);
3257
+ timing.pendingByCallId.delete(oldest);
3258
+ }
3259
+ }
3121
3260
  function resolveCompressionDuration(startedAt, eventTime, partTime) {
3122
3261
  const runningAt = typeof partTime?.start === "number" && Number.isFinite(partTime.start) ? partTime.start : eventTime;
3123
3262
  const pendingToRunningMs = typeof startedAt === "number" && typeof runningAt === "number" ? Math.max(0, runningAt - startedAt) : void 0;
@@ -3141,24 +3280,165 @@ function applyPendingCompressionDurations(state) {
3141
3280
  if (applied > 0) {
3142
3281
  updates += applied;
3143
3282
  state.compressionTiming.pendingByCallId.delete(key);
3283
+ state.compressionTiming.recordedAtByCallId.delete(key);
3144
3284
  }
3145
3285
  }
3146
3286
  return updates;
3147
3287
  }
3148
3288
 
3289
+ // lib/state/store.ts
3290
+ var MAX_RESIDENT_SESSIONS = 64;
3291
+ var MAX_MESSAGE_OWNERS = 2e4;
3292
+ var MAX_TIMING_ENTRIES2 = 2048;
3293
+ var TIMING_TTL_MS2 = 30 * 60 * 1e3;
3294
+ var SessionStateStore = class {
3295
+ constructor(createState) {
3296
+ this.createState = createState;
3297
+ }
3298
+ states = /* @__PURE__ */ new Map();
3299
+ initializationQueues = /* @__PURE__ */ new Map();
3300
+ messageOwners = /* @__PURE__ */ new Map();
3301
+ orphanStarts = /* @__PURE__ */ new Map();
3302
+ orphanPending = /* @__PURE__ */ new Map();
3303
+ get(sessionId) {
3304
+ const existing = this.states.get(sessionId);
3305
+ if (existing) {
3306
+ this.touch(sessionId, existing);
3307
+ return existing;
3308
+ }
3309
+ const state = this.createState();
3310
+ this.states.set(sessionId, state);
3311
+ this.trimSessions();
3312
+ return state;
3313
+ }
3314
+ peek(sessionId) {
3315
+ return this.states.get(sessionId);
3316
+ }
3317
+ async initialize(sessionId, initializer) {
3318
+ const state = this.get(sessionId);
3319
+ const previous = this.initializationQueues.get(sessionId) ?? Promise.resolve();
3320
+ const current = previous.catch(() => void 0).then(() => initializer(state));
3321
+ this.initializationQueues.set(sessionId, current);
3322
+ try {
3323
+ await current;
3324
+ } finally {
3325
+ if (this.initializationQueues.get(sessionId) === current) {
3326
+ this.initializationQueues.delete(sessionId);
3327
+ }
3328
+ }
3329
+ return state;
3330
+ }
3331
+ registerMessages(sessionId, messages) {
3332
+ const state = this.get(sessionId);
3333
+ for (const message of messages) {
3334
+ const messageId = message.info?.id;
3335
+ if (typeof messageId !== "string") continue;
3336
+ this.messageOwners.delete(messageId);
3337
+ this.messageOwners.set(messageId, sessionId);
3338
+ this.moveOrphanTiming(messageId, state.compressionTiming);
3339
+ }
3340
+ while (this.messageOwners.size > MAX_MESSAGE_OWNERS) {
3341
+ const oldest = this.messageOwners.keys().next().value;
3342
+ if (typeof oldest !== "string") break;
3343
+ this.messageOwners.delete(oldest);
3344
+ }
3345
+ return state;
3346
+ }
3347
+ resolveEventState(sessionId, messageId) {
3348
+ const resolvedSessionId = sessionId ?? this.messageOwners.get(messageId);
3349
+ if (!resolvedSessionId) return void 0;
3350
+ this.messageOwners.set(messageId, resolvedSessionId);
3351
+ return this.get(resolvedSessionId);
3352
+ }
3353
+ recordOrphanStart(key, startedAt) {
3354
+ this.pruneOrphanTiming();
3355
+ if (this.orphanStarts.has(key)) return;
3356
+ this.orphanStarts.set(key, { startedAt, recordedAt: Date.now() });
3357
+ this.trimMap(this.orphanStarts);
3358
+ }
3359
+ consumeOrphanStart(key) {
3360
+ const entry = this.orphanStarts.get(key);
3361
+ this.orphanStarts.delete(key);
3362
+ return entry?.startedAt;
3363
+ }
3364
+ recordOrphanPending(key, entry) {
3365
+ this.pruneOrphanTiming();
3366
+ this.orphanPending.set(key, { ...entry, recordedAt: Date.now() });
3367
+ this.trimMap(this.orphanPending);
3368
+ }
3369
+ deleteOrphanStart(key) {
3370
+ this.orphanStarts.delete(key);
3371
+ }
3372
+ moveOrphanTiming(messageId, timing) {
3373
+ const prefix = `${messageId}:`;
3374
+ for (const [key, entry] of this.orphanStarts) {
3375
+ if (!key.startsWith(prefix)) continue;
3376
+ if (!timing.startsByCallId.has(key)) {
3377
+ timing.startsByCallId.set(key, entry.startedAt);
3378
+ }
3379
+ timing.recordedAtByCallId.set(key, entry.recordedAt);
3380
+ this.orphanStarts.delete(key);
3381
+ }
3382
+ for (const [key, entry] of this.orphanPending) {
3383
+ if (!key.startsWith(prefix)) continue;
3384
+ timing.pendingByCallId.set(key, {
3385
+ messageId: entry.messageId,
3386
+ callId: entry.callId,
3387
+ durationMs: entry.durationMs
3388
+ });
3389
+ timing.recordedAtByCallId.set(key, entry.recordedAt);
3390
+ this.orphanPending.delete(key);
3391
+ }
3392
+ }
3393
+ touch(sessionId, state) {
3394
+ this.states.delete(sessionId);
3395
+ this.states.set(sessionId, state);
3396
+ }
3397
+ trimSessions() {
3398
+ while (this.states.size > MAX_RESIDENT_SESSIONS) {
3399
+ const oldest = Array.from(this.states.keys()).find(
3400
+ (sessionId) => !this.initializationQueues.has(sessionId)
3401
+ );
3402
+ if (!oldest) return;
3403
+ this.states.delete(oldest);
3404
+ for (const [messageId, owner] of this.messageOwners) {
3405
+ if (owner === oldest) this.messageOwners.delete(messageId);
3406
+ }
3407
+ }
3408
+ }
3409
+ pruneOrphanTiming() {
3410
+ const cutoff = Date.now() - TIMING_TTL_MS2;
3411
+ for (const [key, entry] of this.orphanStarts) {
3412
+ if (entry.recordedAt < cutoff) this.orphanStarts.delete(key);
3413
+ }
3414
+ for (const [key, entry] of this.orphanPending) {
3415
+ if (entry.recordedAt < cutoff) this.orphanPending.delete(key);
3416
+ }
3417
+ }
3418
+ trimMap(map) {
3419
+ while (map.size > MAX_TIMING_ENTRIES2) {
3420
+ const oldest = map.keys().next().value;
3421
+ if (typeof oldest !== "string") return;
3422
+ map.delete(oldest);
3423
+ }
3424
+ }
3425
+ };
3426
+ function resolveSessionState(target, sessionId) {
3427
+ return target instanceof SessionStateStore ? target.get(sessionId) : target;
3428
+ }
3429
+
3149
3430
  // lib/state/state.ts
3150
- var checkSession = async (client, state, logger, messages, manualModeDefault) => {
3431
+ var checkSession = async (client, target, logger, messages, manualModeDefault) => {
3151
3432
  const lastUserMessage = getLastUserMessage(messages);
3152
- if (!lastUserMessage) {
3153
- return;
3154
- }
3155
- const lastSessionId = lastUserMessage.info.sessionID;
3433
+ const lastSessionId = lastUserMessage?.info.sessionID ?? messages.findLast((message) => typeof message.info?.sessionID === "string")?.info.sessionID;
3434
+ if (!lastSessionId) return target instanceof SessionStateStore ? void 0 : target;
3435
+ const state = target instanceof SessionStateStore ? target.registerMessages(lastSessionId, messages) : resolveSessionState(target, lastSessionId);
3156
3436
  if (state.sessionId === null || state.sessionId !== lastSessionId) {
3157
3437
  logger.info(`Session changed: ${state.sessionId} -> ${lastSessionId}`);
3158
3438
  try {
3159
- await ensureSessionInitialized(
3439
+ await initializeSessionState(
3160
3440
  client,
3161
- state,
3441
+ target,
3162
3442
  lastSessionId,
3163
3443
  logger,
3164
3444
  messages,
@@ -3182,6 +3462,7 @@ var checkSession = async (client, state, logger, messages, manualModeDefault) =>
3182
3462
  });
3183
3463
  }
3184
3464
  state.currentTurn = countTurns(state, messages);
3465
+ return state;
3185
3466
  };
3186
3467
  function createSessionState() {
3187
3468
  return {
@@ -3205,7 +3486,8 @@ function createSessionState() {
3205
3486
  },
3206
3487
  compressionTiming: {
3207
3488
  startsByCallId: /* @__PURE__ */ new Map(),
3208
- pendingByCallId: /* @__PURE__ */ new Map()
3489
+ pendingByCallId: /* @__PURE__ */ new Map(),
3490
+ recordedAtByCallId: /* @__PURE__ */ new Map()
3209
3491
  },
3210
3492
  toolParameters: /* @__PURE__ */ new Map(),
3211
3493
  subAgentResultCache: /* @__PURE__ */ new Map(),
@@ -3222,6 +3504,7 @@ function createSessionState() {
3222
3504
  };
3223
3505
  }
3224
3506
  function resetSessionState(state) {
3507
+ const modelContextLimit = state.modelContextLimit;
3225
3508
  state.sessionId = null;
3226
3509
  state.isSubAgent = false;
3227
3510
  state.manualMode = false;
@@ -3250,9 +3533,20 @@ function resetSessionState(state) {
3250
3533
  };
3251
3534
  state.lastCompaction = 0;
3252
3535
  state.currentTurn = 0;
3253
- state.modelContextLimit = void 0;
3536
+ state.modelContextLimit = modelContextLimit;
3254
3537
  state.systemPromptTokens = void 0;
3255
3538
  }
3539
+ async function initializeSessionState(client, target, sessionId, logger, messages, manualModeEnabled) {
3540
+ if (target instanceof SessionStateStore) {
3541
+ target.registerMessages(sessionId, messages);
3542
+ return target.initialize(
3543
+ sessionId,
3544
+ (state) => ensureSessionInitialized(client, state, sessionId, logger, messages, manualModeEnabled)
3545
+ );
3546
+ }
3547
+ await ensureSessionInitialized(client, target, sessionId, logger, messages, manualModeEnabled);
3548
+ return target;
3549
+ }
3256
3550
  async function ensureSessionInitialized(client, state, sessionId, logger, messages, manualModeEnabled) {
3257
3551
  if (state.sessionId === sessionId) {
3258
3552
  return;
@@ -3316,6 +3610,7 @@ function syncToolCache(state, config, logger, messages) {
3316
3610
  try {
3317
3611
  logger.info("Syncing tool parameters from OpenCode messages");
3318
3612
  let turnCounter = 0;
3613
+ const candidates = /* @__PURE__ */ new Map();
3319
3614
  for (const msg of messages) {
3320
3615
  if (isMessageCompacted(state, msg)) {
3321
3616
  continue;
@@ -3329,26 +3624,35 @@ function syncToolCache(state, config, logger, messages) {
3329
3624
  if (part.type !== "tool" || !part.callID) {
3330
3625
  continue;
3331
3626
  }
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
- });
3627
+ candidates.delete(part.callID);
3628
+ candidates.set(part.callID, { part, turn: turnCounter });
3629
+ }
3630
+ }
3631
+ const recentCandidates = Array.from(candidates.entries()).filter(([, candidate]) => {
3632
+ const turnProtectionEnabled = config.turnProtection.enabled;
3633
+ const turnProtectionTurns = config.turnProtection.turns;
3634
+ return !(turnProtectionEnabled && turnProtectionTurns > 0 && state.currentTurn - candidate.turn < turnProtectionTurns);
3635
+ }).slice(-MAX_TOOL_CACHE_SIZE);
3636
+ const retainedIds = new Set(recentCandidates.map(([callId]) => callId));
3637
+ for (const callId of state.toolParameters.keys()) {
3638
+ if (!retainedIds.has(callId)) state.toolParameters.delete(callId);
3639
+ }
3640
+ for (const [callId, { part, turn }] of recentCandidates) {
3641
+ const status = part.state.status;
3642
+ const cached = state.toolParameters.get(callId);
3643
+ if (cached?.status === status) continue;
3644
+ const tokenCount = countToolTokens(part);
3645
+ state.toolParameters.set(callId, {
3646
+ tool: part.tool,
3647
+ parameters: part.state?.input ?? {},
3648
+ status,
3649
+ error: part.state.status === "error" ? part.state.error : void 0,
3650
+ turn,
3651
+ tokenCount
3652
+ });
3653
+ if (!cached) {
3350
3654
  logger.info(
3351
- `Cached tool id: ${part.callID} (turn ${turnCounter}${tokenCount !== void 0 ? `, ${tokenCount} tokens` : ""})`
3655
+ `Cached tool id: ${callId} (turn ${turn}${tokenCount !== void 0 ? `, ${tokenCount} tokens` : ""})`
3352
3656
  );
3353
3657
  }
3354
3658
  }
@@ -3494,6 +3798,9 @@ var deduplicate = (state, logger, config, messages) => {
3494
3798
  if (!metadata) {
3495
3799
  continue;
3496
3800
  }
3801
+ if (metadata.status !== void 0 && metadata.status !== "completed" && metadata.status !== "success") {
3802
+ continue;
3803
+ }
3497
3804
  if (isToolNameProtected(metadata.tool, protectedTools)) {
3498
3805
  continue;
3499
3806
  }
@@ -3991,7 +4298,6 @@ ${progressBar}`;
3991
4298
  }
3992
4299
  async function sendIgnoredMessage(client, sessionID, text, params, logger) {
3993
4300
  const agent = params.agent || void 0;
3994
- const variant = params.variant || void 0;
3995
4301
  const model = params.providerId && params.modelId ? {
3996
4302
  providerID: params.providerId,
3997
4303
  modelID: params.modelId
@@ -4005,7 +4311,6 @@ async function sendIgnoredMessage(client, sessionID, text, params, logger) {
4005
4311
  noReply: true,
4006
4312
  agent,
4007
4313
  model,
4008
- variant,
4009
4314
  parts: [
4010
4315
  {
4011
4316
  type: "text",
@@ -4022,7 +4327,18 @@ async function sendIgnoredMessage(client, sessionID, text, params, logger) {
4022
4327
 
4023
4328
  // lib/compress/pipeline.ts
4024
4329
  async function prepareSession(ctx, toolCtx, title) {
4025
- if (ctx.state.manualMode && ctx.state.manualMode !== "compress-pending") {
4330
+ const state = resolveSessionState(ctx.state, toolCtx.sessionID);
4331
+ toolCtx.metadata({ title });
4332
+ const rawMessages = await fetchSessionMessages(ctx.client, toolCtx.sessionID);
4333
+ await initializeSessionState(
4334
+ ctx.client,
4335
+ ctx.state,
4336
+ toolCtx.sessionID,
4337
+ ctx.logger,
4338
+ rawMessages,
4339
+ ctx.config.manualMode.enabled
4340
+ );
4341
+ if (state.manualMode && state.manualMode !== "compress-pending") {
4026
4342
  throw new Error(
4027
4343
  "Manual mode: compress blocked. Do not retry until `<compress triggered manually>` appears in user context."
4028
4344
  );
@@ -4033,35 +4349,26 @@ async function prepareSession(ctx, toolCtx, title) {
4033
4349
  always: ["*"],
4034
4350
  metadata: {}
4035
4351
  });
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);
4352
+ assignMessageRefs(state, rawMessages);
4353
+ deduplicate(state, ctx.logger, ctx.config, rawMessages);
4354
+ purgeErrors(state, ctx.logger, ctx.config, rawMessages);
4049
4355
  return {
4356
+ state,
4050
4357
  rawMessages,
4051
- searchContext: buildSearchContext(ctx.state, rawMessages)
4358
+ searchContext: buildSearchContext(state, rawMessages)
4052
4359
  };
4053
4360
  }
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);
4361
+ async function finalizeSession(ctx, state, toolCtx, rawMessages, entries, batchTopic) {
4362
+ state.manualMode = state.manualMode ? "active" : false;
4363
+ applyPendingCompressionDurations(state);
4364
+ await saveSessionState(state, ctx.logger);
4365
+ const params = getCurrentParams(state, rawMessages, ctx.logger);
4059
4366
  const sessionMessageIds = rawMessages.filter((msg) => !isIgnoredUserMessage(msg)).map((msg) => msg.info.id);
4060
4367
  await sendCompressNotification(
4061
4368
  ctx.client,
4062
4369
  ctx.logger,
4063
4370
  ctx.config,
4064
- ctx.state,
4371
+ state,
4065
4372
  toolCtx.sessionID,
4066
4373
  entries,
4067
4374
  batchTopic,
@@ -4220,7 +4527,7 @@ async function appendProtectedTools(client, state, allowSubAgents, summary, sele
4220
4527
  output = typeof part.state.output === "string" ? part.state.output : JSON.stringify(part.state.output);
4221
4528
  }
4222
4529
  if (allowSubAgents && part.tool === "task" && part.state?.status === "completed" && typeof part.state?.output === "string") {
4223
- const cachedSubAgentResult = state.subAgentResultCache.get(part.callID);
4530
+ const cachedSubAgentResult = getCachedSubAgentResult(state, part.callID);
4224
4531
  if (cachedSubAgentResult !== void 0) {
4225
4532
  if (cachedSubAgentResult) {
4226
4533
  output = mergeSubagentResult(
@@ -4242,7 +4549,11 @@ async function appendProtectedTools(client, state, allowSubAgents, summary, sele
4242
4549
  subAgentResultText = "";
4243
4550
  }
4244
4551
  if (subAgentResultText) {
4245
- state.subAgentResultCache.set(part.callID, subAgentResultText);
4552
+ subAgentResultText = cacheSubAgentResult(
4553
+ state,
4554
+ part.callID,
4555
+ subAgentResultText
4556
+ );
4246
4557
  output = mergeSubagentResult(
4247
4558
  part.state.output,
4248
4559
  subAgentResultText
@@ -4350,6 +4661,21 @@ async function callExternalModel(endpoint, cfg, req) {
4350
4661
  return content;
4351
4662
  }
4352
4663
  async function generateSummaryViaExternal(cfg, req) {
4664
+ if (typeof cfg.url !== "string" || cfg.url.trim().length === 0) {
4665
+ throw new Error("\u5916\u90E8\u538B\u7F29\u6A21\u578B url \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
4666
+ }
4667
+ if (typeof cfg.model !== "string" || cfg.model.trim().length === 0) {
4668
+ throw new Error("\u5916\u90E8\u538B\u7F29\u6A21\u578B model \u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32");
4669
+ }
4670
+ if (cfg.apiKey !== void 0 && typeof cfg.apiKey !== "string") {
4671
+ throw new Error("\u5916\u90E8\u538B\u7F29\u6A21\u578B apiKey \u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
4672
+ }
4673
+ if (cfg.timeout !== void 0 && (!Number.isFinite(cfg.timeout) || cfg.timeout <= 0)) {
4674
+ throw new Error("\u5916\u90E8\u538B\u7F29\u6A21\u578B timeout \u5FC5\u987B\u662F\u6B63\u6570");
4675
+ }
4676
+ if (cfg.retries !== void 0 && (!Number.isInteger(cfg.retries) || cfg.retries < 0)) {
4677
+ throw new Error("\u5916\u90E8\u538B\u7F29\u6A21\u578B retries \u5FC5\u987B\u662F\u975E\u8D1F\u6574\u6570");
4678
+ }
4353
4679
  const endpoint = resolveCompressBaseUrl(cfg.url);
4354
4680
  const maxRetries = cfg.retries ?? 1;
4355
4681
  let lastError;
@@ -4363,18 +4689,57 @@ async function generateSummaryViaExternal(cfg, req) {
4363
4689
  throw lastError;
4364
4690
  }
4365
4691
 
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);
4692
+ // lib/compress/external-content.ts
4693
+ var MAX_PART_CHARS = 1e5;
4694
+ var MAX_TOTAL_CHARS = 1e6;
4695
+ var TRUNCATED_MARKER = "\n[\u5185\u5BB9\u56E0\u957F\u5EA6\u9650\u5236\u88AB\u622A\u65AD]";
4696
+ function stringify(value) {
4697
+ if (typeof value === "string") return value;
4698
+ try {
4699
+ return JSON.stringify(value, null, 2) ?? String(value);
4700
+ } catch {
4701
+ return String(value);
4702
+ }
4703
+ }
4704
+ function truncate2(value, limit = MAX_PART_CHARS) {
4705
+ if (value.length <= limit) return value;
4706
+ return value.slice(0, limit) + TRUNCATED_MARKER;
4707
+ }
4708
+ function serializePart(part) {
4709
+ if (part.type === "text" && typeof part.text === "string") {
4710
+ return [truncate2(part.text)];
4711
+ }
4712
+ if (part.type !== "tool") return [];
4713
+ const chunks = [`[\u5DE5\u5177\uFF1A${part.tool}\uFF1B\u72B6\u6001\uFF1A${part.state.status}]`];
4714
+ if (part.state.input !== void 0) {
4715
+ chunks.push("\u8F93\u5165\uFF1A\n" + truncate2(stringify(part.state.input)));
4716
+ }
4717
+ if (part.state.status === "completed" && part.state.output !== void 0) {
4718
+ chunks.push("\u8F93\u51FA\uFF1A\n" + truncate2(stringify(part.state.output)));
4719
+ } else if (part.state.status === "error" && part.state.error !== void 0) {
4720
+ chunks.push("\u9519\u8BEF\uFF1A\n" + truncate2(stringify(part.state.error)));
4721
+ }
4722
+ return chunks;
4723
+ }
4724
+ function serializeMessagesForExternalSummary(messages) {
4725
+ const chunks = [];
4726
+ let length = 0;
4727
+ for (const message of messages) {
4728
+ const header = `[\u6D88\u606F\uFF1A${message.info.id}\uFF1B\u89D2\u8272\uFF1A${message.info.role}]`;
4729
+ const content = [header, ...message.parts.flatMap(serializePart)].join("\n");
4730
+ if (length + content.length > MAX_TOTAL_CHARS) {
4731
+ const remaining = Math.max(0, MAX_TOTAL_CHARS - length);
4732
+ if (remaining > 0) chunks.push(content.slice(0, remaining));
4733
+ chunks.push(TRUNCATED_MARKER.trim());
4734
+ break;
4374
4735
  }
4736
+ chunks.push(content);
4737
+ length += content.length;
4375
4738
  }
4376
- return parts.join("\n\n");
4739
+ return chunks.join("\n\n");
4377
4740
  }
4741
+
4742
+ // lib/compress/message.ts
4378
4743
  function buildSchema(externalModelEnabled) {
4379
4744
  const summaryField = externalModelEnabled ? tool.schema.string().optional().describe(
4380
4745
  "Complete technical summary replacing that one message (optional when external model is configured)"
@@ -4403,7 +4768,7 @@ function createCompressMessageTool(ctx) {
4403
4768
  const input = args;
4404
4769
  validateArgs(input);
4405
4770
  const callId = typeof toolCtx.callID === "string" ? toolCtx.callID : void 0;
4406
- const { rawMessages, searchContext } = await prepareSession(
4771
+ const { state, rawMessages, searchContext } = await prepareSession(
4407
4772
  ctx,
4408
4773
  toolCtx,
4409
4774
  `Compress Message: ${input.topic}`
@@ -4411,26 +4776,27 @@ function createCompressMessageTool(ctx) {
4411
4776
  const { plans, skippedIssues, skippedCount } = resolveMessages(
4412
4777
  input,
4413
4778
  searchContext,
4414
- ctx.state,
4779
+ state,
4415
4780
  ctx.config
4416
4781
  );
4417
4782
  if (plans.length === 0 && skippedCount > 0) {
4418
4783
  throw new Error(formatIssues(skippedIssues, skippedCount));
4419
4784
  }
4420
- if (ctx.config.compress.externalModel) {
4421
- for (const plan of plans) {
4785
+ const externalModel = ctx.config.compress.externalModel;
4786
+ if (externalModel) {
4787
+ await runWithConcurrency(plans, 3, async (plan) => {
4422
4788
  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
- }
4789
+ const message = searchContext.rawMessagesById.get(plan.entry.messageId);
4790
+ const userContent = serializeMessagesForExternalSummary(
4791
+ message ? [message] : []
4430
4792
  );
4793
+ const generated = await generateSummaryViaExternal(externalModel, {
4794
+ systemPrompt: runtimePrompts.compressMessage,
4795
+ userContent
4796
+ });
4431
4797
  plan.entry.summary = generated;
4432
4798
  }
4433
- }
4799
+ });
4434
4800
  }
4435
4801
  const notifications = [];
4436
4802
  const preparedPlans = [];
@@ -4444,12 +4810,12 @@ function createCompressMessageTool(ctx) {
4444
4810
  plan.entry.summary,
4445
4811
  plan.selection,
4446
4812
  searchContext,
4447
- ctx.state,
4813
+ state,
4448
4814
  ctx.config.compress.protectTags
4449
4815
  );
4450
4816
  const summaryWithTools = await appendProtectedTools(
4451
4817
  ctx.client,
4452
- ctx.state,
4818
+ state,
4453
4819
  ctx.config.experimental.allowSubAgents,
4454
4820
  summaryWithPromptInfo,
4455
4821
  plan.selection,
@@ -4462,13 +4828,13 @@ function createCompressMessageTool(ctx) {
4462
4828
  summaryWithTools
4463
4829
  });
4464
4830
  }
4465
- const runId = allocateRunId(ctx.state);
4831
+ const runId = allocateRunId(state);
4466
4832
  for (const { plan, summaryWithTools } of preparedPlans) {
4467
- const blockId = allocateBlockId(ctx.state);
4833
+ const blockId = allocateBlockId(state);
4468
4834
  const storedSummary = wrapCompressedSummary(blockId, summaryWithTools);
4469
4835
  const summaryTokens = countTokens2(storedSummary);
4470
4836
  applyCompressionState(
4471
- ctx.state,
4837
+ state,
4472
4838
  {
4473
4839
  topic: plan.entry.topic,
4474
4840
  batchTopic: input.topic,
@@ -4493,7 +4859,7 @@ function createCompressMessageTool(ctx) {
4493
4859
  summaryTokens
4494
4860
  });
4495
4861
  }
4496
- await finalizeSession(ctx, toolCtx, rawMessages, notifications, input.topic);
4862
+ await finalizeSession(ctx, state, toolCtx, rawMessages, notifications, input.topic);
4497
4863
  return formatResult(plans.length, skippedIssues, skippedCount);
4498
4864
  }
4499
4865
  });
@@ -4729,19 +5095,6 @@ ${right}`;
4729
5095
  }
4730
5096
 
4731
5097
  // 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
5098
  function buildSchema2(externalModelEnabled) {
4746
5099
  const summaryField = externalModelEnabled ? tool2.schema.string().optional().describe(
4747
5100
  "Complete technical summary replacing all content in range (optional when external model is configured)"
@@ -4774,27 +5127,29 @@ function createCompressRangeTool(ctx) {
4774
5127
  const input = args;
4775
5128
  validateArgs2(input);
4776
5129
  const callId = typeof toolCtx.callID === "string" ? toolCtx.callID : void 0;
4777
- const { rawMessages, searchContext } = await prepareSession(
5130
+ const { state, rawMessages, searchContext } = await prepareSession(
4778
5131
  ctx,
4779
5132
  toolCtx,
4780
5133
  `Compress Range: ${input.topic}`
4781
5134
  );
4782
- const resolvedPlans = resolveRanges(input, searchContext, ctx.state);
5135
+ const resolvedPlans = resolveRanges(input, searchContext, state);
4783
5136
  validateNonOverlapping(resolvedPlans);
4784
- if (ctx.config.compress.externalModel) {
4785
- for (const plan of resolvedPlans) {
5137
+ const externalModel = ctx.config.compress.externalModel;
5138
+ if (externalModel) {
5139
+ await runWithConcurrency(resolvedPlans, 3, async (plan) => {
4786
5140
  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
- );
5141
+ const selectedMessages = plan.selection.messageIds.flatMap((messageId) => {
5142
+ const message = searchContext.rawMessagesById.get(messageId);
5143
+ return message ? [message] : [];
5144
+ });
5145
+ const userContent = serializeMessagesForExternalSummary(selectedMessages);
5146
+ const generated = await generateSummaryViaExternal(externalModel, {
5147
+ systemPrompt: runtimePrompts.compressRange,
5148
+ userContent
5149
+ });
4795
5150
  plan.entry.summary = generated;
4796
5151
  }
4797
- }
5152
+ });
4798
5153
  }
4799
5154
  const notifications = [];
4800
5155
  const preparedPlans = [];
@@ -4824,19 +5179,19 @@ function createCompressRangeTool(ctx) {
4824
5179
  injected.expandedSummary,
4825
5180
  plan.selection,
4826
5181
  searchContext,
4827
- ctx.state,
5182
+ state,
4828
5183
  ctx.config.compress.protectUserMessages
4829
5184
  );
4830
5185
  const summaryWithPromptInfo = appendProtectedPromptInfo(
4831
5186
  summaryWithUsers,
4832
5187
  plan.selection,
4833
5188
  searchContext,
4834
- ctx.state,
5189
+ state,
4835
5190
  ctx.config.compress.protectTags
4836
5191
  );
4837
5192
  const summaryWithTools = await appendProtectedTools(
4838
5193
  ctx.client,
4839
- ctx.state,
5194
+ state,
4840
5195
  ctx.config.experimental.allowSubAgents,
4841
5196
  summaryWithPromptInfo,
4842
5197
  plan.selection,
@@ -4858,13 +5213,13 @@ function createCompressRangeTool(ctx) {
4858
5213
  consumedBlockIds: completedSummary.consumedBlockIds
4859
5214
  });
4860
5215
  }
4861
- const runId = allocateRunId(ctx.state);
5216
+ const runId = allocateRunId(state);
4862
5217
  for (const preparedPlan of preparedPlans) {
4863
- const blockId = allocateBlockId(ctx.state);
5218
+ const blockId = allocateBlockId(state);
4864
5219
  const storedSummary = wrapCompressedSummary(blockId, preparedPlan.finalSummary);
4865
5220
  const summaryTokens = countTokens2(storedSummary);
4866
5221
  const applied = applyCompressionState(
4867
- ctx.state,
5222
+ state,
4868
5223
  {
4869
5224
  topic: input.topic,
4870
5225
  batchTopic: input.topic,
@@ -4890,7 +5245,7 @@ function createCompressRangeTool(ctx) {
4890
5245
  summaryTokens
4891
5246
  });
4892
5247
  }
4893
- await finalizeSession(ctx, toolCtx, rawMessages, notifications, input.topic);
5248
+ await finalizeSession(ctx, state, toolCtx, rawMessages, notifications, input.topic);
4894
5249
  return `Compressed ${totalCompressedMessages} messages into ${COMPRESSED_BLOCK_HEADER}.`;
4895
5250
  }
4896
5251
  });
@@ -5156,51 +5511,51 @@ import { homedir as homedir4 } from "os";
5156
5511
 
5157
5512
  // lib/prompts/system.ts
5158
5513
  var SYSTEM = `
5159
- You run in a context-limited environment. Manage context actively to avoid accumulation and keep retrieval quality high.
5514
+ \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
5515
 
5161
- Your only context-management tool is \`compress\`. It replaces older conversation content with a technical summary you generate.
5516
+ \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
5517
 
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.
5518
+ \u786C\u6027\u7EA6\u675F
5519
+ - \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
5520
+ - \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
5521
+ - \u9700\u8981\u538B\u7F29\u65F6\u8C03\u7528 \`compress\`\uFF0C\u4E0D\u8981\u5728\u56DE\u590D\u4E2D\u5185\u8054\u7F16\u5199\u6458\u8981\u3002
5522
+ - \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
5523
 
5169
- \`<dcp-message-id>\` and \`<dcp-system-reminder>\` are environment-injected metadata. Do not output them.
5524
+ \`<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
5525
 
5171
- When to compress
5526
+ \u4F55\u65F6\u538B\u7F29
5172
5527
 
5173
- HIGHEST-PRIORITY HARD TRIGGER \u2014 call \`compress\` immediately whenever the conversation TOPIC changes. Compress the previous topic's finished context into a summary.
5528
+ \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
5529
 
5175
- A topic change is an objective, low-judgment signal, not a subjective call. Any of these counts:
5530
+ \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
5531
 
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
5532
+ - \u7528\u6237\u63D0\u51FA\u4E0E\u4E0A\u4E00\u4EFB\u52A1\u65E0\u5173\u7684\u95EE\u9898
5533
+ - \u5DE5\u4F5C\u91CD\u70B9\u4ECE\u4E00\u4E2A\u6A21\u5757\u3001\u6587\u4EF6\u6216\u529F\u80FD\u8F6C\u79FB\u5230\u53E6\u4E00\u4E2A
5534
+ - \u76EE\u6807\u4ECE\u63A2\u7D22/\u7814\u7A76\u8F6C\u4E3A\u5B9E\u73B0\uFF0C\u6216\u4ECE\u5B9E\u73B0\u8F6C\u4E3A\u8C03\u8BD5/\u9A8C\u8BC1
5535
+ - \u4E00\u4E2A\u72EC\u7ACB\u95EE\u9898\u5DF2\u7ECF\u89E3\u51B3\uFF0C\u5F00\u59CB\u5904\u7406\u4E0B\u4E00\u4E2A\u95EE\u9898
5181
5536
 
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.
5537
+ \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
5538
 
5184
- Also compress (when a section genuinely closes even without a topic change):
5539
+ \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
5540
 
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
5541
+ - \u7814\u7A76\u5B8C\u6210\uFF0C\u7ED3\u8BBA\u5DF2\u7ECF\u786E\u5B9A
5542
+ - \u5B9E\u73B0\u5B8C\u6210\u5E76\u901A\u8FC7\u9A8C\u8BC1
5543
+ - \u63A2\u7D22\u5DF2\u7ECF\u5145\u5206\uFF0C\u76F8\u5173\u89C4\u5F8B\u5DF2\u7ECF\u660E\u786E
5544
+ - \u5DF2\u786E\u8BA4\u65E0\u6548\u7684\u65B9\u5411\u53EF\u4EE5\u4E22\u5F03\uFF0C\u65E0\u9700\u7B49\u5F85\u6574\u4E2A\u9636\u6BB5\u7ED3\u675F
5190
5545
 
5191
- Do NOT compress when:
5546
+ \u4EE5\u4E0B\u60C5\u51B5\u4E0D\u8981\u538B\u7F29\uFF1A
5192
5547
 
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
5548
+ - \u7F16\u8F91\u6216\u7CBE\u786E\u5F15\u7528\u4ECD\u9700\u8981\u539F\u59CB\u4E0A\u4E0B\u6587
5549
+ - \u76EE\u6807\u5DE5\u4F5C\u4ECD\u5728\u8FDB\u884C
5550
+ - \u540E\u7EED\u6B65\u9AA4\u53EF\u80FD\u9700\u8981\u7CBE\u786E\u7684\u4EE3\u7801\u3001\u9519\u8BEF\u4FE1\u606F\u6216\u6587\u4EF6\u5185\u5BB9
5196
5551
 
5197
- Ask yourself before compressing: _"Is this section closed enough to be summary-only?"_
5552
+ \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
5553
 
5199
- If the answer is "the topic switched, the old content is no longer needed" \u2014 the answer is always yes.
5554
+ \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
5555
 
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.
5556
+ \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
5557
 
5203
- Keeping a sharp, high-quality context window for best performance is your responsibility.
5558
+ \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
5559
  `;
5205
5560
 
5206
5561
  // lib/prompts/compress-range.ts
@@ -5337,44 +5692,44 @@ var COMPRESS_MESSAGE = `\u5C06\u5BF9\u8BDD\u4E2D\u9009\u5B9A\u7684\u5355\u6761\u
5337
5692
 
5338
5693
  // lib/prompts/context-limit-nudge.ts
5339
5694
  var CONTEXT_LIMIT_NUDGE = `<dcp-system-reminder>
5340
- CRITICAL: max context limit reached.
5695
+ \u7D27\u6025\uFF1A\u5DF2\u8FBE\u5230\u4E0A\u4E0B\u6587\u4E0A\u9650\u3002
5341
5696
 
5342
- You are at or beyond the configured max context threshold. This is an emergency context-recovery moment.
5697
+ \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
5698
 
5344
- You MUST call \`compress\` now. Do not continue normal exploration until compression is done.
5699
+ \u4F60\u5FC5\u987B\u73B0\u5728\u8C03\u7528 \`compress\`\u3002\u538B\u7F29\u5B8C\u6210\u524D\u4E0D\u8981\u7EE7\u7EED\u5E38\u89C4\u63A2\u7D22\u3002
5345
5700
 
5346
- If mid-atomic-step, finish that one step, then compress immediately.
5701
+ \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
5702
 
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.
5703
+ \u9009\u62E9\u8303\u56F4
5704
+ \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
5705
+ \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
5706
 
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.
5707
+ \u6458\u8981\u8981\u6C42
5708
+ \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
5709
+ \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
5710
  </dcp-system-reminder>
5356
5711
  `;
5357
5712
 
5358
5713
  // lib/prompts/turn-nudge.ts
5359
5714
  var TURN_NUDGE = `
5360
- New user turn. This is the moment topic changes are most likely.
5715
+ \u7528\u6237\u5F00\u59CB\u4E86\u65B0\u4E00\u8F6E\u5BF9\u8BDD\uFF0C\u6B64\u65F6\u6700\u5BB9\u6613\u53D1\u751F\u4E3B\u9898\u53D8\u5316\u3002
5361
5716
 
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.
5717
+ \u672C\u8F6E\u8BF7\u6C42\u662F\u5426\u4E0E\u4E0A\u4E00\u8F6E\u5C5E\u4E8E\u4E0D\u540C\u4E3B\u9898\u6216\u4EFB\u52A1\uFF1F
5718
+ - \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
5719
+ - \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
5720
 
5366
- Topic change is the clearest, most objective compression trigger \u2014 do not miss it.
5721
+ \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
5722
  `;
5368
5723
 
5369
5724
  // lib/prompts/iteration-nudge.ts
5370
5725
  var ITERATION_NUDGE = `
5371
- Many iterations elapsed without new user input.
5726
+ \u5728\u6CA1\u6709\u65B0\u7528\u6237\u8F93\u5165\u7684\u60C5\u51B5\u4E0B\u5DF2\u7ECF\u6267\u884C\u4E86\u591A\u8F6E\u3002
5372
5727
 
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.
5728
+ \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
5729
+ - \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
5730
+ - \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
5731
 
5377
- Do not let finished exploration directions keep occupying context. Topic drift is the best moment to compress.
5732
+ \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
5733
  `;
5379
5734
 
5380
5735
  // lib/prompts/extensions/system.ts
@@ -5613,6 +5968,7 @@ var PromptStore = class {
5613
5968
  paths;
5614
5969
  customPromptsEnabled;
5615
5970
  runtimePrompts;
5971
+ lastReloadAt = 0;
5616
5972
  constructor(logger, workingDirectory, customPromptsEnabled = false) {
5617
5973
  this.logger = logger;
5618
5974
  this.paths = resolvePromptPaths(workingDirectory);
@@ -5627,6 +5983,9 @@ var PromptStore = class {
5627
5983
  return { ...this.runtimePrompts };
5628
5984
  }
5629
5985
  reload() {
5986
+ const now = Date.now();
5987
+ if (now - this.lastReloadAt < 1e3) return;
5988
+ this.lastReloadAt = now;
5630
5989
  const nextPrompts = createBundledRuntimePrompts();
5631
5990
  if (!this.customPromptsEnabled) {
5632
5991
  this.runtimePrompts = nextPrompts;
@@ -5865,13 +6224,13 @@ var stripHallucinationsFromString = (text) => {
5865
6224
  };
5866
6225
  var stripHallucinations = (messages) => {
5867
6226
  for (const message of messages) {
6227
+ if (message.info.role !== "assistant") {
6228
+ continue;
6229
+ }
5868
6230
  for (const part of message.parts) {
5869
6231
  if (part.type === "text" && typeof part.text === "string") {
5870
6232
  part.text = stripHallucinationsFromString(part.text);
5871
6233
  }
5872
- if (part.type === "tool" && part.state?.status === "completed" && typeof part.state.output === "string") {
5873
- part.state.output = stripHallucinationsFromString(part.state.output);
5874
- }
5875
6234
  }
5876
6235
  }
5877
6236
  };
@@ -5966,7 +6325,8 @@ var filterCompressedRanges = (state, logger, config, messages) => {
5966
6325
  return;
5967
6326
  }
5968
6327
  const result = [];
5969
- for (const msg of messages) {
6328
+ for (let msgIndex = 0; msgIndex < messages.length; msgIndex++) {
6329
+ const msg = messages[msgIndex];
5970
6330
  const msgId = msg.info.id;
5971
6331
  const blockId = state.prune.messages.activeByAnchorMessageId.get(msgId);
5972
6332
  const summary = blockId !== void 0 ? state.prune.messages.blocksById.get(blockId) : void 0;
@@ -5978,7 +6338,6 @@ var filterCompressedRanges = (state, logger, config, messages) => {
5978
6338
  blockId: summary.blockId
5979
6339
  });
5980
6340
  } else {
5981
- const msgIndex = messages.indexOf(msg);
5982
6341
  const userMessage = getLastUserMessage(messages, msgIndex);
5983
6342
  if (userMessage) {
5984
6343
  const userInfo = userMessage.info;
@@ -6105,7 +6464,7 @@ var compressPermission = (state, config) => {
6105
6464
  return state.compressPermission ?? config.compress.permission;
6106
6465
  };
6107
6466
  var syncCompressPermissionState = (state, config, hostPermissions, messages) => {
6108
- const activeAgent = getLastUserMessage(messages)?.info.agent;
6467
+ const activeAgent = getLastUserMessage(messages)?.info?.agent;
6109
6468
  state.compressPermission = resolveEffectiveCompressPermission(
6110
6469
  config.compress.permission,
6111
6470
  hostPermissions,
@@ -6243,11 +6602,13 @@ function listPriorityRefsBeforeIndex(messages, priorities, anchorIndex, priority
6243
6602
 
6244
6603
  // lib/messages/inject/utils.ts
6245
6604
  var MESSAGE_MODE_NUDGE_PRIORITY = "high";
6246
- function getNudgeFrequency(_config) {
6247
- return 1;
6605
+ function getNudgeFrequency(config) {
6606
+ const value = config.compress.nudgeFrequency;
6607
+ return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
6248
6608
  }
6249
- function getIterationNudgeThreshold(_config) {
6250
- return 1;
6609
+ function getIterationNudgeThreshold(config) {
6610
+ const value = config.compress.iterationNudgeThreshold;
6611
+ return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
6251
6612
  }
6252
6613
  function findLastNonIgnoredMessage(messages) {
6253
6614
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -6523,7 +6884,11 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
6523
6884
  state.nudges.contextLimitAnchors.clear();
6524
6885
  state.nudges.turnNudgeAnchors.clear();
6525
6886
  state.nudges.iterationNudgeAnchors.clear();
6526
- void saveSessionState(state, logger);
6887
+ void saveSessionState(state, logger).catch(
6888
+ (error) => logger.warn("Failed to persist context-limit nudge", {
6889
+ error: error instanceof Error ? error.message : String(error)
6890
+ })
6891
+ );
6527
6892
  return;
6528
6893
  }
6529
6894
  const { providerId, modelId } = getModelInfo(messages);
@@ -6594,7 +6959,11 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
6594
6959
  }
6595
6960
  applyAnchoredNudges(state, config, messages, prompts, compressionPriorities);
6596
6961
  if (anchorsChanged) {
6597
- void saveSessionState(state, logger);
6962
+ void saveSessionState(state, logger).catch(
6963
+ (error) => logger.warn("Failed to persist compression nudge", {
6964
+ error: error instanceof Error ? error.message : String(error)
6965
+ })
6966
+ );
6598
6967
  }
6599
6968
  };
6600
6969
  var injectMessageIds = (state, config, messages, compressionPriorities) => {
@@ -6661,6 +7030,7 @@ var injectExtendedSubAgentResults = async (client, state, logger, messages, allo
6661
7030
  if (!allowSubAgents) {
6662
7031
  return;
6663
7032
  }
7033
+ const tasks = [];
6664
7034
  for (const message of messages) {
6665
7035
  const parts = Array.isArray(message.parts) ? message.parts : [];
6666
7036
  for (const part of parts) {
@@ -6673,40 +7043,43 @@ var injectExtendedSubAgentResults = async (client, state, logger, messages, allo
6673
7043
  if (part.state?.status !== "completed" || typeof part.state.output !== "string") {
6674
7044
  continue;
6675
7045
  }
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
- );
7046
+ tasks.push({ part });
6708
7047
  }
6709
7048
  }
7049
+ await runWithConcurrency(tasks, 4, async ({ part }) => {
7050
+ const cachedResult = getCachedSubAgentResult(state, part.callID);
7051
+ if (cachedResult !== void 0) {
7052
+ if (cachedResult) {
7053
+ part.state.output = stripHallucinationsFromString(
7054
+ mergeSubagentResult(part.state.output, cachedResult)
7055
+ );
7056
+ }
7057
+ return;
7058
+ }
7059
+ const subAgentSessionId = getSubAgentId(part);
7060
+ if (!subAgentSessionId) {
7061
+ return;
7062
+ }
7063
+ let subAgentMessages = [];
7064
+ try {
7065
+ subAgentMessages = await fetchSubAgentMessages(client, subAgentSessionId);
7066
+ } catch (error) {
7067
+ logger.warn("Failed to fetch subagent session for output expansion", {
7068
+ subAgentSessionId,
7069
+ callID: part.callID,
7070
+ error: error instanceof Error ? error.message : String(error)
7071
+ });
7072
+ return;
7073
+ }
7074
+ const subAgentResultText = buildSubagentResultText(subAgentMessages);
7075
+ if (!subAgentResultText) {
7076
+ return;
7077
+ }
7078
+ const cached = cacheSubAgentResult(state, part.callID, subAgentResultText);
7079
+ part.state.output = stripHallucinationsFromString(
7080
+ mergeSubagentResult(part.state.output, cached)
7081
+ );
7082
+ });
6710
7083
  };
6711
7084
 
6712
7085
  // lib/messages/reasoning-strip.ts
@@ -7294,11 +7667,7 @@ var COMPRESS_TRIGGER_PROMPT = [
7294
7667
  function getTriggerPrompt(tool3, state, config, messages, userFocus) {
7295
7668
  const base = COMPRESS_TRIGGER_PROMPT;
7296
7669
  const compressedBlockGuidance = config.compress.mode === "message" ? "" : buildCompressedBlockGuidance(state);
7297
- const availableMessageIdGuidance = buildAvailableMessageIdGuidance(
7298
- state,
7299
- config,
7300
- messages
7301
- );
7670
+ const availableMessageIdGuidance = buildAvailableMessageIdGuidance(state, config, messages);
7302
7671
  const sections = [base, availableMessageIdGuidance, compressedBlockGuidance];
7303
7672
  if (userFocus && userFocus.trim().length > 0) {
7304
7673
  sections.push(`Additional user focus:
@@ -7811,13 +8180,14 @@ var INTERNAL_AGENT_SIGNATURES = [
7811
8180
  "You are an anchored context summarization assistant for coding sessions",
7812
8181
  "Summarize what was done in this conversation"
7813
8182
  ];
7814
- function createSystemPromptHandler(state, logger, config, prompts) {
8183
+ function createSystemPromptHandler(target, logger, config, prompts) {
7815
8184
  return async (input, output) => {
7816
- if (input.model?.limit?.context) {
8185
+ const state = input.sessionID ? resolveSessionState(target, input.sessionID) : target instanceof SessionStateStore ? void 0 : target;
8186
+ if (state && input.model?.limit?.context) {
7817
8187
  state.modelContextLimit = input.model.limit.context;
7818
8188
  logger.debug("Cached model context limit", { limit: state.modelContextLimit });
7819
8189
  }
7820
- if (state.isSubAgent && !config.experimental.allowSubAgents) {
8190
+ if (state?.isSubAgent && !config.experimental.allowSubAgents) {
7821
8191
  return;
7822
8192
  }
7823
8193
  const systemText = output.system.join("\n");
@@ -7825,7 +8195,7 @@ function createSystemPromptHandler(state, logger, config, prompts) {
7825
8195
  logger.info("Skipping DCP system prompt injection for internal agent");
7826
8196
  return;
7827
8197
  }
7828
- const effectivePermission = input.sessionID && state.sessionId === input.sessionID ? compressPermission(state, config) : config.compress.permission;
8198
+ const effectivePermission = input.sessionID && state?.sessionId === input.sessionID ? compressPermission(state, config) : config.compress.permission;
7829
8199
  if (effectivePermission === "deny") {
7830
8200
  return;
7831
8201
  }
@@ -7834,8 +8204,8 @@ function createSystemPromptHandler(state, logger, config, prompts) {
7834
8204
  const newPrompt = renderSystemPrompt(
7835
8205
  runtimePrompts,
7836
8206
  buildProtectedToolsExtension(config.compress.protectedTools),
7837
- !!state.manualMode,
7838
- state.isSubAgent && config.experimental.allowSubAgents
8207
+ !!state?.manualMode,
8208
+ !!state?.isSubAgent && config.experimental.allowSubAgents
7839
8209
  );
7840
8210
  if (output.system.length > 0) {
7841
8211
  output.system[output.system.length - 1] += "\n\n" + newPrompt;
@@ -7844,7 +8214,7 @@ function createSystemPromptHandler(state, logger, config, prompts) {
7844
8214
  }
7845
8215
  };
7846
8216
  }
7847
- function createChatMessageTransformHandler(client, state, logger, config, prompts, hostPermissions) {
8217
+ function createChatMessageTransformHandler(client, target, logger, config, prompts, hostPermissions) {
7848
8218
  return async (input, output) => {
7849
8219
  const receivedMessages = Array.isArray(output.messages) ? output.messages.length : 0;
7850
8220
  const messages = filterMessagesInPlace(output.messages);
@@ -7854,7 +8224,14 @@ function createChatMessageTransformHandler(client, state, logger, config, prompt
7854
8224
  usable: messages.length
7855
8225
  });
7856
8226
  }
7857
- await checkSession(client, state, logger, output.messages, config.manualMode.enabled);
8227
+ const state = await checkSession(
8228
+ client,
8229
+ target,
8230
+ logger,
8231
+ output.messages,
8232
+ config.manualMode.enabled
8233
+ );
8234
+ if (!state) return;
7858
8235
  syncCompressPermissionState(state, config, hostPermissions, output.messages);
7859
8236
  if (state.isSubAgent && !config.experimental.allowSubAgents) {
7860
8237
  return;
@@ -7900,7 +8277,7 @@ function createChatMessageTransformHandler(client, state, logger, config, prompt
7900
8277
  }
7901
8278
  };
7902
8279
  }
7903
- function createCommandExecuteHandler(client, state, logger, config, workingDirectory, hostPermissions) {
8280
+ function createCommandExecuteHandler(client, target, logger, config, workingDirectory, hostPermissions) {
7904
8281
  return async (input, output) => {
7905
8282
  if (!config.commands.enabled) {
7906
8283
  return;
@@ -7910,9 +8287,9 @@ function createCommandExecuteHandler(client, state, logger, config, workingDirec
7910
8287
  path: { id: input.sessionID }
7911
8288
  });
7912
8289
  const messages = filterMessages(messagesResponse.data || messagesResponse);
7913
- await ensureSessionInitialized(
8290
+ const state = await initializeSessionState(
7914
8291
  client,
7915
- state,
8292
+ target,
7916
8293
  input.sessionID,
7917
8294
  logger,
7918
8295
  messages,
@@ -7997,7 +8374,7 @@ function createTextCompleteHandler() {
7997
8374
  output.text = stripHallucinationsFromString(output.text);
7998
8375
  };
7999
8376
  }
8000
- function createEventHandler(state, logger) {
8377
+ function createEventHandler(target, logger) {
8001
8378
  return async (input) => {
8002
8379
  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
8380
  if (input.event.type !== "message.part.updated") {
@@ -8007,16 +8384,25 @@ function createEventHandler(state, logger) {
8007
8384
  if (part?.type !== "tool" || part.tool !== "compress") {
8008
8385
  return;
8009
8386
  }
8387
+ if (typeof part.callID !== "string" || typeof part.messageID !== "string") {
8388
+ return;
8389
+ }
8390
+ const eventSessionId = typeof input.event.properties?.sessionID === "string" ? input.event.properties.sessionID : typeof part.sessionID === "string" ? part.sessionID : void 0;
8391
+ const state = target instanceof SessionStateStore ? target.resolveEventState(eventSessionId, part.messageID) : target;
8392
+ const key = buildCompressionTimingKey(part.messageID, part.callID);
8393
+ if (state) pruneCompressionTiming(state);
8010
8394
  if (part.state.status === "pending") {
8011
- if (typeof part.callID !== "string" || typeof part.messageID !== "string") {
8395
+ const startedAt = eventTime ?? Date.now();
8396
+ if (!state && target instanceof SessionStateStore) {
8397
+ target.recordOrphanStart(key, startedAt);
8012
8398
  return;
8013
8399
  }
8014
- const startedAt = eventTime ?? Date.now();
8015
- const key = buildCompressionTimingKey(part.messageID, part.callID);
8400
+ if (!state) return;
8016
8401
  if (state.compressionTiming.startsByCallId.has(key)) {
8017
8402
  return;
8018
8403
  }
8019
8404
  state.compressionTiming.startsByCallId.set(key, startedAt);
8405
+ state.compressionTiming.recordedAtByCallId.set(key, Date.now());
8020
8406
  logger.debug("Recorded compression start", {
8021
8407
  messageID: part.messageID,
8022
8408
  callID: part.callID,
@@ -8025,20 +8411,26 @@ function createEventHandler(state, logger) {
8025
8411
  return;
8026
8412
  }
8027
8413
  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);
8414
+ const start = state ? consumeCompressionStart(state, part.messageID, part.callID) : target instanceof SessionStateStore ? target.consumeOrphanStart(key) : void 0;
8033
8415
  const durationMs = resolveCompressionDuration(start, eventTime, part.state.time);
8034
8416
  if (typeof durationMs !== "number") {
8035
8417
  return;
8036
8418
  }
8419
+ if (!state && target instanceof SessionStateStore) {
8420
+ target.recordOrphanPending(key, {
8421
+ messageId: part.messageID,
8422
+ callId: part.callID,
8423
+ durationMs
8424
+ });
8425
+ return;
8426
+ }
8427
+ if (!state) return;
8037
8428
  state.compressionTiming.pendingByCallId.set(key, {
8038
8429
  messageId: part.messageID,
8039
8430
  callId: part.callID,
8040
8431
  durationMs
8041
8432
  });
8433
+ state.compressionTiming.recordedAtByCallId.set(key, Date.now());
8042
8434
  const updates = applyPendingCompressionDurations(state);
8043
8435
  if (updates === 0) {
8044
8436
  return;
@@ -8055,44 +8447,19 @@ function createEventHandler(state, logger) {
8055
8447
  if (part.state.status === "running") {
8056
8448
  return;
8057
8449
  }
8058
- if (typeof part.callID === "string" && typeof part.messageID === "string") {
8450
+ if (state) {
8059
8451
  state.compressionTiming.startsByCallId.delete(
8060
8452
  buildCompressionTimingKey(part.messageID, part.callID)
8061
8453
  );
8454
+ state.compressionTiming.recordedAtByCallId.delete(key);
8455
+ } else if (target instanceof SessionStateStore) {
8456
+ target.deleteOrphanStart(key);
8062
8457
  }
8063
8458
  };
8064
8459
  }
8065
8460
 
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
8461
  // lib/update.ts
8095
- import { readFile as readFile2, rm } from "fs/promises";
8462
+ import { readFile as readFile2 } from "fs/promises";
8096
8463
  import { basename, dirname as dirname3, join as join5 } from "path";
8097
8464
  import { fileURLToPath } from "url";
8098
8465
  var PACKAGE_NAME = "@lexwdex-org/opencode-dcp";
@@ -8101,15 +8468,16 @@ function startAutoUpdate(ctx, enabled) {
8101
8468
  const controller = new AbortController();
8102
8469
  const timeout = setTimeout(() => controller.abort(), 1e4);
8103
8470
  void checkAutoUpdate(controller.signal).then((result) => {
8104
- if (!result.updated) return;
8471
+ if (!result.available) return;
8105
8472
  setTimeout(() => {
8106
- ctx.client.tui.showToast({
8473
+ void ctx.client.tui.showToast({
8107
8474
  body: {
8108
- title: "DCP update ready",
8109
- message: `Updated ${result.name} from ${result.current} to ${result.latest}. Restart OpenCode to finish.`,
8475
+ title: "DCP update available",
8476
+ message: `${result.name} ${result.latest} is available (current ${result.current}). Reinstall the plugin to update.`,
8110
8477
  variant: "info",
8111
8478
  duration: 7e3
8112
8479
  }
8480
+ }).catch(() => {
8113
8481
  });
8114
8482
  }, 5e3);
8115
8483
  }).catch(() => {
@@ -8117,25 +8485,14 @@ function startAutoUpdate(ctx, enabled) {
8117
8485
  }
8118
8486
  async function checkAutoUpdate(signal) {
8119
8487
  const packageDir = await findPackageDir(PACKAGE_NAME);
8120
- if (!packageDir) return { updated: false };
8488
+ if (!packageDir) return { available: false };
8121
8489
  const pkg = await readPackageJson(join5(packageDir, "package.json"));
8122
- if (!pkg?.name || !pkg.version) return { updated: false };
8490
+ if (!pkg?.name || !pkg.version) return { available: false };
8123
8491
  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 };
8492
+ if (!latest || !isVersionNewer(latest, pkg.version)) return { available: false };
8493
+ const wrapperDir = await findAutoUpdateWrapperDir(packageDir, pkg.name);
8494
+ if (!wrapperDir) return { available: false };
8495
+ return { available: true, name: pkg.name, current: pkg.version, latest };
8139
8496
  }
8140
8497
  async function findPackageDir(name) {
8141
8498
  let dir = dirname3(fileURLToPath(import.meta.url));
@@ -8147,7 +8504,7 @@ async function findPackageDir(name) {
8147
8504
  dir = parent;
8148
8505
  }
8149
8506
  }
8150
- async function updateRemoveDir(packageDir, name) {
8507
+ async function findAutoUpdateWrapperDir(packageDir, name) {
8151
8508
  const packageParent = dirname3(packageDir);
8152
8509
  const nodeModulesDir = basename(packageParent).startsWith("@") ? dirname3(packageParent) : packageParent;
8153
8510
  if (basename(nodeModulesDir) !== "node_modules") return void 0;
@@ -8243,36 +8600,33 @@ var server = (async (ctx) => {
8243
8600
  return {};
8244
8601
  }
8245
8602
  const logger = new Logger(config.debug);
8246
- const state = createSessionState();
8603
+ const states = new SessionStateStore(createSessionState);
8247
8604
  const prompts = new PromptStore(logger, ctx.directory, config.experimental.customPrompts);
8248
8605
  const hostPermissions = {
8249
8606
  global: void 0,
8250
8607
  agents: {}
8251
8608
  };
8252
- if (isSecureMode()) {
8253
- configureClientAuth(ctx.client);
8254
- }
8255
8609
  logger.info("DCP initialized", {
8256
8610
  strategies: config.strategies
8257
8611
  });
8258
8612
  startAutoUpdate(ctx, config.autoUpdate);
8259
8613
  const compressToolContext = {
8260
8614
  client: ctx.client,
8261
- state,
8615
+ state: states,
8262
8616
  logger,
8263
8617
  config,
8264
8618
  prompts
8265
8619
  };
8266
8620
  return {
8267
8621
  "experimental.chat.system.transform": createSystemPromptHandler(
8268
- state,
8622
+ states,
8269
8623
  logger,
8270
8624
  config,
8271
8625
  prompts
8272
8626
  ),
8273
8627
  "experimental.chat.messages.transform": createChatMessageTransformHandler(
8274
8628
  ctx.client,
8275
- state,
8629
+ states,
8276
8630
  logger,
8277
8631
  config,
8278
8632
  prompts,
@@ -8281,13 +8635,13 @@ var server = (async (ctx) => {
8281
8635
  "experimental.text.complete": createTextCompleteHandler(),
8282
8636
  "command.execute.before": createCommandExecuteHandler(
8283
8637
  ctx.client,
8284
- state,
8638
+ states,
8285
8639
  logger,
8286
8640
  config,
8287
8641
  ctx.directory,
8288
8642
  hostPermissions
8289
8643
  ),
8290
- event: createEventHandler(state, logger),
8644
+ event: createEventHandler(states, logger),
8291
8645
  tool: {
8292
8646
  ...config.compress.permission !== "deny" && {
8293
8647
  compress: config.compress.mode === "message" ? createCompressMessageTool(compressToolContext) : createCompressRangeTool(compressToolContext)