@lexwdex-org/opencode-dcp 3.4.12 → 3.4.14

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/README.en.md CHANGED
@@ -54,6 +54,7 @@ DCP loads global, custom-config-directory, then project configuration:
54
54
  "enabled": true,
55
55
  "autoUpdate": true,
56
56
  "debug": false,
57
+ "language": "zh",
57
58
  "commands": { "enabled": true },
58
59
  "experimental": { "customPrompts": false },
59
60
  "summarize": { "failureCooldownMs": 30000 },
@@ -69,15 +70,16 @@ DCP loads global, custom-config-directory, then project configuration:
69
70
  }
70
71
  ```
71
72
 
72
- | Key | Meaning |
73
- | --------------------------- | --------------------------------------------------------------------------------- |
74
- | `tool.enabled` | Register the model-invokable `dcp_prune` tool with heuristic usage guidance |
75
- | `autoPrune.enabled` | Enable plugin-side heuristic auto-compaction |
76
- | `autoPrune.minMessages` | Minimum user messages before any auto-prune signal is considered |
77
- | `autoPrune.volumeThreshold` | User messages since the last prune that trigger compaction by volume |
78
- | `autoPrune.driftThreshold` | Jaccard similarity below which consecutive messages count as a topic change (0–1) |
79
- | `autoPrune.idleGapMs` | Gap between user messages that counts as resuming after a long break |
80
- | `autoPrune.cooldownMs` | Minimum interval between two automatic prunes of the same session |
73
+ | Key | Meaning |
74
+ | --------------------------- | --------------------------------------------------------------------------------------------- |
75
+ | `language` | Language of the bundled compaction prompt: `zh` (default) / `en`; custom overrides always win |
76
+ | `tool.enabled` | Register the model-invokable `dcp_prune` tool with heuristic usage guidance |
77
+ | `autoPrune.enabled` | Enable plugin-side heuristic auto-compaction |
78
+ | `autoPrune.minMessages` | Minimum user messages before any auto-prune signal is considered |
79
+ | `autoPrune.volumeThreshold` | User messages since the last prune that trigger compaction by volume |
80
+ | `autoPrune.driftThreshold` | Jaccard similarity below which consecutive messages count as a topic change (0–1) |
81
+ | `autoPrune.idleGapMs` | Gap between user messages that counts as resuming after a long break |
82
+ | `autoPrune.cooldownMs` | Minimum interval between two automatic prunes of the same session |
81
83
 
82
84
  When `experimental.customPrompts` is enabled, copy the generated
83
85
  `~/.config/opencode/dcp-prompts/defaults/compaction.md` to a project, custom-config, or global `dcp-prompts/overrides/compaction.md` path.
package/README.md CHANGED
@@ -56,6 +56,7 @@ DCP 依次读取以下配置,后面的层覆盖前面的层:
56
56
  "enabled": true,
57
57
  "autoUpdate": true,
58
58
  "debug": false,
59
+ "language": "zh",
59
60
  "commands": {
60
61
  "enabled": true,
61
62
  },
@@ -81,6 +82,7 @@ DCP 依次读取以下配置,后面的层覆盖前面的层:
81
82
 
82
83
  | 键 | 说明 |
83
84
  | --------------------------- | ------------------------------------------------------------------------- |
85
+ | `language` | 内置压缩提示词语言:`zh`(默认)/ `en`;自定义覆盖文件优先于该配置 |
84
86
  | `tool.enabled` | 注册模型可调用的 `dcp_prune` 工具(描述含"话题变更立即使用"的启发式指令) |
85
87
  | `autoPrune.enabled` | 启用插件侧启发式自动压缩 |
86
88
  | `autoPrune.minMessages` | 会话用户消息数达到该值前不做任何自动判定 |
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAejD,QAAA,MAAM,MAAM,EAAE,MA6CK,CAAA;AAEnB,eAAe,MAAM,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAiBjD,QAAA,MAAM,MAAM,EAAE,MA4DK,CAAA;AAEnB,eAAe,MAAM,CAAA"}
package/dist/index.js CHANGED
@@ -1,3 +1,42 @@
1
+ // lib/activity.ts
2
+ var BUSY_TTL_MS = 10 * 6e4;
3
+ var MAX_TRACKED_SESSIONS = 500;
4
+ var SessionActivityTracker = class {
5
+ sessions = /* @__PURE__ */ new Map();
6
+ now;
7
+ constructor(now) {
8
+ this.now = now ?? Date.now;
9
+ }
10
+ observe(type, properties) {
11
+ const sessionID = properties?.sessionID;
12
+ if (typeof sessionID !== "string" || !sessionID) return;
13
+ if (type === "session.status") {
14
+ const status = properties?.status?.type;
15
+ if (status === "busy" || status === "retry") this.set(sessionID, "busy");
16
+ else if (status === "idle") this.set(sessionID, "idle");
17
+ return;
18
+ }
19
+ if (type === "session.idle") this.set(sessionID, "idle");
20
+ }
21
+ state(sessionID) {
22
+ const activity = this.sessions.get(sessionID);
23
+ if (!activity) return "unknown";
24
+ if (activity.state === "busy" && this.now() - activity.at > BUSY_TTL_MS) return "unknown";
25
+ return activity.state;
26
+ }
27
+ dropSession(sessionID) {
28
+ this.sessions.delete(sessionID);
29
+ }
30
+ set(sessionID, state) {
31
+ if (this.sessions.has(sessionID)) this.sessions.delete(sessionID);
32
+ this.sessions.set(sessionID, { state, at: this.now() });
33
+ if (this.sessions.size > MAX_TRACKED_SESSIONS) {
34
+ const oldest = this.sessions.keys().next().value;
35
+ if (oldest !== void 0) this.sessions.delete(oldest);
36
+ }
37
+ }
38
+ };
39
+
1
40
  // lib/auto-prune.ts
2
41
  function tokenize(text) {
3
42
  const tokens = /* @__PURE__ */ new Set();
@@ -27,6 +66,7 @@ function jaccard(a, b) {
27
66
  }
28
67
  var WINDOW_SIZE = 4;
29
68
  var DRIFT_BASELINE = 3;
69
+ var MIN_DRIFT_TOKENS = 6;
30
70
  function extractText(parts) {
31
71
  const texts = [];
32
72
  for (const part of parts) {
@@ -86,11 +126,13 @@ var AutoPruner = class {
86
126
  }
87
127
  if (state.count >= DRIFT_BASELINE && text) {
88
128
  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])));
129
+ if (current.size >= MIN_DRIFT_TOKENS) {
130
+ let max = 0;
131
+ for (let index = Math.max(0, state.window.length - DRIFT_BASELINE); index < state.window.length; index++) {
132
+ max = Math.max(max, jaccard(current, tokenize(state.window[index])));
133
+ }
134
+ if (max < this.config.driftThreshold) signals.push("topic-drift");
92
135
  }
93
- if (max < this.config.driftThreshold) signals.push("topic-drift");
94
136
  }
95
137
  if (state.count + 1 >= this.config.volumeThreshold) signals.push("volume");
96
138
  return signals;
@@ -988,6 +1030,7 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
988
1030
  "enabled",
989
1031
  "autoUpdate",
990
1032
  "debug",
1033
+ "language",
991
1034
  "commands",
992
1035
  "commands.enabled",
993
1036
  "experimental",
@@ -1077,6 +1120,13 @@ function validateConfigTypes(config) {
1077
1120
  if (config.debug !== void 0 && typeof config.debug !== "boolean") {
1078
1121
  errors.push({ key: "debug", expected: "boolean", actual: typeof config.debug });
1079
1122
  }
1123
+ if (config.language !== void 0 && config.language !== "zh" && config.language !== "en") {
1124
+ errors.push({
1125
+ key: "language",
1126
+ expected: '"zh" or "en"',
1127
+ actual: JSON.stringify(config.language)
1128
+ });
1129
+ }
1080
1130
  const commands = config.commands;
1081
1131
  if (commands !== void 0) {
1082
1132
  if (typeof commands !== "object" || commands === null || Array.isArray(commands)) {
@@ -1231,6 +1281,7 @@ var defaultConfig = {
1231
1281
  enabled: true,
1232
1282
  autoUpdate: true,
1233
1283
  debug: false,
1284
+ language: "zh",
1234
1285
  commands: {
1235
1286
  enabled: true
1236
1287
  },
@@ -1371,6 +1422,7 @@ function mergeLayer(config, data) {
1371
1422
  enabled: typeof data.enabled === "boolean" ? data.enabled : config.enabled,
1372
1423
  autoUpdate: typeof data.autoUpdate === "boolean" ? data.autoUpdate : config.autoUpdate,
1373
1424
  debug: typeof data.debug === "boolean" ? data.debug : config.debug,
1425
+ language: data.language === "zh" || data.language === "en" ? data.language : config.language,
1374
1426
  commands: mergeCommands(config.commands, data.commands),
1375
1427
  experimental: mergeExperimental(config.experimental, data.experimental),
1376
1428
  summarize: mergeSummarize(config.summarize, data.summarize),
@@ -1451,6 +1503,142 @@ async function resolveSessionModel(client, sessionID) {
1451
1503
  }
1452
1504
  }
1453
1505
 
1506
+ // lib/prune-service.ts
1507
+ function retrySeconds(retryAfterMs) {
1508
+ return Math.ceil(retryAfterMs / 1e3);
1509
+ }
1510
+ function eventSessionID(properties) {
1511
+ const direct = properties?.sessionID;
1512
+ if (typeof direct === "string" && direct) return direct;
1513
+ const info = properties?.info?.id;
1514
+ if (typeof info === "string" && info) return info;
1515
+ return void 0;
1516
+ }
1517
+ var PruneService = class {
1518
+ deferred = /* @__PURE__ */ new Set();
1519
+ client;
1520
+ summarize;
1521
+ activity;
1522
+ logger;
1523
+ constructor(deps) {
1524
+ this.client = deps.client;
1525
+ this.summarize = deps.summarize;
1526
+ this.activity = deps.activity;
1527
+ this.logger = deps.logger;
1528
+ }
1529
+ /**
1530
+ * Feed every host event through here. Drains the deferral queue on
1531
+ * `session.idle` and forgets queued prunes once a compaction (or the
1532
+ * session itself) is gone.
1533
+ */
1534
+ observeEvent(type, properties) {
1535
+ this.activity.observe(type, properties);
1536
+ const sessionID = eventSessionID(properties);
1537
+ if (!sessionID) return;
1538
+ if (type === "session.idle") {
1539
+ if (!this.deferred.delete(sessionID)) return;
1540
+ void this.drainQueued(sessionID);
1541
+ return;
1542
+ }
1543
+ if (type === "session.compacted") {
1544
+ this.deferred.delete(sessionID);
1545
+ return;
1546
+ }
1547
+ if (type === "session.deleted") {
1548
+ this.deferred.delete(sessionID);
1549
+ this.activity.dropSession(sessionID);
1550
+ }
1551
+ }
1552
+ async request(request) {
1553
+ const beforeModel = await this.gate(request);
1554
+ if (beforeModel) return beforeModel;
1555
+ const model = await resolveSessionModel(this.client, request.sessionID);
1556
+ if (!model) return { status: "no-model" };
1557
+ const afterModel = await this.gate(request);
1558
+ if (afterModel) return afterModel;
1559
+ const serverBusy = await this.isBusyOnServer(request.sessionID);
1560
+ if (serverBusy === true) {
1561
+ const fallback = await this.outcomeFor(request, { action: "stand-down" });
1562
+ if (fallback) return fallback;
1563
+ }
1564
+ const result = await this.summarize.summarize({ sessionID: request.sessionID, model });
1565
+ if (result.status === "rejected") {
1566
+ return { status: "busy" };
1567
+ }
1568
+ return result;
1569
+ }
1570
+ /** One admission check; `null` means the request may proceed. */
1571
+ gate(request) {
1572
+ return this.outcomeFor(request, this.admit(request));
1573
+ }
1574
+ /**
1575
+ * Executes a queued prune at the idle boundary. The tool already promised
1576
+ * its caller this prune would run; losing the busy race must not silently
1577
+ * break that promise, so a busy outcome re-queues for the next idle
1578
+ * boundary. Every other outcome is terminal: it is logged and never
1579
+ * retried — new prune demand arrives through new triggers only.
1580
+ * Execution is re-guarded on every attempt, so re-queueing never violates
1581
+ * the never-interrupt invariant.
1582
+ */
1583
+ async drainQueued(sessionID) {
1584
+ let outcome;
1585
+ try {
1586
+ outcome = await this.request({ sessionID, onBusy: "proceed" });
1587
+ } catch (error) {
1588
+ this.logger.warn("Queued prune drain failed; the prune was not retried", {
1589
+ sessionId: sessionID,
1590
+ error: error instanceof Error ? error.message : String(error)
1591
+ });
1592
+ return;
1593
+ }
1594
+ if (outcome.status === "busy") {
1595
+ this.deferred.add(sessionID);
1596
+ return;
1597
+ }
1598
+ this.logger.debug("Queued prune drain finished", {
1599
+ sessionId: sessionID,
1600
+ status: outcome.status
1601
+ });
1602
+ }
1603
+ /**
1604
+ * Turns an admission decision into a terminal outcome, or `null` when the
1605
+ * request may proceed. Deferral additionally enqueues the session.
1606
+ */
1607
+ async outcomeFor(request, admission) {
1608
+ if (admission.action === "go") return null;
1609
+ if (admission.action === "stand-down") return { status: "busy" };
1610
+ this.deferred.add(request.sessionID);
1611
+ this.logger.debug("Prune deferred to the next session idle boundary", {
1612
+ sessionId: request.sessionID
1613
+ });
1614
+ return { status: "deferred" };
1615
+ }
1616
+ admit(request) {
1617
+ if (request.onBusy === "defer") return { action: "defer" };
1618
+ if (this.activity.state(request.sessionID) === "busy") return { action: "stand-down" };
1619
+ return { action: "go" };
1620
+ }
1621
+ /**
1622
+ * Queries the host's live session status. Returns `null` when the answer
1623
+ * is unknown (endpoint or SDK method missing, request failed) — callers
1624
+ * must fail open in that case.
1625
+ */
1626
+ async isBusyOnServer(sessionID) {
1627
+ try {
1628
+ const statusFn = this.client.session.status;
1629
+ if (typeof statusFn !== "function") return null;
1630
+ const response = await statusFn.call(
1631
+ this.client.session
1632
+ );
1633
+ const map = response?.data ?? response;
1634
+ const info = map?.[sessionID];
1635
+ return info?.type === "busy" || info?.type === "retry";
1636
+ } catch {
1637
+ return null;
1638
+ }
1639
+ }
1640
+ };
1641
+
1454
1642
  // lib/hooks.ts
1455
1643
  function createSessionCompactingHandler(prompts, logger) {
1456
1644
  return async (input, output) => {
@@ -1490,15 +1678,33 @@ var SIGNAL_LABELS = {
1490
1678
  function createEventHandler(deps) {
1491
1679
  async function triggerAutoPrune(sessionID, signals) {
1492
1680
  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 });
1681
+ const result = await deps.prune.request({ sessionID, onBusy: "proceed" });
1682
+ const attempted = result.status === "succeeded" || result.status === "failed" || result.status === "cooldown";
1683
+ if (!attempted) {
1684
+ if (result.status === "busy") {
1685
+ await showToast(
1686
+ deps.client,
1687
+ "DCP \u81EA\u52A8\u538B\u7F29",
1688
+ `\u68C0\u6D4B\u5230${reason}\uFF0C\u4F46\u4F1A\u8BDD\u6B63\u5FD9\uFF0C\u672C\u6B21\u5DF2\u8DF3\u8FC7\uFF1B\u539F\u59CB\u4E0A\u4E0B\u6587\u4FDD\u6301\u4E0D\u53D8\u3002`,
1689
+ "warning"
1690
+ );
1691
+ }
1692
+ deps.logger.debug("Auto prune skipped", {
1693
+ sessionId: sessionID,
1694
+ status: result.status
1695
+ });
1496
1696
  return;
1497
1697
  }
1498
- const result = await deps.summarize.summarize({ sessionID, model });
1499
1698
  deps.autoPruner.markPruned(sessionID);
1500
1699
  if (result.status === "succeeded") {
1501
1700
  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`);
