@lexwdex-org/opencode-dcp 3.5.0 → 5.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.
Files changed (38) hide show
  1. package/README.en.md +60 -47
  2. package/README.md +45 -66
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +699 -1105
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/config.d.ts +7 -32
  7. package/dist/lib/config.d.ts.map +1 -1
  8. package/dist/lib/dtc/digest.d.ts +16 -0
  9. package/dist/lib/dtc/digest.d.ts.map +1 -0
  10. package/dist/lib/dtc/engine.d.ts +64 -0
  11. package/dist/lib/dtc/engine.d.ts.map +1 -0
  12. package/dist/lib/dtc/state.d.ts +28 -0
  13. package/dist/lib/dtc/state.d.ts.map +1 -0
  14. package/dist/lib/dtc/types.d.ts +45 -0
  15. package/dist/lib/dtc/types.d.ts.map +1 -0
  16. package/dist/lib/hooks.d.ts +60 -31
  17. package/dist/lib/hooks.d.ts.map +1 -1
  18. package/dist/lib/prune-tool.d.ts +3 -2
  19. package/dist/lib/prune-tool.d.ts.map +1 -1
  20. package/dist/lib/session-events.d.ts +7 -0
  21. package/dist/lib/session-events.d.ts.map +1 -0
  22. package/dist/lib/text.d.ts +19 -0
  23. package/dist/lib/text.d.ts.map +1 -0
  24. package/package.json +2 -3
  25. package/dist/lib/auto-prune.d.ts +0 -20
  26. package/dist/lib/auto-prune.d.ts.map +0 -1
  27. package/dist/lib/prompts/compaction.d.ts +0 -4
  28. package/dist/lib/prompts/compaction.d.ts.map +0 -1
  29. package/dist/lib/prompts/store.d.ts +0 -17
  30. package/dist/lib/prompts/store.d.ts.map +0 -1
  31. package/dist/lib/prune-service.d.ts +0 -104
  32. package/dist/lib/prune-service.d.ts.map +0 -1
  33. package/dist/lib/session-boundary.d.ts +0 -90
  34. package/dist/lib/session-boundary.d.ts.map +0 -1
  35. package/dist/lib/session-model.d.ts +0 -8
  36. package/dist/lib/session-model.d.ts.map +0 -1
  37. package/dist/lib/summarize.d.ts +0 -38
  38. package/dist/lib/summarize.d.ts.map +0 -1
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,40 +862,460 @@ 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 M_ERROR_KEEP_CHARS = 200;
1048
+ var D_ERROR_KEEP_CHARS = 100;
1049
+ var INPUT_TARGET_KEYS = ["filePath", "path", "file", "filename", "pattern", "directory"];
1050
+ var COMMAND_KEEP_CHARS = 80;
1051
+ var FOLDED_TEXT = " ";
1052
+ var NO_STATS = {
1053
+ messages: 0,
1054
+ userTurns: 0,
1055
+ level: 0,
1056
+ estimatedBefore: 0,
1057
+ estimatedAfter: 0,
1058
+ contextTokens: 0,
1059
+ foldedTools: 0,
1060
+ foldedTexts: 0,
1061
+ reducedInputs: 0,
1062
+ foldedErrors: 0,
1063
+ digestedTurns: 0,
1064
+ skipped: void 0
1065
+ };
1066
+ function segmentTurns(messages) {
1067
+ const turns = [];
1068
+ for (let i = 0; i < messages.length; i++) {
1069
+ const info = messages[i]?.info;
1070
+ if (info?.role !== "user") continue;
1071
+ if ((messages[i]?.parts ?? []).some((p) => p?.type === "compaction")) continue;
1072
+ turns.push({ start: i, end: messages.length });
1073
+ }
1074
+ for (let i = 0; i < turns.length - 1; i++) {
1075
+ turns[i].end = turns[i + 1].start;
1076
+ }
1077
+ return turns;
1078
+ }
1079
+ function transformMessages(messages, deps) {
1080
+ const stats = { ...NO_STATS };
1081
+ if (!Array.isArray(messages) || messages.length === 0) return stats;
1082
+ stats.messages = messages.length;
1083
+ const sessionID = findSessionID(messages);
1084
+ if (sessionID && deps.state.consumeCompactionSkip(sessionID)) {
1085
+ stats.skipped = "compaction";
1086
+ return stats;
1087
+ }
1088
+ const turns = segmentTurns(messages);
1089
+ stats.userTurns = turns.length;
1090
+ const { config, state } = deps;
1091
+ if (turns.length <= config.tailTurns) {
1092
+ stats.skipped = "short";
1093
+ return stats;
1094
+ }
1095
+ const contextTokens = sessionID ? state.contextTokens(sessionID) : void 0;
1096
+ if (!contextTokens) {
1097
+ stats.skipped = "unknown-context";
1098
+ return stats;
1099
+ }
1100
+ stats.contextTokens = contextTokens;
1101
+ const headTurns = turns.slice(0, turns.length - config.tailTurns);
1102
+ const estimatedBefore = estimateSlice(messages, 0, messages.length);
1103
+ stats.estimatedBefore = estimatedBefore;
1104
+ const lowWatermark = Math.floor(contextTokens * config.lowWatermarkRatio);
1105
+ const target = Math.floor(contextTokens * config.targetRatio);
1106
+ const minLevel = sessionID ? state.minLevel(sessionID) : 0;
1107
+ if (estimatedBefore <= lowWatermark && minLevel === 0) {
1108
+ stats.estimatedAfter = estimatedBefore;
1109
+ return stats;
1110
+ }
1111
+ const zones = computeZones(messages, headTurns, sessionID, state, config);
1112
+ const now = (deps.now ?? Date.now)();
1113
+ let level = Math.max(1, minLevel);
1114
+ applyLevel(messages, headTurns, zones, level, deps, stats, now);
1115
+ let estimatedAfter = estimateSlice(messages, 0, messages.length);
1116
+ while (estimatedAfter > target && level < 3) {
1117
+ level = level + 1;
1118
+ applyLevel(messages, headTurns, zones, level, deps, stats, now);
1119
+ estimatedAfter = estimateSlice(messages, 0, messages.length);
1120
+ }
1121
+ stats.level = level;
1122
+ stats.estimatedAfter = estimatedAfter;
1123
+ deps.logger?.debug("DTC transform", {
1124
+ sessionId: sessionID,
1125
+ messages: stats.messages,
1126
+ userTurns: stats.userTurns,
1127
+ level,
1128
+ estimatedBefore,
1129
+ estimatedAfter,
1130
+ contextTokens,
1131
+ foldedTools: stats.foldedTools,
1132
+ reducedInputs: stats.reducedInputs,
1133
+ foldedErrors: stats.foldedErrors,
1134
+ digestedTurns: stats.digestedTurns
1135
+ });
1136
+ return stats;
1137
+ }
1138
+ function computeZones(messages, headTurns, sessionID, state, config) {
1139
+ const boundaries = findTopicBoundaries(messages, headTurns, config.driftThreshold);
1140
+ const lastBoundary = boundaries.length > 0 ? boundaries[boundaries.length - 1] : 0;
1141
+ const secondLast = boundaries.length > 1 ? boundaries[boundaries.length - 2] : 0;
1142
+ let markStart = 0;
1143
+ const markAt = sessionID ? state.boundaryMark(sessionID) : void 0;
1144
+ if (markAt !== void 0) {
1145
+ for (let t = 0; t < headTurns.length; t++) {
1146
+ const created = messages[headTurns[t].start]?.info?.time?.created;
1147
+ if (typeof created === "number" && created <= markAt) markStart = t + 1;
1148
+ }
1149
+ }
1150
+ const cStart = Math.min(
1151
+ headTurns.length,
1152
+ Math.max(lastBoundary, markStart, headTurns.length - C_ZONE_MAX_TURNS)
1153
+ );
1154
+ const mStart = Math.min(cStart, Math.max(secondLast, cStart - M_ZONE_MAX_TURNS));
1155
+ return { mStart, cStart };
1156
+ }
1157
+ function applyLevel(messages, headTurns, zones, level, deps, stats, now) {
1158
+ const prev = level === 1 ? -1 : level - 1;
1159
+ if (level >= 1 && prev < 1) {
1160
+ for (let t = 0; t < zones.mStart; t++) {
1161
+ foldDistant(messages, headTurns[t], t + 1, deps, stats, now);
1162
+ }
1163
+ }
1164
+ if (level >= 2 && prev < 2) {
1165
+ for (let t = zones.mStart; t < zones.cStart; t++) {
1166
+ foldMiddle(messages, headTurns[t], stats, now);
1167
+ }
1168
+ }
1169
+ if (level >= 3 && prev < 3) {
1170
+ for (let t = zones.cStart; t < headTurns.length; t++) {
1171
+ foldCurrent(messages, headTurns[t], deps.config.toolOutputKeepChars, stats);
1172
+ }
1173
+ }
1174
+ }
1175
+ function foldDistant(messages, turn, ordinal, deps, stats, now) {
1176
+ const key = digestKey(messages, turn);
1177
+ let digest = deps.state.cachedDigest(key);
1178
+ if (digest === void 0) {
1179
+ digest = digestTurn(messages, turn, ordinal);
1180
+ deps.state.storeDigest(key, digest);
1181
+ }
1182
+ stats.digestedTurns++;
1183
+ let digestPlaced = false;
1184
+ for (let i = turn.start; i < turn.end; i++) {
1185
+ const message = messages[i];
1186
+ for (const part of message?.parts ?? []) {
1187
+ if (!part || typeof part !== "object") continue;
1188
+ if (part.type === "tool" && part.state?.status === "error") {
1189
+ foldErrorPart(part, D_ERROR_KEEP_CHARS, true, stats);
1190
+ continue;
1191
+ }
1192
+ if (foldToolPart(part, now, "distant", stats)) {
1193
+ stats.foldedTools++;
1194
+ continue;
1195
+ }
1196
+ if (part.type === "reasoning" && typeof part.text === "string" && part.text.length > 0) {
1197
+ part.text = FOLDED_TEXT;
1198
+ continue;
1199
+ }
1200
+ if (part.type === "text" && typeof part.text === "string") {
1201
+ if (!digestPlaced && message?.info?.role === "user") {
1202
+ part.text = digest;
1203
+ digestPlaced = true;
1204
+ } else {
1205
+ part.text = FOLDED_TEXT;
1206
+ }
1207
+ stats.foldedTexts++;
1208
+ }
1209
+ }
1210
+ }
1211
+ }
1212
+ function foldMiddle(messages, turn, stats, now) {
1213
+ for (let i = turn.start; i < turn.end; i++) {
1214
+ for (const part of messages[i]?.parts ?? []) {
1215
+ if (!part || typeof part !== "object") continue;
1216
+ if (part.type === "tool" && part.state?.status === "error") {
1217
+ foldErrorPart(part, M_ERROR_KEEP_CHARS, false, stats);
1218
+ continue;
1219
+ }
1220
+ if (foldToolPart(part, now, "middle", stats)) {
1221
+ stats.foldedTools++;
1222
+ continue;
1223
+ }
1224
+ if (part.type === "reasoning" && typeof part.text === "string" && part.text.length > 0) {
1225
+ part.text = FOLDED_TEXT;
1226
+ continue;
1227
+ }
1228
+ if (part.type === "text" && typeof part.text === "string" && part.text.length > M_TEXT_KEEP_CHARS) {
1229
+ part.text = firstLine(part.text, M_TEXT_KEEP_CHARS) || FOLDED_TEXT;
1230
+ stats.foldedTexts++;
1231
+ }
1232
+ }
1233
+ }
1234
+ }
1235
+ function foldCurrent(messages, turn, keepChars, stats) {
1236
+ for (let i = turn.start; i < turn.end; i++) {
1237
+ for (const part of messages[i]?.parts ?? []) {
1238
+ if (!part || typeof part !== "object" || part.type !== "tool") continue;
1239
+ const state = part.state;
1240
+ if (!state || state.status !== "completed") continue;
1241
+ const output = state.output;
1242
+ if (typeof output !== "string" || output.length <= keepChars) continue;
1243
+ state.output = truncateMiddle(
1244
+ output,
1245
+ keepChars,
1246
+ `[DCP \u5DF2\u6298\u53E0 ${output.length - keepChars} \u5B57\u7B26]`
1247
+ );
1248
+ stats.foldedTools++;
1249
+ }
1250
+ }
1251
+ }
1252
+ function reduceInput(input) {
1253
+ if (!input || typeof input !== "object") return {};
1254
+ const reduced = {};
1255
+ for (const key of INPUT_TARGET_KEYS) {
1256
+ const value = input[key];
1257
+ if (typeof value === "string" && value) reduced[key] = value;
1258
+ }
1259
+ const command = input.command;
1260
+ if (typeof command === "string" && command) {
1261
+ reduced.command = firstLine(command, COMMAND_KEEP_CHARS);
1262
+ }
1263
+ return reduced;
1264
+ }
1265
+ function foldToolPart(part, now, zone, stats) {
1266
+ if (part.type !== "tool") return false;
1267
+ const state = part.state;
1268
+ if (!state || state.status !== "completed") return false;
1269
+ if (state.time && typeof state.time === "object") {
1270
+ state.time.compacted = now;
1271
+ } else {
1272
+ state.time = { compacted: now };
1273
+ }
1274
+ if (state.input && typeof state.input === "object") {
1275
+ state.input = zone === "distant" ? {} : reduceInput(state.input);
1276
+ stats.reducedInputs++;
1277
+ }
1278
+ return true;
1279
+ }
1280
+ function foldErrorPart(part, keepChars, clearInput, stats) {
1281
+ const state = part.state;
1282
+ if (!state) return;
1283
+ if (typeof state.error === "string" && state.error.length > keepChars) {
1284
+ state.error = firstLine(state.error, keepChars) || FOLDED_TEXT;
1285
+ stats.foldedErrors++;
1286
+ }
1287
+ if (state.input && typeof state.input === "object") {
1288
+ state.input = clearInput ? {} : reduceInput(state.input);
1289
+ stats.reducedInputs++;
1290
+ }
1291
+ }
1292
+ function findSessionID(messages) {
1293
+ for (const message of messages) {
1294
+ const id = message?.info?.sessionID;
1295
+ if (typeof id === "string" && id) return id;
1296
+ }
1297
+ return void 0;
1298
+ }
1299
+
980
1300
  // lib/config.ts
