@lexwdex-org/opencode-dcp 3.5.0 → 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,118 +1,3 @@
1
- // lib/auto-prune.ts
2
- function tokenize(text) {
3
- const tokens = /* @__PURE__ */ new Set();
4
- for (const match of text.toLowerCase().matchAll(/[\p{L}\p{N}]+/gu)) {
5
- const word = match[0];
6
- if (/[\u4e00-\u9fff]/.test(word)) {
7
- if (word.length === 1) {
8
- tokens.add(word);
9
- continue;
10
- }
11
- for (let index = 0; index < word.length - 1; index++) {
12
- tokens.add(word.slice(index, index + 2));
13
- }
14
- } else {
15
- tokens.add(word);
16
- }
17
- }
18
- return tokens;
19
- }
20
- function jaccard(a, b) {
21
- if (a.size === 0 && b.size === 0) return 1;
22
- let intersection = 0;
23
- for (const token of a) {
24
- if (b.has(token)) intersection++;
25
- }
26
- return intersection / (a.size + b.size - intersection);
27
- }
28
- var WINDOW_SIZE = 4;
29
- var DRIFT_BASELINE = 3;
30
- var MIN_DRIFT_TOKENS = 6;
31
- function extractText(parts) {
32
- const texts = [];
33
- for (const part of parts) {
34
- if (part && typeof part === "object" && part.type === "text" && typeof part.text === "string") {
35
- texts.push(part.text);
36
- }
37
- }
38
- return texts.join(" ").trim();
39
- }
40
- var AutoPruner = class {
41
- constructor(config, now) {
42
- this.config = config;
43
- this.now = now ?? Date.now;
44
- }
45
- sessions = /* @__PURE__ */ new Map();
46
- now;
47
- observeUserMessage(sessionID, parts, at = this.now()) {
48
- const state = this.state(sessionID);
49
- const text = extractText(parts);
50
- const signals = this.evaluate(state, text, at);
51
- if (text) {
52
- state.window.push(text);
53
- if (state.window.length > WINDOW_SIZE) state.window.shift();
54
- }
55
- state.count += 1;
56
- state.lastAt = at;
57
- for (const signal of signals) {
58
- if (!state.pendingSignals.includes(signal)) state.pendingSignals.push(signal);
59
- }
60
- return { signals };
61
- }
62
- consumePending(sessionID, at = this.now()) {
63
- const state = this.sessions.get(sessionID);
64
- if (!state || state.pendingSignals.length === 0) return null;
65
- const signals = [...state.pendingSignals];
66
- state.pendingSignals = [];
67
- if (at - state.lastTriggerAt < this.config.cooldownMs) return null;
68
- state.lastTriggerAt = at;
69
- return signals;
70
- }
71
- markPruned(sessionID, at = this.now()) {
72
- const state = this.sessions.get(sessionID);
73
- if (!state) return;
74
- state.count = 0;
75
- state.window = [];
76
- state.pendingSignals = [];
77
- state.lastTriggerAt = at;
78
- }
79
- dropSession(sessionID) {
80
- this.sessions.delete(sessionID);
81
- }
82
- evaluate(state, text, at) {
83
- if (state.count + 1 < this.config.minMessages) return [];
84
- const signals = [];
85
- const enabled = this.config.signals;
86
- if (enabled.idleGap && state.count > 0 && at - state.lastAt >= this.config.idleGapMs) {
87
- signals.push("idle-gap");
88
- }
89
- if (enabled.topicDrift && state.count >= DRIFT_BASELINE && text) {
90
- const current = tokenize(text);
91
- if (current.size >= MIN_DRIFT_TOKENS) {
92
- let max = 0;
93
- for (let index = Math.max(0, state.window.length - DRIFT_BASELINE); index < state.window.length; index++) {
94
- max = Math.max(max, jaccard(current, tokenize(state.window[index])));
95
- }
96
- if (max < this.config.driftThreshold) signals.push("topic-drift");
97
- }
98
- }
99
- if (enabled.volume && state.count + 1 >= this.config.volumeThreshold) signals.push("volume");
100
- return signals;
101
- }
102
- state(sessionID) {
103
- let state = this.sessions.get(sessionID);
104
- if (!state) {
105
- state = { window: [], count: 0, lastAt: 0, pendingSignals: [], lastTriggerAt: 0 };
106
- this.sessions.set(sessionID, state);
107
- if (this.sessions.size > 200) {
108
- const oldest = this.sessions.keys().next().value;
109
- if (oldest !== void 0 && oldest !== sessionID) this.sessions.delete(oldest);
110
- }
111
- }
112
- return state;
113
- }
114
- };
115
-
116
1
  // lib/config.ts
117
2
  import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from "fs";
118
3
  import { join, dirname } from "path";
@@ -977,16 +862,403 @@ var ParseErrorCode;
977
862
  ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
978
863
  })(ParseErrorCode || (ParseErrorCode = {}));
979
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
+
980
1258
  // lib/config.ts
981
- var DEFAULT_FAILURE_COOLDOWN_MS = 3e4;
982
- var DEFAULT_AUTO_PRUNE = {
1259
+ var DEFAULT_DTC = {
983
1260
  enabled: true,
984
- signals: { topicDrift: true, volume: false, idleGap: false },
985
- minMessages: 8,
986
- volumeThreshold: 30,
987
- driftThreshold: 0.18,
988
- idleGapMs: 30 * 6e4,
989
- cooldownMs: 5 * 6e4
1261
+ ...DTC_DEFAULTS
990
1262
  };
991
1263
  var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
992
1264
  "$schema",
@@ -998,19 +1270,13 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
998
1270
  "commands.enabled",
999
1271
  "experimental",
1000
1272
  "experimental.customPrompts",
1001
- "summarize",
1002
- "summarize.failureCooldownMs",
1003
- "autoPrune",
1004
- "autoPrune.enabled",
1005
- "autoPrune.signals",
1006
- "autoPrune.signals.topicDrift",
1007
- "autoPrune.signals.volume",
1008
- "autoPrune.signals.idleGap",
1009
- "autoPrune.minMessages",
1010
- "autoPrune.volumeThreshold",
1011
- "autoPrune.driftThreshold",
1012
- "autoPrune.idleGapMs",
1013
- "autoPrune.cooldownMs",
1273
+ "dtc",
1274
+ "dtc.enabled",
1275
+ "dtc.tailTurns",
1276
+ "dtc.lowWatermarkRatio",
1277
+ "dtc.targetRatio",
1278
+ "dtc.driftThreshold",
1279
+ "dtc.toolOutputKeepChars",
1014
1280
  "tool",
1015
1281
  "tool.enabled"
1016
1282
  ]);
@@ -1055,7 +1321,21 @@ var DEPRECATED_CONFIG_KEYS = /* @__PURE__ */ new Set([
1055
1321
  "pruneNotificationType",
1056
1322
  "protectedFilePatterns",
1057
1323
  "commands.protectedTools",
1058
- "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"
1059
1339
  ]);