1701
+ } else if (result.status === "cooldown") {
1702
+ await showToast(
1703
+ deps.client,
1704
+ "DCP \u81EA\u52A8\u538B\u7F29",
1705
+ `\u68C0\u6D4B\u5230${reason}\uFF0C\u4F46\u4E0A\u4E00\u6B21\u538B\u7F29\u5931\u8D25\uFF1B${retrySeconds(result.retryAfterMs)} \u79D2\u540E\u53EF\u91CD\u8BD5\u3002`,
1706
+ "warning"
1707
+ );
1502
1708
  } else {
1503
1709
  await showToast(
1504
1710
  deps.client,
@@ -1511,9 +1717,10 @@ function createEventHandler(deps) {
1511
1717
  }
1512
1718
  return async (input) => {
1513
1719
  const event = input.event;
1514
- const sessionID = event.properties?.sessionID;
1515
- if (typeof sessionID !== "string" || !sessionID) return;
1720
+ const sessionID = eventSessionID(event.properties);
1721
+ if (!sessionID) return;
1516
1722
  try {
1723
+ deps.prune.observeEvent(event.type, event.properties);
1517
1724
  if (event.type === "session.idle") {
1518
1725
  if (!deps.config.enabled) return;
1519
1726
  const signals = deps.autoPruner.consumePending(sessionID);
@@ -1536,7 +1743,7 @@ function createEventHandler(deps) {
1536
1743
  }
1537
1744
  };
1538
1745
  }
1539
- function createCommandExecuteHandler(client, summarize, logger) {
1746
+ function createCommandExecuteHandler(client, prune, logger) {
1540
1747
  return async (input, _output) => {
1541
1748
  if (input.command !== "dcp") return;
1542
1749
  const subcommand = (input.arguments ?? "").trim().split(/\s+/, 1)[0]?.toLowerCase();
@@ -1548,8 +1755,17 @@ function createCommandExecuteHandler(client, summarize, logger) {
1548
1755
  );
1549
1756
  throw new Error("__DCP_HELP_HANDLED__");
1550
1757
  }
1551
- const model = await resolveSessionModel(client, input.sessionID);
1552
- if (!model) {
1758
+ const result = await prune.request({ sessionID: input.sessionID, onBusy: "proceed" });
1759
+ if (result.status === "busy") {
1760
+ await showToast(
1761
+ client,
1762
+ "DCP summarize",
1763
+ "Session is busy; the prune will not interrupt the current turn. Try again once it finishes.",
1764
+ "warning"
1765
+ );
1766
+ throw new Error("__DCP_SUMMARIZE_HANDLED__");
1767
+ }
1768
+ if (result.status === "no-model") {
1553
1769
  await showToast(
1554
1770
  client,
1555
1771
  "DCP summarize",
@@ -1558,14 +1774,13 @@ function createCommandExecuteHandler(client, summarize, logger) {
1558
1774
  );
1559
1775
  throw new Error("__DCP_SUMMARIZE_NO_MODEL__");
1560
1776
  }
1561
- const result = await summarize.summarize({ sessionID: input.sessionID, model });
1562
1777
  if (result.status === "succeeded") {
1563
1778
  await showToast(client, "DCP summarize", "Semantic pruning checkpoint created.");
1564
1779
  } else if (result.status === "cooldown") {
1565
1780
  await showToast(
1566
1781
  client,
1567
1782
  "DCP summarize",
1568
- `Previous attempt failed; retry in ${Math.ceil(result.retryAfterMs / 1e3)}s.`,
1783
+ `Previous attempt failed; retry in ${retrySeconds(result.retryAfterMs)}s.`,
1569
1784
  "warning"
1570
1785
  );
1571
1786
  } else {
@@ -1784,7 +1999,7 @@ import { homedir as homedir3 } from "os";
1784
1999
  import { dirname as dirname2, join as join3 } from "path";
1785
2000
 
1786
2001
  // lib/prompts/compaction.ts
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
2002
+ 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
1788
2003
 
1789
2004
  \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
1790
2005
 
@@ -1792,13 +2007,10 @@ var COMPACTION = `\u4F60\u6B63\u5728\u751F\u6210\u5F53\u524D\u4F1A\u8BDD\u552F\u
1792
2007
  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
1793
2008
  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
1794
2009
  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
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
2010
+ 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
1796
2011
 
1797
2012
  \u4F7F\u7528\u4EE5\u4E0B\u56FA\u5B9A\u7ED3\u6784\uFF0C\u7701\u7565\u786E\u5B9E\u4E3A\u7A7A\u7684\u6761\u76EE\uFF1A
1798
2013
 
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
2014
  ## \u5386\u53F2\u6982\u8981
1803
2015
  \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
2016
 
@@ -1809,9 +2021,37 @@ AGENTS.md\u3001\u9879\u76EE\u89C4\u5219\u3001\u7528\u6237\u5168\u5C40\u7EA6\u5B9
1809
2021
  \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
2022
 
1811
2023
  ## \u672A\u89E3\u51B3\u95EE\u9898
1812
- \u8DE8\u4EFB\u52A1\u7684\u9057\u7559\u98CE\u9669\u548C\u5F85\u786E\u8BA4\u4E8B\u9879\u3002
2024
+ \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
1813
2025
 
1814
2026
  \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`;
2027
+ 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.
2028
+
2029
+ This is not a chat-log summary but a semantic pruning result one can resume working from directly. Compress by these rules:
2030
+
2031
+ 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.
2032
+ 2. Remove irrelevant chitchat, conversations about other projects or repositories, repeated explanations, and approaches that were overturned and no longer carry diagnostic value.
2033
+ 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.
2034
+ 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.
2035
+ 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.
2036
+
2037
+ Use the following fixed structure, omitting sections that are truly empty:
2038
+
2039
+ ## History Overview
2040
+ Topics and background from early and middle history, one concluding sentence each; long-completed tasks belong here, without execution process.
2041
+
2042
+ ## Completed Task Summaries
2043
+ Recently completed tasks, one sentence per task covering its outcome and key outputs.
2044
+
2045
+ ## In-Progress Task Details
2046
+ 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.
2047
+
2048
+ ## Unresolved Issues
2049
+ Cross-task lingering risks and items awaiting confirmation. Only list items not already covered under In-Progress Task Details, to avoid duplication.
2050
+
2051
+ 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.`;
2052
+ function getCompactionPrompt(language) {
2053
+ return language === "en" ? COMPACTION_EN : COMPACTION;
2054
+ }
1815
2055
 
1816
2056
  // lib/prompts/store.ts
1817
2057
  function findOpencodeDir2(startDir) {
@@ -1842,15 +2082,18 @@ function normalize(content) {
1842
2082
  return content.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n").replace(/<!--[\s\S]*?-->/g, "").trim();
1843
2083
  }
1844
2084
  var PromptStore = class {
1845
- constructor(logger, workingDirectory, customPromptsEnabled = false) {
2085
+ constructor(logger, workingDirectory, customPromptsEnabled = false, language) {
1846
2086
  this.logger = logger;
1847
2087
  this.customPromptsEnabled = customPromptsEnabled;
1848
2088
  this.paths = resolvePaths(workingDirectory);
2089
+ this.defaultPrompt = getCompactionPrompt(language);
2090
+ this.runtime = { compaction: this.defaultPrompt };
1849
2091
  if (customPromptsEnabled) this.ensureDefaults();
1850
2092
  this.reload();
1851
2093
  }
1852
2094
  paths;
1853
- runtime = { compaction: COMPACTION };
2095
+ defaultPrompt;
2096
+ runtime;
1854
2097
  lastReloadAt = 0;
1855
2098
  getRuntimePrompts() {
1856
2099
  return { ...this.runtime };
@@ -1859,7 +2102,7 @@ var PromptStore = class {
1859
2102
  const now = Date.now();
1860
2103
  if (now - this.lastReloadAt < 1e3) return;
1861
2104
  this.lastReloadAt = now;
1862
- this.runtime = { compaction: COMPACTION };
2105
+ this.runtime = { compaction: this.defaultPrompt };
1863
2106
  if (!this.customPromptsEnabled) return;
1864
2107
  for (const path of this.paths.overrides) {
1865
2108
  if (!existsSync3(path)) continue;
@@ -1878,8 +2121,11 @@ var PromptStore = class {
1878
2121
  ensureDefaults() {
1879
2122
  try {
1880
2123
  mkdirSync2(this.paths.defaultsDir, { recursive: true });
1881
- writeFileSync2(join3(this.paths.defaultsDir, "compaction.md"), `${COMPACTION.trim()}
1882
- `);
2124
+ writeFileSync2(
2125
+ join3(this.paths.defaultsDir, "compaction.md"),
2126
+ `${this.defaultPrompt.trim()}
2127
+ `
2128
+ );
1883
2129
  writeFileSync2(
1884
2130
  join3(this.paths.defaultsDir, "README.md"),
1885
2131
  "# DCP compaction prompt\n\nCopy `compaction.md` to an `overrides` directory and restart OpenCode.\n"
@@ -1895,33 +2141,37 @@ var PromptStore = class {
1895
2141
  // lib/prune-tool.ts
1896
2142
  import { tool } from "@opencode-ai/plugin";
1897
2143
  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
2144
+ var PRUNE_TOOL_DESCRIPTION = `\u628A\u5F53\u524D\u4F1A\u8BDD\u7684\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
2145
 
1900
- \u6EE1\u8DB3\u4EFB\u4E00\u542F\u53D1\u5F0F\u6761\u4EF6\u65F6\uFF0C\u5FC5\u987B\u7ACB\u5373\u8C03\u7528\u672C\u5DE5\u5177\uFF1A
2146
+ \u4EC5\u5728\u8FD9\u4E9B\u60C5\u51B5\u4E0B\u8C03\u7528\uFF1A
1901
2147
  - \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
2148
+ - \u7528\u6237\u660E\u786E\u8981\u6C42\u538B\u7F29\u4E0A\u4E0B\u6587\u3002
1904
2149
 
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`;
2150
+ \u540C\u4E00\u4EFB\u52A1\u5185\u7684\u591A\u8F6E\u8FFD\u95EE\u3001\u53C2\u6570\u5FAE\u8C03\u3001\u5EF6\u7EED\u5F53\u524D\u5DE5\u4F5C\uFF0C\u90FD\u4E0D\u8981\u8C03\u7528\u3002\u8C03\u7528\u4E0D\u4F1A\u6253\u65AD\u5F53\u524D\u5DE5\u4F5C\uFF1A\u538B\u7F29\u4F1A\u6392\u961F\uFF0C\u5E76\u5728\u4E0B\u4E00\u4E2A\u7A7A\u95F2\u8FB9\u754C\u5C1D\u8BD5\u6267\u884C\uFF1B\u5E76\u53D1\u8BF7\u6C42\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\u8C03\u7528\u3002`;
1906
2151
  function createPruneTool(deps) {
1907
2152
  return tool({
1908
2153
  description: PRUNE_TOOL_DESCRIPTION,
1909
2154
  args: {},
1910
2155
  execute: async (_args, context) => {
1911
2156
  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 });
2157
+ const result = await deps.prune.request({ sessionID, onBusy: "defer" });
1917
2158
  if (result.status === "succeeded") {
1918
2159
  deps.logger.debug("Prune tool triggered native compaction", {
1919
2160
  sessionId: sessionID
1920
2161
  });
1921
2162
  return "DCP\uFF1A\u8BED\u4E49\u538B\u7F29\u5B8C\u6210\uFF0C\u65E7\u4E0A\u4E0B\u6587\u5DF2\u6298\u53E0\u4E3A\u65B0\u68C0\u67E5\u70B9\u3002";
1922
2163
  }
2164
+ if (result.status === "deferred") {
2165
+ return "DCP\uFF1A\u4F1A\u8BDD\u4ECD\u5728\u5DE5\u4F5C\u4E2D\uFF0C\u538B\u7F29\u5DF2\u6392\u961F\uFF0C\u5C06\u5728\u4E0B\u4E00\u4E2A\u7A7A\u95F2\u8FB9\u754C\u5C1D\u8BD5\u81EA\u52A8\u6267\u884C\uFF1B\u5F53\u524D\u4E0A\u4E0B\u6587\u4E0D\u53D7\u5F71\u54CD\u3002";
2166
+ }
2167
+ if (result.status === "busy") {
2168
+ return "DCP\uFF1A\u4F1A\u8BDD\u6B63\u5FD9\uFF0C\u4E3A\u907F\u514D\u6253\u65AD\u5F53\u524D\u5DE5\u4F5C\uFF0C\u672C\u6B21\u672A\u6267\u884C\u538B\u7F29\u3002";
2169
+ }
1923
2170
  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`;
2171
+ return `DCP\uFF1A\u4E0A\u4E00\u6B21\u538B\u7F29\u5931\u8D25\uFF0C${retrySeconds(result.retryAfterMs)} \u79D2\u540E\u624D\u80FD\u91CD\u8BD5\u3002`;
2172
+ }
2173
+ if (result.status === "no-model") {
2174
+ return "DCP\uFF1A\u4F1A\u8BDD\u4E2D\u8FD8\u6CA1\u6709\u53EF\u7528\u7684\u6A21\u578B\u4FE1\u606F\uFF0C\u65E0\u6CD5\u6267\u884C\u538B\u7F29\u3002";
1925
2175
  }
1926
2176
  return `DCP\uFF1A\u538B\u7F29\u5931\u8D25\uFF08${result.error}\uFF09\uFF0C\u539F\u59CB\u4E0A\u4E0B\u6587\u4FDD\u6301\u4E0D\u53D8\u3002`;
1927
2177
  }
@@ -1938,6 +2188,16 @@ function errorMessage(error) {
1938
2188
  return "Unknown native compaction error";
1939
2189
  }
1940
2190
  }
2191
+ function isBusyRejection(error) {
2192
+ if (error instanceof Error && /\bbusy\b/i.test(error.message)) return true;
2193
+ if (typeof error === "string" && /\bbusy\b/i.test(error)) return true;
2194
+ const structured = error;
2195
+ if (!structured || typeof structured !== "object") return false;
2196
+ if (structured.status === 409 || structured.statusCode === 409 || structured.code === 409) {
2197
+ return true;
2198
+ }
2199
+ return typeof structured.name === "string" && /busy/i.test(structured.name);
2200
+ }
1941
2201
  var SummarizeCoordinator = class {
1942
2202
  constructor(client, logger, options) {
1943
2203
  this.client = client;
@@ -1973,12 +2233,19 @@ var SummarizeCoordinator = class {
1973
2233
  path: { id: request.sessionID },
1974
2234
  body: request.model
1975
2235
  });
1976
- if (response?.error || response?.data !== true) {
1977
- throw new Error(errorMessage(response?.error ?? "Native summarize returned false"));
2236
+ const nativeError = response?.error;
2237
+ if (nativeError && isBusyRejection(nativeError)) {
2238
+ return { status: "rejected", reason: "busy" };
2239
+ }
2240
+ if (nativeError || response?.data !== true) {
2241
+ throw new Error(errorMessage(nativeError ?? "Native summarize returned false"));
1978
2242
  }
1979
2243
  this.failedAt.delete(request.sessionID);
1980
2244
  return { status: "succeeded" };
1981
2245
  } catch (error) {
2246
+ if (isBusyRejection(error)) {
2247
+ return { status: "rejected", reason: "busy" };
2248
+ }
1982
2249
  this.failedAt.set(request.sessionID, this.now());
1983
2250
  const message = errorMessage(error);
1984
2251
  await this.logger.warn("Native summarize failed; context remains unchanged", {
@@ -2130,10 +2397,21 @@ var server = (async (ctx) => {
2130
2397
  const config = getConfig(ctx);
2131
2398
  if (!config.enabled) return {};
2132
2399
  const logger = new Logger(config.debug);
2133
- const prompts = new PromptStore(logger, ctx.directory, config.experimental.customPrompts);
2400
+ const prompts = new PromptStore(
2401
+ logger,
2402
+ ctx.directory,
2403
+ config.experimental.customPrompts,
2404
+ config.language
2405
+ );
2134
2406
  const summarize = new SummarizeCoordinator(ctx.client, logger, {
2135
2407
  failureCooldownMs: config.summarize.failureCooldownMs
2136
2408
  });
2409
+ const prune = new PruneService({
2410
+ client: ctx.client,
2411
+ summarize,
2412
+ activity: new SessionActivityTracker(),
2413
+ logger
2414
+ });
2137
2415
  const autoPruner = new AutoPruner(config.autoPrune);
2138
2416
  logger.info("DCP initialized", {
2139
2417
  commands: config.commands.enabled,
@@ -2145,20 +2423,24 @@ var server = (async (ctx) => {
2145
2423
  return {
2146
2424
  "experimental.session.compacting": createSessionCompactingHandler(prompts, logger),
2147
2425
  ...config.autoPrune.enabled && {
2148
- "chat.message": createChatMessageHandler(autoPruner),
2426
+ "chat.message": createChatMessageHandler(autoPruner)
2427
+ },
2428
+ // The event feed drives both auto prune and the tool's deferred prunes,
2429
+ // so it stays registered whenever either surface is on.
2430
+ ...(config.autoPrune.enabled || config.tool.enabled) && {
2149
2431
  event: createEventHandler({
2150
2432
  client: ctx.client,
2151
- summarize,
2433
+ prune,
2152
2434
  autoPruner,
2153
2435
  config: config.autoPrune,
2154
2436
  logger
2155
2437
  })
2156
2438
  },
2157
2439
  ...config.tool.enabled && {
2158
- tool: { [PRUNE_TOOL_NAME]: createPruneTool({ client: ctx.client, summarize, logger }) }
2440
+ tool: { [PRUNE_TOOL_NAME]: createPruneTool({ prune, logger }) }
2159
2441
  },
2160
2442
  ...config.commands.enabled && {
2161
- "command.execute.before": createCommandExecuteHandler(ctx.client, summarize, logger),
2443
+ "command.execute.before": createCommandExecuteHandler(ctx.client, prune, logger),
2162
2444
  config: async (opencodeConfig) => {
2163
2445
  opencodeConfig.command ??= {};
2164
2446
  opencodeConfig.command.dcp = {