@lexwdex-org/opencode-dcp 3.5.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +72 -47
- package/README.md +43 -52
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +698 -918
- package/dist/index.js.map +1 -1
- package/dist/lib/config.d.ts +7 -26
- 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 +62 -25
- 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/auto-prune.d.ts +0 -20
- package/dist/lib/auto-prune.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,16 +862,403 @@ var ParseErrorCode;
|
|
|
977
862
|
ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter";
|
|
978
863
|
})(ParseErrorCode || (ParseErrorCode = {}));
|
|
979
864
|
|
|
865
|
+
// lib/text.ts
|
|
866
|
+
function tokenize(text) {
|
|
867
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
868
|
+
for (const match of text.toLowerCase().matchAll(/[\p{L}\p{N}]+/gu)) {
|
|
869
|
+
const word = match[0];
|
|
870
|
+
if (/[\u4e00-\u9fff]/.test(word)) {
|
|
871
|
+
if (word.length === 1) {
|
|
872
|
+
tokens.add(word);
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
for (let index = 0; index < word.length - 1; index++) {
|
|
876
|
+
tokens.add(word.slice(index, index + 2));
|
|
877
|
+
}
|
|
878
|
+
} else {
|
|
879
|
+
tokens.add(word);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return tokens;
|
|
883
|
+
}
|
|
884
|
+
function jaccard(a, b) {
|
|
885
|
+
if (a.size === 0 && b.size === 0) return 1;
|
|
886
|
+
let intersection = 0;
|
|
887
|
+
for (const token of a) {
|
|
888
|
+
if (b.has(token)) intersection++;
|
|
889
|
+
}
|
|
890
|
+
return intersection / (a.size + b.size - intersection);
|
|
891
|
+
}
|
|
892
|
+
function estimateTokens(text) {
|
|
893
|
+
let cjk = 0;
|
|
894
|
+
let other = 0;
|
|
895
|
+
for (let index = 0; index < text.length; index++) {
|
|
896
|
+
const code = text.charCodeAt(index);
|
|
897
|
+
if (code >= 19968 && code <= 40959) cjk++;
|
|
898
|
+
else other++;
|
|
899
|
+
}
|
|
900
|
+
return Math.ceil(cjk * 0.7 + other / 4);
|
|
901
|
+
}
|
|
902
|
+
function hashString(text) {
|
|
903
|
+
let hash = 5381;
|
|
904
|
+
for (let index = 0; index < text.length; index++) {
|
|
905
|
+
hash = (hash << 5) + hash + text.charCodeAt(index) | 0;
|
|
906
|
+
}
|
|
907
|
+
return (hash >>> 0).toString(36);
|
|
908
|
+
}
|
|
909
|
+
function firstLine(text, maxChars) {
|
|
910
|
+
const line = text.split("\n", 1)[0]?.trim() ?? "";
|
|
911
|
+
return line.length > maxChars ? `${line.slice(0, maxChars)}\u2026` : line;
|
|
912
|
+
}
|
|
913
|
+
function truncateMiddle(text, keepChars, note) {
|
|
914
|
+
if (text.length <= keepChars) return text;
|
|
915
|
+
const head = Math.ceil(keepChars / 2);
|
|
916
|
+
const tail = Math.floor(keepChars / 2);
|
|
917
|
+
return `${text.slice(0, head)}
|
|
918
|
+
${note}
|
|
919
|
+
${text.slice(text.length - tail)}`;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// lib/dtc/digest.ts
|
|
923
|
+
var INTENT_MAX = 120;
|
|
924
|
+
var RESULT_MAX = 160;
|
|
925
|
+
var MAX_TOOLS = 8;
|
|
926
|
+
var MAX_FILES = 6;
|
|
927
|
+
var DIGEST_MAX_CHARS = 600;
|
|
928
|
+
var PATH_KEYS = ["filePath", "path", "file", "filename", "pattern", "directory"];
|
|
929
|
+
function toolSummary(parts) {
|
|
930
|
+
const counts = /* @__PURE__ */ new Map();
|
|
931
|
+
const files = [];
|
|
932
|
+
let errors = 0;
|
|
933
|
+
for (const part of parts) {
|
|
934
|
+
if (!part || typeof part !== "object" || part.type !== "tool") continue;
|
|
935
|
+
const name = part.tool ?? "tool";
|
|
936
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
937
|
+
if (part.state?.status === "error") errors++;
|
|
938
|
+
const input = part.state?.input;
|
|
939
|
+
if (input && typeof input === "object") {
|
|
940
|
+
for (const key of PATH_KEYS) {
|
|
941
|
+
const value = input[key];
|
|
942
|
+
if (typeof value === "string" && value && !files.includes(value)) {
|
|
943
|
+
files.push(value);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
const command = input.command;
|
|
947
|
+
if (typeof command === "string" && command && !files.includes(firstLine(command, 60))) {
|
|
948
|
+
files.push(firstLine(command, 60));
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
const actions = [...counts.entries()].map(([name, count]) => count > 1 ? `${name}\xD7${count}` : name).slice(0, MAX_TOOLS).join(", ");
|
|
953
|
+
return { actions, files: files.slice(0, MAX_FILES).join(" "), errors };
|
|
954
|
+
}
|
|
955
|
+
function digestTurn(messages, turn, index) {
|
|
956
|
+
const slice = messages.slice(turn.start, turn.end);
|
|
957
|
+
const user = slice[0];
|
|
958
|
+
const intent = firstLine(userText(user), INTENT_MAX) || "(\u65E0\u6587\u672C)";
|
|
959
|
+
const { actions, files, errors } = toolSummary(
|
|
960
|
+
slice.flatMap((m) => m && typeof m === "object" ? m.parts ?? [] : [])
|
|
961
|
+
);
|
|
962
|
+
let result = "";
|
|
963
|
+
for (let i = slice.length - 1; i >= 0; i--) {
|
|
964
|
+
if (slice[i]?.info?.role !== "assistant") continue;
|
|
965
|
+
const text = assistantText(slice[i]);
|
|
966
|
+
if (text) {
|
|
967
|
+
result = firstLine(text, RESULT_MAX);
|
|
968
|
+
break;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
const parts = [`[DCP\xB7\u8F6E${index}]`, `\u610F\u56FE: ${intent}`];
|
|
972
|
+
if (actions) parts.push(`\u52A8\u4F5C: ${actions}`);
|
|
973
|
+
if (files) parts.push(`\u6D89\u53CA: ${files}`);
|
|
974
|
+
if (result) parts.push(`\u7ED3\u679C: ${result}`);
|
|
975
|
+
if (errors > 0) parts.push(`\u9519\u8BEF\xD7${errors}`);
|
|
976
|
+
const digest = parts.join(" | ");
|
|
977
|
+
return digest.length > DIGEST_MAX_CHARS ? `${digest.slice(0, DIGEST_MAX_CHARS)}\u2026` : digest;
|
|
978
|
+
}
|
|
979
|
+
function userText(message) {
|
|
980
|
+
const parts = message?.parts ?? [];
|
|
981
|
+
const texts = parts.filter((p) => p?.type === "text" && typeof p.text === "string").map((p) => p.text);
|
|
982
|
+
return texts.join(" ").trim();
|
|
983
|
+
}
|
|
984
|
+
function assistantText(message) {
|
|
985
|
+
return userText(message);
|
|
986
|
+
}
|
|
987
|
+
function digestKey(messages, turn) {
|
|
988
|
+
const first = messages[turn.start];
|
|
989
|
+
const id = first?.info?.id ?? "noid";
|
|
990
|
+
let shape = "";
|
|
991
|
+
for (let i = turn.start; i < turn.end && i < messages.length; i++) {
|
|
992
|
+
const message = messages[i];
|
|
993
|
+
shape += `${message?.info?.role ?? "?"}:${(message?.parts ?? []).length};`;
|
|
994
|
+
for (const part of message?.parts ?? []) {
|
|
995
|
+
if (!part || typeof part !== "object") continue;
|
|
996
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
997
|
+
shape += part.text.slice(0, 64);
|
|
998
|
+
} else if (part.type === "tool") {
|
|
999
|
+
shape += `${part.tool ?? "tool"}:${part.state?.status ?? "?"}:${(part.state?.output ?? "").length}`;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
return `${id}:${hashString(shape)}`;
|
|
1004
|
+
}
|
|
1005
|
+
function findTopicBoundaries(messages, turns, driftThreshold) {
|
|
1006
|
+
const boundaries = [];
|
|
1007
|
+
let previous;
|
|
1008
|
+
for (let t = 0; t < turns.length; t++) {
|
|
1009
|
+
const text = userText(messages[turns[t].start]);
|
|
1010
|
+
const tokens = tokenize(text);
|
|
1011
|
+
if (tokens.size < 6) continue;
|
|
1012
|
+
if (previous && jaccard(tokens, previous) < driftThreshold) boundaries.push(t);
|
|
1013
|
+
previous = tokens;
|
|
1014
|
+
}
|
|
1015
|
+
return boundaries;
|
|
1016
|
+
}
|
|
1017
|
+
function estimateSlice(messages, start, end) {
|
|
1018
|
+
let tokens = 0;
|
|
1019
|
+
for (let i = start; i < end && i < messages.length; i++) {
|
|
1020
|
+
for (const part of messages[i]?.parts ?? []) {
|
|
1021
|
+
if (!part || typeof part !== "object") continue;
|
|
1022
|
+
if (typeof part.text === "string") tokens += estimateTokens(part.text);
|
|
1023
|
+
const state = part.state;
|
|
1024
|
+
if (!state) continue;
|
|
1025
|
+
if (state.time?.compacted) {
|
|
1026
|
+
tokens += 8;
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
if (typeof state.output === "string") tokens += estimateTokens(state.output);
|
|
1030
|
+
if (state.input) tokens += estimateTokens(JSON.stringify(state.input));
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
return tokens;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// lib/dtc/engine.ts
|
|
1037
|
+
var DTC_DEFAULTS = {
|
|
1038
|
+
tailTurns: 4,
|
|
1039
|
+
lowWatermarkRatio: 0.5,
|
|
1040
|
+
targetRatio: 0.7,
|
|
1041
|
+
driftThreshold: 0.18,
|
|
1042
|
+
toolOutputKeepChars: 4e3
|
|
1043
|
+
};
|
|
1044
|
+
var C_ZONE_MAX_TURNS = 8;
|
|
1045
|
+
var M_ZONE_MAX_TURNS = 12;
|
|
1046
|
+
var M_TEXT_KEEP_CHARS = 200;
|
|
1047
|
+
var FOLDED_TEXT = " ";
|
|
1048
|
+
var NO_STATS = {
|
|
1049
|
+
messages: 0,
|
|
1050
|
+
userTurns: 0,
|
|
1051
|
+
level: 0,
|
|
1052
|
+
estimatedBefore: 0,
|
|
1053
|
+
estimatedAfter: 0,
|
|
1054
|
+
contextTokens: 0,
|
|
1055
|
+
foldedTools: 0,
|
|
1056
|
+
foldedTexts: 0,
|
|
1057
|
+
digestedTurns: 0,
|
|
1058
|
+
skipped: void 0
|
|
1059
|
+
};
|
|
1060
|
+
function segmentTurns(messages) {
|
|
1061
|
+
const turns = [];
|
|
1062
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1063
|
+
const info = messages[i]?.info;
|
|
1064
|
+
if (info?.role !== "user") continue;
|
|
1065
|
+
if ((messages[i]?.parts ?? []).some((p) => p?.type === "compaction")) continue;
|
|
1066
|
+
turns.push({ start: i, end: messages.length });
|
|
1067
|
+
}
|
|
1068
|
+
for (let i = 0; i < turns.length - 1; i++) {
|
|
1069
|
+
turns[i].end = turns[i + 1].start;
|
|
1070
|
+
}
|
|
1071
|
+
return turns;
|
|
1072
|
+
}
|
|
1073
|
+
function transformMessages(messages, deps) {
|
|
1074
|
+
const stats = { ...NO_STATS };
|
|
1075
|
+
if (!Array.isArray(messages) || messages.length === 0) return stats;
|
|
1076
|
+
stats.messages = messages.length;
|
|
1077
|
+
const sessionID = findSessionID(messages);
|
|
1078
|
+
if (sessionID && deps.state.consumeCompactionSkip(sessionID)) {
|
|
1079
|
+
stats.skipped = "compaction";
|
|
1080
|
+
return stats;
|
|
1081
|
+
}
|
|
1082
|
+
const turns = segmentTurns(messages);
|
|
1083
|
+
stats.userTurns = turns.length;
|
|
1084
|
+
const { config, state } = deps;
|
|
1085
|
+
if (turns.length <= config.tailTurns) {
|
|
1086
|
+
stats.skipped = "short";
|
|
1087
|
+
return stats;
|
|
1088
|
+
}
|
|
1089
|
+
const contextTokens = sessionID ? state.contextTokens(sessionID) : void 0;
|
|
1090
|
+
if (!contextTokens) {
|
|
1091
|
+
stats.skipped = "unknown-context";
|
|
1092
|
+
return stats;
|
|
1093
|
+
}
|
|
1094
|
+
stats.contextTokens = contextTokens;
|
|
1095
|
+
const headTurns = turns.slice(0, turns.length - config.tailTurns);
|
|
1096
|
+
const estimatedBefore = estimateSlice(messages, 0, messages.length);
|
|
1097
|
+
stats.estimatedBefore = estimatedBefore;
|
|
1098
|
+
const lowWatermark = Math.floor(contextTokens * config.lowWatermarkRatio);
|
|
1099
|
+
const target = Math.floor(contextTokens * config.targetRatio);
|
|
1100
|
+
const minLevel = sessionID ? state.minLevel(sessionID) : 0;
|
|
1101
|
+
if (estimatedBefore <= lowWatermark && minLevel === 0) {
|
|
1102
|
+
stats.estimatedAfter = estimatedBefore;
|
|
1103
|
+
return stats;
|
|
1104
|
+
}
|
|
1105
|
+
const zones = computeZones(messages, headTurns, sessionID, state, config);
|
|
1106
|
+
const now = (deps.now ?? Date.now)();
|
|
1107
|
+
let level = Math.max(1, minLevel);
|
|
1108
|
+
applyLevel(messages, headTurns, zones, level, deps, stats, now);
|
|
1109
|
+
let estimatedAfter = estimateSlice(messages, 0, messages.length);
|
|
1110
|
+
while (estimatedAfter > target && level < 3) {
|
|
1111
|
+
level = level + 1;
|
|
1112
|
+
applyLevel(messages, headTurns, zones, level, deps, stats, now);
|
|
1113
|
+
estimatedAfter = estimateSlice(messages, 0, messages.length);
|
|
1114
|
+
}
|
|
1115
|
+
stats.level = level;
|
|
1116
|
+
stats.estimatedAfter = estimatedAfter;
|
|
1117
|
+
deps.logger?.debug("DTC transform", {
|
|
1118
|
+
sessionId: sessionID,
|
|
1119
|
+
messages: stats.messages,
|
|
1120
|
+
userTurns: stats.userTurns,
|
|
1121
|
+
level,
|
|
1122
|
+
estimatedBefore,
|
|
1123
|
+
estimatedAfter,
|
|
1124
|
+
contextTokens,
|
|
1125
|
+
foldedTools: stats.foldedTools,
|
|
1126
|
+
digestedTurns: stats.digestedTurns
|
|
1127
|
+
});
|
|
1128
|
+
return stats;
|
|
1129
|
+
}
|
|
1130
|
+
function computeZones(messages, headTurns, sessionID, state, config) {
|
|
1131
|
+
const boundaries = findTopicBoundaries(messages, headTurns, config.driftThreshold);
|
|
1132
|
+
const lastBoundary = boundaries.length > 0 ? boundaries[boundaries.length - 1] : 0;
|
|
1133
|
+
const secondLast = boundaries.length > 1 ? boundaries[boundaries.length - 2] : 0;
|
|
1134
|
+
let markStart = 0;
|
|
1135
|
+
const markAt = sessionID ? state.boundaryMark(sessionID) : void 0;
|
|
1136
|
+
if (markAt !== void 0) {
|
|
1137
|
+
for (let t = 0; t < headTurns.length; t++) {
|
|
1138
|
+
const created = messages[headTurns[t].start]?.info?.time?.created;
|
|
1139
|
+
if (typeof created === "number" && created <= markAt) markStart = t + 1;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
const cStart = Math.min(
|
|
1143
|
+
headTurns.length,
|
|
1144
|
+
Math.max(lastBoundary, markStart, headTurns.length - C_ZONE_MAX_TURNS)
|
|
1145
|
+
);
|
|
1146
|
+
const mStart = Math.min(cStart, Math.max(secondLast, cStart - M_ZONE_MAX_TURNS));
|
|
1147
|
+
return { mStart, cStart };
|
|
1148
|
+
}
|
|
1149
|
+
function applyLevel(messages, headTurns, zones, level, deps, stats, now) {
|
|
1150
|
+
const prev = level === 1 ? -1 : level - 1;
|
|
1151
|
+
if (level >= 1 && prev < 1) {
|
|
1152
|
+
for (let t = 0; t < zones.mStart; t++) {
|
|
1153
|
+
foldDistant(messages, headTurns[t], t + 1, deps, stats, now);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
if (level >= 2 && prev < 2) {
|
|
1157
|
+
for (let t = zones.mStart; t < zones.cStart; t++) {
|
|
1158
|
+
foldMiddle(messages, headTurns[t], stats, now);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (level >= 3 && prev < 3) {
|
|
1162
|
+
for (let t = zones.cStart; t < headTurns.length; t++) {
|
|
1163
|
+
foldCurrent(messages, headTurns[t], deps.config.toolOutputKeepChars, stats);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
function foldDistant(messages, turn, ordinal, deps, stats, now) {
|
|
1168
|
+
const key = digestKey(messages, turn);
|
|
1169
|
+
let digest = deps.state.cachedDigest(key);
|
|
1170
|
+
if (digest === void 0) {
|
|
1171
|
+
digest = digestTurn(messages, turn, ordinal);
|
|
1172
|
+
deps.state.storeDigest(key, digest);
|
|
1173
|
+
}
|
|
1174
|
+
stats.digestedTurns++;
|
|
1175
|
+
let digestPlaced = false;
|
|
1176
|
+
for (let i = turn.start; i < turn.end; i++) {
|
|
1177
|
+
const message = messages[i];
|
|
1178
|
+
for (const part of message?.parts ?? []) {
|
|
1179
|
+
if (!part || typeof part !== "object") continue;
|
|
1180
|
+
if (foldToolPart(part, now, true)) {
|
|
1181
|
+
stats.foldedTools++;
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
if (part.type === "reasoning" && typeof part.text === "string" && part.text.length > 0) {
|
|
1185
|
+
part.text = FOLDED_TEXT;
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
1189
|
+
if (!digestPlaced && message?.info?.role === "user") {
|
|
1190
|
+
part.text = digest;
|
|
1191
|
+
digestPlaced = true;
|
|
1192
|
+
} else {
|
|
1193
|
+
part.text = FOLDED_TEXT;
|
|
1194
|
+
}
|
|
1195
|
+
stats.foldedTexts++;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
function foldMiddle(messages, turn, stats, now) {
|
|
1201
|
+
for (let i = turn.start; i < turn.end; i++) {
|
|
1202
|
+
for (const part of messages[i]?.parts ?? []) {
|
|
1203
|
+
if (!part || typeof part !== "object") continue;
|
|
1204
|
+
if (foldToolPart(part, now, false)) {
|
|
1205
|
+
stats.foldedTools++;
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
if (part.type === "reasoning" && typeof part.text === "string" && part.text.length > 0) {
|
|
1209
|
+
part.text = FOLDED_TEXT;
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
if (part.type === "text" && typeof part.text === "string" && part.text.length > M_TEXT_KEEP_CHARS) {
|
|
1213
|
+
part.text = firstLine(part.text, M_TEXT_KEEP_CHARS) || FOLDED_TEXT;
|
|
1214
|
+
stats.foldedTexts++;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
function foldCurrent(messages, turn, keepChars, stats) {
|
|
1220
|
+
for (let i = turn.start; i < turn.end; i++) {
|
|
1221
|
+
for (const part of messages[i]?.parts ?? []) {
|
|
1222
|
+
if (!part || typeof part !== "object" || part.type !== "tool") continue;
|
|
1223
|
+
const state = part.state;
|
|
1224
|
+
if (!state || state.status !== "completed") continue;
|
|
1225
|
+
const output = state.output;
|
|
1226
|
+
if (typeof output !== "string" || output.length <= keepChars) continue;
|
|
1227
|
+
state.output = truncateMiddle(
|
|
1228
|
+
output,
|
|
1229
|
+
keepChars,
|
|
1230
|
+
`[DCP \u5DF2\u6298\u53E0 ${output.length - keepChars} \u5B57\u7B26]`
|
|
1231
|
+
);
|
|
1232
|
+
stats.foldedTools++;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
function foldToolPart(part, now, clearInput) {
|
|
1237
|
+
if (part.type !== "tool") return false;
|
|
1238
|
+
const state = part.state;
|
|
1239
|
+
if (!state || state.status !== "completed") return false;
|
|
1240
|
+
if (state.time && typeof state.time === "object") {
|
|
1241
|
+
state.time.compacted = now;
|
|
1242
|
+
} else {
|
|
1243
|
+
state.time = { compacted: now };
|
|
1244
|
+
}
|
|
1245
|
+
if (clearInput && state.input && typeof state.input === "object") {
|
|
1246
|
+
state.input = {};
|
|
1247
|
+
}
|
|
1248
|
+
return true;
|
|
1249
|
+
}
|
|
1250
|
+
function findSessionID(messages) {
|
|
1251
|
+
for (const message of messages) {
|
|
1252
|
+
const id = message?.info?.sessionID;
|
|
1253
|
+
if (typeof id === "string" && id) return id;
|
|
1254
|
+
}
|
|
1255
|
+
return void 0;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
980
1258
|
// lib/config.ts
|
|
981
|
-
var
|
|
982
|
-
var DEFAULT_AUTO_PRUNE = {
|
|
1259
|
+
var DEFAULT_DTC = {
|
|
983
1260
|
enabled: true,
|
|
984
|
-
|
|
985
|
-
minMessages: 8,
|
|
986
|
-
volumeThreshold: 30,
|
|
987
|
-
driftThreshold: 0.18,
|
|
988
|
-
idleGapMs: 30 * 6e4,
|
|
989
|
-
cooldownMs: 5 * 6e4
|
|
1261
|
+
...DTC_DEFAULTS
|
|
990
1262
|
};
|
|
991
1263
|
var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
992
1264
|
"$schema",
|
|
@@ -998,19 +1270,13 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
998
1270
|
"commands.enabled",
|
|
999
1271
|
"experimental",
|
|
1000
1272
|
"experimental.customPrompts",
|
|
1001
|
-
"
|
|
1002
|
-
"
|
|
1003
|
-
"
|
|
1004
|
-
"
|
|
1005
|
-
"
|
|
1006
|
-
"
|
|
1007
|
-
"
|
|
1008
|
-
"autoPrune.signals.idleGap",
|
|
1009
|
-
"autoPrune.minMessages",
|
|
1010
|
-
"autoPrune.volumeThreshold",
|
|
1011
|
-
"autoPrune.driftThreshold",
|
|
1012
|
-
"autoPrune.idleGapMs",
|
|
1013
|
-
"autoPrune.cooldownMs",
|
|
1273
|
+
"dtc",
|
|
1274
|
+
"dtc.enabled",
|
|
1275
|
+
"dtc.tailTurns",
|
|
1276
|
+
"dtc.lowWatermarkRatio",
|
|
1277
|
+
"dtc.targetRatio",
|
|
1278
|
+
"dtc.driftThreshold",
|
|
1279
|
+
"dtc.toolOutputKeepChars",
|
|
1014
1280
|
"tool",
|
|
1015
1281
|
"tool.enabled"
|
|
1016
1282
|
]);
|
|
@@ -1055,7 +1321,21 @@ var DEPRECATED_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
1055
1321
|
"pruneNotificationType",
|
|
1056
1322
|
"protectedFilePatterns",
|
|
1057
1323
|
"commands.protectedTools",
|
|
1058
|
-
"experimental.allowSubAgents"
|
|
1324
|
+
"experimental.allowSubAgents",
|
|
1325
|
+
"summarize",
|
|
1326
|
+
"summarize.failureCooldownMs",
|
|
1327
|
+
"autoPrune",
|
|
1328
|
+
"autoPrune.enabled",
|
|
1329
|
+
"autoPrune.signals",
|
|
1330
|
+
"autoPrune.signals.topicDrift",
|
|
1331
|
+
"autoPrune.signals.volume",
|
|
1332
|
+
"autoPrune.signals.idleGap",
|
|
1333
|
+
"autoPrune.autoContinue",
|
|
1334
|
+
"autoPrune.minMessages",
|
|
1335
|
+
"autoPrune.volumeThreshold",
|
|
1336
|
+
"autoPrune.driftThreshold",
|
|
1337
|
+
"autoPrune.idleGapMs",
|
|
1338
|
+
"autoPrune.cooldownMs"
|
|
1059
1339
|
]);
|
|
1060
1340
|
function getConfigKeyPaths(obj, prefix = "") {
|
|
1061
1341
|
const keys = [];
|
|
@@ -1097,11 +1377,7 @@ function validateConfigTypes(config) {
|
|
|
1097
1377
|
const commands = config.commands;
|
|
1098
1378
|
if (commands !== void 0) {
|
|
1099
1379
|
if (typeof commands !== "object" || commands === null || Array.isArray(commands)) {
|
|
1100
|
-
errors.push({
|
|
1101
|
-
key: "commands",
|
|
1102
|
-
expected: "object",
|
|
1103
|
-
actual: typeof commands
|
|
1104
|
-
});
|
|
1380
|
+
errors.push({ key: "commands", expected: "object", actual: typeof commands });
|
|
1105
1381
|
} else if (commands.enabled !== void 0 && typeof commands.enabled !== "boolean") {
|
|
1106
1382
|
errors.push({
|
|
1107
1383
|
key: "commands.enabled",
|
|
@@ -1113,11 +1389,7 @@ function validateConfigTypes(config) {
|
|
|
1113
1389
|
const experimental = config.experimental;
|
|
1114
1390
|
if (experimental !== void 0) {
|
|
1115
1391
|
if (typeof experimental !== "object" || experimental === null || Array.isArray(experimental)) {
|
|
1116
|
-
errors.push({
|
|
1117
|
-
key: "experimental",
|
|
1118
|
-
expected: "object",
|
|
1119
|
-
actual: typeof experimental
|
|
1120
|
-
});
|
|
1392
|
+
errors.push({ key: "experimental", expected: "object", actual: typeof experimental });
|
|
1121
1393
|
} else if (experimental.customPrompts !== void 0 && typeof experimental.customPrompts !== "boolean") {
|
|
1122
1394
|
errors.push({
|
|
1123
1395
|
key: "experimental.customPrompts",
|
|
@@ -1126,125 +1398,65 @@ function validateConfigTypes(config) {
|
|
|
1126
1398
|
});
|
|
1127
1399
|
}
|
|
1128
1400
|
}
|
|
1129
|
-
const
|
|
1130
|
-
if (
|
|
1131
|
-
if (typeof
|
|
1132
|
-
errors.push({
|
|
1133
|
-
key: "summarize",
|
|
1134
|
-
expected: "object",
|
|
1135
|
-
actual: typeof summarize
|
|
1136
|
-
});
|
|
1401
|
+
const dtc = config.dtc;
|
|
1402
|
+
if (dtc !== void 0) {
|
|
1403
|
+
if (typeof dtc !== "object" || dtc === null || Array.isArray(dtc)) {
|
|
1404
|
+
errors.push({ key: "dtc", expected: "object", actual: typeof dtc });
|
|
1137
1405
|
} else {
|
|
1138
|
-
if (
|
|
1139
|
-
errors.push({
|
|
1140
|
-
key: "summarize.failureCooldownMs",
|
|
1141
|
-
expected: "non-negative finite number",
|
|
1142
|
-
actual: JSON.stringify(summarize.failureCooldownMs)
|
|
1143
|
-
});
|
|
1406
|
+
if (dtc.enabled !== void 0 && typeof dtc.enabled !== "boolean") {
|
|
1407
|
+
errors.push({ key: "dtc.enabled", expected: "boolean", actual: typeof dtc.enabled });
|
|
1144
1408
|
}
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
const autoPrune = config.autoPrune;
|
|
1148
|
-
if (autoPrune !== void 0) {
|
|
1149
|
-
if (typeof autoPrune !== "object" || autoPrune === null || Array.isArray(autoPrune)) {
|
|
1150
|
-
errors.push({
|
|
1151
|
-
key: "autoPrune",
|
|
1152
|
-
expected: "object",
|
|
1153
|
-
actual: typeof autoPrune
|
|
1154
|
-
});
|
|
1155
|
-
} else {
|
|
1156
1409
|
const numericKeys = [
|
|
1157
|
-
["
|
|
1158
|
-
["
|
|
1410
|
+
["tailTurns", 0, Number.POSITIVE_INFINITY],
|
|
1411
|
+
["lowWatermarkRatio", 0, 1],
|
|
1412
|
+
["targetRatio", 0, 1],
|
|
1159
1413
|
["driftThreshold", 0, 1],
|
|
1160
|
-
["
|
|
1161
|
-
["cooldownMs", 0, Number.POSITIVE_INFINITY]
|
|
1414
|
+
["toolOutputKeepChars", 200, Number.POSITIVE_INFINITY]
|
|
1162
1415
|
];
|
|
1163
1416
|
for (const [key, min, max] of numericKeys) {
|
|
1164
|
-
const value =
|
|
1417
|
+
const value = dtc[key];
|
|
1165
1418
|
if (value !== void 0 && (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max)) {
|
|
1166
1419
|
errors.push({
|
|
1167
|
-
key: `
|
|
1420
|
+
key: `dtc.${key}`,
|
|
1168
1421
|
expected: `number in [${min}, ${max === Number.POSITIVE_INFINITY ? "\u221E" : max}]`,
|
|
1169
1422
|
actual: JSON.stringify(value)
|
|
1170
1423
|
});
|
|
1171
1424
|
}
|
|
1172
1425
|
}
|
|
1173
|
-
for (const key of ["enabled"]) {
|
|
1174
|
-
const value = autoPrune[key];
|
|
1175
|
-
if (value !== void 0 && typeof value !== "boolean") {
|
|
1176
|
-
errors.push({
|
|
1177
|
-
key: `autoPrune.${key}`,
|
|
1178
|
-
expected: "boolean",
|
|
1179
|
-
actual: typeof value
|
|
1180
|
-
});
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
const signals = autoPrune.signals;
|
|
1184
|
-
if (signals !== void 0) {
|
|
1185
|
-
if (typeof signals !== "object" || signals === null || Array.isArray(signals)) {
|
|
1186
|
-
errors.push({
|
|
1187
|
-
key: "autoPrune.signals",
|
|
1188
|
-
expected: "object",
|
|
1189
|
-
actual: typeof signals
|
|
1190
|
-
});
|
|
1191
|
-
} else {
|
|
1192
|
-
for (const key of ["topicDrift", "volume", "idleGap"]) {
|
|
1193
|
-
const value = signals[key];
|
|
1194
|
-
if (value !== void 0 && typeof value !== "boolean") {
|
|
1195
|
-
errors.push({
|
|
1196
|
-
key: `autoPrune.signals.${key}`,
|
|
1197
|
-
expected: "boolean",
|
|
1198
|
-
actual: typeof value
|
|
1199
|
-
});
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
1426
|
}
|
|
1205
1427
|
}
|
|
1206
1428
|
const tool2 = config.tool;
|
|
1207
1429
|
if (tool2 !== void 0) {
|
|
1208
1430
|
if (typeof tool2 !== "object" || tool2 === null || Array.isArray(tool2)) {
|
|
1209
|
-
errors.push({
|
|
1210
|
-
key: "tool",
|
|
1211
|
-
expected: "object",
|
|
1212
|
-
actual: typeof tool2
|
|
1213
|
-
});
|
|
1431
|
+
errors.push({ key: "tool", expected: "object", actual: typeof tool2 });
|
|
1214
1432
|
} else if (tool2.enabled !== void 0 && typeof tool2.enabled !== "boolean") {
|
|
1215
|
-
errors.push({
|
|
1216
|
-
key: "tool.enabled",
|
|
1217
|
-
expected: "boolean",
|
|
1218
|
-
actual: typeof tool2.enabled
|
|
1219
|
-
});
|
|
1433
|
+
errors.push({ key: "tool.enabled", expected: "boolean", actual: typeof tool2.enabled });
|
|
1220
1434
|
}
|
|
1221
1435
|
}
|
|
1222
1436
|
return errors;
|
|
1223
1437
|
}
|
|
1224
|
-
function
|
|
1438
|
+
function legacyDriftThreshold(configData) {
|
|
1225
1439
|
const autoPrune = configData.autoPrune;
|
|
1226
|
-
|
|
1440
|
+
if (autoPrune === null || typeof autoPrune !== "object" || Array.isArray(autoPrune)) {
|
|
1441
|
+
return void 0;
|
|
1442
|
+
}
|
|
1443
|
+
const value = autoPrune.driftThreshold;
|
|
1444
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1 ? value : void 0;
|
|
1227
1445
|
}
|
|
1228
1446
|
function showConfigWarnings(ctx, configPath, configData, isProject) {
|
|
1229
1447
|
const invalidKeys = getInvalidConfigKeys(configData);
|
|
1230
1448
|
const deprecatedKeys = getDeprecatedConfigKeys(configData);
|
|
1231
1449
|
const typeErrors = validateConfigTypes(configData);
|
|
1232
|
-
|
|
1233
|
-
if (!signalsHint && invalidKeys.length === 0 && deprecatedKeys.length === 0 && typeErrors.length === 0) {
|
|
1450
|
+
if (invalidKeys.length === 0 && deprecatedKeys.length === 0 && typeErrors.length === 0) {
|
|
1234
1451
|
return;
|
|
1235
1452
|
}
|
|
1236
1453
|
const configType = isProject ? "project config" : "config";
|
|
1237
1454
|
const messages = [];
|
|
1238
|
-
if (signalsHint) {
|
|
1239
|
-
messages.push(
|
|
1240
|
-
"auto-prune signals `volume` and `idleGap` are now disabled by default; re-enable via autoPrune.signals.*"
|
|
1241
|
-
);
|
|
1242
|
-
}
|
|
1243
1455
|
if (deprecatedKeys.length > 0) {
|
|
1244
1456
|
const keyList = deprecatedKeys.slice(0, 3).join(", ");
|
|
1245
1457
|
const suffix = deprecatedKeys.length > 3 ? ` (+${deprecatedKeys.length - 3} more)` : "";
|
|
1246
1458
|
messages.push(
|
|
1247
|
-
`Removed legacy
|
|
1459
|
+
`Removed legacy keys are ignored: ${keyList}${suffix}. Compression now runs as dynamic request-time folding (dtc.*).`
|
|
1248
1460
|
);
|
|
1249
1461
|
}
|
|
1250
1462
|
if (invalidKeys.length > 0) {
|
|
@@ -1286,10 +1498,7 @@ var defaultConfig = {
|
|
|
1286
1498
|
experimental: {
|
|
1287
1499
|
customPrompts: false
|
|
1288
1500
|
},
|
|
1289
|
-
|
|
1290
|
-
failureCooldownMs: DEFAULT_FAILURE_COOLDOWN_MS
|
|
1291
|
-
},
|
|
1292
|
-
autoPrune: { ...DEFAULT_AUTO_PRUNE },
|
|
1501
|
+
dtc: { ...DEFAULT_DTC },
|
|
1293
1502
|
tool: {
|
|
1294
1503
|
enabled: true
|
|
1295
1504
|
}
|
|
@@ -1375,33 +1584,19 @@ function mergeExperimental(base, override) {
|
|
|
1375
1584
|
customPrompts: typeof override.customPrompts === "boolean" ? override.customPrompts : base.customPrompts
|
|
1376
1585
|
};
|
|
1377
1586
|
}
|
|
1378
|
-
function
|
|
1379
|
-
|
|
1380
|
-
return base;
|
|
1381
|
-
}
|
|
1382
|
-
return {
|
|
1383
|
-
failureCooldownMs: typeof override.failureCooldownMs === "number" && Number.isFinite(override.failureCooldownMs) && override.failureCooldownMs >= 0 ? override.failureCooldownMs : base.failureCooldownMs
|
|
1384
|
-
};
|
|
1385
|
-
}
|
|
1386
|
-
function mergeAutoPrune(base, override) {
|
|
1587
|
+
function mergeDtc(base, override, legacyDrift) {
|
|
1588
|
+
const driftFallback = legacyDrift ?? base.driftThreshold;
|
|
1387
1589
|
if (!override || typeof override !== "object" || Array.isArray(override)) {
|
|
1388
|
-
return base;
|
|
1590
|
+
return { ...base, driftThreshold: driftFallback };
|
|
1389
1591
|
}
|
|
1390
|
-
const number = (key, min, max
|
|
1391
|
-
const signalsOverride = override.signals;
|
|
1392
|
-
const signals = signalsOverride && typeof signalsOverride === "object" && !Array.isArray(signalsOverride) ? {
|
|
1393
|
-
topicDrift: typeof signalsOverride.topicDrift === "boolean" ? signalsOverride.topicDrift : base.signals.topicDrift,
|
|
1394
|
-
volume: typeof signalsOverride.volume === "boolean" ? signalsOverride.volume : base.signals.volume,
|
|
1395
|
-
idleGap: typeof signalsOverride.idleGap === "boolean" ? signalsOverride.idleGap : base.signals.idleGap
|
|
1396
|
-
} : base.signals;
|
|
1592
|
+
const number = (key, min, max) => typeof override[key] === "number" && Number.isFinite(override[key]) && override[key] >= min && override[key] <= max ? override[key] : base[key];
|
|
1397
1593
|
return {
|
|
1398
1594
|
enabled: typeof override.enabled === "boolean" ? override.enabled : base.enabled,
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
driftThreshold: number(
|
|
1403
|
-
|
|
1404
|
-
cooldownMs: number("cooldownMs", 0)
|
|
1595
|
+
tailTurns: number("tailTurns", 0, Number.POSITIVE_INFINITY),
|
|
1596
|
+
lowWatermarkRatio: number("lowWatermarkRatio", 0, 1),
|
|
1597
|
+
targetRatio: number("targetRatio", 0, 1),
|
|
1598
|
+
driftThreshold: typeof override.driftThreshold === "number" && Number.isFinite(override.driftThreshold) && override.driftThreshold >= 0 && override.driftThreshold <= 1 ? override.driftThreshold : driftFallback,
|
|
1599
|
+
toolOutputKeepChars: number("toolOutputKeepChars", 200, Number.POSITIVE_INFINITY)
|
|
1405
1600
|
};
|
|
1406
1601
|
}
|
|
1407
1602
|
function mergeTool(base, override) {
|
|
@@ -1417,8 +1612,7 @@ function deepCloneConfig(config) {
|
|
|
1417
1612
|
...config,
|
|
1418
1613
|
commands: { ...config.commands },
|
|
1419
1614
|
experimental: { ...config.experimental },
|
|
1420
|
-
|
|
1421
|
-
autoPrune: { ...config.autoPrune, signals: { ...config.autoPrune.signals } },
|
|
1615
|
+
dtc: { ...config.dtc },
|
|
1422
1616
|
tool: { ...config.tool }
|
|
1423
1617
|
};
|
|
1424
1618
|
}
|
|
@@ -1430,8 +1624,7 @@ function mergeLayer(config, data) {
|
|
|
1430
1624
|
language: data.language === "zh" || data.language === "en" ? data.language : config.language,
|
|
1431
1625
|
commands: mergeCommands(config.commands, data.commands),
|
|
1432
1626
|
experimental: mergeExperimental(config.experimental, data.experimental),
|
|
1433
|
-
|
|
1434
|
-
autoPrune: mergeAutoPrune(config.autoPrune, data.autoPrune),
|
|
1627
|
+
dtc: mergeDtc(config.dtc, data.dtc, legacyDriftThreshold(data)),
|
|
1435
1628
|
tool: mergeTool(config.tool, data.tool)
|
|
1436
1629
|
};
|
|
1437
1630
|
}
|
|
@@ -1485,225 +1678,76 @@ Using previous/default values`
|
|
|
1485
1678
|
return config;
|
|
1486
1679
|
}
|
|
1487
1680
|
|
|
1488
|
-
// lib/
|
|
1489
|
-
var
|
|
1490
|
-
var
|
|
1491
|
-
|
|
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);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
var DtcState = class {
|
|
1494
1694
|
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
|
-
);
|
|
1695
|
+
digests = /* @__PURE__ */ new Map();
|
|
1696
|
+
observeContextLimit(sessionID, contextTokens) {
|
|
1697
|
+
if (!sessionID || !contextTokens || !Number.isFinite(contextTokens) || contextTokens <= 0) {
|
|
1527
1698
|
return;
|
|
1528
1699
|
}
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1700
|
+
const state = this.session(sessionID);
|
|
1701
|
+
state.contextTokens = contextTokens;
|
|
1702
|
+
lruSet(this.sessions, sessionID, state, SESSION_LIMIT);
|
|
1532
1703
|
}
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
observeLegacyIdle(sessionID) {
|
|
1536
|
-
if (!sessionID) return;
|
|
1537
|
-
this.observeIdle(sessionID, this.entry(sessionID), "legacy-idle");
|
|
1704
|
+
contextTokens(sessionID) {
|
|
1705
|
+
return this.sessions.get(sessionID)?.contextTokens;
|
|
1538
1706
|
}
|
|
1539
|
-
/**
|
|
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
|
-
});
|
|
1707
|
+
/** Called by the compacting hook; consumed by the very next transform. */
|
|
1708
|
+
armCompactionSkip(sessionID) {
|
|
1709
|
+
const state = this.session(sessionID);
|
|
1710
|
+
state.skipNextTransform = true;
|
|
1711
|
+
lruSet(this.sessions, sessionID, state, SESSION_LIMIT);
|
|
1564
1712
|
}
|
|
1565
|
-
|
|
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;
|
|
1713
|
+
consumeCompactionSkip(sessionID) {
|
|
1714
|
+
const state = this.sessions.get(sessionID);
|
|
1715
|
+
if (!state?.skipNextTransform) return false;
|
|
1716
|
+
state.skipNextTransform = false;
|
|
1717
|
+
return true;
|
|
1575
1718
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1719
|
+
/** `dcp_prune` / `/dcp fold`: mark a boundary now and deepen folding. */
|
|
1720
|
+
markBoundary(sessionID, at, minLevel = 2) {
|
|
1721
|
+
const state = this.session(sessionID);
|
|
1722
|
+
state.boundaryMarkAt = at;
|
|
1723
|
+
state.minLevel = Math.max(state.minLevel ?? 0, minLevel);
|
|
1724
|
+
lruSet(this.sessions, sessionID, state, SESSION_LIMIT);
|
|
1578
1725
|
}
|
|
1579
|
-
|
|
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
|
-
});
|
|
1726
|
+
boundaryMark(sessionID) {
|
|
1727
|
+
return this.sessions.get(sessionID)?.boundaryMarkAt;
|
|
1610
1728
|
}
|
|
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
|
-
}
|
|
1729
|
+
minLevel(sessionID) {
|
|
1730
|
+
return this.sessions.get(sessionID)?.minLevel ?? 0;
|
|
1678
1731
|
}
|
|
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;
|
|
1732
|
+
cachedDigest(key) {
|
|
1733
|
+
return this.digests.get(key);
|
|
1689
1734
|
}
|
|
1690
|
-
|
|
1735
|
+
storeDigest(key, digest) {
|
|
1736
|
+
lruSet(this.digests, key, digest, DIGEST_LIMIT);
|
|
1737
|
+
}
|
|
1738
|
+
dropSession(sessionID) {
|
|
1691
1739
|
this.sessions.delete(sessionID);
|
|
1692
|
-
this.sessions.set(sessionID, entry);
|
|
1693
|
-
this.evictIfNeeded(sessionID);
|
|
1694
1740
|
}
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
this.sessions.delete(oldest);
|
|
1741
|
+
/** Test/inspection surface. */
|
|
1742
|
+
stats() {
|
|
1743
|
+
return { sessions: this.sessions.size, digests: this.digests.size };
|
|
1744
|
+
}
|
|
1745
|
+
session(sessionID) {
|
|
1746
|
+
return this.sessions.get(sessionID) ?? {};
|
|
1702
1747
|
}
|
|
1703
1748
|
};
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
}
|
|
1749
|
+
|
|
1750
|
+
// lib/session-events.ts
|
|
1707
1751
|
function eventSessionID(properties) {
|
|
1708
1752
|
const direct = properties?.sessionID;
|
|
1709
1753
|
if (typeof direct === "string" && direct) return direct;
|
|
@@ -1713,162 +1757,176 @@ function eventSessionID(properties) {
|
|
|
1713
1757
|
}
|
|
1714
1758
|
|
|
1715
1759
|
// lib/hooks.ts
|
|
1716
|
-
|
|
1760
|
+
var DEFAULT_TAIL_TURNS = 4;
|
|
1761
|
+
var DEFAULT_PRESERVE_RECENT_TOKENS = 32e3;
|
|
1762
|
+
function extractPreviousCheckpoint(messages) {
|
|
1763
|
+
if (!Array.isArray(messages)) return void 0;
|
|
1764
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
1765
|
+
const message = messages[index];
|
|
1766
|
+
const info = message?.info;
|
|
1767
|
+
if (info?.role !== "assistant" || info.summary !== true) continue;
|
|
1768
|
+
const parts = Array.isArray(message?.parts) ? message.parts : [];
|
|
1769
|
+
const text = parts.filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text.trim()).filter(Boolean).join("\n\n").trim();
|
|
1770
|
+
if (text) return text;
|
|
1771
|
+
}
|
|
1772
|
+
return void 0;
|
|
1773
|
+
}
|
|
1774
|
+
async function fetchPreviousCheckpoint(client, sessionID) {
|
|
1775
|
+
if (!sessionID) return void 0;
|
|
1776
|
+
try {
|
|
1777
|
+
const response = await client.session.messages({ path: { id: sessionID } });
|
|
1778
|
+
return extractPreviousCheckpoint(response.data ?? response);
|
|
1779
|
+
} catch {
|
|
1780
|
+
return void 0;
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
function createSessionCompactingHandler(deps) {
|
|
1717
1784
|
return async (input, output) => {
|
|
1718
1785
|
try {
|
|
1719
|
-
|
|
1720
|
-
|
|
1786
|
+
deps.state.armCompactionSkip(input.sessionID);
|
|
1787
|
+
deps.prompts.reload();
|
|
1788
|
+
const prompt = deps.prompts.getRuntimePrompts().compaction;
|
|
1789
|
+
const previous = await fetchPreviousCheckpoint(deps.client, input.sessionID);
|
|
1790
|
+
const withCheckpoint = previous ? `${prompt}
|
|
1791
|
+
|
|
1792
|
+
<previous-checkpoint>
|
|
1793
|
+
${previous}
|
|
1794
|
+
</previous-checkpoint>` : prompt;
|
|
1721
1795
|
if (!output.prompt) {
|
|
1722
|
-
output.prompt =
|
|
1796
|
+
output.prompt = withCheckpoint;
|
|
1723
1797
|
} else if (!output.context.includes(prompt)) {
|
|
1724
|
-
output.context.push(
|
|
1798
|
+
output.context.push(withCheckpoint);
|
|
1725
1799
|
}
|
|
1726
|
-
logger.debug("Applied semantic pruning prompt", {
|
|
1727
|
-
} catch (error) {
|
|
1728
|
-
logger.warn("Failed to apply semantic pruning prompt; native compaction continues", {
|
|
1800
|
+
deps.logger.debug("Applied semantic pruning prompt", {
|
|
1729
1801
|
sessionId: input.sessionID,
|
|
1730
|
-
|
|
1802
|
+
carriedCheckpoint: Boolean(previous)
|
|
1731
1803
|
});
|
|
1804
|
+
} catch (error) {
|
|
1805
|
+
deps.logger.warn(
|
|
1806
|
+
"Failed to apply semantic pruning prompt; native compaction continues",
|
|
1807
|
+
{
|
|
1808
|
+
sessionId: input.sessionID,
|
|
1809
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1810
|
+
}
|
|
1811
|
+
);
|
|
1732
1812
|
}
|
|
1733
1813
|
};
|
|
1734
1814
|
}
|
|
1735
|
-
|
|
1736
|
-
await client.tui.showToast({
|
|
1737
|
-
body: { title, message, variant, duration: 5e3 }
|
|
1738
|
-
}).catch(() => void 0);
|
|
1739
|
-
}
|
|
1740
|
-
function createChatMessageHandler(autoPruner) {
|
|
1815
|
+
function createChatParamsHandler(deps) {
|
|
1741
1816
|
return async (input, _output) => {
|
|
1742
|
-
|
|
1743
|
-
|
|
1817
|
+
try {
|
|
1818
|
+
const context = input.model?.limit?.context;
|
|
1819
|
+
if (typeof context === "number" && Number.isFinite(context) && context > 0) {
|
|
1820
|
+
deps.state.observeContextLimit(input.sessionID, context);
|
|
1821
|
+
}
|
|
1822
|
+
} catch (error) {
|
|
1823
|
+
deps.logger.debug("chat.params observation failed", {
|
|
1824
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1825
|
+
});
|
|
1826
|
+
}
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
function createTransformHandler(deps) {
|
|
1830
|
+
return async (_input, output) => {
|
|
1831
|
+
try {
|
|
1832
|
+
if (!Array.isArray(output?.messages) || output.messages.length === 0) return;
|
|
1833
|
+
transformMessages(output.messages, {
|
|
1834
|
+
state: deps.state,
|
|
1835
|
+
config: deps.config,
|
|
1836
|
+
logger: deps.logger
|
|
1837
|
+
});
|
|
1838
|
+
} catch (error) {
|
|
1839
|
+
deps.logger.warn("DTC transform failed; the request proceeds unfolded", {
|
|
1840
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1744
1843
|
};
|
|
1745
1844
|
}
|
|
1746
|
-
var SIGNAL_LABELS = {
|
|
1747
|
-
"topic-drift": "\u8BDD\u9898\u53D8\u66F4",
|
|
1748
|
-
volume: "\u6D88\u606F\u91CF\u8FBE\u5230\u9608\u503C",
|
|
1749
|
-
"idle-gap": "\u957F\u65F6\u95F4\u4E2D\u65AD\u540E\u6062\u590D"
|
|
1750
|
-
};
|
|
1751
1845
|
function createEventHandler(deps) {
|
|
1752
1846
|
return async (input) => {
|
|
1753
|
-
const event = input.event;
|
|
1754
1847
|
try {
|
|
1755
|
-
|
|
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
|
-
}
|
|
1848
|
+
if (input.event.type !== "session.deleted") return;
|
|
1849
|
+
const sessionID = eventSessionID(input.event.properties);
|
|
1850
|
+
if (sessionID) deps.state.dropSession(sessionID);
|
|
1765
1851
|
} catch (error) {
|
|
1766
1852
|
deps.logger.warn("Event handler failed", {
|
|
1767
|
-
type: event.type,
|
|
1853
|
+
type: input.event.type,
|
|
1768
1854
|
error: error instanceof Error ? error.message : String(error)
|
|
1769
1855
|
});
|
|
1770
1856
|
}
|
|
1771
1857
|
};
|
|
1772
1858
|
}
|
|
1773
|
-
async function
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1859
|
+
async function showToast(client, title, message, variant = "info") {
|
|
1860
|
+
await client.tui.showToast({
|
|
1861
|
+
body: { title, message, variant, duration: 5e3 }
|
|
1862
|
+
}).catch(() => void 0);
|
|
1863
|
+
}
|
|
1864
|
+
function createCommandExecuteHandler(deps) {
|
|
1865
|
+
return async (input, _output) => {
|
|
1866
|
+
if (input.command !== "dcp") return;
|
|
1867
|
+
const subcommand = (input.arguments ?? "").trim().split(/\s+/, 1)[0]?.toLowerCase();
|
|
1868
|
+
if (subcommand === "fold") {
|
|
1869
|
+
deps.state.markBoundary(input.sessionID, Date.now(), 3);
|
|
1779
1870
|
await showToast(
|
|
1780
1871
|
deps.client,
|
|
1781
|
-
"DCP
|
|
1782
|
-
|
|
1783
|
-
"warning"
|
|
1872
|
+
"DCP fold",
|
|
1873
|
+
"\u5DF2\u6807\u8BB0\u8BDD\u9898\u8FB9\u754C\u5E76\u52A0\u6DF1\u6298\u53E0\uFF1B\u4E0B\u4E00\u6B21\u6A21\u578B\u8BF7\u6C42\u8D77\u751F\u6548\uFF0C\u4F1A\u8BDD\u4E0D\u4E2D\u65AD\u3002"
|
|
1784
1874
|
);
|
|
1875
|
+
deps.logger.debug("Handled DCP fold command", { sessionId: input.sessionID });
|
|
1876
|
+
throw new Error("__DCP_FOLD_HANDLED__");
|
|
1877
|
+
}
|
|
1878
|
+
if (subcommand === "status") {
|
|
1879
|
+
const message = await buildStatusMessage(deps, input.sessionID);
|
|
1880
|
+
await showToast(deps.client, "DCP status", message);
|
|
1881
|
+
throw new Error("__DCP_STATUS_HANDLED__");
|
|
1785
1882
|
}
|
|
1786
|
-
deps.logger.debug("Auto prune skipped", {
|
|
1787
|
-
sessionId: sessionID,
|
|
1788
|
-
status: result.status
|
|
1789
|
-
});
|
|
1790
|
-
return;
|
|
1791
|
-
}
|
|
1792
|
-
deps.autoPruner.markPruned(sessionID);
|
|
1793
|
-
if (result.status === "succeeded") {
|
|
1794
|
-
await showToast(deps.client, "DCP \u81EA\u52A8\u538B\u7F29", `\u68C0\u6D4B\u5230${reason}\uFF0C\u5DF2\u751F\u6210\u65B0\u7684\u8BED\u4E49\u68C0\u67E5\u70B9\u3002`);
|
|
1795
|
-
} else if (result.status === "cooldown") {
|
|
1796
|
-
await showToast(
|
|
1797
|
-
deps.client,
|
|
1798
|
-
"DCP \u81EA\u52A8\u538B\u7F29",
|
|
1799
|
-
`\u68C0\u6D4B\u5230${reason}\uFF0C\u4F46\u4E0A\u4E00\u6B21\u538B\u7F29\u5931\u8D25\uFF1B${retrySeconds(result.retryAfterMs)} \u79D2\u540E\u53EF\u91CD\u8BD5\u3002`,
|
|
1800
|
-
"warning"
|
|
1801
|
-
);
|
|
1802
|
-
} else {
|
|
1803
1883
|
await showToast(
|
|
1804
1884
|
deps.client,
|
|
1805
|
-
"DCP
|
|
1806
|
-
|
|
1807
|
-
"warning"
|
|
1885
|
+
"DCP",
|
|
1886
|
+
"\u7528\u6CD5\uFF1A/dcp fold\uFF08\u7ACB\u5373\u6DF1\u6298\u53E0\uFF09\u6216 /dcp status\uFF08\u67E5\u770B\u5206\u533A\u72B6\u6001\uFF09\u3002\u538B\u7F29\u7531\u52A8\u6001\u5206\u7EA7\u5F15\u64CE\u5728\u6BCF\u6B21\u8BF7\u6C42\u65F6\u81EA\u52A8\u5B8C\u6210\u3002"
|
|
1808
1887
|
);
|
|
1809
|
-
|
|
1810
|
-
deps.logger.debug("Auto prune finished", { sessionId: sessionID, status: result.status });
|
|
1811
|
-
}
|
|
1812
|
-
function createAtRestAutoPruneListener(deps) {
|
|
1813
|
-
return async (sessionID, _reason) => {
|
|
1814
|
-
if (!deps.config.enabled) return;
|
|
1815
|
-
const signals = deps.autoPruner.consumePending(sessionID);
|
|
1816
|
-
if (signals) await triggerAutoPrune(deps, sessionID, signals);
|
|
1888
|
+
throw new Error("__DCP_HELP_HANDLED__");
|
|
1817
1889
|
};
|
|
1818
1890
|
}
|
|
1819
|
-
function
|
|
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
|
-
);
|
|
1891
|
+
async function buildStatusMessage(deps, sessionID) {
|
|
1892
|
+
try {
|
|
1893
|
+
const response = await deps.client.session.messages({ path: { id: sessionID } });
|
|
1894
|
+
const data = response.data ?? response;
|
|
1895
|
+
const messages = Array.isArray(data) ? data : [];
|
|
1896
|
+
const turns = segmentTurns(messages);
|
|
1897
|
+
const estimated = estimateSlice(messages, 0, messages.length);
|
|
1898
|
+
const context = deps.state.contextTokens(sessionID);
|
|
1899
|
+
const tail = Math.min(deps.config.tailTurns, turns.length);
|
|
1900
|
+
const lines = [
|
|
1901
|
+
`\u6D88\u606F ${messages.length} \u6761 / \u5BF9\u8BDD\u8F6E ${turns.length}\uFF08\u5C3E\u90E8\u4FDD\u62A4 ${tail} \u8F6E\uFF09`,
|
|
1902
|
+
`\u4F30\u7B97 ${estimated.toLocaleString()} tokens` + (context ? ` / \u4E0A\u4E0B\u6587\u7A97\u53E3 ${context.toLocaleString()}` : "\uFF08\u7A97\u53E3\u672A\u77E5\uFF0C\u6682\u672A\u6298\u53E0\uFF09"),
|
|
1903
|
+
`\u624B\u52A8\u964D\u7EA7\u6863\u4F4D\uFF1A${deps.state.minLevel(sessionID)}`
|
|
1904
|
+
];
|
|
1905
|
+
return lines.join("\n");
|
|
1906
|
+
} catch {
|
|
1907
|
+
return "\u65E0\u6CD5\u8BFB\u53D6\u4F1A\u8BDD\u72B6\u6001\u3002";
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
function createConfigHandler(config, logger) {
|
|
1911
|
+
return async (opencodeConfig) => {
|
|
1912
|
+
const compaction = opencodeConfig.compaction ??= {};
|
|
1913
|
+
if (compaction.tail_turns === void 0) {
|
|
1914
|
+
compaction.tail_turns = DEFAULT_TAIL_TURNS;
|
|
1915
|
+
}
|
|
1916
|
+
if (compaction.preserve_recent_tokens === void 0) {
|
|
1917
|
+
compaction.preserve_recent_tokens = DEFAULT_PRESERVE_RECENT_TOKENS;
|
|
1918
|
+
}
|
|
1919
|
+
if (config.commands.enabled) {
|
|
1920
|
+
opencodeConfig.command ??= {};
|
|
1921
|
+
opencodeConfig.command.dcp = {
|
|
1922
|
+
template: "",
|
|
1923
|
+
description: "Dynamic context pruning: /dcp fold | /dcp status"
|
|
1924
|
+
};
|
|
1866
1925
|
}
|
|
1867
|
-
logger.debug("
|
|
1868
|
-
|
|
1869
|
-
|
|
1926
|
+
logger.debug("Applied host config defaults", {
|
|
1927
|
+
tailTurns: compaction.tail_turns,
|
|
1928
|
+
preserveRecentTokens: compaction.preserve_recent_tokens
|
|
1870
1929
|
});
|
|
1871
|
-
throw new Error("__DCP_SUMMARIZE_HANDLED__");
|
|
1872
1930
|
};
|
|
1873
1931
|
}
|
|
1874
1932
|
|
|
@@ -2072,15 +2130,16 @@ import { homedir as homedir3 } from "os";
|
|
|
2072
2130
|
import { dirname as dirname2, join as join3 } from "path";
|
|
2073
2131
|
|
|
2074
2132
|
// lib/prompts/compaction.ts
|
|
2075
|
-
var COMPACTION = `\u4F60\u6B63\u5728\u751F\u6210\u5F53\u524D\u4F1A\u8BDD\u552F\u4E00\u7684\u6EDA\u52A8\u68C0\u67E5\u70B9\u3002\u8F93\u51FA\u5C06\u66FF\u4EE3\u65E7\u5BF9\u8BDD\u524D\u7F00\uFF0C\u6210\u4E3A\u540E\u7EED\u6A21\u578B\u770B\u5230\u7684\u7B2C\u4E00\u6BB5\u4E0A\u4E0B\u6587\uFF1BOpenCode \u4F1A\u5728\
|
|
2133
|
+
var COMPACTION = `\u4F60\u6B63\u5728\u751F\u6210\u5F53\u524D\u4F1A\u8BDD\u552F\u4E00\u7684\u6EDA\u52A8\u68C0\u67E5\u70B9\u3002\u8F93\u51FA\u5C06\u66FF\u4EE3\u65E7\u5BF9\u8BDD\u524D\u7F00\uFF0C\u6210\u4E3A\u540E\u7EED\u6A21\u578B\u770B\u5230\u7684\u7B2C\u4E00\u6BB5\u4E0A\u4E0B\u6587\uFF1BOpenCode \u4F1A\u5728\u68C0\u67E5\u70B9\u4E4B\u540E\u76F4\u63A5\u4FDD\u7559\u6700\u8FD1\u82E5\u5E72\u8F6E\u5B8C\u6574\u5BF9\u8BDD\uFF08\u4E0D\u7ECF\u538B\u7F29\uFF0C\u8303\u56F4\u7531\u5BBF\u4E3B\u914D\u7F6E\u51B3\u5B9A\uFF09\uFF0C\u8FD9\u90E8\u5206\u5C3E\u90E8\u4E0D\u5C5E\u4E8E\u538B\u7F29\u5BF9\u8C61\uFF0C\u4E0D\u8981\u590D\u8FF0\u3001\u603B\u7ED3\u6216\u6539\u5199\u5B83\u4EEC\u3002\u7CFB\u7EDF\u7EA7\u5185\u5BB9\uFF08AGENTS.md\u3001\u9879\u76EE\u89C4\u5219\u7B49\uFF09\u7531 OpenCode \u5728\u6BCF\u6B21\u8BF7\u6C42\u65F6\u72EC\u7ACB\u6CE8\u5165\uFF0C\u4E0D\u5C5E\u4E8E\u538B\u7F29\u8303\u56F4\uFF0C\u4E0D\u8981\u590D\u8FF0\u5B83\u4EEC\u3002
|
|
2076
2134
|
|
|
2077
|
-
\u8FD9\u4E0D\u662F\u804A\u5929\u8BB0\u5F55\u6458\u8981\uFF0C\u800C\u662F\u53EF\u76F4\u63A5\u7EE7\u7EED\u5DE5\u4F5C\u7684\u8BED\u4E49\u526A\u679D\u7ED3\u679C\u3002\u6309\u4EE5\u4E0B\u89C4\u5219\u538B\u7F29\uFF1A
|
|
2135
|
+
\u8FD9\u4E0D\u662F\u804A\u5929\u8BB0\u5F55\u6458\u8981\uFF0C\u800C\u662F\u53EF\u76F4\u63A5\u7EE7\u7EED\u5DE5\u4F5C\u7684\u8BED\u4E49\u526A\u679D\u7ED3\u679C\u3002\u6309\u4EE5\u4E0B\u5206\u5C42\u89C4\u5219\u538B\u7F29\uFF1A
|
|
2078
2136
|
|
|
2079
|
-
1. \u5982\u679C\u8F93\u5165\u542B\u6709\u4E0A\u4E00\u4EFD\u68C0\u67E5\u70B9\uFF0C\u628A\u4ECD\u6709\u6548\u7684\u4FE1\u606F\u5408\u5E76\u8FDB\u65B0\u68C0\u67E5\u70B9\uFF1B\u4E0D\u8981\u5D4C\u5957\u3001\u5F15\u7528\u6216\u91CD\u590D\u65E7\u68C0\u67E5\u70B9\u3002
|
|
2080
|
-
2. \
|
|
2081
|
-
3. \
|
|
2082
|
-
4. \
|
|
2083
|
-
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
|
|
2084
2143
|
|
|
2085
2144
|
\u4F7F\u7528\u4EE5\u4E0B\u56FA\u5B9A\u7ED3\u6784\uFF0C\u7701\u7565\u786E\u5B9E\u4E3A\u7A7A\u7684\u6761\u76EE\uFF1A
|
|
2086
2145
|
|
|
@@ -2097,15 +2156,16 @@ var COMPACTION = `\u4F60\u6B63\u5728\u751F\u6210\u5F53\u524D\u4F1A\u8BDD\u552F\u
|
|
|
2097
2156
|
\u8DE8\u4EFB\u52A1\u7684\u9057\u7559\u98CE\u9669\u548C\u5F85\u786E\u8BA4\u4E8B\u9879\u3002\u53EA\u5199\u672A\u5728\u300C\u8FDB\u884C\u4E2D\u4EFB\u52A1\u8BE6\u60C5\u300D\u4E2D\u51FA\u73B0\u7684\u5185\u5BB9\uFF0C\u907F\u514D\u4E0E\u8BE5\u8282\u91CD\u590D\u3002
|
|
2098
2157
|
|
|
2099
2158
|
\u4FDD\u6301\u5177\u4F53\u3001\u53EF\u9A8C\u8BC1\u548C\u9879\u76EE\u5185\u805A\u3002\u8FDB\u884C\u4E2D\u7684\u4EFB\u52A1\u5FC5\u987B\u80FD\u51ED\u68C0\u67E5\u70B9\u76F4\u63A5\u7EE7\u7EED\uFF0C\u4E0D\u8981\u4F9D\u8D56\u5DF2\u88AB\u538B\u7F29\u6389\u7684\u4E2D\u95F4\u8FC7\u7A0B\u3002\u4FDD\u7559\u6587\u4EF6\u8DEF\u5F84\u3001\u63A5\u53E3\u3001\u547D\u4EE4\u3001\u6D4B\u8BD5\u7ED3\u679C\u548C\u9519\u8BEF\u4E8B\u5B9E\u7B49\u786C\u4E8B\u5B9E\uFF0C\u4F46\u4E0D\u8981\u4FDD\u7559\u6D88\u606F ID\u3001\u5757 ID\u3001\u951A\u70B9\u3001\u5360\u4F4D\u7B26\u3001\u63A7\u5236\u6807\u7B7E\u6216\u8FC7\u7A0B\u6027\u804A\u5929\u3002`;
|
|
2100
|
-
var COMPACTION_EN = `You are generating the single rolling checkpoint for this session. Your output will replace the old conversation prefix as the first context the model sees afterwards; OpenCode keeps
|
|
2159
|
+
var COMPACTION_EN = `You are generating the single rolling checkpoint for this session. Your output will replace the old conversation prefix as the first context the model sees afterwards; OpenCode keeps the most recent conversation turns directly after the checkpoint, uncompacted (their range is a host setting). That tail is outside the compaction scope: do not restate, summarize, or rewrite it. System-level content (AGENTS.md, project rules, etc.) is injected independently by OpenCode on every request and is not part of compaction; do not restate it.
|
|
2101
2160
|
|
|
2102
|
-
This is not a chat-log summary but a semantic pruning result one can resume working from directly. Compress by these rules:
|
|
2161
|
+
This is not a chat-log summary but a semantic pruning result one can resume working from directly. Compress by these tiered rules:
|
|
2103
2162
|
|
|
2104
|
-
1.
|
|
2105
|
-
2.
|
|
2106
|
-
3.
|
|
2107
|
-
4. When
|
|
2108
|
-
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.
|
|
2109
2169
|
|
|
2110
2170
|
Use the following fixed structure, omitting sections that are truly empty:
|
|
2111
2171
|
|
|
@@ -2213,301 +2273,30 @@ var PromptStore = class {
|
|
|
2213
2273
|
|
|
2214
2274
|
// lib/prune-tool.ts
|
|
2215
2275
|
import { tool } from "@opencode-ai/plugin";
|
|
2216
|
-
|
|
2217
|
-
// lib/session-model.ts
|
|
2218
|
-
function latestUserModel(messages) {
|
|
2219
|
-
if (!Array.isArray(messages)) return null;
|
|
2220
|
-
for (let index = messages.length - 1; index >= 0; index--) {
|
|
2221
|
-
const info = messages[index]?.info;
|
|
2222
|
-
if (info?.role !== "user") continue;
|
|
2223
|
-
const providerID = info.model?.providerID;
|
|
2224
|
-
const modelID = info.model?.modelID;
|
|
2225
|
-
if (typeof providerID === "string" && typeof modelID === "string") {
|
|
2226
|
-
return { providerID, modelID };
|
|
2227
|
-
}
|
|
2228
|
-
}
|
|
2229
|
-
return null;
|
|
2230
|
-
}
|
|
2231
|
-
async function resolveSessionModel(client, sessionID) {
|
|
2232
|
-
try {
|
|
2233
|
-
const response = await client.session.messages({ path: { id: sessionID } });
|
|
2234
|
-
return latestUserModel(response.data ?? response);
|
|
2235
|
-
} catch {
|
|
2236
|
-
return null;
|
|
2237
|
-
}
|
|
2238
|
-
}
|
|
2239
|
-
|
|
2240
|
-
// lib/prune-service.ts
|
|
2241
|
-
var PruneService = class {
|
|
2242
|
-
deferred = /* @__PURE__ */ new Set();
|
|
2243
|
-
client;
|
|
2244
|
-
summarize;
|
|
2245
|
-
logger;
|
|
2246
|
-
probeTimeoutMs;
|
|
2247
|
-
/** THE single busy/idle event state machine (absorbed the former
|
|
2248
|
-
* SessionActivityTracker — no second busy cache may exist). */
|
|
2249
|
-
boundary;
|
|
2250
|
-
constructor(deps) {
|
|
2251
|
-
this.client = deps.client;
|
|
2252
|
-
this.summarize = deps.summarize;
|
|
2253
|
-
this.logger = deps.logger;
|
|
2254
|
-
this.probeTimeoutMs = deps.probeTimeoutMs ?? PROBE_TIMEOUT_MS;
|
|
2255
|
-
this.boundary = new SessionBoundaryTracker({
|
|
2256
|
-
probeBusy: (sessionID) => this.probeBusy(sessionID),
|
|
2257
|
-
logger: deps.logger,
|
|
2258
|
-
now: deps.now,
|
|
2259
|
-
setTimer: deps.setTimer,
|
|
2260
|
-
clearTimer: deps.clearTimer
|
|
2261
|
-
});
|
|
2262
|
-
this.boundary.onAtRest((sessionID) => {
|
|
2263
|
-
if (!this.deferred.delete(sessionID)) return;
|
|
2264
|
-
void this.drainQueued(sessionID);
|
|
2265
|
-
});
|
|
2266
|
-
}
|
|
2267
|
-
/**
|
|
2268
|
-
* Feed every host event through here. `session.status` and legacy
|
|
2269
|
-
* `session.idle` feed the boundary tracker (quiet window + expiry probe);
|
|
2270
|
-
* the deferral queue drains at the confirmed at-rest classification, and
|
|
2271
|
-
* queued prunes are forgotten once a compaction (or the session itself)
|
|
2272
|
-
* is gone.
|
|
2273
|
-
*/
|
|
2274
|
-
observeEvent(type, properties) {
|
|
2275
|
-
const sessionID = eventSessionID(properties);
|
|
2276
|
-
if (type === "session.status") {
|
|
2277
|
-
this.boundary.observeStatus(sessionID, properties?.status?.type);
|
|
2278
|
-
return;
|
|
2279
|
-
}
|
|
2280
|
-
if (type === "session.idle") {
|
|
2281
|
-
this.boundary.observeLegacyIdle(sessionID);
|
|
2282
|
-
return;
|
|
2283
|
-
}
|
|
2284
|
-
if (!sessionID) return;
|
|
2285
|
-
if (type === "session.compacted") {
|
|
2286
|
-
this.deferred.delete(sessionID);
|
|
2287
|
-
this.boundary.observeCompacted(sessionID);
|
|
2288
|
-
return;
|
|
2289
|
-
}
|
|
2290
|
-
if (type === "session.deleted") {
|
|
2291
|
-
this.deferred.delete(sessionID);
|
|
2292
|
-
this.boundary.observeDeleted(sessionID);
|
|
2293
|
-
}
|
|
2294
|
-
}
|
|
2295
|
-
async request(request) {
|
|
2296
|
-
const beforeModel = await this.gate(request);
|
|
2297
|
-
if (beforeModel) return beforeModel;
|
|
2298
|
-
const model = await resolveSessionModel(this.client, request.sessionID);
|
|
2299
|
-
if (!model) return { status: "no-model" };
|
|
2300
|
-
const afterModel = await this.gate(request);
|
|
2301
|
-
if (afterModel) return afterModel;
|
|
2302
|
-
const serverBusy = await this.probeBusy(request.sessionID);
|
|
2303
|
-
if (serverBusy === true) {
|
|
2304
|
-
const fallback = await this.outcomeFor(request, { action: "stand-down" });
|
|
2305
|
-
if (fallback) return fallback;
|
|
2306
|
-
}
|
|
2307
|
-
const result = await this.summarize.summarize({ sessionID: request.sessionID, model });
|
|
2308
|
-
if (result.status === "rejected") {
|
|
2309
|
-
return { status: "busy" };
|
|
2310
|
-
}
|
|
2311
|
-
return result;
|
|
2312
|
-
}
|
|
2313
|
-
/** One admission check; `null` means the request may proceed. */
|
|
2314
|
-
gate(request) {
|
|
2315
|
-
return this.outcomeFor(request, this.admit(request));
|
|
2316
|
-
}
|
|
2317
|
-
/**
|
|
2318
|
-
* Executes a queued prune at the confirmed at-rest boundary. The tool
|
|
2319
|
-
* already promised its caller this prune would run; losing the busy race
|
|
2320
|
-
* must not silently break that promise, so a busy outcome re-queues for
|
|
2321
|
-
* the next at-rest boundary. Every other outcome is terminal: it is
|
|
2322
|
-
* logged and never retried — new prune demand arrives through new
|
|
2323
|
-
* triggers only. Execution is re-guarded on every attempt, so re-queueing
|
|
2324
|
-
* never violates the never-interrupt invariant.
|
|
2325
|
-
*/
|
|
2326
|
-
async drainQueued(sessionID) {
|
|
2327
|
-
let outcome;
|
|
2328
|
-
try {
|
|
2329
|
-
outcome = await this.request({ sessionID, onBusy: "proceed" });
|
|
2330
|
-
} catch (error) {
|
|
2331
|
-
this.logger.warn("Queued prune drain failed; the prune was not retried", {
|
|
2332
|
-
sessionId: sessionID,
|
|
2333
|
-
error: error instanceof Error ? error.message : String(error)
|
|
2334
|
-
});
|
|
2335
|
-
return;
|
|
2336
|
-
}
|
|
2337
|
-
if (outcome.status === "busy") {
|
|
2338
|
-
this.deferred.add(sessionID);
|
|
2339
|
-
return;
|
|
2340
|
-
}
|
|
2341
|
-
this.logger.debug("Queued prune drain finished", {
|
|
2342
|
-
sessionId: sessionID,
|
|
2343
|
-
status: outcome.status
|
|
2344
|
-
});
|
|
2345
|
-
}
|
|
2346
|
-
/**
|
|
2347
|
-
* Turns an admission decision into a terminal outcome, or `null` when the
|
|
2348
|
-
* request may proceed. Deferral additionally enqueues the session.
|
|
2349
|
-
*/
|
|
2350
|
-
async outcomeFor(request, admission) {
|
|
2351
|
-
if (admission.action === "go") return null;
|
|
2352
|
-
if (admission.action === "stand-down") return { status: "busy" };
|
|
2353
|
-
this.deferred.add(request.sessionID);
|
|
2354
|
-
this.logger.debug("Prune deferred to the next session at-rest boundary", {
|
|
2355
|
-
sessionId: request.sessionID
|
|
2356
|
-
});
|
|
2357
|
-
return { status: "deferred" };
|
|
2358
|
-
}
|
|
2359
|
-
admit(request) {
|
|
2360
|
-
if (request.onBusy === "defer") return { action: "defer" };
|
|
2361
|
-
if (this.boundary.state(request.sessionID) === "busy") return { action: "stand-down" };
|
|
2362
|
-
return { action: "go" };
|
|
2363
|
-
}
|
|
2364
|
-
/**
|
|
2365
|
-
* THE single live busy probe, reused verbatim by the boundary tracker's
|
|
2366
|
-
* expiry check. Bounded by a finite deadline: a never-returning probe
|
|
2367
|
-
* resolves `null` within the timeout (fail-open). Returns `null` when the
|
|
2368
|
-
* answer is unknown (endpoint or SDK method missing, request failed,
|
|
2369
|
-
* timeout) — callers must fail open in that case.
|
|
2370
|
-
*/
|
|
2371
|
-
async probeBusy(sessionID) {
|
|
2372
|
-
const statusFn = this.client.session.status;
|
|
2373
|
-
if (typeof statusFn !== "function") return null;
|
|
2374
|
-
const query = (async () => {
|
|
2375
|
-
try {
|
|
2376
|
-
return await statusFn.call(
|
|
2377
|
-
this.client.session
|
|
2378
|
-
);
|
|
2379
|
-
} catch {
|
|
2380
|
-
return null;
|
|
2381
|
-
}
|
|
2382
|
-
})();
|
|
2383
|
-
const response = await Promise.race([
|
|
2384
|
-
query,
|
|
2385
|
-
new Promise((resolve) => setTimeout(() => resolve(null), this.probeTimeoutMs))
|
|
2386
|
-
]);
|
|
2387
|
-
const map = response?.data ?? response;
|
|
2388
|
-
const info = map?.[sessionID];
|
|
2389
|
-
return info?.type === "busy" || info?.type === "retry";
|
|
2390
|
-
}
|
|
2391
|
-
};
|
|
2392
|
-
|
|
2393
|
-
// lib/prune-tool.ts
|
|
2394
2276
|
var PRUNE_TOOL_NAME = "dcp_prune";
|
|
2395
|
-
var PRUNE_TOOL_DESCRIPTION = `\
|
|
2277
|
+
var PRUNE_TOOL_DESCRIPTION = `\u6807\u8BB0\u8BDD\u9898\u8FB9\u754C\u5E76\u52A0\u6DF1\u672C\u4F1A\u8BDD\u7684\u52A8\u6001\u4E0A\u4E0B\u6587\u6298\u53E0\u3002\u538B\u7F29\u672C\u8EAB\u7531 DCP \u5728\u6BCF\u6B21\u6A21\u578B\u8BF7\u6C42\u65F6\u81EA\u52A8\u5206\u7EA7\u5B8C\u6210\uFF08\u8FDC\u8DDD\u79BB\u91CD\u5EA6\u6298\u53E0\u3001\u5F53\u524D\u4EFB\u52A1\u8F7B\u5EA6\u6298\u53E0\u3001\u6700\u8FD1\u6570\u8F6E\u539F\u6837\u4FDD\u7559\uFF09\uFF0C\u672C\u5DE5\u5177\u53EA\u8C03\u6574\u6298\u53E0\u7B56\u7565\uFF0C\u4E0D\u6267\u884C\u4EFB\u4F55\u4F1A\u8BDD\u64CD\u4F5C\u3002
|
|
2396
2278
|
|
|
2397
2279
|
\u4EC5\u5728\u8FD9\u4E9B\u60C5\u51B5\u4E0B\u8C03\u7528\uFF1A
|
|
2398
2280
|
- \u5BF9\u8BDD\u8BDD\u9898\u53D1\u751F\u660E\u663E\u53D8\u66F4\uFF1A\u5F00\u59CB\u5904\u7406\u65B0\u7684\u95EE\u9898\u57DF\u3001\u5207\u6362\u5230\u53E6\u4E00\u4E2A\u6A21\u5757/\u4ED3\u5E93/\u4EFB\u52A1\uFF1B
|
|
2399
2281
|
- \u7528\u6237\u660E\u786E\u8981\u6C42\u538B\u7F29\u4E0A\u4E0B\u6587\u3002
|
|
2400
2282
|
|
|
2401
|
-
\u540C\u4E00\u4EFB\u52A1\u5185\u7684\u591A\u8F6E\u8FFD\u95EE\u3001\u53C2\u6570\u5FAE\u8C03\u3001\u5EF6\u7EED\u5F53\u524D\u5DE5\u4F5C\uFF0C\u90FD\u4E0D\u8981\u8C03\u7528\u3002\u8C03\u7528\u4E0D\
|
|
2283
|
+
\u540C\u4E00\u4EFB\u52A1\u5185\u7684\u591A\u8F6E\u8FFD\u95EE\u3001\u53C2\u6570\u5FAE\u8C03\u3001\u5EF6\u7EED\u5F53\u524D\u5DE5\u4F5C\uFF0C\u90FD\u4E0D\u8981\u8C03\u7528\u3002\u8C03\u7528\u77AC\u65F6\u5B8C\u6210\uFF0C\u7EDD\u4E0D\u6253\u65AD\u5F53\u524D\u5DE5\u4F5C\uFF1A\u65E7\u4EFB\u52A1\u5185\u5BB9\u4ECE\u4E0B\u4E00\u6B21\u6A21\u578B\u8BF7\u6C42\u8D77\u88AB\u6298\u53E0\u4E3A\u7ED3\u6784\u5316\u6458\u8981\uFF0C\u6700\u8FD1\u5BF9\u8BDD\u4E0E\u5F53\u524D\u4EFB\u52A1\u7EC6\u8282\u4E0D\u53D7\u5F71\u54CD\u3002\u4E0D\u8981\u4E3A\u540C\u4E00\u8BDD\u9898\u53CD\u590D\u8C03\u7528\u3002`;
|
|
2402
2284
|
function createPruneTool(deps) {
|
|
2403
2285
|
return tool({
|
|
2404
2286
|
description: PRUNE_TOOL_DESCRIPTION,
|
|
2405
2287
|
args: {},
|
|
2406
2288
|
execute: async (_args, context) => {
|
|
2407
2289
|
const sessionID = context.sessionID;
|
|
2408
|
-
const
|
|
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`;
|
|
2290
|
+
const now = (deps.now ?? Date.now)();
|
|
2291
|
+
deps.state.markBoundary(sessionID, now, 2);
|
|
2292
|
+
deps.logger.debug("Prune tool marked a topic boundary", {
|
|
2293
|
+
sessionId: sessionID
|
|
2294
|
+
});
|
|
2295
|
+
return "DCP\uFF1A\u5DF2\u6807\u8BB0\u8BDD\u9898\u8FB9\u754C\u5E76\u52A0\u6DF1\u672C\u4F1A\u8BDD\u6298\u53E0\u2014\u2014\u65E7\u4EFB\u52A1\u5185\u5BB9\u5C06\u4ECE\u4E0B\u4E00\u6B21\u6A21\u578B\u8BF7\u6C42\u8D77\u5206\u7EA7\u6298\u53E0\u4E3A\u7ED3\u6784\u5316\u6458\u8981\uFF1B\u6700\u8FD1\u5BF9\u8BDD\u4E0E\u5F53\u524D\u4EFB\u52A1\u7EC6\u8282\u4E0D\u53D7\u5F71\u54CD\uFF0C\u4F1A\u8BDD\u672A\u4E2D\u65AD\u3002";
|
|
2428
2296
|
}
|
|
2429
2297
|
});
|
|
2430
2298
|
}
|
|
2431
2299
|
|
|
2432
|
-
// lib/summarize.ts
|
|
2433
|
-
function errorMessage(error) {
|
|
2434
|
-
if (error instanceof Error) return error.message;
|
|
2435
|
-
if (typeof error === "string") return error;
|
|
2436
|
-
try {
|
|
2437
|
-
return JSON.stringify(error);
|
|
2438
|
-
} catch {
|
|
2439
|
-
return "Unknown native compaction error";
|
|
2440
|
-
}
|
|
2441
|
-
}
|
|
2442
|
-
function isBusyRejection(error) {
|
|
2443
|
-
if (error instanceof Error && /\bbusy\b/i.test(error.message)) return true;
|
|
2444
|
-
if (typeof error === "string" && /\bbusy\b/i.test(error)) return true;
|
|
2445
|
-
const structured = error;
|
|
2446
|
-
if (!structured || typeof structured !== "object") return false;
|
|
2447
|
-
if (structured.status === 409 || structured.statusCode === 409 || structured.code === 409) {
|
|
2448
|
-
return true;
|
|
2449
|
-
}
|
|
2450
|
-
return typeof structured.name === "string" && /busy/i.test(structured.name);
|
|
2451
|
-
}
|
|
2452
|
-
var SummarizeCoordinator = class {
|
|
2453
|
-
constructor(client, logger, options) {
|
|
2454
|
-
this.client = client;
|
|
2455
|
-
this.logger = logger;
|
|
2456
|
-
this.options = options;
|
|
2457
|
-
this.now = options.now ?? Date.now;
|
|
2458
|
-
}
|
|
2459
|
-
inFlight = /* @__PURE__ */ new Map();
|
|
2460
|
-
failedAt = /* @__PURE__ */ new Map();
|
|
2461
|
-
now;
|
|
2462
|
-
summarize(request) {
|
|
2463
|
-
const active = this.inFlight.get(request.sessionID);
|
|
2464
|
-
if (active) return active;
|
|
2465
|
-
const failedAt = this.failedAt.get(request.sessionID);
|
|
2466
|
-
if (failedAt !== void 0) {
|
|
2467
|
-
const retryAfterMs = this.options.failureCooldownMs - (this.now() - failedAt);
|
|
2468
|
-
if (retryAfterMs > 0) {
|
|
2469
|
-
return Promise.resolve({ status: "cooldown", retryAfterMs });
|
|
2470
|
-
}
|
|
2471
|
-
this.failedAt.delete(request.sessionID);
|
|
2472
|
-
}
|
|
2473
|
-
const promise = this.invokeNative(request).finally(() => {
|
|
2474
|
-
if (this.inFlight.get(request.sessionID) === promise) {
|
|
2475
|
-
this.inFlight.delete(request.sessionID);
|
|
2476
|
-
}
|
|
2477
|
-
});
|
|
2478
|
-
this.inFlight.set(request.sessionID, promise);
|
|
2479
|
-
return promise;
|
|
2480
|
-
}
|
|
2481
|
-
async invokeNative(request) {
|
|
2482
|
-
try {
|
|
2483
|
-
const response = await this.client.session.summarize({
|
|
2484
|
-
path: { id: request.sessionID },
|
|
2485
|
-
body: request.model
|
|
2486
|
-
});
|
|
2487
|
-
const nativeError = response?.error;
|
|
2488
|
-
if (nativeError && isBusyRejection(nativeError)) {
|
|
2489
|
-
return { status: "rejected", reason: "busy" };
|
|
2490
|
-
}
|
|
2491
|
-
if (nativeError || response?.data !== true) {
|
|
2492
|
-
throw new Error(errorMessage(nativeError ?? "Native summarize returned false"));
|
|
2493
|
-
}
|
|
2494
|
-
this.failedAt.delete(request.sessionID);
|
|
2495
|
-
return { status: "succeeded" };
|
|
2496
|
-
} catch (error) {
|
|
2497
|
-
if (isBusyRejection(error)) {
|
|
2498
|
-
return { status: "rejected", reason: "busy" };
|
|
2499
|
-
}
|
|
2500
|
-
this.failedAt.set(request.sessionID, this.now());
|
|
2501
|
-
const message = errorMessage(error);
|
|
2502
|
-
await this.logger.warn("Native summarize failed; context remains unchanged", {
|
|
2503
|
-
sessionId: request.sessionID,
|
|
2504
|
-
error: message
|
|
2505
|
-
});
|
|
2506
|
-
return { status: "failed", error: message };
|
|
2507
|
-
}
|
|
2508
|
-
}
|
|
2509
|
-
};
|
|
2510
|
-
|
|
2511
2300
|
// lib/update.ts
|
|
2512
2301
|
import { readFile } from "fs/promises";
|
|
2513
2302
|
import { basename, dirname as dirname3, join as join4 } from "path";
|
|
@@ -2654,57 +2443,48 @@ var server = (async (ctx) => {
|
|
|
2654
2443
|
config.experimental.customPrompts,
|
|
2655
2444
|
config.language
|
|
2656
2445
|
);
|
|
2657
|
-
const
|
|
2658
|
-
failureCooldownMs: config.summarize.failureCooldownMs
|
|
2659
|
-
});
|
|
2660
|
-
const prune = new PruneService({ client: ctx.client, summarize, logger });
|
|
2661
|
-
const autoPruner = new AutoPruner(config.autoPrune);
|
|
2662
|
-
if (config.autoPrune.enabled) {
|
|
2663
|
-
prune.boundary.onAtRest(
|
|
2664
|
-
createAtRestAutoPruneListener({
|
|
2665
|
-
client: ctx.client,
|
|
2666
|
-
prune,
|
|
2667
|
-
autoPruner,
|
|
2668
|
-
config: config.autoPrune,
|
|
2669
|
-
logger
|
|
2670
|
-
})
|
|
2671
|
-
);
|
|
2672
|
-
}
|
|
2446
|
+
const state = new DtcState();
|
|
2673
2447
|
logger.info("DCP initialized", {
|
|
2674
2448
|
commands: config.commands.enabled,
|
|
2675
|
-
|
|
2449
|
+
dtc: config.dtc.enabled,
|
|
2676
2450
|
tool: config.tool.enabled,
|
|
2677
2451
|
customPrompts: config.experimental.customPrompts
|
|
2678
2452
|
});
|
|
2679
2453
|
startAutoUpdate(ctx, config.autoUpdate);
|
|
2680
2454
|
return {
|
|
2681
|
-
"experimental.session.compacting": createSessionCompactingHandler(
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
//
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2455
|
+
"experimental.session.compacting": createSessionCompactingHandler({
|
|
2456
|
+
prompts,
|
|
2457
|
+
logger,
|
|
2458
|
+
client: ctx.client,
|
|
2459
|
+
state
|
|
2460
|
+
}),
|
|
2461
|
+
// THE compression surface: dynamic tiered folding on every model
|
|
2462
|
+
// request, plus the chat.params feed that teaches the engine each
|
|
2463
|
+
// session's context-window size.
|
|
2464
|
+
...config.dtc.enabled && {
|
|
2465
|
+
"experimental.chat.messages.transform": createTransformHandler({
|
|
2466
|
+
state,
|
|
2467
|
+
config: config.dtc,
|
|
2692
2468
|
logger
|
|
2693
|
-
})
|
|
2469
|
+
}),
|
|
2470
|
+
"chat.params": createChatParamsHandler({ state, logger })
|
|
2694
2471
|
},
|
|
2472
|
+
// Lifecycle cleanup for DTC session state (LRU-bounded regardless).
|
|
2473
|
+
event: createEventHandler({ state, logger }),
|
|
2695
2474
|
...config.tool.enabled && {
|
|
2696
|
-
tool: { [PRUNE_TOOL_NAME]: createPruneTool({
|
|
2475
|
+
tool: { [PRUNE_TOOL_NAME]: createPruneTool({ state, logger }) }
|
|
2697
2476
|
},
|
|
2698
2477
|
...config.commands.enabled && {
|
|
2699
|
-
"command.execute.before": createCommandExecuteHandler(
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2478
|
+
"command.execute.before": createCommandExecuteHandler({
|
|
2479
|
+
client: ctx.client,
|
|
2480
|
+
state,
|
|
2481
|
+
config: config.dtc,
|
|
2482
|
+
logger
|
|
2483
|
+
})
|
|
2484
|
+
},
|
|
2485
|
+
// Always registered: besides the optional /dcp command it raises the
|
|
2486
|
+
// host's compaction tail protection to DCP's tiered defaults.
|
|
2487
|
+
config: createConfigHandler(config, logger)
|
|
2708
2488
|
};
|
|
2709
2489
|
});
|
|
2710
2490
|
var index_default = server;
|