@lexwdex-org/opencode-dcp 5.0.0 → 5.0.2
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 +2 -2
- package/README.md +2 -2
- package/dist/index.js +61 -13
- package/dist/index.js.map +1 -1
- package/dist/lib/dtc/digest.d.ts.map +1 -1
- package/dist/lib/dtc/engine.d.ts.map +1 -1
- package/dist/lib/dtc/state.d.ts +19 -1
- package/dist/lib/dtc/state.d.ts.map +1 -1
- package/dist/lib/hooks.d.ts +5 -0
- package/dist/lib/hooks.d.ts.map +1 -1
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -24,7 +24,7 @@ The host fires `experimental.chat.messages.transform` before every model request
|
|
|
24
24
|
- **M middle zone**: long texts keep their first line; tool outputs get the host's native fold marker (rendered by the host as its own `[Old tool result content cleared]`); **tool arguments reduce to a target skeleton** (only filePath / command first line and friends — `oldString`/`newString`/`content` payloads are dropped); **failed attempts (error parts) fold to a short first line** — three edits on one file with two failures collapse to three recognizable call skeletons plus two single-line errors, with the final state owned by disk and the current-task zone; reasoning is emptied.
|
|
25
25
|
- **D distant zone**: whole turns collapse into one mechanical digest line (intent / actions / files touched / outcome / error count) and tool inputs are cleared — hard facts live on in the digest.
|
|
26
26
|
|
|
27
|
-
**Dynamic budget**: below `lowWatermarkRatio` of the context window (default 50%) **nothing is folded at all** — short sessions pay zero cost. Above it, folding escalates D→M→C from the far end until the estimate fits `targetRatio` (default 70%). The window size is learned per session from the `chat.params` hook; until it is known the engine fails open (no folding).
|
|
27
|
+
**Dynamic budget**: below `lowWatermarkRatio` of the context window (default 50%) **nothing is folded at all** — short sessions pay zero cost. Above it, folding escalates D→M→C from the far end until the estimate fits `targetRatio` (default 70%). The window size is learned per session from the `chat.params` hook; until it is known the engine fails open (no folding). Session attribution reads `sessionID` straight off the message payload on official hosts; on fork hosts that strip message IDs into database columns, DTC falls back to a `chat.params`-recorded "user-message timestamp → session" index (content-keyed, so concurrent sessions never cross-attribute; a session's very first request has no record yet and skips folding).
|
|
28
28
|
|
|
29
29
|
**Three structural laws** (the constructive exclusion of the old versions' "lost markers" failure):
|
|
30
30
|
|
|
@@ -89,7 +89,7 @@ DCP reads these layers in order; later layers override earlier ones:
|
|
|
89
89
|
|
|
90
90
|
## Migrating from 3.x / 4.0
|
|
91
91
|
|
|
92
|
-
The 3.x `summarize` and `autoPrune` config blocks are gone (DCP no longer calls native summarize and has no heuristic triggers); since
|
|
92
|
+
The 3.x `summarize` and `autoPrune` config blocks are gone (DCP no longer calls native summarize and has no heuristic triggers); since 5.0 `language` and `experimental.customPrompts` are gone as well — DCP no longer replaces the host's compaction prompt, so the `dcp-prompts` override machinery retired with it. Such keys produce a migration warning and are ignored. `autoPrune.driftThreshold` migrates automatically to `dtc.driftThreshold`. Older `compress`, `manualMode`, `strategies`, `turnProtection` keys remain removed.
|
|
93
93
|
|
|
94
94
|
Behavior change: compression no longer produces a visible "checkpoint turn" and needs no at-rest boundaries or continuation machinery — context folds automatically inside each request, invisible to the session and its state machine. Semantic checkpoints are fully handed back to the host's native compaction (manual `/compact` or the overflow fallback): native prompt, native rolling merge — DCP only contributes the tail-protection defaults and full-fidelity protection of the summarizer input.
|
|
95
95
|
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ DCP 以**动态分级请求期压缩(DTC)**管理 OpenCode 会话上下文
|
|
|
24
24
|
- **M 中距离区**:长文本保留首行;tool 输出打宿主原生折叠标记(渲染为宿主自己的 `[Old tool result content cleared]`);**tool 参数规约为目标骨架**(仅留 filePath/command 首行等,`oldString`/`newString`/`content` 等载荷全部丢弃);**失败尝试(error part)折为短首行**——同一文件连改 3 次错 2 次,折叠后只剩 3 个可辨识调用骨架 + 2 条单行错误,最终状态以磁盘与当前任务区为准;推理内容清空。
|
|
25
25
|
- **D 远距离区**:整轮塌缩为一行机械摘要(意图 / 动作 / 涉及文件 / 结果 / 错误数),tool 参数一并清空——硬事实保留在摘要里。
|
|
26
26
|
|
|
27
|
-
**动态预算**:估算低于上下文窗口的 `lowWatermarkRatio`(默认 50%)时**完全不折叠**,短会话零开销;超出则按 D→M→C 从远到近逐级加深,直到估算落回 `targetRatio`(默认 70%)以内。窗口大小由 `chat.params` 钩子按会话自动学习,未知时 fail-open
|
|
27
|
+
**动态预算**:估算低于上下文窗口的 `lowWatermarkRatio`(默认 50%)时**完全不折叠**,短会话零开销;超出则按 D→M→C 从远到近逐级加深,直到估算落回 `targetRatio`(默认 70%)以内。窗口大小由 `chat.params` 钩子按会话自动学习,未知时 fail-open 不折叠。会话归属按官方宿主形状直读消息体 `sessionID`;fork 宿主把 id/sessionID 剥离进数据库列时,自动改用 `chat.params` 预记录的「用户消息时间戳 → 会话」索引反查归属(内容键关联,并发会话不串扰;会话首个请求尚无记录则跳过折叠)。
|
|
28
28
|
|
|
29
29
|
**三条结构铁律**(老版本"丢失标记号"问题的构造性排除):
|
|
30
30
|
|
|
@@ -89,7 +89,7 @@ DCP 依次读取以下配置,后面的层覆盖前面的层:
|
|
|
89
89
|
|
|
90
90
|
## 从 3.x / 4.0 迁移
|
|
91
91
|
|
|
92
|
-
3.x 的 `summarize`、`autoPrune` 配置块已删除(DCP 不再调用原生 summarize,也不再有启发式触发器);
|
|
92
|
+
3.x 的 `summarize`、`autoPrune` 配置块已删除(DCP 不再调用原生 summarize,也不再有启发式触发器);5.0 起 `language` 与 `experimental.customPrompts` 也已删除——DCP 不再替换宿主压缩提示词,`dcp-prompts` 覆盖机制随之退役。这些键会显示迁移警告并被忽略。`autoPrune.driftThreshold` 自动迁移为 `dtc.driftThreshold`。更早的 `compress`、`manualMode`、`strategies`、`turnProtection` 等键维持删除状态。
|
|
93
93
|
|
|
94
94
|
行为变化:压缩不再产生可见的"检查点回合",也不再需要任何静息边界或续跑机制——上下文折叠在每次请求内自动完成,会话与状态机零感知。语义检查点完全交还宿主原生 compaction(手动 `/compact` 或溢出兜底):原生提示词、原生滚动合并,DCP 仅贡献尾部保护默认值与摘要输入的全保真保护。
|
|
95
95
|
|
package/dist/index.js
CHANGED
|
@@ -1028,6 +1028,7 @@ function estimateSlice(messages, start, end) {
|
|
|
1028
1028
|
}
|
|
1029
1029
|
if (typeof state.output === "string") tokens += estimateTokens(state.output);
|
|
1030
1030
|
if (state.input) tokens += estimateTokens(JSON.stringify(state.input));
|
|
1031
|
+
if (typeof state.error === "string") tokens += estimateTokens(state.error);
|
|
1031
1032
|
}
|
|
1032
1033
|
}
|
|
1033
1034
|
return tokens;
|
|
@@ -1080,7 +1081,7 @@ function transformMessages(messages, deps) {
|
|
|
1080
1081
|
const stats = { ...NO_STATS };
|
|
1081
1082
|
if (!Array.isArray(messages) || messages.length === 0) return stats;
|
|
1082
1083
|
stats.messages = messages.length;
|
|
1083
|
-
const sessionID = findSessionID(messages);
|
|
1084
|
+
const sessionID = findSessionID(messages, deps.state);
|
|
1084
1085
|
if (sessionID && deps.state.consumeCompactionSkip(sessionID)) {
|
|
1085
1086
|
stats.skipped = "compaction";
|
|
1086
1087
|
return stats;
|
|
@@ -1111,11 +1112,18 @@ function transformMessages(messages, deps) {
|
|
|
1111
1112
|
const zones = computeZones(messages, headTurns, sessionID, state, config);
|
|
1112
1113
|
const now = (deps.now ?? Date.now)();
|
|
1113
1114
|
let level = Math.max(1, minLevel);
|
|
1114
|
-
|
|
1115
|
+
let appliedBand = 0;
|
|
1116
|
+
const applyUpTo = (lvl) => {
|
|
1117
|
+
while (appliedBand < lvl) {
|
|
1118
|
+
appliedBand++;
|
|
1119
|
+
applyBand(messages, headTurns, zones, appliedBand, deps, stats, now);
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
applyUpTo(level);
|
|
1115
1123
|
let estimatedAfter = estimateSlice(messages, 0, messages.length);
|
|
1116
1124
|
while (estimatedAfter > target && level < 3) {
|
|
1117
1125
|
level = level + 1;
|
|
1118
|
-
|
|
1126
|
+
applyUpTo(level);
|
|
1119
1127
|
estimatedAfter = estimateSlice(messages, 0, messages.length);
|
|
1120
1128
|
}
|
|
1121
1129
|
stats.level = level;
|
|
@@ -1154,19 +1162,16 @@ function computeZones(messages, headTurns, sessionID, state, config) {
|
|
|
1154
1162
|
const mStart = Math.min(cStart, Math.max(secondLast, cStart - M_ZONE_MAX_TURNS));
|
|
1155
1163
|
return { mStart, cStart };
|
|
1156
1164
|
}
|
|
1157
|
-
function
|
|
1158
|
-
|
|
1159
|
-
if (level >= 1 && prev < 1) {
|
|
1165
|
+
function applyBand(messages, headTurns, zones, band, deps, stats, now) {
|
|
1166
|
+
if (band === 1) {
|
|
1160
1167
|
for (let t = 0; t < zones.mStart; t++) {
|
|
1161
1168
|
foldDistant(messages, headTurns[t], t + 1, deps, stats, now);
|
|
1162
1169
|
}
|
|
1163
|
-
}
|
|
1164
|
-
if (level >= 2 && prev < 2) {
|
|
1170
|
+
} else if (band === 2) {
|
|
1165
1171
|
for (let t = zones.mStart; t < zones.cStart; t++) {
|
|
1166
1172
|
foldMiddle(messages, headTurns[t], stats, now);
|
|
1167
1173
|
}
|
|
1168
|
-
}
|
|
1169
|
-
if (level >= 3 && prev < 3) {
|
|
1174
|
+
} else if (band === 3) {
|
|
1170
1175
|
for (let t = zones.cStart; t < headTurns.length; t++) {
|
|
1171
1176
|
foldCurrent(messages, headTurns[t], deps.config.toolOutputKeepChars, stats);
|
|
1172
1177
|
}
|
|
@@ -1289,11 +1294,20 @@ function foldErrorPart(part, keepChars, clearInput, stats) {
|
|
|
1289
1294
|
stats.reducedInputs++;
|
|
1290
1295
|
}
|
|
1291
1296
|
}
|
|
1292
|
-
function findSessionID(messages) {
|
|
1297
|
+
function findSessionID(messages, state) {
|
|
1293
1298
|
for (const message of messages) {
|
|
1294
1299
|
const id = message?.info?.sessionID;
|
|
1295
1300
|
if (typeof id === "string" && id) return id;
|
|
1296
1301
|
}
|
|
1302
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1303
|
+
const message = messages[i];
|
|
1304
|
+
if (message?.info?.role !== "user") continue;
|
|
1305
|
+
if ((message.parts ?? []).some((p) => p?.type === "compaction")) continue;
|
|
1306
|
+
const created = message.info?.time?.created;
|
|
1307
|
+
if (typeof created !== "number") continue;
|
|
1308
|
+
const sessionID = state.sessionByUserTime(created);
|
|
1309
|
+
if (sessionID) return sessionID;
|
|
1310
|
+
}
|
|
1297
1311
|
return void 0;
|
|
1298
1312
|
}
|
|
1299
1313
|
|
|
@@ -1689,6 +1703,7 @@ Using previous/default values`
|
|
|
1689
1703
|
// lib/dtc/state.ts
|
|
1690
1704
|
var SESSION_LIMIT = 500;
|
|
1691
1705
|
var DIGEST_LIMIT = 2e3;
|
|
1706
|
+
var PARAMS_INDEX_LIMIT = 200;
|
|
1692
1707
|
function lruSet(map, key, value, limit) {
|
|
1693
1708
|
if (map.has(key)) map.delete(key);
|
|
1694
1709
|
map.set(key, value);
|
|
@@ -1701,6 +1716,8 @@ function lruSet(map, key, value, limit) {
|
|
|
1701
1716
|
var DtcState = class {
|
|
1702
1717
|
sessions = /* @__PURE__ */ new Map();
|
|
1703
1718
|
digests = /* @__PURE__ */ new Map();
|
|
1719
|
+
/** User-message creation time → sessionID, fed by `chat.params`. */
|
|
1720
|
+
paramsIndex = /* @__PURE__ */ new Map();
|
|
1704
1721
|
observeContextLimit(sessionID, contextTokens) {
|
|
1705
1722
|
if (!sessionID || !contextTokens || !Number.isFinite(contextTokens) || contextTokens <= 0) {
|
|
1706
1723
|
return;
|
|
@@ -1712,7 +1729,27 @@ var DtcState = class {
|
|
|
1712
1729
|
contextTokens(sessionID) {
|
|
1713
1730
|
return this.sessions.get(sessionID)?.contextTokens;
|
|
1714
1731
|
}
|
|
1715
|
-
/**
|
|
1732
|
+
/**
|
|
1733
|
+
* Fork hosts strip id/sessionID out of message payloads (they live in
|
|
1734
|
+
* database columns), so the transform cannot read the session off the
|
|
1735
|
+
* messages like it does on the official host. `chat.params` fires on
|
|
1736
|
+
* every request carrying both the sessionID and the triggering user
|
|
1737
|
+
* message; recording that pair lets the NEXT request's transform resolve
|
|
1738
|
+
* its session by matching user-message creation times. The key is
|
|
1739
|
+
* content (a timestamp), not recency, so concurrent sessions never
|
|
1740
|
+
* cross-attribute.
|
|
1741
|
+
*/
|
|
1742
|
+
recordParamsSession(sessionID, userTimeCreated) {
|
|
1743
|
+
if (!sessionID || !Number.isFinite(userTimeCreated)) return;
|
|
1744
|
+
lruSet(this.paramsIndex, userTimeCreated, sessionID, PARAMS_INDEX_LIMIT);
|
|
1745
|
+
}
|
|
1746
|
+
sessionByUserTime(userTimeCreated) {
|
|
1747
|
+
return this.paramsIndex.get(userTimeCreated);
|
|
1748
|
+
}
|
|
1749
|
+
/** Called by the compacting hook; consumed by the very next transform.
|
|
1750
|
+
* Accepted leak: if the host aborts between the hook and the summarizer
|
|
1751
|
+
* transform, the flag skips folding once for the next unrelated request —
|
|
1752
|
+
* fail-open (extra tokens, never lost information) and self-healing. */
|
|
1716
1753
|
armCompactionSkip(sessionID) {
|
|
1717
1754
|
const state = this.session(sessionID);
|
|
1718
1755
|
state.skipNextTransform = true;
|
|
@@ -1745,10 +1782,17 @@ var DtcState = class {
|
|
|
1745
1782
|
}
|
|
1746
1783
|
dropSession(sessionID) {
|
|
1747
1784
|
this.sessions.delete(sessionID);
|
|
1785
|
+
for (const [time, sid] of this.paramsIndex) {
|
|
1786
|
+
if (sid === sessionID) this.paramsIndex.delete(time);
|
|
1787
|
+
}
|
|
1748
1788
|
}
|
|
1749
1789
|
/** Test/inspection surface. */
|
|
1750
1790
|
stats() {
|
|
1751
|
-
return {
|
|
1791
|
+
return {
|
|
1792
|
+
sessions: this.sessions.size,
|
|
1793
|
+
digests: this.digests.size,
|
|
1794
|
+
params: this.paramsIndex.size
|
|
1795
|
+
};
|
|
1752
1796
|
}
|
|
1753
1797
|
session(sessionID) {
|
|
1754
1798
|
return this.sessions.get(sessionID) ?? {};
|
|
@@ -1789,6 +1833,10 @@ function createChatParamsHandler(deps) {
|
|
|
1789
1833
|
if (typeof context === "number" && Number.isFinite(context) && context > 0) {
|
|
1790
1834
|
deps.state.observeContextLimit(input.sessionID, context);
|
|
1791
1835
|
}
|
|
1836
|
+
const created = input.message?.time?.created;
|
|
1837
|
+
if (input.sessionID && typeof created === "number" && Number.isFinite(created)) {
|
|
1838
|
+
deps.state.recordParamsSession(input.sessionID, created);
|
|
1839
|
+
}
|
|
1792
1840
|
} catch (error) {
|
|
1793
1841
|
deps.logger.debug("chat.params observation failed", {
|
|
1794
1842
|
error: error instanceof Error ? error.message : String(error)
|