981
- var DEFAULT_FAILURE_COOLDOWN_MS = 3e4;
982
- var DEFAULT_AUTO_PRUNE = {
1301
+ var DEFAULT_DTC = {
983
1302
  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
1303
+ ...DTC_DEFAULTS
990
1304
  };
991
1305
  var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
992
1306
  "$schema",
993
1307
  "enabled",
994
1308
  "autoUpdate",
995
1309
  "debug",
996
- "language",
997
1310
  "commands",
998
1311
  "commands.enabled",
999
- "experimental",
1000
- "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",
1312
+ "dtc",
1313
+ "dtc.enabled",
1314
+ "dtc.tailTurns",
1315
+ "dtc.lowWatermarkRatio",
1316
+ "dtc.targetRatio",
1317
+ "dtc.driftThreshold",
1318
+ "dtc.toolOutputKeepChars",
1014
1319
  "tool",
1015
1320
  "tool.enabled"
1016
1321
  ]);
@@ -1055,7 +1360,24 @@ var DEPRECATED_CONFIG_KEYS = /* @__PURE__ */ new Set([
1055
1360
  "pruneNotificationType",
1056
1361
  "protectedFilePatterns",
1057
1362
  "commands.protectedTools",
1058
- "experimental.allowSubAgents"
1363
+ "experimental.allowSubAgents",
1364
+ "language",
1365
+ "experimental",
1366
+ "experimental.customPrompts",
1367
+ "summarize",
1368
+ "summarize.failureCooldownMs",
1369
+ "autoPrune",
1370
+ "autoPrune.enabled",
1371
+ "autoPrune.signals",
1372
+ "autoPrune.signals.topicDrift",
1373
+ "autoPrune.signals.volume",
1374
+ "autoPrune.signals.idleGap",
1375
+ "autoPrune.autoContinue",
1376
+ "autoPrune.minMessages",
1377
+ "autoPrune.volumeThreshold",
1378
+ "autoPrune.driftThreshold",
1379
+ "autoPrune.idleGapMs",
1380
+ "autoPrune.cooldownMs"
1059
1381
  ]);
