billion-context-dsh 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [English](./README.en.md) | [中文](./README.md)
4
4
 
5
5
  > **⚠️ Beta notice — not for production use**
6
- > This project (**v0.1.3**) is a work-in-progress beta. The [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) itself is also in **public beta**. **Do not use either in engineering / production environments** — expect breaking changes and rough edges.
6
+ > This project (**v0.1.5**) is a work-in-progress beta. The [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) itself is also in **public beta**. **Do not use either in engineering / production environments** — expect breaking changes and rough edges.
7
7
 
8
8
  <p align="center">
9
9
  <strong>Built with gratitude on top of these projects</strong> — please give them a ⭐:
@@ -50,6 +50,8 @@ This is the DeepSeek Harness port of [billion-context-pi](https://github.com/ran
50
50
 
51
51
  ## Install
52
52
 
53
+ > 💡 **Want DeepSeek Harness to install it for you?** This repo itself runs on DSH: hand [docs/INSTALL.md](docs/INSTALL.md) to an agent in a session and it will read the guide, inspect your profile, wire the composition, and verify the mount. Two preconditions: ① the config lives under `~/.dsh`, so you approve one file-permission prompt; ② afterwards ask it to call `acp_status` as proof.
54
+
53
55
  ```bash
54
56
  npm install billion-context-dsh
55
57
  ```
@@ -103,6 +105,7 @@ DSH derives every model request from its append-only session log (the *surface*)
103
105
  | `search_context` | scores block summaries + originals rebuilt from the log |
104
106
  | `acp_status` | block ledger + context pressure |
105
107
  | block state | in-memory kernel state + **log-rebuilt ledger** (no sidecar files) |
108
+ | tiered distillation (T2/T3) | re-compressing a block's summary node distills that block (tier 2); distilling a tier-2 block yields tier 3. Tier + kernel block ids are persisted to the log, so kernel state rehydrates from the log after a restart and stays distillable |
106
109
 
107
110
  The load-bearing compression guidance (tools, philosophy, summary rules) is registered as a one-time system-prompt section, so nudges stay short. There is deliberately **no automatic summarization**: automatic policy only nudges the model (`compactIfNeeded` returns null).
108
111
 
@@ -116,7 +119,7 @@ A walkthrough of the ACP philosophy this project inherits — how active context
116
119
 
117
120
  | Tool | What it does |
118
121
  | --- | --- |
119
- | `compress` | Replace a seq range with a dense summary you write (edges auto-balanced to tool-pair boundaries) |
122
+ | `compress` | Replace a seq range with a dense summary you write (edges auto-balanced to tool-pair boundaries); re-compressing a block's summary node distills it (tier 2/3) |
120
123
  | `decompress` | Restore a previously compressed block's original content (read-only) |
121
124
  | `search_context` | Search compressed block summaries and originals by keyword |
122
125
  | `acp_status` | Context usage, compressed blocks, compressible ranges |
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [中文](./README.md) | [English](./README.en.md)
4
4
 
5
5
  > **⚠️ 测试版声明——请勿用于生产环境**
6
- > 本项目(**v0.1.3**)仍处于开发中的测试版。[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 本身也处于**公开测试版**阶段。**请勿将两者用于工程化 / 生产环境**——预期会有破坏性变更与粗糙之处。
6
+ > 本项目(**v0.1.5**)仍处于开发中的测试版。[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 本身也处于**公开测试版**阶段。**请勿将两者用于工程化 / 生产环境**——预期会有破坏性变更与粗糙之处。
7
7
 
8
8
  <p align="center">
9
9
  <strong>衷心感谢以下项目——请给它们一个 ⭐:</strong>
@@ -50,6 +50,11 @@
50
50
 
51
51
  ## 安装
52
52
 
53
+ > 💡 **想让 DeepSeek Harness 帮你装?** 本仓库本身就运行在 DSH 上:把
54
+ > [docs/INSTALL.md](docs/INSTALL.md) 交给会话里的 agent,它会读取指南、解析
55
+ > 你的 profile、编辑组合配置并验证挂载。前提:① 配置写在 `~/.dsh` 下,需要
56
+ > 你批准一次文件权限;② 装完让它调用 `acp_status` 自证。
57
+
53
58
  ```bash
54
59
  npm install billion-context-dsh
55
60
  ```
@@ -102,6 +107,7 @@ DSH 的每个模型请求都派生自其 append-only 会话日志(*surface*)
102
107
  | `search_context` | 对从日志重建的块摘要与原文打分 |
103
108
  | `acp_status` | 块账本与上下文压力 |
104
109
  | 块状态 | 内存内核状态 + **日志重建账本**(无旁车文件) |
110
+ | 分层蒸馏(T2/T3) | 再次压缩某块的摘要节点 = 蒸馏该块(tier 2),蒸馏 tier-2 块得 tier 3;tier 与内核块 id 持久化进日志,重启后内核状态从日志再水合、可继续蒸馏 |
105
111
 
106
112
  承载性的压缩指引(工具、哲学、摘要规则)注册为一次性系统提示段,因此 nudge 保持简短。刻意**不做自动摘要**:自动策略只 nudge 模型(`compactIfNeeded` 返回 null)。
107
113
 
@@ -115,7 +121,7 @@ DSH 的每个模型请求都派生自其 append-only 会话日志(*surface*)
115
121
 
116
122
  | 工具 | 作用 |
117
123
  | --- | --- |
118
- | `compress` | 用你书写的紧凑摘要替换 seq 范围(边界自动平衡到 tool-call/result 配对点) |
124
+ | `compress` | 用你书写的紧凑摘要替换 seq 范围(边界自动平衡到 tool-call/result 配对点);对某块的摘要节点再次压缩 = 分层蒸馏(tier 2/3) |
119
125
  | `decompress` | 恢复已压缩块的原始内容(只读) |
120
126
  | `search_context` | 按关键词搜索压缩块摘要与原文 |
121
127
  | `acp_status` | 上下文占用、压缩块、可压缩范围 |
package/dist/index.d.ts CHANGED
@@ -35,7 +35,7 @@ export { ACP_SYSTEM_PROMPT, ACP_SYSTEM_PROMPT_ORDER } from './system-prompt.ts';
35
35
  export { makeTools, type ToolEnvironment } from './tools.ts';
36
36
  export { acpCommand } from './commands.ts';
37
37
  export { buildNudge, type NudgeEnvironment, type NudgeOutcome } from './nudge.ts';
38
- export { rebuildBlockLedger, resolveSurfaceRange, runCompactionTransaction, shadowedSeqsOf, findOpenTurn, assertNoActiveCompaction, type AcpBlockLedgerEntry, type CompactionTransactionInput, } from './region.ts';
38
+ export { rebuildBlockLedger, resolveSurfaceRange, runCompactionTransaction, shadowedSeqsOf, findOpenTurn, assertNoActiveCompaction, blockRegistry, blockRefForSummarySeq, compactionIdsOfKernelBlocks, summarySeqOfKernelBlock, expandShadowedSeqs, type AcpBlockLedgerEntry, type CompactionTransactionInput, } from './region.ts';
39
39
  export { eventsToCoreMessages, projectEvent, surfaceEventsOf, extractEventText } from './messages.ts';
40
40
  export interface AcpConfig {
41
41
  /** The context window used for pressure decisions. Default 128000. */
package/dist/index.js CHANGED
@@ -7,42 +7,6 @@ import { createCore } from "acp-kernel";
7
7
 
8
8
  // src/state.ts
9
9
  import { createInitialState } from "acp-kernel";
10
- var AcpStateStore = class {
11
- states = /* @__PURE__ */ new Map();
12
- /** Kernel state for one session, initialised on first access. */
13
- stateFor(session) {
14
- const id = session.id;
15
- const existing = this.states.get(id);
16
- if (existing !== void 0) return existing;
17
- const state = createInitialState();
18
- this.states.set(id, state);
19
- return state;
20
- }
21
- set(session, state) {
22
- this.states.set(session.id, state);
23
- }
24
- delete(session) {
25
- this.states.delete(session.id);
26
- }
27
- };
28
-
29
- // src/tools.ts
30
- import { defineTool } from "@deepseek-ai/dsh-tools";
31
- import { defaultCountTokens as defaultCountTokens2 } from "acp-kernel";
32
-
33
- // src/config.ts
34
- import { defaultConfig } from "acp-kernel";
35
- function kernelConfigFor(input) {
36
- const nudgePatch = {};
37
- if (input.nudgeMinContextLimitPct !== void 0) nudgePatch.minContextLimitPct = input.nudgeMinContextLimitPct;
38
- if (input.nudgeMaxContextLimitPct !== void 0) nudgePatch.maxContextLimitPct = input.nudgeMaxContextLimitPct;
39
- if (input.nudgeEmergencyThresholdPct !== void 0) nudgePatch.emergencyThresholdPct = input.nudgeEmergencyThresholdPct;
40
- const overrides = { ...input.coreOverrides };
41
- if (Object.keys(nudgePatch).length > 0) {
42
- overrides.nudge = { ...defaultConfig(input.modelContextLimit).nudge, ...nudgePatch };
43
- }
44
- return defaultConfig(input.modelContextLimit, overrides);
45
- }
46
10
 
47
11
  // src/region.ts
48
12
  import { randomUUID } from "crypto";
@@ -145,6 +109,9 @@ function eventsToCoreMessages(events) {
145
109
  function surfaceEventsOf(session) {
146
110
  return session.surface.nodes.map((seq) => session.events[seq]).filter((event) => event !== void 0);
147
111
  }
112
+ function allLogMessages(session) {
113
+ return eventsToCoreMessages(session.events);
114
+ }
148
115
  function extractEventText(event) {
149
116
  switch (event.type) {
150
117
  case "user/message":
@@ -245,6 +212,9 @@ function shadowedSeqsOf(session, start, end) {
245
212
  const endIdx = nodes.indexOf(end);
246
213
  return nodes.slice(startIdx, endIdx + 1);
247
214
  }
215
+ function readCompactionSummary(event) {
216
+ return event.data;
217
+ }
248
218
  function runCompactionTransaction(session, input) {
249
219
  assertNoActiveCompaction(session.events);
250
220
  const turn = findOpenTurn(session.events);
@@ -258,7 +228,12 @@ function runCompactionTransaction(session, input) {
258
228
  shadowedSeqs: [...input.shadowedSeqs],
259
229
  shadowedTokenCount: input.shadowedTokenCount,
260
230
  provider: input.provider,
261
- model: input.model
231
+ model: input.model,
232
+ tier: input.tier ?? 1,
233
+ ...input.kernelBlockId === void 0 ? {} : { kernelBlockId: input.kernelBlockId },
234
+ ...input.parentBlockIds === void 0 || input.parentBlockIds.length === 0 ? {} : { parentBlockIds: [...input.parentBlockIds] },
235
+ ...input.directMessageIds === void 0 ? {} : { directMessageIds: [...input.directMessageIds] },
236
+ ...input.effectiveMessageIds === void 0 ? {} : { effectiveMessageIds: [...input.effectiveMessageIds] }
262
237
  }).seq);
263
238
  const message = createUserMessage({
264
239
  content: input.summary,
@@ -271,11 +246,19 @@ function runCompactionTransaction(session, input) {
271
246
  seqs.push(session.append("compaction/end", { compactionId, turn }).seq);
272
247
  return { compactionId, seqs };
273
248
  }
249
+ function summarySeqOfCompaction(events, compactionId) {
250
+ for (const event of events) {
251
+ if (event.type !== "user/message") continue;
252
+ const source = event.data.source;
253
+ if (source?.plugin === "compact" && source.compactionId === compactionId) return event.seq;
254
+ }
255
+ return null;
256
+ }
274
257
  function rebuildBlockLedger(events) {
275
258
  const ledger = [];
276
259
  for (const event of events) {
277
260
  if (event.type !== "compaction/summary") continue;
278
- const data = event.data;
261
+ const data = readCompactionSummary(event);
279
262
  let shadowedTokenCount = data.shadowedTokenCount;
280
263
  if (shadowedTokenCount === 0) {
281
264
  shadowedTokenCount = 0;
@@ -284,13 +267,25 @@ function rebuildBlockLedger(events) {
284
267
  if (original !== void 0) shadowedTokenCount += defaultCountTokens(extractEventText(original));
285
268
  }
286
269
  }
270
+ const tier = data.tier === 2 || data.tier === 3 ? data.tier : 1;
271
+ const parentBlockIds = Array.isArray(data.parentBlockIds) ? [...data.parentBlockIds] : [];
272
+ const directMessageIds = Array.isArray(data.directMessageIds) ? [...data.directMessageIds] : void 0;
273
+ const effectiveMessageIds = Array.isArray(data.effectiveMessageIds) ? [...data.effectiveMessageIds] : void 0;
274
+ const summarySeq = summarySeqOfCompaction(events, data.compactionId);
287
275
  ledger.push({
288
276
  blockId: data.compactionId,
289
277
  summary: extractText(data.summary),
290
278
  shadowedSeqs: [...data.shadowedSeqs],
291
279
  shadowedTokenCount,
292
280
  start: data.shadowedRange.start,
293
- end: data.shadowedRange.end
281
+ end: data.shadowedRange.end,
282
+ tier,
283
+ parentBlockIds,
284
+ ...typeof data.kernelBlockId === "string" ? { kernelBlockId: data.kernelBlockId } : {},
285
+ ...summarySeq === null ? {} : { summarySeq },
286
+ ...directMessageIds === void 0 ? {} : { directMessageIds },
287
+ ...effectiveMessageIds === void 0 ? {} : { effectiveMessageIds },
288
+ createdAt: event.time
294
289
  });
295
290
  }
296
291
  return ledger;
@@ -354,6 +349,184 @@ function surfaceSummary(session) {
354
349
  const last = nodes[nodes.length - 1];
355
350
  return `${nodes.length} nodes, seqs ${first}..${last}`;
356
351
  }
352
+ function blockRegistry(session) {
353
+ const ledger = rebuildBlockLedger(session.events);
354
+ const kernelIdOf = /* @__PURE__ */ new Map();
355
+ const raw = [];
356
+ let next = 1;
357
+ for (const entry of ledger) {
358
+ let kernelBlockId;
359
+ if (entry.kernelBlockId !== void 0 && /^b\d+$/.test(entry.kernelBlockId)) {
360
+ kernelBlockId = entry.kernelBlockId;
361
+ const num = Number(kernelBlockId.slice(1));
362
+ if (Number.isInteger(num)) next = Math.max(next, num + 1);
363
+ } else {
364
+ kernelBlockId = `b${next}`;
365
+ next += 1;
366
+ }
367
+ kernelIdOf.set(entry.blockId, kernelBlockId);
368
+ raw.push({
369
+ blockId: entry.blockId,
370
+ kernelBlockId,
371
+ tier: entry.tier,
372
+ summarySeq: entry.summarySeq ?? null,
373
+ active: true,
374
+ parentBlockIds: [...entry.parentBlockIds]
375
+ });
376
+ }
377
+ const consumed = /* @__PURE__ */ new Set();
378
+ for (const entry of raw) {
379
+ for (const parent of entry.parentBlockIds) consumed.add(parent);
380
+ }
381
+ return raw.map((entry) => ({
382
+ ...entry,
383
+ active: !consumed.has(entry.blockId)
384
+ }));
385
+ }
386
+ function blockRefForSummarySeq(session, seq) {
387
+ const event = session.events[seq];
388
+ if (event?.type !== "user/message") return null;
389
+ const source = event.data.source;
390
+ if (source?.plugin !== "compact" || source.compactionId === void 0) return null;
391
+ const entry = blockRegistry(session).find((r) => r.blockId === source.compactionId);
392
+ if (entry === void 0) return null;
393
+ return entry.kernelBlockId;
394
+ }
395
+ function compactionIdsOfKernelBlocks(session, kernelBlockIds) {
396
+ if (kernelBlockIds.length === 0) return [];
397
+ const byKernel = new Map(blockRegistry(session).map((r) => [r.kernelBlockId, r.blockId]));
398
+ return kernelBlockIds.map((id) => byKernel.get(id)).filter((id) => id !== void 0);
399
+ }
400
+ function summarySeqOfKernelBlock(session, kernelBlockId) {
401
+ const entry = blockRegistry(session).find((r) => r.kernelBlockId === kernelBlockId);
402
+ return entry?.active ? entry.summarySeq : null;
403
+ }
404
+ function checkpointBlockIdOf(events, seq) {
405
+ const event = events[seq];
406
+ if (event?.type !== "user/message") return null;
407
+ const source = event.data.source;
408
+ if (source?.plugin !== "compact" || source.compactionId === void 0) return null;
409
+ return source.compactionId;
410
+ }
411
+ function expandShadowedSeqs(session, blockId) {
412
+ const ledger = rebuildBlockLedger(session.events);
413
+ const byId = new Map(ledger.map((entry) => [entry.blockId, entry]));
414
+ const root = byId.get(blockId);
415
+ if (root === void 0) return [];
416
+ const out = [];
417
+ const seen = /* @__PURE__ */ new Set();
418
+ const visit = (entry) => {
419
+ if (seen.has(entry.blockId)) return;
420
+ seen.add(entry.blockId);
421
+ for (const seq of entry.shadowedSeqs) {
422
+ const childId = checkpointBlockIdOf(session.events, seq);
423
+ const child = childId === null ? void 0 : byId.get(childId);
424
+ if (child !== void 0) visit(child);
425
+ else out.push(seq);
426
+ }
427
+ };
428
+ visit(root);
429
+ return out;
430
+ }
431
+
432
+ // src/state.ts
433
+ function rebuildKernelBlocks(events) {
434
+ const ledger = rebuildBlockLedger(events);
435
+ if (ledger.length === 0) return [];
436
+ const kernelIdOf = /* @__PURE__ */ new Map();
437
+ const parentKernelIds = /* @__PURE__ */ new Map();
438
+ let next = 1;
439
+ for (const entry of ledger) {
440
+ let kernelBlockId;
441
+ if (entry.kernelBlockId !== void 0 && /^b\d+$/.test(entry.kernelBlockId)) {
442
+ kernelBlockId = entry.kernelBlockId;
443
+ const num = Number(kernelBlockId.slice(1));
444
+ if (Number.isInteger(num)) next = Math.max(next, num + 1);
445
+ } else {
446
+ kernelBlockId = `b${next}`;
447
+ next += 1;
448
+ }
449
+ kernelIdOf.set(entry.blockId, kernelBlockId);
450
+ parentKernelIds.set(
451
+ entry.blockId,
452
+ entry.parentBlockIds.map((parent) => kernelIdOf.get(parent)).filter((id) => id !== void 0)
453
+ );
454
+ }
455
+ const consumed = /* @__PURE__ */ new Set();
456
+ for (const entry of ledger) {
457
+ for (const parent of entry.parentBlockIds) consumed.add(parent);
458
+ }
459
+ const blocks = [];
460
+ for (const entry of ledger) {
461
+ const blockId = kernelIdOf.get(entry.blockId);
462
+ const direct = entry.directMessageIds ?? [...entry.shadowedSeqs.map(String)];
463
+ const effective = entry.effectiveMessageIds ?? (entry.tier > 1 ? entry.summarySeq === void 0 ? [...entry.shadowedSeqs.map(String)] : [String(entry.summarySeq)] : [...entry.shadowedSeqs.map(String)]);
464
+ blocks.push({
465
+ blockId,
466
+ runId: `r${blocks.length + 1}`,
467
+ tier: entry.tier,
468
+ summary: entry.summary,
469
+ directMessageIds: [...direct],
470
+ effectiveMessageIds: [...effective],
471
+ directBlockIds: parentKernelIds.get(entry.blockId) ?? [],
472
+ compressedTokens: entry.shadowedTokenCount,
473
+ createdAt: entry.createdAt,
474
+ survivedCount: 0,
475
+ generation: "young",
476
+ active: !consumed.has(entry.blockId)
477
+ });
478
+ }
479
+ return blocks;
480
+ }
481
+ function nextBlockIdAfter(events) {
482
+ const blocks = rebuildKernelBlocks(events);
483
+ let max = 0;
484
+ for (const block of blocks) {
485
+ const num = Number(block.blockId.slice(1));
486
+ if (Number.isInteger(num)) max = Math.max(max, num);
487
+ }
488
+ return max + 1;
489
+ }
490
+ var AcpStateStore = class {
491
+ states = /* @__PURE__ */ new Map();
492
+ /** Kernel state for one session, initialised on first access. */
493
+ stateFor(session) {
494
+ const id = session.id;
495
+ const existing = this.states.get(id);
496
+ if (existing !== void 0) return existing;
497
+ const state = createInitialState();
498
+ if (session.events.some((event) => event.type === "compaction/summary")) {
499
+ state.blocks = rebuildKernelBlocks(session.events);
500
+ state.nextBlockId = nextBlockIdAfter(session.events);
501
+ }
502
+ this.states.set(id, state);
503
+ return state;
504
+ }
505
+ set(session, state) {
506
+ this.states.set(session.id, state);
507
+ }
508
+ delete(session) {
509
+ this.states.delete(session.id);
510
+ }
511
+ };
512
+
513
+ // src/tools.ts
514
+ import { defineTool } from "@deepseek-ai/dsh-tools";
515
+ import { defaultCountTokens as defaultCountTokens2 } from "acp-kernel";
516
+
517
+ // src/config.ts
518
+ import { defaultConfig } from "acp-kernel";
519
+ function kernelConfigFor(input) {
520
+ const nudgePatch = {};
521
+ if (input.nudgeMinContextLimitPct !== void 0) nudgePatch.minContextLimitPct = input.nudgeMinContextLimitPct;
522
+ if (input.nudgeMaxContextLimitPct !== void 0) nudgePatch.maxContextLimitPct = input.nudgeMaxContextLimitPct;
523
+ if (input.nudgeEmergencyThresholdPct !== void 0) nudgePatch.emergencyThresholdPct = input.nudgeEmergencyThresholdPct;
524
+ const overrides = { ...input.coreOverrides };
525
+ if (Object.keys(nudgePatch).length > 0) {
526
+ overrides.nudge = { ...defaultConfig(input.modelContextLimit).nudge, ...nudgePatch };
527
+ }
528
+ return defaultConfig(input.modelContextLimit, overrides);
529
+ }
357
530
 
358
531
  // src/tools.ts
359
532
  function textOutput() {
@@ -412,8 +585,9 @@ async function handleCompress(env, args, exec) {
412
585
  const agent = requireAgent(exec);
413
586
  const session = agent.session;
414
587
  const state = env.store.stateFor(session);
415
- const coreMessages = eventsToCoreMessages(surfaceEventsOf(session));
416
- const tokenCount = coreMessages.reduce((sum, message) => sum + defaultCountTokens2(message.text ?? ""), 0);
588
+ const coreMessages = allLogMessages(session);
589
+ const surfaceMessages = eventsToCoreMessages(surfaceEventsOf(session));
590
+ const tokenCount = surfaceMessages.reduce((sum, message) => sum + defaultCountTokens2(message.text ?? ""), 0);
417
591
  const config = kernelConfigFor(env);
418
592
  const turn = env.kernel.processTurn({ messages: coreMessages, state, config, tokenCount });
419
593
  env.store.set(session, turn.state);
@@ -422,8 +596,10 @@ async function handleCompress(env, args, exec) {
422
596
  const startSeq = parseSeq(range.startSeq);
423
597
  const endSeq = parseSeq(range.endSeq);
424
598
  const { start, end } = resolveSurfaceRange(session, startSeq, endSeq);
425
- const startRef = byRaw[String(start)];
426
- const endRef = byRaw[String(end)];
599
+ const startBlockRef = blockRefForSummarySeq(session, start);
600
+ const endBlockRef = blockRefForSummarySeq(session, end);
601
+ const startRef = startBlockRef ?? byRaw[String(start)];
602
+ const endRef = endBlockRef ?? byRaw[String(end)];
427
603
  if (startRef === void 0 || endRef === void 0) {
428
604
  throw new Error(
429
605
  `billion-context-dsh: seq ${start}..${end} has no assigned ref \u2014 the range must be on the current surface (run acp_status for the live seq list)`
@@ -445,15 +621,44 @@ async function handleCompress(env, args, exec) {
445
621
  messages: coreMessages,
446
622
  state: turn.state,
447
623
  config
624
+ // Deliberately NOT overriding protectedMessageIds: with the full log the
625
+ // kernel's recent/last-user protection is computed over the same
626
+ // non-block-covered messages as the visible feed, so default behavior is
627
+ // preserved. Any 'Excluded N protected message(s)' warning is surfaced.
448
628
  });
449
629
  if (applied.result.errors.length > 0) {
450
630
  return { text: `compress failed: ${applied.result.errors.join("; ")}` };
451
631
  }
452
632
  env.store.set(session, applied.state);
633
+ const previousIds = new Set(turn.state.blocks.map((block) => block.blockId));
634
+ const newBlocks = applied.state.blocks.filter((block) => !previousIds.has(block.blockId));
635
+ const blockByRangeKey = new Map(newBlocks.map((block) => [`${block.startRef}::${block.endRef}`, block]));
636
+ const warningByRangeKey = /* @__PURE__ */ new Map();
637
+ const freeWarnings = [];
638
+ for (const warning of applied.result.warnings) {
639
+ const match = /^Skipped range \((.+?)\.\.(.+?)\)/.exec(warning);
640
+ if (match !== null) {
641
+ const key = `${match[1]}::${match[2]}`;
642
+ const list = warningByRangeKey.get(key) ?? [];
643
+ list.push(warning);
644
+ warningByRangeKey.set(key, list);
645
+ } else {
646
+ freeWarnings.push(warning);
647
+ }
648
+ }
453
649
  const lines = [];
650
+ let skippedRanges = 0;
454
651
  for (let index = 0; index < ranges.length; index += 1) {
455
652
  const range = ranges[index];
456
653
  const original = args.content[index];
654
+ const key = `${range.startRef}::${range.endRef}`;
655
+ const block = blockByRangeKey.get(key);
656
+ if (block === void 0) {
657
+ skippedRanges += 1;
658
+ const warnings = warningByRangeKey.get(key) ?? [];
659
+ for (const warning of warnings) lines.push(` ${warning}`);
660
+ continue;
661
+ }
457
662
  const { start, end } = range;
458
663
  const shadowed = shadowedSeqsOf(session, start, end);
459
664
  let shadowedTokens = 0;
@@ -461,6 +666,8 @@ async function handleCompress(env, args, exec) {
461
666
  const event = session.events[seq];
462
667
  if (event !== void 0) shadowedTokens += defaultCountTokens2(extractEventText(event));
463
668
  }
669
+ const tier = block.tier === 2 || block.tier === 3 ? block.tier : 1;
670
+ const parentBlockIds = compactionIdsOfKernelBlocks(session, block.directBlockIds);
464
671
  const { compactionId } = runCompactionTransaction(session, {
465
672
  start,
466
673
  end,
@@ -468,17 +675,27 @@ async function handleCompress(env, args, exec) {
468
675
  summary: [{ type: "text", text: original.summary }],
469
676
  shadowedTokenCount: shadowedTokens,
470
677
  provider: agent.options.provider ?? "",
471
- model: agent.options.model ?? ""
678
+ model: agent.options.model ?? "",
679
+ tier,
680
+ kernelBlockId: block.blockId,
681
+ ...parentBlockIds.length === 0 ? {} : { parentBlockIds },
682
+ // Record the kernel block's raw coverage so a restarted engine
683
+ // rehydrates the SAME effective messages (a tier-2 block's coverage is
684
+ // its parents' originals, not the checkpoint node).
685
+ directMessageIds: block.directMessageIds,
686
+ effectiveMessageIds: block.effectiveMessageIds
472
687
  });
473
688
  const adjusted = start !== range.startSeq || end !== range.endSeq;
689
+ const tierLabel = tier === 1 ? "" : `, tier ${tier}`;
474
690
  lines.push(
475
- ` block ${compactionId.slice(0, 8)}: seqs ${start}..${end}, ${shadowed.length} messages shadowed` + (adjusted ? ` (adjusted from ${range.startSeq}..${range.endSeq} to balanced edges)` : "")
691
+ ` block ${compactionId.slice(0, 8)}: seqs ${start}..${end}, ${shadowed.length} messages shadowed${tierLabel}` + (adjusted ? ` (adjusted from ${range.startSeq}..${range.endSeq} to balanced edges)` : "")
476
692
  );
477
693
  }
478
- return {
479
- text: `Compressed ${applied.result.blocksCreated} block(s), ~${applied.result.tokensCompressed} tokens reclaimed.
480
- ${lines.join("\n")}`
481
- };
694
+ const summaryLine = `Compressed ${applied.result.blocksCreated} block(s), ~${applied.result.tokensCompressed} tokens reclaimed.`;
695
+ const warningLines = [...freeWarnings.map((warning) => ` ${warning}`), ...lines];
696
+ const footer = skippedRanges > 0 ? ` (${skippedRanges} range(s) skipped \u2014 see warnings above)` : "";
697
+ return { text: `${summaryLine}
698
+ ${[...warningLines, footer].filter((line) => line !== "").join("\n")}` };
482
699
  }
483
700
  var decompressParameters = {
484
701
  blockId: { type: "string", required: true, description: "Block id from acp_status or search_context (the compaction id)." }
@@ -491,13 +708,14 @@ function handleDecompress(_env, args, exec) {
491
708
  return { text: `decompress: block "${args.blockId}" not found (see acp_status for the block list)` };
492
709
  }
493
710
  const parts = [];
494
- for (const seq of block.shadowedSeqs) {
711
+ for (const seq of expandShadowedSeqs(session, block.blockId)) {
495
712
  const event = session.events[seq];
496
713
  const text = event === void 0 ? "" : extractEventText(event);
497
714
  if (text.length > 0) parts.push(`[seq ${seq}] ${text}`);
498
715
  }
716
+ const tierNote = block.tier > 1 ? ` (tier ${block.tier}, distills ${block.parentBlockIds.length} block(s))` : "";
499
717
  return {
500
- text: `Block ${block.blockId} \u2014 ${block.summary}
718
+ text: `Block ${block.blockId} \u2014 ${block.summary}${tierNote}
501
719
 
502
720
  ${parts.join("\n\n") || "(no recoverable content)"}`
503
721
  };
@@ -551,7 +769,7 @@ function makeTools(env) {
551
769
  return [
552
770
  defineTool({
553
771
  name: "compress",
554
- description: "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. Never compress content the current step is actively using.",
772
+ description: "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.",
555
773
  parameters: compressParameters,
556
774
  output: textOutput(),
557
775
  async execute(args, exec) {
@@ -604,7 +822,8 @@ function statusText(env, agent) {
604
822
  ` estimated context: ${estimated} / ${limit} (${Math.round(estimated / limit * 100)}%)`
605
823
  ];
606
824
  for (const block of ledger.slice(0, 10)) {
607
- lines.push(` - ${block.blockId.slice(0, 8)}: seqs ${block.start}..${block.end} \u2014 ${block.summary.slice(0, 80)}`);
825
+ const tier = block.tier > 1 ? ` [T${block.tier}]` : "";
826
+ lines.push(` - ${block.blockId.slice(0, 8)}${tier}: seqs ${block.start}..${block.end} \u2014 ${block.summary.slice(0, 80)}`);
608
827
  }
609
828
  return lines.join("\n");
610
829
  }
@@ -620,6 +839,9 @@ function compressText(env, agent, args) {
620
839
  }
621
840
  const session = agent.session;
622
841
  const { start, end } = resolveSurfaceRange(session, startSeq, endSeq);
842
+ if (blockRefForSummarySeq(session, start) !== null || blockRefForSummarySeq(session, end) !== null) {
843
+ return "/acp compress: the range touches a compressed block summary node \u2014 distill it with the compress tool (seq-based batch), not /acp compress";
844
+ }
623
845
  const shadowed = shadowedSeqsOf(session, startSeq, endSeq);
624
846
  let shadowedTokens = 0;
625
847
  for (const seq of shadowed) {
@@ -643,7 +865,7 @@ function decompressText(_env, agent, args) {
643
865
  const ledger = rebuildBlockLedger(session.events);
644
866
  const block = ledger.find((entry) => entry.blockId.startsWith(args[0]));
645
867
  if (block === void 0) return `block "${args[0]}" not found (see /acp status)`;
646
- const parts = block.shadowedSeqs.map((seq) => extractEventText(session.events[seq])).filter((text) => text.length > 0);
868
+ const parts = expandShadowedSeqs(session, block.blockId).map((seq) => extractEventText(session.events[seq])).filter((text) => text.length > 0);
647
869
  return `Block ${block.blockId} \u2014 ${block.summary}
648
870
 
649
871
  ${parts.join("\n\n") || "(no recoverable content)"}`;
@@ -680,9 +902,9 @@ function rangeTable(session) {
680
902
  return [
681
903
  "",
682
904
  `Surface: ${surfaceSummary(session)}`,
683
- "Compressible ranges (refs are surface seqs):",
905
+ "Compressible ranges (suggestions only \u2014 compress any consumed span; refs are surface seqs):",
684
906
  ...lines,
685
- "Compress with: compress({ content: [{ startSeq, endSeq, summary }] })"
907
+ "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."
686
908
  ].join("\n");
687
909
  }
688
910
  function measuredTokenCount(agent, coreMessages) {
@@ -694,8 +916,9 @@ function measuredTokenCount(agent, coreMessages) {
694
916
  function buildNudge(agent, env, lastNudgeTurn) {
695
917
  const session = agent.session;
696
918
  const state = env.store.stateFor(session);
697
- const coreMessages = eventsToCoreMessages(surfaceEventsOf(session));
698
- const tokenCount = measuredTokenCount(agent, coreMessages);
919
+ const coreMessages = allLogMessages(session);
920
+ const surfaceMessages = eventsToCoreMessages(surfaceEventsOf(session));
921
+ const tokenCount = measuredTokenCount(agent, surfaceMessages);
699
922
  const config = kernelConfigFor(env);
700
923
  const turn = env.kernel.processTurn({ messages: coreMessages, state, config, tokenCount });
701
924
  env.store.set(session, turn.state);
@@ -717,7 +940,18 @@ function buildNudgeText(nudge, emergency, session) {
717
940
  const pct = Math.round(Math.min(nudge.contextUsage, 1) * 100);
718
941
  const frame = emergency ? `\u26A0\uFE0F Context usage is at ${pct}% of the window \u2014 nearly full. Consider compressing consumed ranges soon so working context stays available; the choice and timing are yours.` : `Context usage is at ${pct}%. This is a suggestion, not a requirement \u2014 you decide whether and when to compress.`;
719
942
  const guidance = "Compress by need, not by percentage: replace only ranges you have genuinely consumed, with dense self-contained summaries.";
720
- return [frame, "", guidance, rangeTable(session)].join("\n");
943
+ const parts = [frame, "", guidance];
944
+ if ((nudge.tier === 2 || nudge.tier === 3) && (nudge.tierTargetBlocks?.length ?? 0) > 0) {
945
+ const targets = nudge.tierTargetBlocks;
946
+ const summarySeqs = targets.map((block) => summarySeqOfKernelBlock(session, block.blockId)).filter((seq) => seq !== null);
947
+ const pending = nudge.tier === 2 ? nudge.breakdown?.pendingT2 : nudge.breakdown?.pendingT3;
948
+ const tokens = typeof pending === "number" ? pending : 0;
949
+ parts.push(
950
+ `Tier ${nudge.tier}: ${targets.length} tier-${nudge.tier - 1} block(s) distillable (${tokens} tokens) \u2014 compress their summary node(s) [seqs ${summarySeqs.join(", ")}] to reclaim the original messages.`
951
+ );
952
+ }
953
+ parts.push(rangeTable(session));
954
+ return parts.join("\n");
721
955
  }
722
956
 
723
957
  // src/system-prompt.ts
@@ -729,11 +963,13 @@ YOU decide whether and when to compress context. Nothing forces you: the injecte
729
963
  ${COMPRESS_PHILOSOPHY}
730
964
 
731
965
  Compression tools (refs are SURFACE SEQS, not ids):
732
- - compress: replace a seq range with your dense self-contained summary. compress({ content: [{ startSeq, endSeq, summary }] }). Edges are auto-balanced to tool-call/result boundaries; a trailing #callId fragment in a seq is ignored. Ranges must be on the current surface \u2014 stale seqs fail with guidance.
966
+ - 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. Ranges must be on the current surface \u2014 stale seqs fail with guidance.
733
967
  - decompress: recover a compressed block's original content, read-only. decompress({ blockId }).
734
968
  - search_context: find information inside compressed blocks BEFORE decompressing. search_context({ query }).
735
969
  - acp_status: current context usage and the live compressible-range list. Run it before compressing when in doubt.
736
970
 
971
+ Tiered compression: each compressed block appears on the surface as one summary node. Compressing that node again DISTILLS the block (tier 2): the parent summary folds into your new summary and the original messages are freed. Distilling a tier-2 block yields tier 3. Distill when a summary itself is consumed \u2014 decompress on the tier-2 block recovers the full originals.
972
+
737
973
  When you write a summary, it becomes the ONLY record of that range: keep file paths, signatures, exact values, decisions, and error strings verbatim so a later reader (or you, after decompress) can continue without the original. Never reuse historical seqs \u2014 the surface moves as messages land and compress; verify with acp_status.`;
738
974
  var ACP_SYSTEM_PROMPT_ORDER = 150;
739
975
 
@@ -850,9 +1086,13 @@ export {
850
1086
  AcpStateStore,
851
1087
  acpCommand,
852
1088
  assertNoActiveCompaction,
1089
+ blockRefForSummarySeq,
1090
+ blockRegistry,
853
1091
  buildNudge,
1092
+ compactionIdsOfKernelBlocks,
854
1093
  index_default as default,
855
1094
  eventsToCoreMessages,
1095
+ expandShadowedSeqs,
856
1096
  extractEventText,
857
1097
  findOpenTurn,
858
1098
  kernelConfigFor,
@@ -863,6 +1103,7 @@ export {
863
1103
  resolveSurfaceRange,
864
1104
  runCompactionTransaction,
865
1105
  shadowedSeqsOf,
1106
+ summarySeqOfKernelBlock,
866
1107
  surfaceEventsOf
867
1108
  };
868
1109
  //# sourceMappingURL=index.js.map