@lexwdex-org/opencode-dcp 3.4.11 → 3.4.12

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,6 +975,14 @@ 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",
@@ -874,7 +993,16 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
874
993
  "experimental",
875
994
  "experimental.customPrompts",
876
995
  "summarize",
877
- "summarize.failureCooldownMs"
996
+ "summarize.failureCooldownMs",
997
+ "autoPrune",
998
+ "autoPrune.enabled",
999
+ "autoPrune.minMessages",
1000
+ "autoPrune.volumeThreshold",
1001
+ "autoPrune.driftThreshold",
1002
+ "autoPrune.idleGapMs",
1003
+ "autoPrune.cooldownMs",
1004
+ "tool",
1005
+ "tool.enabled"
878
1006
  ]);
879
1007
  var DEPRECATED_CONFIG_KEYS = /* @__PURE__ */ new Set([
880
1008
  "compress",
@@ -999,6 +1127,60 @@ function validateConfigTypes(config) {
999
1127
  }
1000
1128
  }
1001
1129
  }
1130
+ const autoPrune = config.autoPrune;
1131
+ if (autoPrune !== void 0) {
1132
+ if (typeof autoPrune !== "object" || autoPrune === null || Array.isArray(autoPrune)) {
1133
+ errors.push({
1134
+ key: "autoPrune",
1135
+ expected: "object",
1136
+ actual: typeof autoPrune
1137
+ });
1138
+ } else {
1139
+ const numericKeys = [
1140
+ ["minMessages", 1, Number.POSITIVE_INFINITY],
1141
+ ["volumeThreshold", 2, Number.POSITIVE_INFINITY],
1142
+ ["driftThreshold", 0, 1],
1143
+ ["idleGapMs", 0, Number.POSITIVE_INFINITY],
1144
+ ["cooldownMs", 0, Number.POSITIVE_INFINITY]
1145
+ ];
1146
+ for (const [key, min, max] of numericKeys) {
1147
+ const value = autoPrune[key];
1148
+ if (value !== void 0 && (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max)) {
1149
+ errors.push({
1150
+ key: `autoPrune.${key}`,
1151
+ expected: `number in [${min}, ${max === Number.POSITIVE_INFINITY ? "\u221E" : max}]`,
1152
+ actual: JSON.stringify(value)
1153
+ });
1154
+ }
1155
+ }
1156
+ for (const key of ["enabled"]) {
1157
+ const value = autoPrune[key];
1158
+ if (value !== void 0 && typeof value !== "boolean") {
1159
+ errors.push({
1160
+ key: `autoPrune.${key}`,
1161
+ expected: "boolean",
1162
+ actual: typeof value
1163
+ });
1164
+ }
1165
+ }
1166
+ }
1167
+ }
1168
+ const tool2 = config.tool;
1169
+ if (tool2 !== void 0) {
1170
+ if (typeof tool2 !== "object" || tool2 === null || Array.isArray(tool2)) {
1171
+ errors.push({
1172
+ key: "tool",
1173
+ expected: "object",
1174
+ actual: typeof tool2
1175
+ });
1176
+ } else if (tool2.enabled !== void 0 && typeof tool2.enabled !== "boolean") {
1177
+ errors.push({
1178
+ key: "tool.enabled",
1179
+ expected: "boolean",
1180
+ actual: typeof tool2.enabled
1181
+ });
1182
+ }
1183
+ }
1002
1184
  return errors;
1003
1185
  }
1004
1186
  function showConfigWarnings(ctx, configPath, configData, isProject) {
@@ -1057,6 +1239,10 @@ var defaultConfig = {
1057
1239
  },
1058
1240
  summarize: {
1059
1241
  failureCooldownMs: DEFAULT_FAILURE_COOLDOWN_MS
1242
+ },
1243
+ autoPrune: { ...DEFAULT_AUTO_PRUNE },
1244
+ tool: {
1245
+ enabled: true
1060
1246
  }
1061
1247
  };