1060
1382
  function getConfigKeyPaths(obj, prefix = "") {
1061
1383
  const keys = [];
@@ -1087,21 +1409,10 @@ function validateConfigTypes(config) {
1087
1409
  if (config.debug !== void 0 && typeof config.debug !== "boolean") {
1088
1410
  errors.push({ key: "debug", expected: "boolean", actual: typeof config.debug });
1089
1411
  }
1090
- if (config.language !== void 0 && config.language !== "zh" && config.language !== "en") {
1091
- errors.push({
1092
- key: "language",
1093
- expected: '"zh" or "en"',
1094
- actual: JSON.stringify(config.language)
1095
- });
1096
- }
1097
1412
  const commands = config.commands;
1098
1413
  if (commands !== void 0) {
1099
1414
  if (typeof commands !== "object" || commands === null || Array.isArray(commands)) {
1100
- errors.push({
1101
- key: "commands",
1102
- expected: "object",
1103
- actual: typeof commands
1104
- });
1415
+ errors.push({ key: "commands", expected: "object", actual: typeof commands });
1105
1416
  } else if (commands.enabled !== void 0 && typeof commands.enabled !== "boolean") {
1106
1417
  errors.push({
1107
1418
  key: "commands.enabled",
@@ -1110,141 +1421,65 @@ function validateConfigTypes(config) {
1110
1421
  });
1111
1422
  }
1112
1423
  }
1113
- const experimental = config.experimental;
1114
- if (experimental !== void 0) {
1115
- if (typeof experimental !== "object" || experimental === null || Array.isArray(experimental)) {
1116
- errors.push({
1117
- key: "experimental",
1118
- expected: "object",
1119
- actual: typeof experimental
1120
- });
1121
- } else if (experimental.customPrompts !== void 0 && typeof experimental.customPrompts !== "boolean") {
1122
- errors.push({
1123
- key: "experimental.customPrompts",
1124
- expected: "boolean",
1125
- actual: typeof experimental.customPrompts
1126
- });
1127
- }
1128
- }
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
- });
1424
+ const dtc = config.dtc;
1425
+ if (dtc !== void 0) {
1426
+ if (typeof dtc !== "object" || dtc === null || Array.isArray(dtc)) {
1427
+ errors.push({ key: "dtc", expected: "object", actual: typeof dtc });
1137
1428
  } 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
- });
1429
+ if (dtc.enabled !== void 0 && typeof dtc.enabled !== "boolean") {
1430
+ errors.push({ key: "dtc.enabled", expected: "boolean", actual: typeof dtc.enabled });
1144
1431
  }
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
1432
  const numericKeys = [
1157
- ["minMessages", 1, Number.POSITIVE_INFINITY],
1158
- ["volumeThreshold", 2, Number.POSITIVE_INFINITY],
1433
+ ["tailTurns", 0, Number.POSITIVE_INFINITY],
1434
+ ["lowWatermarkRatio", 0, 1],
1435
+ ["targetRatio", 0, 1],
1159
1436
  ["driftThreshold", 0, 1],
1160
- ["idleGapMs", 0, Number.POSITIVE_INFINITY],
1161
- ["cooldownMs", 0, Number.POSITIVE_INFINITY]
1437
+ ["toolOutputKeepChars", 200, Number.POSITIVE_INFINITY]
1162
1438
  ];
1163
1439
  for (const [key, min, max] of numericKeys) {
1164
- const value = autoPrune[key];
1440
+ const value = dtc[key];
1165
1441
  if (value !== void 0 && (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max)) {
1166
1442
  errors.push({
1167
- key: `autoPrune.${key}`,
1443
+ key: `dtc.${key}`,
1168
1444
  expected: `number in [${min}, ${max === Number.POSITIVE_INFINITY ? "\u221E" : max}]`,
1169
1445
  actual: JSON.stringify(value)
1170
1446
  });
1171
1447
  }
1172
1448
  }
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
1449
  }
1205
1450
  }
1206
1451
  const tool2 = config.tool;
