@lexwdex-org/opencode-dcp 3.4.14 → 4.0.0

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.js CHANGED
@@ -1,156 +1,3 @@
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
-
40
- // lib/auto-prune.ts
41
- function tokenize(text) {
42
- const tokens = /* @__PURE__ */ new Set();
43
- for (const match of text.toLowerCase().matchAll(/[\p{L}\p{N}]+/gu)) {
44
- const word = match[0];
45
- if (/[\u4e00-\u9fff]/.test(word)) {
46
- if (word.length === 1) {
47
- tokens.add(word);
48
- continue;
49
- }
50
- for (let index = 0; index < word.length - 1; index++) {
51
- tokens.add(word.slice(index, index + 2));
52
- }
53
- } else {
54
- tokens.add(word);
55
- }
56
- }
57
- return tokens;
58
- }
59
- function jaccard(a, b) {
60
- if (a.size === 0 && b.size === 0) return 1;
61
- let intersection = 0;
62
- for (const token of a) {
63
- if (b.has(token)) intersection++;
64
- }
65
- return intersection / (a.size + b.size - intersection);
66
- }
67
- var WINDOW_SIZE = 4;
68
- var DRIFT_BASELINE = 3;
69
- var MIN_DRIFT_TOKENS = 6;
70
- function extractText(parts) {
71
- const texts = [];
72
- for (const part of parts) {
73
- if (part && typeof part === "object" && part.type === "text" && typeof part.text === "string") {
74
- texts.push(part.text);
75
- }
76
- }
77
- return texts.join(" ").trim();
78
- }
79
- var AutoPruner = class {
80
- constructor(config, now) {
81
- this.config = config;
82
- this.now = now ?? Date.now;
83
- }
84
- sessions = /* @__PURE__ */ new Map();
85
- now;
86
- observeUserMessage(sessionID, parts, at = this.now()) {
87
- const state = this.state(sessionID);
88
- const text = extractText(parts);
89
- const signals = this.evaluate(state, text, at);
90
- if (text) {
91
- state.window.push(text);
92
- if (state.window.length > WINDOW_SIZE) state.window.shift();
93
- }
94
- state.count += 1;
95
- state.lastAt = at;
96
- for (const signal of signals) {
97
- if (!state.pendingSignals.includes(signal)) state.pendingSignals.push(signal);
98
- }
99
- return { signals };
100
- }
101
- consumePending(sessionID, at = this.now()) {
102
- const state = this.sessions.get(sessionID);
103
- if (!state || state.pendingSignals.length === 0) return null;
104
- const signals = [...state.pendingSignals];
105
- state.pendingSignals = [];
106
- if (at - state.lastTriggerAt < this.config.cooldownMs) return null;
107
- state.lastTriggerAt = at;
108
- return signals;
109
- }
110
- markPruned(sessionID, at = this.now()) {
111
- const state = this.sessions.get(sessionID);
112
- if (!state) return;
113
- state.count = 0;
114
- state.window = [];
115
- state.pendingSignals = [];
116
- state.lastTriggerAt = at;
117
- }
118
- dropSession(sessionID) {
119
- this.sessions.delete(sessionID);
120
- }
121
- evaluate(state, text, at) {
122
- if (state.count + 1 < this.config.minMessages) return [];
123
- const signals = [];
124
- if (state.count > 0 && at - state.lastAt >= this.config.idleGapMs) {
125
- signals.push("idle-gap");
126
- }
127
- if (state.count >= DRIFT_BASELINE && text) {
128
- const current = tokenize(text);
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");
135
- }
136
- }
137
- if (state.count + 1 >= this.config.volumeThreshold) signals.push("volume");
138
- return signals;
139
- }
140
- state(sessionID) {
141
- let state = this.sessions.get(sessionID);
142
- if (!state) {
143
- state = { window: [], count: 0, lastAt: 0, pendingSignals: [], lastTriggerAt: 0 };
144
- this.sessions.set(sessionID, state);
145
- if (this.sessions.size > 200) {
146
- const oldest = this.sessions.keys().next().value;
147
- if (oldest !== void 0 && oldest !== sessionID) this.sessions.delete(oldest);
148
- }
149
- }
150
- return state;
151
- }
152
- };
153
-
154
1
  // lib/config.ts
155
2
  import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from "fs";
156
3
  import { join, dirname } from "path";
@@ -1015,15 +862,403 @@ var ParseErrorCode;
1015
862
  ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
1016
863
  })(ParseErrorCode || (ParseErrorCode = {}));
1017
864
 
