@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.
- package/README.en.md +60 -47
- package/README.md +45 -66
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +699 -1105
- package/dist/index.js.map +1 -1
- package/dist/lib/config.d.ts +7 -32
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/dtc/digest.d.ts +16 -0
- package/dist/lib/dtc/digest.d.ts.map +1 -0
- package/dist/lib/dtc/engine.d.ts +64 -0
- package/dist/lib/dtc/engine.d.ts.map +1 -0
- package/dist/lib/dtc/state.d.ts +28 -0
- package/dist/lib/dtc/state.d.ts.map +1 -0
- package/dist/lib/dtc/types.d.ts +45 -0
- package/dist/lib/dtc/types.d.ts.map +1 -0
- package/dist/lib/hooks.d.ts +60 -31
- package/dist/lib/hooks.d.ts.map +1 -1
- package/dist/lib/prune-tool.d.ts +3 -2
- package/dist/lib/prune-tool.d.ts.map +1 -1
- package/dist/lib/session-events.d.ts +7 -0
- package/dist/lib/session-events.d.ts.map +1 -0
- package/dist/lib/text.d.ts +19 -0
- package/dist/lib/text.d.ts.map +1 -0
- package/package.json +2 -3
- package/dist/lib/auto-prune.d.ts +0 -20
- package/dist/lib/auto-prune.d.ts.map +0 -1
- package/dist/lib/prompts/compaction.d.ts +0 -4
- package/dist/lib/prompts/compaction.d.ts.map +0 -1
- package/dist/lib/prompts/store.d.ts +0 -17
- package/dist/lib/prompts/store.d.ts.map +0 -1
- package/dist/lib/prune-service.d.ts +0 -104
- package/dist/lib/prune-service.d.ts.map +0 -1
- package/dist/lib/session-boundary.d.ts +0 -90
- package/dist/lib/session-boundary.d.ts.map +0 -1
- package/dist/lib/session-model.d.ts +0 -8
- package/dist/lib/session-model.d.ts.map +0 -1
- package/dist/lib/summarize.d.ts +0 -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
|
|
982
|
-
var DEFAULT_AUTO_PRUNE = {
|
|
1301
|
+
var DEFAULT_DTC = {
|
|
983
1302
|
enabled: true,
|
|
984
|
-
|
|
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
|
-
"
|
|
1000
|
-
"
|
|
1001
|
-
"
|
|
1002
|
-
"
|
|
1003
|
-
"
|
|
1004
|
-
"
|
|
1005
|
-
"
|
|
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
|
|
1114
|
-
if (
|
|
1115
|
-
if (typeof
|
|
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 (
|
|
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
|
-
["
|
|
1158
|
-
["
|
|
1433
|
+
["tailTurns", 0, Number.POSITIVE_INFINITY],
|
|
1434
|
+
["lowWatermarkRatio", 0, 1],
|
|
1435
|
+
["targetRatio", 0, 1],
|
|
1159
1436
|
["driftThreshold", 0, 1],
|
|
1160
|
-
["
|
|
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 =
|
|
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: `
|
|
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
|
|
1461
|
+
function legacyDriftThreshold(configData) {
|
|
1225
1462
|
const autoPrune = configData.autoPrune;
|
|
1226
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1371
|
-
|
|
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
|
|
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
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
driftThreshold: number(
|
|
1403
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
1489
|
-
var
|
|
1490
|
-
var
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
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
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
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
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1708
|
+
const state = this.session(sessionID);
|
|
1709
|
+
state.contextTokens = contextTokens;
|
|
1710
|
+
lruSet(this.sessions, sessionID, state, SESSION_LIMIT);
|
|
1532
1711
|
}
|
|
1533
|
-
|
|
1534
|
-
|
|
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
|
-
/**
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
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
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
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
|
-
|
|
1577
|
-
|
|
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
|
-
|
|
1580
|
-
|
|
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
|
-
|
|
1612
|
-
|
|
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
|
-
|
|
1680
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
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
|
-
|
|
1705
|
-
|
|
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
|
-
|
|
1717
|
-
|
|
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
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1743
|
-
|
|
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
|
-
|
|
1756
|
-
const sessionID = eventSessionID(event.properties);
|
|
1757
|
-
if (
|
|
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
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
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
|
|
1782
|
-
|
|
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
|
|
1806
|
-
|
|
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
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
const
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
}
|
|
1841
|
-
if (
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
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("
|
|
1868
|
-
|
|
1869
|
-
|
|
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 = `\
|
|
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\
|
|
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
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
2160
|
+
let dir = dirname2(fileURLToPath(import.meta.url));
|
|
2549
2161
|
for (; ; ) {
|
|
2550
|
-
const pkg = await readPackageJson(
|
|
2162
|
+
const pkg = await readPackageJson(join3(dir, "package.json"));
|
|
2551
2163
|
if (pkg?.name === name) return dir;
|
|
2552
|
-
const parent =
|
|
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 =
|
|
2559
|
-
const nodeModulesDir = basename(packageParent).startsWith("@") ?
|
|
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 =
|
|
2562
|
-
const wrapperPkg = await readPackageJson(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
//
|
|
2686
|
-
//
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
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({
|
|
2289
|
+
tool: { [PRUNE_TOOL_NAME]: createPruneTool({ state, logger }) }
|
|
2697
2290
|
},
|
|
2698
2291
|
...config.commands.enabled && {
|
|
2699
|
-
"command.execute.before": createCommandExecuteHandler(
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
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;
|