1062
1248
  var GLOBAL_CONFIG_DIR = process.env.XDG_CONFIG_HOME ? join(process.env.XDG_CONFIG_HOME, "opencode") : join(homedir(), ".config", "opencode");
@@ -1148,12 +1334,36 @@ function mergeSummarize(base, override) {
1148
1334
  failureCooldownMs: typeof override.failureCooldownMs === "number" && Number.isFinite(override.failureCooldownMs) && override.failureCooldownMs >= 0 ? override.failureCooldownMs : base.failureCooldownMs
1149
1335
  };
1150
1336
  }
1337
+ function mergeAutoPrune(base, override) {
1338
+ if (!override || typeof override !== "object" || Array.isArray(override)) {
1339
+ return base;
1340
+ }
1341
+ 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];
1342
+ return {
1343
+ enabled: typeof override.enabled === "boolean" ? override.enabled : base.enabled,
1344
+ minMessages: number("minMessages", 1),
1345
+ volumeThreshold: number("volumeThreshold", 2),
1346
+ driftThreshold: number("driftThreshold", 0, 1),
1347
+ idleGapMs: number("idleGapMs", 0),
1348
+ cooldownMs: number("cooldownMs", 0)
1349
+ };
1350
+ }
1351
+ function mergeTool(base, override) {
1352
+ if (!override) {
1353
+ return base;
1354
+ }
1355
+ return {
1356
+ enabled: typeof override.enabled === "boolean" ? override.enabled : base.enabled
1357
+ };
1358
+ }
1151
1359
  function deepCloneConfig(config) {
1152
1360
  return {
1153
1361
  ...config,
1154
1362
  commands: { ...config.commands },
1155
1363
  experimental: { ...config.experimental },
1156
- summarize: { ...config.summarize }
1364
+ summarize: { ...config.summarize },
1365
+ autoPrune: { ...config.autoPrune },
1366
+ tool: { ...config.tool }
1157
1367
  };
1158
1368
  }
1159
1369
  function mergeLayer(config, data) {
@@ -1163,7 +1373,9 @@ function mergeLayer(config, data) {
1163
1373
  debug: typeof data.debug === "boolean" ? data.debug : config.debug,
1164
1374
  commands: mergeCommands(config.commands, data.commands),
1165
1375
  experimental: mergeExperimental(config.experimental, data.experimental),
1166
- summarize: mergeSummarize(config.summarize, data.summarize)
1376
+ summarize: mergeSummarize(config.summarize, data.summarize),
1377
+ autoPrune: mergeAutoPrune(config.autoPrune, data.autoPrune),
1378
+ tool: mergeTool(config.tool, data.tool)
1167
1379
  };
1168
1380
  }