865
+ // lib/text.ts
866
+ function tokenize(text) {
867
+ const tokens = /* @__PURE__ */ new Set();
868
+ for (const match of text.toLowerCase().matchAll(/[\p{L}\p{N}]+/gu)) {
869
+ const word = match[0];
870
+ if (/[\u4e00-\u9fff]/.test(word)) {
871
+ if (word.length === 1) {
872
+ tokens.add(word);
873
+ continue;
874
+ }
875
+ for (let index = 0; index < word.length - 1; index++) {
876
+ tokens.add(word.slice(index, index + 2));
877
+ }
878
+ } else {
879
+ tokens.add(word);
880
+ }
881
+ }
882
+ return tokens;
883
+ }
884
+ function jaccard(a, b) {
885
+ if (a.size === 0 && b.size === 0) return 1;
886
+ let intersection = 0;
887
+ for (const token of a) {
888
+ if (b.has(token)) intersection++;
889
+ }
890
+ return intersection / (a.size + b.size - intersection);
891
+ }
892
+ function estimateTokens(text) {
893
+ let cjk = 0;
894
+ let other = 0;
895
+ for (let index = 0; index < text.length; index++) {
896
+ const code = text.charCodeAt(index);
897
+ if (code >= 19968 && code <= 40959) cjk++;
898
+ else other++;
899
+ }
900
+ return Math.ceil(cjk * 0.7 + other / 4);
901
+ }
902
+ function hashString(text) {
903
+ let hash = 5381;
904
+ for (let index = 0; index < text.length; index++) {
905
+ hash = (hash << 5) + hash + text.charCodeAt(index) | 0;
906
+ }
907
+ return (hash >>> 0).toString(36);
908
+ }
909
+ function firstLine(text, maxChars) {
910
+ const line = text.split("\n", 1)[0]?.trim() ?? "";
911
+ return line.length > maxChars ? `${line.slice(0, maxChars)}\u2026` : line;
912
+ }
913
+ function truncateMiddle(text, keepChars, note) {
914
+ if (text.length <= keepChars) return text;
915
+ const head = Math.ceil(keepChars / 2);
916
+ const tail = Math.floor(keepChars / 2);
917
+ return `${text.slice(0, head)}
918
+ ${note}
919
+ ${text.slice(text.length - tail)}`;
920
+ }
921
+
922
+ // lib/dtc/digest.ts
923
+ var INTENT_MAX = 120;
924
+ var RESULT_MAX = 160;
925
+ var MAX_TOOLS = 8;
926
+ var MAX_FILES = 6;
927
+ var DIGEST_MAX_CHARS = 600;
928
+ var PATH_KEYS = ["filePath", "path", "file", "filename", "pattern", "directory"];
929
+ function toolSummary(parts) {
930
+ const counts = /* @__PURE__ */ new Map();
931
+ const files = [];
932
+ let errors = 0;
933
+ for (const part of parts) {
934
+ if (!part || typeof part !== "object" || part.type !== "tool") continue;
935
+ const name = part.tool ?? "tool";
936
+ counts.set(name, (counts.get(name) ?? 0) + 1);
937
+ if (part.state?.status === "error") errors++;
938
+ const input = part.state?.input;
939
+ if (input && typeof input === "object") {
940
+ for (const key of PATH_KEYS) {
941
+ const value = input[key];
942
+ if (typeof value === "string" && value && !files.includes(value)) {
943
+ files.push(value);
944
+ }
945
+ }
946
+ const command = input.command;
947
+ if (typeof command === "string" && command && !files.includes(firstLine(command, 60))) {
948
+ files.push(firstLine(command, 60));
949
+ }
950
+ }
951
+ }
952
+ const actions = [...counts.entries()].map(([name, count]) => count > 1 ? `${name}\xD7${count}` : name).slice(0, MAX_TOOLS).join(", ");
953
+ return { actions, files: files.slice(0, MAX_FILES).join(" "), errors };
954
+ }
955
+ function digestTurn(messages, turn, index) {
956
+ const slice = messages.slice(turn.start, turn.end);
957
+ const user = slice[0];
958
+ const intent = firstLine(userText(user), INTENT_MAX) || "(\u65E0\u6587\u672C)";
959
+ const { actions, files, errors } = toolSummary(
960
+ slice.flatMap((m) => m && typeof m === "object" ? m.parts ?? [] : [])
961
+ );
962
+ let result = "";
963
+ for (let i = slice.length - 1; i >= 0; i--) {
964
+ if (slice[i]?.info?.role !== "assistant") continue;
965
+ const text = assistantText(slice[i]);
966
+ if (text) {
967
+ result = firstLine(text, RESULT_MAX);
968
+ break;
969
+ }
970
+ }
971
+ const parts = [`[DCP\xB7\u8F6E${index}]`, `\u610F\u56FE: ${intent}`];
972
+ if (actions) parts.push(`\u52A8\u4F5C: ${actions}`);
973
+ if (files) parts.push(`\u6D89\u53CA: ${files}`);
974
+ if (result) parts.push(`\u7ED3\u679C: ${result}`);
975
+ if (errors > 0) parts.push(`\u9519\u8BEF\xD7${errors}`);
976
+ const digest = parts.join(" | ");
977
+ return digest.length > DIGEST_MAX_CHARS ? `${digest.slice(0, DIGEST_MAX_CHARS)}\u2026` : digest;
978
+ }
979
+ function userText(message) {
980
+ const parts = message?.parts ?? [];
981
+ const texts = parts.filter((p) => p?.type === "text" && typeof p.text === "string").map((p) => p.text);
982
+ return texts.join(" ").trim();
983
+ }
984
+ function assistantText(message) {
985
+ return userText(message);
986
+ }
987
+ function digestKey(messages, turn) {
988
+ const first = messages[turn.start];
989
+ const id = first?.info?.id ?? "noid";
990
+ let shape = "";
991
+ for (let i = turn.start; i < turn.end && i < messages.length; i++) {
992
+ const message = messages[i];
993
+ shape += `${message?.info?.role ?? "?"}:${(message?.parts ?? []).length};`;
994
+ for (const part of message?.parts ?? []) {
995
+ if (!part || typeof part !== "object") continue;
996
+ if (part.type === "text" && typeof part.text === "string") {
997
+ shape += part.text.slice(0, 64);
998
+ } else if (part.type === "tool") {
999
+ shape += `${part.tool ?? "tool"}:${part.state?.status ?? "?"}:${(part.state?.output ?? "").length}`;
1000
+ }
1001
+ }
1002
+ }
1003
+ return `${id}:${hashString(shape)}`;
1004
+ }
1005
+ function findTopicBoundaries(messages, turns, driftThreshold) {
1006
+ const boundaries = [];
1007
+ let previous;
1008
+ for (let t = 0; t < turns.length; t++) {
1009
+ const text = userText(messages[turns[t].start]);
1010
+ const tokens = tokenize(text);
1011
+ if (tokens.size < 6) continue;
1012
+ if (previous && jaccard(tokens, previous) < driftThreshold) boundaries.push(t);
1013
+ previous = tokens;
1014
+ }
1015
+ return boundaries;
1016
+ }
1017
+ function estimateSlice(messages, start, end) {
1018
+ let tokens = 0;
1019
+ for (let i = start; i < end && i < messages.length; i++) {
1020
+ for (const part of messages[i]?.parts ?? []) {
1021
+ if (!part || typeof part !== "object") continue;
1022
+ if (typeof part.text === "string") tokens += estimateTokens(part.text);
1023
+ const state = part.state;
1024
+ if (!state) continue;
1025
+ if (state.time?.compacted) {
1026
+ tokens += 8;
1027
+ continue;
1028
+ }
1029
+ if (typeof state.output === "string") tokens += estimateTokens(state.output);
1030
+ if (state.input) tokens += estimateTokens(JSON.stringify(state.input));
1031
+ }
1032
+ }
1033
+ return tokens;
1034
+ }
1035
+
1036
+ // lib/dtc/engine.ts
1037
+ var DTC_DEFAULTS = {
1038
+ tailTurns: 4,
1039
+ lowWatermarkRatio: 0.5,
1040
+ targetRatio: 0.7,
1041
+ driftThreshold: 0.18,
1042
+ toolOutputKeepChars: 4e3
1043
+ };
1044
+ var C_ZONE_MAX_TURNS = 8;
1045
+ var M_ZONE_MAX_TURNS = 12;
1046
+ var M_TEXT_KEEP_CHARS = 200;
1047
+ var FOLDED_TEXT = " ";
1048
+ var NO_STATS = {
1049
+ messages: 0,
1050
+ userTurns: 0,
1051
+ level: 0,
1052
+ estimatedBefore: 0,
1053
+ estimatedAfter: 0,
1054
+ contextTokens: 0,
1055
+ foldedTools: 0,
1056
+ foldedTexts: 0,
1057
+ digestedTurns: 0,
1058
+ skipped: void 0
1059
+ };
1060
+ function segmentTurns(messages) {
1061
+ const turns = [];
1062
+ for (let i = 0; i < messages.length; i++) {
1063
+ const info = messages[i]?.info;
1064
+ if (info?.role !== "user") continue;
1065
+ if ((messages[i]?.parts ?? []).some((p) => p?.type === "compaction")) continue;
1066
+ turns.push({ start: i, end: messages.length });
1067
+ }
1068
+ for (let i = 0; i < turns.length - 1; i++) {
1069
+ turns[i].end = turns[i + 1].start;
1070
+ }
1071
+ return turns;
1072
+ }
1073
+ function transformMessages(messages, deps) {
1074
+ const stats = { ...NO_STATS };
1075
+ if (!Array.isArray(messages) || messages.length === 0) return stats;
1076
+ stats.messages = messages.length;
1077
+ const sessionID = findSessionID(messages);
1078
+ if (sessionID && deps.state.consumeCompactionSkip(sessionID)) {
1079
+ stats.skipped = "compaction";
1080
+ return stats;
1081
+ }
1082
+ const turns = segmentTurns(messages);
1083
+ stats.userTurns = turns.length;
1084
+ const { config, state } = deps;
1085
+ if (turns.length <= config.tailTurns) {
1086
+ stats.skipped = "short";
1087
+ return stats;
1088
+ }
1089
+ const contextTokens = sessionID ? state.contextTokens(sessionID) : void 0;
1090
+ if (!contextTokens) {
1091
+ stats.skipped = "unknown-context";
1092
+ return stats;
1093
+ }
1094
+ stats.contextTokens = contextTokens;
1095
+ const headTurns = turns.slice(0, turns.length - config.tailTurns);
1096
+ const estimatedBefore = estimateSlice(messages, 0, messages.length);
1097
+ stats.estimatedBefore = estimatedBefore;
1098
+ const lowWatermark = Math.floor(contextTokens * config.lowWatermarkRatio);
1099
+ const target = Math.floor(contextTokens * config.targetRatio);
1100
+ const minLevel = sessionID ? state.minLevel(sessionID) : 0;
1101
+ if (estimatedBefore <= lowWatermark && minLevel === 0) {
1102
+ stats.estimatedAfter = estimatedBefore;
1103
+ return stats;
1104
+ }
1105
+ const zones = computeZones(messages, headTurns, sessionID, state, config);
1106
+ const now = (deps.now ?? Date.now)();
1107
+ let level = Math.max(1, minLevel);
1108
+ applyLevel(messages, headTurns, zones, level, deps, stats, now);
1109
+ let estimatedAfter = estimateSlice(messages, 0, messages.length);
1110
+ while (estimatedAfter > target && level < 3) {
1111
+ level = level + 1;
1112
+ applyLevel(messages, headTurns, zones, level, deps, stats, now);
1113
+ estimatedAfter = estimateSlice(messages, 0, messages.length);
1114
+ }
1115
+ stats.level = level;
1116
+ stats.estimatedAfter = estimatedAfter;
1117
+ deps.logger?.debug("DTC transform", {
1118
+ sessionId: sessionID,
1119
+ messages: stats.messages,
1120
+ userTurns: stats.userTurns,
1121
+ level,
1122
+ estimatedBefore,
1123
+ estimatedAfter,
1124
+ contextTokens,
1125
+ foldedTools: stats.foldedTools,
1126
+ digestedTurns: stats.digestedTurns
1127
+ });
1128
+ return stats;
1129
+ }
1130
+ function computeZones(messages, headTurns, sessionID, state, config) {
1131
+ const boundaries = findTopicBoundaries(messages, headTurns, config.driftThreshold);
1132
+ const lastBoundary = boundaries.length > 0 ? boundaries[boundaries.length - 1] : 0;
1133
+ const secondLast = boundaries.length > 1 ? boundaries[boundaries.length - 2] : 0;
1134
+ let markStart = 0;
1135
+ const markAt = sessionID ? state.boundaryMark(sessionID) : void 0;
1136
+ if (markAt !== void 0) {
1137
+ for (let t = 0; t < headTurns.length; t++) {
1138
+ const created = messages[headTurns[t].start]?.info?.time?.created;
1139
+ if (typeof created === "number" && created <= markAt) markStart = t + 1;
1140
+ }
1141
+ }
1142
+ const cStart = Math.min(
1143
+ headTurns.length,
1144
+ Math.max(lastBoundary, markStart, headTurns.length - C_ZONE_MAX_TURNS)
1145
+ );
1146
+ const mStart = Math.min(cStart, Math.max(secondLast, cStart - M_ZONE_MAX_TURNS));
1147
+ return { mStart, cStart };
1148
+ }
1149
+ function applyLevel(messages, headTurns, zones, level, deps, stats, now) {
1150
+ const prev = level === 1 ? -1 : level - 1;
1151
+ if (level >= 1 && prev < 1) {
1152
+ for (let t = 0; t < zones.mStart; t++) {
1153
+ foldDistant(messages, headTurns[t], t + 1, deps, stats, now);
1154
+ }
1155
+ }
1156
+ if (level >= 2 && prev < 2) {
1157
+ for (let t = zones.mStart; t < zones.cStart; t++) {
1158
+ foldMiddle(messages, headTurns[t], stats, now);
1159
+ }
1160
+ }
1161
+ if (level >= 3 && prev < 3) {
1162
+ for (let t = zones.cStart; t < headTurns.length; t++) {
1163
+ foldCurrent(messages, headTurns[t], deps.config.toolOutputKeepChars, stats);
1164
+ }
1165
+ }
1166
+ }
1167
+ function foldDistant(messages, turn, ordinal, deps, stats, now) {
1168
+ const key = digestKey(messages, turn);
1169
+ let digest = deps.state.cachedDigest(key);
1170
+ if (digest === void 0) {
1171
+ digest = digestTurn(messages, turn, ordinal);
1172
+ deps.state.storeDigest(key, digest);
1173
+ }
1174
+ stats.digestedTurns++;
1175
+ let digestPlaced = false;
1176
+ for (let i = turn.start; i < turn.end; i++) {
1177
+ const message = messages[i];
1178
+ for (const part of message?.parts ?? []) {
1179
+ if (!part || typeof part !== "object") continue;
1180
+ if (foldToolPart(part, now, true)) {
1181
+ stats.foldedTools++;
1182
+ continue;
1183
+ }
1184
+ if (part.type === "reasoning" && typeof part.text === "string" && part.text.length > 0) {
1185
+ part.text = FOLDED_TEXT;
1186
+ continue;
1187
+ }
1188
+ if (part.type === "text" && typeof part.text === "string") {
1189
+ if (!digestPlaced && message?.info?.role === "user") {
1190
+ part.text = digest;
1191
+ digestPlaced = true;
1192
+ } else {
1193
+ part.text = FOLDED_TEXT;
1194
+ }
1195
+ stats.foldedTexts++;
1196
+ }
1197
+ }
1198
+ }
1199
+ }
1200
+ function foldMiddle(messages, turn, stats, now) {
1201
+ for (let i = turn.start; i < turn.end; i++) {
1202
+ for (const part of messages[i]?.parts ?? []) {
1203
+ if (!part || typeof part !== "object") continue;
1204
+ if (foldToolPart(part, now, false)) {
1205
+ stats.foldedTools++;
1206
+ continue;
1207
+ }
1208
+ if (part.type === "reasoning" && typeof part.text === "string" && part.text.length > 0) {
1209
+ part.text = FOLDED_TEXT;
1210
+ continue;
1211
+ }
1212
+ if (part.type === "text" && typeof part.text === "string" && part.text.length > M_TEXT_KEEP_CHARS) {
1213
+ part.text = firstLine(part.text, M_TEXT_KEEP_CHARS) || FOLDED_TEXT;
1214
+ stats.foldedTexts++;
1215
+ }
1216
+ }
1217
+ }
1218
+ }
1219
+ function foldCurrent(messages, turn, keepChars, stats) {
1220
+ for (let i = turn.start; i < turn.end; i++) {
1221
+ for (const part of messages[i]?.parts ?? []) {
1222
+ if (!part || typeof part !== "object" || part.type !== "tool") continue;
1223
+ const state = part.state;
1224
+ if (!state || state.status !== "completed") continue;
1225
+ const output = state.output;
1226
+ if (typeof output !== "string" || output.length <= keepChars) continue;
1227
+ state.output = truncateMiddle(
1228
+ output,
1229
+ keepChars,
1230
+ `[DCP \u5DF2\u6298\u53E0 ${output.length - keepChars} \u5B57\u7B26]`
1231
+ );
1232
+ stats.foldedTools++;
1233
+ }
1234
+ }
1235
+ }
1236
+ function foldToolPart(part, now, clearInput) {
1237
+ if (part.type !== "tool") return false;
1238
+ const state = part.state;
1239
+ if (!state || state.status !== "completed") return false;
1240
+ if (state.time && typeof state.time === "object") {
1241
+ state.time.compacted = now;
1242
+ } else {
1243
+ state.time = { compacted: now };
1244
+ }
1245
+ if (clearInput && state.input && typeof state.input === "object") {
1246
+ state.input = {};
1247
+ }
1248
+ return true;
1249
+ }
1250
+ function findSessionID(messages) {
1251
+ for (const message of messages) {
1252
+ const id = message?.info?.sessionID;
1253
+ if (typeof id === "string" && id) return id;
1254
+ }
1255
+ return void 0;
1256
+ }
1257
+
1018
1258
  // lib/config.ts