1207
1452
  if (tool2 !== void 0) {
1208
1453
  if (typeof tool2 !== "object" || tool2 === null || Array.isArray(tool2)) {
1209
- errors.push({
1210
- key: "tool",
1211
- expected: "object",
1212
- actual: typeof tool2
1213
- });
1454
+ errors.push({ key: "tool", expected: "object", actual: typeof tool2 });
1214
1455
  } 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
- });
1456
+ errors.push({ key: "tool.enabled", expected: "boolean", actual: typeof tool2.enabled });
1220
1457
  }
1221
1458
  }
1222
1459
  return errors;
1223
1460
  }
1224
- function needsSignalsMigrationHint(configData) {
1461
+ function legacyDriftThreshold(configData) {
1225
1462
  const autoPrune = configData.autoPrune;
1226
- return autoPrune !== null && typeof autoPrune === "object" && !Array.isArray(autoPrune) && autoPrune.enabled === true && autoPrune.signals === void 0;
1463
+ if (autoPrune === null || typeof autoPrune !== "object" || Array.isArray(autoPrune)) {
1464
+ return void 0;
1465
+ }
1466
+ const value = autoPrune.driftThreshold;
1467
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1 ? value : void 0;
1227
1468
  }
1228
1469
  function showConfigWarnings(ctx, configPath, configData, isProject) {
1229
1470
  const invalidKeys = getInvalidConfigKeys(configData);
1230
1471
  const deprecatedKeys = getDeprecatedConfigKeys(configData);
1231
1472
  const typeErrors = validateConfigTypes(configData);
1232
- const signalsHint = needsSignalsMigrationHint(configData);
1233
- if (!signalsHint && invalidKeys.length === 0 && deprecatedKeys.length === 0 && typeErrors.length === 0) {
1473
+ if (invalidKeys.length === 0 && deprecatedKeys.length === 0 && typeErrors.length === 0) {
1234
1474
  return;
1235
1475
  }
1236
1476
  const configType = isProject ? "project config" : "config";
1237
1477
  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
1478
  if (deprecatedKeys.length > 0) {
1244
1479
  const keyList = deprecatedKeys.slice(0, 3).join(", ");
1245
1480
  const suffix = deprecatedKeys.length > 3 ? ` (+${deprecatedKeys.length - 3} more)` : "";
1246
1481
  messages.push(
1247
- `Removed legacy compression keys are ignored: ${keyList}${suffix}. Pruning now runs through OpenCode's native compaction.`
1482
+ `Removed legacy keys are ignored: ${keyList}${suffix}. Compression now runs as dynamic request-time folding (dtc.*).`
1248
1483
  );
1249
1484
  }
1250
1485
  if (invalidKeys.length > 0) {
@@ -1279,17 +1514,10 @@ var defaultConfig = {
1279
1514
  enabled: true,
1280
1515
  autoUpdate: true,
1281
1516
  debug: false,
1282
- language: "zh",
1283
1517
  commands: {
1284
1518
  enabled: true
1285
1519
  },
1286
- experimental: {
1287
- customPrompts: false
1288
- },
1289
- summarize: {
1290
- failureCooldownMs: DEFAULT_FAILURE_COOLDOWN_MS
1291
- },
1292
- autoPrune: { ...DEFAULT_AUTO_PRUNE },
1520
+ dtc: { ...DEFAULT_DTC },
1293
1521
  tool: {
1294
1522
  enabled: true
1295
1523
  }
@@ -1367,41 +1595,19 @@ function mergeCommands(base, override) {
1367
1595
  enabled: typeof override.enabled === "boolean" ? override.enabled : base.enabled
1368
1596
  };
1369
1597
  }
1370
- function mergeExperimental(base, override) {
1371
- if (!override) {
1372
- return base;
1373
- }
1374
- return {
1375
- customPrompts: typeof override.customPrompts === "boolean" ? override.customPrompts : base.customPrompts
1376
- };
1377
- }
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) {
1598
+ function mergeDtc(base, override, legacyDrift) {
1599
+ const driftFallback = legacyDrift ?? base.driftThreshold;
1387
1600
  if (!override || typeof override !== "object" || Array.isArray(override)) {
1388
- return base;
1601
+ return { ...base, driftThreshold: driftFallback };
1389
1602
  }
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;
1603
+ const number = (key, min, max) => typeof override[key] === "number" && Number.isFinite(override[key]) && override[key] >= min && override[key] <= max ? override[key] : base[key];
1397
1604
  return {
1398
1605
  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)
1606
+ tailTurns: number("tailTurns", 0, Number.POSITIVE_INFINITY),
1607
+ lowWatermarkRatio: number("lowWatermarkRatio", 0, 1),
1608
+ targetRatio: number("targetRatio", 0, 1),
1609
+ driftThreshold: typeof override.driftThreshold === "number" && Number.isFinite(override.driftThreshold) && override.driftThreshold >= 0 && override.driftThreshold <= 1 ? override.driftThreshold : driftFallback,
1610
+ toolOutputKeepChars: number("toolOutputKeepChars", 200, Number.POSITIVE_INFINITY)
1405
1611
  };
1406
1612
  }
1407
1613
  function mergeTool(base, override) {
@@ -1416,9 +1622,7 @@ function deepCloneConfig(config) {
1416
1622
  return {
1417
1623
  ...config,
1418
1624
  commands: { ...config.commands },
1419
- experimental: { ...config.experimental },
1420
- summarize: { ...config.summarize },
1421
- autoPrune: { ...config.autoPrune, signals: { ...config.autoPrune.signals } },
1625
+ dtc: { ...config.dtc },
1422
1626
  tool: { ...config.tool }
1423
1627
  };
1424
1628
  }
@@ -1427,11 +1631,8 @@ function mergeLayer(config, data) {
1427
1631
  enabled: typeof data.enabled === "boolean" ? data.enabled : config.enabled,
1428
1632
  autoUpdate: typeof data.autoUpdate === "boolean" ? data.autoUpdate : config.autoUpdate,
1429
1633
  debug: typeof data.debug === "boolean" ? data.debug : config.debug,
1430
- language: data.language === "zh" || data.language === "en" ? data.language : config.language,
1431
1634
  commands: mergeCommands(config.commands, data.commands),
1432
- experimental: mergeExperimental(config.experimental, data.experimental),
1433
- summarize: mergeSummarize(config.summarize, data.summarize),
1434
- autoPrune: mergeAutoPrune(config.autoPrune, data.autoPrune),
1635
+ dtc: mergeDtc(config.dtc, data.dtc, legacyDriftThreshold(data)),
1435
1636
  tool: mergeTool(config.tool, data.tool)
1436
1637
  };
1437
1638
  }
@@ -1485,225 +1686,76 @@ Using previous/default values`
1485
1686
  return config;
1486
1687
  }
1487
1688
 
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 {
1689
+ // lib/dtc/state.ts
1690
+ var SESSION_LIMIT = 500;
1691
+ var DIGEST_LIMIT = 2e3;
1692
+ function lruSet(map, key, value, limit) {
1693
+ if (map.has(key)) map.delete(key);
1694
+ map.set(key, value);
1695
+ while (map.size > limit) {
1696
+ const oldest = map.keys().next().value;
1697
+ if (oldest === void 0) break;
1698
+ map.delete(oldest);
1699
+ }
1700
+ }
1701
+ var DtcState = class {
1494
1702
  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
- );
1703
+ digests = /* @__PURE__ */ new Map();
1704
+ observeContextLimit(sessionID, contextTokens) {
1705
+ if (!sessionID || !contextTokens || !Number.isFinite(contextTokens) || contextTokens <= 0) {
1527
1706
  return;
1528
1707
  }
1529
- if (statusType === "idle") {
1530
- this.observeIdle(sessionID, entry, "status");
1531
- }
1708
+ const state = this.session(sessionID);
1709
+ state.contextTokens = contextTokens;
1710
+ lruSet(this.sessions, sessionID, state, SESSION_LIMIT);
1532
1711
  }
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");
1712
+ contextTokens(sessionID) {
1713
+ return this.sessions.get(sessionID)?.contextTokens;
1538
1714
  }
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
- });
1715
+ /** Called by the compacting hook; consumed by the very next transform. */
1716
+ armCompactionSkip(sessionID) {
1717
+ const state = this.session(sessionID);
1718
+ state.skipNextTransform = true;
1719
+ lruSet(this.sessions, sessionID, state, SESSION_LIMIT);
1564
1720
  }
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;
1721
+ consumeCompactionSkip(sessionID) {
1722
+ const state = this.sessions.get(sessionID);
1723
+ if (!state?.skipNextTransform) return false;
1724
+ state.skipNextTransform = false;
1725
+ return true;
1575
1726
  }
1576
- dispose(sessionID) {
1577
- this.observeDeleted(sessionID);
1727
+ /** `dcp_prune` / `/dcp fold`: mark a boundary now and deepen folding. */
1728
+ markBoundary(sessionID, at, minLevel = 2) {
1729
+ const state = this.session(sessionID);
1730
+ state.boundaryMarkAt = at;
1731
+ state.minLevel = Math.max(state.minLevel ?? 0, minLevel);
1732
+ lruSet(this.sessions, sessionID, state, SESSION_LIMIT);
1578
1733
  }
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
- });
1734
+ boundaryMark(sessionID) {
1735
+ return this.sessions.get(sessionID)?.boundaryMarkAt;
1610
1736
  }
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
- }
1737
+ minLevel(sessionID) {
1738
+ return this.sessions.get(sessionID)?.minLevel ?? 0;
1678
1739
  }
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;
1740
+ cachedDigest(key) {
1741
+ return this.digests.get(key);
1689
1742
  }
1690
- refresh(sessionID, entry) {
1743
+ storeDigest(key, digest) {
1744
+ lruSet(this.digests, key, digest, DIGEST_LIMIT);
1745
+ }
1746
+ dropSession(sessionID) {
1691
1747
  this.sessions.delete(sessionID);
1692
- this.sessions.set(sessionID, entry);
1693
- this.evictIfNeeded(sessionID);
1694
1748
  }
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);
1749
+ /** Test/inspection surface. */
1750
+ stats() {
1751
+ return { sessions: this.sessions.size, digests: this.digests.size };
1752
+ }
1753
+ session(sessionID) {
1754
+ return this.sessions.get(sessionID) ?? {};
1702
1755
  }
1703
1756
  };
1704
- function retrySeconds(retryAfterMs) {
1705
- return Math.ceil(retryAfterMs / 1e3);
1706
- }
1757
+
1758
+ // lib/session-events.ts
1707
1759
  function eventSessionID(properties) {
1708
1760
  const direct = properties?.sessionID;
1709
1761
  if (typeof direct === "string" && direct) return direct;
@@ -1713,162 +1765,138 @@ function eventSessionID(properties) {
1713
1765
  }
1714
1766
 
1715
1767
  // lib/hooks.ts
1716
- function createSessionCompactingHandler(prompts, logger) {
1717
- return async (input, output) => {
1768
+ var DEFAULT_TAIL_TURNS = 4;
1769
+ var DEFAULT_PRESERVE_RECENT_TOKENS = 32e3;
1770
+ function createSessionCompactingHandler(deps) {
1771
+ return async (input, _output) => {
1718
1772
  try {
1719
- prompts.reload();
1720
- const prompt = prompts.getRuntimePrompts().compaction;
1721
- if (!output.prompt) {
1722
- output.prompt = prompt;
1723
- } else if (!output.context.includes(prompt)) {
1724
- output.context.push(prompt);
1725
- }
1726
- logger.debug("Applied semantic pruning prompt", { sessionId: input.sessionID });
1773
+ deps.state.armCompactionSkip(input.sessionID);
1774
+ deps.logger.debug("Armed DTC skip for the native compaction input", {
1775
+ sessionId: input.sessionID
1776
+ });
1727
1777
  } catch (error) {
1728
- logger.warn("Failed to apply semantic pruning prompt; native compaction continues", {
1778
+ deps.logger.warn("Failed to arm the compaction skip; native compaction continues", {
1729
1779
  sessionId: input.sessionID,
1730
1780
  error: error instanceof Error ? error.message : String(error)
1731
1781
  });
1732
1782
  }
1733
1783
  };
1734
1784
  }
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) {
1785
+ function createChatParamsHandler(deps) {
1741
1786
  return async (input, _output) => {
1742
- const parts = _output?.parts ?? [];
1743
- autoPruner.observeUserMessage(input.sessionID, parts);
1787
+ try {
1788
+ const context = input.model?.limit?.context;
1789
+ if (typeof context === "number" && Number.isFinite(context) && context > 0) {
1790
+ deps.state.observeContextLimit(input.sessionID, context);
1791
+ }
1792
+ } catch (error) {
1793
+ deps.logger.debug("chat.params observation failed", {
1794
+ error: error instanceof Error ? error.message : String(error)
1795
+ });
1796
+ }
1797
+ };
1798
+ }
1799
+ function createTransformHandler(deps) {
1800
+ return async (_input, output) => {
1801
+ try {
1802
+ if (!Array.isArray(output?.messages) || output.messages.length === 0) return;
1803
+ transformMessages(output.messages, {
1804
+ state: deps.state,
1805
+ config: deps.config,
1806
+ logger: deps.logger
1807
+ });
1808
+ } catch (error) {
1809
+ deps.logger.warn("DTC transform failed; the request proceeds unfolded", {
1810
+ error: error instanceof Error ? error.message : String(error)
1811
+ });
1812
+ }
1744
1813
  };
1745
1814
  }
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
1815
  function createEventHandler(deps) {
1752
1816
  return async (input) => {
1753
- const event = input.event;
1754
1817
  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
- }
1818
+ if (input.event.type !== "session.deleted") return;
1819
+ const sessionID = eventSessionID(input.event.properties);
1820
+ if (sessionID) deps.state.dropSession(sessionID);
1765
1821
  } catch (error) {
1766
1822
  deps.logger.warn("Event handler failed", {
1767
- type: event.type,
1823
+ type: input.event.type,
1768
1824
  error: error instanceof Error ? error.message : String(error)
1769
1825
  });
1770
1826
  }
1771
1827
  };
1772
1828
  }
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") {
1829
+ async function showToast(client, title, message, variant = "info") {
1830
+ await client.tui.showToast({
1831
+ body: { title, message, variant, duration: 5e3 }
1832
+ }).catch(() => void 0);
1833
+ }
1834
+ function createCommandExecuteHandler(deps) {
1835
+ return async (input, _output) => {
1836
+ if (input.command !== "dcp") return;
1837
+ const subcommand = (input.arguments ?? "").trim().split(/\s+/, 1)[0]?.toLowerCase();
1838
+ if (subcommand === "fold") {
1839
+ deps.state.markBoundary(input.sessionID, Date.now(), 3);
1779
1840
  await showToast(
1780
1841
  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"
1842
+ "DCP fold",
1843
+ "\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
1844
  );
1845
+ deps.logger.debug("Handled DCP fold command", { sessionId: input.sessionID });
1846
+ throw new Error("__DCP_FOLD_HANDLED__");
1847
+ }
1848
+ if (subcommand === "status") {
1849
+ const message = await buildStatusMessage(deps, input.sessionID);
1850
+ await showToast(deps.client, "DCP status", message);
1851
+ throw new Error("__DCP_STATUS_HANDLED__");
1785
1852
  }
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
1853
  await showToast(
1804
1854
  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"
1855
+ "DCP",
1856
+ "\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
1857
  );
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);
1858
+ throw new Error("__DCP_HELP_HANDLED__");
1817
1859
  };
1818
1860
  }
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
- );
1861
+ async function buildStatusMessage(deps, sessionID) {
1862
+ try {
1863
+ const response = await deps.client.session.messages({ path: { id: sessionID } });
1864
+ const data = response.data ?? response;
1865
+ const messages = Array.isArray(data) ? data : [];
1866
+ const turns = segmentTurns(messages);
1867
+ const estimated = estimateSlice(messages, 0, messages.length);
1868
+ const context = deps.state.contextTokens(sessionID);
1869
+ const tail = Math.min(deps.config.tailTurns, turns.length);
1870
+ const lines = [
1871
+ `\u6D88\u606F ${messages.length} \u6761 / \u5BF9\u8BDD\u8F6E ${turns.length}\uFF08\u5C3E\u90E8\u4FDD\u62A4 ${tail} \u8F6E\uFF09`,
1872
+ `\u4F30\u7B97 ${estimated.toLocaleString()} tokens` + (context ? ` / \u4E0A\u4E0B\u6587\u7A97\u53E3 ${context.toLocaleString()}` : "\uFF08\u7A97\u53E3\u672A\u77E5\uFF0C\u6682\u672A\u6298\u53E0\uFF09"),
1873
+ `\u624B\u52A8\u964D\u7EA7\u6863\u4F4D\uFF1A${deps.state.minLevel(sessionID)}`
1874
+ ];
1875
+ return lines.join("\n");
1876
+ } catch {
1877
+ return "\u65E0\u6CD5\u8BFB\u53D6\u4F1A\u8BDD\u72B6\u6001\u3002";
1878
+ }
1879
+ }
1880
+ function createConfigHandler(config, logger) {
1881
+ return async (opencodeConfig) => {
1882
+ const compaction = opencodeConfig.compaction ??= {};
1883
+ if (compaction.tail_turns === void 0) {
1884
+ compaction.tail_turns = DEFAULT_TAIL_TURNS;
1885
+ }
1886
+ if (compaction.preserve_recent_tokens === void 0) {
1887
+ compaction.preserve_recent_tokens = DEFAULT_PRESERVE_RECENT_TOKENS;
1888
+ }
1889
+ if (config.commands.enabled) {
1890
+ opencodeConfig.command ??= {};
1891
+ opencodeConfig.command.dcp = {
1892
+ template: "",
1893
+ description: "Dynamic context pruning: /dcp fold | /dcp status"
1894
+ };
1866
1895
  }
1867
- logger.debug("Handled DCP summarize command", {
1868
- sessionId: input.sessionID,
1869
- status: result.status
1896
+ logger.debug("Applied host config defaults", {
1897
+ tailTurns: compaction.tail_turns,
1898
+ preserveRecentTokens: compaction.preserve_recent_tokens
1870
1899
  });
1871
- throw new Error("__DCP_SUMMARIZE_HANDLED__");
1872
1900
  };
1873
1901
  }
1874
1902
 
@@ -2066,451 +2094,35 @@ var Logger = class {
2066
2094
  }
2067
2095
  };
2068
2096
 
2069
- // lib/prompts/store.ts
2070
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
2071
- import { homedir as homedir3 } from "os";
2072
- import { dirname as dirname2, join as join3 } from "path";
2073
-
2074
- // 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
2076
-
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
2078
-
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
2084
-
2085
- \u4F7F\u7528\u4EE5\u4E0B\u56FA\u5B9A\u7ED3\u6784\uFF0C\u7701\u7565\u786E\u5B9E\u4E3A\u7A7A\u7684\u6761\u76EE\uFF1A
2086
-
2087
- ## \u5386\u53F2\u6982\u8981
2088
- \u65E9\u671F\u4E0E\u4E2D\u90E8\u5386\u53F2\u7684\u4E3B\u9898\u548C\u80CC\u666F\uFF0C\u6BCF\u9879\u4E00\u53E5\u8BDD\u7ED3\u8BBA\uFF1B\u8FDC\u671F\u5DF2\u5B8C\u6210\u4EFB\u52A1\u5F52\u5165\u6B64\u5904\uFF0C\u4E0D\u542B\u6267\u884C\u8FC7\u7A0B\u3002
2089
-
2090
- ## \u5DF2\u5B8C\u6210\u4EFB\u52A1\u7684\u6982\u62EC
2091
- \u6700\u8FD1\u7684\u5DF2\u5B8C\u6210\u4EFB\u52A1\uFF0C\u6BCF\u4E2A\u4EFB\u52A1\u4E00\u53E5\u8BDD\u6982\u62EC\u5176\u7ED3\u679C\u4E0E\u5173\u952E\u4EA7\u51FA\u3002
2092
-
2093
- ## \u8FDB\u884C\u4E2D\u4EFB\u52A1\u8BE6\u60C5
2094
- \u5F53\u524D\u6B63\u5728\u8FDB\u884C\u7684\u4EFB\u52A1\u9010\u9879\u5199\u6E05\uFF1A\u76EE\u6807\u3001\u5DF2\u5B8C\u6210\u6B65\u9AA4\u3001\u6D89\u53CA\u6587\u4EF6\u8DEF\u5F84\u4E0E\u63A5\u53E3\u3001\u5173\u952E\u51B3\u7B56\u3001\u9047\u5230\u7684\u963B\u585E\u3001\u4E0B\u4E00\u6B65\u5177\u4F53\u52A8\u4F5C\u3002\u672C\u8282\u5C5E\u4E8E\u8F7B\u5EA6\u538B\u7F29\u533A\uFF0C\u5B81\u53EF\u591A\u4FDD\u7559\u7EC6\u8282\uFF0C\u4E0D\u505A\u4E8C\u6B21\u63A8\u65AD\u3002
2095
-
2096
- ## \u672A\u89E3\u51B3\u95EE\u9898
2097
- \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
-
2099
- \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.
2101
-
2102
- This is not a chat-log summary but a semantic pruning result one can resume working from directly. Compress by these rules:
2103
-
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.
2109
-
2110
- Use the following fixed structure, omitting sections that are truly empty:
2111
-
2112
- ## History Overview
2113
- Topics and background from early and middle history, one concluding sentence each; long-completed tasks belong here, without execution process.
2114
-
2115
- ## Completed Task Summaries
2116
- Recently completed tasks, one sentence per task covering its outcome and key outputs.
2117
-
2118
- ## In-Progress Task Details
2119
- For the current task, itemize: goal, completed steps, file paths and interfaces involved, key decisions, blockers encountered, concrete next actions. This is the lightly-compressed zone; prefer keeping more detail and avoid second-order inference.
2120
-
2121
- ## Unresolved Issues
2122
- Cross-task lingering risks and items awaiting confirmation. Only list items not already covered under In-Progress Task Details, to avoid duplication.
2123
-
2124
- Stay specific, verifiable, and project-cohesive. An in-progress task must be resumable straight from this checkpoint, without relying on intermediate process that was pruned away. Keep hard facts such as file paths, interfaces, commands, test results, and error facts, but never message IDs, block IDs, anchors, placeholders, control tags, or procedural chatter.`;
2125
- function getCompactionPrompt(language) {
2126
- return language === "en" ? COMPACTION_EN : COMPACTION;
2127
- }
2128
-
2129
- // lib/prompts/store.ts
2130
- function findOpencodeDir2(startDir) {
2131
- let current = startDir;
2132
- while (current !== "/") {
2133
- const candidate = join3(current, ".opencode");
2134
- if (existsSync3(candidate) && statSync2(candidate).isDirectory()) return candidate;
2135
- const parent = dirname2(current);
2136
- if (parent === current) break;
2137
- current = parent;
2138
- }
2139
- return null;
2140
- }
2141
- function resolvePaths(workingDirectory) {
2142
- const configHome = process.env.XDG_CONFIG_HOME || join3(homedir3(), ".config");
2143
- const globalRoot = join3(configHome, "opencode", "dcp-prompts");
2144
- const opencodeDir = findOpencodeDir2(workingDirectory);
2145
- return {
2146
- defaultsDir: join3(globalRoot, "defaults"),
2147
- overrides: [
2148
- opencodeDir ? join3(opencodeDir, "dcp-prompts", "overrides", "compaction.md") : "",
2149
- process.env.OPENCODE_CONFIG_DIR ? join3(process.env.OPENCODE_CONFIG_DIR, "dcp-prompts", "overrides", "compaction.md") : "",
2150
- join3(globalRoot, "overrides", "compaction.md")
2151
- ].filter(Boolean)
2152
- };
2153
- }
2154
- function normalize(content) {
2155
- return content.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n").replace(/<!--[\s\S]*?-->/g, "").trim();
2156
- }
2157
- var PromptStore = class {
2158
- constructor(logger, workingDirectory, customPromptsEnabled = false, language) {
2159
- this.logger = logger;
2160
- this.customPromptsEnabled = customPromptsEnabled;
2161
- this.paths = resolvePaths(workingDirectory);
2162
- this.defaultPrompt = getCompactionPrompt(language);
2163
- this.runtime = { compaction: this.defaultPrompt };
2164
- if (customPromptsEnabled) this.ensureDefaults();
2165
- this.reload();
2166
- }
2167
- paths;
2168
- defaultPrompt;
2169
- runtime;
2170
- lastReloadAt = 0;
2171
- getRuntimePrompts() {
2172
- return { ...this.runtime };
2173
- }
2174
- reload() {
2175
- const now = Date.now();
2176
- if (now - this.lastReloadAt < 1e3) return;
2177
- this.lastReloadAt = now;
2178
- this.runtime = { compaction: this.defaultPrompt };
2179
- if (!this.customPromptsEnabled) return;
2180
- for (const path of this.paths.overrides) {
2181
- if (!existsSync3(path)) continue;
2182
- try {
2183
- const prompt = normalize(readFileSync2(path, "utf-8"));
2184
- if (prompt) this.runtime = { compaction: prompt };
2185
- return;
2186
- } catch (error) {
2187
- this.logger.warn("Failed to load compaction prompt override", {
2188
- path,
2189
- error: error instanceof Error ? error.message : String(error)
2190
- });
2191
- }
2192
- }
2193
- }
2194
- ensureDefaults() {
2195
- try {
2196
- mkdirSync2(this.paths.defaultsDir, { recursive: true });
2197
- writeFileSync2(
2198
- join3(this.paths.defaultsDir, "compaction.md"),
2199
- `${this.defaultPrompt.trim()}
2200
- `
2201
- );
2202
- writeFileSync2(
2203
- join3(this.paths.defaultsDir, "README.md"),
2204
- "# DCP compaction prompt\n\nCopy `compaction.md` to an `overrides` directory and restart OpenCode.\n"
2205
- );
2206
- } catch (error) {
2207
- this.logger.warn("Failed to write bundled compaction prompt", {
2208
- error: error instanceof Error ? error.message : String(error)
2209
- });
2210
- }
2211
- }
2212
- };
2213
-
2214
2097
  // lib/prune-tool.ts
2215
2098
  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
2099
  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
2100
+ 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
2101
 
2397
2102
  \u4EC5\u5728\u8FD9\u4E9B\u60C5\u51B5\u4E0B\u8C03\u7528\uFF1A
2398
2103
  - \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
2104
  - \u7528\u6237\u660E\u786E\u8981\u6C42\u538B\u7F29\u4E0A\u4E0B\u6587\u3002
2400
2105
 
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`;
2106
+ \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
2107
  function createPruneTool(deps) {
2403
2108
  return tool({
2404
2109
  description: PRUNE_TOOL_DESCRIPTION,
2405
2110
  args: {},
2406
2111
  execute: async (_args, context) => {
2407
2112
  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`;
2113
+ const now = (deps.now ?? Date.now)();
2114
+ deps.state.markBoundary(sessionID, now, 2);
2115
+ deps.logger.debug("Prune tool marked a topic boundary", {
2116
+ sessionId: sessionID
2117
+ });
2118
+ 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
2119
  }
2429
2120
  });
