@lexwdex-org/opencode-dcp 3.4.13 → 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.
@@ -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,MAkDK,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;
@@ -1461,6 +1503,142 @@ async function resolveSessionModel(client, sessionID) {
1461
1503
  }
1462
1504
  }
1463
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
+
1464
1642
  // lib/hooks.ts
1465
1643
  function createSessionCompactingHandler(prompts, logger) {
1466
1644
  return async (input, output) => {
@@ -1500,15 +1678,33 @@ var SIGNAL_LABELS = {
1500
1678
  function createEventHandler(deps) {
1501
1679
  async function triggerAutoPrune(sessionID, signals) {
1502
1680
  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 });
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
+ });
1506
1696
  return;
1507
1697
  }
1508
- const result = await deps.summarize.summarize({ sessionID, model });
1509
1698
  deps.autoPruner.markPruned(sessionID);
1510
1699
  if (result.status === "succeeded") {
1511
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
+ );
1512
1708
  } else {
1513
1709
  await showToast(
1514
1710
  deps.client,
@@ -1521,9 +1717,10 @@ function createEventHandler(deps) {
1521
1717
  }
1522
1718
  return async (input) => {
1523
1719
  const event = input.event;
1524
- const sessionID = event.properties?.sessionID;
1525
- if (typeof sessionID !== "string" || !sessionID) return;
1720
+ const sessionID = eventSessionID(event.properties);
1721
+ if (!sessionID) return;
1526
1722
  try {
1723
+ deps.prune.observeEvent(event.type, event.properties);
1527
1724
  if (event.type === "session.idle") {
1528
1725
  if (!deps.config.enabled) return;
1529
1726
  const signals = deps.autoPruner.consumePending(sessionID);
@@ -1546,7 +1743,7 @@ function createEventHandler(deps) {
1546
1743
  }
1547
1744
  };
1548
1745
  }
1549
- function createCommandExecuteHandler(client, summarize, logger) {
1746
+ function createCommandExecuteHandler(client, prune, logger) {
1550
1747
  return async (input, _output) => {
1551
1748
  if (input.command !== "dcp") return;
1552
1749
  const subcommand = (input.arguments ?? "").trim().split(/\s+/, 1)[0]?.toLowerCase();
@@ -1558,8 +1755,17 @@ function createCommandExecuteHandler(client, summarize, logger) {
1558
1755
  );
1559
1756
  throw new Error("__DCP_HELP_HANDLED__");
1560
1757
  }
1561
- const model = await resolveSessionModel(client, input.sessionID);
1562
- 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") {
1563
1769
  await showToast(
1564
1770
  client,
1565
1771
  "DCP summarize",
@@ -1568,14 +1774,13 @@ function createCommandExecuteHandler(client, summarize, logger) {
1568
1774
  );
1569
1775
  throw new Error("__DCP_SUMMARIZE_NO_MODEL__");
1570
1776
  }
1571
- const result = await summarize.summarize({ sessionID: input.sessionID, model });
1572
1777
  if (result.status === "succeeded") {
1573
1778
  await showToast(client, "DCP summarize", "Semantic pruning checkpoint created.");
1574
1779
  } else if (result.status === "cooldown") {
1575
1780
  await showToast(
1576
1781
  client,
1577
1782
  "DCP summarize",
1578
- `Previous attempt failed; retry in ${Math.ceil(result.retryAfterMs / 1e3)}s.`,
1783
+ `Previous attempt failed; retry in ${retrySeconds(result.retryAfterMs)}s.`,
1579
1784
  "warning"
1580
1785
  );
1581
1786
  } else {
@@ -1936,33 +2141,37 @@ var PromptStore = class {
1936
2141
  // lib/prune-tool.ts
1937
2142
  import { tool } from "@opencode-ai/plugin";
1938
2143
  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
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
1940
2145
 
1941
- \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
1942
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
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
2148
+ - \u7528\u6237\u660E\u786E\u8981\u6C42\u538B\u7F29\u4E0A\u4E0B\u6587\u3002
1945
2149
 
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`;
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`;
1947
2151
  function createPruneTool(deps) {
1948
2152
  return tool({
1949
2153
  description: PRUNE_TOOL_DESCRIPTION,
1950
2154
  args: {},
1951
2155
  execute: async (_args, context) => {
1952
2156
  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 });
2157
+ const result = await deps.prune.request({ sessionID, onBusy: "defer" });
1958
2158
  if (result.status === "succeeded") {
1959
2159
  deps.logger.debug("Prune tool triggered native compaction", {
1960
2160
  sessionId: sessionID
1961
2161
  });
1962
2162
  return "DCP\uFF1A\u8BED\u4E49\u538B\u7F29\u5B8C\u6210\uFF0C\u65E7\u4E0A\u4E0B\u6587\u5DF2\u6298\u53E0\u4E3A\u65B0\u68C0\u67E5\u70B9\u3002";
1963
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
+ }
1964
2170
  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`;
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";
1966
2175
  }
1967
2176
  return `DCP\uFF1A\u538B\u7F29\u5931\u8D25\uFF08${result.error}\uFF09\uFF0C\u539F\u59CB\u4E0A\u4E0B\u6587\u4FDD\u6301\u4E0D\u53D8\u3002`;
1968
2177
  }
@@ -1979,6 +2188,16 @@ function errorMessage(error) {
1979
2188
  return "Unknown native compaction error";
1980
2189
  }
1981
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
+ }
1982
2201
  var SummarizeCoordinator = class {
1983
2202
  constructor(client, logger, options) {
1984
2203
  this.client = client;
@@ -2014,12 +2233,19 @@ var SummarizeCoordinator = class {
2014
2233
  path: { id: request.sessionID },
2015
2234
  body: request.model
2016
2235
  });
2017
- if (response?.error || response?.data !== true) {
2018
- 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"));
2019
2242
  }
2020
2243
  this.failedAt.delete(request.sessionID);
2021
2244
  return { status: "succeeded" };
2022
2245
  } catch (error) {
2246
+ if (isBusyRejection(error)) {
2247
+ return { status: "rejected", reason: "busy" };
2248
+ }
2023
2249
  this.failedAt.set(request.sessionID, this.now());
2024
2250
  const message = errorMessage(error);
2025
2251
  await this.logger.warn("Native summarize failed; context remains unchanged", {
@@ -2180,6 +2406,12 @@ var server = (async (ctx) => {
2180
2406
  const summarize = new SummarizeCoordinator(ctx.client, logger, {
2181
2407
  failureCooldownMs: config.summarize.failureCooldownMs
2182
2408
  });
2409
+ const prune = new PruneService({
2410
+ client: ctx.client,
2411
+ summarize,
2412
+ activity: new SessionActivityTracker(),
2413
+ logger
2414
+ });
2183
2415
  const autoPruner = new AutoPruner(config.autoPrune);
2184
2416
  logger.info("DCP initialized", {
2185
2417
  commands: config.commands.enabled,
@@ -2191,20 +2423,24 @@ var server = (async (ctx) => {
2191
2423
  return {
2192
2424
  "experimental.session.compacting": createSessionCompactingHandler(prompts, logger),
2193
2425
  ...config.autoPrune.enabled && {
2194
- "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) && {
2195
2431
  event: createEventHandler({
2196
2432
  client: ctx.client,
2197
- summarize,
2433
+ prune,
2198
2434
  autoPruner,
2199
2435
  config: config.autoPrune,
2200
2436
  logger
2201
2437
  })
2202
2438
  },
2203
2439
  ...config.tool.enabled && {
2204
- tool: { [PRUNE_TOOL_NAME]: createPruneTool({ client: ctx.client, summarize, logger }) }
2440
+ tool: { [PRUNE_TOOL_NAME]: createPruneTool({ prune, logger }) }
2205
2441
  },
2206
2442
  ...config.commands.enabled && {
2207
- "command.execute.before": createCommandExecuteHandler(ctx.client, summarize, logger),
2443
+ "command.execute.before": createCommandExecuteHandler(ctx.client, prune, logger),
2208
2444
  config: async (opencodeConfig) => {
2209
2445
  opencodeConfig.command ??= {};
2210
2446
  opencodeConfig.command.dcp = {