@lexwdex-org/opencode-dcp 3.4.11 → 3.4.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,114 @@
1
+ // lib/auto-prune.ts
2
+ function tokenize(text) {
3
+ const tokens = /* @__PURE__ */ new Set();
4
+ for (const match of text.toLowerCase().matchAll(/[\p{L}\p{N}]+/gu)) {
5
+ const word = match[0];
6
+ if (/[\u4e00-\u9fff]/.test(word)) {
7
+ if (word.length === 1) {
8
+ tokens.add(word);
9
+ continue;
10
+ }
11
+ for (let index = 0; index < word.length - 1; index++) {
12
+ tokens.add(word.slice(index, index + 2));
13
+ }
14
+ } else {
15
+ tokens.add(word);
16
+ }
17
+ }
18
+ return tokens;
19
+ }
20
+ function jaccard(a, b) {
21
+ if (a.size === 0 && b.size === 0) return 1;
22
+ let intersection = 0;
23
+ for (const token of a) {
24
+ if (b.has(token)) intersection++;
25
+ }
26
+ return intersection / (a.size + b.size - intersection);
27
+ }
28
+ var WINDOW_SIZE = 4;
29
+ var DRIFT_BASELINE = 3;
30
+ function extractText(parts) {
31
+ const texts = [];
32
+ for (const part of parts) {
33
+ if (part && typeof part === "object" && part.type === "text" && typeof part.text === "string") {
34
+ texts.push(part.text);
35
+ }
36
+ }
37
+ return texts.join(" ").trim();
38
+ }
39
+ var AutoPruner = class {
40
+ constructor(config, now) {
41
+ this.config = config;
42
+ this.now = now ?? Date.now;
43
+ }
44
+ sessions = /* @__PURE__ */ new Map();
45
+ now;
46
+ observeUserMessage(sessionID, parts, at = this.now()) {
47
+ const state = this.state(sessionID);
48
+ const text = extractText(parts);
49
+ const signals = this.evaluate(state, text, at);
50
+ if (text) {
51
+ state.window.push(text);
52
+ if (state.window.length > WINDOW_SIZE) state.window.shift();
53
+ }
54
+ state.count += 1;
55
+ state.lastAt = at;
56
+ for (const signal of signals) {
57
+ if (!state.pendingSignals.includes(signal)) state.pendingSignals.push(signal);
58
+ }
59
+ return { signals };
60
+ }
61
+ consumePending(sessionID, at = this.now()) {
62
+ const state = this.sessions.get(sessionID);
63
+ if (!state || state.pendingSignals.length === 0) return null;
64
+ const signals = [...state.pendingSignals];
65
+ state.pendingSignals = [];
66
+ if (at - state.lastTriggerAt < this.config.cooldownMs) return null;
67
+ state.lastTriggerAt = at;
68
+ return signals;
69
+ }
70
+ markPruned(sessionID, at = this.now()) {
71
+ const state = this.sessions.get(sessionID);
72
+ if (!state) return;
73
+ state.count = 0;
74
+ state.window = [];
75
+ state.pendingSignals = [];
76
+ state.lastTriggerAt = at;
77
+ }
78
+ dropSession(sessionID) {
79
+ this.sessions.delete(sessionID);
80
+ }
81
+ evaluate(state, text, at) {
82
+ if (state.count + 1 < this.config.minMessages) return [];
83
+ const signals = [];
84
+ if (state.count > 0 && at - state.lastAt >= this.config.idleGapMs) {
85
+ signals.push("idle-gap");
86
+ }
87
+ if (state.count >= DRIFT_BASELINE && text) {
88
+ const current = tokenize(text);
89
+ let max = 0;
90
+ for (let index = Math.max(0, state.window.length - DRIFT_BASELINE); index < state.window.length; index++) {
91
+ max = Math.max(max, jaccard(current, tokenize(state.window[index])));
92
+ }
93
+ if (max < this.config.driftThreshold) signals.push("topic-drift");
94
+ }
95
+ if (state.count + 1 >= this.config.volumeThreshold) signals.push("volume");
96
+ return signals;
97
+ }
98
+ state(sessionID) {
99
+ let state = this.sessions.get(sessionID);
100
+ if (!state) {
101
+ state = { window: [], count: 0, lastAt: 0, pendingSignals: [], lastTriggerAt: 0 };
102
+ this.sessions.set(sessionID, state);
103
+ if (this.sessions.size > 200) {
104
+ const oldest = this.sessions.keys().next().value;
105
+ if (oldest !== void 0 && oldest !== sessionID) this.sessions.delete(oldest);
106
+ }
107
+ }
108
+ return state;
109
+ }
110
+ };
111
+
1
112
  // lib/config.ts
2
113
  import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from "fs";
3
114
  import { join, dirname } from "path";
@@ -864,17 +975,35 @@ var ParseErrorCode;
864
975
 
865
976
  // lib/config.ts
866
977
  var DEFAULT_FAILURE_COOLDOWN_MS = 3e4;
978
+ var DEFAULT_AUTO_PRUNE = {
979
+ enabled: true,
980
+ minMessages: 8,
981
+ volumeThreshold: 30,
982
+ driftThreshold: 0.18,
983
+ idleGapMs: 30 * 6e4,
984
+ cooldownMs: 5 * 6e4
985
+ };
867
986
  var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
868
987
  "$schema",
869
988
  "enabled",
870
989
  "autoUpdate",
871
990
  "debug",
991
+ "language",
872
992
  "commands",
873
993
  "commands.enabled",
874
994
  "experimental",
875
995
  "experimental.customPrompts",
876
996
  "summarize",
