billion-context-dsh 0.2.25 → 0.2.26

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.25**) 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.26**) 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 ⭐:
@@ -42,7 +42,7 @@ When conversations get long, the model runs out of context. Most tools hard-trun
42
42
  Unlike DSH's built-in auto-compaction (which replaces a range with an automatically generated summary), billion-context-dsh:
43
43
 
44
44
  - **Model-driven** — the model writes the summary itself; there is no second LLM summarization call
45
- - **Advisory, never imperative** — automatic policy only *nudges*; the model decides whether and when to compress
45
+ - **Advisory, never imperative** — automatic policy only *nudges*; the model decides whether and when to compress (one exception: a provider-confirmed context overflow lets the engine recover once by itself — see `maxOverflowRetries` in the configuration table)
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
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
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
@@ -100,7 +100,7 @@ This only installs the package into your project/global store; it does **not** t
100
100
  **Install from the git source (`github:` spec — the form the plugin store shows).** The prebuilt `dist/` artifacts are committed to this repository, so a git-source install also works out of the box — **no build step needed**, and pnpm 11's default build-script blocking (`allowBuilds`) never applies to this package:
101
101
 
102
102
  ```bash
103
- dsh plugin --profile web add github:Tyan66666/billion-context-dsh#v0.2.25
103
+ dsh plugin --profile web add github:Tyan66666/billion-context-dsh#v0.2.26
104
104
  ```
105
105
 
