billion-context-dsh 0.2.9 → 0.2.11

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.9**) 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.11**) 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 ⭐:
@@ -130,7 +130,7 @@ DSH derives every model request from its append-only session log (the *surface*)
130
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 |
131
131
  | `decompress` | read-only recovery of shadowed originals from the log |
132
132
  | `search_context` | scores a unified doc set (block summaries + shadowed originals) rebuilt from the log via acp-kernel `searchBlocks` (hybrid: stemming + CJK bigrams + char n-gram fuzzy); hits link back to the owning block |
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
+ | `acp_status` | CONTEXT BREAKDOWN (tool/text/summaries shares of the visible total) + compressed-block ledger + nudge decision line + a `Checkpoint seqs` row mapping each ACTIVE block's kernel ref (`bN`) to its checkpoint summary seq — compressing a checkpoint seq distills that block (issue #60); no context-window rows; scope/view/tool/sort/limit drilldown supported |
134
134
  | block state | in-memory kernel state + **log-rebuilt ledger** (no sidecar files) |
135
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 |
136
136
  | compression accounting (shadow price) | `shadowedTokenCount` (what the host occupancy display deducts) is priced in the **host token-meter's vocabulary** (`ctx.tokenMeter.measure` preferred; exact mirror in `src/host-tokens.ts` as fallback) — never the plugin's internal CJK-aware estimate (that is display currency; mixing it into the host ledger can drive `messageTokens` negative and brick a CJK-heavy session, issue #54) |
@@ -150,7 +150,7 @@ A walkthrough of the ACP philosophy this project inherits — how active context
150
150
  | `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) |
151
151
  | `decompress` | Restore a previously compressed block's original content (read-only); accepts the `bN` ref shown by acp_status or a compaction id |
152
152
  | `search_context` | Search compressed block summaries and originals by keyword (acp-kernel hybrid retrieval: stemming + CJK bigrams + fuzzy); hits link back to the owning block |
153
- | `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) — feed them straight to `compress` as `startSeq`/`endSeq` (auto-mapped to the live surface seq); `Surface:` seqs work too |
153
+ | `acp_status` | CONTEXT BREAKDOWN (tool/text/summaries shares of the visible total) + compressed-block ledger + nudge decision line + a `Checkpoint seqs` row mapping each ACTIVE block's kernel ref (`bN`) to its checkpoint summary seq (the distill entry point, issue #60); 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) — feed them straight to `compress` as `startSeq`/`endSeq` (auto-mapped to the live surface seq); `Surface:` seqs work too |
154
154
  | `/acp` | status / compress / decompress from the command bar; status also shows human-side window info (estimated context, window source, compressed-block ledger, and **nudge arbitration** — `nudge: idle/ACTIVE — reason` plus how many tokens remain until the next nudge, decided by the same kernel turn as the nudge path) |
155
155
 
156
156
  ## Upstream & credits
@@ -171,7 +171,7 @@ This project reuses `acp-kernel`'s compression core and `billion-context-pi`'s d
171
171
  | Key | Default | Meaning |
172
172
  |---|---|---|
173
173
  | `modelContextLimit` | auto-detected (fallback `128000`) | Context window used for the kernel's pressure decisions; an explicit value wins and skips the probe |
174
- | `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) |
174
+ | `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). 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 |
175
175
  | `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 |
176
176
  | `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 |
