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/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/index.js
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
- function createInitialState() {
76
- return {
77
- blocks: [],
78
- messageRefs: { byRaw: {}, byRef: {} },
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 coveredMessageIds(state) {
109
- const covered = /* @__PURE__ */ new Set();
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 advanceSurvival(state, promotionThreshold) {
117
- for (const block of state.blocks) {
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
- messages.forEach((message, index) => indexById.set(message.id, index));
135
- const anchors = inject ? collectSummaryAnchors(state, indexById) : [];
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: `acp_summary_${anchor.blockId}`,
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 indexByRawId = /* @__PURE__ */ new Map();
756
+ const indexByMessageId = /* @__PURE__ */ new Map();
401
757
  input.messages.forEach(
402
- (message, index) => indexByRawId.set(message.id, 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
- let startIndex = resolveAnchorIndex(start, input.state, indexByRawId, "start");
405
- let endIndex = resolveAnchorIndex(end, input.state, indexByRawId, "end");
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) messageIds.push(message.id);
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
- const anchor = earliestIndexOfIds(block.effectiveMessageIds, indexByRawId);
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, indexByRawId, endpoint) {
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 = indexByRawId.get(rawId);
448
- if (index === void 0) {
449
- throw new BoundaryNotFoundError(
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
- return index;
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
- const anchor = earliestIndexOfIds(block.effectiveMessageIds, indexByRawId);
473
- if (anchor === null) {
474
- throw new BoundaryNotFoundError(
475
- "consumed",
476
- endpoint,
477
- `${label}="b${boundary.numericId}" not found in visible context (block messages consumed by a higher-tier block).`
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 anchor;
904
+ return ids;
481
905
  }
482
906
  function formatPaddedRef(index) {
483
907
  return `m${String(index).padStart(5, "0")}`;
484
908
  }
485
- function earliestIndexOfIds(ids, indexByRawId) {
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 = indexByRawId.get(id);
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 = 0;
986
+ var KEEP_LAST_ORPHANED = 2;
547
987
  function rangeKey(startRef, endRef) {
548
988
  return `${startRef}::${endRef}`;
549
989
  }
550
- function rewriteCompressText(text, liveKeys) {
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(text ?? "");
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
- const content = obj.content;
560
- if (!Array.isArray(content) || content.length === 0) return null;
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 s = typeof entry.startId === "string" ? entry.startId : typeof entry.messageId === "string" ? entry.messageId : "";
564
- const e = typeof entry.endId === "string" ? entry.endId : typeof entry.messageId === "string" ? entry.messageId : "";
565
- return liveKeys.has(rangeKey(s, e));
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
- if (kept.length === content.length || kept.length === 0) return null;
568
- return JSON.stringify({ ...obj, content: kept });
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 tokens = snapshot ? snapshot[ref] ?? (snapshot[ref] = countTokens(cleanText)) : countTokens(cleanText);
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") && j + 1 > newEndIndex) {
880
- newEndIndex = j + 1;
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 refNum(ref) {
897
- const n = parseInt(ref.slice(1), 10);
898
- return Number.isNaN(n) ? -1 : n;
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: countTokens(msg.text ?? "") });
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
- const rn = refNum(ref);
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
- refNum: rn,
961
- tokens: countTokens(msg.text ?? ""),
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
- refNum: rn,
972
- tokens: countTokens(msg.text ?? ""),
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
- const hasGap = info.refNum > prevRefNum + 1;
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
- const hasGap = info.refNum > pPrevRefNum + 1;
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(input.messages, input.state, input.config, countTokens);
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(spec, error instanceof Error ? error.message : String(error))
1976
+ rangeError(
1977
+ spec,
1978
+ error instanceof Error ? error.message : String(error)
1979
+ )
1142
1980
  );
1143
1981
  }
1144
1982
  }
1145
1983
  }
1146
- const rangeIndexSets = [];
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
- const indices = resolution.resolved.messageIds.map(
1150
- (id) => input.messages.findIndex((m) => m.id === id)
1151
- ).filter((i) => i >= 0);
1152
- rangeIndexSets.push({ spec, indices });
1153
- }
1154
- const sortedRanges = [...rangeIndexSets].sort((a, b) => {
1155
- const aMin = a.indices.length > 0 ? Math.min(...a.indices) : Infinity;
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
- const entryMax = entry.indices.length > 0 ? Math.max(...entry.indices) : -1;
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 (entryMax > acceptedMaxIndex) acceptedMaxIndex = entryMax;
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 gateMessage = consumedRanges.length > 0 ? `Requested range(s) already compressed (e.g. ${consumedRanges[0].startRef}..${consumedRanges[0].endRef}); remaining compressible content ${totalRangeChars} chars < min ${input.config.compress.minCompressRange}. Nothing to do \u2014 run acp_status to see current compressible ranges.` : `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.`;
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(rangeError(spec, error instanceof Error ? error.message : String(error)));
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 { state, result: { blocksCreated, tokensCompressed, errors, warnings } };
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(`[acp-kernel] Config validation warnings: ${configErrors.join("; ")}. Thresholds may not fire correctly.`);
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 { processTurn, applyCompression, defaultNodes, decompress, search, status };
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 = { ...stamped.lastShownByTier, [nudge.tier]: ctx.tokenCount };
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 indexByRawId = /* @__PURE__ */ new Map();
1449
- input.messages.forEach((m, i) => indexByRawId.set(m.id, i));
1450
- const adjustedStart = indexByRawId.get(rangeMessageIds[0]) ?? resolved.startIndex;
1451
- const adjustedEnd = indexByRawId.get(rangeMessageIds[rangeMessageIds.length - 1]) ?? resolved.endIndex;
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
- const anchor = earliestIndexOfIds(block2.effectiveMessageIds, indexByRawId);
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(message?.text ?? "");
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] = { pending: effective.reduce((s, r) => s + r.tokens, 0), targetBlocks: [] };
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] = { pending: t1.reduce((s, b) => s + countTokens(b.summary), 0), targetBlocks: t1 };
1676
- out[3] = { pending: t2.reduce((s, b) => s + countTokens(b.summary), 0), targetBlocks: t2 };
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
- const growthReady = growthSinceReference >= growthFloor;
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 > 0) {
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 readyHint = ready.length > 0 ? `, ready: ${ready.join(", ")}` : "";
1764
- const blocked = eligible.filter((t) => (tiers[t]?.pending ?? 0) >= nudgeGrowthTokens && (state.nudge.lastShownByTier[t] ?? 0) > 0 && tokenCount - (state.nudge.lastShownByTier[t] ?? 0) < growthFloor).map((t) => `T${t} (cadence)`);
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(0, ...Object.values(tiers).map((t) => t.pending));
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) parts.push(`max compressible ${maxPending} < threshold ${nudgeGrowthTokens}`);
1771
- if (growthShort) parts.push(`growth ${growthSinceReference} < floor ${growthFloor}`);
1772
- if (parts.length === 0) parts.push(`max compressible ${maxPending}, growth ${growthSinceReference}`);
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(input.messages, tokenCount, growthSinceReference, countTokens);
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 = count(msg.text ?? "");
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
- merged.push({ ...e });
2804
+ text += tokens;
2073
2805
  }
2074
2806
  }
2075
- const lines = merged.map((e) => {
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 renderNudgeText(decision, prompts = defaultPrompts) {
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
- voice: "gentle",
2137
- text: [
2138
- efficiencyNote(prompts),
2139
- "",
2140
- breakdownStr,
2141
- "",
2142
- prompts.howToCompressRules,
2143
- "",
2144
- rangesStr,
2145
- "",
2146
- `\u{1F4A1} Compress all ranges in one call (pass multiple content entries: \`content: [{...}, {...}]\`).`
2147
- ].join("\n")
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.max(1, Math.round(n / total * 100));
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 = countTokens(message.text ?? "");
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 refNum2 = (ref) => {
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 = refNum2(m.ref);
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
- merged.push({ startRef: m.ref, endRef: m.ref, startNum: num, count: 1, tokens: m.tokens, tool: m.tool });
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.tool}`);
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 text = extractText(event.data.content);
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) => !isCheckpointNode(eventAtOf(session, 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) => toolPairingBalancedBefore(session, nodes[index]) && hasPlainRef(session, nodes[index]);
3035
- const cleanAfter = (index) => toolPairingBalancedAfter(session, nodes[index]) && hasPlainRef(session, nodes[index]);
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: input.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
- tier: input.tier ?? 1,
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: input.summary,
4013
+ content: framedSummary,
3109
4014
  source: compactCheckpointSource(compactionId)
3110
4015
  });
3111
4016
  seqs.push(session.append("user/message", message, {
3112
- surfaceOp: { op: "replace", start: input.start, end: input.end },
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 summarySeqOfCompaction(events, compactionId) {
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
- if (source?.plugin === "compact" && source.compactionId === compactionId) return event.seq;
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 null;
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 tier = data.tier === 2 || data.tier === 3 ? data.tier : 1;
3148
- const parentBlockIds = Array.isArray(data.parentBlockIds) ? [...data.parentBlockIds] : [];
3149
- const directMessageIds = Array.isArray(data.directMessageIds) ? [...data.directMessageIds] : void 0;
3150
- const effectiveMessageIds = Array.isArray(data.effectiveMessageIds) ? [...data.effectiveMessageIds] : void 0;
3151
- const summarySeq = summarySeqOfCompaction(events, data.compactionId);
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
- ...typeof data.topic === "string" ? { topic: data.topic } : {},
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
- ...typeof data.kernelBlockId === "string" ? { kernelBlockId: data.kernelBlockId } : {},
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 isCheckpointNode(event) {
3178
- if (event.type !== "user/message") return false;
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
- function assistantProviderModel(event) {
3195
- if (event.type === "assistant/message") {
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
- if (text !== void 0) {
3219
- session.append("user/message", createUserMessage({
3220
- content: [{ type: "text", text }],
3221
- source: { kind: "plugin", plugin: "billion-context-dsh" }
3222
- }), {
3223
- surfaceOp: { op: "replace", start, end },
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], provider, model, resultText.trim().length > 0 ? resultText : void 0);
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
- const event = eventAtOf(session, seq);
3336
- if (event === void 0) continue;
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 buildCompressibleSeqRanges(session, opts = {}) {
3367
- stripOrphanedSurfaceToolMessages(session);
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?.type === "user/message" && !isCheckpointNode(event)) {
4299
+ if (event !== void 0 && isRealUserTurn(event)) {
3377
4300
  protectedSeqs.add(nodes[index]);
3378
4301
  break;
3379
4302
  }
3380
4303
  }
3381
- const raw = [];
3382
- let cur = null;
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 (cur !== null) raw.push(cur);
3385
- cur = null;
4312
+ if (current !== null) segments.push(current);
4313
+ current = null;
3386
4314
  };
3387
- for (const seq of nodes) {
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
- if (cur !== null && seq < cur.start) {
3394
- flush();
3395
- cur = null;
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 (cur === null) {
3400
- cur = { start: seq, end: seq, count: 1, tokens, toolCount: isTool ? 1 : 0 };
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
- cur = { start: cur.start, end: seq, count: cur.count + 1, tokens: cur.tokens + tokens, toolCount: cur.toolCount + (isTool ? 1 : 0) };
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 raw) {
3408
- try {
3409
- const { start, end } = resolveSurfaceRange(session, range.start, range.end);
3410
- const count = range.count;
3411
- out.push({
3412
- start,
3413
- end,
3414
- count,
3415
- tokens: range.tokens,
3416
- toolPct: count > 0 ? Math.round(range.toolCount / count * 100) : 0
3417
- });
3418
- } catch {
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
- states = /* @__PURE__ */ new Map();
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 resolvePrompts(input) {
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
- normal: "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.\n\n{philosophy}",
3703
- emergency: "\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.\n\n{philosophy}",
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: "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.",
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 rangeTable(session, prompts = DEFAULT_RESOLVED) {
3772
- const ranges = buildCompressibleSeqRanges(session).slice(0, 6);
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 buildNudge(agent, env, lastNudgeTurn) {
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 surfaceMessages = eventsToCoreMessages(surfaceEventsOf(session));
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
- const alreadyShown = !emergency && lastNudgeTurn.get(session.id) === turnNumber;
3810
- if (alreadyShown) return null;
3811
- lastNudgeTurn.set(session.id, turnNumber);
3812
- const text = buildNudgeText(nudge, emergency, session, env.prompts);
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: agent.options.provider ?? "",
4195
- model: agent.options.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 = [...freeWarnings.map((warning) => ` ${warning}`), ...failedLines, ...alreadyCompressedNotes, ...lines];
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 expandShadowedSeqs(session, block.blockId)) {
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
- ${parts.join("\n\n") || "(no recoverable content)"}`
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 ledger = rebuildBlockLedger(sessionEventsOf(session));
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) => !isCheckpointEvent(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/window.ts
4434
- var DEFAULT_CONTEXT_WINDOW = 128e3;
4435
- function windowSourceLabel(window) {
4436
- if (window.source === "explicit") return "configured";
4437
- if (window.source === "projection") {
4438
- return `session projection current route (auto-refreshes on model switch)`;
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 (window.source === "auto") {
4441
- return `auto-detected from ${window.provider ?? "?"}/${window.model ?? "?"}`;
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
- if (window.probeFailed === true) return "default (auto-detection failed \u2014 restart to re-probe)";
4444
- return "default (auto-detection unavailable)";
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 projectedContextWindow(agent) {
4447
- const projections = agent.ctx?.get?.("sessionProjections");
4448
- const window = projections?.snapshot?.(agent.session)?.values?.contextPressure?.contextWindow;
4449
- if (typeof window === "number" && Number.isInteger(window) && window > 0) return window;
4450
- return null;
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
- async function probeModelWindow(agent, provider, model) {
4453
- const llm = agent.ctx?.get?.("llm");
4454
- if (llm?.resolveModelInfo === void 0) return { contextWindow: null, outputReservation: null };
4455
- try {
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
- async function detectContextWindow(agent, provider, model) {
4468
- return (await probeModelWindow(agent, provider, model)).contextWindow;
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, or set modelContextLimit explicitly)`);
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: agent.options.provider ?? "",
4535
- model: agent.options.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 "/acp decompress <blockId>";
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 parts = expandShadowedSeqs(session, block.blockId).map((seq) => extractEventText(eventAtOf(session, seq))).filter((text) => text.length > 0);
4547
- return `Block ${block.blockId} \u2014 ${block.summary}
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
- ${parts.join("\n\n") || "(no recoverable content)"}`;
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
- return { ...DEFAULT_CONFIG, ...config };
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 = /* @__PURE__ */ new Map();
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 = resolvePrompts(config.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
- // Initial value before any probe; windowFor() replaces it per pre-step.
4629
- modelContextLimit: this.config.modelContextLimit ?? DEFAULT_CONTEXT_WINDOW,
4630
- nudgeMinContextLimitPct: this.config.nudgeMinContextLimitPct,
4631
- nudgeMaxContextLimitPct: this.config.nudgeMaxContextLimitPct,
4632
- nudgeEmergencyThresholdPct: this.config.nudgeEmergencyThresholdPct,
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 (!this.config.autoNudge) return next();
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(payload.agent, { ...env, modelContextLimit: window.limit }, this.lastNudgeTurn);
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
- if (this.config.modelContextLimit !== void 0) {
4738
- return { limit: this.config.modelContextLimit, source: "explicit" };
6323
+ const live = this.readSettingsSource();
6324
+ if (live.modelContextLimit !== void 0) {
6325
+ return { limit: live.modelContextLimit, source: "explicit" };
4739
6326
  }
4740
- const provider = agent.options.provider ?? "";
4741
- const model = agent.options.model ?? "";
6327
+ const { provider, model } = routeFor(agent);
4742
6328
  const key = `${provider}\0${model}`;
4743
- if (this.config.autoModelContextLimit) {
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 (!this.config.autoModelContextLimit) {
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, or set modelContextLimit explicitly)`
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
- resolvePrompts,
6467
+ resolveAcpSettings,
6468
+ resolvePreset,
6469
+ resolvePrompts2 as resolvePrompts,
4853
6470
  resolveSurfaceRange,
4854
6471
  resolveTokenCount,
4855
6472
  runCompactionTransaction,