106
106
  Prefer a `#<tag>` ref to get artifacts identical to that npm release; without a ref you get the latest default-branch build. Only building the repo yourself (`npm run build`) requires approving build scripts. Background and trade-offs: [docs/git-source-install-design.md](docs/git-source-install-design.md) (issue #92).
@@ -167,7 +167,7 @@ compaction-acp:
167
167
  /acp-prune config reset all
168
168
  ```
169
169
 
170
- Changing a window key (`modelContextLimit` / `autoModelContextLimit`) clears the window-probe cache — the next pre-step re-probes under the new values (probe failures are cached too, so this is also how a fixed gateway gets re-probed). In provider-less plain-npm compositions `/acp-prune config` degrades to advice text; `settingsEnabled: false` disables the integration entirely (composition-row-only — the switch is deliberately NOT part of the settings layer: it cannot turn itself off). Design details: [docs/settings-integration-design.md](docs/settings-integration-design.md).
170
+ Changing a window key (`modelContextLimit` / `autoModelContextLimit`) clears the window-probe cache — the next pre-step re-probes under the new values (probe failures are cached too, so this is also how a fixed gateway gets re-probed). In provider-less plain-npm compositions `/acp-prune config` degrades to advice text; on DSH lines ≥0.1.7, where the host settings service no longer provides `installSection` (outside this plugin's declared peer range), the engine degrades cleanly too — one startup warning, the six keys stay adjustable through the composition-row `config:`, engine and tools unaffected (issue #173); `settingsEnabled: false` disables the integration entirely (composition-row-only — the switch is deliberately NOT part of the settings layer: it cannot turn itself off). Design details: [docs/settings-integration-design.md](docs/settings-integration-design.md).
171
171
 
172
172
  **Per-mode — an agent preset's `compaction` realm.** First *disable (or delete) the realm's existing `dsh-compaction-basic` row*, then mount this engine — two backends cannot coexist in the same realm:
173
173
 
@@ -251,6 +251,7 @@ This project reuses `acp-kernel`'s compression core and `billion-context-pi`'s d
251
251
  | `autoTools` | `true` | Register the four model tools on `ctx.tools` |
252
252
  | `autoCommand` | `true` | Register the `/acp-prune` command on `ctx.commands` |
253
253
  | `autoNudge` | `true` | Inject the nudge into `agent/pre-step` (runtime-adjustable: `/acp-prune config`) |
254
+ | `maxOverflowRetries` | `1` | Budget for automatic recovery from a provider-confirmed context overflow (`CONTEXT_WINDOW_EXCEEDED`): the engine hides the largest eligible range with a marker summary it writes itself (no model call — the request was just rejected, so there is no model turn to write one; every original stays in the session log and `search_context`/`decompress` find it again), then answers the host's `agent/request-error` so the request is retried. The budget counts per request and resets once the model makes progress (an assistant message lands) or the agent goes idle; `0` disables the automatic action (the error surfaces unchanged). This is ACP's ONLY automatic compaction action — the pressure side stays nudge-only, the model decides (composition-only, not in the settings layer; rationale and trade-offs in [docs/overflow-recovery-design.md](docs/overflow-recovery-design.md)) |
254
255
  | `settingsEnabled` | `true` (enabled when unset) | (optional) Disable the runtime-settings integration entirely (composition-row-only, deliberately NOT in the settings layer — the switch cannot turn itself off; with it off the composition-row `config:` stays the only effective channel) |
255
256
  | `prompts` | — | (optional) Custom prompt copy: per-slot overrides for nudge / range table / system prompt / tool descriptions (template + named placeholders, validated at construction; see “Custom prompt copy” above and [docs/configurable-prompts-design.md](docs/configurable-prompts-design.md)) |
256
257
 
@@ -267,6 +268,7 @@ If you do not want to tune three percentages by hand, pick the nudge aggressiven
267
268
  | `aggressive` | 0.30 | 0.50 | 0.70 | Lean context — compresses early and frequently |
268
269
 
269
270
  - **Fills only what you left unset**: `preset` fills ONLY the `nudge*ContextLimitPct` values you did not set explicitly; if you set both a `preset` and one of those thresholds, your explicit value wins (precedence: explicit > preset > default).
271
+ - **Presets reach the runtime settings layer too**: the threshold values a composed preset fills are registered as the settings layer's `base` — `/acp-prune config list` attributes them to `source: base`, and `/acp-prune config reset <key>` returns them to the **preset value**, not the engine default (a runtime reset restores what the composition chose). Without a preset, nothing changes.
270
272
  - **No other knob is touched**: `modelContextLimit`, `autoNudge`, `prompts`, and `coreOverrides` are unaffected; `coreOverrides.nudge` still lands last and its same-name keys outrank everything.
271
273
  - **See the active tier**: `/acp-prune status` prints the effective `preset` and the three thresholds **actually in force** — the line mirrors `kernelConfigFor`'s merge order, so any explicit override you made on top of the preset AND any same-name key in `coreOverrides.nudge` are shown as they really apply.
272
274
  - **A typo fails loudly**: an unknown name throws at engine construction and lists the valid tiers (the same fail-fast contract as custom prompt templates) — it never silently falls back to the defaults. The bundle row itself carries no `config`, so a `preset` can only come from your own same-id `compaction-acp` row; if that row fails to construct, the profile stays down until you fix it (intended fail-fast, not a defect).
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [中文](./README.md) | [English](./README.en.md)
4
4
 
5
5
  > **⚠️ 测试版声明——请勿用于生产环境**
6
- > 本项目(**v0.2.25**)仍处于开发中的测试版。[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 本身也处于**公开测试版**阶段。**请勿将两者用于工程化 / 生产环境**——预期会有破坏性变更与粗糙之处。
6
+ > 本项目(**v0.2.26**)仍处于开发中的测试版。[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 本身也处于**公开测试版**阶段。**请勿将两者用于工程化 / 生产环境**——预期会有破坏性变更与粗糙之处。
7
7
 
8
8
  <p align="center">
9
9
  <strong>衷心感谢以下项目——请给它们一个 ⭐:</strong>
@@ -42,7 +42,7 @@
42
42
  与 DSH 内置的自动压缩(用自动生成的摘要替换一段范围)不同,billion-context-dsh:
43
43
 
44
44
  - **模型驱动** —— 摘要由模型自己书写,没有第二次 LLM 摘要调用
45
- - **只建议、不强令** —— 自动策略只 *nudge*(提醒),是否压缩、何时压缩由模型决定
45
+ - **只建议、不强令** —— 自动策略只 *nudge*(提醒),是否压缩、何时压缩由模型决定(唯一的例外:provider 确认的上下文超窗会让引擎自己抢救一次——见配置表的 `maxOverflowRetries`)
46
46
  - **持久且可恢复** —— 压缩范围成为 checkpoint 节点,原文保留在 append-only 会话日志中;`decompress` 可恢复,`search_context` 可在块内查找
47
47
  - **长任务稳得住** —— 每一步都接着前面的成果走,关键结论持续可用、不断叠加,超长任务更容易跑完
48
48
  - **上下文始终精简** —— 每次请求都只用少量、精炼的上下文,只保留关键信息;不做大段统一压缩,细节不随之衰失,token 消耗自然更低
@@ -99,7 +99,7 @@ npm install billion-context-dsh
99
99
  **git 源安装(`github:` 规格,插件商店展示的形态)。** 预构建产物 `dist/` 已提交到仓库,从 git 源安装同样开箱即用——**无需任何构建步骤**,pnpm 11 默认拦截构建脚本(`allowBuilds`)的机制对这个包不构成障碍:
100
100
 
101
101
  ```bash
102
- dsh plugin --profile web add github:Tyan66666/billion-context-dsh#v0.2.25
102
+ dsh plugin --profile web add github:Tyan66666/billion-context-dsh#v0.2.26
103
103
  ```
104
104
 
105
105
  建议带 `#<tag>` 安装,拿到与对应 npm 版本完全一致的产物;不带 ref 则装默认分支的最新构建。只有 clone 仓库自行从源码构建(`npm run build`)才需要放行构建。背景与方案取舍见 [docs/git-source-install-design.md](docs/git-source-install-design.md)(issue #92)。
@@ -164,7 +164,7 @@ compaction-acp:
164
164
  /acp-prune config reset all
165
165
  ```
166
166
 
167
- 窗口相关键(`modelContextLimit` / `autoModelContextLimit`)改动会清空窗口探测缓存——下一次 pre-step 按新值重新探测(探测失败也会被缓存,正是靠这个机制在修复网关后重新探测)。无 settings provider 的纯 npm 安装组合下 `/acp-prune config` 降级为指引文案;`settingsEnabled: false` 可整体关闭该集成(组合行专用,不进 settings 层——开关不能关掉自己)。设计细节见 [docs/settings-integration-design.md](docs/settings-integration-design.md)。
167
+ 窗口相关键(`modelContextLimit` / `autoModelContextLimit`)改动会清空窗口探测缓存——下一次 pre-step 按新值重新探测(探测失败也会被缓存,正是靠这个机制在修复网关后重新探测)。无 settings provider 的纯 npm 安装组合下 `/acp-prune config` 降级为指引文案;DSH 0.1.7+ 宿主线(settings 服务已移除 `installSection`,在本插件声明的 peer 区间之外)同样干净降级:启动时记一条 warn,六个键改由组合行 `config:` 调整,引擎与工具不受影响(issue #173);`settingsEnabled: false` 可整体关闭该集成(组合行专用,不进 settings 层——开关不能关掉自己)。设计细节见 [docs/settings-integration-design.md](docs/settings-integration-design.md)。
168
168
 
169
169
  **单模式生效(agent preset 的 `compaction` realm)**。先在该 realm 内*禁用(或删除)原有的 `dsh-compaction-basic` 行*,再插入本引擎——同一 realm 内两个后端不能并存:
170
170
 
@@ -248,6 +248,7 @@ DSH 的每个模型请求都派生自其 append-only 会话日志(*surface*)
248
248
  | `autoTools` | `true` | 在 `ctx.tools` 注册四个模型工具 |
249
249
  | `autoCommand` | `true` | 在 `ctx.commands` 注册 `/acp-prune` 命令 |
250
250
  | `autoNudge` | `true` | 当内核建议时向 `agent/pre-step` 注入 nudge(运行时热调:`/acp-prune config`) |
251
+ | `maxOverflowRetries` | `1` | 宿主确认的上下文超窗(`CONTEXT_WINDOW_EXCEEDED`)自动抢救预算:超窗时引擎挑一段**最大且合规**的范围做一次紧急压缩(摘要是引擎自己写的标记,不调模型——请求刚被拒,本就没有模型回合可写摘要;原文都留在日志里,`search_context`/`decompress` 找得回来),然后应答宿主的 `agent/request-error` 让该请求重试。预算按请求计数,模型有进展(落一条 assistant 消息)或 agent 回到 idle 即重置;`0` 关闭这个自动动作(错误原样抛给用户)。这是 ACP 唯一的自动压缩动作——压力侧始终只 nudge、由模型决定(组合行专用,不进 settings 层;原理与取舍见 [docs/overflow-recovery-design.md](docs/overflow-recovery-design.md)) |
251
252
  | `settingsEnabled` | `true`(未配置即启用) | (可选)整体关闭运行时设置集成(组合行专用,不进 settings 层——开关不能关掉自己;关闭后组合行 `config:` 仍是唯一生效通道) |
252
253
  | `prompts` | — | (可选)自定义提示词文案:nudge / 范围表 / system prompt / 工具描述按槽位覆盖(模板 + 命名占位符,构造期校验;见上文「自定义提示词文案」与 [docs/configurable-prompts-design.md](docs/configurable-prompts-design.md)) |
253
254
 
@@ -264,6 +265,7 @@ DSH 的每个模型请求都派生自其 append-only 会话日志(*surface*)
264
265
  | `aggressive` | 0.30 | 0.50 | 0.70 | 精简上下文,更早更频繁地压缩 |
265
266
 
266
267
  - **只填未设的阈值**:`preset` 仅填充你没有显式设置的 `nudge*ContextLimitPct`;同时写了 `preset` 和某个阈值时,该阈值以你的显式值为准(优先级 显式 > preset > 默认)。
268
+ - **preset 同样落到运行时设置层**:组合 preset 填出的三个阈值注册为 settings 层的 `base` 层——`/acp-prune config list` 把它们归因为 `source: base`,`/acp-prune config reset <key>` 退回的是 **preset 值**而非引擎默认(运行时 reset 恢复的是组合的选择);未配 preset 时行为完全不变。
267
269
  - **不碰其他旋钮**:`modelContextLimit`、`autoNudge`、`prompts`、`coreOverrides` 完全不受影响;`coreOverrides.nudge` 仍最后落地、同名键最高优先。
268
270
  - **查看当前档位**:`/acp-prune status` 会打印生效的 `preset` 及其**真实生效的**三个阈值——这一行镜像 `kernelConfigFor` 的合并顺序,所以你在其上做的显式覆盖、以及 `coreOverrides.nudge` 里的同名键都会如实显示。
269
271
  - **拼错即报错**:未知名称在引擎构造期直接抛错并列出合法值(与自定义提示词模板同一约定),不会静默回退默认。注意 bundle 行本身不带 `config`,`preset` 只能由你自己的同 id `compaction-acp` 行提供;该行构造失败即挂载失败,profile 会在你修好配置前一直起不来(fail-fast 的既定行为)。
package/dist/index.d.ts CHANGED
@@ -115,6 +115,15 @@ export interface AcpConfig {
115
115
  readonly autoCommand: boolean;
116
116
  /** Inject the nudge into `agent/pre-step` when the kernel recommends it. Default true. */
117
117
  readonly autoNudge: boolean;
118
+ /**
119
+ * How many consecutive provider-confirmed context-overflow failures one
120
+ * agent may answer with an emergency compaction + retry before the original
121
+ * error is preserved (mirrors compaction-basic's `maxOverflowRetries`;
122
+ * default 1, the host's own default). The budget resets when the agent
123
+ * makes progress (an assistant message lands) or returns to idle, so a
124
+ * request that cannot be repaired cannot retry forever.
125
+ */
126
+ readonly maxOverflowRetries?: number;
118
127
  /**
119
128
  * Escape hatch: disable the runtime-settings integration entirely
120
129
  * (composition-layer ONLY — deliberately not exposed through the settings
@@ -163,6 +172,10 @@ export declare class AcpCompactionEngine extends CompactionEngine {
163
172
  readonly settingsCommand: SettingsCommandSurface;
164
173
  /** Per route the adapter's per-request output cap (the output reservation); null = undisclosed. */
165
174
  private readonly outputReservationCache;
175
+ /** Per-agent context-overflow recovery budget (mirrors the host's `overflowRetries`). */
176
+ private readonly overflowRetries;
177
+ /** Per-session overflow agents, so session progress can reset the recovery budget. */
178
+ private readonly overflowSessions;
166
179
  constructor(ctx: Context, config?: Partial<AcpConfig>);
167
180
  /**
168
181
  * Resolve the effective context window for an agent. An explicitly
@@ -208,6 +221,18 @@ export declare class AcpCompactionEngine extends CompactionEngine {
208
221
  * (degenerate config) — the raw-window behavior is preserved.
209
222
  */
210
223
  private applyReservation;
224
+ /**
225
+ * Best-effort emergency compaction for one provider-confirmed context
226
+ * overflow: pick the largest eligible (guarded, tool-pairing-balanced)
227
+ * surface range and land the normal durable transaction with a fixed
228
+ * engine-written marker summary. No LLM call — the provider just rejected
229
+ * the request for being too large, so there is no model turn available to
230
+ * write a summary; the originals stay in the append-only log, so
231
+ * search_context still indexes them, decompress restores them, and the
232
+ * model can re-run the compress tool over the marker later to write a real
233
+ * summary. Returns null when nothing eligible exists (nothing to reclaim).
234
+ */
235
+ private compactForOverflow;
211
236
  /** ACP is model-driven: automatic pressure policy never summarizes by itself. */
212
237
  compactIfNeeded(_agent: CompactionAgentContext, _trigger: CompactionTrigger, signal: AbortSignal): Promise<CompactionResult | null>;
213
238
  /** Explicit idle-session compaction: ACP leaves the decision to the model. */
package/dist/index.js CHANGED
@@ -3363,6 +3363,9 @@ function makePreview(text, query, len) {
3363
3363
  return prefix + text.slice(start, end).trim() + suffix;
3364
3364
  }
3365
3365
 
3366
+ // src/index.ts
3367
+ import { CONTEXT_WINDOW_EXCEEDED_CODE } from "@deepseek-ai/dsh-llm";
3368
+
3366
3369
  // src/lru.ts
3367
3370
  var DEFAULT_SESSION_CACHE_LIMIT = 512;
3368
3371
  var LruMap = class extends Map {
@@ -3478,9 +3481,18 @@ function buildToolCallIndex(events) {
3478
3481
  }
3479
3482
  var SUMMARY_FRAME_PREFIX = "[Model-written summary \u2014 not user words; re-verify any obligations before relying on them]";
3480
3483
  function withSummaryFramePrefix(text) {
3481
- return text.startsWith(SUMMARY_FRAME_PREFIX) ? text : `${SUMMARY_FRAME_PREFIX}
3484
+ if (text.startsWith(SUMMARY_FRAME_PREFIX)) return text;
3485
+ if (isEngineWrittenSummary(text)) return text;
3486
+ return `${SUMMARY_FRAME_PREFIX}
3482
3487
  ${text}`;
3483
3488
  }