1019
- var DEFAULT_FAILURE_COOLDOWN_MS = 3e4;
1020
- var DEFAULT_AUTO_PRUNE = {
1259
+ var DEFAULT_DTC = {
1021
1260
  enabled: true,
1022
- minMessages: 8,
1023
- volumeThreshold: 30,
1024
- driftThreshold: 0.18,
1025
- idleGapMs: 30 * 6e4,
1026
- cooldownMs: 5 * 6e4
1261
+ ...DTC_DEFAULTS
1027
1262
  };
1028
1263
  var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
1029
1264
  "$schema",
@@ -1035,15 +1270,13 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
1035
1270
  "commands.enabled",
1036
1271
  "experimental",
1037
1272
  "experimental.customPrompts",
1038
- "summarize",
1039
- "summarize.failureCooldownMs",
1040
- "autoPrune",
1041
- "autoPrune.enabled",
1042
- "autoPrune.minMessages",
1043
- "autoPrune.volumeThreshold",
1044
- "autoPrune.driftThreshold",
1045
- "autoPrune.idleGapMs",
1046
- "autoPrune.cooldownMs",
1273
+ "dtc",
1274
+ "dtc.enabled",
1275
+ "dtc.tailTurns",
1276
+ "dtc.lowWatermarkRatio",
1277
+ "dtc.targetRatio",
1278
+ "dtc.driftThreshold",
1279
+ "dtc.toolOutputKeepChars",
1047
1280
  "tool",
1048
1281
  "tool.enabled"
1049
1282
  ]);
@@ -1088,7 +1321,21 @@ var DEPRECATED_CONFIG_KEYS = /* @__PURE__ */ new Set([
1088
1321
  "pruneNotificationType",
1089
1322
  "protectedFilePatterns",
1090
1323
  "commands.protectedTools",
1091
- "experimental.allowSubAgents"
1324
+ "experimental.allowSubAgents",
1325
+ "summarize",
1326
+ "summarize.failureCooldownMs",
1327
+ "autoPrune",
1328
+ "autoPrune.enabled",
1329
+ "autoPrune.signals",
1330
+ "autoPrune.signals.topicDrift",
1331
+ "autoPrune.signals.volume",
1332
+ "autoPrune.signals.idleGap",
1333
+ "autoPrune.autoContinue",
1334
+ "autoPrune.minMessages",
1335
+ "autoPrune.volumeThreshold",
1336
+ "autoPrune.driftThreshold",
1337
+ "autoPrune.idleGapMs",
1338
+ "autoPrune.cooldownMs"
1092
1339
  ]);