1060
1340
  function getConfigKeyPaths(obj, prefix = "") {
1061
1341
  const keys = [];
@@ -1097,11 +1377,7 @@ function validateConfigTypes(config) {
1097
1377
  const commands = config.commands;
1098
1378
  if (commands !== void 0) {
1099
1379
  if (typeof commands !== "object" || commands === null || Array.isArray(commands)) {
1100
- errors.push({
1101
- key: "commands",
1102
- expected: "object",
1103
- actual: typeof commands
1104
- });
1380
+ errors.push({ key: "commands", expected: "object", actual: typeof commands });
1105
1381
  } else if (commands.enabled !== void 0 && typeof commands.enabled !== "boolean") {
1106
1382
  errors.push({
1107
1383
  key: "commands.enabled",
@@ -1113,11 +1389,7 @@ function validateConfigTypes(config) {
1113
1389
  const experimental = config.experimental;
1114
1390
  if (experimental !== void 0) {
1115
1391
  if (typeof experimental !== "object" || experimental === null || Array.isArray(experimental)) {
1116
- errors.push({
1117
- key: "experimental",
1118
- expected: "object",
1119
- actual: typeof experimental
1120
- });
1392
+ errors.push({ key: "experimental", expected: "object", actual: typeof experimental });
1121
1393
  } else if (experimental.customPrompts !== void 0 && typeof experimental.customPrompts !== "boolean") {
1122
1394
  errors.push({
1123
1395
  key: "experimental.customPrompts",
@@ -1126,125 +1398,65 @@ function validateConfigTypes(config) {
1126
1398
  });
1127
1399
  }
1128
1400
  }
1129
- const summarize = config.summarize;
1130
- if (summarize !== void 0) {
1131
- if (typeof summarize !== "object" || summarize === null || Array.isArray(summarize)) {
1132
- errors.push({
1133
- key: "summarize",
1134
- expected: "object",
1135
- actual: typeof summarize
1136
- });
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 });
1137
1405
  } else {
1138
- if (summarize.failureCooldownMs !== void 0 && (typeof summarize.failureCooldownMs !== "number" || !Number.isFinite(summarize.failureCooldownMs) || summarize.failureCooldownMs < 0)) {
1139
- errors.push({
1140
- key: "summarize.failureCooldownMs",
1141
- expected: "non-negative finite number",
1142
- actual: JSON.stringify(summarize.failureCooldownMs)
1143
- });
1406
+ if (dtc.enabled !== void 0 && typeof dtc.enabled !== "boolean") {
1407
+ errors.push({ key: "dtc.enabled", expected: "boolean", actual: typeof dtc.enabled });
1144
1408
  }
1145
- }
1146
- }
1147
- const autoPrune = config.autoPrune;
1148
- if (autoPrune !== void 0) {
1149
- if (typeof autoPrune !== "object" || autoPrune === null || Array.isArray(autoPrune)) {
1150
- errors.push({
1151
- key: "autoPrune",
1152
- expected: "object",
1153
- actual: typeof autoPrune
1154
- });
1155
- } else {
1156
1409
  const numericKeys = [
1157
- ["minMessages", 1, Number.POSITIVE_INFINITY],
1158
- ["volumeThreshold", 2, Number.POSITIVE_INFINITY],
1410
+ ["tailTurns", 0, Number.POSITIVE_INFINITY],
1411
+ ["lowWatermarkRatio", 0, 1],
1412
+ ["targetRatio", 0, 1],
1159
1413
  ["driftThreshold", 0, 1],
1160
- ["idleGapMs", 0, Number.POSITIVE_INFINITY],
1161
- ["cooldownMs", 0, Number.POSITIVE_INFINITY]
1414
+ ["toolOutputKeepChars", 200, Number.POSITIVE_INFINITY]
1162
1415
  ];
1163
1416
  for (const [key, min, max] of numericKeys) {
1164
- const value = autoPrune[key];
1417
+ const value = dtc[key];
1165
1418
  if (value !== void 0 && (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max)) {
1166
1419
  errors.push({
1167
- key: `autoPrune.${key}`,
1420
+ key: `dtc.${key}`,
1168
1421
  expected: `number in [${min}, ${max === Number.POSITIVE_INFINITY ? "\u221E" : max}]`,
1169
1422
  actual: JSON.stringify(value)
1170
1423
  });
1171
1424
  }
1172
1425
  }
1173
- for (const key of ["enabled"]) {
1174
- const value = autoPrune[key];
1175
- if (value !== void 0 && typeof value !== "boolean") {
1176
- errors.push({
1177
- key: `autoPrune.${key}`,
1178
- expected: "boolean",
1179
- actual: typeof value
1180
- });
1181
- }
1182
- }
1183
- const signals = autoPrune.signals;
1184
- if (signals !== void 0) {
1185
- if (typeof signals !== "object" || signals === null || Array.isArray(signals)) {
1186
- errors.push({
1187
- key: "autoPrune.signals",
1188
- expected: "object",
1189
- actual: typeof signals
1190
- });
1191
- } else {
1192
- for (const key of ["topicDrift", "volume", "idleGap"]) {
1193
- const value = signals[key];
1194
- if (value !== void 0 && typeof value !== "boolean") {
1195
- errors.push({
1196
- key: `autoPrune.signals.${key}`,
1197
- expected: "boolean",
1198
- actual: typeof value
1199
- });
1200
- }
1201
- }
1202
- }
1203
- }
1204
1426
  }
1205
1427
  }
1206
1428
  const tool2 = config.tool;
1207
1429
  if (tool2 !== void 0) {
1208
1430
  if (typeof tool2 !== "object" || tool2 === null || Array.isArray(tool2)) {
1209
- errors.push({
1210
- key: "tool",
1211
- expected: "object",
1212
- actual: typeof tool2
1213
- });
1431
+ errors.push({ key: "tool", expected: "object", actual: typeof tool2 });
1214
1432
  } else if (tool2.enabled !== void 0 && typeof tool2.enabled !== "boolean") {
1215
- errors.push({
1216
- key: "tool.enabled",
1217
- expected: "boolean",
1218
- actual: typeof tool2.enabled
1219
- });
1433
+ errors.push({ key: "tool.enabled", expected: "boolean", actual: typeof tool2.enabled });
1220
1434
  }
1221
1435
  }
1222
1436
  return errors;
1223
1437
  }
1224
- function needsSignalsMigrationHint(configData) {
1438
+ function legacyDriftThreshold(configData) {
1225
1439
  const autoPrune = configData.autoPrune;
1226
- return autoPrune !== null && typeof autoPrune === "object" && !Array.isArray(autoPrune) && autoPrune.enabled === true && autoPrune.signals === void 0;
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;
1227
1445
  }
1228
1446
  function showConfigWarnings(ctx, configPath, configData, isProject) {
1229
1447
  const invalidKeys = getInvalidConfigKeys(configData);
1230
1448
  const deprecatedKeys = getDeprecatedConfigKeys(configData);
1231
1449
  const typeErrors = validateConfigTypes(configData);
1232
- const signalsHint = needsSignalsMigrationHint(configData);
1233
- if (!signalsHint && invalidKeys.length === 0 && deprecatedKeys.length === 0 && typeErrors.length === 0) {
1450
+ if (invalidKeys.length === 0 && deprecatedKeys.length === 0 && typeErrors.length === 0) {
1234
1451
  return;
1235
1452
  }
1236
1453
  const configType = isProject ? "project config" : "config";
1237
1454
  const messages = [];
1238
- if (signalsHint) {
1239
- messages.push(
1240
- "auto-prune signals `volume` and `idleGap` are now disabled by default; re-enable via autoPrune.signals.*"
1241
- );
1242
- }
1243
1455
  if (deprecatedKeys.length > 0) {
1244
1456
  const keyList = deprecatedKeys.slice(0, 3).join(", ");
1245
1457
  const suffix = deprecatedKeys.length > 3 ? ` (+${deprecatedKeys.length - 3} more)` : "";
1246
1458
  messages.push(
1247
- `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.*).`
1248
1460
  );
1249
1461
  }
1250
1462
  if (invalidKeys.length > 0) {
@@ -1286,10 +1498,7 @@ var defaultConfig = {
1286
1498
  experimental: {
1287
1499
  customPrompts: false
1288
1500
  },
1289
- summarize: {
1290
- failureCooldownMs: DEFAULT_FAILURE_COOLDOWN_MS
1291
- },
1292
- autoPrune: { ...DEFAULT_AUTO_PRUNE },
1501
+ dtc: { ...DEFAULT_DTC },
1293
1502
  tool: {
1294
1503
  enabled: true
1295
1504
  }
@@ -1375,33 +1584,19 @@ function mergeExperimental(base, override) {
1375
1584
  customPrompts: typeof override.customPrompts === "boolean" ? override.customPrompts : base.customPrompts
1376
1585
  };
1377
1586
  }
1378
- function mergeSummarize(base, override) {
1379
- if (!override) {
1380
- return base;
1381
- }
1382
- return {
1383
- failureCooldownMs: typeof override.failureCooldownMs === "number" && Number.isFinite(override.failureCooldownMs) && override.failureCooldownMs >= 0 ? override.failureCooldownMs : base.failureCooldownMs
1384
- };
1385
- }
1386
- function mergeAutoPrune(base, override) {
1587
+ function mergeDtc(base, override, legacyDrift) {
1588
+ const driftFallback = legacyDrift ?? base.driftThreshold;
1387
1589
  if (!override || typeof override !== "object" || Array.isArray(override)) {
1388
- return base;
1590
+ return { ...base, driftThreshold: driftFallback };
1389
1591
  }
1390
- 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];
1391
- const signalsOverride = override.signals;
1392
- const signals = signalsOverride && typeof signalsOverride === "object" && !Array.isArray(signalsOverride) ? {
1393
- topicDrift: typeof signalsOverride.topicDrift === "boolean" ? signalsOverride.topicDrift : base.signals.topicDrift,
1394
- volume: typeof signalsOverride.volume === "boolean" ? signalsOverride.volume : base.signals.volume,
1395
- idleGap: typeof signalsOverride.idleGap === "boolean" ? signalsOverride.idleGap : base.signals.idleGap
1396
- } : base.signals;
1592
+ const number = (key, min, max) => typeof override[key] === "number" && Number.isFinite(override[key]) && override[key] >= min && override[key] <= max ? override[key] : base[key];
1397
1593
  return {
1398
1594
  enabled: typeof override.enabled === "boolean" ? override.enabled : base.enabled,
1399
- signals,
1400
- minMessages: number("minMessages", 1),
1401
- volumeThreshold: number("volumeThreshold", 2),
1402
- driftThreshold: number("driftThreshold", 0, 1),
1403
- idleGapMs: number("idleGapMs", 0),
1404
- 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)
1405
1600
  };
1406
1601
  }
1407
1602
  function mergeTool(base, override) {
@@ -1417,8 +1612,7 @@ function deepCloneConfig(config) {
1417
1612
  ...config,
1418
1613
  commands: { ...config.commands },
1419
1614
  experimental: { ...config.experimental },
1420
- summarize: { ...config.summarize },
1421
- autoPrune: { ...config.autoPrune, signals: { ...config.autoPrune.signals } },
1615
+ dtc: { ...config.dtc },
1422
1616
  tool: { ...config.tool }
1423
1617
  };
1424
1618
  }
@@ -1430,8 +1624,7 @@ function mergeLayer(config, data) {
1430
1624
  language: data.language === "zh" || data.language === "en" ? data.language : config.language,
1431
1625
  commands: mergeCommands(config.commands, data.commands),
1432
1626
  experimental: mergeExperimental(config.experimental, data.experimental),
1433
- summarize: mergeSummarize(config.summarize, data.summarize),
1434
- autoPrune: mergeAutoPrune(config.autoPrune, data.autoPrune),
1627
+ dtc: mergeDtc(config.dtc, data.dtc, legacyDriftThreshold(data)),
1435
1628
  tool: mergeTool(config.tool, data.tool)
1436
1629
  };
1437
1630
  }
@@ -1485,225 +1678,76 @@ Using previous/default values`
1485
1678
  return config;
1486
1679
  }
1487
1680
 
1488
- // lib/session-boundary.ts
1489
- var BOUNDARY_QUIET_MS = 2e3;
1490
- var PROBE_TIMEOUT_MS = 2e3;
1491
- var BUSY_EVIDENCE_TTL_MS = 10 * 6e4;
1492
- var MAX_TRACKED_SESSIONS = 500;
1493
- var SessionBoundaryTracker = class {
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);
1691
+ }
1692
+ }
1693
+ var DtcState = class {
1494
1694
  sessions = /* @__PURE__ */ new Map();
1495
- listeners = [];
1496
- probeBusy;
1497
- logger;
1498
- now;
1499
- setTimer;
1500
- clearTimer;
1501
- constructor(deps) {
1502
- this.probeBusy = deps.probeBusy;
1503
- this.logger = deps.logger;
1504
- this.now = deps.now ?? Date.now;
1505
- this.setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
1506
- this.clearTimer = deps.clearTimer ?? ((t) => clearTimeout(t));
1507
- }
1508
- /** Register an at-rest listener; listeners fire in registration order
1509
- * (1. deferred drain, 2. heuristic auto prune) and are individually
1510
- * error-isolated: a throw or rejection is warn-logged and never blocks
1511
- * later listeners nor escapes into the timer callback. */
1512
- onAtRest(listener) {
1513
- this.listeners.push(listener);
1514
- }
1515
- /** Primary input: `session.status` event payload status type. */
1516
- observeStatus(sessionID, statusType) {
1517
- if (!sessionID) return;
1518
- const entry = this.entry(sessionID);
1519
- entry.sawStatus = true;
1520
- if (statusType === "busy" || statusType === "retry") {
1521
- this.transition(
1522
- sessionID,
1523
- entry,
1524
- "busy",
1525
- statusType === "retry" ? "retry-observed" : "busy-observed"
1526
- );
1695
+ digests = /* @__PURE__ */ new Map();
1696
+ observeContextLimit(sessionID, contextTokens) {
1697
+ if (!sessionID || !contextTokens || !Number.isFinite(contextTokens) || contextTokens <= 0) {
1527
1698
  return;
1528
1699
  }
1529
- if (statusType === "idle") {
1530
- this.observeIdle(sessionID, entry, "status");
1531
- }
1700
+ const state = this.session(sessionID);
1701
+ state.contextTokens = contextTokens;
1702
+ lruSet(this.sessions, sessionID, state, SESSION_LIMIT);
1532
1703
  }
1533
- /** First-class input: legacy `session.idle`. Absorbed by dedup on hosts
1534
- * that dual-publish; the only boundary signal on status-less hosts (T3). */
1535
- observeLegacyIdle(sessionID) {
1536
- if (!sessionID) return;
1537
- this.observeIdle(sessionID, this.entry(sessionID), "legacy-idle");
1704
+ contextTokens(sessionID) {
1705
+ return this.sessions.get(sessionID)?.contextTokens;
1538
1706
  }
1539
- /** Lifecycle: cancel any pending window and forget the session. */
1540
- observeDeleted(sessionID) {
1541
- if (!sessionID) return;
1542
- const entry = this.sessions.get(sessionID);
1543
- if (!entry) return;
1544
- this.cancelWindow(entry);
1545
- this.sessions.delete(sessionID);
1546
- this.logger.debug("Boundary session dropped", { sessionId: sessionID, reason: "deleted" });
1547
- }
1548
- /** Lifecycle: compaction ended; reset to unknown so the next idle opens a
1549
- * fresh window. */
1550
- observeCompacted(sessionID) {
1551
- if (!sessionID) return;
1552
- const entry = this.sessions.get(sessionID);
1553
- if (!entry) return;
1554
- const from = entry.phase;
1555
- this.cancelWindow(entry);
1556
- entry.phase = "unknown";
1557
- delete entry.busyAt;
1558
- this.logger.debug("Boundary transition", {
1559
- sessionId: sessionID,
1560
- from,
1561
- to: "unknown",
1562
- reason: "compacted"
1563
- });
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);
1564
1712
  }
1565
- /** Absorbed busy-evidence cache: `busy` decays to `unknown` after the TTL;
1566
- * the table is LRU-bounded (pending-rest/at-rest entries have no TTL exit
1567
- * and rely on the cap). */
1568
- state(sessionID) {
1569
- const entry = this.sessions.get(sessionID);
1570
- if (!entry) return "unknown";
1571
- if (entry.phase === "busy" && entry.busyAt !== void 0 && this.now() - entry.busyAt > BUSY_EVIDENCE_TTL_MS) {
1572
- return "unknown";
1573
- }
1574
- return entry.phase;
1713
+ consumeCompactionSkip(sessionID) {
1714
+ const state = this.sessions.get(sessionID);
1715
+ if (!state?.skipNextTransform) return false;
1716
+ state.skipNextTransform = false;
1717
+ return true;
1575
1718
  }
1576
- dispose(sessionID) {
1577
- this.observeDeleted(sessionID);
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);
1578
1725
  }
1579
- observeIdle(sessionID, entry, source) {
1580
- if (entry.phase === "pending-rest") {
1581
- this.logger.debug("Boundary duplicate idle ignored", { sessionId: sessionID, source });
1582
- return;
1583
- }
1584
- if (entry.phase === "at-rest") {
1585
- if (source === "legacy-idle" && !entry.sawStatus) {
1586
- this.armWindow(sessionID, entry, "degrade-rearm");
1587
- } else {
1588
- this.logger.debug("Boundary duplicate idle ignored", {
1589
- sessionId: sessionID,
1590
- source
1591
- });
1592
- }
1593
- return;
1594
- }
1595
- this.armWindow(sessionID, entry, "idle-observed");
1596
- }
1597
- armWindow(sessionID, entry, reason) {
1598
- this.cancelWindow(entry);
1599
- entry.phase = "pending-rest";
1600
- entry.timer = this.setTimer(() => {
1601
- entry.timer = void 0;
1602
- void this.onWindowExpiry(sessionID, entry);
1603
- }, BOUNDARY_QUIET_MS);
1604
- this.refresh(sessionID, entry);
1605
- this.logger.debug("Boundary quiet window armed", {
1606
- sessionId: sessionID,
1607
- reason,
1608
- generation: entry.generation
1609
- });
1726
+ boundaryMark(sessionID) {
1727
+ return this.sessions.get(sessionID)?.boundaryMarkAt;
1610
1728
  }
1611
- async onWindowExpiry(sessionID, entry) {
1612
- if (entry.phase !== "pending-rest") return;
1613
- const generation = entry.generation;
1614
- let busy;
1615
- try {
1616
- busy = await this.probeBusy(sessionID);
1617
- } catch {
1618
- busy = null;
1619
- }
1620
- if (entry.phase !== "pending-rest" || entry.generation !== generation || this.sessions.get(sessionID) !== entry) {
1621
- this.logger.debug("Boundary probe result discarded", {
1622
- sessionId: sessionID,
1623
- generation
1624
- });
1625
- return;
1626
- }
1627
- if (busy === true) {
1628
- this.transition(sessionID, entry, "busy", "probe-busy");
1629
- return;
1630
- }
1631
- const reason = busy === false ? "window-expired" : "probe-failopen";
1632
- const from = entry.phase;
1633
- entry.phase = "at-rest";
1634
- this.refresh(sessionID, entry);
1635
- this.logger.debug("Boundary transition", {
1636
- sessionId: sessionID,
1637
- from,
1638
- to: "at-rest",
1639
- reason
1640
- });
1641
- this.fireAtRest(sessionID, reason);
1642
- }
1643
- transition(sessionID, entry, phase, reason) {
1644
- const from = entry.phase;
1645
- this.cancelWindow(entry);
1646
- entry.phase = phase;
1647
- if (phase === "busy") entry.busyAt = this.now();
1648
- else delete entry.busyAt;
1649
- this.refresh(sessionID, entry);
1650
- this.logger.debug("Boundary transition", { sessionId: sessionID, from, to: phase, reason });
1651
- }
1652
- cancelWindow(entry) {
1653
- if (entry.timer !== void 0) {
1654
- this.clearTimer(entry.timer);
1655
- entry.timer = void 0;
1656
- }
1657
- entry.generation += 1;
1658
- }
1659
- fireAtRest(sessionID, reason) {
1660
- for (const listener of this.listeners) {
1661
- try {
1662
- const result = listener(sessionID, reason);
1663
- if (result instanceof Promise) {
1664
- result.catch((error) => {
1665
- this.logger.warn("At-rest listener rejected", {
1666
- sessionId: sessionID,
1667
- error: error instanceof Error ? error.message : String(error)
1668
- });
1669
- });
1670
- }
1671
- } catch (error) {
1672
- this.logger.warn("At-rest listener failed", {
1673
- sessionId: sessionID,
1674
- error: error instanceof Error ? error.message : String(error)
1675
- });
1676
- }
1677
- }
1729
+ minLevel(sessionID) {
1730
+ return this.sessions.get(sessionID)?.minLevel ?? 0;
1678
1731
  }
1679
- entry(sessionID) {
1680
- let entry = this.sessions.get(sessionID);
1681
- if (!entry) {
1682
- entry = { phase: "unknown", generation: 0, sawStatus: false };
1683
- this.sessions.set(sessionID, entry);
1684
- this.evictIfNeeded(sessionID);
1685
- return entry;
1686
- }
1687
- this.refresh(sessionID, entry);
1688
- return entry;
1732
+ cachedDigest(key) {
1733
+ return this.digests.get(key);
1689
1734
  }
1690
- refresh(sessionID, entry) {
1735
+ storeDigest(key, digest) {
1736
+ lruSet(this.digests, key, digest, DIGEST_LIMIT);
1737
+ }
1738
+ dropSession(sessionID) {
1691
1739
  this.sessions.delete(sessionID);
1692
- this.sessions.set(sessionID, entry);
1693
- this.evictIfNeeded(sessionID);
1694
1740
  }
1695
- evictIfNeeded(kept) {
1696
- if (this.sessions.size <= MAX_TRACKED_SESSIONS) return;
1697
- const oldest = this.sessions.keys().next().value;
1698
- if (oldest === void 0 || oldest === kept) return;
1699
- const victim = this.sessions.get(oldest);
1700
- if (victim?.timer !== void 0) this.clearTimer(victim.timer);
1701
- this.sessions.delete(oldest);
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) ?? {};
1702
1747
  }
1703
1748
  };
1704
- function retrySeconds(retryAfterMs) {
1705
- return Math.ceil(retryAfterMs / 1e3);
1706
- }
1749
+
1750
+ // lib/session-events.ts
1707
1751
  function eventSessionID(properties) {
1708
1752
  const direct = properties?.sessionID;
1709
1753
  if (typeof direct === "string" && direct) return direct;
@@ -1713,162 +1757,176 @@ function eventSessionID(properties) {
1713
1757
  }
1714
1758
 
1715
1759
  // lib/hooks.ts
1716
- 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) {
1717
1784
  return async (input, output) => {
1718
1785
  try {
1719
- prompts.reload();
1720
- 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;
1721
1795
  if (!output.prompt) {
1722
- output.prompt = prompt;
1796
+ output.prompt = withCheckpoint;
1723
1797
  } else if (!output.context.includes(prompt)) {
1724
- output.context.push(prompt);
1798
+ output.context.push(withCheckpoint);
1725
1799
  }
1726
- logger.debug("Applied semantic pruning prompt", { sessionId: input.sessionID });
1727
- } catch (error) {
1728
- logger.warn("Failed to apply semantic pruning prompt; native compaction continues", {
1800
+ deps.logger.debug("Applied semantic pruning prompt", {
1729
1801
  sessionId: input.sessionID,
1730
- error: error instanceof Error ? error.message : String(error)
1802
+ carriedCheckpoint: Boolean(previous)
1731
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
+ );
1732
1812
  }
1733
1813
  };
1734
1814
  }
1735
- async function showToast(client, title, message, variant = "info") {
1736
- await client.tui.showToast({
1737
- body: { title, message, variant, duration: 5e3 }
1738
- }).catch(() => void 0);
1739
- }
1740
- function createChatMessageHandler(autoPruner) {
1815
+ function createChatParamsHandler(deps) {
1741
1816
  return async (input, _output) => {
1742
- const parts = _output?.parts ?? [];
1743
- autoPruner.observeUserMessage(input.sessionID, parts);
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);
1821
+ }
1822
+ } catch (error) {
1823
+ deps.logger.debug("chat.params observation failed", {
1824
+ error: error instanceof Error ? error.message : String(error)
1825
+ });
1826
+ }
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
+ });
1842
+ }
1744
1843
  };
1745
1844
  }
1746
- var SIGNAL_LABELS = {
1747
- "topic-drift": "\u8BDD\u9898\u53D8\u66F4",
1748
- volume: "\u6D88\u606F\u91CF\u8FBE\u5230\u9608\u503C",
1749
- "idle-gap": "\u957F\u65F6\u95F4\u4E2D\u65AD\u540E\u6062\u590D"
1750
- };
1751
1845
  function createEventHandler(deps) {
1752
1846
  return async (input) => {
1753
- const event = input.event;
1754
1847
  try {
1755
- deps.prune.observeEvent(event.type, event.properties);
1756
- const sessionID = eventSessionID(event.properties);
1757
- if (!sessionID) return;
1758
- if (event.type === "session.compacted") {
1759
- deps.autoPruner.markPruned(sessionID);
1760
- return;
1761
- }
1762
- if (event.type === "session.deleted") {
1763
- deps.autoPruner.dropSession(sessionID);
1764
- }
1848
+ if (input.event.type !== "session.deleted") return;
1849
+ const sessionID = eventSessionID(input.event.properties);
1850
+ if (sessionID) deps.state.dropSession(sessionID);
1765
1851
  } catch (error) {
1766
1852
  deps.logger.warn("Event handler failed", {
1767
- type: event.type,
1853
+ type: input.event.type,
1768
1854
  error: error instanceof Error ? error.message : String(error)
1769
1855
  });
1770
1856
  }
1771
1857
  };
1772
1858
  }
1773
- async function triggerAutoPrune(deps, sessionID, signals) {
1774
- const reason = signals.map((signal) => SIGNAL_LABELS[signal]).join("\u3001");
1775
- const result = await deps.prune.request({ sessionID, onBusy: "proceed" });
1776
- const attempted = result.status === "succeeded" || result.status === "failed" || result.status === "cooldown";
1777
- if (!attempted) {
1778
- if (result.status === "busy") {
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) {
1865
+ return async (input, _output) => {
1866
+ if (input.command !== "dcp") return;
1867
+ const subcommand = (input.arguments ?? "").trim().split(/\s+/, 1)[0]?.toLowerCase();
1868
+ if (subcommand === "fold") {
1869
+ deps.state.markBoundary(input.sessionID, Date.now(), 3);
1779
1870
  await showToast(
1780
1871
  deps.client,
1781
- "DCP \u81EA\u52A8\u538B\u7F29",
1782
- `\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`,
1783
- "warning"
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"
1784
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__");
1785
1882
  }
1786
- deps.logger.debug("Auto prune skipped", {
1787
- sessionId: sessionID,
1788
- status: result.status
1789
- });
1790
- return;
1791
- }
1792
- deps.autoPruner.markPruned(sessionID);
1793
- if (result.status === "succeeded") {
1794
- 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`);
1795
- } else if (result.status === "cooldown") {
1796
- await showToast(
1797
- deps.client,
1798
- "DCP \u81EA\u52A8\u538B\u7F29",
1799
- `\u68C0\u6D4B\u5230${reason}\uFF0C\u4F46\u4E0A\u4E00\u6B21\u538B\u7F29\u5931\u8D25\uFF1B${retrySeconds(result.retryAfterMs)} \u79D2\u540E\u53EF\u91CD\u8BD5\u3002`,
1800
- "warning"
1801
- );
1802
- } else {
1803
1883
  await showToast(
1804
1884
  deps.client,
1805
- "DCP \u81EA\u52A8\u538B\u7F29",
1806
- `\u68C0\u6D4B\u5230${reason}\uFF0C\u4F46\u538B\u7F29\u5931\u8D25\uFF1B\u539F\u59CB\u4E0A\u4E0B\u6587\u4FDD\u6301\u4E0D\u53D8\u3002`,
1807
- "warning"
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"
1808
1887
  );
1809
- }
1810
- deps.logger.debug("Auto prune finished", { sessionId: sessionID, status: result.status });
1811
- }
1812
- function createAtRestAutoPruneListener(deps) {
1813
- return async (sessionID, _reason) => {
1814
- if (!deps.config.enabled) return;
1815
- const signals = deps.autoPruner.consumePending(sessionID);
1816
- if (signals) await triggerAutoPrune(deps, sessionID, signals);
1888
+ throw new Error("__DCP_HELP_HANDLED__");
1817
1889
  };
1818
1890
  }
1819
- function createCommandExecuteHandler(client, prune, logger) {
1820
- return async (input, _output) => {
1821
- if (input.command !== "dcp") return;
1822
- const subcommand = (input.arguments ?? "").trim().split(/\s+/, 1)[0]?.toLowerCase();
1823
- if (subcommand !== "summarize") {
1824
- await showToast(
1825
- client,
1826
- "DCP",
1827
- "Use /dcp summarize for semantic pruning, or OpenCode's native /compact command."
1828
- );
1829
- throw new Error("__DCP_HELP_HANDLED__");
1830
- }
1831
- const result = await prune.request({ sessionID: input.sessionID, onBusy: "proceed" });
1832
- if (result.status === "busy") {
1833
- await showToast(
1834
- client,
1835
- "DCP summarize",
1836
- "Session is busy; the prune will not interrupt the current turn. Try again once it finishes.",
1837
- "warning"
1838
- );
1839
- throw new Error("__DCP_SUMMARIZE_HANDLED__");
1840
- }
1841
- if (result.status === "no-model") {
1842
- await showToast(
1843
- client,
1844
- "DCP summarize",
1845
- "No session model is available yet.",
1846
- "warning"
1847
- );
1848
- throw new Error("__DCP_SUMMARIZE_NO_MODEL__");
1849
- }
1850
- if (result.status === "succeeded") {
1851
- await showToast(client, "DCP summarize", "Semantic pruning checkpoint created.");
1852
- } else if (result.status === "cooldown") {
1853
- await showToast(
1854
- client,
1855
- "DCP summarize",
1856
- `Previous attempt failed; retry in ${retrySeconds(result.retryAfterMs)}s.`,
1857
- "warning"
1858
- );
1859
- } else {
1860
- await showToast(
1861
- client,
1862
- "DCP summarize",
1863
- "Native compaction failed; the original context was kept.",
1864
- "error"
1865
- );
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
+ };
1866
1925
  }
1867
- logger.debug("Handled DCP summarize command", {
1868
- sessionId: input.sessionID,
1869
- status: result.status
1926
+ logger.debug("Applied host config defaults", {
1927
+ tailTurns: compaction.tail_turns,
1928
+ preserveRecentTokens: compaction.preserve_recent_tokens
1870
1929
  });
1871
- throw new Error("__DCP_SUMMARIZE_HANDLED__");
1872
1930
  };
1873
1931
  }
1874
1932
 
@@ -2072,15 +2130,16 @@ import { homedir as homedir3 } from "os";
2072
2130
  import { dirname as dirname2, join as join3 } from "path";
2073
2131
 
2074
2132
  // lib/prompts/compaction.ts
2075
- 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
2076
2134
 
2077
- \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
2078
2136
 
2079
- 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
2080
- 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
2081
- 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
2082
- 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
2083
- 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
2084
2143
 
2085
2144
  \u4F7F\u7528\u4EE5\u4E0B\u56FA\u5B9A\u7ED3\u6784\uFF0C\u7701\u7565\u786E\u5B9E\u4E3A\u7A7A\u7684\u6761\u76EE\uFF1A
2086
2145
 
@@ -2097,15 +2156,16 @@ var COMPACTION = `\u4F60\u6B63\u5728\u751F\u6210\u5F53\u524D\u4F1A\u8BDD\u552F\u
2097
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
2098
2157
 
2099
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`;
2100
- 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.
2101
2160
 
2102
- 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:
2103
2162
 
2104
- 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.
2105
- 2. Remove irrelevant chitchat, conversations about other projects or repositories, repeated explanations, and approaches that were overturned and no longer carry diagnostic value.
2106
- 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.
2107
- 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.
2108
- 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.
2109
2169
 
2110
2170
  Use the following fixed structure, omitting sections that are truly empty:
2111
2171
 
@@ -2213,301 +2273,30 @@ var PromptStore = class {
2213
2273
 
2214
2274
  // lib/prune-tool.ts
2215
2275
  import { tool } from "@opencode-ai/plugin";
2216
-
2217
- // lib/session-model.ts
2218
- function latestUserModel(messages) {
2219
- if (!Array.isArray(messages)) return null;
2220
- for (let index = messages.length - 1; index >= 0; index--) {
2221
- const info = messages[index]?.info;
2222
- if (info?.role !== "user") continue;
2223
- const providerID = info.model?.providerID;
2224
- const modelID = info.model?.modelID;
2225
- if (typeof providerID === "string" && typeof modelID === "string") {
2226
- return { providerID, modelID };
2227
- }
2228
- }
2229
- return null;
2230
- }
2231
- async function resolveSessionModel(client, sessionID) {
2232
- try {
2233
- const response = await client.session.messages({ path: { id: sessionID } });
2234
- return latestUserModel(response.data ?? response);
2235
- } catch {
2236
- return null;
2237
- }
2238
- }
2239
-
2240
- // lib/prune-service.ts
2241
- var PruneService = class {
2242
- deferred = /* @__PURE__ */ new Set();
2243
- client;
2244
- summarize;
2245
- logger;
2246
- probeTimeoutMs;
2247
- /** THE single busy/idle event state machine (absorbed the former
2248
- * SessionActivityTracker — no second busy cache may exist). */
2249
- boundary;
2250
- constructor(deps) {
2251
- this.client = deps.client;
2252
- this.summarize = deps.summarize;
2253
- this.logger = deps.logger;
2254
- this.probeTimeoutMs = deps.probeTimeoutMs ?? PROBE_TIMEOUT_MS;
2255
- this.boundary = new SessionBoundaryTracker({
2256
- probeBusy: (sessionID) => this.probeBusy(sessionID),
2257
- logger: deps.logger,
2258
- now: deps.now,
2259
- setTimer: deps.setTimer,
2260
- clearTimer: deps.clearTimer
2261
- });
2262
- this.boundary.onAtRest((sessionID) => {
2263
- if (!this.deferred.delete(sessionID)) return;
2264
- void this.drainQueued(sessionID);
2265
- });
2266
- }
2267
- /**
2268
- * Feed every host event through here. `session.status` and legacy
2269
- * `session.idle` feed the boundary tracker (quiet window + expiry probe);
2270
- * the deferral queue drains at the confirmed at-rest classification, and
2271
- * queued prunes are forgotten once a compaction (or the session itself)
2272
- * is gone.
2273
- */
2274
- observeEvent(type, properties) {
2275
- const sessionID = eventSessionID(properties);
2276
- if (type === "session.status") {
2277
- this.boundary.observeStatus(sessionID, properties?.status?.type);
2278
- return;
2279
- }
2280
- if (type === "session.idle") {
2281
- this.boundary.observeLegacyIdle(sessionID);
2282
- return;
2283
- }
2284
- if (!sessionID) return;
2285
- if (type === "session.compacted") {
2286
- this.deferred.delete(sessionID);
2287
- this.boundary.observeCompacted(sessionID);
2288
- return;
2289
- }
2290
- if (type === "session.deleted") {
2291
- this.deferred.delete(sessionID);
2292
- this.boundary.observeDeleted(sessionID);
2293
- }
2294
- }
2295
- async request(request) {
2296
- const beforeModel = await this.gate(request);
2297
- if (beforeModel) return beforeModel;
2298
- const model = await resolveSessionModel(this.client, request.sessionID);
2299
- if (!model) return { status: "no-model" };
2300
- const afterModel = await this.gate(request);
2301
- if (afterModel) return afterModel;
2302
- const serverBusy = await this.probeBusy(request.sessionID);
2303
- if (serverBusy === true) {
2304
- const fallback = await this.outcomeFor(request, { action: "stand-down" });
2305
- if (fallback) return fallback;
2306
- }
2307
- const result = await this.summarize.summarize({ sessionID: request.sessionID, model });
2308
- if (result.status === "rejected") {
2309
- return { status: "busy" };
2310
- }
2311
- return result;
2312
- }
2313
- /** One admission check; `null` means the request may proceed. */
2314
- gate(request) {
2315
- return this.outcomeFor(request, this.admit(request));
2316
- }
2317
- /**
2318
- * Executes a queued prune at the confirmed at-rest boundary. The tool
2319
- * already promised its caller this prune would run; losing the busy race
2320
- * must not silently break that promise, so a busy outcome re-queues for
2321
- * the next at-rest boundary. Every other outcome is terminal: it is
2322
- * logged and never retried — new prune demand arrives through new
2323
- * triggers only. Execution is re-guarded on every attempt, so re-queueing
2324
- * never violates the never-interrupt invariant.
2325
- */
2326
- async drainQueued(sessionID) {
2327
- let outcome;
2328
- try {
2329
- outcome = await this.request({ sessionID, onBusy: "proceed" });
2330
- } catch (error) {
2331
- this.logger.warn("Queued prune drain failed; the prune was not retried", {
2332
- sessionId: sessionID,
2333
- error: error instanceof Error ? error.message : String(error)
2334
- });
2335
- return;
2336
- }
2337
- if (outcome.status === "busy") {
2338
- this.deferred.add(sessionID);
2339
- return;
2340
- }
2341
- this.logger.debug("Queued prune drain finished", {
2342
- sessionId: sessionID,
2343
- status: outcome.status
2344
- });
2345
- }
2346
- /**
2347
- * Turns an admission decision into a terminal outcome, or `null` when the
2348
- * request may proceed. Deferral additionally enqueues the session.
2349
- */
2350
- async outcomeFor(request, admission) {
2351
- if (admission.action === "go") return null;
2352
- if (admission.action === "stand-down") return { status: "busy" };
2353
- this.deferred.add(request.sessionID);
2354
- this.logger.debug("Prune deferred to the next session at-rest boundary", {
2355
- sessionId: request.sessionID
2356
- });
2357
- return { status: "deferred" };
2358
- }
2359
- admit(request) {
2360
- if (request.onBusy === "defer") return { action: "defer" };
2361
- if (this.boundary.state(request.sessionID) === "busy") return { action: "stand-down" };
2362
- return { action: "go" };
2363
- }
2364
- /**
2365
- * THE single live busy probe, reused verbatim by the boundary tracker's
2366
- * expiry check. Bounded by a finite deadline: a never-returning probe
2367
- * resolves `null` within the timeout (fail-open). Returns `null` when the
2368
- * answer is unknown (endpoint or SDK method missing, request failed,
2369
- * timeout) — callers must fail open in that case.
2370
- */
2371
- async probeBusy(sessionID) {
2372
- const statusFn = this.client.session.status;
2373
- if (typeof statusFn !== "function") return null;
2374
- const query = (async () => {
2375
- try {
2376
- return await statusFn.call(
2377
- this.client.session
2378
- );
2379
- } catch {
2380
- return null;
2381
- }
2382
- })();
2383
- const response = await Promise.race([
2384
- query,
2385
- new Promise((resolve) => setTimeout(() => resolve(null), this.probeTimeoutMs))
2386
- ]);
2387
- const map = response?.data ?? response;
2388
- const info = map?.[sessionID];
2389
- return info?.type === "busy" || info?.type === "retry";
2390
- }
2391
- };
2392
-
2393
- // lib/prune-tool.ts
2394
2276
  var PRUNE_TOOL_NAME = "dcp_prune";
2395
- 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
2396
2278
 
2397
2279
  \u4EC5\u5728\u8FD9\u4E9B\u60C5\u51B5\u4E0B\u8C03\u7528\uFF1A
2398
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
2399
2281
  - \u7528\u6237\u660E\u786E\u8981\u6C42\u538B\u7F29\u4E0A\u4E0B\u6587\u3002
2400
2282
 
2401
- \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\u786E\u8BA4\u7684\u9759\u606F\u8FB9\u754C\uFF08\u77ED\u6682\u9759\u9ED8\u7A97\u53E3\u52A0\u5B9E\u65F6\u72B6\u6001\u68C0\u67E5\uFF09\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`;
2402
2284
  function createPruneTool(deps) {
2403
2285
  return tool({
2404
2286
  description: PRUNE_TOOL_DESCRIPTION,
2405
2287
  args: {},
2406
2288
  execute: async (_args, context) => {
2407
2289
  const sessionID = context.sessionID;
2408
- const result = await deps.prune.request({ sessionID, onBusy: "defer" });
2409
- if (result.status === "succeeded") {
2410
- deps.logger.debug("Prune tool triggered native compaction", {
2411
- sessionId: sessionID
2412
- });
2413
- return "DCP\uFF1A\u8BED\u4E49\u538B\u7F29\u5B8C\u6210\uFF0C\u65E7\u4E0A\u4E0B\u6587\u5DF2\u6298\u53E0\u4E3A\u65B0\u68C0\u67E5\u70B9\u3002";
2414
- }
2415
- if (result.status === "deferred") {
2416
- return "DCP\uFF1A\u4F1A\u8BDD\u4ECD\u5728\u5DE5\u4F5C\u4E2D\uFF0C\u538B\u7F29\u5DF2\u6392\u961F\uFF0C\u5C06\u5728\u4E0B\u4E00\u4E2A\u786E\u8BA4\u7684\u9759\u606F\u8FB9\u754C\u5C1D\u8BD5\u81EA\u52A8\u6267\u884C\uFF1B\u5F53\u524D\u4E0A\u4E0B\u6587\u4E0D\u53D7\u5F71\u54CD\u3002";
2417
- }
2418
- if (result.status === "busy") {
2419
- 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";
2420
- }
2421
- if (result.status === "cooldown") {
2422
- return `DCP\uFF1A\u4E0A\u4E00\u6B21\u538B\u7F29\u5931\u8D25\uFF0C${retrySeconds(result.retryAfterMs)} \u79D2\u540E\u624D\u80FD\u91CD\u8BD5\u3002`;
2423
- }
2424
- if (result.status === "no-model") {
2425
- return "DCP\uFF1A\u4F1A\u8BDD\u4E2D\u8FD8\u6CA1\u6709\u53EF\u7528\u7684\u6A21\u578B\u4FE1\u606F\uFF0C\u65E0\u6CD5\u6267\u884C\u538B\u7F29\u3002";
2426
- }
2427
- 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";
2428
2296
  }
2429
2297
  });
2430
2298
  }
2431
2299
 
2432
- // lib/summarize.ts
2433
- function errorMessage(error) {
2434
- if (error instanceof Error) return error.message;
2435
- if (typeof error === "string") return error;
2436
- try {
2437
- return JSON.stringify(error);
2438
- } catch {
2439
- return "Unknown native compaction error";
2440
- }
2441
- }
2442
- function isBusyRejection(error) {
2443
- if (error instanceof Error && /\bbusy\b/i.test(error.message)) return true;
2444
- if (typeof error === "string" && /\bbusy\b/i.test(error)) return true;
2445
- const structured = error;
2446
- if (!structured || typeof structured !== "object") return false;
2447
- if (structured.status === 409 || structured.statusCode === 409 || structured.code === 409) {
2448
- return true;
2449
- }
2450
- return typeof structured.name === "string" && /busy/i.test(structured.name);
2451
- }
2452
- var SummarizeCoordinator = class {
2453
- constructor(client, logger, options) {
2454
- this.client = client;
2455
- this.logger = logger;
2456
- this.options = options;
2457
- this.now = options.now ?? Date.now;
2458
- }
2459
- inFlight = /* @__PURE__ */ new Map();
2460
- failedAt = /* @__PURE__ */ new Map();
2461
- now;
2462
- summarize(request) {
2463
- const active = this.inFlight.get(request.sessionID);
2464
- if (active) return active;
2465
- const failedAt = this.failedAt.get(request.sessionID);
2466
- if (failedAt !== void 0) {
2467
- const retryAfterMs = this.options.failureCooldownMs - (this.now() - failedAt);
2468
- if (retryAfterMs > 0) {
2469
- return Promise.resolve({ status: "cooldown", retryAfterMs });
2470
- }
2471
- this.failedAt.delete(request.sessionID);
2472
- }
2473
- const promise = this.invokeNative(request).finally(() => {
2474
- if (this.inFlight.get(request.sessionID) === promise) {
2475
- this.inFlight.delete(request.sessionID);
2476
- }
2477
- });
2478
- this.inFlight.set(request.sessionID, promise);
2479
- return promise;
2480
- }
2481
- async invokeNative(request) {
2482
- try {
2483
- const response = await this.client.session.summarize({
2484
- path: { id: request.sessionID },
2485
- body: request.model
2486
- });
2487
- const nativeError = response?.error;
2488
- if (nativeError && isBusyRejection(nativeError)) {
2489
- return { status: "rejected", reason: "busy" };
2490
- }
2491
- if (nativeError || response?.data !== true) {
2492
- throw new Error(errorMessage(nativeError ?? "Native summarize returned false"));
2493
- }
2494
- this.failedAt.delete(request.sessionID);
2495
- return { status: "succeeded" };
2496
- } catch (error) {
2497
- if (isBusyRejection(error)) {
2498
- return { status: "rejected", reason: "busy" };
2499
- }
2500
- this.failedAt.set(request.sessionID, this.now());
2501
- const message = errorMessage(error);
2502
- await this.logger.warn("Native summarize failed; context remains unchanged", {
2503
- sessionId: request.sessionID,
2504
- error: message
2505
- });
2506
- return { status: "failed", error: message };
2507
- }
2508
- }
2509
- };
2510
-
2511
2300
  // lib/update.ts
2512
2301
  import { readFile } from "fs/promises";
2513
2302
  import { basename, dirname as dirname3, join as join4 } from "path";
@@ -2654,57 +2443,48 @@ var server = (async (ctx) => {
2654
2443
  config.experimental.customPrompts,
2655
2444
  config.language
2656
2445
  );
2657
- const summarize = new SummarizeCoordinator(ctx.client, logger, {
2658
- failureCooldownMs: config.summarize.failureCooldownMs
2659
- });
2660
- const prune = new PruneService({ client: ctx.client, summarize, logger });
2661
- const autoPruner = new AutoPruner(config.autoPrune);
2662
- if (config.autoPrune.enabled) {
2663
- prune.boundary.onAtRest(
2664
- createAtRestAutoPruneListener({
2665
- client: ctx.client,
2666
- prune,
2667
- autoPruner,
2668
- config: config.autoPrune,
2669
- logger
2670
- })
2671
- );
2672
- }
2446
+ const state = new DtcState();
2673
2447
  logger.info("DCP initialized", {
2674
2448
  commands: config.commands.enabled,
2675
- autoPrune: config.autoPrune.enabled,
2449
+ dtc: config.dtc.enabled,
2676
2450
  tool: config.tool.enabled,
2677
2451
  customPrompts: config.experimental.customPrompts
2678
2452
  });
2679
2453
  startAutoUpdate(ctx, config.autoUpdate);
2680
2454
  return {
2681
- "experimental.session.compacting": createSessionCompactingHandler(prompts, logger),
2682
- ...config.autoPrune.enabled && {
2683
- "chat.message": createChatMessageHandler(autoPruner)
2684
- },
2685
- // The event feed drives both the boundary classification (status/idle
2686
- // observations) and the tool's deferred prunes, so it stays registered
2687
- // whenever either surface is on.
2688
- ...(config.autoPrune.enabled || config.tool.enabled) && {
2689
- event: createEventHandler({
2690
- prune,
2691
- autoPruner,
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,
2692
2468
  logger
2693
- })
2469
+ }),
2470
+ "chat.params": createChatParamsHandler({ state, logger })
2694
2471
  },
2472
+ // Lifecycle cleanup for DTC session state (LRU-bounded regardless).
2473
+ event: createEventHandler({ state, logger }),
2695
2474
  ...config.tool.enabled && {
2696
- tool: { [PRUNE_TOOL_NAME]: createPruneTool({ prune, logger }) }
2475
+ tool: { [PRUNE_TOOL_NAME]: createPruneTool({ state, logger }) }
2697
2476
  },
2698
2477
  ...config.commands.enabled && {
2699
- "command.execute.before": createCommandExecuteHandler(ctx.client, prune, logger),
2700
- config: async (opencodeConfig) => {
2701
- opencodeConfig.command ??= {};
2702
- opencodeConfig.command.dcp = {
2703
- template: "",
2704
- description: "Run semantic context pruning with native compaction"
2705
- };
2706
- }
2707
- }
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)
2708
2488
  };
2709
2489
  });
2710
2490
  var index_default = server;