3489
+ var ENGINE_SUMMARY_LEAD = "[engine-written summary \u2014 context-overflow emergency compaction";
3490
+ function isEngineWrittenSummary(text) {
3491
+ return text.startsWith(ENGINE_SUMMARY_LEAD);
3492
+ }
3493
+ function overflowMarkerSummary(hiddenCount) {
3494
+ return `${ENGINE_SUMMARY_LEAD}: ${hiddenCount} surface message(s) hidden because the provider rejected the request as exceeding the context window. The originals are intact in the session log \u2014 use search_context or decompress (see acp_status) to read them, or re-run the compress tool over this range to write a proper summary.]`;
3495
+ }
3484
3496
  function projectEvent(event, toolNames) {
3485
3497
  switch (event.type) {
3486
3498
  case "user/message": {
@@ -3606,7 +3618,12 @@ function contentBlocksOfEvent(event) {
3606
3618
  function isCheckpointNode(event) {
3607
3619
  if (event.type !== "user/message") return false;
3608
3620
  const source = event.data.source;
3609
- return source?.plugin === "compact";
3621
+ return source?.plugin === "compact" || source?.kind === "compact-checkpoint";
3622
+ }
3623
+ function checkpointCompactionIdOf(event) {
3624
+ if (!isCheckpointNode(event)) return null;
3625
+ const source = event.data.source;
3626
+ return typeof source?.compactionId === "string" ? source.compactionId : null;
3610
3627
  }
3611
3628
  var METADATA_PLUGINS = /* @__PURE__ */ new Set([
3612
3629
  "acp-nudge",
@@ -3619,17 +3636,44 @@ var REAL_CONTENT_PLUGINS = /* @__PURE__ */ new Set([
3619
3636
  "user-approval",
3620
3637
  "tools-ptc"
3621
3638
  ]);
3639
+ var REAL_CONTENT_KINDS = /* @__PURE__ */ new Set([
3640
+ "runtime-context",
3641
+ // dynamic-context snapshot (was '@deepseek-ai/dsh-system-prompt')
3642
+ "ptc-mode"
3643
+ // deferred tool context (was 'tools-ptc' / 'tools-code-mode')
3644
+ ]);
3645
+ var AUDITED_RELAY_KINDS = /* @__PURE__ */ new Set([
3646
+ "subagent-report",
3647
+ "subagent-settled"
3648
+ ]);
3622
3649
  var HOST_INSTRUCTION_KINDS = /* @__PURE__ */ new Set([
3623
3650
  "agent-instructions",
3624
3651
  // AGENTS.md injection (hook shape: {kind:'agent-instructions', form:'instructions'})
3625
- "skill-catalog"
3652
+ "skill-catalog",
3626
3653
  // skill catalog (form:'catalog')
3654
+ // Host compaction summary row (DSH >= 0.1.7; was plugin 'dsh-compaction-basic').
3655
+ // That legacy name was never whitelisted, so its rows were barriers already —
3656
+ // the renamed spelling keeps exactly that treatment instead of silently
3657
+ // becoming foldable content (issue #169).
3658
+ "compact-basic"
3627
3659
  ]);
3660
+ function sourcePluginOf(source) {
3661
+ if (source === void 0 || typeof source !== "object") return void 0;
3662
+ const kind = source.kind;
3663
+ if (kind === "plugin") {
3664
+ return typeof source.plugin === "string" && source.plugin.length > 0 ? source.plugin : void 0;
3665
+ }
3666
+ if (typeof kind === "string" && kind.startsWith("plugin:")) {
3667
+ const name = kind.slice("plugin:".length);
3668
+ return name.length > 0 ? name : void 0;
3669
+ }
3670
+ return void 0;
3671
+ }
3628
3672
  function isAgentInstructionsRow(event) {
3629
3673
  if (event.type !== "user/message") return false;
3630
3674
  const source = event.data.source;
3631
3675
  if (!source) return false;
3632
- return source.kind === "agent-instructions" || source.kind === "plugin" && source.plugin === "agent-instructions";
3676
+ return source.kind === "agent-instructions" || sourcePluginOf(source) === "agent-instructions";
3633
3677
  }
3634
3678
  function classifySurfaceEvent(event) {
3635
3679
  if (isCheckpointNode(event)) return "checkpoint";
@@ -3638,20 +3682,26 @@ function classifySurfaceEvent(event) {
3638
3682
  if (!source) return "real";
3639
3683
  const kind = source.kind;
3640
3684
  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";
3685
+ const plugin = sourcePluginOf(source);
3686
+ if (kind === "plugin" || plugin !== void 0) {
3687
+ if (plugin !== void 0 && METADATA_PLUGINS.has(plugin)) return "metadata";
3688
+ if (plugin !== void 0 && REAL_CONTENT_PLUGINS.has(plugin)) return "real";
3644
3689
  return "instruction";
3645
3690
  }
3646
- if (kind !== void 0 && HOST_INSTRUCTION_KINDS.has(kind)) return "instruction";
3647
- return "real";
3691
+ if (typeof kind !== "string") return "real";
3692
+ if (HOST_INSTRUCTION_KINDS.has(kind)) return "instruction";
3693
+ if (REAL_CONTENT_KINDS.has(kind) || AUDITED_RELAY_KINDS.has(kind)) return "real";
3694
+ return "instruction";
3648
3695
  }
3649
3696
  function isRealUserTurn(event) {
3650
3697
  if (event.type !== "user/message") return false;
3651
3698
  if (classifySurfaceEvent(event) !== "real") return false;
3652
3699
  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";
3700
+ const plugin = sourcePluginOf(source);
3701
+ if (plugin !== void 0 && REAL_CONTENT_PLUGINS.has(plugin)) return false;
3702
+ const kind = source?.kind;
3703
+ if (typeof kind === "string" && REAL_CONTENT_KINDS.has(kind)) return false;
3704
+ return kind !== "subagent-report" && kind !== "subagent-settled";
3655
3705
  }
3656
3706
 
3657
3707
  // src/host-tokens.ts
@@ -4036,9 +4086,8 @@ function summarySeqIndex(events) {
4036
4086
  const index = /* @__PURE__ */ new Map();
4037
4087
  for (const event of events) {
4038
4088
  if (event.type !== "user/message") continue;
4039
- const source = event.data.source;
4040
- const compactionId = source?.plugin === "compact" ? source.compactionId : void 0;
4041
- if (compactionId !== void 0 && !index.has(compactionId)) index.set(compactionId, event.seq);
4089
+ const compactionId = checkpointCompactionIdOf(event);
4090
+ if (compactionId !== null && !index.has(compactionId)) index.set(compactionId, event.seq);
4042
4091
  }
4043
4092
  return index;
4044
4093
  }
@@ -4128,7 +4177,10 @@ function hideSurfaceSeqs(session, seqs, text, priceEvent = hostPriceEvent) {
4128
4177
  const body = text !== void 0 && text.trim().length > 0 ? text : PRUNE_NOTE;
4129
4178
  session.append("user/message", createUserMessage({
4130
4179
  content: [{ type: "text", text: body }],
4131
- source: { kind: "plugin", plugin: "billion-context-dsh" }
4180
+ // V4 producer kind (issue #163): DSH ≥0.1.7's V4 admission rejects the
4181
+ // legacy wrapper `{ kind: 'plugin', plugin: … }`; `plugin:<name>` is what
4182
+ // the host's own V3→V4 migration emits and is accepted by 0.1.5 too.
4183
+ source: { kind: "plugin:billion-context-dsh" }
4132
4184
  }), {
4133
4185
  surfaceOp: { op: "replace", startSeq: start, endSeq: end },
4134
4186
  sourceEventSeqs: [...seqs]
@@ -4436,10 +4488,10 @@ function blockRegistry(session) {
4436
4488
  }
4437
4489
  function blockRefForSummarySeq(session, seq) {
4438
4490
  const event = eventAtOf(session, seq);
4439
- if (event?.type !== "user/message") return null;
4440
- const source = event.data.source;
4441
- if (source?.plugin !== "compact" || source.compactionId === void 0) return null;
4442
- const entry = blockRegistry(session).find((r) => r.blockId === source.compactionId);
4491
+ if (event === void 0) return null;
4492
+ const compactionId = checkpointCompactionIdOf(event);
4493
+ if (compactionId === null) return null;
4494
+ const entry = blockRegistry(session).find((r) => r.blockId === compactionId);
4443
4495
  if (entry === void 0) return null;
4444
4496
  return entry.kernelBlockId;
4445
4497
  }
@@ -4459,10 +4511,8 @@ function summarySeqOfKernelBlock(session, kernelBlockId) {
4459
4511
  }
4460
4512
  function checkpointBlockIdOf(events, seq) {
4461
4513
  const event = events[seq];
4462
- if (event?.type !== "user/message") return null;
4463
- const source = event.data.source;
4464
- if (source?.plugin !== "compact" || source.compactionId === void 0) return null;
4465
- return source.compactionId;
4514
+ if (event === void 0) return null;
4515
+ return checkpointCompactionIdOf(event);
4466
4516
  }
4467
4517
  function expandShadowedSeqs(session, blockId) {
4468
4518
  const ledger = rebuildBlockLedger(sessionEventsOf(session));
@@ -4902,7 +4952,13 @@ function buildNudge(agent, env, lastNudgeTurn, emergencyNudges, onEmergencyCapHi
4902
4952
  );
4903
4953
  const message = createUserMessage2({
4904
4954
  content: [{ type: "text", text }],
4905
- source: { kind: "plugin", plugin: "acp-nudge" }
4955
+ // V4 producer kind (issue #163): DSH ≥0.1.7's V4 admission rejects the
4956
+ // legacy wrapper shape `{ kind: 'plugin', plugin: … }` outright (a wedged
4957
+ // batch fails the NEXT turn with "format v4 message requires a
4958
+ // producer-owned source kind"). `plugin:<name>` is exactly what the host's
4959
+ // own V3→V4 migration emits for unregistered plugins, and DSH 0.1.5
4960
+ // sessions accept it too (probe-verified), so no version gate is needed.
4961
+ source: { kind: "plugin:acp-nudge" }
4906
4962
  });
4907
4963
  return { message, emergency };
4908
4964
  }
@@ -6107,6 +6163,9 @@ var DEFAULT_CONFIG = {
6107
6163
  autoTools: true,
6108
6164
  autoCommand: true,
6109
6165
  autoNudge: true,
6166
+ // Same default as the host's compaction-basic policy: one owned retry per
6167
+ // unrelieved overflow, then the original error is preserved.
6168
+ maxOverflowRetries: 1,
6110
6169
  // Nudge thresholds: engine defaults 0.70/0.85 — deliberately below the
6111
6170
  // kernel/billion-context-pi 0.75/0.95. 0.95 leaves no room to act before
6112
6171
  // the API rejects, and the host's compaction-basic line (thresholdRatio
@@ -6120,7 +6179,11 @@ var DEFAULT_CONFIG = {
6120
6179
  function resolveAcpConfig(config = {}) {
6121
6180
  const resolved = resolvePresetThresholds({ ...DEFAULT_CONFIG, ...config }, config);
6122
6181
  assertNudgeThresholdOrder(resolved);
6123
- return resolved;
6182
+ const maxOverflowRetries = resolved.maxOverflowRetries ?? 1;
6183
+ if (!Number.isInteger(maxOverflowRetries) || maxOverflowRetries < 0) {
6184
+ throw new Error(`maxOverflowRetries must be a non-negative integer (got ${maxOverflowRetries})`);
6185
+ }
6186
+ return { ...resolved, maxOverflowRetries };
6124
6187
  }
6125
6188
  function resolvePresetThresholds(base, config) {
6126
6189
  if (base.preset === void 0) return base;
@@ -6132,6 +6195,17 @@ function resolvePresetThresholds(base, config) {
6132
6195
  nudgeEmergencyThresholdPct: config.nudgeEmergencyThresholdPct ?? preset.nudgeEmergencyThresholdPct
6133
6196
  };
6134
6197
  }
6198
+ function presetFilledSettingsEntry(config) {
6199
+ const entry = filterSettingsEntry(config);
6200
+ if (config.preset === void 0) return entry;
6201
+ const preset = resolvePreset(config.preset);
6202
+ return {
6203
+ ...entry,
6204
+ nudgeMinContextLimitPct: config.nudgeMinContextLimitPct ?? preset.nudgeMinContextLimitPct,
6205
+ nudgeMaxContextLimitPct: config.nudgeMaxContextLimitPct ?? preset.nudgeMaxContextLimitPct,
6206
+ nudgeEmergencyThresholdPct: config.nudgeEmergencyThresholdPct ?? preset.nudgeEmergencyThresholdPct
6207
+ };
6208
+ }
6135
6209
  function assertNudgeThresholdOrder(config) {
6136
6210
  const { nudgeMinContextLimitPct: min, nudgeMaxContextLimitPct: max, nudgeEmergencyThresholdPct: emergency } = config;
6137
6211
  const describe = `min ${min ?? "kernel default"} / max ${max ?? "kernel default"} / emergency ${emergency ?? "kernel default"}`;
@@ -6177,6 +6251,10 @@ var AcpCompactionEngine = class extends CompactionEngine {
6177
6251
  settingsCommand;
6178
6252
  /** Per route the adapter's per-request output cap (the output reservation); null = undisclosed. */
6179
6253
  outputReservationCache = /* @__PURE__ */ new Map();
6254
+ /** Per-agent context-overflow recovery budget (mirrors the host's `overflowRetries`). */
6255
+ overflowRetries = /* @__PURE__ */ new Map();
6256
+ /** Per-session overflow agents, so session progress can reset the recovery budget. */
6257
+ overflowSessions = /* @__PURE__ */ new Map();
6180
6258
  constructor(ctx, config = {}) {
6181
6259
  super(ctx);
6182
6260
  this.config = resolveAcpConfig(config);
@@ -6185,7 +6263,7 @@ var AcpCompactionEngine = class extends CompactionEngine {
6185
6263
  this.kernel = createCore(ports);
6186
6264
  setDocCacheCap(128 * 1024 * 1024);
6187
6265
  this.store = new AcpStateStore();
6188
- const compositionEntry = filterSettingsEntry(config);
6266
+ const compositionEntry = presetFilledSettingsEntry(config);
6189
6267
  let current = resolveAcpSettings(compositionEntry);
6190
6268
  this.readSettingsSource = () => current;
6191
6269
  const engine = this;
@@ -6202,6 +6280,12 @@ var AcpCompactionEngine = class extends CompactionEngine {
6202
6280
  this.settingsCommand = makeSettingsCommandSurface(() => this.settingsService, () => current);
6203
6281
  if (this.config.settingsEnabled !== false) {
6204
6282
  ctx.inject(["settings"], (settingsCtx) => {
6283
+ if (typeof settingsCtx.settings?.installSection !== "function") {
6284
+ this.ctx.logger.warn(
6285
+ "billion-context-dsh: host settings service has no installSection (removed in dsh-settings >= 0.1.7) \u2014 the compaction-acp settings section is not registered; the six knobs keep their composition values and /acp-prune config reports the section unavailable"
6286
+ );
6287
+ return void 0;
6288
+ }
6205
6289
  settingsCtx.settings.installSection(ctx, ACP_SETTINGS_NAMESPACE, AcpSettingsSchema, compositionEntry, {
6206
6290
  // The seam's source type follows the entry it registered, so `source`
6207
6291
  // is a partial view of the settings; re-resolve it into a
@@ -6282,6 +6366,13 @@ var AcpCompactionEngine = class extends CompactionEngine {
6282
6366
  });
6283
6367
  }
6284
6368
  ctx.on("session/event", (session, event) => {
6369
+ if (event.type === "assistant/message") {
6370
+ const overflowAgent = this.overflowSessions.get(session);
6371
+ if (overflowAgent !== void 0) {
6372
+ this.overflowRetries.delete(overflowAgent);
6373
+ this.overflowSessions.delete(session);
6374
+ }
6375
+ }
6285
6376
  if (event.type !== "tool/result") return;
6286
6377
  const message = event.data.message;
6287
6378
  const block = message.content[0];
@@ -6312,6 +6403,49 @@ var AcpCompactionEngine = class extends CompactionEngine {
6312
6403
  if (outcome === null) return decision;
6313
6404
  return { kind: "enter", messages: [...decision.messages, outcome.message] };
6314
6405
  });
6406
+ ctx.on("agent/status", ({ agent, status }) => {
6407
+ if (status !== "idle") return;
6408
+ this.overflowRetries.delete(agent);
6409
+ this.overflowSessions.delete(agent.session);
6410
+ });
6411
+ ctx.on("agent/request-error", async ({ agent, failure, signal }, next) => {
6412
+ if (failure.code !== CONTEXT_WINDOW_EXCEEDED_CODE || signal.aborted) return next();
6413
+ this.overflowSessions.set(agent.session, agent);
6414
+ const retries = this.overflowRetries.get(agent) ?? 0;
6415
+ const max = this.config.maxOverflowRetries ?? 1;
6416
+ if (retries >= max) {
6417
+ this.ctx.logger.warn(
6418
+ `billion-context-dsh: context-overflow recovery budget spent (${max} retries) for session ${agent.session.id}; preserving the original request error`
6419
+ );
6420
+ return next();
6421
+ }
6422
+ const generation = agent.session.surface.replaceGeneration;
6423
+ let result;
6424
+ try {
6425
+ result = await this.compactForOverflow(agent, signal);
6426
+ } catch (error) {
6427
+ const message = error instanceof Error ? error.message : String(error);
6428
+ if (!signal.aborted && agent.session.surface.replaceGeneration > generation) {
6429
+ this.ctx.logger.warn(
6430
+ `billion-context-dsh: context-overflow compaction failed after durable surface progress: ${message}; retrying from the replacement surface`
6431
+ );
6432
+ this.overflowRetries.set(agent, retries + 1);
6433
+ return { kind: "retry" };
6434
+ }
6435
+ this.ctx.logger.warn(
6436
+ `billion-context-dsh: context-overflow compaction failed: ${message}; ${signal.aborted ? "cancellation prevents retry" : "preserving the original request error"}`
6437
+ );
6438
+ return next();
6439
+ }
6440
+ if (signal.aborted || agent.session.surface.replaceGeneration <= generation) return next();
6441
+ if (result !== null) {
6442
+ this.ctx.logger.info(
6443
+ `compaction (context overflow recovery): shadowed ${result.shadowedSeqs.length} surface nodes (seqs ${result.start}-${result.end}, ~${result.shadowedTokenCount} tokens)`
6444
+ );
6445
+ }
6446
+ this.overflowRetries.set(agent, retries + 1);
6447
+ return { kind: "retry" };
6448
+ });
6315
6449
  const systemPrompt = ctx.get("systemPrompt");
6316
6450
  if (systemPrompt !== void 0) {
6317
6451
  systemPrompt.section({
@@ -6435,6 +6569,81 @@ var AcpCompactionEngine = class extends CompactionEngine {
6435
6569
  if (cap === null || cap >= window.limit) return window;
6436
6570
  return { ...window, rawLimit: window.limit, outputReserved: cap, limit: window.limit - cap };
6437
6571
  }
6572
+ /**
6573
+ * Best-effort emergency compaction for one provider-confirmed context
6574
+ * overflow: pick the largest eligible (guarded, tool-pairing-balanced)
6575
+ * surface range and land the normal durable transaction with a fixed
6576
+ * engine-written marker summary. No LLM call — the provider just rejected
6577
+ * the request for being too large, so there is no model turn available to
6578
+ * write a summary; the originals stay in the append-only log, so
6579
+ * search_context still indexes them, decompress restores them, and the
6580
+ * model can re-run the compress tool over the marker later to write a real
6581
+ * summary. Returns null when nothing eligible exists (nothing to reclaim).
6582
+ */
6583
+ async compactForOverflow(agent, signal) {
6584
+ signal.throwIfAborted();
6585
+ const session = agent.session;
6586
+ const state = this.store.stateFor(session);
6587
+ const coreMessages = allLogMessages(session);
6588
+ const surfaceMessages = eventsToCoreMessages(surfaceEventsOf(session));
6589
+ const tokenCount = resolveTokenCount(agent, surfaceMessages);
6590
+ const window = await this.windowFor(agent);
6591
+ const config = kernelConfigFor({ ...this.env, modelContextLimit: window.limit });
6592
+ const turn = this.kernel.processTurn({ messages: coreMessages, state, config, tokenCount, renderTags: "none" });
6593
+ this.store.set(session, turn.state);
6594
+ const byRaw = turn.state.messageRefs.byRaw;
6595
+ let firstSeq;
6596
+ let lastSeq;
6597
+ for (const seq of session.surface.nodes) {
6598
+ if (byRaw[String(seq)] === void 0) continue;
6599
+ firstSeq ??= seq;
6600
+ lastSeq = seq;
6601
+ }
6602
+ if (firstSeq === void 0 || lastSeq === void 0) return null;
6603
+ const view = {
6604
+ ranges: [{ startRef: byRaw[String(firstSeq)], endRef: byRaw[String(lastSeq)] }],
6605
+ refs: { byRef: turn.state.messageRefs.byRef }
6606
+ };
6607
+ const ranges = buildCompressibleSeqRanges(session, view, {
6608
+ preserveRecent: 5,
6609
+ mediaPriceOf: meterMediaPriceResolver(agent, session)
6610
+ });
6611
+ if (ranges.length === 0) return null;
6612
+ const best = ranges.reduce((largest, range) => range.tokens > largest.tokens ? range : largest);
6613
+ const startRef = byRaw[String(best.start)];
6614
+ const endRef = byRaw[String(best.end)];
6615
+ if (startRef === void 0 || endRef === void 0) return null;
6616
+ const summary = overflowMarkerSummary(best.count);
6617
+ const applied = this.kernel.applyCompression({
6618
+ ranges: [{ startRef, endRef, summary, topic: "context-overflow recovery" }],
6619
+ messages: coreMessages,
6620
+ state: turn.state,
6621
+ config
6622
+ });
6623
+ if (applied.result.blocksCreated === 0) return null;
6624
+ this.store.set(session, applied.state);
6625
+ const previousIds = new Set(turn.state.blocks.map((block) => block.blockId));
6626
+ const created = applied.state.blocks.find((block) => !previousIds.has(block.blockId));
6627
+ if (created === void 0) return null;
6628
+ const shadowed = shadowedSeqsOf(session, best.start, best.end);
6629
+ if (shadowed.length === 0) return null;
6630
+ const shadowedTokenCount = shadowedTokensViaMeter(session, shadowed, agent.ctx);
6631
+ const { provider, model } = routeFor(agent);
6632
+ runCompactionTransaction(session, {
6633
+ start: best.start,
6634
+ end: best.end,
6635
+ shadowedSeqs: shadowed,
6636
+ summary: [{ type: "text", text: summary }],
6637
+ shadowedTokenCount,
6638
+ provider,
6639
+ model,
6640
+ topic: "context-overflow recovery",
6641
+ kernelBlockId: created.blockId,
6642
+ directMessageIds: created.directMessageIds,
6643
+ effectiveMessageIds: created.effectiveMessageIds
6644
+ });
6645
+ return { start: best.start, end: best.end, shadowedSeqs: shadowed, shadowedTokenCount };
6646
+ }
6438
6647
  /** ACP is model-driven: automatic pressure policy never summarizes by itself. */
6439
6648
  async compactIfNeeded(_agent, _trigger, signal) {
6440
6649
  signal.throwIfAborted();