billion-context-dsh 0.2.21 → 0.2.22
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 +81 -28
- package/README.md +77 -24
- package/dist/block-ledger.d.ts +66 -0
- package/dist/commands.d.ts +2 -1
- package/dist/config.d.ts +1 -1
- package/dist/host-tokens.d.ts +49 -0
- package/dist/index.d.ts +48 -5
- package/dist/index.js +2446 -829
- package/dist/index.js.map +1 -1
- package/dist/lru.d.ts +15 -0
- package/dist/messages.d.ts +100 -0
- package/dist/nudge.d.ts +59 -13
- package/dist/presets.d.ts +61 -0
- package/dist/prompts.d.ts +2 -2
- package/dist/region.d.ts +185 -34
- package/dist/settings.d.ts +157 -0
- package/dist/state.d.ts +11 -0
- package/dist/tools.d.ts +113 -2
- package/dist/window.d.ts +32 -3
- package/package.json +25 -17
- package/dist/tool-pairing.d.ts +0 -36
package/dist/index.js
CHANGED
|
@@ -4,8 +4,366 @@ import {
|
|
|
4
4
|
ManualCompactionError
|
|
5
5
|
} from "@deepseek-ai/dsh-compaction";
|
|
6
6
|
|
|
7
|
-
// node_modules/acp-kernel/dist/
|
|
7
|
+
// node_modules/acp-kernel/dist/chunk-6TAK7DSI.js
|
|
8
8
|
import { createRequire } from "module";
|
|
9
|
+
var require2 = createRequire(import.meta.url);
|
|
10
|
+
function defaultCountTokens(text) {
|
|
11
|
+
if (!text) return 0;
|
|
12
|
+
const cjk = text.match(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/g);
|
|
13
|
+
const cjkCount = cjk?.length ?? 0;
|
|
14
|
+
return cjkCount + Math.ceil((text.length - cjkCount) / 4);
|
|
15
|
+
}
|
|
16
|
+
function thinkingTokenValue(thinking) {
|
|
17
|
+
return typeof thinking === "number" && Number.isFinite(thinking) && thinking > 0 ? thinking : 0;
|
|
18
|
+
}
|
|
19
|
+
function countMessageTokens(message, countTokens = defaultCountTokens) {
|
|
20
|
+
return countTokens(message.text ?? "") + thinkingTokenValue(message.thinkingTokens);
|
|
21
|
+
}
|
|
22
|
+
var COMPRESS_PHILOSOPHY = `Compression Philosophy:
|
|
23
|
+
- All compression serves the primary task, but be frugal.
|
|
24
|
+
- Context capacity is precious. Save context by compressing consumed outputs, not by avoiding tools.
|
|
25
|
+
- Compress by need, not by percentage.
|
|
26
|
+
- Work from summaries, not raw tool outputs. All listed ranges (user prompts, tool outputs, code, logs, exploration, intermediate steps) should be compressed to summary format \u2014 the ONLY exceptions are protected content, content the current step is actively using, or critical content you cannot reconstruct.`;
|
|
27
|
+
var HOW_TO_COMPRESS_RULES = `HOW TO COMPRESS
|
|
28
|
+
|
|
29
|
+
When you call \`compress\`, the summary you write becomes the only record of the replaced conversation. Make it self-contained and complete: every user request, experiment purpose, and work task in the range must be accurately captured. A later reader (or you, after decompressing) should be able to continue the task WITHOUT needing the original. The summary records the PAST as of this block's creation: label recorded task state as history ("TASK AS OF THIS BLOCK: ...") \u2014 never as a live instruction, so a later reader treats it as settled context, not something to re-execute. Write plain text with real unicode characters; never copy \\uXXXX escape sequences or JSON-escaped fragments out of tool output.
|
|
30
|
+
|
|
31
|
+
KEEP VERBATIM \u2014 never paraphrase or abbreviate these:
|
|
32
|
+
- Full file paths with line numbers, directory prefix on every mention (\`lib/hooks.ts:347\`, \`src/index.ts:12-18\`, \`gatenet_v3/model.py:45\`). Never abbreviate to a bare filename (\`hooks.ts\`, \`model.py\`) \u2014 they are ambiguous and cannot be grepped or decompressed-to later.
|
|
33
|
+
- Function, class, and type signatures (exact names, params, return types) AND critical code lines that encode logic \u2014 the line that IS the finding, not just the function name (e.g. \`kv_keys += define_gate * a_key[i](emb)\` is more useful than "see model_kvnet.py").
|
|
34
|
+
- Error messages and stack traces (exact text \u2014 you need the literal string to grep for it later).
|
|
35
|
+
- Key details from reports and analyses \u2014 not just the conclusion. Keep the comparison numbers and the mechanism, not "X is worse" alone (write "1.76\xD7 PPL gap because KV store is static", not "KVNet underperforms").
|
|
36
|
+
- Decisions and their rationale ("chose X over Y because Z" \u2014 the "because" is load-bearing; without it the decision looks arbitrary).
|
|
37
|
+
- Constraints discovered ("must support Node 22", "no new dependencies", "AGENTS.md forbids \`as any\`").
|
|
38
|
+
- Exact values: versions, config keys, thresholds, magic numbers.
|
|
39
|
+
- User intent \u2014 quote short user messages verbatim ONLY WITH their message ref, e.g. \`User said (m00132): "ship it tonight"\`. Without a verifiable ref, paraphrase (\`user previously asked (paraphrased): ...\`) \u2014 this is the one exception to the verbatim rule above; never present a reconstructed or half-remembered phrase as a verbatim quote. When the message is too long to quote, preserve intent with extra care: do not change scope, constraints, priorities, acceptance criteria, or requested outcomes. Quotes are historical records, never current directives. Losing these changes the task itself.
|
|
40
|
+
- The user's overall goal and any changes to it \u2014 the big-picture objective plus how it evolved during the compressed range. Each summary must reflect the goal as it stood at the end of the range, including pivots (e.g., "initially: fix bug X \u2192 pivoted to: refactor module Y after discovering root cause"). Losing the goal or its evolution makes all subsequent work appear unmotivated.
|
|
41
|
+
- Purpose behind each significant action \u2014 preserve not just what was done but why: the hypothesis behind each experiment, the question behind each exploration, the task goal behind each work action. Without purpose, the summary reads as disconnected technical steps with no through-line.
|
|
42
|
+
- Open questions and unresolved TODOs \u2014 losing these changes what work appears to remain.
|
|
43
|
+
- Message refs of key anchors (\`m00420\`, \`m00510\u2013m00520\`) \u2014 they let you or a later reader jump back via decompress to the exact original.
|
|
44
|
+
|
|
45
|
+
DROP \u2014 extract the signal, discard the vessel:
|
|
46
|
+
- Verbose logs (build/test/\`npm\` output) once you have captured the error line or the result.
|
|
47
|
+
- Duplicate file reads once the needed content is recorded.
|
|
48
|
+
- Consumed exploration \u2014 search hits, agent return values, successful tool outputs \u2014 once you have extracted the facts you need (same rule as dead-ends, but nothing went wrong; the content is simply spent).
|
|
49
|
+
- Dead-end exploration \u2014 but PRESERVE the lesson in one line: "tried X, failed because Y".
|
|
50
|
+
- Back-and-forth discussion and self-corrections once the final position is captured (keep the outcome, drop the journey to it).
|
|
51
|
+
- Repeated status checks (\`git status\`, \`ls\`) once state is known.
|
|
52
|
+
|
|
53
|
+
For each significant item you DROP (scripts, reports, large analyses, long tool outputs), add a one-line CONTENT description of what it covers \u2014 not where it lives. Bad: "probe script at /path/probe_kvnet.py". Good: "probe_kvnet.py: tests n-gram baseline, generation quality, long-range dependency, position sensitivity, op pipeline, QUERY attention." This lets a later decompress target the right block by relevance, not by guessing locations.
|
|
54
|
+
|
|
55
|
+
PRIORITY \u2014 when the summary must be compact, preserve in this order:
|
|
56
|
+
1. User's overall goal, goal evolution, intent, and hard constraints (losing these changes the task).
|
|
57
|
+
2. Decisions and rationale.
|
|
58
|
+
3. Exact technical artifacts: paths, signatures, errors, values.
|
|
59
|
+
4. Conclusions and key findings.
|
|
60
|
+
5. Lessons learned: what failed and why.
|
|
61
|
+
|
|
62
|
+
Write dense, scannable bullets \u2014 not narrative prose. If the range spans distinct concerns (request \u2192 findings \u2192 decision), group bullets under short thematic headers so a reader can scan to the part they need. Every line must earn its place. Do not mimic the style of existing summaries in context; follow these rules.`;
|
|
63
|
+
var TIER2_DISTILL_RULES = `TIER 2 COMPRESSION \u2014 DISTILLATION
|
|
64
|
+
|
|
65
|
+
You are compressing historical summaries (not raw conversation). These summaries have already captured the details. Your job is to DISTILL them: extract only what matters for future work, discard the process.
|
|
66
|
+
|
|
67
|
+
KEEP \u2014 these are the only things that survive distillation:
|
|
68
|
+
- Decisions and their rationale ("chose X over Y because Z" \u2014 the "because" is load-bearing).
|
|
69
|
+
- Final outcomes: version numbers shipped, PR numbers merged/closed, bugs fixed or deferred.
|
|
70
|
+
- Key lessons: what failed and why ("tried X, failed because Y"). These prevent repeating mistakes.
|
|
71
|
+
- Critical constraints discovered ("must support Node 22", "AGENTS.md forbids as any").
|
|
72
|
+
- Design decisions with architectural impact ("chose compress-as-anchor over synthetic messages because prefix cache").
|
|
73
|
+
- User quotes and task state only as attributed history: keep the source ref with any user quote; never carry a tier-1 "CURRENT TASK" claim forward as a live directive \u2014 relabel it "TASK AS OF THIS BLOCK".
|
|
74
|
+
- Whether content is OBSOLETE or SUPERSEDED \u2014 mark with one line: "[SUPERSEDED by PR #NNN]" or "[OBSOLETE: deleted in vX.Y.Z]". Do NOT keep the obsolete content's details \u2014 just the marker and reason.
|
|
75
|
+
- Function/class/type names and module paths that are the SUBJECT of the work \u2014 e.g., "fixed filterCompressedRanges in prune.ts", "added SessionStateRegistry in state.ts". Not exact line numbers or full signatures \u2014 just enough to LOCATE the code without searching.
|
|
76
|
+
- Exploration findings: if a block was exploratory with no decision, keep the CONCLUSION in one line ("explored X, not viable because Y"). Do not keep the exploration process.
|
|
77
|
+
|
|
78
|
+
DROP \u2014 these were useful during the work but are no longer needed:
|
|
79
|
+
- Exact line numbers, diffs, verbose function signatures, full code listings.
|
|
80
|
+
- Build/deploy process details, test execution steps.
|
|
81
|
+
- Review process details (who reviewed, what rounds, test counts).
|
|
82
|
+
- Verbose logs, command output, intermediate debugging steps.
|
|
83
|
+
|
|
84
|
+
FORMAT:
|
|
85
|
+
- Start each distilled block with a source header line:
|
|
86
|
+
\`Source: bN+bM+... (XK\u2192YK tok, Zx). [original topic]\`
|
|
87
|
+
Example: \`Source: b5+b7 (56K+44K\u2192268 tok, 375x). [Tool-result recap + publish]\`
|
|
88
|
+
- 3-5 bullet points per source block, each a self-contained fact.
|
|
89
|
+
- Dense, scannable \u2014 no narrative prose.
|
|
90
|
+
- Start with the outcome, not the process: "v1.13.0 shipped (7 PRs bundled)" not "implemented 7 PRs then reviewed then merged".
|
|
91
|
+
- Cross-block synthesis: if multiple source blocks cover the same topic (same PR, same feature, same bug), MERGE them into a single group of bullets. Do not repeat the same fact from different blocks \u2014 keep it once under the most relevant source header.
|
|
92
|
+
|
|
93
|
+
SIZE TARGET: 50-150 tokens per source block (excluding the header). If you can't fit it in 150 tokens, you're keeping too much process. If a block has nothing worth keeping (pure noise), output just the header followed by "[no actionable content]."`;
|
|
94
|
+
var TIER3_CONDENSE_RULES = `TIER 3 COMPRESSION \u2014 ULTRA-CONDENSATION
|
|
95
|
+
|
|
96
|
+
You are compressing distilled summaries (Tier 2) into ultra-condensed facts (Tier 3). The distilled summaries already contain only decisions and outcomes. Your job is to reduce them to bare factual references.
|
|
97
|
+
|
|
98
|
+
PRIORITY \u2014 when a source block has more facts than the size target allows, keep in this order:
|
|
99
|
+
1. Shipped outcomes (versions released, PRs merged) \u2014 these are permanent record.
|
|
100
|
+
2. Open work (PRs/issues still pending) \u2014 these may need follow-up.
|
|
101
|
+
3. Key decisions with architectural impact ("chose X over Y because Z").
|
|
102
|
+
4. Critical constraints ("must support Node 22").
|
|
103
|
+
Drop everything else. Tier 3 is a lookup index, not a knowledge base.
|
|
104
|
+
|
|
105
|
+
FORMAT:
|
|
106
|
+
- Start with a source header line:
|
|
107
|
+
\`Source: bN+bM+... (XK\u2192YK tok, Zx). [original topic]\`
|
|
108
|
+
- Output 1-3 facts per source block. Each fact is a single line: subject + outcome.
|
|
109
|
+
- No explanations, no rationale, no process \u2014 just the fact.
|
|
110
|
+
- Format: "[PR/Issue/Version] \u2014 [outcome in \u22648 words]"
|
|
111
|
+
- Merge related facts from different source blocks if they concern the same topic.
|
|
112
|
+
|
|
113
|
+
EXAMPLES:
|
|
114
|
+
- "v1.13.0 shipped \u2014 quality gate + GC fix (7 PRs)"
|
|
115
|
+
- "PR #196 merged \u2014 preserve-first-user (supersedes #169)"
|
|
116
|
+
- "Bug 1214 fixed \u2014 compress consumed all user messages"
|
|
117
|
+
- "Chose compress-as-anchor \u2014 prefix cache benefit over synthetic injection"
|
|
118
|
+
- "Constraint: AGENTS.md forbids as any \u2014 never suppress types"
|
|
119
|
+
|
|
120
|
+
DROP:
|
|
121
|
+
- Multi-sentence context. If a fact needs >1 sentence, it's too detailed for Tier 3.
|
|
122
|
+
- Lessons learned ("tried X, failed because Y") \u2014 drop UNLESS the failure is likely to recur and the block is <30 days old.
|
|
123
|
+
- Design rationale details \u2014 keep the decision, drop the "because" unless it's a critical constraint.
|
|
124
|
+
- Anything marked [OBSOLETE] or [SUPERSEDED] \u2014 drop entirely, note "[N blocks obsolete]" in the summary.
|
|
125
|
+
|
|
126
|
+
SIZE TARGET: 30-60 tokens per source block (including header). For a batch of N source blocks, total output \u2248 N \xD7 40 tokens. If a source block has only one trivial fact, output just the header + one line.`;
|
|
127
|
+
var defaultPrompts = Object.freeze({
|
|
128
|
+
compressPhilosophy: COMPRESS_PHILOSOPHY,
|
|
129
|
+
howToCompressRules: HOW_TO_COMPRESS_RULES,
|
|
130
|
+
tier2DistillRules: TIER2_DISTILL_RULES,
|
|
131
|
+
tier3CondenseRules: TIER3_CONDENSE_RULES
|
|
132
|
+
});
|
|
133
|
+
function efficiencyNote(prompts) {
|
|
134
|
+
return `This is an efficiency nudge to compress early and keep context lean \u2014 not an overflow warning. A separate, stronger alert will appear if the context is actually full.
|
|
135
|
+
|
|
136
|
+
${prompts.compressPhilosophy}`;
|
|
137
|
+
}
|
|
138
|
+
function emergencyHeader(prompts) {
|
|
139
|
+
return `\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.
|
|
140
|
+
|
|
141
|
+
${prompts.compressPhilosophy}`;
|
|
142
|
+
}
|
|
143
|
+
function formatK(n) {
|
|
144
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
145
|
+
return `${n}`;
|
|
146
|
+
}
|
|
147
|
+
function formatBreakdown(bd) {
|
|
148
|
+
if (!bd) return "";
|
|
149
|
+
const parts = [];
|
|
150
|
+
if (bd.system > 0) parts.push(`${formatK(bd.system)} system`);
|
|
151
|
+
if (bd.tool > 0) parts.push(`${formatK(bd.tool)} tool`);
|
|
152
|
+
if (bd.summaries > 0) parts.push(`${formatK(bd.summaries)} summaries`);
|
|
153
|
+
if (bd.code > 0) parts.push(`${formatK(bd.code)} code`);
|
|
154
|
+
if (bd.text > 0) parts.push(`${formatK(bd.text)} text`);
|
|
155
|
+
const growth = bd.growth > 0 ? `
|
|
156
|
+
+${formatK(bd.growth)} since last nudge` : "";
|
|
157
|
+
return `Context breakdown: ${parts.join(" | ")}${growth}`;
|
|
158
|
+
}
|
|
159
|
+
function formatTierTargetBlocks(blocks) {
|
|
160
|
+
if (blocks.length === 0) {
|
|
161
|
+
return "Target blocks: (none \u2014 no tier blocks found)";
|
|
162
|
+
}
|
|
163
|
+
const lines = blocks.map((b) => {
|
|
164
|
+
const summaryTokens = Math.ceil((b.summary ?? "").length / 4);
|
|
165
|
+
const topic = b.topic ? ` "${b.topic}"` : "";
|
|
166
|
+
return ` ${b.blockId} ${b.effectiveMessageIds.length} msgs ${formatK(b.compressedTokens)}\u2192${formatK(summaryTokens)}${topic}`;
|
|
167
|
+
});
|
|
168
|
+
return `Target ${blocks[0].tier === 1 ? "tier-1" : "tier-2"} blocks to distill (${blocks.length}):
|
|
169
|
+
${lines.join("\n")}`;
|
|
170
|
+
}
|
|
171
|
+
function formatRanges(compressible, protectedRanges) {
|
|
172
|
+
if (compressible.length === 0 && protectedRanges.length === 0) {
|
|
173
|
+
return "[No specific ranges detected \u2014 compress any consumed content.]";
|
|
174
|
+
}
|
|
175
|
+
const refNum = (ref) => {
|
|
176
|
+
const m = ref.match(/\d+/);
|
|
177
|
+
return m ? parseInt(m[0], 10) : 0;
|
|
178
|
+
};
|
|
179
|
+
const entries = [];
|
|
180
|
+
for (const r of compressible) {
|
|
181
|
+
entries.push({
|
|
182
|
+
startRef: r.startRef,
|
|
183
|
+
endRef: r.endRef,
|
|
184
|
+
startNum: refNum(r.startRef),
|
|
185
|
+
endNum: refNum(r.endRef),
|
|
186
|
+
count: r.count,
|
|
187
|
+
tokens: r.tokens,
|
|
188
|
+
toolPct: r.toolPct,
|
|
189
|
+
textPct: r.textPct,
|
|
190
|
+
compressibleTokens: r.tokens,
|
|
191
|
+
compressibleCount: r.count,
|
|
192
|
+
protectedTokens: 0,
|
|
193
|
+
protectedCount: 0,
|
|
194
|
+
protectedTools: [],
|
|
195
|
+
dangerous: r.dangerous ?? false
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
for (const r of protectedRanges) {
|
|
199
|
+
entries.push({
|
|
200
|
+
startRef: r.startRef,
|
|
201
|
+
endRef: r.endRef,
|
|
202
|
+
startNum: refNum(r.startRef),
|
|
203
|
+
endNum: refNum(r.endRef),
|
|
204
|
+
count: r.count,
|
|
205
|
+
tokens: r.tokens,
|
|
206
|
+
toolPct: 0,
|
|
207
|
+
textPct: 0,
|
|
208
|
+
compressibleTokens: 0,
|
|
209
|
+
compressibleCount: 0,
|
|
210
|
+
protectedTokens: r.tokens,
|
|
211
|
+
protectedCount: r.count,
|
|
212
|
+
protectedTools: [...r.tools],
|
|
213
|
+
dangerous: false
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
entries.sort((a, b) => a.startNum - b.startNum);
|
|
217
|
+
const merged = [];
|
|
218
|
+
for (const e of entries) {
|
|
219
|
+
const last = merged[merged.length - 1];
|
|
220
|
+
if (last && e.startNum <= last.endNum + 1) {
|
|
221
|
+
last.endRef = e.endRef;
|
|
222
|
+
last.endNum = Math.max(last.endNum, e.endNum);
|
|
223
|
+
last.count += e.count;
|
|
224
|
+
last.tokens += e.tokens;
|
|
225
|
+
last.compressibleTokens += e.compressibleTokens;
|
|
226
|
+
last.compressibleCount += e.compressibleCount;
|
|
227
|
+
last.protectedTokens += e.protectedTokens;
|
|
228
|
+
last.protectedCount += e.protectedCount;
|
|
229
|
+
if (e.dangerous) last.dangerous = true;
|
|
230
|
+
for (const t of e.protectedTools) {
|
|
231
|
+
if (!last.protectedTools.includes(t)) last.protectedTools.push(t);
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
merged.push({ ...e });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const lines = merged.map((e) => {
|
|
238
|
+
const suffix = e.dangerous && e.compressibleTokens > 0 ? " \u26A0\uFE0F NOT recommended unless you are certain." : "";
|
|
239
|
+
if (e.protectedTokens > 0 && e.compressibleTokens === 0) {
|
|
240
|
+
return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${formatK(e.tokens)} [PROTECTED: ${e.protectedTools.join(", ")} \u2014 not compressible]${suffix}`;
|
|
241
|
+
}
|
|
242
|
+
if (e.protectedTokens > 0 && e.compressibleTokens > 0) {
|
|
243
|
+
return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${formatK(e.tokens)} [${formatK(e.compressibleTokens)} compressible | ${formatK(e.protectedTokens)} protected: ${e.protectedTools.join(", ")}]${suffix}`;
|
|
244
|
+
}
|
|
245
|
+
return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${formatK(e.tokens)} [tool ${e.toolPct}% | text ${e.textPct}%]${suffix}`;
|
|
246
|
+
});
|
|
247
|
+
return `Compressible ranges (${merged.length}, oldest first):
|
|
248
|
+
${lines.join("\n")}`;
|
|
249
|
+
}
|
|
250
|
+
function renderNudgeText(decision, prompts = defaultPrompts) {
|
|
251
|
+
const breakdownStr = formatBreakdown(decision.contextBreakdown);
|
|
252
|
+
const rangesStr = formatRanges(decision.compressibleRanges, decision.protectedRanges ?? []);
|
|
253
|
+
const isEmergency = !!decision.breakdown?.emergencyOverride || !!decision.breakdown?.overLimit;
|
|
254
|
+
if (decision.tier !== null && decision.tier >= 2) {
|
|
255
|
+
const isT2 = decision.tier === 2;
|
|
256
|
+
const targets = decision.tierTargetBlocks ?? [];
|
|
257
|
+
const blockList = formatTierTargetBlocks(targets);
|
|
258
|
+
const startId = targets[0]?.blockId ?? "b1";
|
|
259
|
+
const endId = targets[targets.length - 1]?.blockId ?? "b5";
|
|
260
|
+
const voice = isEmergency ? "emergency" : "gentle";
|
|
261
|
+
const triggerLine = isEmergency ? `[EMERGENCY \u2014 TIER ${decision.tier} ${isT2 ? "DISTILLATION" : "CONDENSATION"}] Context limit reached \u2014 distill NOW into a denser summary to reclaim tokens.` : `[TIER ${decision.tier} ${isT2 ? "DISTILLATION" : "CONDENSATION"} TRIGGER]`;
|
|
262
|
+
return {
|
|
263
|
+
voice,
|
|
264
|
+
text: [
|
|
265
|
+
efficiencyNote(prompts),
|
|
266
|
+
"",
|
|
267
|
+
breakdownStr,
|
|
268
|
+
"",
|
|
269
|
+
triggerLine,
|
|
270
|
+
isT2 ? `Your tier-1 compression summaries have accumulated. Distill them into a single denser tier-2 summary. Use block IDs as boundaries (startId and endId as bN). Any raw (uncompressed) messages sitting between the boundary blocks are absorbed into the tier-2 block as well \u2014 apply HOW TO COMPRESS to those raw messages and the TIER 2 distillation rules to the existing summaries, so the whole span is covered and nothing is lost.` : `Your tier-2 compression summaries have accumulated. Condense them further into a tier-3 ultra-condensed summary. Use block IDs as boundaries (startId and endId as bN). Any raw (uncompressed) messages sitting between the boundary blocks are absorbed into the tier-3 block as well \u2014 apply HOW TO COMPRESS to those raw messages and the TIER 3 condensation rules to the existing summaries, so the whole span is covered and nothing is lost.`,
|
|
271
|
+
blockList,
|
|
272
|
+
`Example: compress({ content: [{ startId: "${startId}", endId: "${endId}", summary: "..." }] })`,
|
|
273
|
+
"",
|
|
274
|
+
prompts.howToCompressRules,
|
|
275
|
+
"",
|
|
276
|
+
isT2 ? prompts.tier2DistillRules : prompts.tier3CondenseRules
|
|
277
|
+
].join("\n")
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
if (isEmergency) {
|
|
281
|
+
return {
|
|
282
|
+
voice: "emergency",
|
|
283
|
+
text: [
|
|
284
|
+
emergencyHeader(prompts),
|
|
285
|
+
"",
|
|
286
|
+
breakdownStr,
|
|
287
|
+
"",
|
|
288
|
+
prompts.howToCompressRules,
|
|
289
|
+
"",
|
|
290
|
+
`{ "topic": "...", "content": [{ "startId": "<ID>", "endId": "<ID>", "summary": "..." }] }`,
|
|
291
|
+
"Only use IDs from visible messages above. Compress older work first.",
|
|
292
|
+
"",
|
|
293
|
+
rangesStr
|
|
294
|
+
].join("\n")
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
voice: "gentle",
|
|
299
|
+
text: [
|
|
300
|
+
efficiencyNote(prompts),
|
|
301
|
+
"",
|
|
302
|
+
breakdownStr,
|
|
303
|
+
"",
|
|
304
|
+
prompts.howToCompressRules,
|
|
305
|
+
"",
|
|
306
|
+
rangesStr,
|
|
307
|
+
"",
|
|
308
|
+
`\u{1F4A1} Compress all ranges in one call (pass multiple content entries: \`content: [{...}, {...}]\`).`
|
|
309
|
+
].join("\n")
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// node_modules/acp-kernel/dist/chunk-UX4LINT7.js
|
|
314
|
+
function createInitialState() {
|
|
315
|
+
return {
|
|
316
|
+
blocks: [],
|
|
317
|
+
messageRefs: { byRaw: {}, byRef: {} },
|
|
318
|
+
tokenSnapshot: {},
|
|
319
|
+
nudge: {
|
|
320
|
+
lastPerMessageNudgeTokens: 0,
|
|
321
|
+
lastNudgeShownTokens: 0,
|
|
322
|
+
baselineTokens: 0,
|
|
323
|
+
anchors: {},
|
|
324
|
+
lastShownByTier: {}
|
|
325
|
+
},
|
|
326
|
+
stats: { tokensCompressed: 0, compressionCount: 0, absorbedTokens: 0 },
|
|
327
|
+
absorbed: [],
|
|
328
|
+
nextBlockId: 1,
|
|
329
|
+
nextRunId: 1
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function allocateBlockId(state) {
|
|
333
|
+
const id = state.nextBlockId;
|
|
334
|
+
state.nextBlockId = Math.max(1, id) + 1;
|
|
335
|
+
return `b${id}`;
|
|
336
|
+
}
|
|
337
|
+
function allocateRunId(state) {
|
|
338
|
+
const id = state.nextRunId;
|
|
339
|
+
state.nextRunId = Math.max(1, id) + 1;
|
|
340
|
+
return `r${id}`;
|
|
341
|
+
}
|
|
342
|
+
function blockById(state, blockId) {
|
|
343
|
+
return state.blocks.find((block) => block.blockId === blockId);
|
|
344
|
+
}
|
|
345
|
+
function activeBlocks(state) {
|
|
346
|
+
return state.blocks.filter((block) => block.active);
|
|
347
|
+
}
|
|
348
|
+
function coveredMessageIds(state) {
|
|
349
|
+
const covered = /* @__PURE__ */ new Set();
|
|
350
|
+
for (const block of state.blocks) {
|
|
351
|
+
if (!block.active) continue;
|
|
352
|
+
for (const id of block.effectiveMessageIds) covered.add(id);
|
|
353
|
+
}
|
|
354
|
+
return covered;
|
|
355
|
+
}
|
|
356
|
+
function advanceSurvival(state, promotionThreshold) {
|
|
357
|
+
for (const block of state.blocks) {
|
|
358
|
+
if (!block.active) continue;
|
|
359
|
+
block.survivedCount += 1;
|
|
360
|
+
if (block.survivedCount >= promotionThreshold) {
|
|
361
|
+
block.generation = "old";
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// node_modules/acp-kernel/dist/index.js
|
|
9
367
|
var REF_WIDTH = 5;
|
|
10
368
|
var MIN_INDEX = 1;
|
|
11
369
|
var MAX_INDEX = 99999;
|
|
@@ -72,57 +430,17 @@ function highestUsedIndex(map) {
|
|
|
72
430
|
}
|
|
73
431
|
return highest;
|
|
74
432
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
tokenSnapshot: {},
|
|
80
|
-
nudge: {
|
|
81
|
-
lastPerMessageNudgeTokens: 0,
|
|
82
|
-
lastNudgeShownTokens: 0,
|
|
83
|
-
baselineTokens: 0,
|
|
84
|
-
anchors: {},
|
|
85
|
-
lastShownByTier: {}
|
|
86
|
-
},
|
|
87
|
-
stats: { tokensCompressed: 0, compressionCount: 0 },
|
|
88
|
-
nextBlockId: 1,
|
|
89
|
-
nextRunId: 1
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
function allocateBlockId(state) {
|
|
93
|
-
const id = state.nextBlockId;
|
|
94
|
-
state.nextBlockId = Math.max(1, id) + 1;
|
|
95
|
-
return `b${id}`;
|
|
96
|
-
}
|
|
97
|
-
function allocateRunId(state) {
|
|
98
|
-
const id = state.nextRunId;
|
|
99
|
-
state.nextRunId = Math.max(1, id) + 1;
|
|
100
|
-
return `r${id}`;
|
|
101
|
-
}
|
|
102
|
-
function blockById(state, blockId) {
|
|
103
|
-
return state.blocks.find((block) => block.blockId === blockId);
|
|
104
|
-
}
|
|
105
|
-
function activeBlocks(state) {
|
|
106
|
-
return state.blocks.filter((block) => block.active);
|
|
433
|
+
var SUMMARY_HEADER = "[Compressed conversation section]";
|
|
434
|
+
var SUMMARY_ID_PREFIX = "acp_summary_";
|
|
435
|
+
function summaryMessageId(blockId) {
|
|
436
|
+
return `${SUMMARY_ID_PREFIX}${blockId}`;
|
|
107
437
|
}
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
for (const block of state.blocks) {
|
|
111
|
-
if (!block.active) continue;
|
|
112
|
-
for (const id of block.effectiveMessageIds) covered.add(id);
|
|
113
|
-
}
|
|
114
|
-
return covered;
|
|
438
|
+
function isSummaryMessageId(id) {
|
|
439
|
+
return id.startsWith(SUMMARY_ID_PREFIX);
|
|
115
440
|
}
|
|
116
|
-
function
|
|
117
|
-
|
|
118
|
-
if (!block.active) continue;
|
|
119
|
-
block.survivedCount += 1;
|
|
120
|
-
if (block.survivedCount >= promotionThreshold) {
|
|
121
|
-
block.generation = "old";
|
|
122
|
-
}
|
|
123
|
-
}
|
|
441
|
+
function isRenderedSummaryMessage(message) {
|
|
442
|
+
return isSummaryMessageId(message.id) && message.role === "system" && message.contentType === "text";
|
|
124
443
|
}
|
|
125
|
-
var SUMMARY_HEADER = "[Compressed conversation section]";
|
|
126
444
|
function prune(messages, state, options = {}) {
|
|
127
445
|
const covered = coveredMessageIds(state);
|
|
128
446
|
if (covered.size === 0) return [...messages];
|
|
@@ -131,8 +449,13 @@ function prune(messages, state, options = {}) {
|
|
|
131
449
|
(message) => message.role === "user"
|
|
132
450
|
);
|
|
133
451
|
const indexById = /* @__PURE__ */ new Map();
|
|
134
|
-
|
|
135
|
-
|
|
452
|
+
const summaryIndexById = /* @__PURE__ */ new Map();
|
|
453
|
+
messages.forEach((message, index) => {
|
|
454
|
+
indexById.set(message.id, index);
|
|
455
|
+
if (isRenderedSummaryMessage(message))
|
|
456
|
+
summaryIndexById.set(message.id, index);
|
|
457
|
+
});
|
|
458
|
+
const anchors = inject ? collectSummaryAnchors(state, indexById, summaryIndexById) : [];
|
|
136
459
|
return stripOrphanedReasoning(
|
|
137
460
|
stripOrphanedToolResults(
|
|
138
461
|
stripOrphanedToolCalls(
|
|
@@ -141,9 +464,19 @@ function prune(messages, state, options = {}) {
|
|
|
141
464
|
)
|
|
142
465
|
);
|
|
143
466
|
}
|
|
144
|
-
function collectSummaryAnchors(state, indexById) {
|
|
467
|
+
function collectSummaryAnchors(state, indexById, summaryIndexById) {
|
|
145
468
|
const anchors = [];
|
|
146
469
|
for (const block of activeBlocks(state)) {
|
|
470
|
+
const existingIndex = summaryIndexById.get(summaryMessageId(block.blockId));
|
|
471
|
+
if (existingIndex !== void 0) {
|
|
472
|
+
anchors.push({
|
|
473
|
+
blockId: block.blockId,
|
|
474
|
+
summary: block.summary,
|
|
475
|
+
topic: block.topic,
|
|
476
|
+
insertAt: existingIndex
|
|
477
|
+
});
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
147
480
|
let earliest = null;
|
|
148
481
|
for (const id of block.effectiveMessageIds) {
|
|
149
482
|
const index = indexById.get(id);
|
|
@@ -164,6 +497,9 @@ function collectSummaryAnchors(state, indexById) {
|
|
|
164
497
|
function rebuildMessages(messages, covered, firstUserIndex, anchors) {
|
|
165
498
|
const result = [];
|
|
166
499
|
const pending = [...anchors];
|
|
500
|
+
const anchoredSummaryIds = new Set(
|
|
501
|
+
anchors.map((anchor) => summaryMessageId(anchor.blockId))
|
|
502
|
+
);
|
|
167
503
|
for (let index = 0; index < messages.length; index++) {
|
|
168
504
|
while (pending.length > 0 && pending[0].insertAt === index) {
|
|
169
505
|
result.push(renderSummary(pending.shift()));
|
|
@@ -173,6 +509,8 @@ function rebuildMessages(messages, covered, firstUserIndex, anchors) {
|
|
|
173
509
|
continue;
|
|
174
510
|
}
|
|
175
511
|
if (covered.has(messages[index].id)) continue;
|
|
512
|
+
if (isRenderedSummaryMessage(messages[index]) && anchoredSummaryIds.has(messages[index].id))
|
|
513
|
+
continue;
|
|
176
514
|
result.push(messages[index]);
|
|
177
515
|
}
|
|
178
516
|
while (pending.length > 0) {
|
|
@@ -186,7 +524,7 @@ function renderSummary(anchor) {
|
|
|
186
524
|
const text = body.length === 0 ? topicLine : `${topicLine}
|
|
187
525
|
${body}`;
|
|
188
526
|
return {
|
|
189
|
-
id:
|
|
527
|
+
id: summaryMessageId(anchor.blockId),
|
|
190
528
|
role: "system",
|
|
191
529
|
contentType: "text",
|
|
192
530
|
text
|
|
@@ -250,6 +588,7 @@ function syncBlocks(messages, state) {
|
|
|
250
588
|
tokenSnapshot: { ...state.tokenSnapshot ?? {} },
|
|
251
589
|
nudge: { ...state.nudge, anchors: { ...state.nudge.anchors } },
|
|
252
590
|
stats: { ...state.stats },
|
|
591
|
+
absorbed: (state.absorbed ?? []).map((record) => ({ ...record })),
|
|
253
592
|
nextBlockId: state.nextBlockId,
|
|
254
593
|
nextRunId: state.nextRunId
|
|
255
594
|
};
|
|
@@ -274,10 +613,12 @@ function syncBlocks(messages, state) {
|
|
|
274
613
|
block.active = false;
|
|
275
614
|
continue;
|
|
276
615
|
}
|
|
616
|
+
if (block.expanded) {
|
|
617
|
+
block.active = false;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
277
620
|
block.active = true;
|
|
278
|
-
const stillPresent = block.effectiveMessageIds.some(
|
|
279
|
-
(id) => presentIds.has(id)
|
|
280
|
-
);
|
|
621
|
+
const stillPresent = block.effectiveMessageIds.some((id) => presentIds.has(id)) || presentIds.has(summaryMessageId(block.blockId));
|
|
281
622
|
if (!stillPresent) {
|
|
282
623
|
block.active = false;
|
|
283
624
|
deactivated.push(block.blockId);
|
|
@@ -285,13 +626,6 @@ function syncBlocks(messages, state) {
|
|
|
285
626
|
}
|
|
286
627
|
return { state: result, deactivated };
|
|
287
628
|
}
|
|
288
|
-
var require2 = createRequire(import.meta.url);
|
|
289
|
-
function defaultCountTokens(text) {
|
|
290
|
-
if (!text) return 0;
|
|
291
|
-
const cjk = text.match(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/g);
|
|
292
|
-
const cjkCount = cjk?.length ?? 0;
|
|
293
|
-
return cjkCount + Math.ceil((text.length - cjkCount) / 4);
|
|
294
|
-
}
|
|
295
629
|
function defaultConfig(modelContextLimit, overrides = {}) {
|
|
296
630
|
const base = {
|
|
297
631
|
tiers: { enabled: true, tier2Trigger: 5, tier3Trigger: 10 },
|
|
@@ -319,7 +653,14 @@ function defaultConfig(modelContextLimit, overrides = {}) {
|
|
|
319
653
|
protectedTools: [],
|
|
320
654
|
preserveRecentMessages: 5,
|
|
321
655
|
preserveRecentTokens: 5e3,
|
|
322
|
-
modelContextLimit
|
|
656
|
+
modelContextLimit,
|
|
657
|
+
absorb: {
|
|
658
|
+
enabled: false,
|
|
659
|
+
toolName: "absorb",
|
|
660
|
+
minToolTokens: 1e3,
|
|
661
|
+
contextThresholdPct: 0,
|
|
662
|
+
excludeTools: []
|
|
663
|
+
}
|
|
323
664
|
};
|
|
324
665
|
return {
|
|
325
666
|
...base,
|
|
@@ -327,7 +668,8 @@ function defaultConfig(modelContextLimit, overrides = {}) {
|
|
|
327
668
|
tiers: { ...base.tiers, ...overrides.tiers },
|
|
328
669
|
nudge: { ...base.nudge, ...overrides.nudge },
|
|
329
670
|
truncate: { ...base.truncate, ...overrides.truncate },
|
|
330
|
-
compress: { ...base.compress, ...overrides.compress }
|
|
671
|
+
compress: { ...base.compress, ...overrides.compress },
|
|
672
|
+
absorb: overrides.absorb ? { ...base.absorb, ...overrides.absorb } : base.absorb
|
|
331
673
|
};
|
|
332
674
|
}
|
|
333
675
|
function validateConfig(config) {
|
|
@@ -345,6 +687,9 @@ function validateConfig(config) {
|
|
|
345
687
|
"nudge.maxContextLimitPct must not exceed nudge.emergencyThresholdPct"
|
|
346
688
|
);
|
|
347
689
|
}
|
|
690
|
+
if (config.nudge.minPressureBenefitTokens !== void 0 && (!Number.isFinite(config.nudge.minPressureBenefitTokens) || config.nudge.minPressureBenefitTokens < 0)) {
|
|
691
|
+
errors.push("nudge.minPressureBenefitTokens must be finite and >= 0");
|
|
692
|
+
}
|
|
348
693
|
if (config.promotionThreshold < 1) {
|
|
349
694
|
errors.push("promotionThreshold must be >= 1");
|
|
350
695
|
}
|
|
@@ -357,6 +702,17 @@ function validateConfig(config) {
|
|
|
357
702
|
if (config.tiers.tier3Trigger <= config.tiers.tier2Trigger) {
|
|
358
703
|
errors.push("tiers.tier3Trigger must be greater than tiers.tier2Trigger");
|
|
359
704
|
}
|
|
705
|
+
if (config.absorb) {
|
|
706
|
+
if (config.absorb.enabled && !config.absorb.toolName) {
|
|
707
|
+
errors.push("absorb.toolName must be a non-empty string when enabled");
|
|
708
|
+
}
|
|
709
|
+
if (!Number.isFinite(config.absorb.minToolTokens) || config.absorb.minToolTokens < 0) {
|
|
710
|
+
errors.push("absorb.minToolTokens must be >= 0");
|
|
711
|
+
}
|
|
712
|
+
if (config.absorb.contextThresholdPct < 0 || config.absorb.contextThresholdPct > 1) {
|
|
713
|
+
errors.push("absorb.contextThresholdPct must be in [0, 1]");
|
|
714
|
+
}
|
|
715
|
+
}
|
|
360
716
|
return errors;
|
|
361
717
|
}
|
|
362
718
|
var MESSAGE_REF_PATTERN = /^m0*(\d{1,5})$/;
|
|
@@ -397,26 +753,41 @@ function resolveBoundaries(input) {
|
|
|
397
753
|
`Invalid boundary ref(s): startId="${input.startRef}", endId="${input.endRef}". Use mNNNNN or bN.`
|
|
398
754
|
);
|
|
399
755
|
}
|
|
400
|
-
const
|
|
756
|
+
const indexByMessageId = /* @__PURE__ */ new Map();
|
|
401
757
|
input.messages.forEach(
|
|
402
|
-
(message, index) =>
|
|
758
|
+
(message, index) => indexByMessageId.set(message.id, index)
|
|
759
|
+
);
|
|
760
|
+
let snappedBoundaries = [];
|
|
761
|
+
const startAnchor = resolveAnchorIndex(
|
|
762
|
+
start,
|
|
763
|
+
input.state,
|
|
764
|
+
indexByMessageId,
|
|
765
|
+
"start"
|
|
766
|
+
);
|
|
767
|
+
if (startAnchor.snapped) snappedBoundaries.push(startAnchor.snapped);
|
|
768
|
+
const endAnchor = resolveAnchorIndex(
|
|
769
|
+
end,
|
|
770
|
+
input.state,
|
|
771
|
+
indexByMessageId,
|
|
772
|
+
"end"
|
|
403
773
|
);
|
|
404
|
-
|
|
405
|
-
let
|
|
774
|
+
if (endAnchor.snapped) snappedBoundaries.push(endAnchor.snapped);
|
|
775
|
+
let startIndex = startAnchor.index;
|
|
776
|
+
let endIndex = endAnchor.index;
|
|
406
777
|
if (startIndex > endIndex) {
|
|
407
778
|
[startIndex, endIndex] = [endIndex, startIndex];
|
|
408
779
|
}
|
|
409
780
|
const messageIds = [];
|
|
410
781
|
for (let index = startIndex; index <= endIndex; index++) {
|
|
411
782
|
const message = input.messages[index];
|
|
412
|
-
if (message
|
|
783
|
+
if (message && !isRenderedSummaryMessage(message))
|
|
784
|
+
messageIds.push(message.id);
|
|
413
785
|
}
|
|
414
786
|
const boundaryKind = start.kind === "block" || end.kind === "block" ? "block" : "message";
|
|
415
787
|
const nestedBlockIds = [];
|
|
416
788
|
const nestedSeen = /* @__PURE__ */ new Set();
|
|
417
789
|
for (const block of activeBlocks(input.state)) {
|
|
418
|
-
|
|
419
|
-
if (anchor !== null && anchor >= startIndex && anchor <= endIndex) {
|
|
790
|
+
if (blockVisibleInRange(block, indexByMessageId, startIndex, endIndex)) {
|
|
420
791
|
if (!nestedSeen.has(block.blockId)) {
|
|
421
792
|
nestedSeen.add(block.blockId);
|
|
422
793
|
nestedBlockIds.push(block.blockId);
|
|
@@ -430,10 +801,11 @@ function resolveBoundaries(input) {
|
|
|
430
801
|
messageIds,
|
|
431
802
|
nestedBlockIds,
|
|
432
803
|
boundaryKind,
|
|
433
|
-
protectedGaps
|
|
804
|
+
protectedGaps,
|
|
805
|
+
snappedBoundaries
|
|
434
806
|
};
|
|
435
807
|
}
|
|
436
|
-
function resolveAnchorIndex(boundary, state,
|
|
808
|
+
function resolveAnchorIndex(boundary, state, indexByMessageId, endpoint) {
|
|
437
809
|
const label = endpoint === "start" ? "startId" : "endId";
|
|
438
810
|
if (boundary.kind === "message") {
|
|
439
811
|
const rawId = state.messageRefs.byRef[boundary.raw] ?? state.messageRefs.byRef[formatPaddedRef(boundary.numericId)];
|
|
@@ -444,15 +816,22 @@ function resolveAnchorIndex(boundary, state, indexByRawId, endpoint) {
|
|
|
444
816
|
`${label}="${boundary.raw}" does not exist in this session (typo or wrong session) \u2014 run acp_status for current refs.`
|
|
445
817
|
);
|
|
446
818
|
}
|
|
447
|
-
const index =
|
|
448
|
-
if (index
|
|
449
|
-
|
|
450
|
-
"consumed",
|
|
451
|
-
endpoint,
|
|
452
|
-
`${label}="${boundary.raw}" not found in visible context (likely consumed by an existing block).`
|
|
453
|
-
);
|
|
819
|
+
const index = indexByMessageId.get(rawId);
|
|
820
|
+
if (index !== void 0) {
|
|
821
|
+
return { index, snapped: null };
|
|
454
822
|
}
|
|
455
|
-
|
|
823
|
+
const owner2 = activeOwnerAnchor(state, [rawId], indexByMessageId);
|
|
824
|
+
if (owner2 !== null) {
|
|
825
|
+
return {
|
|
826
|
+
index: owner2,
|
|
827
|
+
snapped: `${label}="${boundary.raw}" refers to a message already compressed into an active block \u2014 anchored to the active block covering it instead.`
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
throw new BoundaryNotFoundError(
|
|
831
|
+
"consumed",
|
|
832
|
+
endpoint,
|
|
833
|
+
`${label}="${boundary.raw}" not found in visible context (likely consumed by an existing block).`
|
|
834
|
+
);
|
|
456
835
|
}
|
|
457
836
|
const block = blockById(state, `b${boundary.numericId}`);
|
|
458
837
|
if (!block) {
|
|
@@ -462,6 +841,23 @@ function resolveAnchorIndex(boundary, state, indexByRawId, endpoint) {
|
|
|
462
841
|
`${label}="b${boundary.numericId}" does not exist in this session (typo or wrong session) \u2014 run acp_status for current refs.`
|
|
463
842
|
);
|
|
464
843
|
}
|
|
844
|
+
if (block.active) {
|
|
845
|
+
const anchor = visibleBlockAnchor(block, indexByMessageId);
|
|
846
|
+
if (anchor !== null) {
|
|
847
|
+
return { index: anchor, snapped: null };
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
const owner = activeOwnerAnchor(
|
|
851
|
+
state,
|
|
852
|
+
block.effectiveMessageIds,
|
|
853
|
+
indexByMessageId
|
|
854
|
+
);
|
|
855
|
+
if (owner !== null) {
|
|
856
|
+
return {
|
|
857
|
+
index: owner,
|
|
858
|
+
snapped: `${label}="b${boundary.numericId}" was consumed by a higher-tier block \u2014 anchored to the active block covering its content instead.`
|
|
859
|
+
};
|
|
860
|
+
}
|
|
465
861
|
if (!block.active) {
|
|
466
862
|
throw new BoundaryNotFoundError(
|
|
467
863
|
"consumed",
|
|
@@ -469,23 +865,67 @@ function resolveAnchorIndex(boundary, state, indexByRawId, endpoint) {
|
|
|
469
865
|
`${label}="b${boundary.numericId}" not found in visible context (block distilled/consumed by a higher-tier block).`
|
|
470
866
|
);
|
|
471
867
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
868
|
+
throw new BoundaryNotFoundError(
|
|
869
|
+
"consumed",
|
|
870
|
+
endpoint,
|
|
871
|
+
`${label}="b${boundary.numericId}" is an active block but none of its content (raw messages or rendered summary) is visible in the current context \u2014 run acp_status to verify.`
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
function activeOwnerAnchor(state, ownedIds, indexByMessageId) {
|
|
875
|
+
if (ownedIds.length === 0) return null;
|
|
876
|
+
const owned = new Set(ownedIds);
|
|
877
|
+
let best = null;
|
|
878
|
+
for (const block of state.blocks) {
|
|
879
|
+
if (!block.active) continue;
|
|
880
|
+
const inherited = inheritedContentIds(state, block);
|
|
881
|
+
let ownsInherited = false;
|
|
882
|
+
for (const id of owned) {
|
|
883
|
+
if (inherited.has(id)) {
|
|
884
|
+
ownsInherited = true;
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
if (!ownsInherited) continue;
|
|
889
|
+
const anchor = visibleBlockAnchor(block, indexByMessageId);
|
|
890
|
+
if (anchor === null) continue;
|
|
891
|
+
if (best === null || anchor < best) {
|
|
892
|
+
best = anchor;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return best;
|
|
896
|
+
}
|
|
897
|
+
function inheritedContentIds(state, block) {
|
|
898
|
+
const ids = /* @__PURE__ */ new Set();
|
|
899
|
+
for (const childId of block.directBlockIds) {
|
|
900
|
+
const child = blockById(state, childId);
|
|
901
|
+
if (!child) continue;
|
|
902
|
+
for (const id of child.effectiveMessageIds) ids.add(id);
|
|
479
903
|
}
|
|
480
|
-
return
|
|
904
|
+
return ids;
|
|
481
905
|
}
|
|
482
906
|
function formatPaddedRef(index) {
|
|
483
907
|
return `m${String(index).padStart(5, "0")}`;
|
|
484
908
|
}
|
|
485
|
-
function
|
|
909
|
+
function visibleBlockAnchor(block, indexByMessageId) {
|
|
910
|
+
const summaryIndex = indexByMessageId.get(summaryMessageId(block.blockId));
|
|
911
|
+
if (summaryIndex !== void 0) return summaryIndex;
|
|
912
|
+
return earliestIndexOfIds(block.effectiveMessageIds, indexByMessageId);
|
|
913
|
+
}
|
|
914
|
+
function blockVisibleInRange(block, indexByMessageId, startIndex, endIndex) {
|
|
915
|
+
const summaryIndex = indexByMessageId.get(summaryMessageId(block.blockId));
|
|
916
|
+
if (summaryIndex !== void 0 && summaryIndex >= startIndex && summaryIndex <= endIndex) {
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
const rawIndex = earliestIndexOfIds(
|
|
920
|
+
block.effectiveMessageIds,
|
|
921
|
+
indexByMessageId
|
|
922
|
+
);
|
|
923
|
+
return rawIndex !== null && rawIndex >= startIndex && rawIndex <= endIndex;
|
|
924
|
+
}
|
|
925
|
+
function earliestIndexOfIds(ids, indexByMessageId) {
|
|
486
926
|
let earliest = null;
|
|
487
927
|
for (const id of ids) {
|
|
488
|
-
const index =
|
|
928
|
+
const index = indexByMessageId.get(id);
|
|
489
929
|
if (index !== void 0 && (earliest === null || index < earliest)) {
|
|
490
930
|
earliest = index;
|
|
491
931
|
}
|
|
@@ -543,29 +983,75 @@ function truncateLargeToolOutputs(messages, tokenCount, config, countTokens, opt
|
|
|
543
983
|
);
|
|
544
984
|
return { messages: updated, truncatedCount, savedTokens };
|
|
545
985
|
}
|
|
546
|
-
var KEEP_LAST_ORPHANED =
|
|
986
|
+
var KEEP_LAST_ORPHANED = 2;
|
|
547
987
|
function rangeKey(startRef, endRef) {
|
|
548
988
|
return `${startRef}::${endRef}`;
|
|
549
989
|
}
|
|
550
|
-
function
|
|
990
|
+
function parseCallText(text) {
|
|
991
|
+
const raw = text ?? "";
|
|
992
|
+
const start = raw.indexOf("{");
|
|
993
|
+
if (start < 0) return null;
|
|
551
994
|
let parsed;
|
|
552
995
|
try {
|
|
553
|
-
parsed = JSON.parse(
|
|
996
|
+
parsed = JSON.parse(raw.slice(start));
|
|
554
997
|
} catch {
|
|
555
998
|
return null;
|
|
556
999
|
}
|
|
557
1000
|
if (!parsed || typeof parsed !== "object") return null;
|
|
558
1001
|
const obj = parsed;
|
|
559
|
-
|
|
560
|
-
|
|
1002
|
+
let content = null;
|
|
1003
|
+
let contentWasString = false;
|
|
1004
|
+
if (Array.isArray(obj.content)) {
|
|
1005
|
+
content = obj.content;
|
|
1006
|
+
} else if (typeof obj.content === "string") {
|
|
1007
|
+
contentWasString = true;
|
|
1008
|
+
try {
|
|
1009
|
+
const inner = JSON.parse(obj.content);
|
|
1010
|
+
if (Array.isArray(inner)) content = inner;
|
|
1011
|
+
} catch {
|
|
1012
|
+
content = null;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
if (!content || content.length === 0) return null;
|
|
1016
|
+
return { prefix: raw.slice(0, start), obj, content, contentWasString };
|
|
1017
|
+
}
|
|
1018
|
+
function rewriteCompressText(text, liveKeys) {
|
|
1019
|
+
const parsed = parseCallText(text);
|
|
1020
|
+
if (!parsed) return null;
|
|
1021
|
+
const { prefix, obj, content, contentWasString } = parsed;
|
|
561
1022
|
const kept = content.filter((entry) => {
|
|
562
1023
|
if (!entry || typeof entry !== "object") return false;
|
|
563
|
-
const
|
|
564
|
-
const
|
|
565
|
-
|
|
1024
|
+
const e = entry;
|
|
1025
|
+
const s = typeof e.startId === "string" ? e.startId : typeof e.messageId === "string" ? e.messageId : "";
|
|
1026
|
+
const end = typeof e.endId === "string" ? e.endId : typeof e.messageId === "string" ? e.messageId : "";
|
|
1027
|
+
return liveKeys.has(rangeKey(s, end));
|
|
1028
|
+
});
|
|
1029
|
+
if (kept.length === 0) return null;
|
|
1030
|
+
return prefix + serializeCompacted(obj, kept, contentWasString).text;
|
|
1031
|
+
}
|
|
1032
|
+
var SUMMARY_STUB_CHARS = 200;
|
|
1033
|
+
function compactEntry(entry) {
|
|
1034
|
+
if (!entry || typeof entry !== "object") return entry;
|
|
1035
|
+
const e = entry;
|
|
1036
|
+
if (typeof e.summary !== "string" || e.summary.length <= SUMMARY_STUB_CHARS) return entry;
|
|
1037
|
+
return { ...e, summary: `${e.summary.slice(0, SUMMARY_STUB_CHARS - 1)}\u2026` };
|
|
1038
|
+
}
|
|
1039
|
+
function serializeCompacted(obj, content, contentWasString) {
|
|
1040
|
+
let changed = false;
|
|
1041
|
+
const compacted = content.map((entry) => {
|
|
1042
|
+
const out = compactEntry(entry);
|
|
1043
|
+
if (out !== entry) changed = true;
|
|
1044
|
+
return out;
|
|
566
1045
|
});
|
|
567
|
-
|
|
568
|
-
return JSON.stringify({ ...obj, content:
|
|
1046
|
+
const outContent = contentWasString ? JSON.stringify(compacted) : compacted;
|
|
1047
|
+
return { text: JSON.stringify({ ...obj, content: outContent }), changed };
|
|
1048
|
+
}
|
|
1049
|
+
function compactCompressText(text) {
|
|
1050
|
+
const parsed = parseCallText(text);
|
|
1051
|
+
if (!parsed) return null;
|
|
1052
|
+
const { prefix, obj, content, contentWasString } = parsed;
|
|
1053
|
+
const { text: out, changed } = serializeCompacted(obj, content, contentWasString);
|
|
1054
|
+
return changed ? prefix + out : null;
|
|
569
1055
|
}
|
|
570
1056
|
function hideConsumedCompressCalls(state, messages) {
|
|
571
1057
|
const allBlockCallIds = /* @__PURE__ */ new Set();
|
|
@@ -624,11 +1110,316 @@ function hideConsumedCompressCalls(state, messages) {
|
|
|
624
1110
|
continue;
|
|
625
1111
|
}
|
|
626
1112
|
}
|
|
1113
|
+
const compacted = compactCompressText(message.text);
|
|
1114
|
+
if (compacted !== null) {
|
|
1115
|
+
result.push({ ...message, text: compacted });
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
627
1118
|
}
|
|
628
1119
|
result.push(message);
|
|
629
1120
|
}
|
|
630
1121
|
return { messages: result, hidden };
|
|
631
1122
|
}
|
|
1123
|
+
var COMPRESS_TOOL_NAME = "compress";
|
|
1124
|
+
var DECOMPRESS_TOOL_NAME = "decompress";
|
|
1125
|
+
var SEARCH_CONTEXT_TOOL_NAME = "search_context";
|
|
1126
|
+
var ACP_STATUS_TOOL_NAME = "acp_status";
|
|
1127
|
+
var ABSORB_TOOL_NAME = "absorb";
|
|
1128
|
+
var COMPRESS_TOOL = {
|
|
1129
|
+
name: COMPRESS_TOOL_NAME,
|
|
1130
|
+
description: "Replace a contiguous range of older conversation with a detailed summary you write. Use when content is genuinely consumed. Batch form: content=[{startId,endId,summary,topic?}]. REQUIRED \u2014 compress without content is invalid.",
|
|
1131
|
+
input_schema: {
|
|
1132
|
+
type: "object",
|
|
1133
|
+
properties: {
|
|
1134
|
+
topic: {
|
|
1135
|
+
type: "string",
|
|
1136
|
+
description: "Optional short title for the compressed range"
|
|
1137
|
+
},
|
|
1138
|
+
content: {
|
|
1139
|
+
type: "array",
|
|
1140
|
+
description: "One or more ranges to compress into separate summary blocks",
|
|
1141
|
+
items: {
|
|
1142
|
+
type: "object",
|
|
1143
|
+
properties: {
|
|
1144
|
+
topic: { type: "string" },
|
|
1145
|
+
startId: {
|
|
1146
|
+
type: "string",
|
|
1147
|
+
description: "mNNNNN ref at the start of the range"
|
|
1148
|
+
},
|
|
1149
|
+
endId: {
|
|
1150
|
+
type: "string",
|
|
1151
|
+
description: "mNNNNN ref at the end of the range"
|
|
1152
|
+
},
|
|
1153
|
+
summary: {
|
|
1154
|
+
type: "string",
|
|
1155
|
+
description: "Self-contained summary replacing the range"
|
|
1156
|
+
}
|
|
1157
|
+
},
|
|
1158
|
+
required: ["startId", "endId", "summary"]
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
},
|
|
1162
|
+
required: ["content"]
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
var COMPRESS_TOOL_OPENAI = {
|
|
1166
|
+
type: "function",
|
|
1167
|
+
function: {
|
|
1168
|
+
name: COMPRESS_TOOL_NAME,
|
|
1169
|
+
description: COMPRESS_TOOL.description,
|
|
1170
|
+
parameters: {
|
|
1171
|
+
type: "object",
|
|
1172
|
+
properties: {
|
|
1173
|
+
topic: {
|
|
1174
|
+
type: "string",
|
|
1175
|
+
description: "Optional short title for the compressed range"
|
|
1176
|
+
},
|
|
1177
|
+
content: {
|
|
1178
|
+
type: "array",
|
|
1179
|
+
description: "One or more ranges to compress into separate summary blocks. REQUIRED \u2014 compress without content is invalid.",
|
|
1180
|
+
items: {
|
|
1181
|
+
type: "object",
|
|
1182
|
+
properties: {
|
|
1183
|
+
topic: { type: "string" },
|
|
1184
|
+
startId: {
|
|
1185
|
+
type: "string",
|
|
1186
|
+
description: "mNNNNN ref at the start of the range"
|
|
1187
|
+
},
|
|
1188
|
+
endId: {
|
|
1189
|
+
type: "string",
|
|
1190
|
+
description: "mNNNNN ref at the end of the range"
|
|
1191
|
+
},
|
|
1192
|
+
summary: {
|
|
1193
|
+
type: "string",
|
|
1194
|
+
description: "Self-contained summary replacing the range"
|
|
1195
|
+
}
|
|
1196
|
+
},
|
|
1197
|
+
required: ["startId", "endId", "summary"]
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
},
|
|
1201
|
+
required: ["content"]
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
var DECOMPRESS_TOOL_OPENAI = {
|
|
1206
|
+
type: "function",
|
|
1207
|
+
function: {
|
|
1208
|
+
name: DECOMPRESS_TOOL_NAME,
|
|
1209
|
+
description: "Restores previously compressed content. Use when you need exact details lost in compression. By default restores one tier up. Use full:true for all the way to original messages. Use toFile to write to file instead of inflating context.",
|
|
1210
|
+
parameters: {
|
|
1211
|
+
type: "object",
|
|
1212
|
+
properties: {
|
|
1213
|
+
blockId: {
|
|
1214
|
+
type: "string",
|
|
1215
|
+
description: "Block ID to decompress (e.g. b5)"
|
|
1216
|
+
},
|
|
1217
|
+
toFile: {
|
|
1218
|
+
type: "string",
|
|
1219
|
+
description: "Optional: write content to file instead of context"
|
|
1220
|
+
},
|
|
1221
|
+
full: {
|
|
1222
|
+
type: "boolean",
|
|
1223
|
+
description: "Restore all the way to original messages"
|
|
1224
|
+
}
|
|
1225
|
+
},
|
|
1226
|
+
required: ["blockId"]
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
var SEARCH_CONTEXT_TOOL_OPENAI = {
|
|
1231
|
+
type: "function",
|
|
1232
|
+
function: {
|
|
1233
|
+
name: SEARCH_CONTEXT_TOOL_NAME,
|
|
1234
|
+
description: "Search through compressed block summaries by keyword. Use BEFORE decompressing to find the right block.",
|
|
1235
|
+
parameters: {
|
|
1236
|
+
type: "object",
|
|
1237
|
+
properties: {
|
|
1238
|
+
query: { type: "string", description: "Search query" },
|
|
1239
|
+
limit: { type: "number", description: "Max results (default 5)" }
|
|
1240
|
+
},
|
|
1241
|
+
required: ["query"]
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
var ACP_STATUS_TOOL_OPENAI = {
|
|
1246
|
+
type: "function",
|
|
1247
|
+
function: {
|
|
1248
|
+
name: ACP_STATUS_TOOL_NAME,
|
|
1249
|
+
description: "Show context usage and compressible ranges. No args = overview. Use to find what to compress next.",
|
|
1250
|
+
parameters: {
|
|
1251
|
+
type: "object",
|
|
1252
|
+
properties: {}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
var DECOMPRESS_TOOL = {
|
|
1257
|
+
name: DECOMPRESS_TOOL_NAME,
|
|
1258
|
+
description: DECOMPRESS_TOOL_OPENAI.function.description,
|
|
1259
|
+
input_schema: DECOMPRESS_TOOL_OPENAI.function.parameters
|
|
1260
|
+
};
|
|
1261
|
+
var SEARCH_CONTEXT_TOOL = {
|
|
1262
|
+
name: SEARCH_CONTEXT_TOOL_NAME,
|
|
1263
|
+
description: SEARCH_CONTEXT_TOOL_OPENAI.function.description,
|
|
1264
|
+
input_schema: SEARCH_CONTEXT_TOOL_OPENAI.function.parameters
|
|
1265
|
+
};
|
|
1266
|
+
var ACP_STATUS_TOOL = {
|
|
1267
|
+
name: ACP_STATUS_TOOL_NAME,
|
|
1268
|
+
description: ACP_STATUS_TOOL_OPENAI.function.description,
|
|
1269
|
+
input_schema: ACP_STATUS_TOOL_OPENAI.function.parameters
|
|
1270
|
+
};
|
|
1271
|
+
var COMPRESS_TOOL_RESPONSES = {
|
|
1272
|
+
type: "function",
|
|
1273
|
+
name: COMPRESS_TOOL_NAME,
|
|
1274
|
+
description: COMPRESS_TOOL.description,
|
|
1275
|
+
parameters: COMPRESS_TOOL_OPENAI.function.parameters
|
|
1276
|
+
};
|
|
1277
|
+
var DECOMPRESS_TOOL_RESPONSES = {
|
|
1278
|
+
type: "function",
|
|
1279
|
+
name: DECOMPRESS_TOOL_OPENAI.function.name,
|
|
1280
|
+
description: DECOMPRESS_TOOL_OPENAI.function.description,
|
|
1281
|
+
parameters: DECOMPRESS_TOOL_OPENAI.function.parameters
|
|
1282
|
+
};
|
|
1283
|
+
var SEARCH_CONTEXT_TOOL_RESPONSES = {
|
|
1284
|
+
type: "function",
|
|
1285
|
+
name: SEARCH_CONTEXT_TOOL_OPENAI.function.name,
|
|
1286
|
+
description: SEARCH_CONTEXT_TOOL_OPENAI.function.description,
|
|
1287
|
+
parameters: SEARCH_CONTEXT_TOOL_OPENAI.function.parameters
|
|
1288
|
+
};
|
|
1289
|
+
var ACP_STATUS_TOOL_RESPONSES = {
|
|
1290
|
+
type: "function",
|
|
1291
|
+
name: ACP_STATUS_TOOL_OPENAI.function.name,
|
|
1292
|
+
description: ACP_STATUS_TOOL_OPENAI.function.description,
|
|
1293
|
+
parameters: ACP_STATUS_TOOL_OPENAI.function.parameters
|
|
1294
|
+
};
|
|
1295
|
+
var ACP_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
1296
|
+
COMPRESS_TOOL_NAME,
|
|
1297
|
+
DECOMPRESS_TOOL_NAME,
|
|
1298
|
+
SEARCH_CONTEXT_TOOL_NAME,
|
|
1299
|
+
ACP_STATUS_TOOL_NAME
|
|
1300
|
+
]);
|
|
1301
|
+
var ALWAYS_PROTECTED_TOOLS = ["compress"];
|
|
1302
|
+
var NEVER_PRESERVE_RECENT_TOOLS = [
|
|
1303
|
+
"decompress",
|
|
1304
|
+
"search_context",
|
|
1305
|
+
"read",
|
|
1306
|
+
"bash"
|
|
1307
|
+
];
|
|
1308
|
+
function isNeverPreserveRecent(msg) {
|
|
1309
|
+
if (msg.contentType !== "tool-call" && msg.contentType !== "tool-result") {
|
|
1310
|
+
return false;
|
|
1311
|
+
}
|
|
1312
|
+
if (!msg.toolName) return false;
|
|
1313
|
+
return NEVER_PRESERVE_RECENT_TOOLS.includes(msg.toolName);
|
|
1314
|
+
}
|
|
1315
|
+
function matchToolPattern(toolName, pattern) {
|
|
1316
|
+
if (pattern.endsWith("*")) {
|
|
1317
|
+
return toolName.startsWith(pattern.slice(0, -1));
|
|
1318
|
+
}
|
|
1319
|
+
return toolName === pattern;
|
|
1320
|
+
}
|
|
1321
|
+
function isMessageProtected(msg, config) {
|
|
1322
|
+
if (msg.contentType !== "tool-call" && msg.contentType !== "tool-result" || !msg.toolName) {
|
|
1323
|
+
return false;
|
|
1324
|
+
}
|
|
1325
|
+
if (ALWAYS_PROTECTED_TOOLS.includes(msg.toolName)) {
|
|
1326
|
+
return true;
|
|
1327
|
+
}
|
|
1328
|
+
for (const pattern of config.protectedTools) {
|
|
1329
|
+
if (matchToolPattern(msg.toolName, pattern)) return true;
|
|
1330
|
+
}
|
|
1331
|
+
if (config.isToolProtected?.(msg.toolName, msg.text)) return true;
|
|
1332
|
+
return false;
|
|
1333
|
+
}
|
|
1334
|
+
function collectProtectedToolCallIds(messages, config) {
|
|
1335
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1336
|
+
for (const m of messages) {
|
|
1337
|
+
if (m.contentType === "tool-call" && m.toolCallId && isMessageProtected(m, config)) {
|
|
1338
|
+
ids.add(m.toolCallId);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
return ids;
|
|
1342
|
+
}
|
|
1343
|
+
function isMessageProtectedWithPairing(msg, config, protectedCallIds) {
|
|
1344
|
+
if (isMessageProtected(msg, config)) return true;
|
|
1345
|
+
if (msg.contentType === "tool-result" && msg.toolCallId && protectedCallIds.has(msg.toolCallId)) {
|
|
1346
|
+
return true;
|
|
1347
|
+
}
|
|
1348
|
+
return false;
|
|
1349
|
+
}
|
|
1350
|
+
var ABSORB_PROMPT_MARKER = "[ACP absorb]";
|
|
1351
|
+
var DEFAULT_ABSORB_CONFIG = {
|
|
1352
|
+
enabled: false,
|
|
1353
|
+
toolName: ABSORB_TOOL_NAME,
|
|
1354
|
+
minToolTokens: 1e3,
|
|
1355
|
+
contextThresholdPct: 0,
|
|
1356
|
+
excludeTools: []
|
|
1357
|
+
};
|
|
1358
|
+
function resolveAbsorbConfig(config) {
|
|
1359
|
+
return { ...DEFAULT_ABSORB_CONFIG, ...config.absorb ?? {} };
|
|
1360
|
+
}
|
|
1361
|
+
function formatTokenCount(tokens) {
|
|
1362
|
+
if (tokens < 1e3) return String(tokens);
|
|
1363
|
+
if (tokens < 1e4) return (tokens / 1e3).toFixed(1) + "K";
|
|
1364
|
+
return Math.round(tokens / 1e3) + "K";
|
|
1365
|
+
}
|
|
1366
|
+
function buildAbsorbPrompt(ref, tokens, toolName = ABSORB_TOOL_NAME) {
|
|
1367
|
+
return `${ABSORB_PROMPT_MARKER} This tool result (~${formatTokenCount(tokens)} tokens) will be REMOVED from context. Your IMMEDIATE next action: call ${toolName}({ ref: "${ref}", summary: "..." }) \u2014 summary = distilled essentials only (outcome, key values, exact paths:lines, error text verbatim, decisions). Afterwards work from your summary; do NOT re-run this tool. If the result contains nothing you need, call ${toolName} with summary "(nothing needed)".`;
|
|
1368
|
+
}
|
|
1369
|
+
function isAcpOrConfiguredTool(toolName, cfg) {
|
|
1370
|
+
if (!toolName) return false;
|
|
1371
|
+
if (toolName === cfg.toolName) return true;
|
|
1372
|
+
return ACP_TOOL_NAMES.has(toolName);
|
|
1373
|
+
}
|
|
1374
|
+
function isAbsorbCandidate(msg, config) {
|
|
1375
|
+
if (msg.contentType !== "tool-result" || !msg.toolCallId) return false;
|
|
1376
|
+
const cfg = resolveAbsorbConfig(config);
|
|
1377
|
+
if (isAcpOrConfiguredTool(msg.toolName, cfg)) return false;
|
|
1378
|
+
if (isMessageProtected(msg, config)) return false;
|
|
1379
|
+
for (const pattern of cfg.excludeTools) {
|
|
1380
|
+
if (msg.toolName && matchToolPattern(msg.toolName, pattern)) return false;
|
|
1381
|
+
}
|
|
1382
|
+
return true;
|
|
1383
|
+
}
|
|
1384
|
+
function hideAbsorbedMessages(messages, state) {
|
|
1385
|
+
const records = state.absorbed ?? [];
|
|
1386
|
+
if (records.length === 0) return messages;
|
|
1387
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
1388
|
+
for (const record of records) {
|
|
1389
|
+
if (record.callMessageId) hidden.add(record.callMessageId);
|
|
1390
|
+
if (record.resultMessageId) hidden.add(record.resultMessageId);
|
|
1391
|
+
}
|
|
1392
|
+
return messages.filter((msg) => !hidden.has(msg.id));
|
|
1393
|
+
}
|
|
1394
|
+
function appendAbsorbPrompts(messages, state, config, tokenCount, countTokens) {
|
|
1395
|
+
const cfg = resolveAbsorbConfig(config);
|
|
1396
|
+
if (!cfg.enabled) return { messages, promptedCount: 0 };
|
|
1397
|
+
const limit = config.modelContextLimit;
|
|
1398
|
+
if (cfg.contextThresholdPct > 0 && limit > 0 && tokenCount < cfg.contextThresholdPct * limit) {
|
|
1399
|
+
return { messages, promptedCount: 0 };
|
|
1400
|
+
}
|
|
1401
|
+
const absorbedIds = /* @__PURE__ */ new Set();
|
|
1402
|
+
for (const record of state.absorbed ?? []) {
|
|
1403
|
+
if (record.resultMessageId) absorbedIds.add(record.resultMessageId);
|
|
1404
|
+
}
|
|
1405
|
+
let promptedCount = 0;
|
|
1406
|
+
const out = messages.map((msg) => {
|
|
1407
|
+
if (!isAbsorbCandidate(msg, config)) return msg;
|
|
1408
|
+
if (absorbedIds.has(msg.id)) return msg;
|
|
1409
|
+
const text = msg.text ?? "";
|
|
1410
|
+
if (text.includes(ABSORB_PROMPT_MARKER)) return msg;
|
|
1411
|
+
const tokens = countTokens(text);
|
|
1412
|
+
if (tokens < cfg.minToolTokens) return msg;
|
|
1413
|
+
const ref = refForRaw(state.messageRefs, msg.id);
|
|
1414
|
+
if (!ref || ref === BLOCKED_REF) return msg;
|
|
1415
|
+
promptedCount++;
|
|
1416
|
+
return {
|
|
1417
|
+
...msg,
|
|
1418
|
+
text: text + "\n\n" + buildAbsorbPrompt(ref, tokens, cfg.toolName)
|
|
1419
|
+
};
|
|
1420
|
+
});
|
|
1421
|
+
return { messages: out, promptedCount };
|
|
1422
|
+
}
|
|
632
1423
|
var registry = /* @__PURE__ */ new Map();
|
|
633
1424
|
function listMessageFilters() {
|
|
634
1425
|
return [...registry.values()];
|
|
@@ -748,7 +1539,8 @@ function renderMessage(message, map, countTokens, strategy, snapshot = null) {
|
|
|
748
1539
|
"^" + escapeRegex(TAG_OPEN) + "[^>]*" + GT + escapeRegex(ref) + escapeRegex(TAG_CLOSE) + "\\n?"
|
|
749
1540
|
);
|
|
750
1541
|
const cleanText = (message.text || "").replace(ownTagRe, "");
|
|
751
|
-
const
|
|
1542
|
+
const textTokens = snapshot ? snapshot[ref] ?? (snapshot[ref] = countTokens(cleanText)) : countTokens(cleanText);
|
|
1543
|
+
const tokens = textTokens + thinkingTokenValue(message.thinkingTokens);
|
|
752
1544
|
const type = classifyType(message);
|
|
753
1545
|
const prefix = acpTag(ref, tokens, type) + "\n";
|
|
754
1546
|
if (!cleanText) return { ...message, text: prefix };
|
|
@@ -779,55 +1571,6 @@ function createRenderRefsNode(strategy) {
|
|
|
779
1571
|
};
|
|
780
1572
|
}
|
|
781
1573
|
var renderRefsNode = createRenderRefsNode("all");
|
|
782
|
-
var ALWAYS_PROTECTED_TOOLS = ["compress"];
|
|
783
|
-
var NEVER_PRESERVE_RECENT_TOOLS = [
|
|
784
|
-
"decompress",
|
|
785
|
-
"search_context",
|
|
786
|
-
"read",
|
|
787
|
-
"bash"
|
|
788
|
-
];
|
|
789
|
-
function isNeverPreserveRecent(msg) {
|
|
790
|
-
if (msg.contentType !== "tool-call" && msg.contentType !== "tool-result") {
|
|
791
|
-
return false;
|
|
792
|
-
}
|
|
793
|
-
if (!msg.toolName) return false;
|
|
794
|
-
return NEVER_PRESERVE_RECENT_TOOLS.includes(msg.toolName);
|
|
795
|
-
}
|
|
796
|
-
function matchToolPattern(toolName, pattern) {
|
|
797
|
-
if (pattern.endsWith("*")) {
|
|
798
|
-
return toolName.startsWith(pattern.slice(0, -1));
|
|
799
|
-
}
|
|
800
|
-
return toolName === pattern;
|
|
801
|
-
}
|
|
802
|
-
function isMessageProtected(msg, config) {
|
|
803
|
-
if (msg.contentType !== "tool-call" && msg.contentType !== "tool-result" || !msg.toolName) {
|
|
804
|
-
return false;
|
|
805
|
-
}
|
|
806
|
-
if (ALWAYS_PROTECTED_TOOLS.includes(msg.toolName)) {
|
|
807
|
-
return true;
|
|
808
|
-
}
|
|
809
|
-
for (const pattern of config.protectedTools) {
|
|
810
|
-
if (matchToolPattern(msg.toolName, pattern)) return true;
|
|
811
|
-
}
|
|
812
|
-
if (config.isToolProtected?.(msg.toolName, msg.text)) return true;
|
|
813
|
-
return false;
|
|
814
|
-
}
|
|
815
|
-
function collectProtectedToolCallIds(messages, config) {
|
|
816
|
-
const ids = /* @__PURE__ */ new Set();
|
|
817
|
-
for (const m of messages) {
|
|
818
|
-
if (m.contentType === "tool-call" && m.toolCallId && isMessageProtected(m, config)) {
|
|
819
|
-
ids.add(m.toolCallId);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
return ids;
|
|
823
|
-
}
|
|
824
|
-
function isMessageProtectedWithPairing(msg, config, protectedCallIds) {
|
|
825
|
-
if (isMessageProtected(msg, config)) return true;
|
|
826
|
-
if (msg.contentType === "tool-result" && msg.toolCallId && protectedCallIds.has(msg.toolCallId)) {
|
|
827
|
-
return true;
|
|
828
|
-
}
|
|
829
|
-
return false;
|
|
830
|
-
}
|
|
831
1574
|
function adjustBoundariesForToolPairs(startIndex, endIndex, messages, maxScan = 20) {
|
|
832
1575
|
const callIdsInRange = /* @__PURE__ */ new Set();
|
|
833
1576
|
for (let i = startIndex; i <= endIndex; i++) {
|
|
@@ -876,8 +1619,12 @@ function adjustBoundariesForReasoningPairs(startIndex, endIndex, messages) {
|
|
|
876
1619
|
j++;
|
|
877
1620
|
}
|
|
878
1621
|
const companion = messages[j + 1];
|
|
879
|
-
if (companion !== void 0 && companion.role === "assistant" && (companion.contentType === "text" || companion.contentType === "tool-call")
|
|
880
|
-
|
|
1622
|
+
if (companion !== void 0 && companion.role === "assistant" && (companion.contentType === "text" || companion.contentType === "tool-call")) {
|
|
1623
|
+
let e = j + 1;
|
|
1624
|
+
while (e + 1 < messages.length && messages[e + 1].role === "assistant" && (messages[e + 1].contentType === "text" || messages[e + 1].contentType === "tool-call")) {
|
|
1625
|
+
e++;
|
|
1626
|
+
}
|
|
1627
|
+
if (e > newEndIndex) newEndIndex = e;
|
|
881
1628
|
}
|
|
882
1629
|
}
|
|
883
1630
|
if (msg.role === "assistant" && (msg.contentType === "text" || msg.contentType === "tool-call")) {
|
|
@@ -893,9 +1640,63 @@ function adjustBoundariesForReasoningPairs(startIndex, endIndex, messages) {
|
|
|
893
1640
|
}
|
|
894
1641
|
return { startIndex: newStartIndex, endIndex: newEndIndex };
|
|
895
1642
|
}
|
|
896
|
-
function
|
|
897
|
-
|
|
898
|
-
|
|
1643
|
+
function isAssistantAct(msg) {
|
|
1644
|
+
return msg.role === "assistant" && (msg.contentType === "text" || msg.contentType === "tool-call");
|
|
1645
|
+
}
|
|
1646
|
+
function computeTurnGroups(messages) {
|
|
1647
|
+
const resultIdByCallId = /* @__PURE__ */ new Map();
|
|
1648
|
+
for (const msg of messages) {
|
|
1649
|
+
if (msg.contentType === "tool-result" && typeof msg.toolCallId === "string" && msg.id) {
|
|
1650
|
+
if (!resultIdByCallId.has(msg.toolCallId))
|
|
1651
|
+
resultIdByCallId.set(msg.toolCallId, msg.id);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
const grouped = /* @__PURE__ */ new Set();
|
|
1655
|
+
const groups = [];
|
|
1656
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1657
|
+
const msg = messages[i];
|
|
1658
|
+
if (!msg.id || grouped.has(msg.id)) continue;
|
|
1659
|
+
if (!(msg.contentType === "reasoning" || isAssistantAct(msg))) continue;
|
|
1660
|
+
let reasoningStart = i;
|
|
1661
|
+
if (msg.contentType === "reasoning") {
|
|
1662
|
+
while (reasoningStart > 0 && messages[reasoningStart - 1].contentType === "reasoning") {
|
|
1663
|
+
reasoningStart--;
|
|
1664
|
+
}
|
|
1665
|
+
} else {
|
|
1666
|
+
let s = i;
|
|
1667
|
+
while (s > 0 && isAssistantAct(messages[s - 1])) s--;
|
|
1668
|
+
reasoningStart = s;
|
|
1669
|
+
while (reasoningStart > 0 && messages[reasoningStart - 1].contentType === "reasoning") {
|
|
1670
|
+
reasoningStart--;
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
const burstStart = (() => {
|
|
1674
|
+
let s = reasoningStart;
|
|
1675
|
+
while (s < messages.length && messages[s].contentType === "reasoning")
|
|
1676
|
+
s++;
|
|
1677
|
+
return s;
|
|
1678
|
+
})();
|
|
1679
|
+
if (burstStart >= messages.length || !isAssistantAct(messages[burstStart])) {
|
|
1680
|
+
continue;
|
|
1681
|
+
}
|
|
1682
|
+
let burstEnd = burstStart;
|
|
1683
|
+
while (burstEnd + 1 < messages.length && isAssistantAct(messages[burstEnd + 1])) {
|
|
1684
|
+
burstEnd++;
|
|
1685
|
+
}
|
|
1686
|
+
const members = /* @__PURE__ */ new Set();
|
|
1687
|
+
for (let k = reasoningStart; k <= burstEnd; k++) {
|
|
1688
|
+
const m = messages[k];
|
|
1689
|
+
if (!m.id) continue;
|
|
1690
|
+
members.add(m.id);
|
|
1691
|
+
if (m.role === "assistant" && m.contentType === "tool-call" && typeof m.toolCallId === "string") {
|
|
1692
|
+
const rid = resultIdByCallId.get(m.toolCallId);
|
|
1693
|
+
if (rid) members.add(rid);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
for (const id of members) grouped.add(id);
|
|
1697
|
+
groups.push([...members]);
|
|
1698
|
+
}
|
|
1699
|
+
return groups;
|
|
899
1700
|
}
|
|
900
1701
|
function estimateTextTokens(text) {
|
|
901
1702
|
return Math.ceil(text.length / 4);
|
|
@@ -920,7 +1721,7 @@ function computeProtectedRefs(messages, state, config, countTokens = estimateTex
|
|
|
920
1721
|
if (isNeverPreserveRecent(msg)) continue;
|
|
921
1722
|
const ref = state.messageRefs.byRaw[msg.id];
|
|
922
1723
|
if (!ref || ref === "BLOCKED") continue;
|
|
923
|
-
visible.push({ ref, tokens:
|
|
1724
|
+
visible.push({ ref, tokens: countMessageTokens(msg, countTokens) });
|
|
924
1725
|
}
|
|
925
1726
|
if (preserveN > 0) {
|
|
926
1727
|
for (const m of visible.slice(-preserveN)) {
|
|
@@ -949,42 +1750,50 @@ function buildCompressibleRanges(messages, state, config, protectedZoneRefs, cou
|
|
|
949
1750
|
const compressibleMsgs = [];
|
|
950
1751
|
const protectedMsgs = [];
|
|
951
1752
|
const protectedCallIds = collectProtectedToolCallIds(messages, config);
|
|
1753
|
+
let skipSinceCompressible = false;
|
|
1754
|
+
let skipSinceProtected = false;
|
|
952
1755
|
for (const msg of messages) {
|
|
953
|
-
if (isSyntheticOrPruned(msg, state)) continue;
|
|
954
1756
|
const ref = state.messageRefs.byRaw[msg.id];
|
|
955
1757
|
if (!ref || ref === "BLOCKED") continue;
|
|
956
|
-
|
|
1758
|
+
if (isSyntheticOrPruned(msg, state)) {
|
|
1759
|
+
skipSinceCompressible = true;
|
|
1760
|
+
skipSinceProtected = true;
|
|
1761
|
+
continue;
|
|
1762
|
+
}
|
|
957
1763
|
if (isMessageProtectedWithPairing(msg, config, protectedCallIds)) {
|
|
958
1764
|
protectedMsgs.push({
|
|
959
1765
|
ref,
|
|
960
|
-
|
|
961
|
-
tokens:
|
|
1766
|
+
gapBefore: skipSinceProtected,
|
|
1767
|
+
tokens: countMessageTokens(msg, countTokens),
|
|
962
1768
|
tools: msg.toolName ? [msg.toolName] : []
|
|
963
1769
|
});
|
|
1770
|
+
skipSinceProtected = false;
|
|
1771
|
+
skipSinceCompressible = true;
|
|
964
1772
|
continue;
|
|
965
1773
|
}
|
|
966
1774
|
if (protectedZoneRefs?.has(ref)) {
|
|
1775
|
+
skipSinceCompressible = true;
|
|
1776
|
+
skipSinceProtected = true;
|
|
967
1777
|
continue;
|
|
968
1778
|
}
|
|
969
1779
|
compressibleMsgs.push({
|
|
970
1780
|
ref,
|
|
971
|
-
|
|
972
|
-
tokens:
|
|
1781
|
+
gapBefore: skipSinceCompressible,
|
|
1782
|
+
tokens: countMessageTokens(msg, countTokens),
|
|
973
1783
|
chars: (msg.text ?? "").length,
|
|
974
1784
|
isTool: isToolMessage(msg),
|
|
975
1785
|
isUser: msg.role === "user"
|
|
976
1786
|
});
|
|
1787
|
+
skipSinceCompressible = false;
|
|
1788
|
+
skipSinceProtected = true;
|
|
977
1789
|
}
|
|
978
1790
|
const compressible = [];
|
|
979
1791
|
let cur = null;
|
|
980
|
-
let prevRefNum = -2;
|
|
981
1792
|
for (const info of compressibleMsgs) {
|
|
982
|
-
|
|
983
|
-
if (cur && (info.isUser && cur.count >= 3 || hasGap)) {
|
|
1793
|
+
if (cur && (info.isUser && cur.count >= 3 || info.gapBefore)) {
|
|
984
1794
|
compressible.push(cur);
|
|
985
1795
|
cur = null;
|
|
986
1796
|
}
|
|
987
|
-
prevRefNum = info.refNum;
|
|
988
1797
|
if (!cur) {
|
|
989
1798
|
cur = {
|
|
990
1799
|
startRef: info.ref,
|
|
@@ -1011,14 +1820,11 @@ function buildCompressibleRanges(messages, state, config, protectedZoneRefs, cou
|
|
|
1011
1820
|
if (cur) compressible.push(cur);
|
|
1012
1821
|
const protectedRanges = [];
|
|
1013
1822
|
let pcur = null;
|
|
1014
|
-
let pPrevRefNum = -2;
|
|
1015
1823
|
for (const info of protectedMsgs) {
|
|
1016
|
-
|
|
1017
|
-
if (pcur && hasGap) {
|
|
1824
|
+
if (pcur && info.gapBefore) {
|
|
1018
1825
|
protectedRanges.push(pcur);
|
|
1019
1826
|
pcur = null;
|
|
1020
1827
|
}
|
|
1021
|
-
pPrevRefNum = info.refNum;
|
|
1022
1828
|
if (!pcur) {
|
|
1023
1829
|
pcur = {
|
|
1024
1830
|
startRef: info.ref,
|
|
@@ -1098,6 +1904,30 @@ function runPipeline(nodes, initial, ctx) {
|
|
|
1098
1904
|
function rangeError(spec, message) {
|
|
1099
1905
|
return `range ${spec.startRef}..${spec.endRef}: ${message}`;
|
|
1100
1906
|
}
|
|
1907
|
+
function numericBlockId(id) {
|
|
1908
|
+
const parsed = /^b(\d+)$/.exec(id);
|
|
1909
|
+
return parsed ? Number(parsed[1]) : 0;
|
|
1910
|
+
}
|
|
1911
|
+
function refGateDiagnostics(state, requestedRanges, unknownCount) {
|
|
1912
|
+
const highest = highestUsedIndex(state.messageRefs);
|
|
1913
|
+
const highestRef = highest > 0 ? indexToRef(highest) : "none";
|
|
1914
|
+
return `[diagnostics: session highest ref=${highestRef}, unknown ranges in request=${unknownCount}/${requestedRanges}, session history=${state.stats.compressionCount} compression(s), ${state.blocks.length} block(s)]`;
|
|
1915
|
+
}
|
|
1916
|
+
function danglingMessageRefs(state, messages, spec) {
|
|
1917
|
+
const visible = new Set(messages.map((m) => m.id));
|
|
1918
|
+
const dangling = [];
|
|
1919
|
+
for (const ref of [spec.startRef, spec.endRef]) {
|
|
1920
|
+
const parsed = parseBoundary(ref);
|
|
1921
|
+
if (!parsed || parsed.kind !== "message") continue;
|
|
1922
|
+
const rawId = state.messageRefs.byRef[parsed.raw] ?? state.messageRefs.byRef[indexToRef(parsed.numericId)];
|
|
1923
|
+
if (!rawId || visible.has(rawId)) continue;
|
|
1924
|
+
const covered = state.blocks.some(
|
|
1925
|
+
(block) => block.active && block.effectiveMessageIds.includes(rawId)
|
|
1926
|
+
);
|
|
1927
|
+
if (!covered) dangling.push(parsed.raw);
|
|
1928
|
+
}
|
|
1929
|
+
return dangling;
|
|
1930
|
+
}
|
|
1101
1931
|
function createCore(ports = {}) {
|
|
1102
1932
|
const countTokens = ports.countTokens ?? defaultCountTokens;
|
|
1103
1933
|
function applyCompression(input) {
|
|
@@ -1107,7 +1937,12 @@ function createCore(ports = {}) {
|
|
|
1107
1937
|
let tokensCompressed = 0;
|
|
1108
1938
|
const errors = [];
|
|
1109
1939
|
const warnings = [];
|
|
1110
|
-
const protectedMessageIds = input.protectedMessageIds ?? computeProtectedRefs(
|
|
1940
|
+
const protectedMessageIds = input.protectedMessageIds ?? computeProtectedRefs(
|
|
1941
|
+
input.messages,
|
|
1942
|
+
input.state,
|
|
1943
|
+
input.config,
|
|
1944
|
+
countTokens
|
|
1945
|
+
);
|
|
1111
1946
|
const preExistingCoverage = collectCoverage(state);
|
|
1112
1947
|
const classifications = /* @__PURE__ */ new Map();
|
|
1113
1948
|
const classificationErrors = [];
|
|
@@ -1138,37 +1973,41 @@ function createCore(ports = {}) {
|
|
|
1138
1973
|
error: error instanceof Error ? error : new Error(String(error))
|
|
1139
1974
|
});
|
|
1140
1975
|
classificationErrors.push(
|
|
1141
|
-
rangeError(
|
|
1976
|
+
rangeError(
|
|
1977
|
+
spec,
|
|
1978
|
+
error instanceof Error ? error.message : String(error)
|
|
1979
|
+
)
|
|
1142
1980
|
);
|
|
1143
1981
|
}
|
|
1144
1982
|
}
|
|
1145
1983
|
}
|
|
1146
|
-
|
|
1984
|
+
let resolvableCount = 0;
|
|
1985
|
+
let unknownCount = 0;
|
|
1986
|
+
for (const resolution of classifications.values()) {
|
|
1987
|
+
if (resolution.status === "ok") resolvableCount++;
|
|
1988
|
+
else if (resolution.status === "unknown") unknownCount++;
|
|
1989
|
+
}
|
|
1990
|
+
const rangeSpans = [];
|
|
1147
1991
|
for (const [spec, resolution] of classifications) {
|
|
1148
1992
|
if (resolution.status !== "ok") continue;
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
const bMin = b.indices.length > 0 ? Math.min(...b.indices) : Infinity;
|
|
1157
|
-
return aMin - bMin;
|
|
1158
|
-
});
|
|
1993
|
+
rangeSpans.push({
|
|
1994
|
+
spec,
|
|
1995
|
+
start: resolution.resolved.startIndex,
|
|
1996
|
+
end: resolution.resolved.endIndex
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
const sortedRanges = [...rangeSpans].sort((a, b) => a.start - b.start);
|
|
1159
2000
|
const skipSpecs = /* @__PURE__ */ new Set();
|
|
1160
2001
|
let acceptedMaxIndex = -1;
|
|
1161
2002
|
for (const entry of sortedRanges) {
|
|
1162
|
-
|
|
1163
|
-
const entryMin = entry.indices.length > 0 ? Math.min(...entry.indices) : -1;
|
|
1164
|
-
if (entryMin >= 0 && entryMin <= acceptedMaxIndex) {
|
|
2003
|
+
if (entry.start <= acceptedMaxIndex) {
|
|
1165
2004
|
skipSpecs.add(entry.spec);
|
|
1166
2005
|
warnings.push(
|
|
1167
2006
|
`Skipped range (${entry.spec.startRef}..${entry.spec.endRef}) \u2014 overlaps an earlier range in the batch; the earlier range takes precedence. Keep ranges disjoint.`
|
|
1168
2007
|
);
|
|
1169
2008
|
continue;
|
|
1170
2009
|
}
|
|
1171
|
-
if (
|
|
2010
|
+
if (entry.end > acceptedMaxIndex) acceptedMaxIndex = entry.end;
|
|
1172
2011
|
}
|
|
1173
2012
|
if (input.config.compress.minCompressRange > 0 && input.ranges.length > 0) {
|
|
1174
2013
|
let totalRangeChars = 0;
|
|
@@ -1187,7 +2026,17 @@ function createCore(ports = {}) {
|
|
|
1187
2026
|
}
|
|
1188
2027
|
}
|
|
1189
2028
|
if (!hasBlockBoundaryRange && totalRangeChars < input.config.compress.minCompressRange) {
|
|
1190
|
-
const
|
|
2029
|
+
const live = activeBlocks(state).map((b) => b.blockId).sort((x, y) => numericBlockId(x) - numericBlockId(y));
|
|
2030
|
+
const liveHint = live.length > 0 ? ` Current active blocks span ${live[0]}..${live[live.length - 1]} \u2014 retry with startId/endId set to active block IDs in that span.` : "";
|
|
2031
|
+
const diagnostics = refGateDiagnostics(
|
|
2032
|
+
state,
|
|
2033
|
+
input.ranges.length,
|
|
2034
|
+
unknownCount
|
|
2035
|
+
);
|
|
2036
|
+
const danglingRefs = consumedRanges.flatMap(
|
|
2037
|
+
(spec) => danglingMessageRefs(state, input.messages, spec)
|
|
2038
|
+
);
|
|
2039
|
+
const gateMessage = resolvableCount === 0 && consumedRanges.length === 0 && unknownCount > 0 ? `None of the ${input.ranges.length} requested range(s) resolved \u2014 every ref is unknown to this session. Refs are per-session snapshots, assigned once when a message is first rendered; no compress reassigns them, so unknown refs cannot come from an earlier compress in this session. They come from a different generation: a previous session instance (switching model or upstream mid-conversation starts a fresh session whose refs restart at m00001), the generation before a native-compaction rebase (which also resets refs to m00001), or a typo. ${diagnostics} Run acp_status, then call the compress tool again using only the refs it reports.` : consumedRanges.length > 0 ? danglingRefs.length > 0 ? `Requested range(s) cannot be anchored (e.g. ${consumedRanges[0].startRef}..${consumedRanges[0].endRef}) \u2014 the refs exist in this session's ref map, but the messages they point to are no longer in the visible context and no active block covers them: the message content changed (or the message was filtered out of the view) and now carries a new ref, leaving your old refs dangling. ${diagnostics} Run acp_status, then call the compress tool again using only the refs it reports.` : `Requested range(s) already compressed (e.g. ${consumedRanges[0].startRef}..${consumedRanges[0].endRef}) \u2014 those refs no longer point to directly compressible content: the range is covered by active block(s) or the block ref(s) are stale (distilled or consumed). ${diagnostics} Run acp_status, then call the compress tool again using only the CURRENT compressible ranges it reports.${liveHint}` : `Total compressible content too small (${totalRangeChars} chars across ${countedRanges} range(s), min ${input.config.compress.minCompressRange}). Combine more messages into your range(s) to meet the threshold.`;
|
|
1191
2040
|
return {
|
|
1192
2041
|
state: input.state,
|
|
1193
2042
|
result: {
|
|
@@ -1213,6 +2062,7 @@ function createCore(ports = {}) {
|
|
|
1213
2062
|
errors.push(rangeError(spec, resolution.error.message));
|
|
1214
2063
|
continue;
|
|
1215
2064
|
}
|
|
2065
|
+
warnings.push(...resolution.resolved.snappedBoundaries);
|
|
1216
2066
|
try {
|
|
1217
2067
|
const outcome = applySingleRange({
|
|
1218
2068
|
spec,
|
|
@@ -1228,7 +2078,12 @@ function createCore(ports = {}) {
|
|
|
1228
2078
|
tokensCompressed += outcome.tokens;
|
|
1229
2079
|
warnings.push(...outcome.warnings);
|
|
1230
2080
|
} catch (error) {
|
|
1231
|
-
errors.push(
|
|
2081
|
+
errors.push(
|
|
2082
|
+
rangeError(
|
|
2083
|
+
spec,
|
|
2084
|
+
error instanceof Error ? error.message : String(error)
|
|
2085
|
+
)
|
|
2086
|
+
);
|
|
1232
2087
|
}
|
|
1233
2088
|
}
|
|
1234
2089
|
state.stats.compressionCount += blocksCreated;
|
|
@@ -1238,12 +2093,17 @@ function createCore(ports = {}) {
|
|
|
1238
2093
|
state.nudge.lastNudgeShownTokens = 0;
|
|
1239
2094
|
state.nudge.lastShownByTier = {};
|
|
1240
2095
|
}
|
|
1241
|
-
return {
|
|
2096
|
+
return {
|
|
2097
|
+
state,
|
|
2098
|
+
result: { blocksCreated, tokensCompressed, errors, warnings }
|
|
2099
|
+
};
|
|
1242
2100
|
}
|
|
1243
2101
|
function processTurn(input) {
|
|
1244
2102
|
const configErrors = validateConfig(input.config);
|
|
1245
2103
|
if (configErrors.length > 0) {
|
|
1246
|
-
console.warn(
|
|
2104
|
+
console.warn(
|
|
2105
|
+
`[acp-kernel] Config validation warnings: ${configErrors.join("; ")}. Thresholds may not fire correctly.`
|
|
2106
|
+
);
|
|
1247
2107
|
}
|
|
1248
2108
|
const ctx = {
|
|
1249
2109
|
config: input.config,
|
|
@@ -1294,6 +2154,8 @@ function createCore(ports = {}) {
|
|
|
1294
2154
|
assignRefsNode,
|
|
1295
2155
|
syncBlocksNode,
|
|
1296
2156
|
pruneNode,
|
|
2157
|
+
absorbHideNode,
|
|
2158
|
+
absorbPromptNode,
|
|
1297
2159
|
filterNode,
|
|
1298
2160
|
hideCompressCallsNode,
|
|
1299
2161
|
recommendNode,
|
|
@@ -1303,7 +2165,14 @@ function createCore(ports = {}) {
|
|
|
1303
2165
|
if (strategy === "none") return base;
|
|
1304
2166
|
return [...base, createRenderRefsNode(strategy)];
|
|
1305
2167
|
}
|
|
1306
|
-
return {
|
|
2168
|
+
return {
|
|
2169
|
+
processTurn,
|
|
2170
|
+
applyCompression,
|
|
2171
|
+
defaultNodes,
|
|
2172
|
+
decompress,
|
|
2173
|
+
search,
|
|
2174
|
+
status
|
|
2175
|
+
};
|
|
1307
2176
|
}
|
|
1308
2177
|
var assignRefsNode = {
|
|
1309
2178
|
name: "assign-refs",
|
|
@@ -1332,6 +2201,31 @@ var pruneNode = {
|
|
|
1332
2201
|
return { ...io, messages: prune(io.messages, io.state) };
|
|
1333
2202
|
}
|
|
1334
2203
|
};
|
|
2204
|
+
var absorbHideNode = {
|
|
2205
|
+
name: "absorb-hide",
|
|
2206
|
+
enabled: (io) => (io.state.absorbed?.length ?? 0) > 0,
|
|
2207
|
+
run(io) {
|
|
2208
|
+
return { ...io, messages: hideAbsorbedMessages(io.messages, io.state) };
|
|
2209
|
+
}
|
|
2210
|
+
};
|
|
2211
|
+
var absorbPromptNode = {
|
|
2212
|
+
name: "absorb-prompt",
|
|
2213
|
+
enabled: (_io, ctx) => ctx.config.absorb?.enabled === true,
|
|
2214
|
+
run(io, ctx) {
|
|
2215
|
+
const applied = appendAbsorbPrompts(
|
|
2216
|
+
io.messages,
|
|
2217
|
+
io.state,
|
|
2218
|
+
ctx.config,
|
|
2219
|
+
ctx.tokenCount,
|
|
2220
|
+
ctx.countTokens
|
|
2221
|
+
);
|
|
2222
|
+
return {
|
|
2223
|
+
...io,
|
|
2224
|
+
messages: applied.messages,
|
|
2225
|
+
effects: { ...io.effects, absorbPromptedCount: applied.promptedCount }
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
};
|
|
1335
2229
|
var filterNode = {
|
|
1336
2230
|
name: "filter",
|
|
1337
2231
|
enabled: (_io, ctx) => !!ctx.config.messageFilters?.enabled && listMessageFilters().length > 0,
|
|
@@ -1403,7 +2297,10 @@ var nudgeNode = {
|
|
|
1403
2297
|
if (nudge.shouldInject) {
|
|
1404
2298
|
stamped.lastNudgeShownTokens = ctx.tokenCount;
|
|
1405
2299
|
if (nudge.tier !== null) {
|
|
1406
|
-
stamped.lastShownByTier = {
|
|
2300
|
+
stamped.lastShownByTier = {
|
|
2301
|
+
...stamped.lastShownByTier,
|
|
2302
|
+
[nudge.tier]: ctx.tokenCount
|
|
2303
|
+
};
|
|
1407
2304
|
}
|
|
1408
2305
|
}
|
|
1409
2306
|
return {
|
|
@@ -1443,17 +2340,16 @@ function applySingleRange(input) {
|
|
|
1443
2340
|
const rangeMessageIds = applyPairBoundaryAdjustments(
|
|
1444
2341
|
resolved,
|
|
1445
2342
|
input.messages
|
|
1446
|
-
);
|
|
2343
|
+
).filter((id) => !isSummaryMessageId(id));
|
|
1447
2344
|
if (rangeMessageIds.length > resolved.messageIds.length) {
|
|
1448
|
-
const
|
|
1449
|
-
input.messages.forEach((m, i) =>
|
|
1450
|
-
const adjustedStart =
|
|
1451
|
-
const adjustedEnd =
|
|
2345
|
+
const indexByMessageId = /* @__PURE__ */ new Map();
|
|
2346
|
+
input.messages.forEach((m, i) => indexByMessageId.set(m.id, i));
|
|
2347
|
+
const adjustedStart = rangeMessageIds.length > 0 ? indexByMessageId.get(rangeMessageIds[0]) ?? resolved.startIndex : resolved.startIndex;
|
|
2348
|
+
const adjustedEnd = rangeMessageIds.length > 0 ? indexByMessageId.get(rangeMessageIds[rangeMessageIds.length - 1]) ?? resolved.endIndex : resolved.endIndex;
|
|
1452
2349
|
const nestedSeen = new Set(resolved.nestedBlockIds);
|
|
1453
2350
|
for (const block2 of activeBlocks(input.state)) {
|
|
1454
2351
|
if (nestedSeen.has(block2.blockId)) continue;
|
|
1455
|
-
|
|
1456
|
-
if (anchor !== null && anchor >= adjustedStart && anchor <= adjustedEnd) {
|
|
2352
|
+
if (blockVisibleInRange(block2, indexByMessageId, adjustedStart, adjustedEnd)) {
|
|
1457
2353
|
nestedSeen.add(block2.blockId);
|
|
1458
2354
|
resolved.nestedBlockIds.push(block2.blockId);
|
|
1459
2355
|
}
|
|
@@ -1516,11 +2412,56 @@ function applySingleRange(input) {
|
|
|
1516
2412
|
)} from compression range (recent/last-user zone).`
|
|
1517
2413
|
);
|
|
1518
2414
|
}
|
|
2415
|
+
{
|
|
2416
|
+
const reasoningIds = /* @__PURE__ */ new Set();
|
|
2417
|
+
const callIds = /* @__PURE__ */ new Set();
|
|
2418
|
+
for (const m of input.messages) {
|
|
2419
|
+
if (!m.id) continue;
|
|
2420
|
+
if (m.contentType === "reasoning") reasoningIds.add(m.id);
|
|
2421
|
+
if (m.role === "assistant" && m.contentType === "tool-call") callIds.add(m.id);
|
|
2422
|
+
}
|
|
2423
|
+
const withdrawIds = /* @__PURE__ */ new Set();
|
|
2424
|
+
let splitTurnCount = 0;
|
|
2425
|
+
for (const group of computeTurnGroups(input.messages)) {
|
|
2426
|
+
const foldHasReasoning = group.some(
|
|
2427
|
+
(id) => effectiveMessageIds.has(id) && reasoningIds.has(id)
|
|
2428
|
+
);
|
|
2429
|
+
if (!foldHasReasoning) continue;
|
|
2430
|
+
const keptHasCall = group.some(
|
|
2431
|
+
(id) => !effectiveMessageIds.has(id) && callIds.has(id)
|
|
2432
|
+
);
|
|
2433
|
+
if (!keptHasCall) continue;
|
|
2434
|
+
splitTurnCount++;
|
|
2435
|
+
for (const id of group) withdrawIds.add(id);
|
|
2436
|
+
}
|
|
2437
|
+
if (withdrawIds.size > 0) {
|
|
2438
|
+
for (const id of withdrawIds) effectiveMessageIds.delete(id);
|
|
2439
|
+
const beforeWithdraw = filteredIds.length;
|
|
2440
|
+
filteredIds = filteredIds.filter((id) => !withdrawIds.has(id));
|
|
2441
|
+
if (filteredIds.length === 0 && consumedBlockIds.length === 0) {
|
|
2442
|
+
throw new Error(
|
|
2443
|
+
`Range would split ${splitTurnCount} turn(s) at the protected-zone boundary: a visible tool-call must keep its reasoning run (strict-echo providers reject a rebuilt request that lost it). Shrink the range to end before the turn starts, or wait until the whole turn ages out of the protected zone.`
|
|
2444
|
+
);
|
|
2445
|
+
}
|
|
2446
|
+
warnings.push(
|
|
2447
|
+
`Withdrawn ${beforeWithdraw - filteredIds.length} message(s) from compression range to keep ${splitTurnCount} turn(s) intact (visible tool-call would lose its reasoning run).`
|
|
2448
|
+
);
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
if (!isBlockBoundary && filteredIds.length === 0 && consumedBlockIds.length > 0) {
|
|
2452
|
+
const first = consumedBlockIds[0];
|
|
2453
|
+
const last = consumedBlockIds[consumedBlockIds.length - 1];
|
|
2454
|
+
throw new Error(
|
|
2455
|
+
`Range ${input.spec.startRef}..${input.spec.endRef} contains no new compressible messages \u2014 every message in it is already covered by active block(s) ${consumedBlockIds.join(
|
|
2456
|
+
", "
|
|
2457
|
+
)}. Nothing was compressed. To rewrite or merge those blocks, reference them by block ID (${first}..${last}); otherwise run acp_status and compress a range it reports as compressible.`
|
|
2458
|
+
);
|
|
2459
|
+
}
|
|
1519
2460
|
validateCompressionRange(input, filteredIds, consumedBlockIds.length);
|
|
1520
2461
|
let compressedTokens = 0;
|
|
1521
2462
|
for (const id of filteredIds) {
|
|
1522
2463
|
const message = input.messages.find((entry) => entry.id === id);
|
|
1523
|
-
compressedTokens += input.countTokens
|
|
2464
|
+
compressedTokens += message ? countMessageTokens(message, input.countTokens) : 0;
|
|
1524
2465
|
}
|
|
1525
2466
|
for (const consumedId of consumedBlockIds) {
|
|
1526
2467
|
const consumed = blockById(input.state, consumedId);
|
|
@@ -1664,16 +2605,28 @@ function resolveAdaptiveGrowth(modelContextLimit, nudge) {
|
|
|
1664
2605
|
)
|
|
1665
2606
|
);
|
|
1666
2607
|
}
|
|
2608
|
+
function resolveMinPressureBenefit(modelContextLimit, nudge) {
|
|
2609
|
+
return nudge.minPressureBenefitTokens ?? Math.max(5e3, Math.round(modelContextLimit * 0.01));
|
|
2610
|
+
}
|
|
1667
2611
|
function pendingByTier(state, recommendation, countTokens, minCompressRange) {
|
|
1668
2612
|
const out = {};
|
|
1669
2613
|
const merged = recommendation?.recommendedRanges ?? [];
|
|
1670
2614
|
const effective = minCompressRange > 0 ? merged.filter((r) => (r.chars ?? r.tokens * 4) >= minCompressRange) : merged;
|
|
1671
|
-
out[1] = {
|
|
2615
|
+
out[1] = {
|
|
2616
|
+
pending: effective.reduce((s, r) => s + r.tokens, 0),
|
|
2617
|
+
targetBlocks: []
|
|
2618
|
+
};
|
|
1672
2619
|
const active = activeBlocks(state);
|
|
1673
2620
|
const t1 = active.filter((b) => b.tier === 1);
|
|
1674
2621
|
const t2 = active.filter((b) => b.tier === 2);
|
|
1675
|
-
out[2] = {
|
|
1676
|
-
|
|
2622
|
+
out[2] = {
|
|
2623
|
+
pending: t1.reduce((s, b) => s + countTokens(b.summary), 0),
|
|
2624
|
+
targetBlocks: t1
|
|
2625
|
+
};
|
|
2626
|
+
out[3] = {
|
|
2627
|
+
pending: t2.reduce((s, b) => s + countTokens(b.summary), 0),
|
|
2628
|
+
targetBlocks: t2
|
|
2629
|
+
};
|
|
1677
2630
|
return out;
|
|
1678
2631
|
}
|
|
1679
2632
|
function decideNudge(input) {
|
|
@@ -1681,6 +2634,7 @@ function decideNudge(input) {
|
|
|
1681
2634
|
const limit = config.modelContextLimit;
|
|
1682
2635
|
const usage = limit > 0 ? tokenCount / limit : 0;
|
|
1683
2636
|
const nudgeGrowthTokens = resolveAdaptiveGrowth(limit, config.nudge);
|
|
2637
|
+
const minPressureBenefit = resolveMinPressureBenefit(limit, config.nudge);
|
|
1684
2638
|
const overLimit = usage >= config.nudge.maxContextLimitPct;
|
|
1685
2639
|
const emergencyOverride = usage >= config.nudge.emergencyThresholdPct;
|
|
1686
2640
|
const pressure = overLimit || emergencyOverride;
|
|
@@ -1706,17 +2660,23 @@ function decideNudge(input) {
|
|
|
1706
2660
|
);
|
|
1707
2661
|
let injectedTier = null;
|
|
1708
2662
|
let injectedReason = "";
|
|
1709
|
-
|
|
2663
|
+
let bestPending = 0;
|
|
1710
2664
|
const t1Eff = tiers[1]?.pending ?? 0;
|
|
1711
2665
|
const t2Pen = tiers[2]?.pending ?? 0;
|
|
1712
2666
|
const t3Pen = tiers[3]?.pending ?? 0;
|
|
2667
|
+
const firstSightMassReady = state.nudge.lastNudgeShownTokens === 0 && baseline === 0 && usage >= config.nudge.minContextLimitPct && Math.max(t1Eff, t2Pen, t3Pen) >= nudgeGrowthTokens;
|
|
2668
|
+
const growthReady = firstSightMassReady || growthSinceReference >= growthFloor;
|
|
2669
|
+
const t2Count = tiers[2]?.targetBlocks.length ?? 0;
|
|
2670
|
+
const t3Count = tiers[3]?.targetBlocks.length ?? 0;
|
|
2671
|
+
const tierCountUsageFloor = config.nudge.minContextLimitPct;
|
|
2672
|
+
const t2CountReady = t2Count >= config.tiers.tier2Trigger && usage >= tierCountUsageFloor;
|
|
2673
|
+
const t3CountReady = t3Count >= config.tiers.tier3Trigger && usage >= tierCountUsageFloor;
|
|
1713
2674
|
if (pressure) {
|
|
1714
2675
|
const candidates = [1];
|
|
1715
2676
|
if (config.tiers.enabled) {
|
|
1716
2677
|
candidates.push(2, 3);
|
|
1717
2678
|
}
|
|
1718
2679
|
let best = null;
|
|
1719
|
-
let bestPending = 0;
|
|
1720
2680
|
for (const t of candidates) {
|
|
1721
2681
|
const p = tiers[t]?.pending ?? 0;
|
|
1722
2682
|
if (p > bestPending) {
|
|
@@ -1724,7 +2684,7 @@ function decideNudge(input) {
|
|
|
1724
2684
|
best = t;
|
|
1725
2685
|
}
|
|
1726
2686
|
}
|
|
1727
|
-
if (best !== null && bestPending
|
|
2687
|
+
if (best !== null && bestPending >= minPressureBenefit) {
|
|
1728
2688
|
injectedTier = best;
|
|
1729
2689
|
const label = emergencyOverride ? "EMERGENCY" : "OVER-LIMIT";
|
|
1730
2690
|
injectedReason = best === 1 ? `${label} T1: max effective pending ${bestPending}, usage ${Math.round(usage * 100)}%` : `${label} T${best} distill: max pending ${bestPending} (T1 effective ${t1Eff}, T2 ${t2Pen}, T3 ${t3Pen}), usage ${Math.round(usage * 100)}%`;
|
|
@@ -1733,46 +2693,74 @@ function decideNudge(input) {
|
|
|
1733
2693
|
if (t1Eff >= nudgeGrowthTokens) {
|
|
1734
2694
|
injectedTier = 1;
|
|
1735
2695
|
injectedReason = `T1 effective ${t1Eff} >= ${nudgeGrowthTokens}, growth ${growthSinceReference}, usage ${Math.round(usage * 100)}%`;
|
|
1736
|
-
} else if (config.tiers.enabled && t2Pen >= tier2Threshold && t2Pen > t1Eff) {
|
|
2696
|
+
} else if (config.tiers.enabled && (t2CountReady || t2Pen >= tier2Threshold && t2Pen > t1Eff)) {
|
|
1737
2697
|
const lastShown = state.nudge.lastShownByTier[2] ?? 0;
|
|
1738
2698
|
const cadenceMet = lastShown === 0 || tokenCount - lastShown >= growthFloor;
|
|
1739
2699
|
if (cadenceMet) {
|
|
1740
2700
|
injectedTier = 2;
|
|
1741
|
-
injectedReason = `T2 distill ready: ${tiers[2].targetBlocks.length} tier-1 blocks (${t2Pen} tokens) >= ${tier2Threshold} (1.5x) and > T1 effective ${t1Eff}, usage ${Math.round(usage * 100)}%`;
|
|
2701
|
+
injectedReason = t2CountReady ? `T2 distill ready: ${t2Count} tier-1 blocks >= tier2Trigger ${config.tiers.tier2Trigger} (${t2Pen} tokens), usage ${Math.round(usage * 100)}%` : `T2 distill ready: ${tiers[2].targetBlocks.length} tier-1 blocks (${t2Pen} tokens) >= ${tier2Threshold} (1.5x) and > T1 effective ${t1Eff}, usage ${Math.round(usage * 100)}%`;
|
|
1742
2702
|
}
|
|
1743
|
-
} else if (config.tiers.enabled && t3Pen >= tier2Threshold && t3Pen > t2Pen && t3Pen > t1Eff) {
|
|
2703
|
+
} else if (config.tiers.enabled && (t3CountReady || t3Pen >= tier2Threshold && t3Pen > t2Pen && t3Pen > t1Eff)) {
|
|
1744
2704
|
const lastShown = state.nudge.lastShownByTier[3] ?? 0;
|
|
1745
2705
|
const cadenceMet = lastShown === 0 || tokenCount - lastShown >= growthFloor;
|
|
1746
2706
|
if (cadenceMet) {
|
|
1747
2707
|
injectedTier = 3;
|
|
1748
|
-
injectedReason = `T3 condense ready: ${tiers[3].targetBlocks.length} tier-2 blocks (${t3Pen} tokens) >= ${tier2Threshold} (1.5x) and > T2 ${t2Pen} and > T1 effective ${t1Eff}, usage ${Math.round(usage * 100)}%`;
|
|
2708
|
+
injectedReason = t3CountReady ? `T3 condense ready: ${t3Count} tier-2 blocks >= tier3Trigger ${config.tiers.tier3Trigger} (${t3Pen} tokens), usage ${Math.round(usage * 100)}%` : `T3 condense ready: ${tiers[3].targetBlocks.length} tier-2 blocks (${t3Pen} tokens) >= ${tier2Threshold} (1.5x) and > T2 ${t2Pen} and > T1 effective ${t1Eff}, usage ${Math.round(usage * 100)}%`;
|
|
1749
2709
|
}
|
|
1750
2710
|
}
|
|
1751
2711
|
}
|
|
1752
2712
|
const shouldInject = injectedTier !== null;
|
|
2713
|
+
if (shouldInject && firstSightMassReady) {
|
|
2714
|
+
injectedReason += " [first-sight mass]";
|
|
2715
|
+
}
|
|
1753
2716
|
let reason;
|
|
1754
2717
|
if (injectedTier !== null) {
|
|
1755
2718
|
reason = injectedReason;
|
|
1756
2719
|
} else if (pressure) {
|
|
1757
2720
|
const label = emergencyOverride ? "EMERGENCY" : "OVER-LIMIT";
|
|
1758
|
-
reason = `${label}: usage ${Math.round(usage * 100)}% but no tier has effective compressible content (T1 effective ${t1Eff}, T2 ${t2Pen}, T3 ${t3Pen}) \u2014 nudge suppressed to avoid offering ranges below minCompressRange`;
|
|
2721
|
+
reason = bestPending === 0 ? `${label}: usage ${Math.round(usage * 100)}% but no tier has effective compressible content (T1 effective ${t1Eff}, T2 ${t2Pen}, T3 ${t3Pen}) \u2014 nudge suppressed to avoid offering ranges below minCompressRange` : `${label}: usage ${Math.round(usage * 100)}% but max pending ${bestPending} < min benefit ${minPressureBenefit} tokens (T1 effective ${t1Eff}, T2 ${t2Pen}, T3 ${t3Pen}) \u2014 suppressed: rewriting below the benefit floor reclaims almost nothing while usage stays high; truncate.threshold remains the safety valve`;
|
|
1759
2722
|
} else {
|
|
1760
2723
|
const tiersList = [1, 2, 3];
|
|
1761
2724
|
const eligible = tiersList.filter((t) => config.tiers.enabled || t === 1);
|
|
2725
|
+
const countReadyUngated = (t) => t === 2 ? t2Count >= config.tiers.tier2Trigger : t === 3 ? t3Count >= config.tiers.tier3Trigger : false;
|
|
2726
|
+
const countReady = (t) => countReadyUngated(t) && usage >= tierCountUsageFloor;
|
|
1762
2727
|
const ready = eligible.filter((t) => (tiers[t]?.pending ?? 0) >= nudgeGrowthTokens).map((t) => `T${t} ${tiers[t].pending}`);
|
|
1763
|
-
const
|
|
1764
|
-
|
|
2728
|
+
const readyCount = eligible.filter(
|
|
2729
|
+
(t) => (tiers[t]?.pending ?? 0) < nudgeGrowthTokens && countReadyUngated(t)
|
|
2730
|
+
).map(
|
|
2731
|
+
(t) => `T${t} ${t === 2 ? t2Count : t3Count} blocks (count${usage >= tierCountUsageFloor ? "" : ", usage-gated"})`
|
|
2732
|
+
);
|
|
2733
|
+
const readyAll = [...ready, ...readyCount];
|
|
2734
|
+
const readyHint = readyAll.length > 0 ? `, ready: ${readyAll.join(", ")}` : "";
|
|
2735
|
+
const blocked = eligible.filter(
|
|
2736
|
+
(t) => ((tiers[t]?.pending ?? 0) >= nudgeGrowthTokens || countReady(t)) && (state.nudge.lastShownByTier[t] ?? 0) > 0 && tokenCount - (state.nudge.lastShownByTier[t] ?? 0) < growthFloor
|
|
2737
|
+
).map((t) => `T${t} (cadence)`);
|
|
1765
2738
|
const blockedHint = blocked.length > 0 ? `, blocked: ${blocked.join(", ")}` : "";
|
|
1766
|
-
const maxPending = Math.max(
|
|
2739
|
+
const maxPending = Math.max(
|
|
2740
|
+
0,
|
|
2741
|
+
...Object.values(tiers).map((t) => t.pending)
|
|
2742
|
+
);
|
|
1767
2743
|
const pendingShort = maxPending < nudgeGrowthTokens;
|
|
1768
2744
|
const growthShort = growthSinceReference < growthFloor;
|
|
1769
2745
|
const parts = [];
|
|
1770
|
-
if (pendingShort)
|
|
1771
|
-
|
|
1772
|
-
|
|
2746
|
+
if (pendingShort)
|
|
2747
|
+
parts.push(
|
|
2748
|
+
`max compressible ${maxPending} < threshold ${nudgeGrowthTokens}`
|
|
2749
|
+
);
|
|
2750
|
+
if (growthShort)
|
|
2751
|
+
parts.push(`growth ${growthSinceReference} < floor ${growthFloor}`);
|
|
2752
|
+
if (parts.length === 0)
|
|
2753
|
+
parts.push(
|
|
2754
|
+
`max compressible ${maxPending}, growth ${growthSinceReference}`
|
|
2755
|
+
);
|
|
1773
2756
|
reason = `${parts.join("; ")}${readyHint}${blockedHint}`;
|
|
1774
2757
|
}
|
|
1775
|
-
const ctxBreakdown = computeContextBreakdown(
|
|
2758
|
+
const ctxBreakdown = computeContextBreakdown(
|
|
2759
|
+
input.messages,
|
|
2760
|
+
tokenCount,
|
|
2761
|
+
growthSinceReference,
|
|
2762
|
+
countTokens
|
|
2763
|
+
);
|
|
1776
2764
|
return {
|
|
1777
2765
|
shouldInject,
|
|
1778
2766
|
reason,
|
|
@@ -1791,6 +2779,7 @@ function decideNudge(input) {
|
|
|
1791
2779
|
hasPendingNudge: hasPendingNudge ? 1 : 0,
|
|
1792
2780
|
overLimit: overLimit ? 1 : 0,
|
|
1793
2781
|
emergencyOverride: emergencyOverride ? 1 : 0,
|
|
2782
|
+
minPressureBenefit,
|
|
1794
2783
|
pendingT1: tiers[1].pending,
|
|
1795
2784
|
pendingT2: tiers[2].pending,
|
|
1796
2785
|
pendingT3: tiers[3].pending
|
|
@@ -1802,358 +2791,70 @@ function computeContextBreakdown(messages, total, growth, countTokens) {
|
|
|
1802
2791
|
const count = countTokens ?? ((t) => Math.ceil(t.length / 4));
|
|
1803
2792
|
let system = 0, tool = 0, summaries = 0, code = 0, text = 0;
|
|
1804
2793
|
for (const msg of messages) {
|
|
1805
|
-
const tokens =
|
|
2794
|
+
const tokens = countMessageTokens(msg, count);
|
|
1806
2795
|
if (msg.text?.startsWith("[Compressed conversation section]")) {
|
|
1807
2796
|
summaries += tokens;
|
|
1808
2797
|
} else if (msg.contentType === "tool-call" || msg.contentType === "tool-result") {
|
|
1809
|
-
tool += tokens;
|
|
1810
|
-
} else if (msg.role === "system") {
|
|
1811
|
-
system += tokens;
|
|
1812
|
-
} else if (msg.text?.includes("```")) {
|
|
1813
|
-
code += tokens;
|
|
1814
|
-
} else {
|
|
1815
|
-
text += tokens;
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
return { system, tool, summaries, code, text, total, growth };
|
|
1819
|
-
}
|
|
1820
|
-
function cloneState(state) {
|
|
1821
|
-
return {
|
|
1822
|
-
blocks: state.blocks.map((block) => ({
|
|
1823
|
-
...block,
|
|
1824
|
-
directMessageIds: [...block.directMessageIds],
|
|
1825
|
-
effectiveMessageIds: [...block.effectiveMessageIds],
|
|
1826
|
-
directBlockIds: [...block.directBlockIds]
|
|
1827
|
-
})),
|
|
1828
|
-
messageRefs: {
|
|
1829
|
-
byRaw: { ...state.messageRefs.byRaw },
|
|
1830
|
-
byRef: { ...state.messageRefs.byRef }
|
|
1831
|
-
},
|
|
1832
|
-
tokenSnapshot: { ...state.tokenSnapshot ?? {} },
|
|
1833
|
-
nudge: { ...state.nudge, anchors: { ...state.nudge.anchors } },
|
|
1834
|
-
stats: { ...state.stats },
|
|
1835
|
-
nextBlockId: state.nextBlockId,
|
|
1836
|
-
nextRunId: state.nextRunId
|
|
1837
|
-
};
|
|
1838
|
-
}
|
|
1839
|
-
function scoreRelevance(block, terms) {
|
|
1840
|
-
const topic = (block.topic ?? "").toLowerCase();
|
|
1841
|
-
const summary = block.summary.toLowerCase();
|
|
1842
|
-
let score = 0;
|
|
1843
|
-
for (const term of terms) {
|
|
1844
|
-
const topicHits = countOccurrences(topic, term);
|
|
1845
|
-
if (topicHits > 0) score += Math.min(topicHits * 0.15, 0.45);
|
|
1846
|
-
const summaryHits = countOccurrences(summary, term);
|
|
1847
|
-
if (summaryHits > 0) score += Math.min(summaryHits * 0.04, 0.2);
|
|
1848
|
-
}
|
|
1849
|
-
return Math.min(score, 1);
|
|
1850
|
-
}
|
|
1851
|
-
function countOccurrences(haystack, needle) {
|
|
1852
|
-
if (!haystack || !needle) return 0;
|
|
1853
|
-
let count = 0;
|
|
1854
|
-
let position = 0;
|
|
1855
|
-
while ((position = haystack.indexOf(needle, position)) !== -1) {
|
|
1856
|
-
count++;
|
|
1857
|
-
position += needle.length;
|
|
1858
|
-
}
|
|
1859
|
-
return count;
|
|
1860
|
-
}
|
|
1861
|
-
var COMPRESS_PHILOSOPHY = `Compression Philosophy:
|
|
1862
|
-
- All compression serves the primary task, but be frugal.
|
|
1863
|
-
- Context capacity is precious. Save context by compressing consumed outputs, not by avoiding tools.
|
|
1864
|
-
- Compress by need, not by percentage.
|
|
1865
|
-
- Work from summaries, not raw tool outputs. All listed ranges (user prompts, tool outputs, code, logs, exploration, intermediate steps) should be compressed to summary format \u2014 the ONLY exceptions are protected content, content the current step is actively using, or critical content you cannot reconstruct.`;
|
|
1866
|
-
var HOW_TO_COMPRESS_RULES = `HOW TO COMPRESS
|
|
1867
|
-
|
|
1868
|
-
When you call \`compress\`, the summary you write becomes the only record of the replaced conversation. Make it self-contained and complete: every user request, experiment purpose, and work task in the range must be accurately captured. A later reader (or you, after decompressing) should be able to continue the task WITHOUT needing the original.
|
|
1869
|
-
|
|
1870
|
-
KEEP VERBATIM \u2014 never paraphrase or abbreviate these:
|
|
1871
|
-
- Full file paths with line numbers, directory prefix on every mention (\`lib/hooks.ts:347\`, \`src/index.ts:12-18\`, \`gatenet_v3/model.py:45\`). Never abbreviate to a bare filename (\`hooks.ts\`, \`model.py\`) \u2014 they are ambiguous and cannot be grepped or decompressed-to later.
|
|
1872
|
-
- Function, class, and type signatures (exact names, params, return types) AND critical code lines that encode logic \u2014 the line that IS the finding, not just the function name (e.g. \`kv_keys += define_gate * a_key[i](emb)\` is more useful than "see model_kvnet.py").
|
|
1873
|
-
- Error messages and stack traces (exact text \u2014 you need the literal string to grep for it later).
|
|
1874
|
-
- Key details from reports and analyses \u2014 not just the conclusion. Keep the comparison numbers and the mechanism, not "X is worse" alone (write "1.76\xD7 PPL gap because KV store is static", not "KVNet underperforms").
|
|
1875
|
-
- Decisions and their rationale ("chose X over Y because Z" \u2014 the "because" is load-bearing; without it the decision looks arbitrary).
|
|
1876
|
-
- Constraints discovered ("must support Node 22", "no new dependencies", "AGENTS.md forbids \`as any\`").
|
|
1877
|
-
- Exact values: versions, config keys, thresholds, magic numbers.
|
|
1878
|
-
- User intent \u2014 quote short user messages verbatim. When the message is too long to quote, preserve intent with extra care: do not change scope, constraints, priorities, acceptance criteria, or requested outcomes. Mark them clearly as past quotes (e.g., "User said: ..."), not as current directives. Losing these changes the task itself.
|
|
1879
|
-
- The user's overall goal and any changes to it \u2014 the big-picture objective plus how it evolved during the compressed range. Each summary must reflect the goal as it stood at the end of the range, including pivots (e.g., "initially: fix bug X \u2192 pivoted to: refactor module Y after discovering root cause"). Losing the goal or its evolution makes all subsequent work appear unmotivated.
|
|
1880
|
-
- Purpose behind each significant action \u2014 preserve not just what was done but why: the hypothesis behind each experiment, the question behind each exploration, the task goal behind each work action. Without purpose, the summary reads as disconnected technical steps with no through-line.
|
|
1881
|
-
- Open questions and unresolved TODOs \u2014 losing these changes what work appears to remain.
|
|
1882
|
-
- Message refs of key anchors (\`m00420\`, \`m00510\u2013m00520\`) \u2014 they let you or a later reader jump back via decompress to the exact original.
|
|
1883
|
-
|
|
1884
|
-
DROP \u2014 extract the signal, discard the vessel:
|
|
1885
|
-
- Verbose logs (build/test/\`npm\` output) once you have captured the error line or the result.
|
|
1886
|
-
- Duplicate file reads once the needed content is recorded.
|
|
1887
|
-
- Consumed exploration \u2014 search hits, agent return values, successful tool outputs \u2014 once you have extracted the facts you need (same rule as dead-ends, but nothing went wrong; the content is simply spent).
|
|
1888
|
-
- Dead-end exploration \u2014 but PRESERVE the lesson in one line: "tried X, failed because Y".
|
|
1889
|
-
- Back-and-forth discussion and self-corrections once the final position is captured (keep the outcome, drop the journey to it).
|
|
1890
|
-
- Repeated status checks (\`git status\`, \`ls\`) once state is known.
|
|
1891
|
-
|
|
1892
|
-
For each significant item you DROP (scripts, reports, large analyses, long tool outputs), add a one-line CONTENT description of what it covers \u2014 not where it lives. Bad: "probe script at /path/probe_kvnet.py". Good: "probe_kvnet.py: tests n-gram baseline, generation quality, long-range dependency, position sensitivity, op pipeline, QUERY attention." This lets a later decompress target the right block by relevance, not by guessing locations.
|
|
1893
|
-
|
|
1894
|
-
PRIORITY \u2014 when the summary must be compact, preserve in this order:
|
|
1895
|
-
1. User's overall goal, goal evolution, intent, and hard constraints (losing these changes the task).
|
|
1896
|
-
2. Decisions and rationale.
|
|
1897
|
-
3. Exact technical artifacts: paths, signatures, errors, values.
|
|
1898
|
-
4. Conclusions and key findings.
|
|
1899
|
-
5. Lessons learned: what failed and why.
|
|
1900
|
-
|
|
1901
|
-
Write dense, scannable bullets \u2014 not narrative prose. If the range spans distinct concerns (request \u2192 findings \u2192 decision), group bullets under short thematic headers so a reader can scan to the part they need. Every line must earn its place. Do not mimic the style of existing summaries in context; follow these rules.`;
|
|
1902
|
-
var TIER2_DISTILL_RULES = `TIER 2 COMPRESSION \u2014 DISTILLATION
|
|
1903
|
-
|
|
1904
|
-
You are compressing historical summaries (not raw conversation). These summaries have already captured the details. Your job is to DISTILL them: extract only what matters for future work, discard the process.
|
|
1905
|
-
|
|
1906
|
-
KEEP \u2014 these are the only things that survive distillation:
|
|
1907
|
-
- Decisions and their rationale ("chose X over Y because Z" \u2014 the "because" is load-bearing).
|
|
1908
|
-
- Final outcomes: version numbers shipped, PR numbers merged/closed, bugs fixed or deferred.
|
|
1909
|
-
- Key lessons: what failed and why ("tried X, failed because Y"). These prevent repeating mistakes.
|
|
1910
|
-
- Critical constraints discovered ("must support Node 22", "AGENTS.md forbids as any").
|
|
1911
|
-
- Design decisions with architectural impact ("chose compress-as-anchor over synthetic messages because prefix cache").
|
|
1912
|
-
- Whether content is OBSOLETE or SUPERSEDED \u2014 mark with one line: "[SUPERSEDED by PR #NNN]" or "[OBSOLETE: deleted in vX.Y.Z]". Do NOT keep the obsolete content's details \u2014 just the marker and reason.
|
|
1913
|
-
- Function/class/type names and module paths that are the SUBJECT of the work \u2014 e.g., "fixed filterCompressedRanges in prune.ts", "added SessionStateRegistry in state.ts". Not exact line numbers or full signatures \u2014 just enough to LOCATE the code without searching.
|
|
1914
|
-
- Exploration findings: if a block was exploratory with no decision, keep the CONCLUSION in one line ("explored X, not viable because Y"). Do not keep the exploration process.
|
|
1915
|
-
|
|
1916
|
-
DROP \u2014 these were useful during the work but are no longer needed:
|
|
1917
|
-
- Exact line numbers, diffs, verbose function signatures, full code listings.
|
|
1918
|
-
- Build/deploy process details, test execution steps.
|
|
1919
|
-
- Review process details (who reviewed, what rounds, test counts).
|
|
1920
|
-
- Verbose logs, command output, intermediate debugging steps.
|
|
1921
|
-
|
|
1922
|
-
FORMAT:
|
|
1923
|
-
- Start each distilled block with a source header line:
|
|
1924
|
-
\`Source: bN+bM+... (XK\u2192YK tok, Zx). [original topic]\`
|
|
1925
|
-
Example: \`Source: b5+b7 (56K+44K\u2192268 tok, 375x). [Tool-result recap + publish]\`
|
|
1926
|
-
- 3-5 bullet points per source block, each a self-contained fact.
|
|
1927
|
-
- Dense, scannable \u2014 no narrative prose.
|
|
1928
|
-
- Start with the outcome, not the process: "v1.13.0 shipped (7 PRs bundled)" not "implemented 7 PRs then reviewed then merged".
|
|
1929
|
-
- Cross-block synthesis: if multiple source blocks cover the same topic (same PR, same feature, same bug), MERGE them into a single group of bullets. Do not repeat the same fact from different blocks \u2014 keep it once under the most relevant source header.
|
|
1930
|
-
|
|
1931
|
-
SIZE TARGET: 50-150 tokens per source block (excluding the header). If you can't fit it in 150 tokens, you're keeping too much process. If a block has nothing worth keeping (pure noise), output just the header followed by "[no actionable content]."`;
|
|
1932
|
-
var TIER3_CONDENSE_RULES = `TIER 3 COMPRESSION \u2014 ULTRA-CONDENSATION
|
|
1933
|
-
|
|
1934
|
-
You are compressing distilled summaries (Tier 2) into ultra-condensed facts (Tier 3). The distilled summaries already contain only decisions and outcomes. Your job is to reduce them to bare factual references.
|
|
1935
|
-
|
|
1936
|
-
PRIORITY \u2014 when a source block has more facts than the size target allows, keep in this order:
|
|
1937
|
-
1. Shipped outcomes (versions released, PRs merged) \u2014 these are permanent record.
|
|
1938
|
-
2. Open work (PRs/issues still pending) \u2014 these may need follow-up.
|
|
1939
|
-
3. Key decisions with architectural impact ("chose X over Y because Z").
|
|
1940
|
-
4. Critical constraints ("must support Node 22").
|
|
1941
|
-
Drop everything else. Tier 3 is a lookup index, not a knowledge base.
|
|
1942
|
-
|
|
1943
|
-
FORMAT:
|
|
1944
|
-
- Start with a source header line:
|
|
1945
|
-
\`Source: bN+bM+... (XK\u2192YK tok, Zx). [original topic]\`
|
|
1946
|
-
- Output 1-3 facts per source block. Each fact is a single line: subject + outcome.
|
|
1947
|
-
- No explanations, no rationale, no process \u2014 just the fact.
|
|
1948
|
-
- Format: "[PR/Issue/Version] \u2014 [outcome in \u22648 words]"
|
|
1949
|
-
- Merge related facts from different source blocks if they concern the same topic.
|
|
1950
|
-
|
|
1951
|
-
EXAMPLES:
|
|
1952
|
-
- "v1.13.0 shipped \u2014 quality gate + GC fix (7 PRs)"
|
|
1953
|
-
- "PR #196 merged \u2014 preserve-first-user (supersedes #169)"
|
|
1954
|
-
- "Bug 1214 fixed \u2014 compress consumed all user messages"
|
|
1955
|
-
- "Chose compress-as-anchor \u2014 prefix cache benefit over synthetic injection"
|
|
1956
|
-
- "Constraint: AGENTS.md forbids as any \u2014 never suppress types"
|
|
1957
|
-
|
|
1958
|
-
DROP:
|
|
1959
|
-
- Multi-sentence context. If a fact needs >1 sentence, it's too detailed for Tier 3.
|
|
1960
|
-
- Lessons learned ("tried X, failed because Y") \u2014 drop UNLESS the failure is likely to recur and the block is <30 days old.
|
|
1961
|
-
- Design rationale details \u2014 keep the decision, drop the "because" unless it's a critical constraint.
|
|
1962
|
-
- Anything marked [OBSOLETE] or [SUPERSEDED] \u2014 drop entirely, note "[N blocks obsolete]" in the summary.
|
|
1963
|
-
|
|
1964
|
-
SIZE TARGET: 30-60 tokens per source block (including header). For a batch of N source blocks, total output \u2248 N \xD7 40 tokens. If a source block has only one trivial fact, output just the header + one line.`;
|
|
1965
|
-
var defaultPrompts = Object.freeze({
|
|
1966
|
-
compressPhilosophy: COMPRESS_PHILOSOPHY,
|
|
1967
|
-
howToCompressRules: HOW_TO_COMPRESS_RULES,
|
|
1968
|
-
tier2DistillRules: TIER2_DISTILL_RULES,
|
|
1969
|
-
tier3CondenseRules: TIER3_CONDENSE_RULES
|
|
1970
|
-
});
|
|
1971
|
-
function efficiencyNote(prompts) {
|
|
1972
|
-
return `This is an efficiency nudge to compress early and keep context lean \u2014 not an overflow warning. A separate, stronger alert will appear if the context is actually full.
|
|
1973
|
-
|
|
1974
|
-
${prompts.compressPhilosophy}`;
|
|
1975
|
-
}
|
|
1976
|
-
function emergencyHeader(prompts) {
|
|
1977
|
-
return `\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.
|
|
1978
|
-
|
|
1979
|
-
${prompts.compressPhilosophy}`;
|
|
1980
|
-
}
|
|
1981
|
-
function formatK(n) {
|
|
1982
|
-
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
1983
|
-
return `${n}`;
|
|
1984
|
-
}
|
|
1985
|
-
function formatBreakdown(bd) {
|
|
1986
|
-
if (!bd) return "";
|
|
1987
|
-
const parts = [];
|
|
1988
|
-
if (bd.system > 0) parts.push(`${formatK(bd.system)} system`);
|
|
1989
|
-
if (bd.tool > 0) parts.push(`${formatK(bd.tool)} tool`);
|
|
1990
|
-
if (bd.summaries > 0) parts.push(`${formatK(bd.summaries)} summaries`);
|
|
1991
|
-
if (bd.code > 0) parts.push(`${formatK(bd.code)} code`);
|
|
1992
|
-
if (bd.text > 0) parts.push(`${formatK(bd.text)} text`);
|
|
1993
|
-
const growth = bd.growth > 0 ? `
|
|
1994
|
-
+${formatK(bd.growth)} since last nudge` : "";
|
|
1995
|
-
return `Context breakdown: ${parts.join(" | ")}${growth}`;
|
|
1996
|
-
}
|
|
1997
|
-
function formatTierTargetBlocks(blocks) {
|
|
1998
|
-
if (blocks.length === 0) {
|
|
1999
|
-
return "Target blocks: (none \u2014 no tier blocks found)";
|
|
2000
|
-
}
|
|
2001
|
-
const lines = blocks.map((b) => {
|
|
2002
|
-
const summaryTokens = Math.ceil((b.summary ?? "").length / 4);
|
|
2003
|
-
const topic = b.topic ? ` "${b.topic}"` : "";
|
|
2004
|
-
return ` ${b.blockId} ${b.effectiveMessageIds.length} msgs ${formatK(b.compressedTokens)}\u2192${formatK(summaryTokens)}${topic}`;
|
|
2005
|
-
});
|
|
2006
|
-
return `Target ${blocks[0].tier === 1 ? "tier-1" : "tier-2"} blocks to distill (${blocks.length}):
|
|
2007
|
-
${lines.join("\n")}`;
|
|
2008
|
-
}
|
|
2009
|
-
function formatRanges(compressible, protectedRanges) {
|
|
2010
|
-
if (compressible.length === 0 && protectedRanges.length === 0) {
|
|
2011
|
-
return "[No specific ranges detected \u2014 compress any consumed content.]";
|
|
2012
|
-
}
|
|
2013
|
-
const refNum2 = (ref) => {
|
|
2014
|
-
const m = ref.match(/\d+/);
|
|
2015
|
-
return m ? parseInt(m[0], 10) : 0;
|
|
2016
|
-
};
|
|
2017
|
-
const entries = [];
|
|
2018
|
-
for (const r of compressible) {
|
|
2019
|
-
entries.push({
|
|
2020
|
-
startRef: r.startRef,
|
|
2021
|
-
endRef: r.endRef,
|
|
2022
|
-
startNum: refNum2(r.startRef),
|
|
2023
|
-
endNum: refNum2(r.endRef),
|
|
2024
|
-
count: r.count,
|
|
2025
|
-
tokens: r.tokens,
|
|
2026
|
-
toolPct: r.toolPct,
|
|
2027
|
-
textPct: r.textPct,
|
|
2028
|
-
compressibleTokens: r.tokens,
|
|
2029
|
-
compressibleCount: r.count,
|
|
2030
|
-
protectedTokens: 0,
|
|
2031
|
-
protectedCount: 0,
|
|
2032
|
-
protectedTools: [],
|
|
2033
|
-
dangerous: r.dangerous ?? false
|
|
2034
|
-
});
|
|
2035
|
-
}
|
|
2036
|
-
for (const r of protectedRanges) {
|
|
2037
|
-
entries.push({
|
|
2038
|
-
startRef: r.startRef,
|
|
2039
|
-
endRef: r.endRef,
|
|
2040
|
-
startNum: refNum2(r.startRef),
|
|
2041
|
-
endNum: refNum2(r.endRef),
|
|
2042
|
-
count: r.count,
|
|
2043
|
-
tokens: r.tokens,
|
|
2044
|
-
toolPct: 0,
|
|
2045
|
-
textPct: 0,
|
|
2046
|
-
compressibleTokens: 0,
|
|
2047
|
-
compressibleCount: 0,
|
|
2048
|
-
protectedTokens: r.tokens,
|
|
2049
|
-
protectedCount: r.count,
|
|
2050
|
-
protectedTools: [...r.tools],
|
|
2051
|
-
dangerous: false
|
|
2052
|
-
});
|
|
2053
|
-
}
|
|
2054
|
-
entries.sort((a, b) => a.startNum - b.startNum);
|
|
2055
|
-
const merged = [];
|
|
2056
|
-
for (const e of entries) {
|
|
2057
|
-
const last = merged[merged.length - 1];
|
|
2058
|
-
if (last && e.startNum <= last.endNum + 1) {
|
|
2059
|
-
last.endRef = e.endRef;
|
|
2060
|
-
last.endNum = Math.max(last.endNum, e.endNum);
|
|
2061
|
-
last.count += e.count;
|
|
2062
|
-
last.tokens += e.tokens;
|
|
2063
|
-
last.compressibleTokens += e.compressibleTokens;
|
|
2064
|
-
last.compressibleCount += e.compressibleCount;
|
|
2065
|
-
last.protectedTokens += e.protectedTokens;
|
|
2066
|
-
last.protectedCount += e.protectedCount;
|
|
2067
|
-
if (e.dangerous) last.dangerous = true;
|
|
2068
|
-
for (const t of e.protectedTools) {
|
|
2069
|
-
if (!last.protectedTools.includes(t)) last.protectedTools.push(t);
|
|
2070
|
-
}
|
|
2798
|
+
tool += tokens;
|
|
2799
|
+
} else if (msg.role === "system") {
|
|
2800
|
+
system += tokens;
|
|
2801
|
+
} else if (msg.text?.includes("```")) {
|
|
2802
|
+
code += tokens;
|
|
2071
2803
|
} else {
|
|
2072
|
-
|
|
2804
|
+
text += tokens;
|
|
2073
2805
|
}
|
|
2074
2806
|
}
|
|
2075
|
-
|
|
2076
|
-
const suffix = e.dangerous && e.compressibleTokens > 0 ? " \u26A0\uFE0F NOT recommended unless you are certain." : "";
|
|
2077
|
-
if (e.protectedTokens > 0 && e.compressibleTokens === 0) {
|
|
2078
|
-
return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${formatK(e.tokens)} [PROTECTED: ${e.protectedTools.join(", ")} \u2014 not compressible]${suffix}`;
|
|
2079
|
-
}
|
|
2080
|
-
if (e.protectedTokens > 0 && e.compressibleTokens > 0) {
|
|
2081
|
-
return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${formatK(e.tokens)} [${formatK(e.compressibleTokens)} compressible | ${formatK(e.protectedTokens)} protected: ${e.protectedTools.join(", ")}]${suffix}`;
|
|
2082
|
-
}
|
|
2083
|
-
return ` ${e.startRef}\u2013${e.endRef} ${e.count} msgs ${formatK(e.tokens)} [tool ${e.toolPct}% | text ${e.textPct}%]${suffix}`;
|
|
2084
|
-
});
|
|
2085
|
-
return `Compressible ranges (${merged.length}, oldest first):
|
|
2086
|
-
${lines.join("\n")}`;
|
|
2807
|
+
return { system, tool, summaries, code, text, total, growth };
|
|
2087
2808
|
}
|
|
2088
|
-
function
|
|
2089
|
-
const breakdownStr = formatBreakdown(decision.contextBreakdown);
|
|
2090
|
-
const rangesStr = formatRanges(decision.compressibleRanges, decision.protectedRanges ?? []);
|
|
2091
|
-
const isEmergency = !!decision.breakdown?.emergencyOverride || !!decision.breakdown?.overLimit;
|
|
2092
|
-
if (decision.tier !== null && decision.tier >= 2) {
|
|
2093
|
-
const isT2 = decision.tier === 2;
|
|
2094
|
-
const targets = decision.tierTargetBlocks ?? [];
|
|
2095
|
-
const blockList = formatTierTargetBlocks(targets);
|
|
2096
|
-
const startId = targets[0]?.blockId ?? "b1";
|
|
2097
|
-
const endId = targets[targets.length - 1]?.blockId ?? "b5";
|
|
2098
|
-
const voice = isEmergency ? "emergency" : "gentle";
|
|
2099
|
-
const triggerLine = isEmergency ? `[EMERGENCY \u2014 TIER ${decision.tier} ${isT2 ? "DISTILLATION" : "CONDENSATION"}] Context limit reached \u2014 distill NOW into a denser summary to reclaim tokens.` : `[TIER ${decision.tier} ${isT2 ? "DISTILLATION" : "CONDENSATION"} TRIGGER]`;
|
|
2100
|
-
return {
|
|
2101
|
-
voice,
|
|
2102
|
-
text: [
|
|
2103
|
-
efficiencyNote(prompts),
|
|
2104
|
-
"",
|
|
2105
|
-
breakdownStr,
|
|
2106
|
-
"",
|
|
2107
|
-
triggerLine,
|
|
2108
|
-
isT2 ? `Your tier-1 compression summaries have accumulated. Distill them into a single denser tier-2 summary. Use block IDs as boundaries (startId and endId as bN). Any raw (uncompressed) messages sitting between the boundary blocks are absorbed into the tier-2 block as well \u2014 apply HOW TO COMPRESS to those raw messages and the TIER 2 distillation rules to the existing summaries, so the whole span is covered and nothing is lost.` : `Your tier-2 compression summaries have accumulated. Condense them further into a tier-3 ultra-condensed summary. Use block IDs as boundaries (startId and endId as bN). Any raw (uncompressed) messages sitting between the boundary blocks are absorbed into the tier-3 block as well \u2014 apply HOW TO COMPRESS to those raw messages and the TIER 3 condensation rules to the existing summaries, so the whole span is covered and nothing is lost.`,
|
|
2109
|
-
blockList,
|
|
2110
|
-
`Example: compress({ content: [{ startId: "${startId}", endId: "${endId}", summary: "..." }] })`,
|
|
2111
|
-
"",
|
|
2112
|
-
prompts.howToCompressRules,
|
|
2113
|
-
"",
|
|
2114
|
-
isT2 ? prompts.tier2DistillRules : prompts.tier3CondenseRules
|
|
2115
|
-
].join("\n")
|
|
2116
|
-
};
|
|
2117
|
-
}
|
|
2118
|
-
if (isEmergency) {
|
|
2119
|
-
return {
|
|
2120
|
-
voice: "emergency",
|
|
2121
|
-
text: [
|
|
2122
|
-
emergencyHeader(prompts),
|
|
2123
|
-
"",
|
|
2124
|
-
breakdownStr,
|
|
2125
|
-
"",
|
|
2126
|
-
prompts.howToCompressRules,
|
|
2127
|
-
"",
|
|
2128
|
-
`{ "topic": "...", "content": [{ "startId": "<ID>", "endId": "<ID>", "summary": "..." }] }`,
|
|
2129
|
-
"Only use IDs from visible messages above. Compress older work first.",
|
|
2130
|
-
"",
|
|
2131
|
-
rangesStr
|
|
2132
|
-
].join("\n")
|
|
2133
|
-
};
|
|
2134
|
-
}
|
|
2809
|
+
function cloneState(state) {
|
|
2135
2810
|
return {
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2811
|
+
blocks: state.blocks.map((block) => ({
|
|
2812
|
+
...block,
|
|
2813
|
+
directMessageIds: [...block.directMessageIds],
|
|
2814
|
+
effectiveMessageIds: [...block.effectiveMessageIds],
|
|
2815
|
+
directBlockIds: [...block.directBlockIds]
|
|
2816
|
+
})),
|
|
2817
|
+
messageRefs: {
|
|
2818
|
+
byRaw: { ...state.messageRefs.byRaw },
|
|
2819
|
+
byRef: { ...state.messageRefs.byRef }
|
|
2820
|
+
},
|
|
2821
|
+
tokenSnapshot: { ...state.tokenSnapshot ?? {} },
|
|
2822
|
+
nudge: { ...state.nudge, anchors: { ...state.nudge.anchors } },
|
|
2823
|
+
stats: { ...state.stats },
|
|
2824
|
+
absorbed: (state.absorbed ?? []).map((record) => ({ ...record })),
|
|
2825
|
+
nextBlockId: state.nextBlockId,
|
|
2826
|
+
nextRunId: state.nextRunId
|
|
2148
2827
|
};
|
|
2149
2828
|
}
|
|
2829
|
+
function scoreRelevance(block, terms) {
|
|
2830
|
+
const topic = (block.topic ?? "").toLowerCase();
|
|
2831
|
+
const summary = block.summary.toLowerCase();
|
|
2832
|
+
let score = 0;
|
|
2833
|
+
for (const term of terms) {
|
|
2834
|
+
const topicHits = countOccurrences(topic, term);
|
|
2835
|
+
if (topicHits > 0) score += Math.min(topicHits * 0.15, 0.45);
|
|
2836
|
+
const summaryHits = countOccurrences(summary, term);
|
|
2837
|
+
if (summaryHits > 0) score += Math.min(summaryHits * 0.04, 0.2);
|
|
2838
|
+
}
|
|
2839
|
+
return Math.min(score, 1);
|
|
2840
|
+
}
|
|
2841
|
+
function countOccurrences(haystack, needle) {
|
|
2842
|
+
if (!haystack || !needle) return 0;
|
|
2843
|
+
let count = 0;
|
|
2844
|
+
let position = 0;
|
|
2845
|
+
while ((position = haystack.indexOf(needle, position)) !== -1) {
|
|
2846
|
+
count++;
|
|
2847
|
+
position += needle.length;
|
|
2848
|
+
}
|
|
2849
|
+
return count;
|
|
2850
|
+
}
|
|
2150
2851
|
function formatTokens2(n) {
|
|
2151
2852
|
if (!Number.isFinite(n) || n <= 0) return "0";
|
|
2152
2853
|
return n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
2153
2854
|
}
|
|
2154
2855
|
function pct(n, total) {
|
|
2155
2856
|
if (n <= 0 || total <= 0) return 0;
|
|
2156
|
-
return Math.
|
|
2857
|
+
return Math.round(n / total * 100);
|
|
2157
2858
|
}
|
|
2158
2859
|
function numericPart2(blockId) {
|
|
2159
2860
|
const match = /^b(\d+)$/.exec(blockId);
|
|
@@ -2192,12 +2893,18 @@ function collectVisible(messages, state, countTokens) {
|
|
|
2192
2893
|
if (block.active) summaryTokens += summaryTokensOf(block, countTokens);
|
|
2193
2894
|
}
|
|
2194
2895
|
const visible = [];
|
|
2896
|
+
const toolCallNames = /* @__PURE__ */ new Map();
|
|
2897
|
+
for (const message of messages) {
|
|
2898
|
+
if (message.contentType === "tool-call" && message.toolCallId && message.toolName) {
|
|
2899
|
+
toolCallNames.set(message.toolCallId, message.toolName);
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2195
2902
|
messages.forEach((message, index) => {
|
|
2196
2903
|
if (coveredIds.has(message.id)) return;
|
|
2197
2904
|
const ref = refForRaw(state.messageRefs, message.id);
|
|
2198
2905
|
if (!ref) return;
|
|
2199
|
-
const tokens =
|
|
2200
|
-
const tool = message.toolName ?? "text";
|
|
2906
|
+
const tokens = countMessageTokens(message, countTokens);
|
|
2907
|
+
const tool = isToolMessage(message) ? message.toolName ?? (message.toolCallId ? toolCallNames.get(message.toolCallId) : void 0) ?? "tool" : "text";
|
|
2201
2908
|
if (tokens > 0) visible.push({ ref, tokens, tool, index });
|
|
2202
2909
|
});
|
|
2203
2910
|
return { visible, summaryTokens };
|
|
@@ -2281,25 +2988,38 @@ function renderUncompressedRanges(visible) {
|
|
|
2281
2988
|
lines.push(" (no uncompressed messages)");
|
|
2282
2989
|
return lines.join("\n");
|
|
2283
2990
|
}
|
|
2284
|
-
const
|
|
2991
|
+
const refNum = (ref) => {
|
|
2285
2992
|
const m = ref.match(/\d+/);
|
|
2286
2993
|
return m ? parseInt(m[0], 10) : 0;
|
|
2287
2994
|
};
|
|
2995
|
+
const dominantTool = (toolTokens) => {
|
|
2996
|
+
let best = "text";
|
|
2997
|
+
let bestN = -1;
|
|
2998
|
+
for (const [tool, n] of toolTokens) {
|
|
2999
|
+
if (n > bestN) {
|
|
3000
|
+
best = tool;
|
|
3001
|
+
bestN = n;
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
return best;
|
|
3005
|
+
};
|
|
2288
3006
|
const merged = [];
|
|
2289
3007
|
for (const m of visible) {
|
|
2290
|
-
const num =
|
|
3008
|
+
const num = refNum(m.ref);
|
|
2291
3009
|
const last = merged[merged.length - 1];
|
|
2292
3010
|
if (last && num === last.startNum + last.count) {
|
|
2293
3011
|
last.endRef = m.ref;
|
|
2294
3012
|
last.count += 1;
|
|
2295
3013
|
last.tokens += m.tokens;
|
|
3014
|
+
last.toolTokens.set(m.tool, (last.toolTokens.get(m.tool) ?? 0) + m.tokens);
|
|
2296
3015
|
} else {
|
|
2297
|
-
|
|
3016
|
+
const toolTokens = /* @__PURE__ */ new Map([[m.tool, m.tokens]]);
|
|
3017
|
+
merged.push({ startRef: m.ref, endRef: m.ref, startNum: num, count: 1, tokens: m.tokens, toolTokens });
|
|
2298
3018
|
}
|
|
2299
3019
|
}
|
|
2300
3020
|
for (const r of merged.slice(0, 30)) {
|
|
2301
3021
|
const range = r.count === 1 ? r.startRef : `${r.startRef}\u2013${r.endRef}`;
|
|
2302
|
-
lines.push(` ${range} (${r.count} msgs, ${formatTokens2(r.tokens)}${r.count > 1 ? ` (${Math.round(r.tokens / r.count)}/msg)` : ""}) ${r.
|
|
3022
|
+
lines.push(` ${range} (${r.count} msgs, ${formatTokens2(r.tokens)}${r.count > 1 ? ` (${Math.round(r.tokens / r.count)}/msg)` : ""}) ${dominantTool(r.toolTokens)}`);
|
|
2303
3023
|
}
|
|
2304
3024
|
if (merged.length > 30) {
|
|
2305
3025
|
lines.push(` ... and ${merged.length - 30} more ranges`);
|
|
@@ -2458,6 +3178,14 @@ function docFeatures(text) {
|
|
|
2458
3178
|
}
|
|
2459
3179
|
return f;
|
|
2460
3180
|
}
|
|
3181
|
+
function setDocCacheCap(chars) {
|
|
3182
|
+
capChars = Math.max(1, chars);
|
|
3183
|
+
while (cachedChars > capChars && cache.size > 0) {
|
|
3184
|
+
const k = cache.keys().next().value;
|
|
3185
|
+
cachedChars -= k.length;
|
|
3186
|
+
cache.delete(k);
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
2461
3189
|
var substringAlgorithm = {
|
|
2462
3190
|
name: "substring",
|
|
2463
3191
|
description: "Exact substring counting (original baseline). Predictable, no normalization.",
|
|
@@ -2635,9 +3363,37 @@ function makePreview(text, query, len) {
|
|
|
2635
3363
|
return prefix + text.slice(start, end).trim() + suffix;
|
|
2636
3364
|
}
|
|
2637
3365
|
|
|
3366
|
+
// src/lru.ts
|
|
3367
|
+
var DEFAULT_SESSION_CACHE_LIMIT = 512;
|
|
3368
|
+
var LruMap = class extends Map {
|
|
3369
|
+
maxEntries;
|
|
3370
|
+
constructor(maxEntries) {
|
|
3371
|
+
super();
|
|
3372
|
+
this.maxEntries = Math.max(1, Math.floor(maxEntries));
|
|
3373
|
+
}
|
|
3374
|
+
get(key) {
|
|
3375
|
+
if (!super.has(key)) return void 0;
|
|
3376
|
+
const value = super.get(key);
|
|
3377
|
+
super.delete(key);
|
|
3378
|
+
super.set(key, value);
|
|
3379
|
+
return value;
|
|
3380
|
+
}
|
|
3381
|
+
set(key, value) {
|
|
3382
|
+
super.delete(key);
|
|
3383
|
+
super.set(key, value);
|
|
3384
|
+
while (this.size > this.maxEntries) {
|
|
3385
|
+
const oldest = this.keys().next().value;
|
|
3386
|
+
if (oldest === void 0) break;
|
|
3387
|
+
super.delete(oldest);
|
|
3388
|
+
}
|
|
3389
|
+
return this;
|
|
3390
|
+
}
|
|
3391
|
+
};
|
|
3392
|
+
|
|
2638
3393
|
// src/region.ts
|
|
2639
3394
|
import { randomUUID } from "crypto";
|
|
2640
|
-
import { CompactionId, compactCheckpointSource } from "@deepseek-ai/dsh-compaction";
|
|
3395
|
+
import { CompactionId, compactCheckpointSource, toolPairingBalancedAfter, toolPairingBalancedBefore } from "@deepseek-ai/dsh-compaction";
|
|
3396
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
2641
3397
|
|
|
2642
3398
|
// src/session-events.ts
|
|
2643
3399
|
function sessionEventsOf(session) {
|
|
@@ -2651,78 +3407,6 @@ function eventAtOf(session, seq) {
|
|
|
2651
3407
|
return session.events[seq];
|
|
2652
3408
|
}
|
|
2653
3409
|
|
|
2654
|
-
// src/tool-pairing.ts
|
|
2655
|
-
var balanceCacheBySession = /* @__PURE__ */ new WeakMap();
|
|
2656
|
-
function eventDelta(event) {
|
|
2657
|
-
if (event.type === "tool/result") return -1;
|
|
2658
|
-
if (event.type === "assistant/message") {
|
|
2659
|
-
const content = event.data.message?.content;
|
|
2660
|
-
if (!Array.isArray(content)) return 0;
|
|
2661
|
-
let calls = 0;
|
|
2662
|
-
for (const block of content) {
|
|
2663
|
-
if (block !== null && typeof block === "object" && block.type === "tool-call") calls += 1;
|
|
2664
|
-
}
|
|
2665
|
-
return calls;
|
|
2666
|
-
}
|
|
2667
|
-
return 0;
|
|
2668
|
-
}
|
|
2669
|
-
function eventForSeq(session, seq) {
|
|
2670
|
-
const event = eventAtOf(session, seq);
|
|
2671
|
-
if (event === void 0 || event.seq !== seq) {
|
|
2672
|
-
throw new Error(`tool-pairing balance: surface seq ${seq} has no matching session event (corrupt surface)`);
|
|
2673
|
-
}
|
|
2674
|
-
return event;
|
|
2675
|
-
}
|
|
2676
|
-
function extendCache(session, cache2, seqs) {
|
|
2677
|
-
const processed = cache2.cutBalanced.length - 1;
|
|
2678
|
-
const tail = seqs.slice(processed);
|
|
2679
|
-
const pendingCuts = [];
|
|
2680
|
-
let inProgressToolCalls = cache2.inProgressToolCalls;
|
|
2681
|
-
for (const seq of tail) {
|
|
2682
|
-
inProgressToolCalls += eventDelta(eventForSeq(session, seq));
|
|
2683
|
-
if (inProgressToolCalls < 0) {
|
|
2684
|
-
throw new Error(`tool-pairing balance: tool/result at surface seq ${seq} has no matching tool-call (corrupt surface)`);
|
|
2685
|
-
}
|
|
2686
|
-
pendingCuts.push(inProgressToolCalls === 0);
|
|
2687
|
-
}
|
|
2688
|
-
tail.forEach((seq, offset) => cache2.indexBySeq.set(seq, processed + offset));
|
|
2689
|
-
cache2.cutBalanced = cache2.cutBalanced.concat(pendingCuts);
|
|
2690
|
-
cache2.inProgressToolCalls = inProgressToolCalls;
|
|
2691
|
-
return cache2;
|
|
2692
|
-
}
|
|
2693
|
-
function balanceCache(session) {
|
|
2694
|
-
const seqs = session.surface.nodes;
|
|
2695
|
-
const generation = session.surface.replaceGeneration;
|
|
2696
|
-
const cached = balanceCacheBySession.get(session);
|
|
2697
|
-
if (cached === void 0 || cached.generation !== generation || cached.cutBalanced.length - 1 > seqs.length) {
|
|
2698
|
-
const rebuilt = extendCache(session, {
|
|
2699
|
-
generation,
|
|
2700
|
-
cutBalanced: [true],
|
|
2701
|
-
indexBySeq: /* @__PURE__ */ new Map(),
|
|
2702
|
-
inProgressToolCalls: 0
|
|
2703
|
-
}, seqs);
|
|
2704
|
-
balanceCacheBySession.set(session, rebuilt);
|
|
2705
|
-
return rebuilt;
|
|
2706
|
-
}
|
|
2707
|
-
if (cached.cutBalanced.length - 1 < seqs.length) return extendCache(session, cached, seqs);
|
|
2708
|
-
return cached;
|
|
2709
|
-
}
|
|
2710
|
-
function cutBalance(cache2, seq, offset) {
|
|
2711
|
-
const index = cache2.indexBySeq.get(seq);
|
|
2712
|
-
const balanced = index === void 0 ? void 0 : cache2.cutBalanced[index + offset];
|
|
2713
|
-
if (balanced === void 0) throw new Error(`tool-pairing balance: surface seq ${seq} not found`);
|
|
2714
|
-
return balanced;
|
|
2715
|
-
}
|
|
2716
|
-
function toolPairingBalancedBefore(session, seq) {
|
|
2717
|
-
return cutBalance(balanceCache(session), seq, 0);
|
|
2718
|
-
}
|
|
2719
|
-
function toolPairingBalancedAfter(session, seq) {
|
|
2720
|
-
return cutBalance(balanceCache(session), seq, 1);
|
|
2721
|
-
}
|
|
2722
|
-
|
|
2723
|
-
// src/region.ts
|
|
2724
|
-
import { createAssistantMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
2725
|
-
|
|
2726
3410
|
// src/messages.ts
|
|
2727
3411
|
function extractText(content) {
|
|
2728
3412
|
if (typeof content === "string") return content;
|
|
@@ -2733,12 +3417,30 @@ function extractText(content) {
|
|
|
2733
3417
|
const b = block;
|
|
2734
3418
|
if (b.type === "text" && typeof b.text === "string") {
|
|
2735
3419
|
parts.push(b.text);
|
|
3420
|
+
} else if (b.type === "image" || b.type === "file") {
|
|
3421
|
+
const placeholder = attachmentPlaceholder(b.type, b.attachment);
|
|
3422
|
+
if (placeholder !== null) parts.push(placeholder);
|
|
2736
3423
|
} else if (Array.isArray(b.content)) {
|
|
2737
3424
|
parts.push(extractText(b.content));
|
|
2738
3425
|
}
|
|
2739
3426
|
}
|
|
2740
3427
|
return parts.join("\n");
|
|
2741
3428
|
}
|
|
3429
|
+
function formatBytes(bytes) {
|
|
3430
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
3431
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
3432
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
3433
|
+
}
|
|
3434
|
+
function attachmentPlaceholder(type, attachment) {
|
|
3435
|
+
if (attachment === null || typeof attachment !== "object") return null;
|
|
3436
|
+
const a = attachment;
|
|
3437
|
+
const name = typeof a.name === "string" && a.name.length > 0 ? a.name : void 0;
|
|
3438
|
+
const size = typeof a.bytes === "number" && Number.isFinite(a.bytes) ? ` ${formatBytes(a.bytes)}` : "";
|
|
3439
|
+
if (type === "file") return `[file ${name ?? "attachment"}${size}]`;
|
|
3440
|
+
const mediaType = typeof a.mediaType === "string" && a.mediaType.length > 0 ? a.mediaType : "image";
|
|
3441
|
+
const dimensions = typeof a.width === "number" && typeof a.height === "number" ? ` ${a.width}x${a.height}` : "";
|
|
3442
|
+
return `[image ${mediaType}${name ? ` ${name}` : ""}${dimensions}${size}]`;
|
|
3443
|
+
}
|
|
2742
3444
|
function toolCallsOf(content) {
|
|
2743
3445
|
if (!Array.isArray(content)) return [];
|
|
2744
3446
|
return content.filter((b) => b.type === "tool-call");
|
|
@@ -2774,10 +3476,16 @@ function buildToolCallIndex(events) {
|
|
|
2774
3476
|
}
|
|
2775
3477
|
return index;
|
|
2776
3478
|
}
|
|
3479
|
+
var SUMMARY_FRAME_PREFIX = "[Model-written summary \u2014 not user words; re-verify any obligations before relying on them]";
|
|
3480
|
+
function withSummaryFramePrefix(text) {
|
|
3481
|
+
return text.startsWith(SUMMARY_FRAME_PREFIX) ? text : `${SUMMARY_FRAME_PREFIX}
|
|
3482
|
+
${text}`;
|
|
3483
|
+
}
|
|
2777
3484
|
function projectEvent(event, toolNames) {
|
|
2778
3485
|
switch (event.type) {
|
|
2779
3486
|
case "user/message": {
|
|
2780
|
-
const
|
|
3487
|
+
const raw = extractText(event.data.content);
|
|
3488
|
+
const text = isCheckpointNode(event) ? withSummaryFramePrefix(raw) : raw;
|
|
2781
3489
|
return text.length > 0 ? [{ id: String(event.seq), role: "user", contentType: "text", text }] : [];
|
|
2782
3490
|
}
|
|
2783
3491
|
case "assistant/message": {
|
|
@@ -2852,6 +3560,99 @@ function extractEventText(event) {
|
|
|
2852
3560
|
return "";
|
|
2853
3561
|
}
|
|
2854
3562
|
}
|
|
3563
|
+
function countAttachmentBlocks(content) {
|
|
3564
|
+
const counts = { images: 0, files: 0 };
|
|
3565
|
+
countAttachments(content, counts);
|
|
3566
|
+
return counts;
|
|
3567
|
+
}
|
|
3568
|
+
function countAttachments(content, counts) {
|
|
3569
|
+
if (!Array.isArray(content)) return;
|
|
3570
|
+
for (const block of content) {
|
|
3571
|
+
if (block === null || typeof block !== "object") continue;
|
|
3572
|
+
const b = block;
|
|
3573
|
+
if (b.type === "image") counts.images += 1;
|
|
3574
|
+
else if (b.type === "file") counts.files += 1;
|
|
3575
|
+
else if (Array.isArray(b.content)) countAttachments(b.content, counts);
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
function attachmentsOfEvent(event) {
|
|
3579
|
+
return countAttachmentBlocks(contentBlocksOfEvent(event));
|
|
3580
|
+
}
|
|
3581
|
+
function mediaBlocksOfEvent(event) {
|
|
3582
|
+
const blocks = [];
|
|
3583
|
+
collectMediaBlocks(contentBlocksOfEvent(event), blocks);
|
|
3584
|
+
return blocks;
|
|
3585
|
+
}
|
|
3586
|
+
function collectMediaBlocks(content, out) {
|
|
3587
|
+
if (!Array.isArray(content)) return;
|
|
3588
|
+
for (const block of content) {
|
|
3589
|
+
if (block === null || typeof block !== "object") continue;
|
|
3590
|
+
const b = block;
|
|
3591
|
+
if (b.type === "image" || b.type === "file") out.push(block);
|
|
3592
|
+
else if (Array.isArray(b.content)) collectMediaBlocks(b.content, out);
|
|
3593
|
+
}
|
|
3594
|
+
}
|
|
3595
|
+
function contentBlocksOfEvent(event) {
|
|
3596
|
+
switch (event.type) {
|
|
3597
|
+
case "user/message":
|
|
3598
|
+
return event.data.content;
|
|
3599
|
+
case "assistant/message":
|
|
3600
|
+
case "tool/result":
|
|
3601
|
+
return event.data.message?.content;
|
|
3602
|
+
default:
|
|
3603
|
+
return void 0;
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
function isCheckpointNode(event) {
|
|
3607
|
+
if (event.type !== "user/message") return false;
|
|
3608
|
+
const source = event.data.source;
|
|
3609
|
+
return source?.plugin === "compact";
|
|
3610
|
+
}
|
|
3611
|
+
var METADATA_PLUGINS = /* @__PURE__ */ new Set([
|
|
3612
|
+
"acp-nudge",
|
|
3613
|
+
// nudge echo (src/nudge.ts)
|
|
3614
|
+
"billion-context-dsh"
|
|
3615
|
+
// compress-pair replacement stub (src/region.ts)
|
|
3616
|
+
]);
|
|
3617
|
+
var REAL_CONTENT_PLUGINS = /* @__PURE__ */ new Set([
|
|
3618
|
+
"@deepseek-ai/dsh-system-prompt",
|
|
3619
|
+
"user-approval",
|
|
3620
|
+
"tools-ptc"
|
|
3621
|
+
]);
|
|
3622
|
+
var HOST_INSTRUCTION_KINDS = /* @__PURE__ */ new Set([
|
|
3623
|
+
"agent-instructions",
|
|
3624
|
+
// AGENTS.md injection (hook shape: {kind:'agent-instructions', form:'instructions'})
|
|
3625
|
+
"skill-catalog"
|
|
3626
|
+
// skill catalog (form:'catalog')
|
|
3627
|
+
]);
|
|
3628
|
+
function isAgentInstructionsRow(event) {
|
|
3629
|
+
if (event.type !== "user/message") return false;
|
|
3630
|
+
const source = event.data.source;
|
|
3631
|
+
if (!source) return false;
|
|
3632
|
+
return source.kind === "agent-instructions" || source.kind === "plugin" && source.plugin === "agent-instructions";
|
|
3633
|
+
}
|
|
3634
|
+
function classifySurfaceEvent(event) {
|
|
3635
|
+
if (isCheckpointNode(event)) return "checkpoint";
|
|
3636
|
+
if (event.type !== "user/message") return "real";
|
|
3637
|
+
const source = event.data.source;
|
|
3638
|
+
if (!source) return "real";
|
|
3639
|
+
const kind = source.kind;
|
|
3640
|
+
if (kind === "user") return "real";
|
|
3641
|
+
if (kind === "plugin") {
|
|
3642
|
+
if (source.plugin !== void 0 && METADATA_PLUGINS.has(source.plugin)) return "metadata";
|
|
3643
|
+
if (source.plugin !== void 0 && REAL_CONTENT_PLUGINS.has(source.plugin)) return "real";
|
|
3644
|
+
return "instruction";
|
|
3645
|
+
}
|
|
3646
|
+
if (kind !== void 0 && HOST_INSTRUCTION_KINDS.has(kind)) return "instruction";
|
|
3647
|
+
return "real";
|
|
3648
|
+
}
|
|
3649
|
+
function isRealUserTurn(event) {
|
|
3650
|
+
if (event.type !== "user/message") return false;
|
|
3651
|
+
if (classifySurfaceEvent(event) !== "real") return false;
|
|
3652
|
+
const source = event.data.source;
|
|
3653
|
+
if (source?.plugin !== void 0 && REAL_CONTENT_PLUGINS.has(source.plugin)) return false;
|
|
3654
|
+
return source?.kind !== "subagent-report" && source?.kind !== "subagent-settled";
|
|
3655
|
+
}
|
|
2855
3656
|
|
|
2856
3657
|
// src/host-tokens.ts
|
|
2857
3658
|
import { deriveEventMessage } from "@deepseek-ai/dsh-session";
|
|
@@ -2909,6 +3710,34 @@ function shadowedHostTokens(session, seqs) {
|
|
|
2909
3710
|
}
|
|
2910
3711
|
return total;
|
|
2911
3712
|
}
|
|
3713
|
+
function hostMediaStructuralPrice(blocks) {
|
|
3714
|
+
if (!Array.isArray(blocks)) return 0;
|
|
3715
|
+
let tokens = 0;
|
|
3716
|
+
for (const block of blocks) {
|
|
3717
|
+
if (block === null || typeof block !== "object") continue;
|
|
3718
|
+
const b = block;
|
|
3719
|
+
if (b.type === "image" || b.type === "file") {
|
|
3720
|
+
tokens += BLOCK_OVERHEAD + Math.ceil(JSON.stringify(block).length / CHARS_PER_TOKEN);
|
|
3721
|
+
} else if (Array.isArray(b.content)) {
|
|
3722
|
+
tokens += hostMediaStructuralPrice(b.content);
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
return tokens;
|
|
3726
|
+
}
|
|
3727
|
+
function mediaPriceViaMeter(session, ctx) {
|
|
3728
|
+
const prices = /* @__PURE__ */ new Map();
|
|
3729
|
+
try {
|
|
3730
|
+
const meter = ctx?.get?.("tokenMeter");
|
|
3731
|
+
if (meter?.measure === void 0) return prices;
|
|
3732
|
+
for (const node of meter.measure(session).nodes) {
|
|
3733
|
+
const heuristic = node.heuristicTokens ?? node.tokens;
|
|
3734
|
+
const routed = node.tokens - heuristic;
|
|
3735
|
+
if (routed > 0) prices.set(node.seq, routed);
|
|
3736
|
+
}
|
|
3737
|
+
} catch {
|
|
3738
|
+
}
|
|
3739
|
+
return prices;
|
|
3740
|
+
}
|
|
2912
3741
|
function shadowedTokensViaMeter(session, seqs, ctx) {
|
|
2913
3742
|
try {
|
|
2914
3743
|
const meter = ctx?.get?.("tokenMeter");
|
|
@@ -2931,6 +3760,59 @@ function shadowedTokensViaMeter(session, seqs, ctx) {
|
|
|
2931
3760
|
return shadowedHostTokens(session, seqs);
|
|
2932
3761
|
}
|
|
2933
3762
|
|
|
3763
|
+
// src/block-ledger.ts
|
|
3764
|
+
var ACP_BLOCK_LEDGER_MARKER = "$dshAcpBlockLedger";
|
|
3765
|
+
var ACP_BLOCK_LEDGER_VERSION = 1;
|
|
3766
|
+
function isStringArray(value) {
|
|
3767
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
3768
|
+
}
|
|
3769
|
+
function encodeAcpBlockLedger(payload) {
|
|
3770
|
+
const obj = { [ACP_BLOCK_LEDGER_MARKER]: ACP_BLOCK_LEDGER_VERSION };
|
|
3771
|
+
if (payload.tier !== void 0) obj.tier = payload.tier;
|
|
3772
|
+
if (payload.kernelBlockId !== void 0) obj.kernelBlockId = payload.kernelBlockId;
|
|
3773
|
+
if (payload.topic !== void 0) obj.topic = payload.topic;
|
|
3774
|
+
if (payload.parentBlockIds !== void 0 && payload.parentBlockIds.length > 0) {
|
|
3775
|
+
obj.parentBlockIds = [...payload.parentBlockIds];
|
|
3776
|
+
}
|
|
3777
|
+
if (payload.directMessageIds !== void 0) obj.directMessageIds = [...payload.directMessageIds];
|
|
3778
|
+
if (payload.effectiveMessageIds !== void 0) obj.effectiveMessageIds = [...payload.effectiveMessageIds];
|
|
3779
|
+
if (payload.verifiedReadings !== void 0 && payload.verifiedReadings.length > 0) {
|
|
3780
|
+
obj.verifiedReadings = [...payload.verifiedReadings];
|
|
3781
|
+
}
|
|
3782
|
+
return [{ type: "text", text: JSON.stringify(obj) }];
|
|
3783
|
+
}
|
|
3784
|
+
function decodeAcpBlockLedger(rawOutput) {
|
|
3785
|
+
try {
|
|
3786
|
+
if (!Array.isArray(rawOutput)) return {};
|
|
3787
|
+
for (const block of rawOutput) {
|
|
3788
|
+
if (block === null || typeof block !== "object") continue;
|
|
3789
|
+
const candidate = block;
|
|
3790
|
+
if (candidate.type !== "text" || typeof candidate.text !== "string") continue;
|
|
3791
|
+
let parsed;
|
|
3792
|
+
try {
|
|
3793
|
+
parsed = JSON.parse(candidate.text);
|
|
3794
|
+
} catch {
|
|
3795
|
+
continue;
|
|
3796
|
+
}
|
|
3797
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) continue;
|
|
3798
|
+
const record = parsed;
|
|
3799
|
+
if (record[ACP_BLOCK_LEDGER_MARKER] !== ACP_BLOCK_LEDGER_VERSION) continue;
|
|
3800
|
+
const result = {};
|
|
3801
|
+
if (record.tier === 1 || record.tier === 2 || record.tier === 3) result.tier = record.tier;
|
|
3802
|
+
if (typeof record.kernelBlockId === "string") result.kernelBlockId = record.kernelBlockId;
|
|
3803
|
+
if (typeof record.topic === "string") result.topic = record.topic;
|
|
3804
|
+
if (isStringArray(record.parentBlockIds)) result.parentBlockIds = [...record.parentBlockIds];
|
|
3805
|
+
if (isStringArray(record.directMessageIds)) result.directMessageIds = [...record.directMessageIds];
|
|
3806
|
+
if (isStringArray(record.effectiveMessageIds)) result.effectiveMessageIds = [...record.effectiveMessageIds];
|
|
3807
|
+
if (isStringArray(record.verifiedReadings)) result.verifiedReadings = [...record.verifiedReadings];
|
|
3808
|
+
return result;
|
|
3809
|
+
}
|
|
3810
|
+
return {};
|
|
3811
|
+
} catch {
|
|
3812
|
+
return {};
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
|
|
2934
3816
|
// src/region.ts
|
|
2935
3817
|
function findOpenTurn(events) {
|
|
2936
3818
|
let open = null;
|
|
@@ -2989,7 +3871,10 @@ function recoverStaleRange(session, start, end) {
|
|
|
2989
3871
|
return { kind: "unresolvable", failedEdge };
|
|
2990
3872
|
}
|
|
2991
3873
|
const liveInside = session.surface.nodes.filter((seq) => seq >= start && seq <= end).sort((a, b) => a - b);
|
|
2992
|
-
const plain = liveInside.filter((seq) =>
|
|
3874
|
+
const plain = liveInside.filter((seq) => {
|
|
3875
|
+
const event = eventAtOf(session, seq);
|
|
3876
|
+
return !isCheckpointNode(event) && !isSystemNode(event);
|
|
3877
|
+
});
|
|
2993
3878
|
if (plain.length === 0) {
|
|
2994
3879
|
const coveringBlockIds = rebuildBlockLedger(sessionEventsOf(session)).filter((entry) => entry.shadowedSeqs.some((seq) => seq >= start && seq <= end)).map((entry) => entry.blockId);
|
|
2995
3880
|
return { kind: "already-compressed", coveringBlockIds };
|
|
@@ -3031,8 +3916,14 @@ function resolveSurfaceRange(session, start, end) {
|
|
|
3031
3916
|
if (start > end) {
|
|
3032
3917
|
throw new Error(`billion-context-dsh: reversed range ${start}..${end}`);
|
|
3033
3918
|
}
|
|
3034
|
-
const cleanBefore = (index) =>
|
|
3035
|
-
|
|
3919
|
+
const cleanBefore = (index) => {
|
|
3920
|
+
const event = eventAtOf(session, nodes[index]);
|
|
3921
|
+
return event !== void 0 && !isSystemNode(event) && toolPairingBalancedBefore(session, nodes[index]) && hasPlainRef(session, nodes[index]);
|
|
3922
|
+
};
|
|
3923
|
+
const cleanAfter = (index) => {
|
|
3924
|
+
const event = eventAtOf(session, nodes[index]);
|
|
3925
|
+
return event !== void 0 && !isSystemNode(event) && toolPairingBalancedAfter(session, nodes[index]) && hasPlainRef(session, nodes[index]);
|
|
3926
|
+
};
|
|
3036
3927
|
let startIdx = requestedStartIdx;
|
|
3037
3928
|
let endIdx = requestedEndIdx;
|
|
3038
3929
|
while (startIdx <= endIdx && !cleanBefore(startIdx)) {
|
|
@@ -3073,6 +3964,15 @@ function shadowedSeqsOf(session, start, end) {
|
|
|
3073
3964
|
function readCompactionSummary(event) {
|
|
3074
3965
|
return event.data;
|
|
3075
3966
|
}
|
|
3967
|
+
function prefixSummaryBlocks(blocks) {
|
|
3968
|
+
let done = false;
|
|
3969
|
+
return blocks.map((block) => {
|
|
3970
|
+
if (done || block.type !== "text") return block;
|
|
3971
|
+
done = true;
|
|
3972
|
+
const textBlock = block;
|
|
3973
|
+
return { ...textBlock, text: withSummaryFramePrefix(textBlock.text) };
|
|
3974
|
+
});
|
|
3975
|
+
}
|
|
3076
3976
|
function runCompactionTransaction(session, input) {
|
|
3077
3977
|
assertNoActiveCompaction(sessionEventsOf(session));
|
|
3078
3978
|
const turn = findOpenTurn(sessionEventsOf(session));
|
|
@@ -3089,27 +3989,32 @@ function runCompactionTransaction(session, input) {
|
|
|
3089
3989
|
}
|
|
3090
3990
|
try {
|
|
3091
3991
|
seqs.push(session.append("compaction/start", { compactionId, turn }).seq);
|
|
3992
|
+
const ledgerPayload = {
|
|
3993
|
+
tier: input.tier ?? 1,
|
|
3994
|
+
...input.kernelBlockId === void 0 ? {} : { kernelBlockId: input.kernelBlockId },
|
|
3995
|
+
...input.topic === void 0 ? {} : { topic: input.topic },
|
|
3996
|
+
...input.parentBlockIds === void 0 || input.parentBlockIds.length === 0 ? {} : { parentBlockIds: [...input.parentBlockIds] },
|
|
3997
|
+
...input.directMessageIds === void 0 ? {} : { directMessageIds: [...input.directMessageIds] },
|
|
3998
|
+
...input.effectiveMessageIds === void 0 ? {} : { effectiveMessageIds: [...input.effectiveMessageIds] },
|
|
3999
|
+
...input.verifiedReadings === void 0 || input.verifiedReadings.length === 0 ? {} : { verifiedReadings: [...input.verifiedReadings] }
|
|
4000
|
+
};
|
|
4001
|
+
const framedSummary = prefixSummaryBlocks(input.summary);
|
|
3092
4002
|
seqs.push(session.append("compaction/summary", {
|
|
3093
4003
|
compactionId,
|
|
3094
|
-
summary:
|
|
4004
|
+
summary: framedSummary,
|
|
3095
4005
|
shadowedRange: { start: input.start, end: input.end },
|
|
3096
4006
|
shadowedSeqs: [...input.shadowedSeqs],
|
|
3097
4007
|
shadowedTokenCount: input.shadowedTokenCount,
|
|
3098
4008
|
provider: input.provider,
|
|
3099
4009
|
model: input.model,
|
|
3100
|
-
|
|
3101
|
-
...input.kernelBlockId === void 0 ? {} : { kernelBlockId: input.kernelBlockId },
|
|
3102
|
-
...input.topic === void 0 ? {} : { topic: input.topic },
|
|
3103
|
-
...input.parentBlockIds === void 0 || input.parentBlockIds.length === 0 ? {} : { parentBlockIds: [...input.parentBlockIds] },
|
|
3104
|
-
...input.directMessageIds === void 0 ? {} : { directMessageIds: [...input.directMessageIds] },
|
|
3105
|
-
...input.effectiveMessageIds === void 0 ? {} : { effectiveMessageIds: [...input.effectiveMessageIds] }
|
|
4010
|
+
rawOutput: encodeAcpBlockLedger(ledgerPayload)
|
|
3106
4011
|
}).seq);
|
|
3107
4012
|
const message = createUserMessage({
|
|
3108
|
-
content:
|
|
4013
|
+
content: framedSummary,
|
|
3109
4014
|
source: compactCheckpointSource(compactionId)
|
|
3110
4015
|
});
|
|
3111
4016
|
seqs.push(session.append("user/message", message, {
|
|
3112
|
-
surfaceOp: { op: "replace",
|
|
4017
|
+
surfaceOp: { op: "replace", startSeq: input.start, endSeq: input.end },
|
|
3113
4018
|
sourceEventSeqs: [...input.shadowedSeqs]
|
|
3114
4019
|
}).seq);
|
|
3115
4020
|
seqs.push(session.append("compaction/end", { compactionId, turn }).seq);
|
|
@@ -3123,15 +4028,21 @@ function runCompactionTransaction(session, input) {
|
|
|
3123
4028
|
}
|
|
3124
4029
|
return { compactionId, seqs };
|
|
3125
4030
|
}
|
|
3126
|
-
function
|
|
4031
|
+
function summarySeqIndex(events) {
|
|
4032
|
+
const index = /* @__PURE__ */ new Map();
|
|
3127
4033
|
for (const event of events) {
|
|
3128
4034
|
if (event.type !== "user/message") continue;
|
|
3129
4035
|
const source = event.data.source;
|
|
3130
|
-
|
|
4036
|
+
const compactionId = source?.plugin === "compact" ? source.compactionId : void 0;
|
|
4037
|
+
if (compactionId !== void 0 && !index.has(compactionId)) index.set(compactionId, event.seq);
|
|
3131
4038
|
}
|
|
3132
|
-
return
|
|
4039
|
+
return index;
|
|
3133
4040
|
}
|
|
4041
|
+
var blockLedgerCache = /* @__PURE__ */ new WeakMap();
|
|
3134
4042
|
function rebuildBlockLedger(events) {
|
|
4043
|
+
const cached = blockLedgerCache.get(events);
|
|
4044
|
+
if (cached !== void 0 && cached.len === events.length) return cached.ledger;
|
|
4045
|
+
const summarySeqs = summarySeqIndex(events);
|
|
3135
4046
|
const ledger = [];
|
|
3136
4047
|
for (const event of events) {
|
|
3137
4048
|
if (event.type !== "compaction/summary") continue;
|
|
@@ -3144,28 +4055,34 @@ function rebuildBlockLedger(events) {
|
|
|
3144
4055
|
if (original !== void 0) shadowedTokenCount += defaultCountTokens(extractEventText(original));
|
|
3145
4056
|
}
|
|
3146
4057
|
}
|
|
3147
|
-
const
|
|
3148
|
-
const
|
|
3149
|
-
const
|
|
3150
|
-
const
|
|
3151
|
-
const
|
|
4058
|
+
const embedded = decodeAcpBlockLedger(data.rawOutput);
|
|
4059
|
+
const tier = embedded.tier ?? (data.tier === 2 || data.tier === 3 ? data.tier : 1);
|
|
4060
|
+
const parentBlockIds = embedded.parentBlockIds ? [...embedded.parentBlockIds] : Array.isArray(data.parentBlockIds) ? [...data.parentBlockIds] : [];
|
|
4061
|
+
const directMessageIds = embedded.directMessageIds ? [...embedded.directMessageIds] : Array.isArray(data.directMessageIds) ? [...data.directMessageIds] : void 0;
|
|
4062
|
+
const effectiveMessageIds = embedded.effectiveMessageIds ? [...embedded.effectiveMessageIds] : Array.isArray(data.effectiveMessageIds) ? [...data.effectiveMessageIds] : void 0;
|
|
4063
|
+
const topic = embedded.topic ?? (typeof data.topic === "string" ? data.topic : void 0);
|
|
4064
|
+
const kernelBlockId = embedded.kernelBlockId ?? (typeof data.kernelBlockId === "string" ? data.kernelBlockId : void 0);
|
|
4065
|
+
const verifiedReadings = embedded.verifiedReadings ? [...embedded.verifiedReadings] : Array.isArray(data.verifiedReadings) ? [...data.verifiedReadings] : void 0;
|
|
4066
|
+
const summarySeq = summarySeqs.get(data.compactionId) ?? null;
|
|
3152
4067
|
ledger.push({
|
|
3153
4068
|
blockId: data.compactionId,
|
|
3154
4069
|
summary: extractText(data.summary),
|
|
3155
|
-
...
|
|
4070
|
+
...topic === void 0 ? {} : { topic },
|
|
3156
4071
|
shadowedSeqs: [...data.shadowedSeqs],
|
|
3157
4072
|
shadowedTokenCount,
|
|
3158
4073
|
start: data.shadowedRange.start,
|
|
3159
4074
|
end: data.shadowedRange.end,
|
|
3160
4075
|
tier,
|
|
3161
4076
|
parentBlockIds,
|
|
3162
|
-
...
|
|
4077
|
+
...kernelBlockId === void 0 ? {} : { kernelBlockId },
|
|
3163
4078
|
...summarySeq === null ? {} : { summarySeq },
|
|
3164
4079
|
...directMessageIds === void 0 ? {} : { directMessageIds },
|
|
3165
4080
|
...effectiveMessageIds === void 0 ? {} : { effectiveMessageIds },
|
|
4081
|
+
...verifiedReadings === void 0 ? {} : { verifiedReadings },
|
|
3166
4082
|
createdAt: event.time
|
|
3167
4083
|
});
|
|
3168
4084
|
}
|
|
4085
|
+
blockLedgerCache.set(events, { len: events.length, ledger });
|
|
3169
4086
|
return ledger;
|
|
3170
4087
|
}
|
|
3171
4088
|
function isToolEvent(event) {
|
|
@@ -3174,10 +4091,8 @@ function isToolEvent(event) {
|
|
|
3174
4091
|
const content = event.data.message?.content;
|
|
3175
4092
|
return Array.isArray(content) && content.some((block) => block?.type === "tool-call");
|
|
3176
4093
|
}
|
|
3177
|
-
function
|
|
3178
|
-
|
|
3179
|
-
const source = event.data.source;
|
|
3180
|
-
return source?.plugin === "compact";
|
|
4094
|
+
function isSystemNode(event) {
|
|
4095
|
+
return event.type === "system/message";
|
|
3181
4096
|
}
|
|
3182
4097
|
function toolCallIdsOfEvent(event) {
|
|
3183
4098
|
if (event.type !== "assistant/message") return [];
|
|
@@ -3191,17 +4106,8 @@ function toolCallIdsOfEvent(event) {
|
|
|
3191
4106
|
}
|
|
3192
4107
|
return ids;
|
|
3193
4108
|
}
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
const message = event.data.message;
|
|
3197
|
-
return {
|
|
3198
|
-
provider: typeof message?.source?.provider === "string" ? message.source.provider : "billion-context-dsh",
|
|
3199
|
-
model: typeof message?.source?.model === "string" ? message.source.model : "surface-prune"
|
|
3200
|
-
};
|
|
3201
|
-
}
|
|
3202
|
-
return { provider: "billion-context-dsh", model: "surface-prune" };
|
|
3203
|
-
}
|
|
3204
|
-
function hideSurfaceSeqs(session, seqs, provider, model, text, priceEvent = hostPriceEvent) {
|
|
4109
|
+
var PRUNE_NOTE = "(removed by context management)";
|
|
4110
|
+
function hideSurfaceSeqs(session, seqs, text, priceEvent = hostPriceEvent) {
|
|
3205
4111
|
if (seqs.length === 0) return;
|
|
3206
4112
|
const start = seqs[0];
|
|
3207
4113
|
const end = seqs[seqs.length - 1];
|
|
@@ -3215,22 +4121,12 @@ function hideSurfaceSeqs(session, seqs, provider, model, text, priceEvent = host
|
|
|
3215
4121
|
shadowedSeqs: [...seqs],
|
|
3216
4122
|
shadowedTokenCount
|
|
3217
4123
|
});
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
sourceEventSeqs: [...seqs]
|
|
3225
|
-
});
|
|
3226
|
-
return;
|
|
3227
|
-
}
|
|
3228
|
-
session.append("assistant/message", {
|
|
3229
|
-
turn: findOpenTurn(sessionEventsOf(session)) ?? 0,
|
|
3230
|
-
step: 0,
|
|
3231
|
-
message: createAssistantMessage({ content: [], source: { provider, model } })
|
|
3232
|
-
}, {
|
|
3233
|
-
surfaceOp: { op: "replace", start, end },
|
|
4124
|
+
const body = text !== void 0 && text.trim().length > 0 ? text : PRUNE_NOTE;
|
|
4125
|
+
session.append("user/message", createUserMessage({
|
|
4126
|
+
content: [{ type: "text", text: body }],
|
|
4127
|
+
source: { kind: "plugin", plugin: "billion-context-dsh" }
|
|
4128
|
+
}), {
|
|
4129
|
+
surfaceOp: { op: "replace", startSeq: start, endSeq: end },
|
|
3234
4130
|
sourceEventSeqs: [...seqs]
|
|
3235
4131
|
});
|
|
3236
4132
|
}
|
|
@@ -3261,10 +4157,9 @@ function hideCompressToolPair(session, callId, resultSeq) {
|
|
|
3261
4157
|
const startIdx = nodes.indexOf(callSeq);
|
|
3262
4158
|
const endIdx = nodes.indexOf(resolvedResultSeq);
|
|
3263
4159
|
if (startIdx < 0 || endIdx < 0 || endIdx - startIdx !== 1) return false;
|
|
3264
|
-
const { provider, model } = assistantProviderModel(events[callSeq]);
|
|
3265
4160
|
const resultEvent = events[resolvedResultSeq];
|
|
3266
4161
|
const resultText = resultEvent === void 0 ? "" : extractEventText(resultEvent);
|
|
3267
|
-
hideSurfaceSeqs(session, [callSeq, resolvedResultSeq],
|
|
4162
|
+
hideSurfaceSeqs(session, [callSeq, resolvedResultSeq], resultText);
|
|
3268
4163
|
return true;
|
|
3269
4164
|
}
|
|
3270
4165
|
function stripOrphanedSurfaceToolMessages(session, inFlightCallIds = /* @__PURE__ */ new Set()) {
|
|
@@ -3332,10 +4227,8 @@ function stripOrphanedSurfaceToolMessages(session, inFlightCallIds = /* @__PURE_
|
|
|
3332
4227
|
const hidden = [...hiddenSet].sort((a, b) => a - b);
|
|
3333
4228
|
let count = 0;
|
|
3334
4229
|
for (const seq of hidden) {
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
const { provider, model } = assistantProviderModel(event);
|
|
3338
|
-
hideSurfaceSeqs(session, [seq], provider, model);
|
|
4230
|
+
if (eventAtOf(session, seq) === void 0) continue;
|
|
4231
|
+
hideSurfaceSeqs(session, [seq]);
|
|
3339
4232
|
count += 1;
|
|
3340
4233
|
}
|
|
3341
4234
|
return count;
|
|
@@ -3363,59 +4256,131 @@ function deferCompressPairHide(session, callId, resultSeq, onError) {
|
|
|
3363
4256
|
}
|
|
3364
4257
|
});
|
|
3365
4258
|
}
|
|
3366
|
-
function
|
|
3367
|
-
|
|
4259
|
+
function newestInstructionSeqsOf(session) {
|
|
4260
|
+
const newest = /* @__PURE__ */ new Map();
|
|
4261
|
+
const events = sessionEventsOf(session);
|
|
4262
|
+
for (let seq = 0; seq < events.length; seq += 1) {
|
|
4263
|
+
const event = events[seq];
|
|
4264
|
+
if (event === void 0 || !isAgentInstructionsRow(event)) continue;
|
|
4265
|
+
const source = event.data.source;
|
|
4266
|
+
const changes = Array.isArray(source?.changes) ? source.changes : [];
|
|
4267
|
+
const scopes = changes.map((change) => typeof change?.scope === "string" ? change.scope : "").filter((scope) => scope.length > 0);
|
|
4268
|
+
if (scopes.length === 0) {
|
|
4269
|
+
continue;
|
|
4270
|
+
}
|
|
4271
|
+
for (const scope of scopes) newest.set(scope, seq);
|
|
4272
|
+
}
|
|
4273
|
+
return new Set(newest.values());
|
|
4274
|
+
}
|
|
4275
|
+
function guardedSurfaceSeqsOf(session) {
|
|
4276
|
+
const guarded = /* @__PURE__ */ new Set();
|
|
4277
|
+
const newestInstructions = newestInstructionSeqsOf(session);
|
|
4278
|
+
for (const seq of session.surface.nodes) {
|
|
4279
|
+
const event = eventAtOf(session, seq);
|
|
4280
|
+
if (event === void 0) continue;
|
|
4281
|
+
if (isAgentInstructionsRow(event) && newestInstructions.has(seq)) guarded.add(seq);
|
|
4282
|
+
}
|
|
4283
|
+
return guarded;
|
|
4284
|
+
}
|
|
4285
|
+
function seqOfKernelRef(refs, ref) {
|
|
4286
|
+
const id = refs.byRef[ref];
|
|
4287
|
+
if (id === void 0) return null;
|
|
4288
|
+
const seq = Number(id);
|
|
4289
|
+
return Number.isInteger(seq) ? seq : null;
|
|
4290
|
+
}
|
|
4291
|
+
function protectedSurfaceSeqs(session, preserve) {
|
|
3368
4292
|
const nodes = session.surface.nodes;
|
|
3369
|
-
const preserve = opts.preserveRecent ?? 5;
|
|
3370
4293
|
const protectedSeqs = /* @__PURE__ */ new Set();
|
|
3371
4294
|
if (preserve > 0) {
|
|
3372
4295
|
for (const seq of nodes.slice(-preserve)) protectedSeqs.add(seq);
|
|
3373
4296
|
}
|
|
3374
4297
|
for (let index = nodes.length - 1; index >= 0; index -= 1) {
|
|
3375
4298
|
const event = eventAtOf(session, nodes[index]);
|
|
3376
|
-
if (event
|
|
4299
|
+
if (event !== void 0 && isRealUserTurn(event)) {
|
|
3377
4300
|
protectedSeqs.add(nodes[index]);
|
|
3378
4301
|
break;
|
|
3379
4302
|
}
|
|
3380
4303
|
}
|
|
3381
|
-
const
|
|
3382
|
-
|
|
4304
|
+
for (const seq of newestInstructionSeqsOf(session)) protectedSeqs.add(seq);
|
|
4305
|
+
return protectedSeqs;
|
|
4306
|
+
}
|
|
4307
|
+
function compressibleSegmentsOf(session, fromIndex, toIndex, protectedSeqs, mediaPriceOf) {
|
|
4308
|
+
const nodes = session.surface.nodes;
|
|
4309
|
+
const segments = [];
|
|
4310
|
+
let current = null;
|
|
3383
4311
|
const flush = () => {
|
|
3384
|
-
if (
|
|
3385
|
-
|
|
4312
|
+
if (current !== null) segments.push(current);
|
|
4313
|
+
current = null;
|
|
3386
4314
|
};
|
|
3387
|
-
for (
|
|
4315
|
+
for (let index = fromIndex; index <= toIndex; index += 1) {
|
|
4316
|
+
const seq = nodes[index];
|
|
4317
|
+
if (seq === void 0) continue;
|
|
3388
4318
|
const event = eventAtOf(session, seq);
|
|
3389
|
-
if (event === void 0 || protectedSeqs.has(seq) || isCheckpointNode(event)) {
|
|
4319
|
+
if (event === void 0 || protectedSeqs.has(seq) || isCheckpointNode(event) || isSystemNode(event) || classifySurfaceEvent(event) === "instruction") {
|
|
3390
4320
|
flush();
|
|
3391
4321
|
continue;
|
|
3392
4322
|
}
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
}
|
|
3397
|
-
const tokens = defaultCountTokens(extractEventText(event));
|
|
4323
|
+
const attachments = attachmentsOfEvent(event);
|
|
4324
|
+
const mediaPrice = attachments.images + attachments.files > 0 ? (mediaPriceOf?.(seq) ?? 0) + hostMediaStructuralPrice(mediaBlocksOfEvent(event)) : 0;
|
|
4325
|
+
const tokens = defaultCountTokens(extractEventText(event)) + mediaPrice;
|
|
3398
4326
|
const isTool = isToolEvent(event);
|
|
3399
|
-
if (
|
|
3400
|
-
|
|
4327
|
+
if (current === null) {
|
|
4328
|
+
current = {
|
|
4329
|
+
start: seq,
|
|
4330
|
+
end: seq,
|
|
4331
|
+
count: 1,
|
|
4332
|
+
tokens,
|
|
4333
|
+
toolCount: isTool ? 1 : 0,
|
|
4334
|
+
images: attachments.images,
|
|
4335
|
+
files: attachments.files
|
|
4336
|
+
};
|
|
3401
4337
|
} else {
|
|
3402
|
-
|
|
4338
|
+
current.start = Math.min(current.start, seq);
|
|
4339
|
+
current.end = Math.max(current.end, seq);
|
|
4340
|
+
current.count += 1;
|
|
4341
|
+
current.tokens += tokens;
|
|
4342
|
+
current.toolCount += isTool ? 1 : 0;
|
|
4343
|
+
current.images += attachments.images;
|
|
4344
|
+
current.files += attachments.files;
|
|
3403
4345
|
}
|
|
3404
4346
|
}
|
|
3405
4347
|
flush();
|
|
4348
|
+
return segments;
|
|
4349
|
+
}
|
|
4350
|
+
function buildCompressibleSeqRanges(session, kernelView, opts = {}) {
|
|
4351
|
+
stripOrphanedSurfaceToolMessages(session);
|
|
4352
|
+
const nodes = session.surface.nodes;
|
|
4353
|
+
const indexOfSeq = /* @__PURE__ */ new Map();
|
|
4354
|
+
for (let index = 0; index < nodes.length; index += 1) indexOfSeq.set(nodes[index], index);
|
|
4355
|
+
const protectedSeqs = protectedSurfaceSeqs(session, opts.preserveRecent ?? 5);
|
|
3406
4356
|
const out = [];
|
|
3407
|
-
for (const range of
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
4357
|
+
for (const range of kernelView.ranges) {
|
|
4358
|
+
const startSeq = seqOfKernelRef(kernelView.refs, range.startRef);
|
|
4359
|
+
const endSeq = seqOfKernelRef(kernelView.refs, range.endRef);
|
|
4360
|
+
const from = startSeq === null ? void 0 : indexOfSeq.get(startSeq);
|
|
4361
|
+
const to = endSeq === null ? void 0 : indexOfSeq.get(endSeq);
|
|
4362
|
+
if (from === void 0 || to === void 0) continue;
|
|
4363
|
+
const segments = compressibleSegmentsOf(
|
|
4364
|
+
session,
|
|
4365
|
+
Math.min(from, to),
|
|
4366
|
+
Math.max(from, to),
|
|
4367
|
+
protectedSeqs,
|
|
4368
|
+
opts.mediaPriceOf
|
|
4369
|
+
);
|
|
4370
|
+
for (const segment of segments) {
|
|
4371
|
+
try {
|
|
4372
|
+
const { start, end } = resolveSurfaceRange(session, segment.start, segment.end);
|
|
4373
|
+
out.push({
|
|
4374
|
+
start,
|
|
4375
|
+
end,
|
|
4376
|
+
count: segment.count,
|
|
4377
|
+
tokens: segment.tokens,
|
|
4378
|
+
toolPct: segment.count > 0 ? Math.round(segment.toolCount / segment.count * 100) : 0,
|
|
4379
|
+
images: segment.images,
|
|
4380
|
+
files: segment.files
|
|
4381
|
+
});
|
|
4382
|
+
} catch {
|
|
4383
|
+
}
|
|
3419
4384
|
}
|
|
3420
4385
|
}
|
|
3421
4386
|
return out.sort((a, b) => a.start - b.start);
|
|
@@ -3515,6 +4480,26 @@ function expandShadowedSeqs(session, blockId) {
|
|
|
3515
4480
|
visit(root);
|
|
3516
4481
|
return out;
|
|
3517
4482
|
}
|
|
4483
|
+
var DEFAULT_DECOMPRESS_PAGE = 100;
|
|
4484
|
+
var DEFAULT_DECOMPRESS_PAGE_CHARS = 7e3;
|
|
4485
|
+
function sliceDecompressPage(expanded, offset, limit, charBudget, renderLen) {
|
|
4486
|
+
const offN = typeof offset === "number" ? offset : Number(offset);
|
|
4487
|
+
const safeOffset = Number.isFinite(offN) && offN > 0 ? Math.floor(offN) : 0;
|
|
4488
|
+
const limN = typeof limit === "number" ? limit : Number(limit);
|
|
4489
|
+
const safeLimit = Number.isFinite(limN) && limN >= 1 ? Math.min(Math.floor(limN), DEFAULT_DECOMPRESS_PAGE) : DEFAULT_DECOMPRESS_PAGE;
|
|
4490
|
+
const start = Math.min(safeOffset, expanded.length);
|
|
4491
|
+
const endCap = Math.min(start + safeLimit, expanded.length);
|
|
4492
|
+
let end = start;
|
|
4493
|
+
let acc = 0;
|
|
4494
|
+
for (let i = start; i < endCap; i += 1) {
|
|
4495
|
+
const seq = expanded[i];
|
|
4496
|
+
const len = renderLen(seq);
|
|
4497
|
+
if (i > start && acc + len > charBudget) break;
|
|
4498
|
+
acc += len;
|
|
4499
|
+
end = i + 1;
|
|
4500
|
+
}
|
|
4501
|
+
return { offset: safeOffset, limit: safeLimit, total: expanded.length, seqs: expanded.slice(start, end), exhausted: end >= expanded.length };
|
|
4502
|
+
}
|
|
3518
4503
|
|
|
3519
4504
|
// src/state.ts
|
|
3520
4505
|
function rebuildKernelBlocks(events) {
|
|
@@ -3575,8 +4560,29 @@ function nextBlockIdAfter(events) {
|
|
|
3575
4560
|
}
|
|
3576
4561
|
return max + 1;
|
|
3577
4562
|
}
|
|
4563
|
+
function nextRunIdAfter(blocks) {
|
|
4564
|
+
let max = 0;
|
|
4565
|
+
for (const block of blocks) {
|
|
4566
|
+
const num = Number(block.runId.slice(1));
|
|
4567
|
+
if (Number.isInteger(num)) max = Math.max(max, num);
|
|
4568
|
+
}
|
|
4569
|
+
return max + 1;
|
|
4570
|
+
}
|
|
3578
4571
|
var AcpStateStore = class {
|
|
3579
|
-
|
|
4572
|
+
/**
|
|
4573
|
+
* Live kernel states, capped by an LRU policy (issue #113): once the cap is
|
|
4574
|
+
* reached the coldest session's state is dropped, and its next access
|
|
4575
|
+
* rehydrates through stateFor's log-rebuild path below. Rehydration is
|
|
4576
|
+
* deterministic — bN ids are recorded in the durable event or synthesised
|
|
4577
|
+
* in ledger order, and run ids continue after the rehydrated max — so block
|
|
4578
|
+
* identity survives eviction exactly as it survives a restart. Kernel
|
|
4579
|
+
* fields that reset on eviction (tokenSnapshot, nudge cadence, stats
|
|
4580
|
+
* counters) all self-heal on the session's next turn.
|
|
4581
|
+
*/
|
|
4582
|
+
states;
|
|
4583
|
+
constructor(limit = DEFAULT_SESSION_CACHE_LIMIT) {
|
|
4584
|
+
this.states = new LruMap(limit);
|
|
4585
|
+
}
|
|
3580
4586
|
/** Kernel state for one session, initialised on first access. */
|
|
3581
4587
|
stateFor(session) {
|
|
3582
4588
|
const id = session.id;
|
|
@@ -3587,6 +4593,7 @@ var AcpStateStore = class {
|
|
|
3587
4593
|
if (events.some((event) => event.type === "compaction/summary")) {
|
|
3588
4594
|
state.blocks = rebuildKernelBlocks(events);
|
|
3589
4595
|
state.nextBlockId = nextBlockIdAfter(events);
|
|
4596
|
+
state.nextRunId = nextRunIdAfter(state.blocks);
|
|
3590
4597
|
}
|
|
3591
4598
|
this.states.set(id, state);
|
|
3592
4599
|
return state;
|
|
@@ -3635,7 +4642,7 @@ var NUDGE_ALLOWED = {
|
|
|
3635
4642
|
var RANGE_TABLE_ALLOWED = {
|
|
3636
4643
|
header: /* @__PURE__ */ new Set(["surface"]),
|
|
3637
4644
|
title: /* @__PURE__ */ new Set(["count"]),
|
|
3638
|
-
line: /* @__PURE__ */ new Set(["start", "end", "count", "tokens"]),
|
|
4645
|
+
line: /* @__PURE__ */ new Set(["start", "end", "count", "tokens", "toolPct", "textPct", "media"]),
|
|
3639
4646
|
footer: /* @__PURE__ */ new Set()
|
|
3640
4647
|
};
|
|
3641
4648
|
var TOOLS_ALLOWED = {
|
|
@@ -3678,7 +4685,7 @@ function mergeGroup(defaults, override, allowed, path) {
|
|
|
3678
4685
|
}
|
|
3679
4686
|
return out;
|
|
3680
4687
|
}
|
|
3681
|
-
function
|
|
4688
|
+
function resolvePrompts2(input) {
|
|
3682
4689
|
if (input === void 0) return DEFAULT_RESOLVED;
|
|
3683
4690
|
return {
|
|
3684
4691
|
nudge: mergeGroup(DEFAULT_PROMPTS.nudge, input.nudge, NUDGE_ALLOWED, "prompts.nudge"),
|
|
@@ -3699,8 +4706,10 @@ var DEFAULT_PROMPTS = {
|
|
|
3699
4706
|
nudge: {
|
|
3700
4707
|
// 与 kernel nudge-text.ts EFFICIENCY_NOTE 逐字对齐——不含 "Context usage is at X%"
|
|
3701
4708
|
// 陈述(usage 只通过 breakdown 传达);{pct} 仍可用作自定义占位符。
|
|
3702
|
-
|
|
3703
|
-
|
|
4709
|
+
// B6(2026-09-08):正文 ≤300 B——philosophy 段移出 nudge(已住系统提示与工具描述),
|
|
4710
|
+
// 每拍复读同一份 6 KB 文本=重复计费。
|
|
4711
|
+
normal: "Efficiency nudge: compress consumed ranges early to keep context lean \u2014 not an overflow warning. A stronger alert appears only if the context is actually full.",
|
|
4712
|
+
emergency: "\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.",
|
|
3704
4713
|
guidance: HOW_TO_COMPRESS_RULES,
|
|
3705
4714
|
tier: "Tier {tier}: {count} tier-{prevTier} block(s) distillable ({tokens} tokens) \u2014 distill them by compressing their checkpoint seq(s) [seqs {seqs}] as one range: compress({ content: [{ startSeq: {firstSeq}, endSeq: {lastSeq}, summary }] }).",
|
|
3706
4715
|
breakdown: "Context breakdown: {system}K system | {tool}K tool | {summaries}K summaries | {code}K code | {text}K text",
|
|
@@ -3710,12 +4719,12 @@ var DEFAULT_PROMPTS = {
|
|
|
3710
4719
|
rangeTable: {
|
|
3711
4720
|
header: "Surface: {surface}",
|
|
3712
4721
|
title: "Compressible ranges ({count}, oldest first; exact surface seqs \u2014 usable as-is):",
|
|
3713
|
-
line: " - seq {start}..{end} \u2014 {count} messages, ~{tokens} tokens [tool {toolPct}% | text {textPct}%]",
|
|
4722
|
+
line: " - seq {start}..{end} \u2014 {count} messages, ~{tokens} tokens [tool {toolPct}% | text {textPct}%]{media}",
|
|
3714
4723
|
footer: "Compress with: compress({ content: [{ startSeq, endSeq, summary }] }) \u2014 content is an array: batch multiple unrelated segments in one call, each entry its own block. Keep ranges disjoint.\nSnapshot taken at nudge time: the seqs go stale once the surface moves (a later compress shadows them), so re-run acp_status for fresh refs before compressing."
|
|
3715
4724
|
},
|
|
3716
4725
|
tools: {
|
|
3717
|
-
compress:
|
|
3718
|
-
decompress: "Recover the original content of a compressed block by its blockId \u2014 the kernel block ref `bN` shown by acp_status (e.g. b1), or a compaction id from search_context (read-only; does not unshadow the range).",
|
|
4726
|
+
compress: 'Replace older conversation ranges with dense summaries you write. Each message seq is a surface reference. Single range: compress({ content: [{ startSeq, endSeq, summary }] }). Batch multiple unrelated ranges in one call (each content entry becomes its own block); keep ranges disjoint. Never compress content the current step is actively using. Compress boundaries are SURFACE SEQS (acp_status Surface: row, latest nudge table) \u2014 NOT the block refs (bN, e.g. b1) that acp_status COMPRESSED BLOCKS shows, which are for decompress only. Drilldown mN refs (e.g. m00306) are ALSO accepted as startSeq/endSeq \u2014 they are auto-mapped to the live surface seq; an unknown mN (never assigned on the current surface) fails with guidance. Seq refs must come from the CURRENT surface (acp_status or the latest nudge): a span whose edges were shadowed by an earlier compress is auto-remapped to its still-live content, a fully compressed span is reported as already compressed, and invented/other-session seqs fail with guidance. Good compression moments: stage or subtask completion whose details you have fully consumed and will not re-check, strategy switches, intermediate milestones, and wrapping up failed exploration \u2014 when the details are consumed and no longer critical for the task ahead. Before compressing, ask: will I need to re-verify any detail from this range in this task? If yes, keep it live. When you write a summary, turn dead-end exploration into a conclusion (what was tried, why it failed, the next step) \u2014 not a blow-by-blow; and keep the summary the ONLY record: self-contained, so a later reader (or you, after decompress) can continue without the original. Optional verifiedReadings: string[] per content entry records acceptance readings that are already green (e.g. "t0-fastpath 8/8") \u2014 stored structurally on the compaction event so later steps need not re-run them.',
|
|
4727
|
+
decompress: "Recover the original content of a compressed block by its blockId \u2014 the kernel block ref `bN` shown by acp_status (e.g. b1), or a compaction id from search_context (read-only; does not unshadow the range). Large blocks are paged so each page stays under the host tool-result trim budget (up to 100 messages per call): pass offset/limit to walk them and follow the continue hint in the result.",
|
|
3719
4728
|
searchContext: "Search inside compressed blocks (summaries and original content) for information the model no longer sees in context. When a summary lacks a detail you need (exact values, error strings, decisions, verbatim code), SEARCH the compressed blocks FIRST \u2014 never guess or reconstruct from memory: search_context(query) locates the right block, then decompress only that block to recover the original.",
|
|
3720
4729
|
acpStatus: 'Context status: overview of the current context \u2014 CONTEXT BREAKDOWN (tool/text/summaries token shares of the visible total), COMPRESSED BLOCKS ledger, and the nudge decision. No args = overview. Percentages are shares of the visible content, not the context window. Note: the block refs in COMPRESSED BLOCKS (bN, e.g. b1) are for decompress; compress uses the Surface: seq range, not bN. Drilldown: pass scope:"compressed" for a per-block list, or scope:"uncompressed" with view:"messages" (every visible message) / view:"ranges" (merged ranges); tool filters to one tool name, sort reorders (size/time/tool; age for compressed), limit caps rows (default 30). Drilldown row refs are kernel ids (mN) \u2014 feed them straight to compress as startSeq/endSeq (auto-mapped to the live surface seq); bN is for decompress, Surface: seqs also work in compress.'
|
|
3721
4730
|
},
|
|
@@ -3744,7 +4753,7 @@ WHEN NOT TO COMPRESS:
|
|
|
3744
4753
|
|
|
3745
4754
|
Compression tools (refs are SURFACE SEQS, not ids):
|
|
3746
4755
|
- compress: replace one or more seq ranges, each with your own dense summary. Single range: compress({ content: [{ startSeq, endSeq, summary }] }). Batch multiple unrelated segments in one call (each entry becomes its own block): compress({ content: [{ startSeq: 1, endSeq: 5, summary: '...' }, { startSeq: 12, endSeq: 18, summary: '...' }] }). Keep ranges disjoint \u2014 overlapping entries in one batch are skipped. Edges are auto-balanced to tool-call/result boundaries; a trailing #callId fragment in a seq is ignored. Seq refs must be on the current surface: seqs from older nudges or earlier compresses go stale as the surface moves, so a stale span is auto-remapped to its still-live remainder (the result reports the adjusted span), a fully compressed span is reported as already compressed, and invented/other-session seqs fail with guidance. The block refs (bN, e.g. b1) in acp_status COMPRESSED BLOCKS are for decompress, NOT compress boundaries.
|
|
3747
|
-
- decompress: recover a compressed block's original content, read-only. decompress({ blockId }) \u2014 accept the bN ref shown by acp_status (e.g. b1) or a compaction id.
|
|
4756
|
+
- decompress: recover a compressed block's original content, read-only. decompress({ blockId }) \u2014 accept the bN ref shown by acp_status (e.g. b1) or a compaction id. Large blocks page (each page sized to stay under the host tool-result trim budget, up to 100 messages): pass offset/limit and follow the continue hint in the result.
|
|
3748
4757
|
- search_context: when a summary lacks the details you need (exact values, error strings, decisions, verbatim code), SEARCH the compressed blocks FIRST \u2014 never guess or reconstruct from memory; search_context(query) locates the right block, decompress only that block.
|
|
3749
4758
|
- acp_status: current context usage and the live compressible-range list. Run it right before compressing \u2014 the only seqs that never go stale are the ones you just read. Drilldown (scope/view/tool/sort/limit) lists per-message or per-block sizes; drilldown rows are kernel ids (mN) \u2014 compress accepts them directly (auto-mapped to the live surface seq).
|
|
3750
4759
|
|
|
@@ -3759,6 +4768,12 @@ When you write a summary, it becomes the ONLY record of that range: keep file pa
|
|
|
3759
4768
|
var DEFAULT_RESOLVED = DEFAULT_PROMPTS;
|
|
3760
4769
|
|
|
3761
4770
|
// src/nudge.ts
|
|
4771
|
+
var GUIDANCE_BLOCKS = [COMPRESS_PHILOSOPHY, HOW_TO_COMPRESS_RULES, TIER2_DISTILL_RULES, TIER3_CONDENSE_RULES];
|
|
4772
|
+
function stripNudgeGuidance(text) {
|
|
4773
|
+
let out = text;
|
|
4774
|
+
for (const block of GUIDANCE_BLOCKS) out = out.split(block).join("");
|
|
4775
|
+
return out.replace(/\n{3,}/g, "\n\n").trim();
|
|
4776
|
+
}
|
|
3762
4777
|
function resolveTokenCount(agent, coreMessages) {
|
|
3763
4778
|
const projections = agent.ctx?.get?.("sessionProjections");
|
|
3764
4779
|
const projected = projections?.snapshot?.(agent.session)?.values?.contextPressure?.projectedTokens;
|
|
@@ -3768,8 +4783,29 @@ function resolveTokenCount(agent, coreMessages) {
|
|
|
3768
4783
|
if (typeof surface === "number" && surface > 0) return surface;
|
|
3769
4784
|
return coreMessages.reduce((sum, message) => sum + defaultCountTokens(message.text ?? ""), 0);
|
|
3770
4785
|
}
|
|
3771
|
-
function
|
|
3772
|
-
|
|
4786
|
+
function kernelRangeViewOf(nudge, state) {
|
|
4787
|
+
return { ranges: nudge.compressibleRanges ?? [], refs: state.messageRefs };
|
|
4788
|
+
}
|
|
4789
|
+
function mediaSuffixOf(range) {
|
|
4790
|
+
if (range.images === 0 && range.files === 0) return "";
|
|
4791
|
+
const parts = [];
|
|
4792
|
+
if (range.images > 0) parts.push(`+${range.images} image${range.images === 1 ? "" : "s"}`);
|
|
4793
|
+
if (range.files > 0) parts.push(`+${range.files} file${range.files === 1 ? "" : "s"}`);
|
|
4794
|
+
return ` [${parts.join(" | ")}]`;
|
|
4795
|
+
}
|
|
4796
|
+
function meterMediaPriceResolver(agent, session) {
|
|
4797
|
+
let prices = null;
|
|
4798
|
+
return (seq) => {
|
|
4799
|
+
if (prices === null) prices = mediaPriceViaMeter(session, agent.ctx);
|
|
4800
|
+
return prices.get(seq) ?? 0;
|
|
4801
|
+
};
|
|
4802
|
+
}
|
|
4803
|
+
function rangeTable(session, kernelView, prompts = DEFAULT_RESOLVED, mediaPriceOf) {
|
|
4804
|
+
const ranges = buildCompressibleSeqRanges(
|
|
4805
|
+
session,
|
|
4806
|
+
kernelView,
|
|
4807
|
+
mediaPriceOf === void 0 ? {} : { mediaPriceOf }
|
|
4808
|
+
).slice(0, 6);
|
|
3773
4809
|
if (ranges.length === 0) return "";
|
|
3774
4810
|
const lines = ranges.map(
|
|
3775
4811
|
(range) => renderTemplate(prompts.rangeTable.line, {
|
|
@@ -3778,7 +4814,8 @@ function rangeTable(session, prompts = DEFAULT_RESOLVED) {
|
|
|
3778
4814
|
count: range.count,
|
|
3779
4815
|
tokens: range.tokens,
|
|
3780
4816
|
toolPct: range.toolPct,
|
|
3781
|
-
textPct: 100 - range.toolPct
|
|
4817
|
+
textPct: 100 - range.toolPct,
|
|
4818
|
+
media: mediaSuffixOf(range)
|
|
3782
4819
|
})
|
|
3783
4820
|
);
|
|
3784
4821
|
return [
|
|
@@ -3793,44 +4830,93 @@ function rangeTable(session, prompts = DEFAULT_RESOLVED) {
|
|
|
3793
4830
|
function measuredTokenCount(agent, coreMessages) {
|
|
3794
4831
|
return resolveTokenCount(agent, coreMessages);
|
|
3795
4832
|
}
|
|
3796
|
-
function
|
|
4833
|
+
function computeSurfaceBreakdown(state, messages, total, growth) {
|
|
4834
|
+
let system = 0;
|
|
4835
|
+
let tool = 0;
|
|
4836
|
+
let code = 0;
|
|
4837
|
+
let text = 0;
|
|
4838
|
+
for (const message of messages) {
|
|
4839
|
+
const tokens = defaultCountTokens(message.text ?? "");
|
|
4840
|
+
if (message.contentType === "tool-call" || message.contentType === "tool-result") {
|
|
4841
|
+
tool += tokens;
|
|
4842
|
+
} else if (message.role === "system") {
|
|
4843
|
+
system += tokens;
|
|
4844
|
+
} else if ((message.text ?? "").includes("```")) {
|
|
4845
|
+
code += tokens;
|
|
4846
|
+
} else {
|
|
4847
|
+
text += tokens;
|
|
4848
|
+
}
|
|
4849
|
+
}
|
|
4850
|
+
let summaries = 0;
|
|
4851
|
+
for (const block of state.blocks) {
|
|
4852
|
+
if (block.active) summaries += defaultCountTokens(block.summary);
|
|
4853
|
+
}
|
|
4854
|
+
return { system, tool, summaries, code, text, total, growth };
|
|
4855
|
+
}
|
|
4856
|
+
var EMERGENCY_NUDGE_MAX_PER_TURN = 3;
|
|
4857
|
+
function buildNudge(agent, env, lastNudgeTurn, emergencyNudges, onEmergencyCapHit) {
|
|
3797
4858
|
const session = agent.session;
|
|
3798
4859
|
const state = env.store.stateFor(session);
|
|
3799
4860
|
const coreMessages = allLogMessages(session);
|
|
3800
|
-
const
|
|
4861
|
+
const surfaceEvents = surfaceEventsOf(session);
|
|
4862
|
+
const surfaceMessages = eventsToCoreMessages(surfaceEvents);
|
|
3801
4863
|
const tokenCount = measuredTokenCount(agent, surfaceMessages);
|
|
3802
4864
|
const config = kernelConfigFor(env);
|
|
3803
4865
|
const turn = env.kernel.processTurn({ messages: coreMessages, state, config, tokenCount });
|
|
3804
4866
|
env.store.set(session, turn.state);
|
|
3805
4867
|
const nudge = turn.nudge;
|
|
3806
4868
|
if (nudge === void 0 || !nudge.shouldInject) return null;
|
|
4869
|
+
const mediaPriceOf = meterMediaPriceResolver(agent, session);
|
|
4870
|
+
const statusMessages = eventsToCoreMessages(
|
|
4871
|
+
surfaceEvents.filter((event) => isCheckpointNode(event) === false)
|
|
4872
|
+
);
|
|
4873
|
+
nudge.contextBreakdown = computeSurfaceBreakdown(turn.state, statusMessages, tokenCount, nudge.contextBreakdown?.growth ?? 0);
|
|
3807
4874
|
const emergency = nudge.breakdown?.emergencyOverride === 1;
|
|
3808
4875
|
const turnNumber = findOpenTurn(sessionEventsOf(session)) ?? 0;
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
4876
|
+
if (!emergency) {
|
|
4877
|
+
if (lastNudgeTurn.get(session.id) === turnNumber) return null;
|
|
4878
|
+
lastNudgeTurn.set(session.id, turnNumber);
|
|
4879
|
+
} else {
|
|
4880
|
+
const record = emergencyNudges.get(session.id);
|
|
4881
|
+
if (record !== void 0 && record.turn === turnNumber) {
|
|
4882
|
+
if (record.count >= EMERGENCY_NUDGE_MAX_PER_TURN) {
|
|
4883
|
+
onEmergencyCapHit?.();
|
|
4884
|
+
return null;
|
|
4885
|
+
}
|
|
4886
|
+
record.count += 1;
|
|
4887
|
+
} else {
|
|
4888
|
+
emergencyNudges.set(session.id, { turn: turnNumber, count: 1 });
|
|
4889
|
+
}
|
|
4890
|
+
}
|
|
4891
|
+
const text = buildNudgeText(
|
|
4892
|
+
nudge,
|
|
4893
|
+
emergency,
|
|
4894
|
+
session,
|
|
4895
|
+
kernelRangeViewOf(nudge, turn.state),
|
|
4896
|
+
env.prompts,
|
|
4897
|
+
mediaPriceOf
|
|
4898
|
+
);
|
|
3813
4899
|
const message = createUserMessage2({
|
|
3814
4900
|
content: [{ type: "text", text }],
|
|
3815
4901
|
source: { kind: "plugin", plugin: "acp-nudge" }
|
|
3816
4902
|
});
|
|
3817
4903
|
return { message, emergency };
|
|
3818
4904
|
}
|
|
3819
|
-
function buildNudgeText(nudge, emergency, session, prompts = DEFAULT_RESOLVED) {
|
|
4905
|
+
function buildNudgeText(nudge, emergency, session, kernelView, prompts = DEFAULT_RESOLVED, mediaPriceOf) {
|
|
3820
4906
|
if (prompts.nudge !== DEFAULT_RESOLVED.nudge) {
|
|
3821
|
-
return renderNudgeFromTemplates(nudge, emergency, session, prompts);
|
|
4907
|
+
return renderNudgeFromTemplates(nudge, emergency, session, kernelView, prompts, mediaPriceOf);
|
|
3822
4908
|
}
|
|
3823
4909
|
const rendered = renderNudgeText(nudge);
|
|
3824
|
-
return adaptKernelNudgeToSeq(rendered.text, nudge, session, prompts);
|
|
4910
|
+
return adaptKernelNudgeToSeq(rendered.text, nudge, session, kernelView, prompts, mediaPriceOf);
|
|
3825
4911
|
}
|
|
3826
|
-
function adaptKernelNudgeToSeq(text, nudge, session, prompts) {
|
|
3827
|
-
let out = text;
|
|
4912
|
+
function adaptKernelNudgeToSeq(text, nudge, session, kernelView, prompts, mediaPriceOf) {
|
|
4913
|
+
let out = stripNudgeGuidance(text);
|
|
3828
4914
|
if ((nudge.tier === 2 || nudge.tier === 3) && (nudge.tierTargetBlocks?.length ?? 0) > 0) {
|
|
3829
4915
|
out = replaceTierTrigger(out, nudge, session, prompts);
|
|
3830
4916
|
} else if (out.includes('"startId"')) {
|
|
3831
4917
|
out = replaceEmergencyExample(out);
|
|
3832
4918
|
}
|
|
3833
|
-
const seqTable = rangeTable(session, prompts);
|
|
4919
|
+
const seqTable = rangeTable(session, kernelView, prompts, mediaPriceOf);
|
|
3834
4920
|
if (seqTable !== "") out = replaceRangesStr(out, seqTable);
|
|
3835
4921
|
return out;
|
|
3836
4922
|
}
|
|
@@ -3875,7 +4961,7 @@ function replaceEmergencyExample(text) {
|
|
|
3875
4961
|
const end = next !== null ? start + 2 + next.index : text.length;
|
|
3876
4962
|
return text.slice(0, start) + "\n\ncompress({ content: [{ startSeq, endSeq, summary }] }) \u2014 use the seqs from the range table above." + text.slice(end);
|
|
3877
4963
|
}
|
|
3878
|
-
function renderNudgeFromTemplates(nudge, emergency, session, prompts) {
|
|
4964
|
+
function renderNudgeFromTemplates(nudge, emergency, session, kernelView, prompts, mediaPriceOf) {
|
|
3879
4965
|
const pct2 = Math.round(Math.min(nudge.contextUsage, 1) * 100);
|
|
3880
4966
|
const frame = renderTemplate(
|
|
3881
4967
|
emergency ? prompts.nudge.emergency : prompts.nudge.normal,
|
|
@@ -3916,10 +5002,68 @@ function renderNudgeFromTemplates(nudge, emergency, session, prompts) {
|
|
|
3916
5002
|
const tierRules = nudge.tier === 2 ? TIER2_DISTILL_RULES : TIER3_CONDENSE_RULES;
|
|
3917
5003
|
parts.push("", tierRules);
|
|
3918
5004
|
} else {
|
|
3919
|
-
parts.push(rangeTable(session, prompts));
|
|
5005
|
+
parts.push(rangeTable(session, kernelView, prompts, mediaPriceOf));
|
|
3920
5006
|
}
|
|
3921
5007
|
if (prompts.nudge.tip !== "") parts.push("", prompts.nudge.tip);
|
|
3922
|
-
return parts.join("\n");
|
|
5008
|
+
return stripNudgeGuidance(parts.join("\n"));
|
|
5009
|
+
}
|
|
5010
|
+
|
|
5011
|
+
// src/window.ts
|
|
5012
|
+
var DEFAULT_CONTEXT_WINDOW = 128e3;
|
|
5013
|
+
function windowSourceLabel(window) {
|
|
5014
|
+
if (window.source === "explicit") return "configured";
|
|
5015
|
+
if (window.source === "projection") {
|
|
5016
|
+
return `session projection current route (auto-refreshes on model switch)`;
|
|
5017
|
+
}
|
|
5018
|
+
if (window.source === "auto") {
|
|
5019
|
+
return `auto-detected from ${window.provider ?? "?"}/${window.model ?? "?"}`;
|
|
5020
|
+
}
|
|
5021
|
+
if (window.probeFailed === true) return "default (auto-detection failed \u2014 see /acp config)";
|
|
5022
|
+
return "default (auto-detection unavailable)";
|
|
5023
|
+
}
|
|
5024
|
+
function projectedContextWindow(agent) {
|
|
5025
|
+
const projections = agent.ctx?.get?.("sessionProjections");
|
|
5026
|
+
const window = projections?.snapshot?.(agent.session)?.values?.contextPressure?.contextWindow;
|
|
5027
|
+
if (typeof window === "number" && Number.isInteger(window) && window > 0) return window;
|
|
5028
|
+
return null;
|
|
5029
|
+
}
|
|
5030
|
+
function liveRoute(agent) {
|
|
5031
|
+
let rc;
|
|
5032
|
+
try {
|
|
5033
|
+
rc = agent.session.requestContext();
|
|
5034
|
+
} catch {
|
|
5035
|
+
return null;
|
|
5036
|
+
}
|
|
5037
|
+
if (rc === void 0 || rc === null) return null;
|
|
5038
|
+
const { provider, model } = rc;
|
|
5039
|
+
if (typeof provider !== "string" || provider === "") return null;
|
|
5040
|
+
if (typeof model !== "string" || model === "") return null;
|
|
5041
|
+
return { provider, model };
|
|
5042
|
+
}
|
|
5043
|
+
function routeFor(agent) {
|
|
5044
|
+
const live = liveRoute(agent);
|
|
5045
|
+
return {
|
|
5046
|
+
provider: live?.provider ?? agent.options.provider ?? "",
|
|
5047
|
+
model: live?.model ?? agent.options.model ?? ""
|
|
5048
|
+
};
|
|
5049
|
+
}
|
|
5050
|
+
async function probeModelWindow(agent, provider, model) {
|
|
5051
|
+
const llm = agent.ctx?.get?.("llm");
|
|
5052
|
+
if (llm?.resolveModelInfo === void 0) return { contextWindow: null, outputReservation: null };
|
|
5053
|
+
try {
|
|
5054
|
+
const info = await llm.resolveModelInfo(provider, model);
|
|
5055
|
+
const window = info?.context?.contextWindow;
|
|
5056
|
+
const cap = info?.defaultMaxTokens;
|
|
5057
|
+
return {
|
|
5058
|
+
contextWindow: typeof window === "number" && Number.isInteger(window) && window > 0 ? window : null,
|
|
5059
|
+
outputReservation: typeof cap === "number" && Number.isInteger(cap) && cap > 0 ? cap : null
|
|
5060
|
+
};
|
|
5061
|
+
} catch {
|
|
5062
|
+
return { contextWindow: null, outputReservation: null };
|
|
5063
|
+
}
|
|
5064
|
+
}
|
|
5065
|
+
async function detectContextWindow(agent, provider, model) {
|
|
5066
|
+
return (await probeModelWindow(agent, provider, model)).contextWindow;
|
|
3923
5067
|
}
|
|
3924
5068
|
|
|
3925
5069
|
// src/tools.ts
|
|
@@ -3987,7 +5131,20 @@ var compressParameters = {
|
|
|
3987
5131
|
]
|
|
3988
5132
|
},
|
|
3989
5133
|
summary: { type: "string", required: true, description: "Complete technical summary replacing the range; keep paths, decisions, values verbatim. Minimum 50 characters." },
|
|
3990
|
-
topic: { type: "string", description: "Short label (3-5 words) for this range." }
|
|
5134
|
+
topic: { type: "string", description: "Short label (3-5 words) for this range." },
|
|
5135
|
+
// B3 (2026-09-08 governance plan): the handler and region.ts have
|
|
5136
|
+
// accepted verifiedReadings since the plan landed, but the declared
|
|
5137
|
+
// parameter schema did not list it — `additionalProperties: false`
|
|
5138
|
+
// then rejected every live call that carried it
|
|
5139
|
+
// (`invalid arguments: "content[0].verifiedReadings" is not a declared
|
|
5140
|
+
// property`), so the structured-loss-stopping field was unreachable
|
|
5141
|
+
// from the model's tool interface. Declared here; additionalProperties
|
|
5142
|
+
// stays false so unknown fields are still rejected.
|
|
5143
|
+
verifiedReadings: {
|
|
5144
|
+
type: "array",
|
|
5145
|
+
items: { type: "string" },
|
|
5146
|
+
description: 'Optional: acceptance readings that are already green before this compression (e.g. "t0-fastpath 8/8", "closedloop 414/414"). Stored structurally on the compaction/summary event and recovered by verifiedReadingsOf, so later steps need not re-run the checks.'
|
|
5147
|
+
}
|
|
3991
5148
|
},
|
|
3992
5149
|
additionalProperties: false
|
|
3993
5150
|
}
|
|
@@ -4069,6 +5226,21 @@ function validateContentItems(content) {
|
|
|
4069
5226
|
});
|
|
4070
5227
|
if (violations.length > 0) throw new ToolArgsError(violations);
|
|
4071
5228
|
}
|
|
5229
|
+
function guardedRowsInSpan(guarded, shadowed) {
|
|
5230
|
+
const inSpan = new Set(shadowed);
|
|
5231
|
+
return [...guarded].filter((seq) => inSpan.has(seq)).sort((a, b) => a - b);
|
|
5232
|
+
}
|
|
5233
|
+
function protectedRowRejectionNote(start, end, hits, shadowed) {
|
|
5234
|
+
const preview = hits.slice(0, 4).join(", ");
|
|
5235
|
+
const more = hits.length > 4 ? ` +${hits.length - 4} more` : "";
|
|
5236
|
+
const first = shadowed.indexOf(hits[0]);
|
|
5237
|
+
const last = shadowed.indexOf(hits[hits.length - 1]);
|
|
5238
|
+
const before = first > 0 ? shadowed.slice(0, first) : [];
|
|
5239
|
+
const after = last >= 0 && last < shadowed.length - 1 ? shadowed.slice(last + 1) : [];
|
|
5240
|
+
const slices = [before, after].filter((slice) => slice.length > 0).map((slice) => `${slice[0]}..${slice[slice.length - 1]}`);
|
|
5241
|
+
const recovery = slices.length === 0 ? "no part of this span is compressible while those rows are current \u2014 pick an OLDER span instead (acp_status lists the live ranges)" : `the compressible part of this span is seq ${slices.join(" and ")} \u2014 submit them as separate content entries (or two compress calls), each with its own summary`;
|
|
5242
|
+
return ` seqs ${start}..${end} rejected \u2014 the span covers ${hits.length} CURRENT injected instruction row(s) (seq ${preview}${more}); the host re-injects the newest AGENTS.md copy the moment it leaves the surface, so compressing it reclaims nothing \u2014 ${recovery} (older/stale copies of the same file are fine to compress)`;
|
|
5243
|
+
}
|
|
4072
5244
|
async function handleCompress(env, args, exec) {
|
|
4073
5245
|
const agent = requireAgent(exec);
|
|
4074
5246
|
const session = agent.session;
|
|
@@ -4093,6 +5265,8 @@ async function handleCompress(env, args, exec) {
|
|
|
4093
5265
|
validateContentItems(args.content);
|
|
4094
5266
|
const ranges = [];
|
|
4095
5267
|
const alreadyCompressedNotes = [];
|
|
5268
|
+
const rejectedNotes = [];
|
|
5269
|
+
const guardedSeqs = guardedSurfaceSeqsOf(session);
|
|
4096
5270
|
for (const range of args.content) {
|
|
4097
5271
|
const startSeq = parseBoundary2(range.startSeq, byRef);
|
|
4098
5272
|
const endSeq = parseBoundary2(range.endSeq, byRef);
|
|
@@ -4110,6 +5284,12 @@ async function handleCompress(env, args, exec) {
|
|
|
4110
5284
|
}
|
|
4111
5285
|
throw error;
|
|
4112
5286
|
}
|
|
5287
|
+
const shadowedSpan = shadowedSeqsOf(session, resolved.start, resolved.end);
|
|
5288
|
+
const instructionHits = guardedRowsInSpan(guardedSeqs, shadowedSpan);
|
|
5289
|
+
if (instructionHits.length > 0) {
|
|
5290
|
+
rejectedNotes.push(protectedRowRejectionNote(resolved.start, resolved.end, instructionHits, shadowedSpan));
|
|
5291
|
+
continue;
|
|
5292
|
+
}
|
|
4113
5293
|
const startBlockRef = blockRefForSummarySeq(session, resolved.start);
|
|
4114
5294
|
const endBlockRef = blockRefForSummarySeq(session, resolved.end);
|
|
4115
5295
|
const startRef = startBlockRef ?? byRaw[String(resolved.start)];
|
|
@@ -4125,14 +5305,18 @@ async function handleCompress(env, args, exec) {
|
|
|
4125
5305
|
endSeq,
|
|
4126
5306
|
startRef,
|
|
4127
5307
|
endRef,
|
|
5308
|
+
// B3:把该段声明的已绿验收读数带上(缺位=不写键)
|
|
5309
|
+
...Array.isArray(range.verifiedReadings) && range.verifiedReadings.length > 0 ? { verifiedReadings: range.verifiedReadings.map(String) } : {},
|
|
4128
5310
|
summary: range.summary,
|
|
4129
5311
|
...(range.topic ?? args.topic) === void 0 ? {} : { topic: range.topic ?? args.topic }
|
|
4130
5312
|
});
|
|
4131
5313
|
}
|
|
4132
5314
|
if (ranges.length === 0) {
|
|
4133
|
-
const text = ["Compressed 0 block(s), ~0 tokens reclaimed.", ...alreadyCompressedNotes];
|
|
5315
|
+
const text = ["Compressed 0 block(s), ~0 tokens reclaimed.", ...alreadyCompressedNotes, ...rejectedNotes];
|
|
4134
5316
|
if (alreadyCompressedNotes.length > 0) {
|
|
4135
5317
|
text.push(" (all requested ranges were already compressed \u2014 decompress a block to recover its originals)");
|
|
5318
|
+
} else if (rejectedNotes.length > 0) {
|
|
5319
|
+
text.push(" (nothing compressed \u2014 every range covered a current injected instruction row; see the rejections above)");
|
|
4136
5320
|
}
|
|
4137
5321
|
return { text: text.join("\n") };
|
|
4138
5322
|
}
|
|
@@ -4185,14 +5369,15 @@ async function handleCompress(env, args, exec) {
|
|
|
4185
5369
|
const shadowedTokens = shadowedTokensViaMeter(session, shadowed, agent.ctx);
|
|
4186
5370
|
const tier = block.tier === 2 || block.tier === 3 ? block.tier : 1;
|
|
4187
5371
|
const parentBlockIds = compactionIdsOfKernelBlocks(session, block.directBlockIds);
|
|
5372
|
+
const { provider, model } = routeFor(agent);
|
|
4188
5373
|
const { compactionId } = runCompactionTransaction(session, {
|
|
4189
5374
|
start,
|
|
4190
5375
|
end,
|
|
4191
5376
|
shadowedSeqs: shadowed,
|
|
4192
5377
|
summary: [{ type: "text", text: range.summary }],
|
|
4193
5378
|
shadowedTokenCount: shadowedTokens,
|
|
4194
|
-
provider
|
|
4195
|
-
model
|
|
5379
|
+
provider,
|
|
5380
|
+
model,
|
|
4196
5381
|
tier,
|
|
4197
5382
|
kernelBlockId: block.blockId,
|
|
4198
5383
|
...range.topic === void 0 ? {} : { topic: range.topic },
|
|
@@ -4201,25 +5386,36 @@ async function handleCompress(env, args, exec) {
|
|
|
4201
5386
|
// rehydrates the SAME effective messages (a tier-2 block's coverage is
|
|
4202
5387
|
// its parents' originals, not the checkpoint node).
|
|
4203
5388
|
directMessageIds: block.directMessageIds,
|
|
4204
|
-
effectiveMessageIds: block.effectiveMessageIds
|
|
5389
|
+
effectiveMessageIds: block.effectiveMessageIds,
|
|
5390
|
+
// B3:已绿验收读数随压缩块落盘(缺位=不写键)
|
|
5391
|
+
...range.verifiedReadings === void 0 ? {} : { verifiedReadings: range.verifiedReadings }
|
|
4205
5392
|
});
|
|
4206
5393
|
const adjusted = start !== range.startSeq || end !== range.endSeq;
|
|
4207
5394
|
const tierLabel2 = `, tier ${tier}`;
|
|
5395
|
+
const readingsLabel = range.verifiedReadings !== void 0 && range.verifiedReadings.length > 0 ? `, verified: ${range.verifiedReadings.join("; ")}` : "";
|
|
4208
5396
|
const note = range.recovered === true ? ` (seqs ${range.startSeq}..${range.endSeq} were already shadowed \u2014 compressed the live remainder ${start}..${end})` : adjusted ? ` (adjusted from ${range.startSeq}..${range.endSeq} to balanced edges)` : "";
|
|
4209
5397
|
lines.push(
|
|
4210
|
-
` block ${compactionId.slice(0, 8)}: seqs ${start}..${end}, ${shadowed.length} messages shadowed${tierLabel2}${note}`
|
|
5398
|
+
` block ${compactionId.slice(0, 8)}: seqs ${start}..${end}, ${shadowed.length} messages shadowed${tierLabel2}${readingsLabel}${note}`
|
|
4211
5399
|
);
|
|
4212
5400
|
}
|
|
4213
5401
|
const summaryLine = `Compressed ${applied.result.blocksCreated} block(s), ~${applied.result.tokensCompressed} tokens reclaimed.`;
|
|
4214
|
-
const totalSkipped = skippedRanges + alreadyCompressedNotes.length;
|
|
5402
|
+
const totalSkipped = skippedRanges + alreadyCompressedNotes.length + rejectedNotes.length;
|
|
4215
5403
|
const failedLines = applied.result.errors.map((error) => ` ${error}`);
|
|
4216
|
-
const warningLines = [
|
|
5404
|
+
const warningLines = [
|
|
5405
|
+
...freeWarnings.map((warning) => ` ${warning}`),
|
|
5406
|
+
...failedLines,
|
|
5407
|
+
...alreadyCompressedNotes,
|
|
5408
|
+
...rejectedNotes,
|
|
5409
|
+
...lines
|
|
5410
|
+
];
|
|
4217
5411
|
const footer = totalSkipped > 0 ? ` (${totalSkipped} range(s) skipped or failed \u2014 see above)` : "";
|
|
4218
5412
|
return { text: `${summaryLine}
|
|
4219
5413
|
${[...warningLines, footer].filter((line) => line !== "").join("\n")}` };
|
|
4220
5414
|
}
|
|
4221
5415
|
var decompressParameters = {
|
|
4222
|
-
blockId: { type: "string", required: true, description: "Block id: the kernel block ref `bN` shown by acp_status (e.g. b1), or a compaction id / prefix from search_context." }
|
|
5416
|
+
blockId: { type: "string", required: true, description: "Block id: the kernel block ref `bN` shown by acp_status (e.g. b1), or a compaction id / prefix from search_context." },
|
|
5417
|
+
offset: { type: "integer", description: "Start position in the block's message list (default 0). Blocks are paged by size \u2014 each page stays under the host tool-result trim budget (up to 100 messages) \u2014 so follow the continue hint in the result to walk the rest." },
|
|
5418
|
+
limit: { type: "integer", description: "Messages per page (default 100; values above 100 are capped to 100). Pages are also bounded by a character budget, so long messages return fewer than this per call." }
|
|
4223
5419
|
};
|
|
4224
5420
|
function resolveBlockId(session, arg) {
|
|
4225
5421
|
const byKernelRef = blockIdOfKernelRef(session, arg);
|
|
@@ -4240,17 +5436,40 @@ function handleDecompress(_env, rawArgs, exec) {
|
|
|
4240
5436
|
if (block === void 0) {
|
|
4241
5437
|
return { text: `decompress: block "${args.blockId}" not found (see acp_status for the block list)` };
|
|
4242
5438
|
}
|
|
5439
|
+
const expanded = expandShadowedSeqs(session, block.blockId);
|
|
5440
|
+
const page = sliceDecompressPage(
|
|
5441
|
+
expanded,
|
|
5442
|
+
args.offset ?? 0,
|
|
5443
|
+
args.limit ?? DEFAULT_DECOMPRESS_PAGE,
|
|
5444
|
+
DEFAULT_DECOMPRESS_PAGE_CHARS,
|
|
5445
|
+
(seq) => {
|
|
5446
|
+
const event = eventAtOf(session, seq);
|
|
5447
|
+
const text = event === void 0 ? "" : extractEventText(event);
|
|
5448
|
+
return text.length === 0 ? 0 : `[seq ${seq}] ${text}`.length;
|
|
5449
|
+
}
|
|
5450
|
+
);
|
|
5451
|
+
if (page.total === 0 || page.seqs.length === 0) {
|
|
5452
|
+
const where = page.total === 0 ? "" : ` has ${page.total} messages; offset ${page.offset} is past the end \u2014 use an offset below ${page.total}, or omit it`;
|
|
5453
|
+
return { text: page.total === 0 ? `Block ${block.blockId} \u2014 ${block.summary}
|
|
5454
|
+
|
|
5455
|
+
(no recoverable content)` : `decompress: block ${block.blockId}${where}` };
|
|
5456
|
+
}
|
|
4243
5457
|
const parts = [];
|
|
4244
|
-
for (const seq of
|
|
5458
|
+
for (const seq of page.seqs) {
|
|
4245
5459
|
const event = eventAtOf(session, seq);
|
|
4246
5460
|
const text = event === void 0 ? "" : extractEventText(event);
|
|
4247
5461
|
if (text.length > 0) parts.push(`[seq ${seq}] ${text}`);
|
|
4248
5462
|
}
|
|
4249
5463
|
const tierNote = block.tier > 1 ? ` (tier ${block.tier}, distills ${block.parentBlockIds.length} block(s))` : "";
|
|
5464
|
+
const lines = [];
|
|
5465
|
+
lines.push(`[messages ${page.offset + 1}..${page.offset + page.seqs.length} of ${page.total}]`);
|
|
5466
|
+
if (!page.exhausted) lines.push(`More available \u2014 continue with decompress({ blockId: "${block.blockId}", offset: ${page.offset + page.seqs.length} })`);
|
|
4250
5467
|
return {
|
|
4251
5468
|
text: `Block ${block.blockId} \u2014 ${block.summary}${tierNote}
|
|
4252
5469
|
|
|
4253
|
-
${
|
|
5470
|
+
${lines.join("\n")}
|
|
5471
|
+
|
|
5472
|
+
${parts.join("\n\n") || "(no text content on this page)"}`
|
|
4254
5473
|
};
|
|
4255
5474
|
}
|
|
4256
5475
|
var searchParameters = {
|
|
@@ -4269,8 +5488,12 @@ function roleOfEvent(event) {
|
|
|
4269
5488
|
return null;
|
|
4270
5489
|
}
|
|
4271
5490
|
}
|
|
5491
|
+
var searchDocsCache = /* @__PURE__ */ new WeakMap();
|
|
4272
5492
|
function buildSearchDocs(session) {
|
|
4273
|
-
const
|
|
5493
|
+
const events = sessionEventsOf(session);
|
|
5494
|
+
const cached = searchDocsCache.get(events);
|
|
5495
|
+
if (cached !== void 0) return cached;
|
|
5496
|
+
const ledger = rebuildBlockLedger(events);
|
|
4274
5497
|
const docs = [];
|
|
4275
5498
|
const claimed = /* @__PURE__ */ new Set();
|
|
4276
5499
|
for (const block of ledger) {
|
|
@@ -4303,6 +5526,7 @@ function buildSearchDocs(session) {
|
|
|
4303
5526
|
});
|
|
4304
5527
|
}
|
|
4305
5528
|
}
|
|
5529
|
+
searchDocsCache.set(events, docs);
|
|
4306
5530
|
return docs;
|
|
4307
5531
|
}
|
|
4308
5532
|
function handleSearch(_env, rawArgs, exec) {
|
|
@@ -4348,11 +5572,6 @@ var statusParameters = {
|
|
|
4348
5572
|
description: "Cap on rows or blocks shown (default 30)."
|
|
4349
5573
|
}
|
|
4350
5574
|
};
|
|
4351
|
-
function isCheckpointEvent(event) {
|
|
4352
|
-
if (event.type !== "user/message") return false;
|
|
4353
|
-
const source = event.data.source;
|
|
4354
|
-
return source?.plugin === "compact";
|
|
4355
|
-
}
|
|
4356
5575
|
async function handleStatus(env, rawArgs, exec) {
|
|
4357
5576
|
const args = unwrapEnvelope(rawArgs);
|
|
4358
5577
|
const agent = requireAgent(exec);
|
|
@@ -4367,7 +5586,7 @@ async function handleStatus(env, rawArgs, exec) {
|
|
|
4367
5586
|
const config = kernelConfigFor({ ...env, modelContextLimit: window.limit });
|
|
4368
5587
|
const turn = env.kernel.processTurn({ messages: coreMessages, state, config, tokenCount });
|
|
4369
5588
|
const statusMessages = eventsToCoreMessages(
|
|
4370
|
-
surface.filter((event) =>
|
|
5589
|
+
surface.filter((event) => isCheckpointNode(event) === false),
|
|
4371
5590
|
toolNames
|
|
4372
5591
|
);
|
|
4373
5592
|
const report = buildStatusReport(turn.state, statusMessages, defaultCountTokens, args);
|
|
@@ -4381,6 +5600,16 @@ async function handleStatus(env, rawArgs, exec) {
|
|
|
4381
5600
|
if (checkpointRows.length > 0) {
|
|
4382
5601
|
lines.push("", `Checkpoint seqs (active blocks \u2014 compress a checkpoint seq to distill it): ${checkpointRows.join(", ")}`);
|
|
4383
5602
|
}
|
|
5603
|
+
const mediaOnSurface = surface.some((event) => {
|
|
5604
|
+
const counts = attachmentsOfEvent(event);
|
|
5605
|
+
return counts.images + counts.files > 0;
|
|
5606
|
+
});
|
|
5607
|
+
if (mediaOnSurface) {
|
|
5608
|
+
lines.push(
|
|
5609
|
+
"",
|
|
5610
|
+
"Note: the pressure line is provider-anchored (images/files priced by the live route); the breakdown above is a text-only estimate. They can differ on media-heavy sessions."
|
|
5611
|
+
);
|
|
5612
|
+
}
|
|
4384
5613
|
}
|
|
4385
5614
|
lines.push("", `Surface: ${surfaceSummary(session)}`);
|
|
4386
5615
|
if (args.scope === "uncompressed") {
|
|
@@ -4430,42 +5659,161 @@ function makeTools(env) {
|
|
|
4430
5659
|
];
|
|
4431
5660
|
}
|
|
4432
5661
|
|
|
4433
|
-
// src/
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
5662
|
+
// src/commands.ts
|
|
5663
|
+
import { SettingsConflictError } from "@deepseek-ai/dsh-settings";
|
|
5664
|
+
|
|
5665
|
+
// src/settings.ts
|
|
5666
|
+
import z from "@deepseek-ai/schemastery";
|
|
5667
|
+
var ACP_SETTINGS_NAMESPACE = "compaction-acp";
|
|
5668
|
+
var SETTINGS_KEYS = [
|
|
5669
|
+
"modelContextLimit",
|
|
5670
|
+
"autoModelContextLimit",
|
|
5671
|
+
"nudgeMinContextLimitPct",
|
|
5672
|
+
"nudgeMaxContextLimitPct",
|
|
5673
|
+
"nudgeEmergencyThresholdPct",
|
|
5674
|
+
"autoNudge"
|
|
5675
|
+
];
|
|
5676
|
+
var SETTING_DEFAULTS = {
|
|
5677
|
+
autoModelContextLimit: true,
|
|
5678
|
+
nudgeMaxContextLimitPct: 0.7,
|
|
5679
|
+
nudgeEmergencyThresholdPct: 0.85,
|
|
5680
|
+
autoNudge: true
|
|
5681
|
+
};
|
|
5682
|
+
function filterSettingsEntry(entry) {
|
|
5683
|
+
return {
|
|
5684
|
+
...entry.modelContextLimit !== void 0 ? { modelContextLimit: entry.modelContextLimit } : {},
|
|
5685
|
+
...entry.autoModelContextLimit !== void 0 ? { autoModelContextLimit: entry.autoModelContextLimit } : {},
|
|
5686
|
+
...entry.nudgeMinContextLimitPct !== void 0 ? { nudgeMinContextLimitPct: entry.nudgeMinContextLimitPct } : {},
|
|
5687
|
+
...entry.nudgeMaxContextLimitPct !== void 0 ? { nudgeMaxContextLimitPct: entry.nudgeMaxContextLimitPct } : {},
|
|
5688
|
+
...entry.nudgeEmergencyThresholdPct !== void 0 ? { nudgeEmergencyThresholdPct: entry.nudgeEmergencyThresholdPct } : {},
|
|
5689
|
+
...entry.autoNudge !== void 0 ? { autoNudge: entry.autoNudge } : {}
|
|
5690
|
+
};
|
|
5691
|
+
}
|
|
5692
|
+
function resolveAcpSettings(input) {
|
|
5693
|
+
return {
|
|
5694
|
+
modelContextLimit: input.modelContextLimit,
|
|
5695
|
+
autoModelContextLimit: input.autoModelContextLimit ?? SETTING_DEFAULTS.autoModelContextLimit,
|
|
5696
|
+
nudgeMinContextLimitPct: input.nudgeMinContextLimitPct,
|
|
5697
|
+
nudgeMaxContextLimitPct: input.nudgeMaxContextLimitPct ?? SETTING_DEFAULTS.nudgeMaxContextLimitPct,
|
|
5698
|
+
nudgeEmergencyThresholdPct: input.nudgeEmergencyThresholdPct ?? SETTING_DEFAULTS.nudgeEmergencyThresholdPct,
|
|
5699
|
+
autoNudge: input.autoNudge ?? SETTING_DEFAULTS.autoNudge
|
|
5700
|
+
};
|
|
5701
|
+
}
|
|
5702
|
+
var AcpSettingsSchema = z.object({
|
|
5703
|
+
modelContextLimit: z.number().step(1).min(1),
|
|
5704
|
+
autoModelContextLimit: z.boolean().default(SETTING_DEFAULTS.autoModelContextLimit),
|
|
5705
|
+
nudgeMinContextLimitPct: z.number().min(0).max(1),
|
|
5706
|
+
nudgeMaxContextLimitPct: z.number().min(0).max(1).default(SETTING_DEFAULTS.nudgeMaxContextLimitPct),
|
|
5707
|
+
nudgeEmergencyThresholdPct: z.number().min(0).max(1).default(SETTING_DEFAULTS.nudgeEmergencyThresholdPct),
|
|
5708
|
+
autoNudge: z.boolean().default(SETTING_DEFAULTS.autoNudge)
|
|
5709
|
+
});
|
|
5710
|
+
function describeSettingsChange(prev, next) {
|
|
5711
|
+
const warnings = [];
|
|
5712
|
+
if (next.nudgeMinContextLimitPct !== void 0 && next.nudgeMinContextLimitPct >= next.nudgeMaxContextLimitPct) {
|
|
5713
|
+
warnings.push(
|
|
5714
|
+
`nudgeMinContextLimitPct (${next.nudgeMinContextLimitPct}) >= nudgeMaxContextLimitPct (${next.nudgeMaxContextLimitPct}) \u2014 the lower bound never engages`
|
|
5715
|
+
);
|
|
4439
5716
|
}
|
|
4440
|
-
if (
|
|
4441
|
-
|
|
5717
|
+
if (next.nudgeMaxContextLimitPct >= next.nudgeEmergencyThresholdPct) {
|
|
5718
|
+
warnings.push(
|
|
5719
|
+
`nudgeMaxContextLimitPct (${next.nudgeMaxContextLimitPct}) >= nudgeEmergencyThresholdPct (${next.nudgeEmergencyThresholdPct}) \u2014 the emergency tier loses its headroom`
|
|
5720
|
+
);
|
|
4442
5721
|
}
|
|
4443
|
-
|
|
4444
|
-
|
|
5722
|
+
return {
|
|
5723
|
+
clearWindowCache: prev.modelContextLimit !== next.modelContextLimit || prev.autoModelContextLimit !== next.autoModelContextLimit,
|
|
5724
|
+
clearNudgeDedup: prev.autoNudge === false && next.autoNudge === true,
|
|
5725
|
+
warnings
|
|
5726
|
+
};
|
|
4445
5727
|
}
|
|
4446
|
-
function
|
|
4447
|
-
const
|
|
4448
|
-
|
|
4449
|
-
if (
|
|
4450
|
-
|
|
5728
|
+
function parseSettingValue(raw) {
|
|
5729
|
+
const text = raw.trim();
|
|
5730
|
+
if (text === "true") return { ok: true, value: true };
|
|
5731
|
+
if (text === "false") return { ok: true, value: false };
|
|
5732
|
+
const num = Number(text);
|
|
5733
|
+
if (text !== "" && Number.isFinite(num)) return { ok: true, value: num };
|
|
5734
|
+
if (text === "null") return { ok: true, value: null };
|
|
5735
|
+
return {
|
|
5736
|
+
ok: false,
|
|
5737
|
+
reason: `"${text}" is not a valid value \u2014 use a number (0.65), true/false, or null to reset the key`
|
|
5738
|
+
};
|
|
4451
5739
|
}
|
|
4452
|
-
|
|
4453
|
-
const
|
|
4454
|
-
if (
|
|
4455
|
-
|
|
4456
|
-
const info = await llm.resolveModelInfo(provider, model);
|
|
4457
|
-
const window = info?.context?.contextWindow;
|
|
4458
|
-
const cap = info?.defaultMaxTokens;
|
|
4459
|
-
return {
|
|
4460
|
-
contextWindow: typeof window === "number" && Number.isInteger(window) && window > 0 ? window : null,
|
|
4461
|
-
outputReservation: typeof cap === "number" && Number.isInteger(cap) && cap > 0 ? cap : null
|
|
4462
|
-
};
|
|
4463
|
-
} catch {
|
|
4464
|
-
return { contextWindow: null, outputReservation: null };
|
|
5740
|
+
function requireService(getService) {
|
|
5741
|
+
const service = getService();
|
|
5742
|
+
if (service === void 0) {
|
|
5743
|
+
throw new Error("runtime settings are not available in this process");
|
|
4465
5744
|
}
|
|
5745
|
+
return service;
|
|
4466
5746
|
}
|
|
4467
|
-
|
|
4468
|
-
return
|
|
5747
|
+
function makeSettingsCommandSurface(getService, getSnapshot) {
|
|
5748
|
+
return {
|
|
5749
|
+
get available() {
|
|
5750
|
+
return getService() !== void 0;
|
|
5751
|
+
},
|
|
5752
|
+
snapshot: getSnapshot,
|
|
5753
|
+
describe() {
|
|
5754
|
+
const service = getService();
|
|
5755
|
+
if (service === void 0) return void 0;
|
|
5756
|
+
return service.describe().find((descriptor) => String(descriptor.ns) === ACP_SETTINGS_NAMESPACE);
|
|
5757
|
+
},
|
|
5758
|
+
async update(patch) {
|
|
5759
|
+
await requireService(getService).update(ACP_SETTINGS_NAMESPACE, patch);
|
|
5760
|
+
},
|
|
5761
|
+
async replaceSection(section) {
|
|
5762
|
+
await requireService(getService).replace(ACP_SETTINGS_NAMESPACE, section);
|
|
5763
|
+
}
|
|
5764
|
+
};
|
|
5765
|
+
}
|
|
5766
|
+
|
|
5767
|
+
// src/presets.ts
|
|
5768
|
+
var PRESET_NAMES = [
|
|
5769
|
+
"preserve",
|
|
5770
|
+
"relaxed",
|
|
5771
|
+
"balanced",
|
|
5772
|
+
"efficient",
|
|
5773
|
+
"aggressive"
|
|
5774
|
+
];
|
|
5775
|
+
var PRESETS = {
|
|
5776
|
+
preserve: {
|
|
5777
|
+
label: "keep context as long as possible \u2014 nudge only close to the limit",
|
|
5778
|
+
nudgeMinContextLimitPct: 0.55,
|
|
5779
|
+
nudgeMaxContextLimitPct: 0.78,
|
|
5780
|
+
nudgeEmergencyThresholdPct: 0.93
|
|
5781
|
+
},
|
|
5782
|
+
relaxed: {
|
|
5783
|
+
label: "light-touch compression \u2014 nudges a little earlier than preserve",
|
|
5784
|
+
nudgeMinContextLimitPct: 0.5,
|
|
5785
|
+
nudgeMaxContextLimitPct: 0.75,
|
|
5786
|
+
nudgeEmergencyThresholdPct: 0.9
|
|
5787
|
+
},
|
|
5788
|
+
balanced: {
|
|
5789
|
+
// == the current out-of-the-box engine defaults (kernel min 0.45, engine
|
|
5790
|
+
// max 0.70, engine emergency 0.85): choosing this changes nothing vs today.
|
|
5791
|
+
label: "default balance \u2014 the same thresholds the plugin ships with",
|
|
5792
|
+
nudgeMinContextLimitPct: 0.45,
|
|
5793
|
+
nudgeMaxContextLimitPct: 0.7,
|
|
5794
|
+
nudgeEmergencyThresholdPct: 0.85
|
|
5795
|
+
},
|
|
5796
|
+
efficient: {
|
|
5797
|
+
label: "trim more often \u2014 favors low token usage over keeping full history",
|
|
5798
|
+
nudgeMinContextLimitPct: 0.4,
|
|
5799
|
+
nudgeMaxContextLimitPct: 0.6,
|
|
5800
|
+
nudgeEmergencyThresholdPct: 0.78
|
|
5801
|
+
},
|
|
5802
|
+
aggressive: {
|
|
5803
|
+
label: "lean context \u2014 compresses early and frequently",
|
|
5804
|
+
nudgeMinContextLimitPct: 0.3,
|
|
5805
|
+
nudgeMaxContextLimitPct: 0.5,
|
|
5806
|
+
nudgeEmergencyThresholdPct: 0.7
|
|
5807
|
+
}
|
|
5808
|
+
};
|
|
5809
|
+
function isPresetName(value) {
|
|
5810
|
+
return typeof value === "string" && PRESET_NAMES.includes(value);
|
|
5811
|
+
}
|
|
5812
|
+
function resolvePreset(name) {
|
|
5813
|
+
if (!isPresetName(name)) {
|
|
5814
|
+
throw new Error(`unknown preset "${name}" \u2014 valid presets: ${PRESET_NAMES.join(", ")}`);
|
|
5815
|
+
}
|
|
5816
|
+
return PRESETS[name];
|
|
4469
5817
|
}
|
|
4470
5818
|
|
|
4471
5819
|
// src/commands.ts
|
|
@@ -4486,8 +5834,15 @@ async function statusText(env, agent) {
|
|
|
4486
5834
|
` estimated context: ${estimated} / ${limit} (${Math.round(estimated / limit * 100)}%)`,
|
|
4487
5835
|
windowLine
|
|
4488
5836
|
];
|
|
5837
|
+
if (env.preset !== void 0) {
|
|
5838
|
+
const ov = env.coreOverrides?.nudge;
|
|
5839
|
+
const pct2 = (value) => `${Math.round((value ?? 0) * 100)}%`;
|
|
5840
|
+
lines.push(
|
|
5841
|
+
` preset: ${env.preset} (${PRESETS[env.preset].label}) [min ${pct2(ov?.minContextLimitPct ?? env.nudgeMinContextLimitPct)} \xB7 max ${pct2(ov?.maxContextLimitPct ?? env.nudgeMaxContextLimitPct)} \xB7 emergency ${pct2(ov?.emergencyThresholdPct ?? env.nudgeEmergencyThresholdPct)}]`
|
|
5842
|
+
);
|
|
5843
|
+
}
|
|
4489
5844
|
if (window.probeFailed === true) {
|
|
4490
|
-
lines.push(` \u26A0 window auto-detection failed \u2014 using the ${limit} fallback (restart to re-probe
|
|
5845
|
+
lines.push(` \u26A0 window auto-detection failed \u2014 using the ${limit} fallback (change modelContextLimit or autoModelContextLimit via /acp config \u2014 or restart \u2014 to re-probe)`);
|
|
4491
5846
|
}
|
|
4492
5847
|
const state = structuredClone(env.store.stateFor(session));
|
|
4493
5848
|
const config = kernelConfigFor({ ...env, modelContextLimit: limit });
|
|
@@ -4524,49 +5879,186 @@ function compressText(env, agent, args) {
|
|
|
4524
5879
|
return "/acp compress: the range touches a compressed block summary node \u2014 distill it with the compress tool (seq-based batch), not /acp compress";
|
|
4525
5880
|
}
|
|
4526
5881
|
const shadowed = shadowedSeqsOf(session, start, end);
|
|
5882
|
+
const instructionHits = guardedRowsInSpan(guardedSurfaceSeqsOf(session), shadowed);
|
|
5883
|
+
if (instructionHits.length > 0) {
|
|
5884
|
+
return protectedRowRejectionNote(start, end, instructionHits, shadowed);
|
|
5885
|
+
}
|
|
4527
5886
|
const shadowedTokens = shadowedTokensViaMeter(session, shadowed, agent.ctx);
|
|
5887
|
+
const { provider, model } = routeFor(agent);
|
|
4528
5888
|
const { compactionId } = runCompactionTransaction(session, {
|
|
4529
5889
|
start,
|
|
4530
5890
|
end,
|
|
4531
5891
|
shadowedSeqs: shadowed,
|
|
4532
5892
|
summary: [{ type: "text", text: summary }],
|
|
4533
5893
|
shadowedTokenCount: shadowedTokens,
|
|
4534
|
-
provider
|
|
4535
|
-
model
|
|
5894
|
+
provider,
|
|
5895
|
+
model
|
|
4536
5896
|
});
|
|
4537
5897
|
return `Compressed seqs ${start}..${end} (${shadowed.length} messages) as block ${compactionId.slice(0, 8)}`;
|
|
4538
5898
|
}
|
|
5899
|
+
var DECOMPRESS_USAGE = "/acp decompress <blockId> [offset] [limit]";
|
|
4539
5900
|
function decompressText(_env, agent, args) {
|
|
4540
|
-
if (args.length < 1) return
|
|
5901
|
+
if (args.length < 1) return DECOMPRESS_USAGE;
|
|
5902
|
+
const offset = args[1] === void 0 ? 0 : Number(args[1]);
|
|
5903
|
+
if (!Number.isInteger(offset) || offset < 0) return `${DECOMPRESS_USAGE} \u2014 offset must be a non-negative integer`;
|
|
5904
|
+
const limit = args[2] === void 0 ? DEFAULT_DECOMPRESS_PAGE : Number(args[2]);
|
|
5905
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > DEFAULT_DECOMPRESS_PAGE) return `${DECOMPRESS_USAGE} \u2014 limit must be an integer between 1 and ${DEFAULT_DECOMPRESS_PAGE}`;
|
|
4541
5906
|
const session = agent.session;
|
|
4542
5907
|
const blockId = blockIdOfKernelRef(session, args[0]);
|
|
4543
5908
|
const ledger = rebuildBlockLedger(sessionEventsOf(session));
|
|
4544
5909
|
const block = blockId === null ? ledger.find((entry) => entry.blockId.startsWith(args[0])) : ledger.find((entry) => entry.blockId === blockId);
|
|
4545
5910
|
if (block === void 0) return `block "${args[0]}" not found (see /acp status)`;
|
|
4546
|
-
const
|
|
4547
|
-
|
|
5911
|
+
const expanded = expandShadowedSeqs(session, block.blockId);
|
|
5912
|
+
const page = sliceDecompressPage(
|
|
5913
|
+
expanded,
|
|
5914
|
+
offset,
|
|
5915
|
+
limit,
|
|
5916
|
+
DEFAULT_DECOMPRESS_PAGE_CHARS,
|
|
5917
|
+
(seq) => extractEventText(eventAtOf(session, seq)).length
|
|
5918
|
+
);
|
|
5919
|
+
if (page.seqs.length === 0) {
|
|
5920
|
+
if (page.total === 0) return `Block ${block.blockId} \u2014 ${block.summary}
|
|
4548
5921
|
|
|
4549
|
-
|
|
5922
|
+
(no recoverable content)`;
|
|
5923
|
+
return `block ${block.blockId} has ${page.total} messages; offset ${offset} is past the end \u2014 use an offset below ${page.total}`;
|
|
5924
|
+
}
|
|
5925
|
+
const parts = page.seqs.map((seq) => extractEventText(eventAtOf(session, seq))).filter((text) => text.length > 0);
|
|
5926
|
+
const lines = [
|
|
5927
|
+
`Block ${block.blockId} \u2014 ${block.summary}`,
|
|
5928
|
+
`[messages ${page.offset + 1}..${page.offset + page.seqs.length} of ${page.total}]`
|
|
5929
|
+
];
|
|
5930
|
+
if (!page.exhausted) lines.push(`Continue with: /acp decompress ${block.blockId.slice(0, 8)} ${page.offset + page.seqs.length}`);
|
|
5931
|
+
lines.push("", parts.join("\n\n") || "(no recoverable content)");
|
|
5932
|
+
return lines.join("\n");
|
|
4550
5933
|
}
|
|
4551
5934
|
function acpCommand(env) {
|
|
4552
5935
|
return {
|
|
4553
5936
|
name: "acp",
|
|
4554
|
-
description: "Active Context Pruning \u2014 model-driven context compression. Usage: /acp status | /acp compress <startSeq> <endSeq> <summary> | /acp decompress <blockId>",
|
|
5937
|
+
description: "Active Context Pruning \u2014 model-driven context compression. Usage: /acp status | /acp compress <startSeq> <endSeq> <summary> | /acp decompress <blockId> [offset] [limit] | /acp config [list|set <key> <value>|reset <key>|all]",
|
|
4555
5938
|
handler: async (invocation) => {
|
|
4556
5939
|
const raw = invocation.rawInput.trim();
|
|
4557
5940
|
if (raw === "" || raw === "status") {
|
|
4558
5941
|
return { kind: "success", text: await statusText(env, invocation.agent) };
|
|
4559
5942
|
}
|
|
5943
|
+
if (raw === "config" || raw.startsWith("config ")) {
|
|
5944
|
+
return { kind: "success", text: await configText(env, raw.slice("config".length).trim()) };
|
|
5945
|
+
}
|
|
4560
5946
|
if (raw.startsWith("compress")) {
|
|
4561
5947
|
return { kind: "success", text: compressText(env, invocation.agent, raw.slice("compress".length).trim().split(/\s+/)) };
|
|
4562
5948
|
}
|
|
4563
5949
|
if (raw.startsWith("decompress")) {
|
|
4564
5950
|
return { kind: "success", text: decompressText(env, invocation.agent, raw.slice("decompress".length).trim().split(/\s+/)) };
|
|
4565
5951
|
}
|
|
4566
|
-
return { kind: "error", text: `unknown /acp subcommand "${raw.split(/\s+/)[0]}" \u2014 use status | compress | decompress` };
|
|
5952
|
+
return { kind: "error", text: `unknown /acp subcommand "${raw.split(/\s+/)[0]}" \u2014 use status | compress | decompress | config` };
|
|
4567
5953
|
}
|
|
4568
5954
|
};
|
|
4569
5955
|
}
|
|
5956
|
+
function isRecord(value) {
|
|
5957
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5958
|
+
}
|
|
5959
|
+
function isSettingsKey(key) {
|
|
5960
|
+
return SETTINGS_KEYS.includes(key);
|
|
5961
|
+
}
|
|
5962
|
+
function settingsWriteFailure(error) {
|
|
5963
|
+
if (error instanceof SettingsConflictError) {
|
|
5964
|
+
return "conflict: another writer changed this setting at the same time \u2014 run /acp config again";
|
|
5965
|
+
}
|
|
5966
|
+
return `rejected: ${String(error)}`;
|
|
5967
|
+
}
|
|
5968
|
+
function formatSettingsValue(key, value) {
|
|
5969
|
+
if (value === void 0) {
|
|
5970
|
+
if (key === "modelContextLimit") return "auto";
|
|
5971
|
+
if (key === "nudgeMinContextLimitPct") return "0.45 (kernel)";
|
|
5972
|
+
return "\u2014";
|
|
5973
|
+
}
|
|
5974
|
+
return String(value);
|
|
5975
|
+
}
|
|
5976
|
+
function configListText(surface) {
|
|
5977
|
+
if (surface === void 0) return "runtime settings are not wired in this engine build";
|
|
5978
|
+
const snapshot = surface.snapshot();
|
|
5979
|
+
const descriptor = surface.describe();
|
|
5980
|
+
const lines = [
|
|
5981
|
+
'ACP runtime settings \u2014 namespace "compaction-acp"',
|
|
5982
|
+
" key value source"
|
|
5983
|
+
];
|
|
5984
|
+
for (const key of SETTINGS_KEYS) {
|
|
5985
|
+
const userSection = isRecord(descriptor?.user) ? descriptor.user : {};
|
|
5986
|
+
const baseSection = isRecord(descriptor?.base) ? descriptor.base : {};
|
|
5987
|
+
const source = key in userSection ? "user" : key in baseSection ? "base" : "default";
|
|
5988
|
+
lines.push(` ${key.padEnd(27)} ${formatSettingsValue(key, snapshot[key]).padEnd(12)} ${source}`);
|
|
5989
|
+
}
|
|
5990
|
+
lines.push("", " changes apply to running sessions immediately (no restart)");
|
|
5991
|
+
lines.push(" coreOverrides (composition layer) merge LAST and beat these values on same-name keys");
|
|
5992
|
+
lines.push(" /acp config reset <key> returns the key to the composition row / engine default");
|
|
5993
|
+
return lines.join("\n");
|
|
5994
|
+
}
|
|
5995
|
+
async function configSetText(surface, key, rawValue) {
|
|
5996
|
+
if (!isSettingsKey(key)) {
|
|
5997
|
+
return `unknown key "${key}" \u2014 keys: ${SETTINGS_KEYS.join(", ")}`;
|
|
5998
|
+
}
|
|
5999
|
+
if (surface === void 0) return "runtime settings are not wired in this engine build";
|
|
6000
|
+
if (!surface.available) {
|
|
6001
|
+
return "no settings provider in this process \u2014 edit the compaction-acp row in cordis.patch.yml instead (a restart applies it)";
|
|
6002
|
+
}
|
|
6003
|
+
const parsed = parseSettingValue(rawValue);
|
|
6004
|
+
if (!parsed.ok) return parsed.reason;
|
|
6005
|
+
if (parsed.value === null) {
|
|
6006
|
+
return configResetText(surface, key);
|
|
6007
|
+
}
|
|
6008
|
+
if ((key === "autoNudge" || key === "autoModelContextLimit") && typeof parsed.value !== "boolean") {
|
|
6009
|
+
return `${key} takes true or false (got "${String(parsed.value)}")`;
|
|
6010
|
+
}
|
|
6011
|
+
const patch = key === "autoNudge" || key === "autoModelContextLimit" ? { [key]: parsed.value } : { [key]: parsed.value };
|
|
6012
|
+
try {
|
|
6013
|
+
await surface.update(patch);
|
|
6014
|
+
} catch (error) {
|
|
6015
|
+
return settingsWriteFailure(error);
|
|
6016
|
+
}
|
|
6017
|
+
const windowNote = key === "modelContextLimit" || key === "autoModelContextLimit" ? "\n window cache cleared \u2014 the next step re-resolves the context window" : "";
|
|
6018
|
+
return `\u2713 ${key} = ${String(parsed.value)} \u2014 applied to running sessions${windowNote}`;
|
|
6019
|
+
}
|
|
6020
|
+
async function configResetText(surface, target) {
|
|
6021
|
+
if (surface === void 0) return "runtime settings are not wired in this engine build";
|
|
6022
|
+
if (!surface.available) {
|
|
6023
|
+
return "no settings provider in this process \u2014 edit the compaction-acp row in cordis.patch.yml instead (a restart applies it)";
|
|
6024
|
+
}
|
|
6025
|
+
if (target === "all") {
|
|
6026
|
+
try {
|
|
6027
|
+
await surface.replaceSection({});
|
|
6028
|
+
} catch (error) {
|
|
6029
|
+
return settingsWriteFailure(error);
|
|
6030
|
+
}
|
|
6031
|
+
return "\u2713 all runtime settings reset \u2014 values now come from the composition row / engine defaults";
|
|
6032
|
+
}
|
|
6033
|
+
if (!isSettingsKey(target)) {
|
|
6034
|
+
return `unknown key "${target}" \u2014 keys: ${SETTINGS_KEYS.join(", ")}`;
|
|
6035
|
+
}
|
|
6036
|
+
const descriptor = surface.describe();
|
|
6037
|
+
const userSection = isRecord(descriptor?.user) ? { ...descriptor.user } : {};
|
|
6038
|
+
delete userSection[target];
|
|
6039
|
+
try {
|
|
6040
|
+
await surface.replaceSection(userSection);
|
|
6041
|
+
} catch (error) {
|
|
6042
|
+
return settingsWriteFailure(error);
|
|
6043
|
+
}
|
|
6044
|
+
const baseSection = isRecord(descriptor?.base) ? descriptor.base : {};
|
|
6045
|
+
const baseValue = baseSection[target];
|
|
6046
|
+
return `\u2713 ${target} reset \u2014 it now reads ${baseValue === void 0 ? "the engine default" : `the composition value ${String(baseValue)}`}`;
|
|
6047
|
+
}
|
|
6048
|
+
async function configText(env, rest) {
|
|
6049
|
+
const surface = env.settingsCommand;
|
|
6050
|
+
const args = rest.split(/\s+/).filter((part) => part.length > 0);
|
|
6051
|
+
const verb = args[0] ?? "list";
|
|
6052
|
+
if (verb === "list") return configListText(surface);
|
|
6053
|
+
if (verb === "set") {
|
|
6054
|
+
if (args.length < 3) return "usage: /acp config set <key> <value> (e.g. /acp config set nudgeMaxContextLimitPct 0.72)";
|
|
6055
|
+
return configSetText(surface, args[1], args.slice(2).join(" "));
|
|
6056
|
+
}
|
|
6057
|
+
if (verb === "reset") {
|
|
6058
|
+
return configResetText(surface, args[1] ?? "all");
|
|
6059
|
+
}
|
|
6060
|
+
return `unknown /acp config verb "${verb}" \u2014 use list | set <key> <value> | reset <key>|all`;
|
|
6061
|
+
}
|
|
4570
6062
|
|
|
4571
6063
|
// src/system-prompt.ts
|
|
4572
6064
|
var ACP_SYSTEM_PROMPT = renderSystemPrompt(DEFAULT_PROMPTS);
|
|
@@ -4589,7 +6081,32 @@ var DEFAULT_CONFIG = {
|
|
|
4589
6081
|
nudgeEmergencyThresholdPct: 0.85
|
|
4590
6082
|
};
|
|
4591
6083
|
function resolveAcpConfig(config = {}) {
|
|
4592
|
-
|
|
6084
|
+
const resolved = resolvePresetThresholds({ ...DEFAULT_CONFIG, ...config }, config);
|
|
6085
|
+
assertNudgeThresholdOrder(resolved);
|
|
6086
|
+
return resolved;
|
|
6087
|
+
}
|
|
6088
|
+
function resolvePresetThresholds(base, config) {
|
|
6089
|
+
if (base.preset === void 0) return base;
|
|
6090
|
+
const preset = resolvePreset(base.preset);
|
|
6091
|
+
return {
|
|
6092
|
+
...base,
|
|
6093
|
+
nudgeMinContextLimitPct: config.nudgeMinContextLimitPct ?? preset.nudgeMinContextLimitPct,
|
|
6094
|
+
nudgeMaxContextLimitPct: config.nudgeMaxContextLimitPct ?? preset.nudgeMaxContextLimitPct,
|
|
6095
|
+
nudgeEmergencyThresholdPct: config.nudgeEmergencyThresholdPct ?? preset.nudgeEmergencyThresholdPct
|
|
6096
|
+
};
|
|
6097
|
+
}
|
|
6098
|
+
function assertNudgeThresholdOrder(config) {
|
|
6099
|
+
const { nudgeMinContextLimitPct: min, nudgeMaxContextLimitPct: max, nudgeEmergencyThresholdPct: emergency } = config;
|
|
6100
|
+
const describe = `min ${min ?? "kernel default"} / max ${max ?? "kernel default"} / emergency ${emergency ?? "kernel default"}`;
|
|
6101
|
+
if (min !== void 0 && max !== void 0 && min > max) {
|
|
6102
|
+
throw new Error(`nudge thresholds are inverted (${describe}) \u2014 nudgeMinContextLimitPct must be <= nudgeMaxContextLimitPct`);
|
|
6103
|
+
}
|
|
6104
|
+
if (max !== void 0 && emergency !== void 0 && max > emergency) {
|
|
6105
|
+
throw new Error(`nudge thresholds are inverted (${describe}) \u2014 nudgeMaxContextLimitPct must be <= nudgeEmergencyThresholdPct`);
|
|
6106
|
+
}
|
|
6107
|
+
if (min !== void 0 && emergency !== void 0 && min > emergency) {
|
|
6108
|
+
throw new Error(`nudge thresholds are inverted (${describe}) \u2014 nudgeMinContextLimitPct must be <= nudgeEmergencyThresholdPct`);
|
|
6109
|
+
}
|
|
4593
6110
|
}
|
|
4594
6111
|
var AcpCompactionEngine = class extends CompactionEngine {
|
|
4595
6112
|
/** The framework-agnostic ACP compression core, reused verbatim. */
|
|
@@ -4608,32 +6125,91 @@ var AcpCompactionEngine = class extends CompactionEngine {
|
|
|
4608
6125
|
* revive lost-config bugs with every unit test green.
|
|
4609
6126
|
*/
|
|
4610
6127
|
env;
|
|
4611
|
-
lastNudgeTurn =
|
|
6128
|
+
lastNudgeTurn = new LruMap(DEFAULT_SESSION_CACHE_LIMIT);
|
|
6129
|
+
/** Per-session emergency-nudge injection budget for the current user turn (issue #108). */
|
|
6130
|
+
emergencyNudges = /* @__PURE__ */ new Map();
|
|
4612
6131
|
/** Successful compress call ids awaiting their tool/result so the pair can be hidden. */
|
|
4613
6132
|
compressCallIdsToHide = /* @__PURE__ */ new Set();
|
|
4614
6133
|
/** Per provider/model route the resolved window (probe failures cached too). */
|
|
4615
6134
|
windowCache = /* @__PURE__ */ new Map();
|
|
6135
|
+
/** Live settings snapshot thunk (composition → user settings layer); swapped when the settings provider attaches (SettingsProvider.installSection). */
|
|
6136
|
+
readSettingsSource = () => resolveAcpSettings({});
|
|
6137
|
+
/** The settings service, captured lazily for /acp config (undefined in provider-less processes). */
|
|
6138
|
+
settingsService;
|
|
6139
|
+
/** /acp config read/write surface. */
|
|
6140
|
+
settingsCommand;
|
|
4616
6141
|
/** Per route the adapter's per-request output cap (the output reservation); null = undisclosed. */
|
|
4617
6142
|
outputReservationCache = /* @__PURE__ */ new Map();
|
|
4618
6143
|
constructor(ctx, config = {}) {
|
|
4619
6144
|
super(ctx);
|
|
4620
6145
|
this.config = resolveAcpConfig(config);
|
|
4621
|
-
this.prompts =
|
|
6146
|
+
this.prompts = resolvePrompts2(config.prompts);
|
|
4622
6147
|
const ports = this.config.countTokens !== void 0 ? { countTokens: this.config.countTokens } : {};
|
|
4623
6148
|
this.kernel = createCore(ports);
|
|
6149
|
+
setDocCacheCap(128 * 1024 * 1024);
|
|
4624
6150
|
this.store = new AcpStateStore();
|
|
6151
|
+
const compositionEntry = filterSettingsEntry(config);
|
|
6152
|
+
let current = resolveAcpSettings(compositionEntry);
|
|
6153
|
+
this.readSettingsSource = () => current;
|
|
6154
|
+
const engine = this;
|
|
6155
|
+
const applySettings = () => {
|
|
6156
|
+
const next = this.readSettingsSource();
|
|
6157
|
+
const prev = current;
|
|
6158
|
+
current = next;
|
|
6159
|
+
try {
|
|
6160
|
+
engine.onSettingsChanged(prev, next);
|
|
6161
|
+
} catch (error) {
|
|
6162
|
+
this.ctx.logger.warn(`billion-context-dsh: applying settings change failed: ${String(error)}`);
|
|
6163
|
+
}
|
|
6164
|
+
};
|
|
6165
|
+
this.settingsCommand = makeSettingsCommandSurface(() => this.settingsService, () => current);
|
|
6166
|
+
if (this.config.settingsEnabled !== false) {
|
|
6167
|
+
ctx.inject(["settings"], (settingsCtx) => {
|
|
6168
|
+
settingsCtx.settings.installSection(ctx, ACP_SETTINGS_NAMESPACE, AcpSettingsSchema, compositionEntry, {
|
|
6169
|
+
// The seam's source type follows the entry it registered, so `source`
|
|
6170
|
+
// is a partial view of the settings; re-resolve it into a
|
|
6171
|
+
// fully-defaulted snapshot so every reader sees the same shape the
|
|
6172
|
+
// composition path produced.
|
|
6173
|
+
setSource: (source) => {
|
|
6174
|
+
this.readSettingsSource = () => resolveAcpSettings(source());
|
|
6175
|
+
},
|
|
6176
|
+
onChange: applySettings
|
|
6177
|
+
});
|
|
6178
|
+
this.settingsService = settingsCtx.settings;
|
|
6179
|
+
return () => {
|
|
6180
|
+
this.settingsService = void 0;
|
|
6181
|
+
this.readSettingsSource = () => current;
|
|
6182
|
+
};
|
|
6183
|
+
});
|
|
6184
|
+
}
|
|
4625
6185
|
const env = {
|
|
4626
6186
|
kernel: this.kernel,
|
|
4627
6187
|
store: this.store,
|
|
4628
|
-
//
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
6188
|
+
// The settings-exposed knobs read LIVE from the settings source, so a
|
|
6189
|
+
// settings.yaml edit (or /acp config set) hot-applies to every
|
|
6190
|
+
// subsequent call — consumers never see stale numbers. (ToolEnvironment
|
|
6191
|
+
// fields are readonly properties; getters satisfy them.)
|
|
6192
|
+
get modelContextLimit() {
|
|
6193
|
+
return engine.readSettingsSource().modelContextLimit ?? DEFAULT_CONTEXT_WINDOW;
|
|
6194
|
+
},
|
|
6195
|
+
get nudgeMinContextLimitPct() {
|
|
6196
|
+
return engine.readSettingsSource().nudgeMinContextLimitPct;
|
|
6197
|
+
},
|
|
6198
|
+
get nudgeMaxContextLimitPct() {
|
|
6199
|
+
return engine.readSettingsSource().nudgeMaxContextLimitPct;
|
|
6200
|
+
},
|
|
6201
|
+
get nudgeEmergencyThresholdPct() {
|
|
6202
|
+
return engine.readSettingsSource().nudgeEmergencyThresholdPct;
|
|
6203
|
+
},
|
|
4633
6204
|
coreOverrides: this.config.coreOverrides,
|
|
6205
|
+
// Display-only: which named preset produced the thresholds above (if any),
|
|
6206
|
+
// so /acp status can name it. The resolved pct values above are what the
|
|
6207
|
+
// kernel actually reads — this field never feeds kernelConfigFor.
|
|
6208
|
+
preset: this.config.preset,
|
|
4634
6209
|
windowFor: (agent) => this.windowFor(agent),
|
|
4635
6210
|
prompts: this.prompts,
|
|
4636
|
-
compressCallIdsToHide: this.compressCallIdsToHide
|
|
6211
|
+
compressCallIdsToHide: this.compressCallIdsToHide,
|
|
6212
|
+
settingsCommand: this.settingsCommand
|
|
4637
6213
|
};
|
|
4638
6214
|
this.env = env;
|
|
4639
6215
|
const tools = ctx.get("tools");
|
|
@@ -4681,11 +6257,21 @@ var AcpCompactionEngine = class extends CompactionEngine {
|
|
|
4681
6257
|
});
|
|
4682
6258
|
ctx.on("agent/pre-step", async (payload, next) => {
|
|
4683
6259
|
stripOrphanedSurfaceToolMessages(payload.agent.session);
|
|
4684
|
-
if (!
|
|
6260
|
+
if (!engine.readSettingsSource().autoNudge) return next();
|
|
4685
6261
|
const decision = await next();
|
|
4686
6262
|
if (decision.kind === "reject") return decision;
|
|
4687
6263
|
const window = await this.windowFor(payload.agent);
|
|
4688
|
-
const outcome = buildNudge(
|
|
6264
|
+
const outcome = buildNudge(
|
|
6265
|
+
payload.agent,
|
|
6266
|
+
{ ...env, modelContextLimit: window.limit },
|
|
6267
|
+
this.lastNudgeTurn,
|
|
6268
|
+
this.emergencyNudges,
|
|
6269
|
+
() => {
|
|
6270
|
+
ctx.logger.warn(
|
|
6271
|
+
`billion-context-dsh: emergency nudge suppressed \u2014 per-turn budget of ${EMERGENCY_NUDGE_MAX_PER_TURN} spent (session ${payload.agent.session.id}); pressure is still above the emergency threshold`
|
|
6272
|
+
);
|
|
6273
|
+
}
|
|
6274
|
+
);
|
|
4689
6275
|
if (outcome === null) return decision;
|
|
4690
6276
|
return { kind: "enter", messages: [...decision.messages, outcome.message] };
|
|
4691
6277
|
});
|
|
@@ -4734,13 +6320,13 @@ var AcpCompactionEngine = class extends CompactionEngine {
|
|
|
4734
6320
|
* fallback.
|
|
4735
6321
|
*/
|
|
4736
6322
|
async windowFor(agent) {
|
|
4737
|
-
|
|
4738
|
-
|
|
6323
|
+
const live = this.readSettingsSource();
|
|
6324
|
+
if (live.modelContextLimit !== void 0) {
|
|
6325
|
+
return { limit: live.modelContextLimit, source: "explicit" };
|
|
4739
6326
|
}
|
|
4740
|
-
const provider = agent
|
|
4741
|
-
const model = agent.options.model ?? "";
|
|
6327
|
+
const { provider, model } = routeFor(agent);
|
|
4742
6328
|
const key = `${provider}\0${model}`;
|
|
4743
|
-
if (
|
|
6329
|
+
if (live.autoModelContextLimit) {
|
|
4744
6330
|
const projected = projectedContextWindow(agent);
|
|
4745
6331
|
if (projected !== null) {
|
|
4746
6332
|
const cap2 = await this.outputCapFor(agent, provider, model);
|
|
@@ -4751,14 +6337,14 @@ var AcpCompactionEngine = class extends CompactionEngine {
|
|
|
4751
6337
|
if (cached !== void 0) return cached;
|
|
4752
6338
|
let window;
|
|
4753
6339
|
let cap = null;
|
|
4754
|
-
if (!
|
|
6340
|
+
if (!live.autoModelContextLimit) {
|
|
4755
6341
|
window = { limit: DEFAULT_CONTEXT_WINDOW, source: "default", provider, model };
|
|
4756
6342
|
} else {
|
|
4757
6343
|
const probe = await probeModelWindow(agent, provider, model);
|
|
4758
6344
|
cap = probe.outputReservation;
|
|
4759
6345
|
if (probe.contextWindow === null) {
|
|
4760
6346
|
this.ctx.logger.warn(
|
|
4761
|
-
`billion-context-dsh: context-window auto-detection failed for ${provider}/${model} \u2014 using the ${DEFAULT_CONTEXT_WINDOW} fallback (restart to re-probe
|
|
6347
|
+
`billion-context-dsh: context-window auto-detection failed for ${provider}/${model} \u2014 using the ${DEFAULT_CONTEXT_WINDOW} fallback (change modelContextLimit or autoModelContextLimit via /acp config \u2014 or restart \u2014 to re-probe)`
|
|
4762
6348
|
);
|
|
4763
6349
|
window = { limit: DEFAULT_CONTEXT_WINDOW, source: "default", provider, model, probeFailed: true };
|
|
4764
6350
|
cap = null;
|
|
@@ -4770,6 +6356,23 @@ var AcpCompactionEngine = class extends CompactionEngine {
|
|
|
4770
6356
|
this.windowCache.set(key, window);
|
|
4771
6357
|
return window;
|
|
4772
6358
|
}
|
|
6359
|
+
/**
|
|
6360
|
+
* Diff handler for runtime settings changes: drop the window cache when a
|
|
6361
|
+
* window-related key changed (probe FAILURES are cached too — clearing is
|
|
6362
|
+
* what lets the next pre-step re-probe after a fix), clear the per-turn
|
|
6363
|
+
* nudge dedup when nudges come back on, and warn on order anomalies
|
|
6364
|
+
* (accepted, never rejected — rejecting a write cannot fix an externally
|
|
6365
|
+
* edited settings.yaml, and an invalid stored section would fail the next
|
|
6366
|
+
* boot loud anyway).
|
|
6367
|
+
*/
|
|
6368
|
+
onSettingsChanged(prev, next) {
|
|
6369
|
+
const effect = describeSettingsChange(prev, next);
|
|
6370
|
+
for (const warning of effect.warnings) {
|
|
6371
|
+
this.ctx.logger.warn(`billion-context-dsh: ${warning}`);
|
|
6372
|
+
}
|
|
6373
|
+
if (effect.clearWindowCache) this.windowCache.clear();
|
|
6374
|
+
if (effect.clearNudgeDedup) this.lastNudgeTurn.clear();
|
|
6375
|
+
}
|
|
4773
6376
|
/**
|
|
4774
6377
|
* The adapter's per-request output cap for a route, from one
|
|
4775
6378
|
* probeModelWindow call (a local catalog lookup — no request is sent),
|
|
@@ -4820,14 +6423,21 @@ var AcpCompactionEngine = class extends CompactionEngine {
|
|
|
4820
6423
|
};
|
|
4821
6424
|
var index_default = AcpCompactionEngine;
|
|
4822
6425
|
export {
|
|
6426
|
+
ACP_SETTINGS_NAMESPACE,
|
|
4823
6427
|
ACP_SYSTEM_PROMPT,
|
|
4824
6428
|
ACP_SYSTEM_PROMPT_ORDER,
|
|
4825
6429
|
AcpCompactionEngine,
|
|
6430
|
+
AcpSettingsSchema,
|
|
4826
6431
|
AcpStateStore,
|
|
4827
6432
|
AlreadyCompressedRangeError,
|
|
4828
6433
|
DEFAULT_CONTEXT_WINDOW,
|
|
4829
6434
|
DEFAULT_PROMPTS,
|
|
4830
6435
|
DEFAULT_RESOLVED,
|
|
6436
|
+
EMERGENCY_NUDGE_MAX_PER_TURN,
|
|
6437
|
+
PRESETS,
|
|
6438
|
+
PRESET_NAMES,
|
|
6439
|
+
SETTINGS_KEYS,
|
|
6440
|
+
SETTING_DEFAULTS,
|
|
4831
6441
|
acpCommand,
|
|
4832
6442
|
assertNoActiveCompaction,
|
|
4833
6443
|
blockRefForSummarySeq,
|
|
@@ -4835,21 +6445,28 @@ export {
|
|
|
4835
6445
|
buildNudge,
|
|
4836
6446
|
compactionIdsOfKernelBlocks,
|
|
4837
6447
|
index_default as default,
|
|
6448
|
+
describeSettingsChange,
|
|
4838
6449
|
detectContextWindow,
|
|
4839
6450
|
eventsToCoreMessages,
|
|
4840
6451
|
expandShadowedSeqs,
|
|
4841
6452
|
extractEventText,
|
|
6453
|
+
filterSettingsEntry,
|
|
4842
6454
|
findOpenTurn,
|
|
4843
6455
|
hideCompressToolPair,
|
|
6456
|
+
isPresetName,
|
|
4844
6457
|
kernelConfigFor,
|
|
6458
|
+
makeSettingsCommandSurface,
|
|
4845
6459
|
makeTools,
|
|
6460
|
+
parseSettingValue,
|
|
4846
6461
|
projectEvent,
|
|
4847
6462
|
projectedContextWindow,
|
|
4848
6463
|
rebuildBlockLedger,
|
|
4849
6464
|
renderSystemPrompt,
|
|
4850
6465
|
renderTemplate,
|
|
4851
6466
|
resolveAcpConfig,
|
|
4852
|
-
|
|
6467
|
+
resolveAcpSettings,
|
|
6468
|
+
resolvePreset,
|
|
6469
|
+
resolvePrompts2 as resolvePrompts,
|
|
4853
6470
|
resolveSurfaceRange,
|
|
4854
6471
|
resolveTokenCount,
|
|
4855
6472
|
runCompactionTransaction,
|