877
- "summarize.failureCooldownMs"
997
+ "summarize.failureCooldownMs",
998
+ "autoPrune",
999
+ "autoPrune.enabled",
1000
+ "autoPrune.minMessages",
1001
+ "autoPrune.volumeThreshold",
1002
+ "autoPrune.driftThreshold",
1003
+ "autoPrune.idleGapMs",
1004
+ "autoPrune.cooldownMs",
1005
+ "tool",
1006
+ "tool.enabled"
878
1007
  ]);
879
1008
  var DEPRECATED_CONFIG_KEYS = /* @__PURE__ */ new Set([
880
1009
  "compress",
@@ -949,6 +1078,13 @@ function validateConfigTypes(config) {
949
1078
  if (config.debug !== void 0 && typeof config.debug !== "boolean") {
950
1079
  errors.push({ key: "debug", expected: "boolean", actual: typeof config.debug });
951
1080
  }
1081
+ if (config.language !== void 0 && config.language !== "zh" && config.language !== "en") {
1082
+ errors.push({
1083
+ key: "language",
1084
+ expected: '"zh" or "en"',
1085
+ actual: JSON.stringify(config.language)
1086
+ });
1087
+ }
952
1088
  const commands = config.commands;
953
1089
  if (commands !== void 0) {
954
1090
  if (typeof commands !== "object" || commands === null || Array.isArray(commands)) {
@@ -999,6 +1135,60 @@ function validateConfigTypes(config) {
999
1135
  }
1000
1136
  }
1001
1137
  }
1138
+ const autoPrune = config.autoPrune;
1139
+ if (autoPrune !== void 0) {
1140
+ if (typeof autoPrune !== "object" || autoPrune === null || Array.isArray(autoPrune)) {
1141
+ errors.push({
1142
+ key: "autoPrune",
1143
+ expected: "object",
1144
+ actual: typeof autoPrune
1145
+ });
1146
+ } else {
1147
+ const numericKeys = [
1148
+ ["minMessages", 1, Number.POSITIVE_INFINITY],
1149
+ ["volumeThreshold", 2, Number.POSITIVE_INFINITY],
1150
+ ["driftThreshold", 0, 1],
1151
+ ["idleGapMs", 0, Number.POSITIVE_INFINITY],
1152
+ ["cooldownMs", 0, Number.POSITIVE_INFINITY]
1153
+ ];
1154
+ for (const [key, min, max] of numericKeys) {
1155
+ const value = autoPrune[key];
1156
+ if (value !== void 0 && (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max)) {
1157
+ errors.push({
1158
+ key: `autoPrune.${key}`,
1159
+ expected: `number in [${min}, ${max === Number.POSITIVE_INFINITY ? "\u221E" : max}]`,
1160
+ actual: JSON.stringify(value)
1161
+ });
1162
+ }
1163
+ }
1164
+ for (const key of ["enabled"]) {
1165
+ const value = autoPrune[key];
1166
+ if (value !== void 0 && typeof value !== "boolean") {
1167
+ errors.push({
1168
+ key: `autoPrune.${key}`,
1169
+ expected: "boolean",
1170
+ actual: typeof value
1171
+ });
1172
+ }
1173
+ }
1174
+ }
1175
+ }
1176
+ const tool2 = config.tool;
1177
+ if (tool2 !== void 0) {
1178
+ if (typeof tool2 !== "object" || tool2 === null || Array.isArray(tool2)) {
1179
+ errors.push({
1180
+ key: "tool",
1181
+ expected: "object",
1182
+ actual: typeof tool2
1183
+ });
1184
+ } else if (tool2.enabled !== void 0 && typeof tool2.enabled !== "boolean") {
1185
+ errors.push({
1186
+ key: "tool.enabled",
1187
+ expected: "boolean",
1188
+ actual: typeof tool2.enabled
1189
+ });
1190
+ }
1191
+ }
1002
1192
  return errors;
1003
1193
  }
1004
1194
  function showConfigWarnings(ctx, configPath, configData, isProject) {
@@ -1049,6 +1239,7 @@ var defaultConfig = {
1049
1239
  enabled: true,
1050
1240
  autoUpdate: true,
1051
1241
  debug: false,
1242
+ language: "zh",
1052
1243
  commands: {
1053
1244
  enabled: true
1054
1245
  },
@@ -1057,6 +1248,10 @@ var defaultConfig = {
1057
1248
  },
1058
1249
  summarize: {
1059
1250
  failureCooldownMs: DEFAULT_FAILURE_COOLDOWN_MS
1251
+ },
1252
+ autoPrune: { ...DEFAULT_AUTO_PRUNE },
1253
+ tool: {
1254
+ enabled: true
1060
1255
  }
1061
1256
  };
1062
1257
  var GLOBAL_CONFIG_DIR = process.env.XDG_CONFIG_HOME ? join(process.env.XDG_CONFIG_HOME, "opencode") : join(homedir(), ".config", "opencode");
@@ -1148,12 +1343,36 @@ function mergeSummarize(base, override) {
1148
1343
  failureCooldownMs: typeof override.failureCooldownMs === "number" && Number.isFinite(override.failureCooldownMs) && override.failureCooldownMs >= 0 ? override.failureCooldownMs : base.failureCooldownMs
1149
1344
  };
1150
1345
  }
1346
+ function mergeAutoPrune(base, override) {
1347
+ if (!override || typeof override !== "object" || Array.isArray(override)) {
1348
+ return base;
1349
+ }
1350
+ const number = (key, min, max = Number.POSITIVE_INFINITY) => typeof override[key] === "number" && Number.isFinite(override[key]) && override[key] >= min && override[key] <= max ? override[key] : base[key];
1351
+ return {
1352
+ enabled: typeof override.enabled === "boolean" ? override.enabled : base.enabled,
1353
+ minMessages: number("minMessages", 1),
1354
+ volumeThreshold: number("volumeThreshold", 2),
1355
+ driftThreshold: number("driftThreshold", 0, 1),
1356
+ idleGapMs: number("idleGapMs", 0),
1357
+ cooldownMs: number("cooldownMs", 0)
1358
+ };
1359
+ }
1360
+ function mergeTool(base, override) {
1361
+ if (!override) {
1362
+ return base;
1363
+ }
1364
+ return {
1365
+ enabled: typeof override.enabled === "boolean" ? override.enabled : base.enabled
1366
+ };
1367
+ }
1151
1368
  function deepCloneConfig(config) {
1152
1369
  return {
1153
1370
  ...config,
1154
1371
  commands: { ...config.commands },
1155
1372
  experimental: { ...config.experimental },
1156
- summarize: { ...config.summarize }
1373
+ summarize: { ...config.summarize },
1374
+ autoPrune: { ...config.autoPrune },
1375
+ tool: { ...config.tool }
1157
1376
  };
1158
1377
  }
1159
1378
  function mergeLayer(config, data) {
@@ -1161,9 +1380,12 @@ function mergeLayer(config, data) {
1161
1380
  enabled: typeof data.enabled === "boolean" ? data.enabled : config.enabled,
1162
1381
  autoUpdate: typeof data.autoUpdate === "boolean" ? data.autoUpdate : config.autoUpdate,
1163
1382
  debug: typeof data.debug === "boolean" ? data.debug : config.debug,
1383
+ language: data.language === "zh" || data.language === "en" ? data.language : config.language,
1164
1384
  commands: mergeCommands(config.commands, data.commands),
1165
1385
  experimental: mergeExperimental(config.experimental, data.experimental),
1166
- summarize: mergeSummarize(config.summarize, data.summarize)
1386
+ summarize: mergeSummarize(config.summarize, data.summarize),
1387
+ autoPrune: mergeAutoPrune(config.autoPrune, data.autoPrune),
1388
+ tool: mergeTool(config.tool, data.tool)
1167
1389
  };
1168
1390
  }
1169
1391
  function scheduleParseWarning(ctx, title, message) {
@@ -1216,6 +1438,29 @@ Using previous/default values`
1216
1438
  return config;
1217
1439
  }
1218
1440
 
1441
+ // lib/session-model.ts
1442
+ function latestUserModel(messages) {
1443
+ if (!Array.isArray(messages)) return null;
1444
+ for (let index = messages.length - 1; index >= 0; index--) {
1445
+ const info = messages[index]?.info;
1446
+ if (info?.role !== "user") continue;
1447
+ const providerID = info.model?.providerID;
1448
+ const modelID = info.model?.modelID;
1449
+ if (typeof providerID === "string" && typeof modelID === "string") {
1450
+ return { providerID, modelID };
1451
+ }
1452
+ }
1453
+ return null;
1454
+ }
1455
+ async function resolveSessionModel(client, sessionID) {
1456
+ try {
1457
+ const response = await client.session.messages({ path: { id: sessionID } });
1458
+ return latestUserModel(response.data ?? response);
1459
+ } catch {
1460
+ return null;
1461
+ }
1462
+ }
1463
+
1219
1464
  // lib/hooks.ts
1220
1465
  function createSessionCompactingHandler(prompts, logger) {
1221
1466
  return async (input, output) => {
@@ -1236,24 +1481,71 @@ function createSessionCompactingHandler(prompts, logger) {
1236
1481
  }
1237
1482
  };
1238
1483
  }
1239
- function latestUserModel(messages) {
1240
- if (!Array.isArray(messages)) return null;
1241
- for (let index = messages.length - 1; index >= 0; index--) {
1242
- const info = messages[index]?.info;
1243
- if (info?.role !== "user") continue;
1244
- const providerID = info.model?.providerID;
1245
- const modelID = info.model?.modelID;
1246
- if (typeof providerID === "string" && typeof modelID === "string") {
1247
- return { providerID, modelID };
1248
- }
1249
- }
1250
- return null;
1251
- }
1252
1484
  async function showToast(client, title, message, variant = "info") {
1253
1485
  await client.tui.showToast({
1254
1486
  body: { title, message, variant, duration: 5e3 }
1255
1487
  }).catch(() => void 0);
1256
1488
  }
1489
+ function createChatMessageHandler(autoPruner) {
1490
+ return async (input, _output) => {
1491
+ const parts = _output?.parts ?? [];
1492
+ autoPruner.observeUserMessage(input.sessionID, parts);
1493
+ };
1494
+ }
1495
+ var SIGNAL_LABELS = {
1496
+ "topic-drift": "\u8BDD\u9898\u53D8\u66F4",
1497
+ volume: "\u6D88\u606F\u91CF\u8FBE\u5230\u9608\u503C",
1498
+ "idle-gap": "\u957F\u65F6\u95F4\u4E2D\u65AD\u540E\u6062\u590D"
1499
+ };
1500
+ function createEventHandler(deps) {
1501
+ async function triggerAutoPrune(sessionID, signals) {
1502
+ const reason = signals.map((signal) => SIGNAL_LABELS[signal]).join("\u3001");
1503
+ const model = await resolveSessionModel(deps.client, sessionID);
1504
+ if (!model) {
1505
+ deps.logger.debug("Auto prune skipped; no session model yet", { sessionId: sessionID });
1506
+ return;
1507
+ }
1508
+ const result = await deps.summarize.summarize({ sessionID, model });
1509
+ deps.autoPruner.markPruned(sessionID);
1510
+ if (result.status === "succeeded") {
1511
+ await showToast(deps.client, "DCP \u81EA\u52A8\u538B\u7F29", `\u68C0\u6D4B\u5230${reason}\uFF0C\u5DF2\u751F\u6210\u65B0\u7684\u8BED\u4E49\u68C0\u67E5\u70B9\u3002`);
1512
+ } else {
1513
+ await showToast(
1514
+ deps.client,
1515
+ "DCP \u81EA\u52A8\u538B\u7F29",
1516
+ `\u68C0\u6D4B\u5230${reason}\uFF0C\u4F46\u538B\u7F29\u5931\u8D25\uFF1B\u539F\u59CB\u4E0A\u4E0B\u6587\u4FDD\u6301\u4E0D\u53D8\u3002`,
1517
+ "warning"
1518
+ );
1519
+ }
1520
+ deps.logger.debug("Auto prune finished", { sessionId: sessionID, status: result.status });
1521
+ }
1522
+ return async (input) => {
1523
+ const event = input.event;
1524
+ const sessionID = event.properties?.sessionID;
1525
+ if (typeof sessionID !== "string" || !sessionID) return;
1526
+ try {
1527
+ if (event.type === "session.idle") {
1528
+ if (!deps.config.enabled) return;
1529
+ const signals = deps.autoPruner.consumePending(sessionID);
1530
+ if (signals) await triggerAutoPrune(sessionID, signals);
1531
+ return;
1532
+ }
1533
+ if (event.type === "session.compacted") {
1534
+ deps.autoPruner.markPruned(sessionID);
1535
+ return;
1536
+ }
1537
+ if (event.type === "session.deleted") {
1538
+ deps.autoPruner.dropSession(sessionID);
1539
+ }
1540
+ } catch (error) {
1541
+ deps.logger.warn("Event handler failed", {
1542
+ type: event.type,
1543
+ sessionId: sessionID,
1544
+ error: error instanceof Error ? error.message : String(error)
1545
+ });
1546
+ }
1547
+ };
1548
+ }
1257
1549
  function createCommandExecuteHandler(client, summarize, logger) {
1258
1550
  return async (input, _output) => {
1259
1551
  if (input.command !== "dcp") return;
@@ -1266,8 +1558,7 @@ function createCommandExecuteHandler(client, summarize, logger) {
1266
1558
  );
1267
1559
  throw new Error("__DCP_HELP_HANDLED__");
1268
1560
  }
1269
- const response = await client.session.messages({ path: { id: input.sessionID } });
1270
- const model = latestUserModel(response.data ?? response);
1561
+ const model = await resolveSessionModel(client, input.sessionID);
1271
1562
  if (!model) {
1272
1563
  await showToast(
1273
1564
  client,
@@ -1503,27 +1794,59 @@ import { homedir as homedir3 } from "os";
1503
1794
  import { dirname as dirname2, join as join3 } from "path";
1504
1795
 
1505
1796
  // lib/prompts/compaction.ts
1506
- var COMPACTION = `\u4F60\u6B63\u5728\u751F\u6210\u5F53\u524D\u4F1A\u8BDD\u552F\u4E00\u7684\u6EDA\u52A8\u68C0\u67E5\u70B9\u3002\u8F93\u51FA\u5C06\u66FF\u4EE3\u65E7\u5BF9\u8BDD\u524D\u7F00\uFF0C\u6210\u4E3A\u540E\u7EED\u6A21\u578B\u770B\u5230\u7684\u7B2C\u4E00\u6BB5\u4E0A\u4E0B\u6587\uFF1BOpenCode \u4F1A\u5728\u5B83\u540E\u9762\u4FDD\u7559\u5C1A\u672A\u538B\u7F29\u7684\u8FD1\u671F\u5C3E\u90E8\u3002
1797
+ var COMPACTION = `\u4F60\u6B63\u5728\u751F\u6210\u5F53\u524D\u4F1A\u8BDD\u552F\u4E00\u7684\u6EDA\u52A8\u68C0\u67E5\u70B9\u3002\u8F93\u51FA\u5C06\u66FF\u4EE3\u65E7\u5BF9\u8BDD\u524D\u7F00\uFF0C\u6210\u4E3A\u540E\u7EED\u6A21\u578B\u770B\u5230\u7684\u7B2C\u4E00\u6BB5\u4E0A\u4E0B\u6587\uFF1BOpenCode \u4F1A\u5728\u5B83\u540E\u9762\u4FDD\u7559\u5C1A\u672A\u538B\u7F29\u7684\u8FD1\u671F\u5C3E\u90E8\u3002\u7CFB\u7EDF\u7EA7\u5185\u5BB9\uFF08AGENTS.md\u3001\u9879\u76EE\u89C4\u5219\u7B49\uFF09\u7531 OpenCode \u5728\u6BCF\u6B21\u8BF7\u6C42\u65F6\u72EC\u7ACB\u6CE8\u5165\uFF0C\u4E0D\u5C5E\u4E8E\u538B\u7F29\u8303\u56F4\uFF0C\u4E0D\u8981\u590D\u8FF0\u5B83\u4EEC\u3002
1507
1798
 
1508
- \u8FD9\u4E0D\u662F\u804A\u5929\u8BB0\u5F55\u6458\u8981\uFF0C\u800C\u662F\u53EF\u76F4\u63A5\u7EE7\u7EED\u5DE5\u4F5C\u7684\u8BED\u4E49\u526A\u679D\u7ED3\u679C\uFF1A
1799
+ \u8FD9\u4E0D\u662F\u804A\u5929\u8BB0\u5F55\u6458\u8981\uFF0C\u800C\u662F\u53EF\u76F4\u63A5\u7EE7\u7EED\u5DE5\u4F5C\u7684\u8BED\u4E49\u526A\u679D\u7ED3\u679C\u3002\u6309\u4EE5\u4E0B\u89C4\u5219\u538B\u7F29\uFF1A
1509
1800
 
1510
1801
  1. \u5982\u679C\u8F93\u5165\u542B\u6709\u4E0A\u4E00\u4EFD\u68C0\u67E5\u70B9\uFF0C\u628A\u4ECD\u6709\u6548\u7684\u4FE1\u606F\u5408\u5E76\u8FDB\u65B0\u68C0\u67E5\u70B9\uFF1B\u4E0D\u8981\u5D4C\u5957\u3001\u5F15\u7528\u6216\u91CD\u590D\u65E7\u68C0\u67E5\u70B9\u3002
1511
1802
  2. \u5220\u9664\u65E0\u5173\u95F2\u804A\u3001\u5176\u4ED6\u9879\u76EE\u6216\u5176\u4ED6\u4ED3\u5E93\u7684\u5BF9\u8BDD\u3001\u91CD\u590D\u89E3\u91CA\u3001\u5DF2\u7ECF\u63A8\u7FFB\u4E14\u4E0D\u518D\u6709\u8BCA\u65AD\u4EF7\u503C\u7684\u65B9\u6848\u3002
1512
1803
  3. \u591A\u6B21\u5DE5\u5177\u8C03\u7528\u8BD5\u9519\u6216\u5931\u8D25\u540E\u6210\u529F\u65F6\uFF0C\u53EA\u4FDD\u7559\u6700\u7EC8\u6210\u529F\u7ED3\u679C\uFF1B\u4EC5\u5F53\u6839\u56E0\u4F1A\u5F71\u54CD\u540E\u7EED\u5DE5\u4F5C\u65F6\u4FDD\u7559\u4E00\u6B21\u7B80\u77ED\u5931\u8D25\u539F\u56E0\u3002
1513
1804
  4. \u540C\u4E00\u5185\u5BB9\u6216\u6587\u4EF6\u88AB\u91CD\u590D\u7F16\u8F91\u65F6\uFF0C\u53EA\u4FDD\u7559\u6700\u7EC8\u72B6\u6001\u3001\u4ECD\u6709\u6548\u7684\u5173\u952E\u51B3\u7B56\u548C\u5FC5\u8981\u7406\u7531\uFF0C\u4E0D\u590D\u8FF0\u6BCF\u8F6E\u4FEE\u6539\u3002
1514
- 5. \u5C0F\u578B\u5DF2\u5B8C\u6210\u4E3B\u9898\u538B\u7F29\u4E3A\u4E00\u53E5\u7ED3\u679C\uFF1B\u4E0D\u8981\u4FDD\u7559\u5B8C\u6574\u6267\u884C\u8FC7\u7A0B\u3002
1805
+ 5. \u6309\u65F6\u95F4\u5206\u5C42\u51B3\u5B9A\u538B\u7F29\u6DF1\u5EA6\uFF1A\u65E9\u671F\u5386\u53F2\u548C\u4E2D\u90E8\u5386\u53F2\u9AD8\u5EA6\u538B\u7F29\u2014\u2014\u6BCF\u4E2A\u4E3B\u9898\u53EA\u7559\u4E00\u53E5\u8BDD\u7ED3\u8BBA\uFF0C\u4E0D\u7559\u8FC7\u7A0B\uFF1B\u6700\u8FD1\u5386\u53F2\u8F7B\u5EA6\u538B\u7F29\u2014\u2014\u5C24\u5176\u662F\u4E0E\u5F53\u524D\u4EFB\u52A1\u76F8\u5173\u7684\u5185\u5BB9\uFF0C\u4FDD\u7559\u7EE7\u7EED\u5DE5\u4F5C\u6240\u9700\u7684\u5173\u952E\u7EC6\u8282\uFF08\u6587\u4EF6\u8DEF\u5F84\u3001\u63A5\u53E3\u3001\u547D\u4EE4\u3001\u6D4B\u8BD5\u7ED3\u679C\u3001\u9519\u8BEF\u4E8B\u5B9E\u3001\u4ECD\u6709\u6548\u7684\u51B3\u7B56\u53CA\u7406\u7531\uFF09\uFF0C\u53EA\u6298\u53E0\u91CD\u590D\u4E0E\u5DF2\u5931\u6548\u7684\u5185\u5BB9\u3002\u6700\u8FD1\u5386\u53F2\u6307\u81EA\u4E0A\u4E00\u4EFD\u68C0\u67E5\u70B9\u4EE5\u6765\u7684\u65B0\u5185\u5BB9\u3002
1515
1806
 
1516
1807
  \u4F7F\u7528\u4EE5\u4E0B\u56FA\u5B9A\u7ED3\u6784\uFF0C\u7701\u7565\u786E\u5B9E\u4E3A\u7A7A\u7684\u6761\u76EE\uFF1A
1517
1808
 
1518
- ## \u5F53\u524D\u76EE\u6807
1519
- ## \u6709\u6548\u7EA6\u675F
1520
- ## \u5DF2\u786E\u8BA4\u51B3\u7B56
1521
- ## \u5F53\u524D\u5B9E\u73B0\u72B6\u6001
1522
- ## \u5DF2\u5B8C\u6210\u7684\u5C0F\u578B\u4E3B\u9898
1809
+ ## \u5386\u53F2\u6982\u8981
1810
+ \u65E9\u671F\u4E0E\u4E2D\u90E8\u5386\u53F2\u7684\u4E3B\u9898\u548C\u80CC\u666F\uFF0C\u6BCF\u9879\u4E00\u53E5\u8BDD\u7ED3\u8BBA\uFF1B\u8FDC\u671F\u5DF2\u5B8C\u6210\u4EFB\u52A1\u5F52\u5165\u6B64\u5904\uFF0C\u4E0D\u542B\u6267\u884C\u8FC7\u7A0B\u3002
1811
+
1812
+ ## \u5DF2\u5B8C\u6210\u4EFB\u52A1\u7684\u6982\u62EC
1813
+ \u6700\u8FD1\u7684\u5DF2\u5B8C\u6210\u4EFB\u52A1\uFF0C\u6BCF\u4E2A\u4EFB\u52A1\u4E00\u53E5\u8BDD\u6982\u62EC\u5176\u7ED3\u679C\u4E0E\u5173\u952E\u4EA7\u51FA\u3002
1814
+
1815
+ ## \u8FDB\u884C\u4E2D\u4EFB\u52A1\u8BE6\u60C5
1816
+ \u5F53\u524D\u6B63\u5728\u8FDB\u884C\u7684\u4EFB\u52A1\u9010\u9879\u5199\u6E05\uFF1A\u76EE\u6807\u3001\u5DF2\u5B8C\u6210\u6B65\u9AA4\u3001\u6D89\u53CA\u6587\u4EF6\u8DEF\u5F84\u4E0E\u63A5\u53E3\u3001\u5173\u952E\u51B3\u7B56\u3001\u9047\u5230\u7684\u963B\u585E\u3001\u4E0B\u4E00\u6B65\u5177\u4F53\u52A8\u4F5C\u3002\u672C\u8282\u5C5E\u4E8E\u8F7B\u5EA6\u538B\u7F29\u533A\uFF0C\u5B81\u53EF\u591A\u4FDD\u7559\u7EC6\u8282\uFF0C\u4E0D\u505A\u4E8C\u6B21\u63A8\u65AD\u3002
1817
+
1523
1818
  ## \u672A\u89E3\u51B3\u95EE\u9898
1524
- ## \u4E0B\u4E00\u6B65
1819
+ \u8DE8\u4EFB\u52A1\u7684\u9057\u7559\u98CE\u9669\u548C\u5F85\u786E\u8BA4\u4E8B\u9879\u3002\u53EA\u5199\u672A\u5728\u300C\u8FDB\u884C\u4E2D\u4EFB\u52A1\u8BE6\u60C5\u300D\u4E2D\u51FA\u73B0\u7684\u5185\u5BB9\uFF0C\u907F\u514D\u4E0E\u8BE5\u8282\u91CD\u590D\u3002
1820
+
1821
+ \u4FDD\u6301\u5177\u4F53\u3001\u53EF\u9A8C\u8BC1\u548C\u9879\u76EE\u5185\u805A\u3002\u8FDB\u884C\u4E2D\u7684\u4EFB\u52A1\u5FC5\u987B\u80FD\u51ED\u68C0\u67E5\u70B9\u76F4\u63A5\u7EE7\u7EED\uFF0C\u4E0D\u8981\u4F9D\u8D56\u5DF2\u88AB\u538B\u7F29\u6389\u7684\u4E2D\u95F4\u8FC7\u7A0B\u3002\u4FDD\u7559\u6587\u4EF6\u8DEF\u5F84\u3001\u63A5\u53E3\u3001\u547D\u4EE4\u3001\u6D4B\u8BD5\u7ED3\u679C\u548C\u9519\u8BEF\u4E8B\u5B9E\u7B49\u786C\u4E8B\u5B9E\uFF0C\u4F46\u4E0D\u8981\u4FDD\u7559\u6D88\u606F ID\u3001\u5757 ID\u3001\u951A\u70B9\u3001\u5360\u4F4D\u7B26\u3001\u63A7\u5236\u6807\u7B7E\u6216\u8FC7\u7A0B\u6027\u804A\u5929\u3002`;
1822
+ var COMPACTION_EN = `You are generating the single rolling checkpoint for this session. Your output will replace the old conversation prefix as the first context the model sees afterwards; OpenCode keeps an uncompacted recent tail right after it. System-level content (AGENTS.md, project rules, etc.) is injected independently by OpenCode on every request and is not part of compaction; do not restate it.
1525
1823
 
1526
- \u4FDD\u6301\u5177\u4F53\u3001\u53EF\u9A8C\u8BC1\u548C\u9879\u76EE\u5185\u805A\u3002\u4FDD\u7559\u7EE7\u7EED\u5DE5\u4F5C\u5FC5\u9700\u7684\u6587\u4EF6\u8DEF\u5F84\u3001\u63A5\u53E3\u3001\u547D\u4EE4\u3001\u6D4B\u8BD5\u7ED3\u679C\u548C\u9519\u8BEF\u4E8B\u5B9E\uFF0C\u4F46\u4E0D\u8981\u4FDD\u7559\u6D88\u606F ID\u3001\u5757 ID\u3001\u951A\u70B9\u3001\u5360\u4F4D\u7B26\u3001\u63A7\u5236\u6807\u7B7E\u6216\u8FC7\u7A0B\u6027\u804A\u5929\u3002`;
1824
+ This is not a chat-log summary but a semantic pruning result one can resume working from directly. Compress by these rules:
1825
+
1826
+ 1. If the input contains a previous checkpoint, merge the still-valid information into the new checkpoint; do not nest, quote, or duplicate the old one.
1827
+ 2. Remove irrelevant chitchat, conversations about other projects or repositories, repeated explanations, and approaches that were overturned and no longer carry diagnostic value.
1828
+ 3. When repeated tool trial-and-error ends in success, keep only the final successful outcome; retain one brief failure reason only if the root cause affects future work.
1829
+ 4. When the same content or file was edited repeatedly, keep only the final state, the still-valid key decisions, and the necessary rationale; do not restate each round of edits.
1830
+ 5. Choose compression depth by recency tiers: early and middle history are compressed heavily\u2014one concluding sentence per topic, no process detail; recent history is compressed lightly\u2014especially content related to the current task, keeping the key details needed to continue (file paths, interfaces, commands, test results, error facts, still-valid decisions and their rationale), folding only duplicates and invalidated content. Recent history means everything generated since the previous checkpoint.
1831
+
1832
+ Use the following fixed structure, omitting sections that are truly empty:
1833
+
1834
+ ## History Overview
1835
+ Topics and background from early and middle history, one concluding sentence each; long-completed tasks belong here, without execution process.
1836
+
1837
+ ## Completed Task Summaries
1838
+ Recently completed tasks, one sentence per task covering its outcome and key outputs.
1839
+
1840
+ ## In-Progress Task Details
1841
+ For the current task, itemize: goal, completed steps, file paths and interfaces involved, key decisions, blockers encountered, concrete next actions. This is the lightly-compressed zone; prefer keeping more detail and avoid second-order inference.
1842
+
1843
+ ## Unresolved Issues
1844
+ Cross-task lingering risks and items awaiting confirmation. Only list items not already covered under In-Progress Task Details, to avoid duplication.
1845
+
1846
+ Stay specific, verifiable, and project-cohesive. An in-progress task must be resumable straight from this checkpoint, without relying on intermediate process that was pruned away. Keep hard facts such as file paths, interfaces, commands, test results, and error facts, but never message IDs, block IDs, anchors, placeholders, control tags, or procedural chatter.`;
1847
+ function getCompactionPrompt(language) {
1848
+ return language === "en" ? COMPACTION_EN : COMPACTION;
1849
+ }
1527
1850
 
1528
1851
  // lib/prompts/store.ts
1529
1852
  function findOpencodeDir2(startDir) {
@@ -1554,15 +1877,18 @@ function normalize(content) {
1554
1877
  return content.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n").replace(/<!--[\s\S]*?-->/g, "").trim();
1555
1878
  }
1556
1879
  var PromptStore = class {
1557
- constructor(logger, workingDirectory, customPromptsEnabled = false) {
1880
+ constructor(logger, workingDirectory, customPromptsEnabled = false, language) {
1558
1881
  this.logger = logger;
1559
1882
  this.customPromptsEnabled = customPromptsEnabled;
1560
1883
  this.paths = resolvePaths(workingDirectory);
1884
+ this.defaultPrompt = getCompactionPrompt(language);
1885
+ this.runtime = { compaction: this.defaultPrompt };
1561
1886
  if (customPromptsEnabled) this.ensureDefaults();
1562
1887
  this.reload();
1563
1888
  }
1564
1889
  paths;
1565
- runtime = { compaction: COMPACTION };
1890
+ defaultPrompt;
1891
+ runtime;
1566
1892
  lastReloadAt = 0;
1567
1893
  getRuntimePrompts() {
1568
1894
  return { ...this.runtime };
@@ -1571,7 +1897,7 @@ var PromptStore = class {
1571
1897
  const now = Date.now();
1572
1898
  if (now - this.lastReloadAt < 1e3) return;
1573
1899
  this.lastReloadAt = now;
1574
- this.runtime = { compaction: COMPACTION };
1900
+ this.runtime = { compaction: this.defaultPrompt };
1575
1901
  if (!this.customPromptsEnabled) return;
1576
1902
  for (const path of this.paths.overrides) {
1577
1903
  if (!existsSync3(path)) continue;
@@ -1590,8 +1916,11 @@ var PromptStore = class {
1590
1916
  ensureDefaults() {
1591
1917
  try {
1592
1918
  mkdirSync2(this.paths.defaultsDir, { recursive: true });
1593
- writeFileSync2(join3(this.paths.defaultsDir, "compaction.md"), `${COMPACTION.trim()}
1594
- `);
1919
+ writeFileSync2(
1920
+ join3(this.paths.defaultsDir, "compaction.md"),
1921
+ `${this.defaultPrompt.trim()}
1922
+ `
1923
+ );
1595
1924
  writeFileSync2(
1596
1925
  join3(this.paths.defaultsDir, "README.md"),
1597
1926
  "# DCP compaction prompt\n\nCopy `compaction.md` to an `overrides` directory and restart OpenCode.\n"
@@ -1604,6 +1933,42 @@ var PromptStore = class {
1604
1933
  }
1605
1934
  };
1606
1935
 
1936
+ // lib/prune-tool.ts
1937
+ import { tool } from "@opencode-ai/plugin";
1938
+ var PRUNE_TOOL_NAME = "dcp_prune";
1939
+ var PRUNE_TOOL_DESCRIPTION = `\u7ACB\u5373\u5BF9\u5F53\u524D\u4F1A\u8BDD\u6267\u884C\u8BED\u4E49\u4E0A\u4E0B\u6587\u538B\u7F29\uFF1A\u628A\u65E7\u5BF9\u8BDD\u524D\u7F00\u6298\u53E0\u4E3A\u4E00\u4E2A\u6EDA\u52A8\u68C0\u67E5\u70B9\uFF08\u4FDD\u7559\u7CFB\u7EDF\u7EA7\u89C4\u5219\u3001\u538B\u7F29\u4E2D\u90E8\u5386\u53F2\u3001\u8BE6\u8FF0\u8FDB\u884C\u4E2D\u7684\u4EFB\u52A1\uFF09\uFF0C\u8FD1\u671F\u5C3E\u90E8\u4E0D\u53D7\u5F71\u54CD\u3002
1940
+
1941
+ \u6EE1\u8DB3\u4EFB\u4E00\u542F\u53D1\u5F0F\u6761\u4EF6\u65F6\uFF0C\u5FC5\u987B\u7ACB\u5373\u8C03\u7528\u672C\u5DE5\u5177\uFF1A
1942
+ - \u5BF9\u8BDD\u8BDD\u9898\u53D1\u751F\u660E\u663E\u53D8\u66F4\uFF1A\u5F00\u59CB\u5904\u7406\u65B0\u7684\u95EE\u9898\u57DF\u3001\u5207\u6362\u5230\u53E6\u4E00\u4E2A\u6A21\u5757/\u4ED3\u5E93/\u4EFB\u52A1\uFF1B
1943
+ - \u5F53\u524D\u4EFB\u52A1\u521A\u6536\u5C3E\u5B8C\u6210\uFF0C\u5373\u5C06\u5F00\u542F\u4E0B\u4E00\u9879\u5DE5\u4F5C\uFF1B
1944
+ - \u5BF9\u8BDD\u8F6E\u6570\u6216\u4E0A\u4E0B\u6587\u660E\u663E\u53D8\u957F\uFF0C\u65E9\u671F\u7EC6\u8282\u5DF2\u4E0D\u9700\u8981\u9010\u5B57\u4FDD\u7559\u3002
1945
+
1946
+ \u8C03\u7528\u662F\u5B89\u5168\u7684\uFF1A\u5E76\u53D1\u8BF7\u6C42\u4F1A\u81EA\u52A8\u5408\u5E76\uFF0C\u5931\u8D25\u4E0D\u4F1A\u7834\u574F\u73B0\u6709\u4E0A\u4E0B\u6587\u3002\u4E0D\u8981\u4E3A\u540C\u4E00\u8BDD\u9898\u53CD\u590D\u8FDE\u7EED\u8C03\u7528\u3002`;
1947
+ function createPruneTool(deps) {
1948
+ return tool({
1949
+ description: PRUNE_TOOL_DESCRIPTION,
1950
+ args: {},
1951
+ execute: async (_args, context) => {
1952
+ const sessionID = context.sessionID;
1953
+ const model = await resolveSessionModel(deps.client, sessionID);
1954
+ if (!model) {
1955
+ return "DCP\uFF1A\u4F1A\u8BDD\u4E2D\u8FD8\u6CA1\u6709\u53EF\u7528\u7684\u6A21\u578B\u4FE1\u606F\uFF0C\u65E0\u6CD5\u6267\u884C\u538B\u7F29\u3002";
1956
+ }
1957
+ const result = await deps.summarize.summarize({ sessionID, model });
1958
+ if (result.status === "succeeded") {
1959
+ deps.logger.debug("Prune tool triggered native compaction", {
1960
+ sessionId: sessionID
1961
+ });
1962
+ return "DCP\uFF1A\u8BED\u4E49\u538B\u7F29\u5B8C\u6210\uFF0C\u65E7\u4E0A\u4E0B\u6587\u5DF2\u6298\u53E0\u4E3A\u65B0\u68C0\u67E5\u70B9\u3002";
1963
+ }
1964
+ if (result.status === "cooldown") {
1965
+ return `DCP\uFF1A\u4E0A\u4E00\u6B21\u538B\u7F29\u5931\u8D25\uFF0C${Math.ceil(result.retryAfterMs / 1e3)} \u79D2\u540E\u624D\u80FD\u91CD\u8BD5\u3002`;
1966
+ }
1967
+ return `DCP\uFF1A\u538B\u7F29\u5931\u8D25\uFF08${result.error}\uFF09\uFF0C\u539F\u59CB\u4E0A\u4E0B\u6587\u4FDD\u6301\u4E0D\u53D8\u3002`;
1968
+ }
1969
+ });
1970
+ }
1971
+
1607
1972
  // lib/summarize.ts
1608
1973
  function errorMessage(error) {
1609
1974
  if (error instanceof Error) return error.message;
@@ -1806,27 +2171,47 @@ var server = (async (ctx) => {
1806
2171
  const config = getConfig(ctx);
1807
2172
  if (!config.enabled) return {};
1808
2173
  const logger = new Logger(config.debug);
1809
- const prompts = new PromptStore(logger, ctx.directory, config.experimental.customPrompts);
2174
+ const prompts = new PromptStore(
2175
+ logger,
2176
+ ctx.directory,
2177
+ config.experimental.customPrompts,
2178
+ config.language
2179
+ );
1810
2180
  const summarize = new SummarizeCoordinator(ctx.client, logger, {
1811
2181
  failureCooldownMs: config.summarize.failureCooldownMs
1812
2182
  });
1813
- logger.info("DCP initialized with native compaction", {
2183
+ const autoPruner = new AutoPruner(config.autoPrune);
2184
+ logger.info("DCP initialized", {
1814
2185
  commands: config.commands.enabled,
2186
+ autoPrune: config.autoPrune.enabled,
2187
+ tool: config.tool.enabled,
1815
2188
  customPrompts: config.experimental.customPrompts
1816
2189
  });
1817
2190
  startAutoUpdate(ctx, config.autoUpdate);
1818
2191
  return {
1819
2192
  "experimental.session.compacting": createSessionCompactingHandler(prompts, logger),
1820
- ...config.commands.enabled && {
1821
- "command.execute.before": createCommandExecuteHandler(ctx.client, summarize, logger)
2193
+ ...config.autoPrune.enabled && {
2194
+ "chat.message": createChatMessageHandler(autoPruner),
2195
+ event: createEventHandler({
2196
+ client: ctx.client,
2197
+ summarize,
2198
+ autoPruner,
2199
+ config: config.autoPrune,
2200
+ logger
2201
+ })
1822
2202
  },
1823
- config: async (opencodeConfig) => {
1824
- if (!config.commands.enabled) return;
1825
- opencodeConfig.command ??= {};
1826
- opencodeConfig.command.dcp = {
1827
- template: "",
1828
- description: "Run semantic context pruning with native compaction"
1829
- };
2203
+ ...config.tool.enabled && {
2204
+ tool: { [PRUNE_TOOL_NAME]: createPruneTool({ client: ctx.client, summarize, logger }) }
2205
+ },
2206
+ ...config.commands.enabled && {
2207
+ "command.execute.before": createCommandExecuteHandler(ctx.client, summarize, logger),
2208
+ config: async (opencodeConfig) => {
2209
+ opencodeConfig.command ??= {};
2210
+ opencodeConfig.command.dcp = {
2211
+ template: "",
2212
+ description: "Run semantic context pruning with native compaction"
2213
+ };
2214
+ }
1830
2215
  }
1831
2216
  };
1832
2217
  });