billion-context-dsh 0.2.18 → 0.2.20

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.18**) 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.20**) 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 ⭐:
@@ -98,7 +98,7 @@ This only installs the package into your project/global store; it does **not** t
98
98
  **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:
99
99
 
100
100
  ```bash
101
- dsh plugin --profile web add github:Tyan66666/billion-context-dsh#v0.2.18
101
+ dsh plugin --profile web add github:Tyan66666/billion-context-dsh#v0.2.20
102
102
  ```
103
103
 
104
104
  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).
@@ -216,8 +216,8 @@ This project reuses `acp-kernel`'s compression core and `billion-context-pi`'s d
216
216
 
217
217
  | Key | Default | Meaning |
218
218
  |---|---|---|
219
- | `modelContextLimit` | auto-detected (fallback `128000`) | Context window used for the kernel's pressure decisions; an explicit value wins and skips detection. When omitted, the host session projection `contextPressure.contextWindow` is read first — the capacity disclosed for the **current real route** (a session that switched models follows automatically, no restart needed) — and the model API is probed only when the projection discloses no window |
220
- | `autoModelContextLimit` | `true` | Resolve the real context window automatically: the host projection first (`windowFor` → `projectedContextWindow`, `src/window.ts`), then the model API probe (`agent.ctx.llm.resolveModelInfo`); both are skipped when `autoModelContextLimit: false`. On probe failure it falls back to the default, and the `/acp` command shows the window source (the `acp_status` model tool carries no window info). A failed probe is surfaced in the host log and the `/acp` panel (`restart to re-probe`) — the failure is cached like a success, so fixing the gateway requires a restart or an explicit `modelContextLimit` before the probe retries |
219
+ | `modelContextLimit` | auto-detected (fallback `128000`) | Context window used for the kernel's pressure decisions; an explicit value wins and skips detection. When omitted, the host session projection `contextPressure.contextWindow` is read first — the capacity disclosed for the **current real route** (a session that switched models follows automatically, no restart needed) — and the model API is probed only when the projection discloses no window; an explicit value also skips the output-reservation subtraction (the operator owns the denominator) |
220
+ | `autoModelContextLimit` | `true` | Resolve the real context window automatically: the host projection first (`windowFor` → `projectedContextWindow`, `src/window.ts`), then the model API probe (`agent.ctx.llm.resolveModelInfo`); both are skipped when `autoModelContextLimit: false`. On probe failure it falls back to the default, and the `/acp` command shows the window source (the `acp_status` model tool carries no window info). A failed probe is surfaced in the host log and the `/acp` panel (`restart to re-probe`) — the failure is cached like a success, so fixing the gateway requires a restart or an explicit `modelContextLimit` before the probe retries. On a successful probe the adapter's per-request output cap (`defaultMaxTokens` — the output reservation the provider guarantees at the end of the window) is SUBTRACTED, so every downstream pressure decision (nudge tiers, truncate, growth) measures usage against the SUSTAINABLE input budget (window − reservation): a 96K window with a 16K cap carries at most 80K of input, and the raw denominator understated usage by cap/window (≈17% there — and the ratio is far higher on short-window models, where the same cap is a quarter or more of the window). When the cap is undisclosed, the limit is explicit, or the probe fails, the raw-window behavior is kept; `/acp status` shows the subtraction (raw − reservation) |
221
221
  | `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 |
222
222
  | `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 (a same-name key in `coreOverrides.nudge` outranks it — see below) |
223
223
  | `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 (a same-name key in `coreOverrides.nudge` outranks it — see below) |
@@ -250,7 +250,7 @@ src/
250
250
  ├── nudge.ts # M4: kernel pressure decision → injected advisory nudge
251
251
  ├── system-prompt.ts# M4: one-time ACP guidance section (keeps nudges short)
252
252
  ├── config.ts # kernel config assembly (thresholds + coreOverrides)
253
- ├── window.ts # auto context-window detection (session projection first, LLM runtime probe fallback, default 128000)
253
+ ├── window.ts # auto context-window detection (session projection first, LLM runtime probe fallback, default 128000) + output-reservation probe (defaultMaxTokens, subtracted in windowFor)
254
254
  └── commands.ts # M4: /acp slash command
255
255
  ```
256
256
 
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [中文](./README.md) | [English](./README.en.md)
4
4
 
5
5
  > **⚠️ 测试版声明——请勿用于生产环境**
