@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.
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +268 -32
- package/dist/index.js.map +1 -1
- package/dist/lib/activity.d.ts +22 -0
- package/dist/lib/activity.d.ts.map +1 -0
- package/dist/lib/auto-prune.d.ts.map +1 -1
- package/dist/lib/hooks.d.ts +3 -3
- package/dist/lib/hooks.d.ts.map +1 -1
- package/dist/lib/prune-service.d.ts +99 -0
- package/dist/lib/prune-service.d.ts.map +1 -0
- package/dist/lib/prune-tool.d.ts +2 -4
- package/dist/lib/prune-tool.d.ts.map +1 -1
- package/dist/lib/summarize.d.ts +3 -0
- package/dist/lib/summarize.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
1504
|
-
|
|
1505
|
-
|
|
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
|
|
1525
|
-
if (
|
|
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,
|
|
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
|
|
1562
|
-
if (
|
|
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 ${
|
|
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 = `\
|
|
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
|
-
\
|
|
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
|
-
- \
|
|
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\
|
|
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
|
|
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${
|
|
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
|
-
|
|
2018
|
-
|
|
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
|
-
|
|
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({
|
|
2440
|
+
tool: { [PRUNE_TOOL_NAME]: createPruneTool({ prune, logger }) }
|
|
2205
2441
|
},
|
|
2206
2442
|
...config.commands.enabled && {
|
|
2207
|
-
"command.execute.before": createCommandExecuteHandler(ctx.client,
|
|
2443
|
+
"command.execute.before": createCommandExecuteHandler(ctx.client, prune, logger),
|
|
2208
2444
|
config: async (opencodeConfig) => {
|
|
2209
2445
|
opencodeConfig.command ??= {};
|
|
2210
2446
|
opencodeConfig.command.dcp = {
|