2430
2121
  }
2431
2122
 
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
2123
  // lib/update.ts
2512
2124
  import { readFile } from "fs/promises";
2513
- import { basename, dirname as dirname3, join as join4 } from "path";
2125
+ import { basename, dirname as dirname2, join as join3 } from "path";
2514
2126
  import { fileURLToPath } from "url";
2515
2127
  var PACKAGE_NAME = "@lexwdex-org/opencode-dcp";
2516
2128
  function startAutoUpdate(ctx, enabled) {
@@ -2536,7 +2148,7 @@ function startAutoUpdate(ctx, enabled) {
2536
2148
  async function checkAutoUpdate(signal) {
2537
2149
  const packageDir = await findPackageDir(PACKAGE_NAME);
2538
2150
  if (!packageDir) return { available: false };
2539
- const pkg = await readPackageJson(join4(packageDir, "package.json"));
2151
+ const pkg = await readPackageJson(join3(packageDir, "package.json"));
2540
2152
  if (!pkg?.name || !pkg.version) return { available: false };
2541
2153
  const latest = await fetchLatestVersion(pkg.name, signal);
2542
2154
  if (!latest || !isVersionNewer(latest, pkg.version)) return { available: false };
@@ -2545,21 +2157,21 @@ async function checkAutoUpdate(signal) {
2545
2157
  return { available: true, name: pkg.name, current: pkg.version, latest };
2546
2158
  }
2547
2159
  async function findPackageDir(name) {
2548
- let dir = dirname3(fileURLToPath(import.meta.url));
2160
+ let dir = dirname2(fileURLToPath(import.meta.url));
2549
2161
  for (; ; ) {
2550
- const pkg = await readPackageJson(join4(dir, "package.json"));
2162
+ const pkg = await readPackageJson(join3(dir, "package.json"));
2551
2163
  if (pkg?.name === name) return dir;
2552
- const parent = dirname3(dir);
2164
+ const parent = dirname2(dir);
2553
2165
  if (parent === dir) return void 0;
2554
2166
  dir = parent;
2555
2167
  }
2556
2168
  }
2557
2169
  async function findAutoUpdateWrapperDir(packageDir, name) {
2558
- const packageParent = dirname3(packageDir);
2559
- const nodeModulesDir = basename(packageParent).startsWith("@") ? dirname3(packageParent) : packageParent;
2170
+ const packageParent = dirname2(packageDir);
2171
+ const nodeModulesDir = basename(packageParent).startsWith("@") ? dirname2(packageParent) : packageParent;
2560
2172
  if (basename(nodeModulesDir) !== "node_modules") return void 0;
2561
- const wrapperDir = dirname3(nodeModulesDir);
2562
- const wrapperPkg = await readPackageJson(join4(wrapperDir, "package.json"));
2173
+ const wrapperDir = dirname2(nodeModulesDir);
2174
+ const wrapperPkg = await readPackageJson(join3(wrapperDir, "package.json"));
2563
2175
  const spec = wrapperSpec(wrapperDir, name) ?? wrapperPkg?.dependencies?.[name];
2564
2176
  if (!spec || !isAutoUpdatableSpec(spec)) return void 0;
2565
2177
  return wrapperDir;
@@ -2567,7 +2179,7 @@ async function findAutoUpdateWrapperDir(packageDir, name) {
2567
2179
  function wrapperSpec(wrapperDir, name) {
2568
2180
  if (name.startsWith("@")) {
2569
2181
  const [scope, pkg] = name.split("/");
2570
- if (!scope || !pkg || basename(dirname3(wrapperDir)) !== scope) return void 0;
2182
+ if (!scope || !pkg || basename(dirname2(wrapperDir)) !== scope) return void 0;
2571
2183
  const prefix2 = `${pkg}@`;
2572
2184
  const base2 = basename(wrapperDir);
2573
2185
  return base2.startsWith(prefix2) ? base2.slice(prefix2.length) : void 0;
@@ -2648,63 +2260,45 @@ var server = (async (ctx) => {
2648
2260
  const config = getConfig(ctx);
2649
2261
  if (!config.enabled) return {};
2650
2262
  const logger = new Logger(config.debug);
2651
- const prompts = new PromptStore(
2652
- logger,
2653
- ctx.directory,
2654
- config.experimental.customPrompts,
2655
- config.language
2656
- );
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
- }
2263
+ const state = new DtcState();
2673
2264
  logger.info("DCP initialized", {
2674
2265
  commands: config.commands.enabled,
2675
- autoPrune: config.autoPrune.enabled,
2676
- tool: config.tool.enabled,
2677
- customPrompts: config.experimental.customPrompts
2266
+ dtc: config.dtc.enabled,
2267
+ tool: config.tool.enabled
2678
2268
  });
2679
2269
  startAutoUpdate(ctx, config.autoUpdate);
2680
2270
  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,
2271
+ // THE compression surface: dynamic tiered folding on every model
2272
+ // request, plus the chat.params feed that teaches the engine each
2273
+ // session's context-window size. The compacting hook rides along
2274
+ // solely to keep the host's native compaction input unfolded — the
2275
+ // native prompt is never touched. With DTC off, DCP registers no
2276
+ // compaction-adjacent hook at all.
2277
+ ...config.dtc.enabled && {
2278
+ "experimental.chat.messages.transform": createTransformHandler({
2279
+ state,
2280
+ config: config.dtc,
2692
2281
  logger
2693
- })
2282
+ }),
2283
+ "chat.params": createChatParamsHandler({ state, logger }),
2284
+ "experimental.session.compacting": createSessionCompactingHandler({ state, logger })
2694
2285
  },
2286
+ // Lifecycle cleanup for DTC session state (LRU-bounded regardless).
2287
+ event: createEventHandler({ state, logger }),
2695
2288
  ...config.tool.enabled && {
2696
- tool: { [PRUNE_TOOL_NAME]: createPruneTool({ prune, logger }) }
2289
+ tool: { [PRUNE_TOOL_NAME]: createPruneTool({ state, logger }) }
2697
2290
  },
2698
2291
  ...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
- }
2292
+ "command.execute.before": createCommandExecuteHandler({
2293
+ client: ctx.client,
2294
+ state,
2295
+ config: config.dtc,
2296
+ logger
2297
+ })
2298
+ },
2299
+ // Always registered: besides the optional /dcp command it raises the
2300
+ // host's compaction tail protection to DCP's tiered defaults.
2301
+ config: createConfigHandler(config, logger)
2708
2302
  };
2709
2303
  });
2710
2304
  var index_default = server;