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