1093
1340
  function getConfigKeyPaths(obj, prefix = "") {
1094
1341
  const keys = [];
@@ -1130,11 +1377,7 @@ function validateConfigTypes(config) {
1130
1377
  const commands = config.commands;
1131
1378
  if (commands !== void 0) {
1132
1379
  if (typeof commands !== "object" || commands === null || Array.isArray(commands)) {
1133
- errors.push({
1134
- key: "commands",
1135
- expected: "object",
1136
- actual: typeof commands
1137
- });
1380
+ errors.push({ key: "commands", expected: "object", actual: typeof commands });
1138
1381
  } else if (commands.enabled !== void 0 && typeof commands.enabled !== "boolean") {
1139
1382
  errors.push({
1140
1383
  key: "commands.enabled",
@@ -1146,11 +1389,7 @@ function validateConfigTypes(config) {
1146
1389
  const experimental = config.experimental;
1147
1390
  if (experimental !== void 0) {
1148
1391
  if (typeof experimental !== "object" || experimental === null || Array.isArray(experimental)) {
1149
- errors.push({
1150
- key: "experimental",
1151
- expected: "object",
1152
- actual: typeof experimental
1153
- });
1392
+ errors.push({ key: "experimental", expected: "object", actual: typeof experimental });
1154
1393
  } else if (experimental.customPrompts !== void 0 && typeof experimental.customPrompts !== "boolean") {
1155
1394
  errors.push({
1156
1395
  key: "experimental.customPrompts",
@@ -1159,80 +1398,51 @@ function validateConfigTypes(config) {
1159
1398
  });
1160
1399
  }
1161
1400
  }
1162
- const summarize = config.summarize;
1163
- if (summarize !== void 0) {
1164
- if (typeof summarize !== "object" || summarize === null || Array.isArray(summarize)) {
1165
- errors.push({
1166
- key: "summarize",
1167
- expected: "object",
1168
- actual: typeof summarize
1169
- });
1401
+ const dtc = config.dtc;
1402
+ if (dtc !== void 0) {
1403
+ if (typeof dtc !== "object" || dtc === null || Array.isArray(dtc)) {
1404
+ errors.push({ key: "dtc", expected: "object", actual: typeof dtc });
1170
1405
  } else {
1171
- if (summarize.failureCooldownMs !== void 0 && (typeof summarize.failureCooldownMs !== "number" || !Number.isFinite(summarize.failureCooldownMs) || summarize.failureCooldownMs < 0)) {
1172
- errors.push({
1173
- key: "summarize.failureCooldownMs",
1174
- expected: "non-negative finite number",
1175
- actual: JSON.stringify(summarize.failureCooldownMs)
1176
- });
1406
+ if (dtc.enabled !== void 0 && typeof dtc.enabled !== "boolean") {
1407
+ errors.push({ key: "dtc.enabled", expected: "boolean", actual: typeof dtc.enabled });
1177
1408
  }
1178
- }
1179
- }
1180
- const autoPrune = config.autoPrune;
1181
- if (autoPrune !== void 0) {
1182
- if (typeof autoPrune !== "object" || autoPrune === null || Array.isArray(autoPrune)) {
1183
- errors.push({
1184
- key: "autoPrune",
1185
- expected: "object",
1186
- actual: typeof autoPrune
1187
- });
1188
- } else {
1189
1409
  const numericKeys = [
1190
- ["minMessages", 1, Number.POSITIVE_INFINITY],
1191
- ["volumeThreshold", 2, Number.POSITIVE_INFINITY],
1410
+ ["tailTurns", 0, Number.POSITIVE_INFINITY],
1411
+ ["lowWatermarkRatio", 0, 1],
1412
+ ["targetRatio", 0, 1],
1192
1413
  ["driftThreshold", 0, 1],
1193
- ["idleGapMs", 0, Number.POSITIVE_INFINITY],
1194
- ["cooldownMs", 0, Number.POSITIVE_INFINITY]
1414
+ ["toolOutputKeepChars", 200, Number.POSITIVE_INFINITY]
1195
1415
  ];
1196
1416
  for (const [key, min, max] of numericKeys) {
1197
- const value = autoPrune[key];
1417
+ const value = dtc[key];
1198
1418
  if (value !== void 0 && (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max)) {
1199
1419
  errors.push({
1200
- key: `autoPrune.${key}`,
1420
+ key: `dtc.${key}`,
1201
1421
  expected: `number in [${min}, ${max === Number.POSITIVE_INFINITY ? "\u221E" : max}]`,
1202
1422
  actual: JSON.stringify(value)
1203
1423
  });
1204
1424
  }
1205
1425
  }
1206
- for (const key of ["enabled"]) {
1207
- const value = autoPrune[key];
1208
- if (value !== void 0 && typeof value !== "boolean") {
1209
- errors.push({
1210
- key: `autoPrune.${key}`,
1211
- expected: "boolean",
1212
- actual: typeof value
1213
- });
1214
- }
1215
- }
1216
1426
  }
1217
1427
  }
1218
1428
  const tool2 = config.tool;
1219
1429
  if (tool2 !== void 0) {
1220
1430
  if (typeof tool2 !== "object" || tool2 === null || Array.isArray(tool2)) {
1221
- errors.push({
1222
- key: "tool",
1223
- expected: "object",
1224
- actual: typeof tool2
1225
- });
1431
+ errors.push({ key: "tool", expected: "object", actual: typeof tool2 });
1226
1432
  } else if (tool2.enabled !== void 0 && typeof tool2.enabled !== "boolean") {
1227
- errors.push({
1228
- key: "tool.enabled",
1229
- expected: "boolean",
1230
- actual: typeof tool2.enabled
1231
- });
1433
+ errors.push({ key: "tool.enabled", expected: "boolean", actual: typeof tool2.enabled });
1232
1434
  }
1233
1435
  }
1234
1436
  return errors;
1235
1437
  }
1438
+ function legacyDriftThreshold(configData) {
1439
+ const autoPrune = configData.autoPrune;
1440
+ if (autoPrune === null || typeof autoPrune !== "object" || Array.isArray(autoPrune)) {
1441
+ return void 0;
1442
+ }
1443
+ const value = autoPrune.driftThreshold;
1444
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1 ? value : void 0;
1445
+ }
1236
1446
  function showConfigWarnings(ctx, configPath, configData, isProject) {
1237
1447
  const invalidKeys = getInvalidConfigKeys(configData);
1238
1448
  const deprecatedKeys = getDeprecatedConfigKeys(configData);
@@ -1246,7 +1456,7 @@ function showConfigWarnings(ctx, configPath, configData, isProject) {
1246
1456
  const keyList = deprecatedKeys.slice(0, 3).join(", ");
1247
1457
  const suffix = deprecatedKeys.length > 3 ? ` (+${deprecatedKeys.length - 3} more)` : "";
1248
1458
  messages.push(
1249
- `Removed legacy compression keys are ignored: ${keyList}${suffix}. Pruning now runs through OpenCode's native compaction.`
1459
+ `Removed legacy keys are ignored: ${keyList}${suffix}. Compression now runs as dynamic request-time folding (dtc.*).`
1250
1460
  );
1251
1461
  }
1252
1462
  if (invalidKeys.length > 0) {
@@ -1288,10 +1498,7 @@ var defaultConfig = {
1288
1498
  experimental: {
1289
1499
  customPrompts: false
1290
1500
  },
1291
- summarize: {
1292
- failureCooldownMs: DEFAULT_FAILURE_COOLDOWN_MS
1293
- },
1294
- autoPrune: { ...DEFAULT_AUTO_PRUNE },
1501
+ dtc: { ...DEFAULT_DTC },
1295
1502
  tool: {
1296
1503
  enabled: true
1297
1504
  }
@@ -1377,26 +1584,19 @@ function mergeExperimental(base, override) {
1377
1584
  customPrompts: typeof override.customPrompts === "boolean" ? override.customPrompts : base.customPrompts
1378
1585
  };
1379
1586
  }
1380
- function mergeSummarize(base, override) {
1381
- if (!override) {
1382
- return base;
1383
- }
1384
- return {
1385
- failureCooldownMs: typeof override.failureCooldownMs === "number" && Number.isFinite(override.failureCooldownMs) && override.failureCooldownMs >= 0 ? override.failureCooldownMs : base.failureCooldownMs
1386
- };
1387
- }
1388
- function mergeAutoPrune(base, override) {
1587
+ function mergeDtc(base, override, legacyDrift) {
1588
+ const driftFallback = legacyDrift ?? base.driftThreshold;
1389
1589
  if (!override || typeof override !== "object" || Array.isArray(override)) {
1390
- return base;
1590
+ return { ...base, driftThreshold: driftFallback };
1391
1591
  }
1392
- const number = (key, min, max = Number.POSITIVE_INFINITY) => typeof override[key] === "number" && Number.isFinite(override[key]) && override[key] >= min && override[key] <= max ? override[key] : base[key];
1592
+ const number = (key, min, max) => typeof override[key] === "number" && Number.isFinite(override[key]) && override[key] >= min && override[key] <= max ? override[key] : base[key];
1393
1593
  return {
1394
1594
  enabled: typeof override.enabled === "boolean" ? override.enabled : base.enabled,
1395
- minMessages: number("minMessages", 1),
1396
- volumeThreshold: number("volumeThreshold", 2),
1397
- driftThreshold: number("driftThreshold", 0, 1),
1398
- idleGapMs: number("idleGapMs", 0),
1399
- cooldownMs: number("cooldownMs", 0)
1595
+ tailTurns: number("tailTurns", 0, Number.POSITIVE_INFINITY),
1596
+ lowWatermarkRatio: number("lowWatermarkRatio", 0, 1),
1597
+ targetRatio: number("targetRatio", 0, 1),
1598
+ driftThreshold: typeof override.driftThreshold === "number" && Number.isFinite(override.driftThreshold) && override.driftThreshold >= 0 && override.driftThreshold <= 1 ? override.driftThreshold : driftFallback,
1599
+ toolOutputKeepChars: number("toolOutputKeepChars", 200, Number.POSITIVE_INFINITY)
1400
1600
  };
1401
1601
  }
1402
1602
  function mergeTool(base, override) {
@@ -1412,8 +1612,7 @@ function deepCloneConfig(config) {
1412
1612
  ...config,
1413
1613
  commands: { ...config.commands },
1414
1614
  experimental: { ...config.experimental },
1415
- summarize: { ...config.summarize },
1416
- autoPrune: { ...config.autoPrune },
1615
+ dtc: { ...config.dtc },
1417
1616
  tool: { ...config.tool }
1418
1617
  };
1419
1618
  }
@@ -1425,8 +1624,7 @@ function mergeLayer(config, data) {
1425
1624
  language: data.language === "zh" || data.language === "en" ? data.language : config.language,
1426
1625
  commands: mergeCommands(config.commands, data.commands),
1427
1626
  experimental: mergeExperimental(config.experimental, data.experimental),
1428
- summarize: mergeSummarize(config.summarize, data.summarize),
1429
- autoPrune: mergeAutoPrune(config.autoPrune, data.autoPrune),
1627
+ dtc: mergeDtc(config.dtc, data.dtc, legacyDriftThreshold(data)),
1430
1628
  tool: mergeTool(config.tool, data.tool)
1431
1629
  };
1432
1630
  }
@@ -1480,322 +1678,255 @@ Using previous/default values`
1480
1678
  return config;
1481
1679
  }
1482
1680
 
1483
- // lib/session-model.ts
1484
- function latestUserModel(messages) {
1485
- if (!Array.isArray(messages)) return null;
1486
- for (let index = messages.length - 1; index >= 0; index--) {
1487
- const info = messages[index]?.info;
1488
- if (info?.role !== "user") continue;
1489
- const providerID = info.model?.providerID;
1490
- const modelID = info.model?.modelID;
1491
- if (typeof providerID === "string" && typeof modelID === "string") {
1492
- return { providerID, modelID };
1493
- }
1681
+ // lib/dtc/state.ts
1682
+ var SESSION_LIMIT = 500;
1683
+ var DIGEST_LIMIT = 2e3;
1684
+ function lruSet(map, key, value, limit) {
1685
+ if (map.has(key)) map.delete(key);
1686
+ map.set(key, value);
1687
+ while (map.size > limit) {
1688
+ const oldest = map.keys().next().value;
1689
+ if (oldest === void 0) break;
1690
+ map.delete(oldest);
1494
1691
  }
1495
- return null;
1496
- }
1497
- async function resolveSessionModel(client, sessionID) {
1498
- try {
1499
- const response = await client.session.messages({ path: { id: sessionID } });
1500
- return latestUserModel(response.data ?? response);
1501
- } catch {
1502
- return null;
1503
- }
1504
- }
1505
-
1506
- // lib/prune-service.ts
1507
- function retrySeconds(retryAfterMs) {
1508
- return Math.ceil(retryAfterMs / 1e3);
1509
1692
  }
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);
1693
+ var DtcState = class {
1694
+ sessions = /* @__PURE__ */ new Map();
1695
+ digests = /* @__PURE__ */ new Map();
1696
+ observeContextLimit(sessionID, contextTokens) {
1697
+ if (!sessionID || !contextTokens || !Number.isFinite(contextTokens) || contextTokens <= 0) {
1545
1698
  return;
1546
1699
  }
1547
- if (type === "session.deleted") {
1548
- this.deferred.delete(sessionID);
1549
- this.activity.dropSession(sessionID);
1550
- }
1700
+ const state = this.session(sessionID);
1701
+ state.contextTokens = contextTokens;
1702
+ lruSet(this.sessions, sessionID, state, SESSION_LIMIT);
1551
1703
  }
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;
1704
+ contextTokens(sessionID) {
1705
+ return this.sessions.get(sessionID)?.contextTokens;
1569
1706
  }
1570
- /** One admission check; `null` means the request may proceed. */
1571
- gate(request) {
1572
- return this.outcomeFor(request, this.admit(request));
1707
+ /** Called by the compacting hook; consumed by the very next transform. */
1708
+ armCompactionSkip(sessionID) {
1709
+ const state = this.session(sessionID);
1710
+ state.skipNextTransform = true;
1711
+ lruSet(this.sessions, sessionID, state, SESSION_LIMIT);
1573
1712
  }
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
- });
1713
+ consumeCompactionSkip(sessionID) {
1714
+ const state = this.sessions.get(sessionID);
1715
+ if (!state?.skipNextTransform) return false;
1716
+ state.skipNextTransform = false;
1717
+ return true;
1602
1718
  }
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" };
1719
+ /** `dcp_prune` / `/dcp fold`: mark a boundary now and deepen folding. */
1720
+ markBoundary(sessionID, at, minLevel = 2) {
1721
+ const state = this.session(sessionID);
1722
+ state.boundaryMarkAt = at;
1723
+ state.minLevel = Math.max(state.minLevel ?? 0, minLevel);
1724
+ lruSet(this.sessions, sessionID, state, SESSION_LIMIT);
1615
1725
  }
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" };
1726
+ boundaryMark(sessionID) {
1727
+ return this.sessions.get(sessionID)?.boundaryMarkAt;
1620
1728
  }
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
- }
1729
+ minLevel(sessionID) {
1730
+ return this.sessions.get(sessionID)?.minLevel ?? 0;
1731
+ }
1732
+ cachedDigest(key) {
1733
+ return this.digests.get(key);
1734
+ }
1735
+ storeDigest(key, digest) {
1736
+ lruSet(this.digests, key, digest, DIGEST_LIMIT);
1737
+ }
1738
+ dropSession(sessionID) {
1739
+ this.sessions.delete(sessionID);
1740
+ }
1741
+ /** Test/inspection surface. */
1742
+ stats() {
1743
+ return { sessions: this.sessions.size, digests: this.digests.size };
1744
+ }
1745
+ session(sessionID) {
1746
+ return this.sessions.get(sessionID) ?? {};
1639
1747
  }
1640
1748
  };
1641
1749
 
1750
+ // lib/session-events.ts
1751
+ function eventSessionID(properties) {
1752
+ const direct = properties?.sessionID;
1753
+ if (typeof direct === "string" && direct) return direct;
1754
+ const info = properties?.info?.id;
1755
+ if (typeof info === "string" && info) return info;
1756
+ return void 0;
1757
+ }
1758
+
1642
1759
  // lib/hooks.ts
1643
- function createSessionCompactingHandler(prompts, logger) {
1760
+ var DEFAULT_TAIL_TURNS = 4;
1761
+ var DEFAULT_PRESERVE_RECENT_TOKENS = 32e3;
1762
+ function extractPreviousCheckpoint(messages) {
1763
+ if (!Array.isArray(messages)) return void 0;
1764
+ for (let index = messages.length - 1; index >= 0; index--) {
1765
+ const message = messages[index];
1766
+ const info = message?.info;
1767
+ if (info?.role !== "assistant" || info.summary !== true) continue;
1768
+ const parts = Array.isArray(message?.parts) ? message.parts : [];
1769
+ const text = parts.filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text.trim()).filter(Boolean).join("\n\n").trim();
1770
+ if (text) return text;
1771
+ }
1772
+ return void 0;
1773
+ }
1774
+ async function fetchPreviousCheckpoint(client, sessionID) {
1775
+ if (!sessionID) return void 0;
1776
+ try {
1777
+ const response = await client.session.messages({ path: { id: sessionID } });
1778
+ return extractPreviousCheckpoint(response.data ?? response);
1779
+ } catch {
1780
+ return void 0;
1781
+ }
1782
+ }
1783
+ function createSessionCompactingHandler(deps) {
1644
1784
  return async (input, output) => {
1645
1785
  try {
1646
- prompts.reload();
1647
- const prompt = prompts.getRuntimePrompts().compaction;
1786
+ deps.state.armCompactionSkip(input.sessionID);
1787
+ deps.prompts.reload();
1788
+ const prompt = deps.prompts.getRuntimePrompts().compaction;
1789
+ const previous = await fetchPreviousCheckpoint(deps.client, input.sessionID);
1790
+ const withCheckpoint = previous ? `${prompt}
1791
+
1792
+ <previous-checkpoint>
1793
+ ${previous}
1794
+ </previous-checkpoint>` : prompt;
1648
1795
  if (!output.prompt) {
1649
- output.prompt = prompt;
1796
+ output.prompt = withCheckpoint;
1650
1797
  } else if (!output.context.includes(prompt)) {
1651
- output.context.push(prompt);
1798
+ output.context.push(withCheckpoint);
1652
1799
  }
1653
- logger.debug("Applied semantic pruning prompt", { sessionId: input.sessionID });
1654
- } catch (error) {
1655
- logger.warn("Failed to apply semantic pruning prompt; native compaction continues", {
1800
+ deps.logger.debug("Applied semantic pruning prompt", {
1656
1801
  sessionId: input.sessionID,
1657
- error: error instanceof Error ? error.message : String(error)
1802
+ carriedCheckpoint: Boolean(previous)
1658
1803
  });
1804
+ } catch (error) {
1805
+ deps.logger.warn(
1806
+ "Failed to apply semantic pruning prompt; native compaction continues",
1807
+ {
1808
+ sessionId: input.sessionID,
1809
+ error: error instanceof Error ? error.message : String(error)
1810
+ }
1811
+ );
1659
1812
  }
1660
1813
  };
1661
1814
  }
1662
- async function showToast(client, title, message, variant = "info") {
1663
- await client.tui.showToast({
1664
- body: { title, message, variant, duration: 5e3 }
1665
- }).catch(() => void 0);
1666
- }
1667
- function createChatMessageHandler(autoPruner) {
1815
+ function createChatParamsHandler(deps) {
1668
1816
  return async (input, _output) => {
1669
- const parts = _output?.parts ?? [];
1670
- autoPruner.observeUserMessage(input.sessionID, parts);
1671
- };
1672
- }
1673
- var SIGNAL_LABELS = {
1674
- "topic-drift": "\u8BDD\u9898\u53D8\u66F4",
1675
- volume: "\u6D88\u606F\u91CF\u8FBE\u5230\u9608\u503C",
1676
- "idle-gap": "\u957F\u65F6\u95F4\u4E2D\u65AD\u540E\u6062\u590D"
1677
- };
1678
- function createEventHandler(deps) {
1679
- async function triggerAutoPrune(sessionID, signals) {
1680
- const reason = signals.map((signal) => SIGNAL_LABELS[signal]).join("\u3001");
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
- );
1817
+ try {
1818
+ const context = input.model?.limit?.context;
1819
+ if (typeof context === "number" && Number.isFinite(context) && context > 0) {
1820
+ deps.state.observeContextLimit(input.sessionID, context);
1691
1821
  }
1692
- deps.logger.debug("Auto prune skipped", {
1693
- sessionId: sessionID,
1694
- status: result.status
1822
+ } catch (error) {
1823
+ deps.logger.debug("chat.params observation failed", {
1824
+ error: error instanceof Error ? error.message : String(error)
1695
1825
  });
1696
- return;
1697
1826
  }
1698
- deps.autoPruner.markPruned(sessionID);
1699
- if (result.status === "succeeded") {
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
- );
1708
- } else {
1709
- await showToast(
1710
- deps.client,
1711
- "DCP \u81EA\u52A8\u538B\u7F29",
1712
- `\u68C0\u6D4B\u5230${reason}\uFF0C\u4F46\u538B\u7F29\u5931\u8D25\uFF1B\u539F\u59CB\u4E0A\u4E0B\u6587\u4FDD\u6301\u4E0D\u53D8\u3002`,
1713
- "warning"
1714
- );
1827
+ };
1828
+ }
1829
+ function createTransformHandler(deps) {
1830
+ return async (_input, output) => {
1831
+ try {
1832
+ if (!Array.isArray(output?.messages) || output.messages.length === 0) return;
1833
+ transformMessages(output.messages, {
1834
+ state: deps.state,
1835
+ config: deps.config,
1836
+ logger: deps.logger
1837
+ });
1838
+ } catch (error) {
1839
+ deps.logger.warn("DTC transform failed; the request proceeds unfolded", {
1840
+ error: error instanceof Error ? error.message : String(error)
1841
+ });
1715
1842
  }
1716
- deps.logger.debug("Auto prune finished", { sessionId: sessionID, status: result.status });
1717
- }
1843
+ };
1844
+ }
1845
+ function createEventHandler(deps) {
1718
1846
  return async (input) => {
1719
- const event = input.event;
1720
- const sessionID = eventSessionID(event.properties);
1721
- if (!sessionID) return;
1722
1847
  try {
1723
- deps.prune.observeEvent(event.type, event.properties);
1724
- if (event.type === "session.idle") {
1725
- if (!deps.config.enabled) return;
1726
- const signals = deps.autoPruner.consumePending(sessionID);
1727
- if (signals) await triggerAutoPrune(sessionID, signals);
1728
- return;
1729
- }
1730
- if (event.type === "session.compacted") {
1731
- deps.autoPruner.markPruned(sessionID);
1732
- return;
1733
- }
1734
- if (event.type === "session.deleted") {
1735
- deps.autoPruner.dropSession(sessionID);
1736
- }
1848
+ if (input.event.type !== "session.deleted") return;
1849
+ const sessionID = eventSessionID(input.event.properties);
1850
+ if (sessionID) deps.state.dropSession(sessionID);
1737
1851
  } catch (error) {
1738
1852
  deps.logger.warn("Event handler failed", {
1739
- type: event.type,
1740
- sessionId: sessionID,
1853
+ type: input.event.type,
1741
1854
  error: error instanceof Error ? error.message : String(error)
1742
1855
  });
1743
1856
  }
1744
1857
  };
1745
1858
  }
1746
- function createCommandExecuteHandler(client, prune, logger) {
1859
+ async function showToast(client, title, message, variant = "info") {
1860
+ await client.tui.showToast({
1861
+ body: { title, message, variant, duration: 5e3 }
1862
+ }).catch(() => void 0);
1863
+ }
1864
+ function createCommandExecuteHandler(deps) {
1747
1865
  return async (input, _output) => {
1748
1866
  if (input.command !== "dcp") return;
1749
1867
  const subcommand = (input.arguments ?? "").trim().split(/\s+/, 1)[0]?.toLowerCase();
1750
- if (subcommand !== "summarize") {
1868
+ if (subcommand === "fold") {
1869
+ deps.state.markBoundary(input.sessionID, Date.now(), 3);
1751
1870
  await showToast(
1752
- client,
1753
- "DCP",
1754
- "Use /dcp summarize for semantic pruning, or OpenCode's native /compact command."
1755
- );
1756
- throw new Error("__DCP_HELP_HANDLED__");
1757
- }
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") {
1769
- await showToast(
1770
- client,
1771
- "DCP summarize",
1772
- "No session model is available yet.",
1773
- "warning"
1774
- );
1775
- throw new Error("__DCP_SUMMARIZE_NO_MODEL__");
1776
- }
1777
- if (result.status === "succeeded") {
1778
- await showToast(client, "DCP summarize", "Semantic pruning checkpoint created.");
1779
- } else if (result.status === "cooldown") {
1780
- await showToast(
1781
- client,
1782
- "DCP summarize",
1783
- `Previous attempt failed; retry in ${retrySeconds(result.retryAfterMs)}s.`,
1784
- "warning"
1785
- );
1786
- } else {
1787
- await showToast(
1788
- client,
1789
- "DCP summarize",
1790
- "Native compaction failed; the original context was kept.",
1791
- "error"
1871
+ deps.client,
1872
+ "DCP fold",
1873
+ "\u5DF2\u6807\u8BB0\u8BDD\u9898\u8FB9\u754C\u5E76\u52A0\u6DF1\u6298\u53E0\uFF1B\u4E0B\u4E00\u6B21\u6A21\u578B\u8BF7\u6C42\u8D77\u751F\u6548\uFF0C\u4F1A\u8BDD\u4E0D\u4E2D\u65AD\u3002"
1792
1874
  );
1875
+ deps.logger.debug("Handled DCP fold command", { sessionId: input.sessionID });
1876
+ throw new Error("__DCP_FOLD_HANDLED__");
1877
+ }
1878
+ if (subcommand === "status") {
1879
+ const message = await buildStatusMessage(deps, input.sessionID);
1880
+ await showToast(deps.client, "DCP status", message);
1881
+ throw new Error("__DCP_STATUS_HANDLED__");
1882
+ }
1883
+ await showToast(
1884
+ deps.client,
1885
+ "DCP",
1886
+ "\u7528\u6CD5\uFF1A/dcp fold\uFF08\u7ACB\u5373\u6DF1\u6298\u53E0\uFF09\u6216 /dcp status\uFF08\u67E5\u770B\u5206\u533A\u72B6\u6001\uFF09\u3002\u538B\u7F29\u7531\u52A8\u6001\u5206\u7EA7\u5F15\u64CE\u5728\u6BCF\u6B21\u8BF7\u6C42\u65F6\u81EA\u52A8\u5B8C\u6210\u3002"
1887
+ );
1888
+ throw new Error("__DCP_HELP_HANDLED__");
1889
+ };
1890
+ }
1891
+ async function buildStatusMessage(deps, sessionID) {
1892
+ try {
1893
+ const response = await deps.client.session.messages({ path: { id: sessionID } });
1894
+ const data = response.data ?? response;
1895
+ const messages = Array.isArray(data) ? data : [];
1896
+ const turns = segmentTurns(messages);
1897
+ const estimated = estimateSlice(messages, 0, messages.length);
1898
+ const context = deps.state.contextTokens(sessionID);
1899
+ const tail = Math.min(deps.config.tailTurns, turns.length);
1900
+ const lines = [
1901
+ `\u6D88\u606F ${messages.length} \u6761 / \u5BF9\u8BDD\u8F6E ${turns.length}\uFF08\u5C3E\u90E8\u4FDD\u62A4 ${tail} \u8F6E\uFF09`,
1902
+ `\u4F30\u7B97 ${estimated.toLocaleString()} tokens` + (context ? ` / \u4E0A\u4E0B\u6587\u7A97\u53E3 ${context.toLocaleString()}` : "\uFF08\u7A97\u53E3\u672A\u77E5\uFF0C\u6682\u672A\u6298\u53E0\uFF09"),
1903
+ `\u624B\u52A8\u964D\u7EA7\u6863\u4F4D\uFF1A${deps.state.minLevel(sessionID)}`
1904
+ ];
1905
+ return lines.join("\n");
1906
+ } catch {
1907
+ return "\u65E0\u6CD5\u8BFB\u53D6\u4F1A\u8BDD\u72B6\u6001\u3002";
1908
+ }
1909
+ }
1910
+ function createConfigHandler(config, logger) {
1911
+ return async (opencodeConfig) => {
1912
+ const compaction = opencodeConfig.compaction ??= {};
1913
+ if (compaction.tail_turns === void 0) {
1914
+ compaction.tail_turns = DEFAULT_TAIL_TURNS;
1915
+ }
1916
+ if (compaction.preserve_recent_tokens === void 0) {
1917
+ compaction.preserve_recent_tokens = DEFAULT_PRESERVE_RECENT_TOKENS;
1918
+ }
1919
+ if (config.commands.enabled) {
1920
+ opencodeConfig.command ??= {};
1921
+ opencodeConfig.command.dcp = {
1922
+ template: "",
1923
+ description: "Dynamic context pruning: /dcp fold | /dcp status"
1924
+ };
1793
1925
  }
1794
- logger.debug("Handled DCP summarize command", {
1795
- sessionId: input.sessionID,
1796
- status: result.status
1926
+ logger.debug("Applied host config defaults", {
1927
+ tailTurns: compaction.tail_turns,
1928
+ preserveRecentTokens: compaction.preserve_recent_tokens
1797
1929
  });
1798
- throw new Error("__DCP_SUMMARIZE_HANDLED__");
1799
1930
  };
1800
1931
  }
1801
1932
 
@@ -1999,15 +2130,16 @@ import { homedir as homedir3 } from "os";
1999
2130
  import { dirname as dirname2, join as join3 } from "path";
2000
2131
 
2001
2132
  // lib/prompts/compaction.ts
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
2133
+ 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\u68C0\u67E5\u70B9\u4E4B\u540E\u76F4\u63A5\u4FDD\u7559\u6700\u8FD1\u82E5\u5E72\u8F6E\u5B8C\u6574\u5BF9\u8BDD\uFF08\u4E0D\u7ECF\u538B\u7F29\uFF0C\u8303\u56F4\u7531\u5BBF\u4E3B\u914D\u7F6E\u51B3\u5B9A\uFF09\uFF0C\u8FD9\u90E8\u5206\u5C3E\u90E8\u4E0D\u5C5E\u4E8E\u538B\u7F29\u5BF9\u8C61\uFF0C\u4E0D\u8981\u590D\u8FF0\u3001\u603B\u7ED3\u6216\u6539\u5199\u5B83\u4EEC\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
2003
2134
 
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
2135
+ \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\u5206\u5C42\u89C4\u5219\u538B\u7F29\uFF1A
2005
2136
 
2006
- 1. \u5982\u679C\u8F93\u5165\u542B\u6709\u4E0A\u4E00\u4EFD\u68C0\u67E5\u70B9\uFF0C\u628A\u4ECD\u6709\u6548\u7684\u4FE1\u606F\u5408\u5E76\u8FDB\u65B0\u68C0\u67E5\u70B9\uFF1B\u4E0D\u8981\u5D4C\u5957\u3001\u5F15\u7528\u6216\u91CD\u590D\u65E7\u68C0\u67E5\u70B9\u3002
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
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
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
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
2137
+ 1. \u6EDA\u52A8\u5408\u5E76\uFF1A\u5982\u679C\u8F93\u5165\u672B\u5C3E\u542B\u6709 <previous-checkpoint> \u6807\u7B7E\u5305\u88F9\u7684\u4E0A\u4E00\u4EFD\u68C0\u67E5\u70B9\uFF0C\u628A\u4ECD\u6709\u6548\u7684\u4FE1\u606F\u5408\u5E76\u8FDB\u65B0\u68C0\u67E5\u70B9\uFF1B\u4E0D\u8981\u5D4C\u5957\u3001\u5F15\u7528\u6216\u9010\u5B57\u91CD\u590D\u65E7\u68C0\u67E5\u70B9\uFF0C\u5931\u6548\u5185\u5BB9\u76F4\u63A5\u4E22\u5F03\u3002\u6CA1\u6709\u8BE5\u6807\u7B7E\u65F6\u5FFD\u7565\u672C\u6761\u3002
2138
+ 2. \u8FDC\u8DDD\u79BB\u5185\u5BB9\uFF08\u65E9\u671F\u4E0E\u4E2D\u90E8\u5386\u53F2\uFF09\u91CD\u5EA6\u538B\u7F29\uFF1A\u6BCF\u4E2A\u4E3B\u9898\u53EA\u7559\u4E00\u53E5\u8BDD\u7ED3\u8BBA\uFF0C\u4E0D\u7559\u8FC7\u7A0B\u3001\u4E0D\u590D\u8FF0\u5BF9\u8BDD\u5F80\u6765\u3002
2139
+ 3. \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
2140
+ 4. \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
2141
+ 5. \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
2142
+ 6. \u8FD1\u8DDD\u79BB\u5185\u5BB9\uFF08\u81EA\u4E0A\u4E00\u4EFD\u68C0\u67E5\u70B9\u4EE5\u6765\u7684\u65B0\u5185\u5BB9\uFF0C\u4E14\u4E0D\u5C5E\u4E8E\u5BBF\u4E3B\u4FDD\u7559\u7684\u5C3E\u90E8\uFF09\u8F7B\u5EA6\u538B\u7F29\uFF1A\u5F53\u524D\u4EFB\u52A1\u3001\u76EE\u6807\u3001\u72B6\u6001\u4E0E\u8FDB\u884C\u4E2D\u7684\u60C5\u51B5\u505A\u6C47\u603B\u4F46\u4E0D\u4E22\u7EC6\u8282\u2014\u2014\u4FDD\u7559\u7EE7\u7EED\u5DE5\u4F5C\u6240\u9700\u7684\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\uFF0C\u53EA\u6298\u53E0\u91CD\u590D\u4E0E\u5DF2\u5931\u6548\u7684\u5185\u5BB9\u3002
2011
2143
 
2012
2144
  \u4F7F\u7528\u4EE5\u4E0B\u56FA\u5B9A\u7ED3\u6784\uFF0C\u7701\u7565\u786E\u5B9E\u4E3A\u7A7A\u7684\u6761\u76EE\uFF1A
2013
2145
 
@@ -2024,15 +2156,16 @@ var COMPACTION = `\u4F60\u6B63\u5728\u751F\u6210\u5F53\u524D\u4F1A\u8BDD\u552F\u
2024
2156
  \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
2025
2157
 
2026
2158
  \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.
2159
+ 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 the most recent conversation turns directly after the checkpoint, uncompacted (their range is a host setting). That tail is outside the compaction scope: do not restate, summarize, or rewrite 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
2160
 
2029
- This is not a chat-log summary but a semantic pruning result one can resume working from directly. Compress by these rules:
2161
+ This is not a chat-log summary but a semantic pruning result one can resume working from directly. Compress by these tiered rules:
2030
2162
 
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.
2163
+ 1. Rolling merge: if the input ends with a <previous-checkpoint> block containing the previous checkpoint, merge its still-valid information into the new checkpoint; do not nest, quote, or repeat it verbatim, and drop stale content outright. Ignore this rule when the tag is absent.
2164
+ 2. Distant content (early and middle history) is compressed heavily: one concluding sentence per topic, no process detail, no dialogue restatement.
2165
+ 3. Remove irrelevant chitchat, conversations about other projects or repositories, repeated explanations, and approaches that were overturned and no longer carry diagnostic value.
2166
+ 4. 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.
2167
+ 5. 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.
2168
+ 6. Recent content (everything generated since the previous checkpoint, excluding the host-retained tail) is compressed lightly: summarize the current task, goals, status, and in-progress work without losing detail\u2014keep the file paths, interfaces, commands, test results, error facts, and still-valid decisions with rationale needed to continue, folding only duplicates and invalidated content.
2036
2169
 
2037
2170
  Use the following fixed structure, omitting sections that are truly empty:
2038
2171
 
@@ -2141,122 +2274,29 @@ var PromptStore = class {
2141
2274
  // lib/prune-tool.ts
2142
2275
  import { tool } from "@opencode-ai/plugin";
2143
2276
  var PRUNE_TOOL_NAME = "dcp_prune";
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
2277
+ var PRUNE_TOOL_DESCRIPTION = `\u6807\u8BB0\u8BDD\u9898\u8FB9\u754C\u5E76\u52A0\u6DF1\u672C\u4F1A\u8BDD\u7684\u52A8\u6001\u4E0A\u4E0B\u6587\u6298\u53E0\u3002\u538B\u7F29\u672C\u8EAB\u7531 DCP \u5728\u6BCF\u6B21\u6A21\u578B\u8BF7\u6C42\u65F6\u81EA\u52A8\u5206\u7EA7\u5B8C\u6210\uFF08\u8FDC\u8DDD\u79BB\u91CD\u5EA6\u6298\u53E0\u3001\u5F53\u524D\u4EFB\u52A1\u8F7B\u5EA6\u6298\u53E0\u3001\u6700\u8FD1\u6570\u8F6E\u539F\u6837\u4FDD\u7559\uFF09\uFF0C\u672C\u5DE5\u5177\u53EA\u8C03\u6574\u6298\u53E0\u7B56\u7565\uFF0C\u4E0D\u6267\u884C\u4EFB\u4F55\u4F1A\u8BDD\u64CD\u4F5C\u3002
2145
2278
 
2146
2279
  \u4EC5\u5728\u8FD9\u4E9B\u60C5\u51B5\u4E0B\u8C03\u7528\uFF1A
2147
2280
  - \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
2148
2281
  - \u7528\u6237\u660E\u786E\u8981\u6C42\u538B\u7F29\u4E0A\u4E0B\u6587\u3002
2149
2282
 
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`;
2283
+ \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\u77AC\u65F6\u5B8C\u6210\uFF0C\u7EDD\u4E0D\u6253\u65AD\u5F53\u524D\u5DE5\u4F5C\uFF1A\u65E7\u4EFB\u52A1\u5185\u5BB9\u4ECE\u4E0B\u4E00\u6B21\u6A21\u578B\u8BF7\u6C42\u8D77\u88AB\u6298\u53E0\u4E3A\u7ED3\u6784\u5316\u6458\u8981\uFF0C\u6700\u8FD1\u5BF9\u8BDD\u4E0E\u5F53\u524D\u4EFB\u52A1\u7EC6\u8282\u4E0D\u53D7\u5F71\u54CD\u3002\u4E0D\u8981\u4E3A\u540C\u4E00\u8BDD\u9898\u53CD\u590D\u8C03\u7528\u3002`;
2151
2284
  function createPruneTool(deps) {
2152
2285
  return tool({
2153
2286
  description: PRUNE_TOOL_DESCRIPTION,
2154
2287
  args: {},
2155
2288
  execute: async (_args, context) => {
2156
2289
  const sessionID = context.sessionID;
2157
- const result = await deps.prune.request({ sessionID, onBusy: "defer" });
2158
- if (result.status === "succeeded") {
2159
- deps.logger.debug("Prune tool triggered native compaction", {
2160
- sessionId: sessionID
2161
- });
2162
- return "DCP\uFF1A\u8BED\u4E49\u538B\u7F29\u5B8C\u6210\uFF0C\u65E7\u4E0A\u4E0B\u6587\u5DF2\u6298\u53E0\u4E3A\u65B0\u68C0\u67E5\u70B9\u3002";
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
- }
2170
- if (result.status === "cooldown") {
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";
2175
- }
2176
- return `DCP\uFF1A\u538B\u7F29\u5931\u8D25\uFF08${result.error}\uFF09\uFF0C\u539F\u59CB\u4E0A\u4E0B\u6587\u4FDD\u6301\u4E0D\u53D8\u3002`;
2290
+ const now = (deps.now ?? Date.now)();
2291
+ deps.state.markBoundary(sessionID, now, 2);
2292
+ deps.logger.debug("Prune tool marked a topic boundary", {
2293
+ sessionId: sessionID
2294
+ });
2295
+ return "DCP\uFF1A\u5DF2\u6807\u8BB0\u8BDD\u9898\u8FB9\u754C\u5E76\u52A0\u6DF1\u672C\u4F1A\u8BDD\u6298\u53E0\u2014\u2014\u65E7\u4EFB\u52A1\u5185\u5BB9\u5C06\u4ECE\u4E0B\u4E00\u6B21\u6A21\u578B\u8BF7\u6C42\u8D77\u5206\u7EA7\u6298\u53E0\u4E3A\u7ED3\u6784\u5316\u6458\u8981\uFF1B\u6700\u8FD1\u5BF9\u8BDD\u4E0E\u5F53\u524D\u4EFB\u52A1\u7EC6\u8282\u4E0D\u53D7\u5F71\u54CD\uFF0C\u4F1A\u8BDD\u672A\u4E2D\u65AD\u3002";
2177
2296
  }
2178
2297
  });
2179
2298
  }
2180
2299
 
2181
- // lib/summarize.ts
2182
- function errorMessage(error) {
2183
- if (error instanceof Error) return error.message;
2184
- if (typeof error === "string") return error;
2185
- try {
2186
- return JSON.stringify(error);
2187
- } catch {
2188
- return "Unknown native compaction error";
2189
- }
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
- }
2201
- var SummarizeCoordinator = class {
2202
- constructor(client, logger, options) {
2203
- this.client = client;
2204
- this.logger = logger;
2205
- this.options = options;
2206
- this.now = options.now ?? Date.now;
2207
- }
2208
- inFlight = /* @__PURE__ */ new Map();
2209
- failedAt = /* @__PURE__ */ new Map();
2210
- now;
2211
- summarize(request) {
2212
- const active = this.inFlight.get(request.sessionID);
2213
- if (active) return active;
2214
- const failedAt = this.failedAt.get(request.sessionID);
2215
- if (failedAt !== void 0) {
2216
- const retryAfterMs = this.options.failureCooldownMs - (this.now() - failedAt);
2217
- if (retryAfterMs > 0) {
2218
- return Promise.resolve({ status: "cooldown", retryAfterMs });
2219
- }
2220
- this.failedAt.delete(request.sessionID);
2221
- }
2222
- const promise = this.invokeNative(request).finally(() => {
2223
- if (this.inFlight.get(request.sessionID) === promise) {
2224
- this.inFlight.delete(request.sessionID);
2225
- }
2226
- });
2227
- this.inFlight.set(request.sessionID, promise);
2228
- return promise;
2229
- }
2230
- async invokeNative(request) {
2231
- try {
2232
- const response = await this.client.session.summarize({
2233
- path: { id: request.sessionID },
2234
- body: request.model
2235
- });
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"));
2242
- }
2243
- this.failedAt.delete(request.sessionID);
2244
- return { status: "succeeded" };
2245
- } catch (error) {
2246
- if (isBusyRejection(error)) {
2247
- return { status: "rejected", reason: "busy" };
2248
- }
2249
- this.failedAt.set(request.sessionID, this.now());
2250
- const message = errorMessage(error);
2251
- await this.logger.warn("Native summarize failed; context remains unchanged", {
2252
- sessionId: request.sessionID,
2253
- error: message
2254
- });
2255
- return { status: "failed", error: message };
2256
- }
2257
- }
2258
- };
2259
-
2260
2300
  // lib/update.ts
2261
2301
  import { readFile } from "fs/promises";
2262
2302
  import { basename, dirname as dirname3, join as join4 } from "path";
@@ -2403,52 +2443,48 @@ var server = (async (ctx) => {
2403
2443
  config.experimental.customPrompts,
2404
2444
  config.language
2405
2445
  );
2406
- const summarize = new SummarizeCoordinator(ctx.client, logger, {
2407
- failureCooldownMs: config.summarize.failureCooldownMs
2408
- });
2409
- const prune = new PruneService({
2410
- client: ctx.client,
2411
- summarize,
2412
- activity: new SessionActivityTracker(),
2413
- logger
2414
- });
2415
- const autoPruner = new AutoPruner(config.autoPrune);
2446
+ const state = new DtcState();
2416
2447
  logger.info("DCP initialized", {
2417
2448
  commands: config.commands.enabled,
2418
- autoPrune: config.autoPrune.enabled,
2449
+ dtc: config.dtc.enabled,
2419
2450
  tool: config.tool.enabled,
2420
2451
  customPrompts: config.experimental.customPrompts
2421
2452
  });
2422
2453
  startAutoUpdate(ctx, config.autoUpdate);
2423
2454
  return {
2424
- "experimental.session.compacting": createSessionCompactingHandler(prompts, logger),
2425
- ...config.autoPrune.enabled && {
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) && {
2431
- event: createEventHandler({
2432
- client: ctx.client,
2433
- prune,
2434
- autoPruner,
2435
- config: config.autoPrune,
2455
+ "experimental.session.compacting": createSessionCompactingHandler({
2456
+ prompts,
2457
+ logger,
2458
+ client: ctx.client,
2459
+ state
2460
+ }),
2461
+ // THE compression surface: dynamic tiered folding on every model
2462
+ // request, plus the chat.params feed that teaches the engine each
2463
+ // session's context-window size.
2464
+ ...config.dtc.enabled && {
2465
+ "experimental.chat.messages.transform": createTransformHandler({
2466
+ state,
2467
+ config: config.dtc,
2436
2468
  logger
2437
- })
2469
+ }),
2470
+ "chat.params": createChatParamsHandler({ state, logger })
2438
2471
  },
2472
+ // Lifecycle cleanup for DTC session state (LRU-bounded regardless).
2473
+ event: createEventHandler({ state, logger }),
2439
2474
  ...config.tool.enabled && {
2440
- tool: { [PRUNE_TOOL_NAME]: createPruneTool({ prune, logger }) }
2475
+ tool: { [PRUNE_TOOL_NAME]: createPruneTool({ state, logger }) }
2441
2476
  },
2442
2477
  ...config.commands.enabled && {
2443
- "command.execute.before": createCommandExecuteHandler(ctx.client, prune, logger),
2444
- config: async (opencodeConfig) => {
2445
- opencodeConfig.command ??= {};
2446
- opencodeConfig.command.dcp = {
2447
- template: "",
2448
- description: "Run semantic context pruning with native compaction"
2449
- };
2450
- }
2451
- }
2478
+ "command.execute.before": createCommandExecuteHandler({
2479
+ client: ctx.client,
2480
+ state,
2481
+ config: config.dtc,
2482
+ logger
2483
+ })
2484
+ },
2485
+ // Always registered: besides the optional /dcp command it raises the
2486
+ // host's compaction tail protection to DCP's tiered defaults.
2487
+ config: createConfigHandler(config, logger)
2452
2488
  };
2453
2489
  });
2454
2490
  var index_default = server;