177
177
  | `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.9**)仍处于开发中的测试版。[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 本身也处于**公开测试版**阶段。**请勿将两者用于工程化 / 生产环境**——预期会有破坏性变更与粗糙之处。
6
+ > 本项目(**v0.2.11**)仍处于开发中的测试版。[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 本身也处于**公开测试版**阶段。**请勿将两者用于工程化 / 生产环境**——预期会有破坏性变更与粗糙之处。
7
7
 
8
8
  <p align="center">
9
9
  <strong>衷心感谢以下项目——请给它们一个 ⭐:</strong>
@@ -131,7 +131,7 @@ DSH 的每个模型请求都派生自其 append-only 会话日志(*surface*)
131
131
  | nudge("效率提示——尽早压缩保持精简") | 由内核的压力决策在 `agent/pre-step` 注入——效率通知 + 上下文分解 + 压缩规则,语气对齐 kernel/pi;绝非命令 |
132
132
  | `decompress` | 从日志只读恢复被遮蔽的原文 |
133
133
  | `search_context` | 从日志重建块摘要 + 被遮蔽原文的统一文档集,交 acp-kernel `searchBlocks`(hybrid:词干化 + CJK bigram + 字符 n-gram 模糊)打分;命中回链所属块 |
134
- | `acp_status` | CONTEXT BREAKDOWN(tool/text/summaries 占可见总量)+ 压缩块账本 + nudge 决策行;不含上下文窗口;支持 scope/view/tool/sort/limit 钻取 |
134
+ | `acp_status` | CONTEXT BREAKDOWN(tool/text/summaries 占可见总量)+ 压缩块账本 + nudge 决策行 + `Checkpoint seqs` 行(active 块的 `bN → seq` 映射——压缩某个 checkpoint seq 即蒸馏该块,issue #60);不含上下文窗口;支持 scope/view/tool/sort/limit 钻取 |
135
135
  | 块状态 | 内存内核状态 + **日志重建账本**(无旁车文件) |
136
136
  | 分层蒸馏(T2/T3) | 再次压缩某块的摘要节点 = 蒸馏该块(tier 2),蒸馏 tier-2 块得 tier 3;tier 与内核块 id 持久化进日志,重启后内核状态从日志再水合、可继续蒸馏 |
137
137
  | 压缩记账(影子价格) | `shadowedTokenCount`(宿主占用率据此扣减)**用宿主 token-meter 的词汇计价**(`ctx.tokenMeter.measure` 优先,`src/host-tokens.ts` 精确镜像兜底)——绝不混用插件内部的 CJK 感知估算(那是展示货币,混用会把宿主账本扣成负数、卡死中文会话,issue #54) |
@@ -151,7 +151,7 @@ DSH 的每个模型请求都派生自其 append-only 会话日志(*surface*)
151
151
  | `compress` | 用你书写的紧凑摘要替换 seq 范围(边界自动平衡到 tool-call/result 配对点);对某块的摘要节点再次压缩 = 分层蒸馏(tier 2/3) |
152
152
  | `decompress` | 恢复已压缩块的原始内容(只读);接受 acp_status 显示的 `bN` 或 compaction id |
153
153
  | `search_context` | 按关键词搜索压缩块摘要与原文(acp-kernel hybrid 检索:词干化 + CJK bigram + 模糊);命中回链所属块 |
154
- | `acp_status` | CONTEXT BREAKDOWN(tool/text/summaries 占可见总量)+ 压缩块账本 + nudge 决策行;不含上下文窗口。支持钻取:`scope:"compressed"` 逐块、`scope:"uncompressed"` + `view:"messages"`/`"ranges"` 逐消息/区间,`tool` 过滤、`sort` 排序、`limit` 截断。钻取行 ref 是内核 mN——可直接作为 `compress` 的 `startSeq`/`endSeq`(自动映射为 live surface seq);`Surface:` 的 seq 同样可用 |
154
+ | `acp_status` | CONTEXT BREAKDOWN(tool/text/summaries 占可见总量)+ 压缩块账本 + nudge 决策行 + `Checkpoint seqs` 行(active 块的 `bN → seq` 蒸馏入口,issue #60);不含上下文窗口。支持钻取:`scope:"compressed"` 逐块、`scope:"uncompressed"` + `view:"messages"`/`"ranges"` 逐消息/区间,`tool` 过滤、`sort` 排序、`limit` 截断。钻取行 ref 是内核 mN——可直接作为 `compress` 的 `startSeq`/`endSeq`(自动映射为 live surface seq);`Surface:` 的 seq 同样可用 |
155
155
  | `/acp` | 从命令栏执行 status / compress / decompress;status 额外展示 human-side 窗口信息(estimated context、context window 来源、压缩账本、**nudge 仲裁**——`nudge: idle/ACTIVE — reason` 及距下一次 nudge 还差多少 token,与 nudge 路径同一内核判定) |
156
156
 
157
157
  ## 上游项目与致谢
@@ -172,7 +172,7 @@ DSH 的每个模型请求都派生自其 append-only 会话日志(*surface*)
172
172
  | 键 | 默认值 | 含义 |
173
173
  |---|---|---|
174
174
  | `modelContextLimit` | 自动探测(回退 `128000`) | 用于内核压力决策的上下文窗口;显式配置时优先且跳过探测 |
175
- | `autoModelContextLimit` | `true` | 从模型 API 自动探测真实窗口(`agent.ctx.llm.resolveModelInfo`);探测失败回退默认值,`/acp` 命令展示窗口来源(模型工具 `acp_status` 不含窗口信息) |
175
+ | `autoModelContextLimit` | `true` | 从模型 API 自动探测真实窗口(`agent.ctx.llm.resolveModelInfo`);探测失败回退默认值,`/acp` 命令展示窗口来源(模型工具 `acp_status` 不含窗口信息)。探测失败会在宿主日志与 `/acp` 面板提示(`restart to re-probe`)——失败结果同样被缓存,修复网关后需重启或显式设置 `modelContextLimit` 才会重新探测 |
176
176
  | `nudgeMinContextLimitPct` | 内核默认 `0.45` | Nudge 窗口下界(用量占比)——仅作配置校验,增长路径的触发没有百分比下限——与 billion-context-pi 相同的默认值 |
177
177
  | `nudgeMaxContextLimitPct` | engine 默认 `0.70`(内核/pi 默认 `0.75`) | 过限线:超过此值则无论增长与否都触发 nudge——刻意低于宿主 compaction-basic 的 80% 自动压缩线,保证强制 nudge 先触发;显式配置优先 |
178
178
  | `nudgeEmergencyThresholdPct` | engine 默认 `0.85`(内核/pi 默认 `0.95`) | 紧急 nudge(绕过每轮去重)——从 `0.95` 下调:95% 时模型已无操作空间且会被 80% 自动压缩线遮蔽;显式配置优先 |
@@ -4,6 +4,6 @@
4
4
  * @module billion-context-dsh/commands
5
5
  */
6
6
  import type { CommandDefinition } from '@deepseek-ai/dsh-commands';
7
- import type { ToolEnvironment } from './tools.ts';
7
+ import { type ToolEnvironment } from './tools.ts';
8
8
  /** Register the /acp command (idempotent per engine). */
9
9
  export declare function acpCommand(env: ToolEnvironment): CommandDefinition;
package/dist/index.js CHANGED
@@ -3525,7 +3525,7 @@ var NUDGE_ALLOWED = {
3525
3525
  normal: /* @__PURE__ */ new Set(["pct", "philosophy"]),
3526
3526
  emergency: /* @__PURE__ */ new Set(["pct", "philosophy"]),
3527
3527
  guidance: /* @__PURE__ */ new Set(),
3528
- tier: /* @__PURE__ */ new Set(["tier", "count", "prevTier", "tokens", "seqs"]),
3528
+ tier: /* @__PURE__ */ new Set(["tier", "count", "prevTier", "tokens", "seqs", "firstSeq", "lastSeq"]),
3529
3529
  breakdown: /* @__PURE__ */ new Set(["system", "tool", "summaries", "code", "text"]),
3530
3530
  growth: /* @__PURE__ */ new Set(["growth"]),
3531
3531
  tip: /* @__PURE__ */ new Set()
@@ -3600,7 +3600,7 @@ var DEFAULT_PROMPTS = {
3600
3600
  normal: "This is an efficiency nudge to compress early and keep context lean \u2014 not an overflow warning. A separate, stronger alert will appear if the context is actually full.\n\n{philosophy}",
3601
3601
  emergency: "\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.\n\n{philosophy}",
3602
3602
  guidance: HOW_TO_COMPRESS_RULES,
3603
- tier: "Tier {tier}: {count} tier-{prevTier} block(s) distillable ({tokens} tokens) \u2014 compress their summary node(s) [seqs {seqs}] to reclaim the original messages.",
3603
+ tier: "Tier {tier}: {count} tier-{prevTier} block(s) distillable ({tokens} tokens) \u2014 distill them by compressing their checkpoint seq(s) [seqs {seqs}] as one range: compress({ content: [{ startSeq: {firstSeq}, endSeq: {lastSeq}, summary }] }).",
3604
3604
  breakdown: "Context breakdown: {system}K system | {tool}K tool | {summaries}K summaries | {code}K code | {text}K text",
3605
3605
  growth: "+{growth}K since last nudge",
3606
3606
  tip: "\u{1F4A1} Compress all ranges in one call (pass multiple content entries: `content: [{...}, {...}]`)."
@@ -3749,7 +3749,7 @@ function replaceTierTrigger(text, nudge, session, prompts) {
3749
3749
  const next = rest.match(/\n\nHOW TO COMPRESS/);
3750
3750
  const end = next !== null ? start + 2 + next.index : text.length;
3751
3751
  const targets = nudge.tierTargetBlocks;
3752
- const summarySeqs = targets.map((block) => summarySeqOfKernelBlock(session, block.blockId)).filter((seq) => seq !== null);
3752
+ const summarySeqs = targets.map((block) => summarySeqOfKernelBlock(session, block.blockId)).filter((seq) => seq !== null).sort((a, b) => a - b);
3753
3753
  const pending = nudge.tier === 2 ? nudge.breakdown?.pendingT2 : nudge.breakdown?.pendingT3;
3754
3754
  const tokens = typeof pending === "number" ? pending : 0;
3755
3755
  const tierValue = nudge.tier === null ? 2 : nudge.tier;
@@ -3758,7 +3758,9 @@ function replaceTierTrigger(text, nudge, session, prompts) {
3758
3758
  count: targets.length,
3759
3759
  prevTier: tierValue - 1,
3760
3760
  tokens,
3761
- seqs: summarySeqs.join(", ")
3761
+ seqs: summarySeqs.join(", "),
3762
+ firstSeq: summarySeqs[0] ?? "n/a",
3763
+ lastSeq: summarySeqs[summarySeqs.length - 1] ?? "n/a"
3762
3764
  });
3763
3765
  return text.slice(0, start) + "\n\n" + tierLine + text.slice(end);
3764
3766
  }
@@ -3795,7 +3797,7 @@ function renderNudgeFromTemplates(nudge, emergency, session, prompts) {
3795
3797
  if (prompts.nudge.guidance !== "") parts.push("", prompts.nudge.guidance);
3796
3798
  if ((nudge.tier === 2 || nudge.tier === 3) && (nudge.tierTargetBlocks?.length ?? 0) > 0) {
3797
3799
  const targets = nudge.tierTargetBlocks;
3798
- const summarySeqs = targets.map((block) => summarySeqOfKernelBlock(session, block.blockId)).filter((seq) => seq !== null);
3800
+ const summarySeqs = targets.map((block) => summarySeqOfKernelBlock(session, block.blockId)).filter((seq) => seq !== null).sort((a, b) => a - b);
3799
3801
  const pending = nudge.tier === 2 ? nudge.breakdown?.pendingT2 : nudge.breakdown?.pendingT3;
3800
3802
  const tokens = typeof pending === "number" ? pending : 0;
3801
3803
  const tierLine = renderTemplate(prompts.nudge.tier, {
@@ -3803,7 +3805,9 @@ function renderNudgeFromTemplates(nudge, emergency, session, prompts) {
3803
3805
  count: targets.length,
3804
3806
  prevTier: nudge.tier - 1,
3805
3807
  tokens,
3806
- seqs: summarySeqs.join(", ")
3808
+ seqs: summarySeqs.join(", "),
3809
+ firstSeq: summarySeqs[0] ?? "n/a",
3810
+ lastSeq: summarySeqs[summarySeqs.length - 1] ?? "n/a"
3807
3811
  });
3808
3812
  if (tierLine !== "") parts.push(tierLine);
3809
3813
  const tierRules = nudge.tier === 2 ? TIER2_DISTILL_RULES : TIER3_CONDENSE_RULES;
@@ -3832,6 +3836,9 @@ function requireAgent(exec) {
3832
3836
  }
3833
3837
  return exec.agent;
3834
3838
  }
3839
+ async function resolveEffectiveWindow(env, agent) {
3840
+ return env.windowFor === void 0 ? { limit: env.modelContextLimit, source: "explicit" } : await env.windowFor(agent);
3841
+ }
3835
3842
  var compressParameters = {
3836
3843
  // Tolerated wrapped-arguments form: some models emit
3837
3844
  // `{ "arguments": "{\"content\": [...]}" }` (double-nested) or
@@ -3943,7 +3950,8 @@ async function handleCompress(env, args, exec) {
3943
3950
  const coreMessages = allLogMessages(session);
3944
3951
  const surfaceMessages = eventsToCoreMessages(surfaceEventsOf(session));
3945
3952
  const tokenCount = resolveTokenCount(agent, surfaceMessages);
3946
- const config = kernelConfigFor(env);
3953
+ const window = await resolveEffectiveWindow(env, agent);
3954
+ const config = kernelConfigFor({ ...env, modelContextLimit: window.limit });
3947
3955
  const turn = env.kernel.processTurn({ messages: coreMessages, state, config, tokenCount });
3948
3956
  env.store.set(session, turn.state);
3949
3957
  const byRaw = turn.state.messageRefs.byRaw;
@@ -4068,7 +4076,7 @@ async function handleCompress(env, args, exec) {
4068
4076
  effectiveMessageIds: block.effectiveMessageIds
4069
4077
  });
4070
4078
  const adjusted = start !== range.startSeq || end !== range.endSeq;
4071
- const tierLabel2 = tier === 1 ? "" : `, tier ${tier}`;
4079
+ const tierLabel2 = `, tier ${tier}`;
4072
4080
  const note = range.recovered === true ? ` (seqs ${range.startSeq}..${range.endSeq} were already shadowed \u2014 compressed the live remainder ${start}..${end})` : adjusted ? ` (adjusted from ${range.startSeq}..${range.endSeq} to balanced edges)` : "";
4073
4081
  lines.push(
4074
4082
  ` block ${compactionId.slice(0, 8)}: seqs ${start}..${end}, ${shadowed.length} messages shadowed${tierLabel2}${note}`
@@ -4227,7 +4235,8 @@ async function handleStatus(env, rawArgs, exec) {
4227
4235
  const coreMessages = allLogMessages(session);
4228
4236
  const surfaceMessages = eventsToCoreMessages(surface, toolNames);
4229
4237
  const tokenCount = resolveTokenCount(agent, surfaceMessages);
4230
- const config = kernelConfigFor(env);
4238
+ const window = await resolveEffectiveWindow(env, agent);
4239
+ const config = kernelConfigFor({ ...env, modelContextLimit: window.limit });
4231
4240
  const turn = env.kernel.processTurn({ messages: coreMessages, state, config, tokenCount });
4232
4241
  const statusMessages = eventsToCoreMessages(
4233
4242
  surface.filter((event) => !isCheckpointEvent(event)),
@@ -4240,6 +4249,10 @@ async function handleStatus(env, rawArgs, exec) {
4240
4249
  if (nudge !== void 0) {
4241
4250
  lines.push("", `Nudge: ${nudge.shouldInject ? "ACTIVE" : "idle"} \u2014 ${nudge.reason}`);
4242
4251
  }
4252
+ const checkpointRows = blockRegistry(session).filter((entry) => entry.active && entry.summarySeq !== null).map((entry) => `${entry.kernelBlockId} \u2192 seq ${entry.summarySeq}`);
4253
+ if (checkpointRows.length > 0) {
4254
+ lines.push("", `Checkpoint seqs (active blocks \u2014 compress a checkpoint seq to distill it): ${checkpointRows.join(", ")}`);
4255
+ }
4243
4256
  }
4244
4257
  lines.push("", `Surface: ${surfaceSummary(session)}`);
4245
4258
  if (args.scope === "uncompressed") {
@@ -4296,6 +4309,7 @@ function windowSourceLabel(window) {
4296
4309
  if (window.source === "auto") {
4297
4310
  return `auto-detected from ${window.provider ?? "?"}/${window.model ?? "?"}`;
4298
4311
  }
4312
+ if (window.probeFailed === true) return "default (auto-detection failed \u2014 restart to re-probe)";
4299
4313
  return "default (auto-detection unavailable)";
4300
4314
  }
4301
4315
  async function detectContextWindow(agent, provider, model) {
@@ -4319,7 +4333,7 @@ async function statusText(env, agent) {
4319
4333
  const coreMessages = allLogMessages(session);
4320
4334
  const surfaceMessages = eventsToCoreMessages(surfaceEventsOf(session));
4321
4335
  const estimated = resolveTokenCount(agent, surfaceMessages);
4322
- const window = env.windowFor === void 0 ? { limit: env.modelContextLimit, source: "explicit" } : await env.windowFor(agent);
4336
+ const window = await resolveEffectiveWindow(env, agent);
4323
4337
  const limit = window.limit;
4324
4338
  const lines = [
4325
4339
  `ACP status \u2014 session ${session.id}`,
@@ -4328,6 +4342,9 @@ async function statusText(env, agent) {
4328
4342
  ` estimated context: ${estimated} / ${limit} (${Math.round(estimated / limit * 100)}%)`,
4329
4343
  ` context window: ${limit} (${windowSourceLabel(window)})`
4330
4344
  ];
4345
+ if (window.probeFailed === true) {
4346
+ lines.push(` \u26A0 window auto-detection failed \u2014 using the ${limit} fallback (restart to re-probe, or set modelContextLimit explicitly)`);
4347
+ }
4331
4348
  const state = structuredClone(env.store.stateFor(session));
4332
4349
  const config = kernelConfigFor({ ...env, modelContextLimit: limit });
4333
4350
  const turn = env.kernel.processTurn({ messages: coreMessages, state, config, tokenCount: estimated });
@@ -4561,7 +4578,14 @@ var AcpCompactionEngine = class extends CompactionEngine {
4561
4578
  window = { limit: DEFAULT_CONTEXT_WINDOW, source: "default", provider, model };
4562
4579
  } else {
4563
4580
  const detected = await detectContextWindow(agent, provider, model);
4564
- window = detected === null ? { limit: DEFAULT_CONTEXT_WINDOW, source: "default", provider, model } : { limit: detected, source: "auto", provider, model };
4581
+ if (detected === null) {
4582
+ this.ctx.logger.warn(
4583
+ `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)`
4584
+ );
4585
+ window = { limit: DEFAULT_CONTEXT_WINDOW, source: "default", provider, model, probeFailed: true };
4586
+ } else {
4587
+ window = { limit: detected, source: "auto", provider, model };
4588
+ }
4565
4589
  }
4566
4590
  this.windowCache.set(key, window);
4567
4591
  return window;