6
- > 本项目(**v0.2.18**)仍处于开发中的测试版。[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 本身也处于**公开测试版**阶段。**请勿将两者用于工程化 / 生产环境**——预期会有破坏性变更与粗糙之处。
6
+ > 本项目(**v0.2.20**)仍处于开发中的测试版。[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 本身也处于**公开测试版**阶段。**请勿将两者用于工程化 / 生产环境**——预期会有破坏性变更与粗糙之处。
7
7
 
8
8
  <p align="center">
9
9
  <strong>衷心感谢以下项目——请给它们一个 ⭐:</strong>
@@ -97,7 +97,7 @@ npm install billion-context-dsh
97
97
  **git 源安装(`github:` 规格,插件商店展示的形态)。** 预构建产物 `dist/` 已提交到仓库,从 git 源安装同样开箱即用——**无需任何构建步骤**,pnpm 11 默认拦截构建脚本(`allowBuilds`)的机制对这个包不构成障碍:
98
98
 
99
99
  ```bash
100
- dsh plugin --profile web add github:Tyan66666/billion-context-dsh#v0.2.18
100
+ dsh plugin --profile web add github:Tyan66666/billion-context-dsh#v0.2.20
101
101
  ```
102
102
 
103
103
  建议带 `#<tag>` 安装,拿到与对应 npm 版本完全一致的产物;不带 ref 则装默认分支的最新构建。只有 clone 仓库自行从源码构建(`npm run build`)才需要放行构建。背景与方案取舍见 [docs/git-source-install-design.md](docs/git-source-install-design.md)(issue #92)。
@@ -213,8 +213,8 @@ DSH 的每个模型请求都派生自其 append-only 会话日志(*surface*)
213
213
 
214
214
  | 键 | 默认值 | 含义 |
215
215
  |---|---|---|
216
- | `modelContextLimit` | 自动探测(回退 `128000`) | 用于内核压力决策的上下文窗口;显式配置时优先且跳过探测。省略时优先读宿主会话投影 `contextPressure.contextWindow`(按**当前真实路由**披露的新窗口,切模型会话自动跟随,无需重启),无投影时再从模型 API 探测 |
217
- | `autoModelContextLimit` | `true` | 从模型 API 自动探测真实窗口(`agent.ctx.llm.resolveModelInfo`);探测失败回退默认值,`/acp` 命令展示窗口来源(模型工具 `acp_status` 不含窗口信息)。省略时窗口先读宿主投影(`windowFor` → `projectedContextWindow`,`src/window.ts`)再走探测;投影与探测在 `autoModelContextLimit: false` 时均跳过。探测失败会在宿主日志与 `/acp` 面板提示(`restart to re-probe`)——失败结果同样被缓存,修复网关后需重启或显式设置 `modelContextLimit` 才会重新探测 |
216
+ | `modelContextLimit` | 自动探测(回退 `128000`) | 用于内核压力决策的上下文窗口;显式配置时优先且跳过探测。省略时优先读宿主会话投影 `contextPressure.contextWindow`(按**当前真实路由**披露的新窗口,切模型会话自动跟随,无需重启),无投影时再从模型 API 探测;显式配置同样跳过输出预留扣减(分母完全由操作者定义) |
217
+ | `autoModelContextLimit` | `true` | 从模型 API 自动探测真实窗口(`agent.ctx.llm.resolveModelInfo`);探测失败回退默认值,`/acp` 命令展示窗口来源(模型工具 `acp_status` 不含窗口信息)。省略时窗口先读宿主投影(`windowFor` → `projectedContextWindow`,`src/window.ts`)再走探测;投影与探测在 `autoModelContextLimit: false` 时均跳过。探测失败会在宿主日志与 `/acp` 面板提示(`restart to re-probe`)——失败结果同样被缓存,修复网关后需重启或显式设置 `modelContextLimit` 才会重新探测。探测成功后还会**扣减 adapter 的每请求输出上限**(`defaultMaxTokens`,窗口末端每请求保证的输出预留):所有下游压力决策(nudge 档位、truncate、growth)以「可持续输入预算」(窗口 − 输出预留)为分母——96K 窗口 + 16K 上限实际最多承载 80K 输入,原裸窗口分母会把用量低估 cap/window(此处 ≈17%;小上下文窗口模型比例更高);上限未披露、显式配置或探测失败时保持裸窗口行为,`/acp status` 展示扣减明细(raw − reservation) |
218
218
  | `nudgeMinContextLimitPct` | 内核默认 `0.45` | Nudge 窗口下界(用量占比)——仅作配置校验,增长路径的触发没有百分比下限——与 billion-context-pi 相同的默认值 |
219
219
  | `nudgeMaxContextLimitPct` | engine 默认 `0.70`(内核/pi 默认 `0.75`) | 过限线:超过此值则无论增长与否都触发 nudge——刻意低于宿主 compaction-basic 的 80% 自动压缩线,保证强制 nudge 先触发;显式配置优先(`coreOverrides.nudge` 同名键优先级更高,见下) |
220
220
  | `nudgeEmergencyThresholdPct` | engine 默认 `0.85`(内核/pi 默认 `0.95`) | 紧急 nudge(绕过每轮去重)——从 `0.95` 下调:95% 时模型已无操作空间且会被 80% 自动压缩线遮蔽;显式配置优先(`coreOverrides.nudge` 同名键优先级更高,见下) |
@@ -247,7 +247,7 @@ src/
247
247
  ├── nudge.ts # M4: 内核压力决策 → 注入的建议式 nudge
248
248
  ├── system-prompt.ts# M4: 一次性 ACP 指引段(让 nudge 保持简短)
249
249
  ├── config.ts # 内核配置组装(阈值 + coreOverrides)
250
- ├── window.ts # 自动上下文窗口探测(宿主投影优先,LLM 运行时探测回退,兜底 128000
250
+ ├── window.ts # 自动上下文窗口探测(宿主投影优先,LLM 运行时探测回退,兜底 128000)+ 输出预留探测(defaultMaxTokens,windowFor 内扣除)
251
251
  └── commands.ts # M4: /acp 斜杠命令
252
252
  ```
253
253
 
package/dist/index.d.ts CHANGED
@@ -128,6 +128,8 @@ export declare class AcpCompactionEngine extends CompactionEngine {
128
128
  private readonly compressCallIdsToHide;
129
129
  /** Per provider/model route the resolved window (probe failures cached too). */
130
130
  private readonly windowCache;
131
+ /** Per route the adapter's per-request output cap (the output reservation); null = undisclosed. */
132
+ private readonly outputReservationCache;
131
133
  constructor(ctx: Context, config?: Partial<AcpConfig>);
132
134
  /**
133
135
  * Resolve the effective context window for an agent. An explicitly
@@ -138,9 +140,31 @@ export declare class AcpCompactionEngine extends CompactionEngine {
138
140
  * projectedContextWindow). Falls back to probing the model's real window
139
141
  * via `agent.ctx.llm.resolveModelInfo` (cached per provider/model route,
140
142
  * probe failures cached too) and finally to DEFAULT_CONTEXT_WINDOW when
141
- * auto-detection is disabled or unavailable.
143
+ * auto-detection is disabled or unavailable. On the auto-detected paths the
144
+ * adapter's per-request output cap is then SUBTRACTED from the window
145
+ * (applyReservation): every downstream usage computation must run against
146
+ * the SUSTAINABLE input budget (window minus output reservation), not the
147
+ * raw window — a 96K window with a 16K cap carries at most 80K of input,
148
+ * so the raw denominator understates usage by cap/window (≈17% there, and
149
+ * far worse on short-window models). An explicit limit keeps the operator's
150
+ * exact value (they own the denominator); a failed probe keeps the raw
151
+ * fallback.
142
152
  */
143
153
  windowFor(agent: Agent): Promise<AcpWindow>;
154
+ /**
155
+ * The adapter's per-request output cap for a route, from one
156
+ * probeModelWindow call (a local catalog lookup — no request is sent),
157
+ * cached per route like the window itself.
158
+ */
159
+ private outputCapFor;
160
+ /**
161
+ * Subtract the output reservation from a resolved window: `limit` becomes
162
+ * the SUSTAINABLE input budget (`rawLimit - outputReserved`) that every
163
+ * downstream usage computation (nudge tiers, truncate, growth) measures
164
+ * against. No-op when the cap is unknown or not smaller than the window
165
+ * (degenerate config) — the raw-window behavior is preserved.
166
+ */
167
+ private applyReservation;
144
168
  /** ACP is model-driven: automatic pressure policy never summarizes by itself. */
145
169
  compactIfNeeded(_agent: CompactionAgentContext, _trigger: CompactionTrigger, signal: AbortSignal): Promise<CompactionResult | null>;
146
170
  /** Explicit idle-session compaction: ACP leaves the decision to the model. */
package/dist/index.js CHANGED
@@ -2881,7 +2881,7 @@ function assertNoActiveCompaction(events) {
2881
2881
  else if (event.type === "compaction/end") active = false;
2882
2882
  }
2883
2883
  if (active) {
2884
- throw new Error("billion-context-dsh: another compaction is already active for this session");
2884
+ console.warn("billion-context-dsh: clearing stale compaction flag \u2014 found a compaction/start with no matching compaction/end");
2885
2885
  }
2886
2886
  }
2887
2887
  function hasPlainRef(session, seq) {
@@ -3012,31 +3012,49 @@ function runCompactionTransaction(session, input) {
3012
3012
  const turn = findOpenTurn(sessionEventsOf(session));
3013
3013
  const compactionId = CompactionId(randomUUID());
3014
3014
  const seqs = [];
3015
- seqs.push(session.append("compaction/start", { compactionId, turn }).seq);
3016
- seqs.push(session.append("compaction/summary", {
3017
- compactionId,
3018
- summary: input.summary,
3019
- shadowedRange: { start: input.start, end: input.end },
3020
- shadowedSeqs: [...input.shadowedSeqs],
3021
- shadowedTokenCount: input.shadowedTokenCount,
3022
- provider: input.provider,
3023
- model: input.model,
3024
- tier: input.tier ?? 1,
3025
- ...input.kernelBlockId === void 0 ? {} : { kernelBlockId: input.kernelBlockId },
3026
- ...input.topic === void 0 ? {} : { topic: input.topic },
3027
- ...input.parentBlockIds === void 0 || input.parentBlockIds.length === 0 ? {} : { parentBlockIds: [...input.parentBlockIds] },
3028
- ...input.directMessageIds === void 0 ? {} : { directMessageIds: [...input.directMessageIds] },
3029
- ...input.effectiveMessageIds === void 0 ? {} : { effectiveMessageIds: [...input.effectiveMessageIds] }
3030
- }).seq);
3031
- const message = createUserMessage({
3032
- content: input.summary,
3033
- source: compactCheckpointSource(compactionId)
3034
- });
3035
- seqs.push(session.append("user/message", message, {
3036
- surfaceOp: { op: "replace", start: input.start, end: input.end },
3037
- sourceEventSeqs: [...input.shadowedSeqs]
3038
- }).seq);
3039
- seqs.push(session.append("compaction/end", { compactionId, turn }).seq);
3015
+ if (input.start > input.end) {
3016
+ throw new Error(`billion-context-dsh: reversed range ${input.start}..${input.end}`);
3017
+ }
3018
+ if (eventAtOf(session, input.start) === void 0 || eventAtOf(session, input.end) === void 0) {
3019
+ const failedEdge = eventAtOf(session, input.start) === void 0 ? input.start : input.end;
3020
+ throw new Error(
3021
+ `billion-context-dsh: seq ${input.start}..${input.end} not in the current surface \u2014 edge seq ${failedEdge} is not in this session's log. Surface seqs are sparse message nodes (only user/message, assistant/message, tool/result events); consult acp_status for the current surface range`
3022
+ );
3023
+ }
3024
+ try {
3025
+ seqs.push(session.append("compaction/start", { compactionId, turn }).seq);
3026
+ seqs.push(session.append("compaction/summary", {
3027
+ compactionId,
3028
+ summary: input.summary,
3029
+ shadowedRange: { start: input.start, end: input.end },
3030
+ shadowedSeqs: [...input.shadowedSeqs],
3031
+ shadowedTokenCount: input.shadowedTokenCount,
3032
+ provider: input.provider,
3033
+ model: input.model,
3034
+ tier: input.tier ?? 1,
3035
+ ...input.kernelBlockId === void 0 ? {} : { kernelBlockId: input.kernelBlockId },
3036
+ ...input.topic === void 0 ? {} : { topic: input.topic },
3037
+ ...input.parentBlockIds === void 0 || input.parentBlockIds.length === 0 ? {} : { parentBlockIds: [...input.parentBlockIds] },
3038
+ ...input.directMessageIds === void 0 ? {} : { directMessageIds: [...input.directMessageIds] },
3039
+ ...input.effectiveMessageIds === void 0 ? {} : { effectiveMessageIds: [...input.effectiveMessageIds] }
3040
+ }).seq);
3041
+ const message = createUserMessage({
3042
+ content: input.summary,
3043
+ source: compactCheckpointSource(compactionId)
3044
+ });
3045
+ seqs.push(session.append("user/message", message, {
3046
+ surfaceOp: { op: "replace", start: input.start, end: input.end },
3047
+ sourceEventSeqs: [...input.shadowedSeqs]
3048
+ }).seq);
3049
+ seqs.push(session.append("compaction/end", { compactionId, turn }).seq);
3050
+ } catch (error) {
3051
+ try {
3052
+ session.append("compaction/end", { compactionId, turn });
3053
+ } catch (compensateError) {
3054
+ console.warn("billion-context-dsh: failed to write a compensating compaction/end", compensateError);
3055
+ }
3056
+ throw error;
3057
+ }
3040
3058
  return { compactionId, seqs };
3041
3059
  }
