Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
# Changelog

### v1.17.0 — context-limit safety net + budget guard: no more silent 400 death loops

Six fixes bundled, two of them new protection subsystems for sessions whose context window was unknown or exceeded:

**1. Context-limit safety net for spawn+resume** (#349, fixes #346 — HIGH):
In headless spawn+resume mode the model-limit catalog seed raced server readiness and stayed empty forever; `state.modelContextLimit` was learned and lost every message, disabling every percentage threshold (nudges, emergency override, GC, truncation). The session grew until the backend rejected it.
- The system hook now persists the learned limit (and its model identity) to session state, so a freshly spawned process resumes with the limit already known.
- `hydrateAndResolve()`: on a catalog miss during a request (server guaranteed up), hydration is retried once per process; concurrent callers await the same promise.
- New `resolveEffectiveContextLimit()` — model limit if known, else new `compress.contextLimitFallback` (default 128000, `0` disables) — now drives nudge thresholds, emergency override, GC batch cleanup, and tool-output truncation uniformly.
- Internal agents (title/summary/compaction) running on a different model no longer overwrite the session's limit.
- `OUTPUT_RESERVE_TOKENS` (16384) subtracted from the GC truncation threshold — the serving wall is window minus system prompt minus max_tokens, not the full window.
- Post-transform hard guard: ERROR log when the outgoing request still exceeds the real budget (the only signal before opencode's silent exit-0 on rejection).

**2. Context budget guard** (#350, fixes #347 — HIGH):
A model with no declared window (`limit.context = 0`, common on custom OpenAI-compatible providers) grew requests past the backend's real window → HTTP 400 → opencode swallows it as empty exit-0 — a permanently stuck session with no error surface.
- New `enforceContextBudget` in `messages.transform`: deterministic truncate-then-clear of the oldest compressible tool outputs when the estimated wire size exceeds `modelContextLimit − compress.completionReserveTokens` (default 32768, covering opencode's 32000 max_tokens fallback). Guards first user message, last 3 messages, protected tools, and compress summaries (Bug 39 parity). Idempotent with GC's truncation marker.
- Enforces ONLY the model-reported window — an absolute `compress.maxContextLimit` stays a soft nudge threshold (pruning to a guessed threshold starves the nudge of compressible targets; observed as an `e2e-blocks-nudges` regression during development).
- One-time per-session WARN with actionable guidance when the model reports no window.
- The competing design (#348, absolute-config fallback chain + clear-only) was closed in favor of this one.

**3. Nudge/exec char-counter alignment** (#360, fixes #359): the compress-recommendation side counted tool parts via `JSON.stringify(part).length / 4` while the execution-side min-size check used `countMessageCharacters` — recommendations could point at ranges the executor then rejected as below floor. Both sides now use `countMessageCharacters(msg) / 4`.

**4. Tier-aware cadence reset** (#365, fixes #364): every tier-1 capture reset the T2/T3 nudge baselines, re-arming the growthFloor wait — in compression-active sessions T2 distillation never fired. New `isCaptureOnlyCompress()`: only block-ref boundaries (real distillations/condensations) reset tier baselines; raw-message captures (all `mNNNNN`) don't. No-boundary/malformed calls conservatively keep the reset (#235 loop-prevention preserved).

**5. Reasoning tokens in context estimates** (#374, fixes #371): `/acp status` overview and drilldowns, and the nudge CONTEXT BREAKDOWN, previously omitted `reasoning` parts entirely; reasoning is now its own tracked category, included in totals and size sorting.

**6. `/acp` command error-log leak** (#297, fixes #296): the command handler's `throw new Error("__DCP_CONTEXT_HANDLED__")` sentinel leaked to opencode's error log on every `/acp` invocation; replaced with a plain `return` (commands already deliver output via `sendIgnoredMessage`).

Files: `lib/state/state.ts`, `lib/state/utils.ts`, `lib/hooks.ts`, `lib/config.ts`, `lib/config-validation.ts`, `lib/messages/inject/utils.ts`, `lib/messages/truncate-tools.ts`, `lib/messages/enforce-budget.ts` (new), `lib/messages/query.ts`, `lib/messages/inject/inject.ts`, `lib/compress/status.ts`, `dcp.schema.json`, CONFIGURATION (EN/zh). Tests: `tests/context-limit-fallback.test.ts`, `tests/model-switch-limits.test.ts`, `tests/truncate-tools.test.ts`, `tests/enforce-budget.test.ts` (new), `tests/recommend-exec-counter-alignment.test.ts` (new), `tests/inject.test.ts`, `tests/query-pure.test.ts`, `tests/acp-status.test.ts`, `tests/hooks-permission.test.ts`. Full suite 1207/1207; all six PRs locally re-verified (typecheck + tests + build) before merge.

**Install**: `opencode plugin opencode-acp@latest --global`

### v1.16.0 — storagePath: custom storage location for session state files

**Problem**: ACP's per-session state files (`{sessionId}.json` — compression blocks, nudge state, token stats) were always written to the hardcoded `$XDG_DATA_HOME/opencode/storage/plugin/acp`. Users on containers, NFS homes, or tight XDG data dirs had no way to relocate them (issue #379).
Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.zh-CN.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
# 更新日志

### v1.17.0 — 上下文窗口安全网 + 预算守卫:终结静默 400 死循环

捆绑六项修复,其中两项是针对“窗口未知/超出窗口”会话的新保护子系统:

**1. spawn+resume 模式的上下文窗口安全网**(#349,修复 #346 —— 高危):
headless spawn+resume 模式下,模型目录的初始化种子与服务器就绪竞态,永久空置;`state.modelContextLimit` 每条消息学到即丢,所有百分比阈值(nudge、紧急覆盖、GC、截断)全部失效,会话无限增长直到后端拒绝。
- system hook 现在把学到的窗口(及其模型身份)持久化到会话状态,新进程启动即已知晓。
- `hydrateAndResolve()`:请求内目录未命中时(此时服务器必然已就绪)每进程重试一次 hydrate;并发调用等待同一 promise。
- 新 `resolveEffectiveContextLimit()` —— 已知模型窗口,否则新的 `compress.contextLimitFallback`(默认 128000,`0` 禁用)—— 统一驱动 nudge 阈值、紧急覆盖、GC 批量清理、工具输出截断。
- 内部 agent(标题/摘要/compaction)跑在不同模型上时不再覆盖会话窗口。
- GC 截断阈值减去 `OUTPUT_RESERVE_TOKENS`(16384)—— 服务端真正的墙是窗口减系统提示减 max_tokens,不是完整窗口。
- 变换后硬守卫:出站请求仍超真实预算时打 ERROR 日志(opencode 静默 exit-0 拒绝前唯一的信号)。

**2. 上下文预算守卫**(#350,修复 #347 —— 高危):
未声明窗口的模型(`limit.context = 0`,自定义 OpenAI 兼容供应商常见)请求增长超过后端真实窗口 → HTTP 400 → opencode 吞掉报错、空响应 exit-0 —— 会话永久卡死且无错误可见。
- 新 `enforceContextBudget`(messages.transform 内):估算线包超过 `modelContextLimit − compress.completionReserveTokens`(默认 32768,覆盖 opencode 对未声明 limit.output 的 32000 max_tokens 回退)时,确定性“先截断后清空”最老的可压缩工具输出。保护首条用户消息、最近 3 条、保护工具、compress 摘要(Bug 39 同等保护);与 GC 截断标记幂等。
- 只强制模型上报的窗口 —— 绝对值 `compress.maxContextLimit` 保持软 nudge 阈值语义(剪到猜测的阈值会饿死 nudge 的可压缩目标;开发中曾触发 `e2e-blocks-nudges` 回归)。
- 模型未报窗口时每会话一次性 WARN,给出可操作的配置指引。
- 竞争方案(#348,绝对配置回退链 + 只清空)已关闭,采纳本方案。

**3. nudge/执行侧字符计数统一**(#360,修复 #359):压缩推荐侧用 `JSON.stringify(part).length / 4` 计数工具 part,而执行侧下限检查用 `countMessageCharacters` —— 推荐可能指向执行侧判定低于下限的范围。两侧统一为 `countMessageCharacters(msg) / 4`。

**4. 分层感知的节奏重置**(#365,修复 #364):每次 T1 捕获都重置 T2/T3 nudge 基线,重新武装 growthFloor 等待 —— 压缩活跃会话中 T2 蒸馏永远不触发。新 `isCaptureOnlyCompress()`:只有块引用边界(真正的蒸馏/凝结)重置分层基线;纯消息捕获(全部 `mNNNNN`)不重置。无边界/畸形输入保守地保持重置(保留 #235 防循环)。

**5. 上下文估算计入 reasoning token**(#374,修复 #371):`/acp status` 总览与下钻、nudge 的 CONTEXT BREAKDOWN 此前完全遗漏 `reasoning` part;现作为独立类别追踪,计入总量与大小排序。

**6. `/acp` 命令错误日志泄漏**(#297,修复 #296):命令处理器的 `throw new Error("__DCP_CONTEXT_HANDLED__")` 哨兵每次 `/acp` 调用都泄漏到 opencode 错误日志;改为普通 `return`(命令本就通过 `sendIgnoredMessage` 交付输出)。

文件:`lib/state/state.ts`、`lib/state/utils.ts`、`lib/hooks.ts`、`lib/config.ts`、`lib/config-validation.ts`、`lib/messages/inject/utils.ts`、`lib/messages/truncate-tools.ts`、`lib/messages/enforce-budget.ts`(新增)、`lib/messages/query.ts`、`lib/messages/inject/inject.ts`、`lib/compress/status.ts`、`dcp.schema.json`、CONFIGURATION(中英)。测试:`tests/context-limit-fallback.test.ts`、`tests/model-switch-limits.test.ts`、`tests/truncate-tools.test.ts`、`tests/enforce-budget.test.ts`(新增)、`tests/recommend-exec-counter-alignment.test.ts`(新增)、`tests/inject.test.ts`、`tests/query-pure.test.ts`、`tests/acp-status.test.ts`、`tests/hooks-permission.test.ts`。全量 1207/1207;六个 PR 合并前均在本机重新验证(typecheck + 测试 + 构建)。

### v1.16.0 — storagePath:自定义会话状态文件存储位置

**问题**:ACP 的每会话状态文件(`{sessionId}.json` —— 压缩块、nudge 状态、token 统计)此前固定写入硬编码路径 `$XDG_DATA_HOME/opencode/storage/plugin/acp`。容器、NFS 家目录或 XDG data 目录空间紧张的用户无法迁移(issue #379)。
Expand Down
31 changes: 31 additions & 0 deletions devlog/2026-09-10_release-v1.17.0/REQ.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# REQ — Release v1.17.0

## Goal

Ship the six reviewed/verified PRs merged to master on 2026-09-10 as v1.17.0.

## Bundled PRs

| PR | Fixes | Summary | Local verification at review time |
|---|---|---|---|
| #349 | #346 | Context-limit safety net for spawn+resume: persist learned limit, lazy catalog hydration, `resolveEffectiveContextLimit` + `compress.contextLimitFallback` (128000), OUTPUT_RESERVE_TOKENS, post-transform hard-guard ERROR | 1050/1050 + typecheck |
| #350 | #347 | Context budget guard: `enforceContextBudget` truncate-then-clear, `compress.completionReserveTokens` (32768), one-time no-window WARN; model-window-only by design | 19/19 (enforce-budget) + full suite on stacked head |
| #360 | #359 | Recommend-side counter aligned to `countMessageCharacters` | 1070/1070 |
| #365 | #364 | `isCaptureOnlyCompress`: T1 captures no longer reset T2/T3 cadence baselines | 1091/1091 |
| #374 | #371 | Reasoning tokens as own category in status/breakdown estimates | 1086/1086 → post-master-sync 1207/1207 |
| #297 | #296 | `__DCP_CONTEXT_HANDLED__` throw → plain return | 9/9 file |

## Decisions

- Version: **1.17.0** (minor) — two new config keys (`compress.contextLimitFallback`, `compress.completionReserveTokens`) and two new subsystems, not just fixes.
- #348 closed in favor of #350 (absolute-config window chain over-prunes; e2e-blocks-nudges regression).
- #374 branch updated with master before merge (strict branch protection); final master suite 1207/1207.

## Acceptance

- [x] package.json bumped to 1.17.0
- [x] CHANGELOG.md + CHANGELOG.zh-CN.md entries with `### v1.17.0`
- [ ] check-pr.sh passes for this branch
- [ ] typecheck + build + full test suite green on release branch
- [ ] PR created; CI green
- [ ] Human merges (per AGENTS §5.1.1.2); release.yml auto-tags v1.17.0 and publishes to npm `latest`
25 changes: 25 additions & 0 deletions devlog/2026-09-10_release-v1.17.0/WORKLOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# WORKLOG — Release v1.17.0

## Steps

1. Confirmed all six PRs merged to master (3a4ac99 = merge of #374, the last one).
2. Branched `2026-09-10_release-v1.17.0` from `github/master`.
3. `package.json` 1.16.0 → 1.17.0.
4. Changelog entries added to `CHANGELOG.md` and `CHANGELOG.zh-CN.md` (six-PR breakdown, EN + zh).
5. Devlog REQ/WORKLOG (this file).
6. Local verification (see below), commit, push, PR.

## Local verification

- `./scripts/ci/check-pr.sh 2026-09-10_release-v1.17.0 github/master` — branch name / devlog / changelog checks
- `npm run typecheck` — 0 errors
- `npm run test` — full suite
- `npm run build` — success

(fill in numbers from the actual run)

## Follow-ups (not in this release)

- Remove `tmp/nested-fork-probe.mts` tracked on master (leaked via #350's implementation commit ce0aca3) — separate cleanup PR.
- Delete 4 orphan remote branches from the conflict-resolution session (pending human confirmation): `2026-09-03_counter-alignment`, `2026-09-04_tier2-cadence`, `2026-09-08_reasoning-in-estimate`, `2026-08-13_remove-dcp-context-handled-throw`.
- PR #376 (fork state transfer) still on hold pending issue #375 author's reply.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "opencode-acp",
"version": "1.16.0",
"version": "1.17.0",
"type": "module",
"description": "Active Context Pruning — model-driven context management for OpenCode (hardened fork of DCP with 35 bug fixes)",
"main": "./dist/index.js",
Expand Down
Loading