billion-context-dsh 0.2.2 → 0.2.3

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.2.2**) 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.2.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.
7
7
 
8
8
  <p align="center">
9
9
  <strong>Built with gratitude on top of these projects</strong> — please give them a ⭐:
@@ -41,10 +41,11 @@ When conversations get long, the model runs out of context. Most tools hard-trun
41
41
 
42
42
  Unlike DSH's built-in auto-compaction (which replaces a range with an automatically generated summary), billion-context-dsh:
43
43
 
44
- - **Model-driven** — the model writes the summary itself; there is no second LLM summarization call (the ACP cost win)
44
+ - **Model-driven** — the model writes the summary itself; there is no second LLM summarization call
45
45
  - **Advisory, never imperative** — automatic policy only *nudges*; the model decides whether and when to compress
46
46
  - **Durable & recoverable** — a compressed range becomes a checkpoint node, the originals stay in the append-only session log; `decompress` restores them, `search_context` finds information inside blocks
47
- - **Seq-based refs** — no message tags; surface seqs are carried by the nudge's range table, with auto-balanced range edges and `#callId` tolerance
47
+ - **Long tasks hold steady** — every step builds on the results before it; key conclusions stay usable and compound, so very long tasks actually finish
48
+ - **Context stays lean** — every request rides on a small, distilled slice of context with only the key information; no bulk compression of large ranges, so details don't decay with it — and tokens stay low
48
49
 
