@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 +11 -9
- package/README.md +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +326 -44
- 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/config.d.ts +2 -0
- package/dist/lib/config.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/prompts/compaction.d.ts +3 -1
- package/dist/lib/prompts/compaction.d.ts.map +1 -1
- package/dist/lib/prompts/store.d.ts +2 -1
- package/dist/lib/prompts/store.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/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
|
-
| `
|
|
75
|
-
| `
|
|
76
|
-
| `autoPrune.
|
|
77
|
-
| `autoPrune.
|
|
78
|
-
| `autoPrune.
|
|
79
|
-
| `autoPrune.
|
|
80
|
-
| `autoPrune.
|
|
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` | 会话用户消息数达到该值前不做任何自动判定 |
|
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;
|
|
@@ -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
|
|
1494
|
-
|
|
1495
|
-
|
|
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
|
|
1515
|
-
if (
|
|
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,
|
|
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
|
|
1552
|
-
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") {
|
|
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 ${
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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 = `\
|
|
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
|
-
\
|
|
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
|
-
- \
|
|
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\
|
|
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
|
|
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${
|
|
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
|
-
|
|
1977
|
-
|
|
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(
|
|
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
|
-
|
|
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({
|
|
2440
|
+
tool: { [PRUNE_TOOL_NAME]: createPruneTool({ prune, logger }) }
|
|
2159
2441
|
},
|
|
2160
2442
|
...config.commands.enabled && {
|
|
2161
|
-
"command.execute.before": createCommandExecuteHandler(ctx.client,
|
|
2443
|
+
"command.execute.before": createCommandExecuteHandler(ctx.client, prune, logger),
|
|
2162
2444
|
config: async (opencodeConfig) => {
|
|
2163
2445
|
opencodeConfig.command ??= {};
|
|
2164
2446
|
opencodeConfig.command.dcp = {
|