3042
3060
  function summarySeqOfCompaction(events, compactionId) {
@@ -4365,18 +4383,24 @@ function projectedContextWindow(agent) {
4365
4383
  if (typeof window === "number" && Number.isInteger(window) && window > 0) return window;
4366
4384
  return null;
4367
4385
  }
4368
- async function detectContextWindow(agent, provider, model) {
4386
+ async function probeModelWindow(agent, provider, model) {
4369
4387
  const llm = agent.ctx?.get?.("llm");
4370
- if (llm?.resolveModelInfo === void 0) return null;
4388
+ if (llm?.resolveModelInfo === void 0) return { contextWindow: null, outputReservation: null };
4371
4389
  try {
4372
4390
  const info = await llm.resolveModelInfo(provider, model);
4373
4391
  const window = info?.context?.contextWindow;
4374
- if (typeof window === "number" && Number.isInteger(window) && window > 0) return window;
4375
- return null;
4392
+ const cap = info?.defaultMaxTokens;
4393
+ return {
4394
+ contextWindow: typeof window === "number" && Number.isInteger(window) && window > 0 ? window : null,
4395
+ outputReservation: typeof cap === "number" && Number.isInteger(cap) && cap > 0 ? cap : null
4396
+ };
4376
4397
  } catch {
4377
- return null;
4398
+ return { contextWindow: null, outputReservation: null };
4378
4399
  }
4379
4400
  }
4401
+ async function detectContextWindow(agent, provider, model) {
4402
+ return (await probeModelWindow(agent, provider, model)).contextWindow;
4403
+ }
4380
4404
 
4381
4405
  // src/commands.ts
4382
4406
  async function statusText(env, agent) {
@@ -4388,12 +4412,13 @@ async function statusText(env, agent) {
4388
4412
  const estimated = resolveTokenCount(agent, surfaceMessages);
4389
4413
  const window = await resolveEffectiveWindow(env, agent);
4390
4414
  const limit = window.limit;
4415
+ const windowLine = window.rawLimit !== void 0 && window.outputReserved !== void 0 ? ` context window: ${limit} (raw ${window.rawLimit} \u2212 ${window.outputReserved} output reservation; ${windowSourceLabel(window)})` : ` context window: ${limit} (${windowSourceLabel(window)})`;
4391
4416
  const lines = [
4392
4417
  `ACP status \u2014 session ${session.id}`,
4393
4418
  ` blocks: ${ledger.length}`,
4394
4419
  ` tokens compressed: ${totalTokens}`,
4395
4420
  ` estimated context: ${estimated} / ${limit} (${Math.round(estimated / limit * 100)}%)`,
4396
- ` context window: ${limit} (${windowSourceLabel(window)})`
4421
+ windowLine
4397
4422
  ];
4398
4423
  if (window.probeFailed === true) {
4399
4424
  lines.push(` \u26A0 window auto-detection failed \u2014 using the ${limit} fallback (restart to re-probe, or set modelContextLimit explicitly)`);
@@ -4522,6 +4547,8 @@ var AcpCompactionEngine = class extends CompactionEngine {
4522
4547
  compressCallIdsToHide = /* @__PURE__ */ new Set();
4523
4548
  /** Per provider/model route the resolved window (probe failures cached too). */
4524
4549
  windowCache = /* @__PURE__ */ new Map();
4550
+ /** Per route the adapter's per-request output cap (the output reservation); null = undisclosed. */
4551
+ outputReservationCache = /* @__PURE__ */ new Map();
4525
4552
  constructor(ctx, config = {}) {
4526
4553
  super(ctx);
4527
4554
  this.config = resolveAcpConfig(config);
@@ -4630,7 +4657,15 @@ var AcpCompactionEngine = class extends CompactionEngine {
4630
4657
  * projectedContextWindow). Falls back to probing the model's real window
4631
4658
  * via `agent.ctx.llm.resolveModelInfo` (cached per provider/model route,
4632
4659
  * probe failures cached too) and finally to DEFAULT_CONTEXT_WINDOW when
4633
- * auto-detection is disabled or unavailable.
4660
+ * auto-detection is disabled or unavailable. On the auto-detected paths the
4661
+ * adapter's per-request output cap is then SUBTRACTED from the window
4662
+ * (applyReservation): every downstream usage computation must run against
4663
+ * the SUSTAINABLE input budget (window minus output reservation), not the
4664
+ * raw window — a 96K window with a 16K cap carries at most 80K of input,
4665
+ * so the raw denominator understates usage by cap/window (≈17% there, and
4666
+ * far worse on short-window models). An explicit limit keeps the operator's
4667
+ * exact value (they own the denominator); a failed probe keeps the raw
4668
+ * fallback.
4634
4669
  */
4635
4670
  async windowFor(agent) {
4636
4671
  if (this.config.modelContextLimit !== void 0) {
@@ -4642,28 +4677,58 @@ var AcpCompactionEngine = class extends CompactionEngine {
4642
4677
  if (this.config.autoModelContextLimit) {
4643
4678
  const projected = projectedContextWindow(agent);
4644
4679
  if (projected !== null) {
4645
- return { limit: projected, source: "projection", provider, model };
4680
+ const cap2 = await this.outputCapFor(agent, provider, model);
4681
+ return this.applyReservation({ limit: projected, source: "projection", provider, model }, cap2);
4646
4682
  }
4647
4683
  }
4648
4684
  const cached = this.windowCache.get(key);
4649
4685
  if (cached !== void 0) return cached;
4650
4686
  let window;
4687
+ let cap = null;
4651
4688
  if (!this.config.autoModelContextLimit) {
4652
4689
  window = { limit: DEFAULT_CONTEXT_WINDOW, source: "default", provider, model };
4653
4690
  } else {
4654
- const detected = await detectContextWindow(agent, provider, model);
4655
- if (detected === null) {
4691
+ const probe = await probeModelWindow(agent, provider, model);
4692
+ cap = probe.outputReservation;
4693
+ if (probe.contextWindow === null) {
4656
4694
  this.ctx.logger.warn(
4657
4695
  `billion-context-dsh: context-window auto-detection failed for ${provider}/${model} \u2014 using the ${DEFAULT_CONTEXT_WINDOW} fallback (restart to re-probe, or set modelContextLimit explicitly)`
4658
4696
  );
4659
4697
  window = { limit: DEFAULT_CONTEXT_WINDOW, source: "default", provider, model, probeFailed: true };
4698
+ cap = null;
4660
4699
  } else {
4661
- window = { limit: detected, source: "auto", provider, model };
4700
+ window = { limit: probe.contextWindow, source: "auto", provider, model };
4662
4701
  }
4663
4702
  }
4703
+ window = this.applyReservation(window, cap);
4664
4704
  this.windowCache.set(key, window);
4665
4705
  return window;
4666
4706
  }
4707
+ /**
4708
+ * The adapter's per-request output cap for a route, from one
4709
+ * probeModelWindow call (a local catalog lookup — no request is sent),
4710
+ * cached per route like the window itself.
4711
+ */
4712
+ async outputCapFor(agent, provider, model) {
4713
+ if (provider === "" || model === "") return null;
4714
+ const key = `${provider}\0${model}`;
4715
+ const known = this.outputReservationCache.get(key);
4716
+ if (known !== void 0) return known;
4717
+ const cap = (await probeModelWindow(agent, provider, model)).outputReservation;
4718
+ this.outputReservationCache.set(key, cap);
4719
+ return cap;
4720
+ }
4721
+ /**
4722
+ * Subtract the output reservation from a resolved window: `limit` becomes
4723
+ * the SUSTAINABLE input budget (`rawLimit - outputReserved`) that every
4724
+ * downstream usage computation (nudge tiers, truncate, growth) measures
4725
+ * against. No-op when the cap is unknown or not smaller than the window
4726
+ * (degenerate config) — the raw-window behavior is preserved.
4727
+ */
4728
+ applyReservation(window, cap) {
4729
+ if (cap === null || cap >= window.limit) return window;
4730
+ return { ...window, rawLimit: window.limit, outputReserved: cap, limit: window.limit - cap };
4731
+ }
4667
4732
  /** ACP is model-driven: automatic pressure policy never summarizes by itself. */
4668
4733
  async compactIfNeeded(_agent, _trigger, signal) {
4669
4734
  signal.throwIfAborted();