49
50
  This is the DeepSeek Harness port of [billion-context-pi](https://github.com/ranxianglei/billion-context-pi) (the Pi coding-agent adapter): the compression core ([acp-kernel](https://github.com/ranxianglei/acp-kernel)) is reused verbatim, and the adapter layer was rewritten against DSH's durable-surface model — see [docs](https://github.com/Tyan66666/billion-context-dsh/tree/main/docs) for the verified mapping.
50
51
 
@@ -129,7 +130,7 @@ DSH derives every model request from its append-only session log (the *surface*)
129
130
  | nudge ("efficiency note — compress early and keep context lean") | injected at `agent/pre-step` by the kernel's pressure decision — efficiency note + context breakdown + compression rules, tone aligned with kernel/pi; never an order |
130
131
  | `decompress` | read-only recovery of shadowed originals from the log |
131
132
  | `search_context` | scores block summaries + originals rebuilt from the log |
132
- | `acp_status` | block ledger + context pressure |
133
+ | `acp_status` | CONTEXT BREAKDOWN (tool/text/summaries shares of the visible total) + compressed-block ledger + nudge decision line; no context-window rows; scope/view/tool/sort/limit drilldown supported |
133
134
  | block state | in-memory kernel state + **log-rebuilt ledger** (no sidecar files) |
134
135
  | 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 |
135
136
 
@@ -146,9 +147,9 @@ A walkthrough of the ACP philosophy this project inherits — how active context
146
147
  | Tool | What it does |
147
148
  | --- | --- |
148
149
  | `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) |
149
- | `decompress` | Restore a previously compressed block's original content (read-only) |
150
+ | `decompress` | Restore a previously compressed block's original content (read-only); accepts the `bN` ref shown by acp_status or a compaction id |
150
151
  | `search_context` | Search compressed block summaries and originals by keyword |
151
- | `acp_status` | Context usage, compressed blocks, compressible ranges |
152
+ | `acp_status` | CONTEXT BREAKDOWN (tool/text/summaries shares of the visible total) + compressed-block ledger + nudge decision line; no context-window rows. Drilldown supported: `scope:"compressed"` per block, `scope:"uncompressed"` + `view:"messages"`/`"ranges"` per message/range, with `tool` filter, `sort` order and `limit` cap. Drilldown row refs are kernel ids (mN, size awareness only) — compress always uses the `Surface:` seqs |
152
153
  | `/acp` | status / compress / decompress from the command bar |
153
154
 
154
155
  ## Upstream & credits
@@ -169,7 +170,7 @@ This project reuses `acp-kernel`'s compression core and `billion-context-pi`'s d
169
170
  | Key | Default | Meaning |
170
171
  |---|---|---|
171
172
  | `modelContextLimit` | auto-detected (fallback `128000`) | Context window used for the kernel's pressure decisions; an explicit value wins and skips the probe |
172
- | `autoModelContextLimit` | `true` | Probe the model's real window from the model API (`agent.ctx.llm.resolveModelInfo`); fall back to the default on failure, `acp_status` shows the window source |
173
+ | `autoModelContextLimit` | `true` | Probe the model's real window from the model API (`agent.ctx.llm.resolveModelInfo`); fall back to the default on failure, the `/acp` command shows the window source (the `acp_status` model tool carries no window info) |
173
174
  | `nudgeMinContextLimitPct` | kernel default `0.45` | Nudge window lower bound (usage fraction) — validation only; the growth-driven trigger has no percentage floor — same default as billion-context-pi |
174
175
  | `nudgeMaxContextLimitPct` | engine default `0.70` (kernel/pi default `0.75`) | Over-limit line: above this the nudge fires regardless of growth — deliberately below the host compaction-basic 80% auto-compaction line so the forced nudge fires first; an explicit value wins |
175
176
  | `nudgeEmergencyThresholdPct` | engine default `0.85` (kernel/pi default `0.95`) | Emergency nudge (bypasses the per-turn dedup) — lowered from `0.95`: at 95% the model has no room to act and the 80% auto-compaction line shadows it; an explicit value wins |
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [中文](./README.md) | [English](./README.en.md)
4
4
 
5
5
  > **⚠️ 测试版声明——请勿用于生产环境**
6
- > 本项目(**v0.2.2**)仍处于开发中的测试版。[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 本身也处于**公开测试版**阶段。**请勿将两者用于工程化 / 生产环境**——预期会有破坏性变更与粗糙之处。
6
+ > 本项目(**v0.2.3**)仍处于开发中的测试版。[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 本身也处于**公开测试版**阶段。**请勿将两者用于工程化 / 生产环境**——预期会有破坏性变更与粗糙之处。
7
7
 
8
8
  <p align="center">
9
9
  <strong>衷心感谢以下项目——请给它们一个 ⭐:</strong>
@@ -41,10 +41,11 @@
41
41
 
42
42
  与 DSH 内置的自动压缩(用自动生成的摘要替换一段范围)不同,billion-context-dsh:
43
43
 
44
- - **模型驱动** —— 摘要由模型自己书写,没有第二次 LLM 摘要调用(ACP 的成本优势)
44
+ - **模型驱动** —— 摘要由模型自己书写,没有第二次 LLM 摘要调用
45
45
  - **只建议、不强令** —— 自动策略只 *nudge*(提醒),是否压缩、何时压缩由模型决定
46
46
  - **持久且可恢复** —— 压缩范围成为 checkpoint 节点,原文保留在 append-only 会话日志中;`decompress` 可恢复,`search_context` 可在块内查找
47
- - **基于 seq 引用** —— 不需要消息标签;surface seq 由 nudge 的范围表携带,范围边界自动平衡、容忍 `#callId` 片段
47
+ - **长任务稳得住** —— 每一步都接着前面的成果走,关键结论持续可用、不断叠加,超长任务更容易跑完
48
+ - **上下文始终精简** —— 每次请求都只用少量、精炼的上下文,只保留关键信息;不做大段统一压缩,细节不随之衰失,token 消耗自然更低
48
49
 
49
50
  这是 [billion-context-pi](https://github.com/ranxianglei/billion-context-pi)(Pi 编码代理适配器)在 DeepSeek Harness 上的移植:压缩内核([acp-kernel](https://github.com/ranxianglei/acp-kernel))原样复用,适配层针对 DSH 的 durable-surface 模型重写——经过验证的映射关系见 [docs](https://github.com/Tyan66666/billion-context-dsh/tree/main/docs)。
50
51
 
@@ -130,7 +131,7 @@ DSH 的每个模型请求都派生自其 append-only 会话日志(*surface*)
130
131
  | nudge("效率提示——尽早压缩保持精简") | 由内核的压力决策在 `agent/pre-step` 注入——效率通知 + 上下文分解 + 压缩规则,语气对齐 kernel/pi;绝非命令 |
131
132
  | `decompress` | 从日志只读恢复被遮蔽的原文 |
132
133
  | `search_context` | 对从日志重建的块摘要与原文打分 |
133
- | `acp_status` | 块账本与上下文压力 |
134
+ | `acp_status` | CONTEXT BREAKDOWN(tool/text/summaries 占可见总量)+ 压缩块账本 + nudge 决策行;不含上下文窗口;支持 scope/view/tool/sort/limit 钻取 |
134
135
  | 块状态 | 内存内核状态 + **日志重建账本**(无旁车文件) |
135
136
  | 分层蒸馏(T2/T3) | 再次压缩某块的摘要节点 = 蒸馏该块(tier 2),蒸馏 tier-2 块得 tier 3;tier 与内核块 id 持久化进日志,重启后内核状态从日志再水合、可继续蒸馏 |
136
137
 
@@ -147,9 +148,9 @@ DSH 的每个模型请求都派生自其 append-only 会话日志(*surface*)
147
148
  | 工具 | 作用 |
148
149
  | --- | --- |
149
150
  | `compress` | 用你书写的紧凑摘要替换 seq 范围(边界自动平衡到 tool-call/result 配对点);对某块的摘要节点再次压缩 = 分层蒸馏(tier 2/3) |
150
- | `decompress` | 恢复已压缩块的原始内容(只读) |
151
+ | `decompress` | 恢复已压缩块的原始内容(只读);接受 acp_status 显示的 `bN` 或 compaction id |
151
152
  | `search_context` | 按关键词搜索压缩块摘要与原文 |
152
- | `acp_status` | 上下文占用、压缩块、可压缩范围 |
153
+ | `acp_status` | CONTEXT BREAKDOWN(tool/text/summaries 占可见总量)+ 压缩块账本 + nudge 决策行;不含上下文窗口。支持钻取:`scope:"compressed"` 逐块、`scope:"uncompressed"` + `view:"messages"`/`"ranges"` 逐消息/区间,`tool` 过滤、`sort` 排序、`limit` 截断。钻取行 ref 是内核 mN(仅供体量感知)——压缩始终用 `Surface:` 的 seq |
153
154
  | `/acp` | 从命令栏执行 status / compress / decompress |
154
155
 
155
156
  ## 上游项目与致谢
@@ -170,7 +171,7 @@ DSH 的每个模型请求都派生自其 append-only 会话日志(*surface*)
170
171
  | 键 | 默认值 | 含义 |
171
172
  |---|---|---|
172
173
  | `modelContextLimit` | 自动探测(回退 `128000`) | 用于内核压力决策的上下文窗口;显式配置时优先且跳过探测 |
173
- | `autoModelContextLimit` | `true` | 从模型 API 自动探测真实窗口(`agent.ctx.llm.resolveModelInfo`);探测失败回退默认值,`acp_status` 展示窗口来源 |
174
+ | `autoModelContextLimit` | `true` | 从模型 API 自动探测真实窗口(`agent.ctx.llm.resolveModelInfo`);探测失败回退默认值,`/acp` 命令展示窗口来源(模型工具 `acp_status` 不含窗口信息) |
174
175
  | `nudgeMinContextLimitPct` | 内核默认 `0.45` | Nudge 窗口下界(用量占比)——仅作配置校验,增长路径的触发没有百分比下限——与 billion-context-pi 相同的默认值 |
175
176
  | `nudgeMaxContextLimitPct` | engine 默认 `0.70`(内核/pi 默认 `0.75`) | 过限线:超过此值则无论增长与否都触发 nudge——刻意低于宿主 compaction-basic 的 80% 自动压缩线,保证强制 nudge 先触发;显式配置优先 |
176
177
  | `nudgeEmergencyThresholdPct` | engine 默认 `0.85`(内核/pi 默认 `0.95`) | 紧急 nudge(绕过每轮去重)——从 `0.95` 下调:95% 时模型已无操作空间且会被 80% 自动压缩线遮蔽;显式配置优先 |
package/dist/index.js CHANGED
@@ -48,7 +48,29 @@ function stringifyArgs(args) {
48
48
  return String(args);
49
49
  }
50
50
  }
51
- function projectEvent(event) {
51
+ function toolCallIdOfResultEvent(event) {
52
+ if (event.type !== "tool/result") return null;
53
+ const message = event.data.message;
54
+ const block = Array.isArray(message?.content) ? message.content.find((candidate) => candidate?.type === "tool-result") : void 0;
55
+ const id = block?.toolCallId ?? message?.source?.callId;
56
+ return typeof id === "string" ? id : null;
57
+ }
58
+ function buildToolCallIndex(events) {
59
+ const index = /* @__PURE__ */ new Map();
60
+ for (const event of events) {
61
+ if (event.type !== "assistant/message") continue;
62
+ const content = event.data.message?.content;
63
+ if (!Array.isArray(content)) continue;
64
+ for (const block of content) {
65
+ const candidate = block;
66
+ if (candidate !== null && typeof candidate === "object" && candidate.type === "tool-call" && typeof candidate.id === "string") {
67
+ index.set(candidate.id, typeof candidate.name === "string" ? candidate.name : "");
68
+ }
69
+ }
70
+ }
71
+ return index;
72
+ }
73
+ function projectEvent(event, toolNames) {
52
74
  switch (event.type) {
53
75
  case "user/message": {
54
76
  const text = extractText(event.data.content);
@@ -88,12 +110,13 @@ ${argStr}` : argStr || text;
88
110
  const message = event.data.message;
89
111
  const text = extractText(message?.content);
90
112
  if (text.length === 0) return [];
113
+ const key = toolCallIdOfResultEvent(event);
91
114
  return [{
92
115
  id: String(event.seq),
93
116
  role: "tool",
94
117
  contentType: "tool-result",
95
- toolName: message?.toolName ?? "",
96
- toolCallId: message?.toolCallId ?? "",
118
+ toolName: toolNames?.get(key ?? "") ?? "",
119
+ toolCallId: message?.toolCallId ?? key ?? "",
97
120
  text
98
121
  }];
99
122
  }
@@ -101,9 +124,10 @@ ${argStr}` : argStr || text;
101
124
  return [];
102
125
  }
103
126
  }
104
- function eventsToCoreMessages(events) {
127
+ function eventsToCoreMessages(events, toolNames) {
128
+ const index = toolNames ?? buildToolCallIndex(events);
105
129
  const out = [];
106
- for (const event of events) out.push(...projectEvent(event));
130
+ for (const event of events) out.push(...projectEvent(event, index));
107
131
  return out;
108
132
  }
109
133
  function surfaceEventsOf(session) {
@@ -359,13 +383,6 @@ function toolCallIdsOfEvent(event) {
359
383
  }
360
384
  return ids;
361
385
  }
362
- function toolCallIdOfResultEvent(event) {
363
- if (event.type !== "tool/result") return null;
364
- const message = event.data.message;
365
- const block = Array.isArray(message?.content) ? message.content.find((candidate) => candidate?.type === "tool-result") : void 0;
366
- const id = block?.toolCallId ?? message?.source?.callId;
367
- return typeof id === "string" ? id : null;
368
- }
369
386
  function assistantProviderModel(event) {
370
387
  if (event.type === "assistant/message") {
371
388
  const message = event.data.message;
@@ -588,8 +605,12 @@ function buildCompressibleSeqRanges(session, opts = {}) {
588
605
  function surfaceSummary(session) {
589
606
  const nodes = session.surface.nodes;
590
607
  if (nodes.length === 0) return "empty";
591
- const first = nodes[0];
592
- const last = nodes[nodes.length - 1];
608
+ let first = nodes[0];
609
+ let last = nodes[0];
610
+ for (const seq of nodes) {
611
+ if (seq < first) first = seq;
612
+ if (seq > last) last = seq;
613
+ }
593
614
  return `${nodes.length} nodes, seqs ${first}..${last}`;
594
615
  }
595
616
  function blockRegistry(session) {
@@ -640,6 +661,11 @@ function compactionIdsOfKernelBlocks(session, kernelBlockIds) {
640
661
  const byKernel = new Map(blockRegistry(session).map((r) => [r.kernelBlockId, r.blockId]));
641
662
  return kernelBlockIds.map((id) => byKernel.get(id)).filter((id) => id !== void 0);
642
663
  }
664
+ function blockIdOfKernelRef(session, kernelRef) {
665
+ if (!/^b\d+$/.test(kernelRef)) return null;
666
+ const entry = blockRegistry(session).find((r) => r.kernelBlockId === kernelRef);
667
+ return entry?.blockId ?? null;
668
+ }
643
669
  function summarySeqOfKernelBlock(session, kernelBlockId) {
644
670
  const entry = blockRegistry(session).find((r) => r.kernelBlockId === kernelBlockId);
645
671
  return entry?.active ? entry.summarySeq : null;
@@ -755,7 +781,7 @@ var AcpStateStore = class {
755
781
 
756
782
  // src/tools.ts
757
783
  import { defineTool } from "@deepseek-ai/dsh-tools";
758
- import { defaultCountTokens as defaultCountTokens3 } from "acp-kernel";
784
+ import { buildStatusReport, defaultCountTokens as defaultCountTokens3 } from "acp-kernel";
759
785
 
760
786
  // src/config.ts
761
787
  import { defaultConfig } from "acp-kernel";
@@ -874,10 +900,10 @@ var DEFAULT_PROMPTS = {
874
900
  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."
875
901
  },
876
902
  tools: {
877
- 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. 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.",
878
- decompress: "Recover the original content of a compressed block by its blockId (read-only; does not unshadow the range).",
903
+ 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. 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.",
904
+ 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).",
879
905
  searchContext: "Search inside compressed blocks (summaries and original content) for information the model no longer sees in context.",
880
- acpStatus: "Report the ACP block ledger: compressed blocks, reclaimed tokens, and current context pressure."
906
+ 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) for size awareness only \u2014 compress always uses the Surface: seqs, never mN.'
881
907
  },
882
908
  systemPromptTemplate: `Active Context Pruning \u2014 model-driven context management
883
909
 
@@ -902,10 +928,10 @@ WHEN NOT TO COMPRESS:
902
928
  {howToCompressRules}
903
929
 
904
930
  Compression tools (refs are SURFACE SEQS, not ids):
905
- - 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.
906
- - decompress: recover a compressed block's original content, read-only. decompress({ blockId }).
931
+ - 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.
932
+ - 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.
907
933
  - search_context: find information inside compressed blocks BEFORE decompressing. search_context({ query }).
908
- - 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.
934
+ - 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) for size awareness \u2014 compress uses seqs, never mN.
909
935
 
910
936
  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.
911
937
 
@@ -1075,28 +1101,6 @@ function renderNudgeFromTemplates(nudge, emergency, session, prompts) {
1075
1101
  return parts.join("\n");
1076
1102
  }
1077
1103
 
1078
- // src/window.ts
1079
- var DEFAULT_CONTEXT_WINDOW = 128e3;
1080
- function windowSourceLabel(window) {
1081
- if (window.source === "explicit") return "configured";
1082
- if (window.source === "auto") {
1083
- return `auto-detected from ${window.provider ?? "?"}/${window.model ?? "?"}`;
1084
- }
1085
- return "default (auto-detection unavailable)";
1086
- }
1087
- async function detectContextWindow(agent, provider, model) {
1088
- const llm = agent.ctx?.get?.("llm");
1089
- if (llm?.resolveModelInfo === void 0) return null;
1090
- try {
1091
- const info = await llm.resolveModelInfo(provider, model);
1092
- const window = info?.context?.contextWindow;
1093
- if (typeof window === "number" && Number.isInteger(window) && window > 0) return window;
1094
- return null;
1095
- } catch {
1096
- return null;
1097
- }
1098
- }
1099
-
1100
1104
  // src/tools.ts
1101
1105
  function textOutput() {
1102
1106
  return {
@@ -1177,6 +1181,20 @@ function unwrapCompressArgs(args) {
1177
1181
  if (content === void 0) return null;
1178
1182
  return { ...args, content };
1179
1183
  }
1184
+ function unwrapEnvelope(args) {
1185
+ const envelope = args.arguments;
1186
+ if (envelope === void 0) return args;
1187
+ let inner = envelope;
1188
+ if (typeof inner === "string") {
1189
+ try {
1190
+ inner = JSON.parse(inner);
1191
+ } catch {
1192
+ return args;
1193
+ }
1194
+ }
1195
+ if (typeof inner !== "object" || inner === null || Array.isArray(inner)) return args;
1196
+ return { ...args, ...inner };
1197
+ }
1180
1198
  async function handleCompress(env, args, exec) {
1181
1199
  const agent = requireAgent(exec);
1182
1200
  const session = agent.session;
@@ -1327,12 +1345,24 @@ async function handleCompress(env, args, exec) {
1327
1345
  ${[...warningLines, footer].filter((line) => line !== "").join("\n")}` };
1328
1346
  }
1329
1347
  var decompressParameters = {
1330
- blockId: { type: "string", required: true, description: "Block id from acp_status or search_context (the compaction id)." }
1348
+ 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." }
1331
1349
  };
1332
- function handleDecompress(_env, args, exec) {
1350
+ function resolveBlockId(session, arg) {
1351
+ const byKernelRef = blockIdOfKernelRef(session, arg);
1352
+ if (byKernelRef !== null) return byKernelRef;
1353
+ const ledger = rebuildBlockLedger(session.events);
1354
+ const byPrefix = ledger.find((entry) => entry.blockId.startsWith(arg));
1355
+ return byPrefix?.blockId ?? null;
1356
+ }
1357
+ function handleDecompress(_env, rawArgs, exec) {
1358
+ const args = unwrapEnvelope(rawArgs);
1333
1359
  const session = requireAgent(exec).session;
1360
+ const blockId = resolveBlockId(session, args.blockId);
1361
+ if (blockId === null) {
1362
+ return { text: `decompress: block "${args.blockId}" not found (see acp_status for the block list)` };
1363
+ }
1334
1364
  const ledger = rebuildBlockLedger(session.events);
1335
- const block = ledger.find((entry) => entry.blockId.startsWith(args.blockId));
1365
+ const block = ledger.find((entry) => entry.blockId === blockId);
1336
1366
  if (block === void 0) {
1337
1367
  return { text: `decompress: block "${args.blockId}" not found (see acp_status for the block list)` };
1338
1368
  }
@@ -1353,7 +1383,8 @@ var searchParameters = {
1353
1383
  query: { type: "string", required: true, description: "Search terms to find inside compressed blocks." },
1354
1384
  limit: { type: "integer", description: "Maximum results (default 5)." }
1355
1385
  };
1356
- function handleSearch(_env, args, exec) {
1386
+ function handleSearch(_env, rawArgs, exec) {
1387
+ const args = unwrapEnvelope(rawArgs);
1357
1388
  const session = requireAgent(exec).session;
1358
1389
  const ledger = rebuildBlockLedger(session.events);
1359
1390
  const terms = args.query.toLowerCase().split(/\s+/).filter(Boolean);
@@ -1374,26 +1405,63 @@ ${original}`.toLowerCase();
1374
1405
  ` + top.map((hit) => ` - ${hit.blockId} (score ${hit.score}): ${hit.summary.slice(0, 160)}`).join("\n") + "\n\nDecompress with: decompress({ blockId })"
1375
1406
  };
1376
1407
  }
1377
- var statusParameters = {};
1378
- async function handleStatus(env, _args, exec) {
1408
+ var statusParameters = {
1409
+ scope: {
1410
+ type: "string",
1411
+ enum: ["compressed", "uncompressed"],
1412
+ description: 'Drilldown scope: "compressed" lists compressed blocks, "uncompressed" lists visible messages. Omit for the overview.'
1413
+ },
1414
+ view: {
1415
+ type: "string",
1416
+ enum: ["ranges", "messages"],
1417
+ description: 'Drilldown view under scope:"uncompressed": "ranges" merges visible messages into ranges (default), "messages" lists every message.'
1418
+ },
1419
+ tool: {
1420
+ type: "string",
1421
+ description: 'Filter drilldown rows to one tool name (scope:"uncompressed" + view:"messages" only).'
1422
+ },
1423
+ sort: {
1424
+ type: "string",
1425
+ enum: ["size", "time", "tool", "age"],
1426
+ description: 'Row order: size (default, most tokens first), time, tool; "age" applies to compressed blocks.'
1427
+ },
1428
+ limit: {
1429
+ type: "integer",
1430
+ description: "Cap on rows or blocks shown (default 30)."
1431
+ }
1432
+ };
1433
+ function isCheckpointEvent(event) {
1434
+ if (event.type !== "user/message") return false;
1435
+ const source = event.data.source;
1436
+ return source?.plugin === "compact";
1437
+ }
1438
+ async function handleStatus(env, rawArgs, exec) {
1439
+ const args = unwrapEnvelope(rawArgs);
1379
1440
  const agent = requireAgent(exec);
1380
1441
  const session = agent.session;
1381
- const ledger = rebuildBlockLedger(session.events);
1382
- const totalTokens = ledger.reduce((sum, block) => sum + block.shadowedTokenCount, 0);
1383
- const coreMessages = eventsToCoreMessages(surfaceEventsOf(session));
1384
- const estimated = resolveTokenCount(agent, coreMessages);
1385
- const window = env.windowFor === void 0 ? { limit: env.modelContextLimit, source: "explicit" } : await env.windowFor(agent);
1386
- const limit = window.limit;
1387
- const lines = [
1388
- `ACP status \u2014 session ${session.id}`,
1389
- ` blocks: ${ledger.length}`,
1390
- ` tokens compressed: ${totalTokens}`,
1391
- ` estimated context: ${estimated} / ${limit} (${Math.round(estimated / limit * 100)}%)`,
1392
- ` context window: ${limit} (${windowSourceLabel(window)})`,
1393
- ` surface: ${surfaceSummary(session)}`
1394
- ];
1395
- for (const block of ledger.slice(0, 10)) {
1396
- lines.push(` - ${block.blockId.slice(0, 8)}: seqs ${block.start}..${block.end} (${block.shadowedSeqs.length} msgs) \u2014 ${block.summary.slice(0, 80)}`);
1442
+ const state = env.store.stateFor(session);
1443
+ const surface = surfaceEventsOf(session);
1444
+ const toolNames = buildToolCallIndex(surface);
1445
+ const coreMessages = allLogMessages(session);
1446
+ const surfaceMessages = eventsToCoreMessages(surface, toolNames);
1447
+ const tokenCount = resolveTokenCount(agent, surfaceMessages);
1448
+ const config = kernelConfigFor(env);
1449
+ const turn = env.kernel.processTurn({ messages: coreMessages, state, config, tokenCount });
1450
+ const statusMessages = eventsToCoreMessages(
1451
+ surface.filter((event) => !isCheckpointEvent(event)),
1452
+ toolNames
1453
+ );
1454
+ const report = buildStatusReport(turn.state, statusMessages, defaultCountTokens3, args);
1455
+ const lines = [report];
1456
+ if (args.scope === void 0) {
1457
+ const nudge = turn.nudge;
1458
+ if (nudge !== void 0) {
1459
+ lines.push("", `Nudge: ${nudge.shouldInject ? "ACTIVE" : "idle"} \u2014 ${nudge.reason}`);
1460
+ }
1461
+ }
1462
+ lines.push("", `Surface: ${surfaceSummary(session)}`);
1463
+ if (args.scope === "uncompressed") {
1464
+ lines.push("", "Note: drilldown rows are kernel refs (mN) for size awareness \u2014 compress uses the Surface: seqs above, never mN.");
1397
1465
  }
1398
1466
  return { text: lines.join("\n") };
1399
1467
  }
@@ -1441,6 +1509,30 @@ function makeTools(env) {
1441
1509
 
1442
1510
  // src/commands.ts
1443
1511
  import { defaultCountTokens as defaultCountTokens4 } from "acp-kernel";
1512
+
1513
+ // src/window.ts
1514
+ var DEFAULT_CONTEXT_WINDOW = 128e3;
1515
+ function windowSourceLabel(window) {
1516
+ if (window.source === "explicit") return "configured";
1517
+ if (window.source === "auto") {
1518
+ return `auto-detected from ${window.provider ?? "?"}/${window.model ?? "?"}`;
1519
+ }
1520
+ return "default (auto-detection unavailable)";
1521
+ }
1522
+ async function detectContextWindow(agent, provider, model) {
1523
+ const llm = agent.ctx?.get?.("llm");
1524
+ if (llm?.resolveModelInfo === void 0) return null;
1525
+ try {
1526
+ const info = await llm.resolveModelInfo(provider, model);
1527
+ const window = info?.context?.contextWindow;
1528
+ if (typeof window === "number" && Number.isInteger(window) && window > 0) return window;
1529
+ return null;
1530
+ } catch {
1531
+ return null;
1532
+ }
1533
+ }
1534
+
1535
+ // src/commands.ts
1444
1536
  async function statusText(env, agent) {
1445
1537
  const session = agent.session;
1446
1538
  const ledger = rebuildBlockLedger(session.events);
@@ -1497,8 +1589,9 @@ function compressText(env, agent, args) {
1497
1589
  function decompressText(_env, agent, args) {
1498
1590
  if (args.length < 1) return "/acp decompress <blockId>";
1499
1591
  const session = agent.session;
1592
+ const blockId = blockIdOfKernelRef(session, args[0]);
1500
1593
  const ledger = rebuildBlockLedger(session.events);
1501
- const block = ledger.find((entry) => entry.blockId.startsWith(args[0]));
1594
+ const block = blockId === null ? ledger.find((entry) => entry.blockId.startsWith(args[0])) : ledger.find((entry) => entry.blockId === blockId);
1502
1595
  if (block === void 0) return `block "${args[0]}" not found (see /acp status)`;
1503
1596
  const parts = expandShadowedSeqs(session, block.blockId).map((seq) => extractEventText(session.events[seq])).filter((text) => text.length > 0);
1504
1597
  return `Block ${block.blockId} \u2014 ${block.summary}