1169
1381
  function scheduleParseWarning(ctx, title, message) {
@@ -1216,6 +1428,29 @@ Using previous/default values`
1216
1428
  return config;
1217
1429
  }
1218
1430
 
1431
+ // lib/session-model.ts
1432
+ function latestUserModel(messages) {
1433
+ if (!Array.isArray(messages)) return null;
1434
+ for (let index = messages.length - 1; index >= 0; index--) {
1435
+ const info = messages[index]?.info;
1436
+ if (info?.role !== "user") continue;
1437
+ const providerID = info.model?.providerID;
1438
+ const modelID = info.model?.modelID;
1439
+ if (typeof providerID === "string" && typeof modelID === "string") {
1440
+ return { providerID, modelID };
1441
+ }
1442
+ }
1443
+ return null;
1444
+ }
1445
+ async function resolveSessionModel(client, sessionID) {
1446
+ try {
1447
+ const response = await client.session.messages({ path: { id: sessionID } });
1448
+ return latestUserModel(response.data ?? response);
1449
+ } catch {
1450
+ return null;
1451
+ }
1452
+ }
1453
+
1219
1454
  // lib/hooks.ts
1220
1455
  function createSessionCompactingHandler(prompts, logger) {
1221
1456
  return async (input, output) => {
@@ -1236,24 +1471,71 @@ function createSessionCompactingHandler(prompts, logger) {
1236
1471
  }
1237
1472
  };
1238
1473
  }
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
1474
  async function showToast(client, title, message, variant = "info") {
1253
1475
  await client.tui.showToast({
1254
1476
  body: { title, message, variant, duration: 5e3 }
1255
1477
  }).catch(() => void 0);
1256
1478
  }
1479
+ function createChatMessageHandler(autoPruner) {
1480
+ return async (input, _output) => {
1481
+ const parts = _output?.parts ?? [];
1482
+ autoPruner.observeUserMessage(input.sessionID, parts);
1483
+ };
1484
+ }
1485
+ var SIGNAL_LABELS = {
1486
+ "topic-drift": "\u8BDD\u9898\u53D8\u66F4",
1487
+ volume: "\u6D88\u606F\u91CF\u8FBE\u5230\u9608\u503C",
1488
+ "idle-gap": "\u957F\u65F6\u95F4\u4E2D\u65AD\u540E\u6062\u590D"
1489
+ };
1490
+ function createEventHandler(deps) {
1491
+ async function triggerAutoPrune(sessionID, signals) {
1492
+ const reason = signals.map((signal) => SIGNAL_LABELS[signal]).join("\u3001");
1493
+ const model = await resolveSessionModel(deps.client, sessionID);
1494
+ if (!model) {
1495
+ deps.logger.debug("Auto prune skipped; no session model yet", { sessionId: sessionID });
1496
+ return;
1497
+ }
1498
+ const result = await deps.summarize.summarize({ sessionID, model });
1499
+ deps.autoPruner.markPruned(sessionID);
1500
+ if (result.status === "succeeded") {
1501
+ 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`);
1502
+ } else {
1503
+ await showToast(
1504
+ deps.client,
1505
+ "DCP \u81EA\u52A8\u538B\u7F29",
1506
+ `\u68C0\u6D4B\u5230${reason}\uFF0C\u4F46\u538B\u7F29\u5931\u8D25\uFF1B\u539F\u59CB\u4E0A\u4E0B\u6587\u4FDD\u6301\u4E0D\u53D8\u3002`,
1507
+ "warning"
1508
+ );
1509
+ }
1510
+ deps.logger.debug("Auto prune finished", { sessionId: sessionID, status: result.status });
1511
+ }
1512
+ return async (input) => {
1513
+ const event = input.event;
1514
+ const sessionID = event.properties?.sessionID;
1515
+ if (typeof sessionID !== "string" || !sessionID) return;
1516
+ try {
1517
+ if (event.type === "session.idle") {
1518
+ if (!deps.config.enabled) return;
1519
+ const signals = deps.autoPruner.consumePending(sessionID);
1520
+ if (signals) await triggerAutoPrune(sessionID, signals);
1521
+ return;
1522
+ }
1523
+ if (event.type === "session.compacted") {
1524
+ deps.autoPruner.markPruned(sessionID);
1525
+ return;
1526
+ }
1527
+ if (event.type === "session.deleted") {
1528
+ deps.autoPruner.dropSession(sessionID);
1529
+ }
1530
+ } catch (error) {
1531
+ deps.logger.warn("Event handler failed", {
1532
+ type: event.type,
1533
+ sessionId: sessionID,
1534
+ error: error instanceof Error ? error.message : String(error)
1535
+ });
1536
+ }
1537
+ };
1538
+ }
1257
1539
  function createCommandExecuteHandler(client, summarize, logger) {
1258
1540
  return async (input, _output) => {
1259
1541
  if (input.command !== "dcp") return;
@@ -1266,8 +1548,7 @@ function createCommandExecuteHandler(client, summarize, logger) {
1266
1548
  );
1267
1549
  throw new Error("__DCP_HELP_HANDLED__");
1268
1550
  }
1269
- const response = await client.session.messages({ path: { id: input.sessionID } });
1270
- const model = latestUserModel(response.data ?? response);
1551
+ const model = await resolveSessionModel(client, input.sessionID);
1271
1552
  if (!model) {
1272
1553
  await showToast(
1273
1554
  client,
@@ -1505,25 +1786,32 @@ import { dirname as dirname2, join as join3 } from "path";
1505
1786
  // lib/prompts/compaction.ts
1506
1787
  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
1507
1788
 
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
1789
+ \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
1790
 
1510
1791
  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
1792
  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
1793
  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
1794
  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
1795
+ 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
1515
1796
 
1516
1797
  \u4F7F\u7528\u4EE5\u4E0B\u56FA\u5B9A\u7ED3\u6784\uFF0C\u7701\u7565\u786E\u5B9E\u4E3A\u7A7A\u7684\u6761\u76EE\uFF1A
1517
1798
 
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
1799
+ ## \u7CFB\u7EDF\u4E0A\u4E0B\u6587
1800
+ AGENTS.md\u3001\u9879\u76EE\u89C4\u5219\u3001\u7528\u6237\u5168\u5C40\u7EA6\u5B9A\u7B49\u7CFB\u7EDF\u7EA7\u8981\u6C42\u3002\u4E0A\u4E00\u4EFD\u68C0\u67E5\u70B9\u7684\u6B64\u8282\u5185\u5BB9\u539F\u6837\u4FDD\u7559\u5408\u5E76\uFF1B\u4EC5\u5F53\u4F1A\u8BDD\u4E2D\u786E\u7ACB\u4E86\u65B0\u7684\u7CFB\u7EDF\u7EA7\u7EA6\u5B9A\u65F6\u624D\u8FFD\u52A0\uFF0C\u4E0D\u5199\u666E\u901A\u4EFB\u52A1\u5185\u5BB9\u3002
1801
+
1802
+ ## \u5386\u53F2\u6982\u8981
1803
+ \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
1804
+
1805
+ ## \u5DF2\u5B8C\u6210\u4EFB\u52A1\u7684\u6982\u62EC
1806
+ \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
1807
+
1808
+ ## \u8FDB\u884C\u4E2D\u4EFB\u52A1\u8BE6\u60C5
1809
+ \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
1810
+
1523
1811
  ## \u672A\u89E3\u51B3\u95EE\u9898
1524
- ## \u4E0B\u4E00\u6B65
1812
+ \u8DE8\u4EFB\u52A1\u7684\u9057\u7559\u98CE\u9669\u548C\u5F85\u786E\u8BA4\u4E8B\u9879\u3002
1525
1813
 
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`;
1814
+ \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`;
1527
1815
 
1528
1816
  // lib/prompts/store.ts
1529
1817
  function findOpencodeDir2(startDir) {
@@ -1604,6 +1892,42 @@ var PromptStore = class {
1604
1892
  }
1605
1893
  };
1606
1894
 
1895
+ // lib/prune-tool.ts
1896
+ import { tool } from "@opencode-ai/plugin";
1897
+ var PRUNE_TOOL_NAME = "dcp_prune";
1898
+ 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
1899
+
1900
+ \u6EE1\u8DB3\u4EFB\u4E00\u542F\u53D1\u5F0F\u6761\u4EF6\u65F6\uFF0C\u5FC5\u987B\u7ACB\u5373\u8C03\u7528\u672C\u5DE5\u5177\uFF1A
1901
+ - \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
1902
+ - \u5F53\u524D\u4EFB\u52A1\u521A\u6536\u5C3E\u5B8C\u6210\uFF0C\u5373\u5C06\u5F00\u542F\u4E0B\u4E00\u9879\u5DE5\u4F5C\uFF1B
1903
+ - \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
1904
+
1905
+ \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`;
1906
+ function createPruneTool(deps) {
1907
+ return tool({
1908
+ description: PRUNE_TOOL_DESCRIPTION,
1909
+ args: {},
1910
+ execute: async (_args, context) => {
1911
+ const sessionID = context.sessionID;
1912
+ const model = await resolveSessionModel(deps.client, sessionID);
1913
+ if (!model) {
1914
+ return "DCP\uFF1A\u4F1A\u8BDD\u4E2D\u8FD8\u6CA1\u6709\u53EF\u7528\u7684\u6A21\u578B\u4FE1\u606F\uFF0C\u65E0\u6CD5\u6267\u884C\u538B\u7F29\u3002";
1915
+ }
1916
+ const result = await deps.summarize.summarize({ sessionID, model });
1917
+ if (result.status === "succeeded") {
1918
+ deps.logger.debug("Prune tool triggered native compaction", {
1919
+ sessionId: sessionID
1920
+ });
1921
+ return "DCP\uFF1A\u8BED\u4E49\u538B\u7F29\u5B8C\u6210\uFF0C\u65E7\u4E0A\u4E0B\u6587\u5DF2\u6298\u53E0\u4E3A\u65B0\u68C0\u67E5\u70B9\u3002";
1922
+ }
1923
+ if (result.status === "cooldown") {
1924
+ return `DCP\uFF1A\u4E0A\u4E00\u6B21\u538B\u7F29\u5931\u8D25\uFF0C${Math.ceil(result.retryAfterMs / 1e3)} \u79D2\u540E\u624D\u80FD\u91CD\u8BD5\u3002`;
1925
+ }
1926
+ return `DCP\uFF1A\u538B\u7F29\u5931\u8D25\uFF08${result.error}\uFF09\uFF0C\u539F\u59CB\u4E0A\u4E0B\u6587\u4FDD\u6301\u4E0D\u53D8\u3002`;
1927
+ }
1928
+ });
1929
+ }
1930
+
1607
1931
  // lib/summarize.ts
1608
1932
  function errorMessage(error) {
1609
1933
  if (error instanceof Error) return error.message;
@@ -1810,23 +2134,38 @@ var server = (async (ctx) => {
1810
2134
  const summarize = new SummarizeCoordinator(ctx.client, logger, {
1811
2135
  failureCooldownMs: config.summarize.failureCooldownMs
1812
2136
  });
1813
- logger.info("DCP initialized with native compaction", {
2137
+ const autoPruner = new AutoPruner(config.autoPrune);
2138
+ logger.info("DCP initialized", {
1814
2139
  commands: config.commands.enabled,
2140
+ autoPrune: config.autoPrune.enabled,
2141
+ tool: config.tool.enabled,
1815
2142
  customPrompts: config.experimental.customPrompts
1816
2143
  });
1817
2144
  startAutoUpdate(ctx, config.autoUpdate);
1818
2145
  return {
1819
2146
  "experimental.session.compacting": createSessionCompactingHandler(prompts, logger),
1820
- ...config.commands.enabled && {
1821
- "command.execute.before": createCommandExecuteHandler(ctx.client, summarize, logger)
2147
+ ...config.autoPrune.enabled && {
2148
+ "chat.message": createChatMessageHandler(autoPruner),
2149
+ event: createEventHandler({
2150
+ client: ctx.client,
2151
+ summarize,
2152
+ autoPruner,
2153
+ config: config.autoPrune,
2154
+ logger
2155
+ })
1822
2156
  },
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
- };
2157
+ ...config.tool.enabled && {
2158
+ tool: { [PRUNE_TOOL_NAME]: createPruneTool({ client: ctx.client, summarize, logger }) }
2159
+ },
2160
+ ...config.commands.enabled && {
2161
+ "command.execute.before": createCommandExecuteHandler(ctx.client, summarize, logger),
2162
+ config: async (opencodeConfig) => {
2163
+ opencodeConfig.command ??= {};
2164
+ opencodeConfig.command.dcp = {
2165
+ template: "",
2166
+ description: "Run semantic context pruning with native compaction"
2167
+ };
2168
+ }
1830
2169
  }
1831
2170
  };
1832
2171
  });