opencode-acp 1.14.1 → 1.14.2

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.md CHANGED
@@ -528,6 +528,14 @@ For the complete list with root cause analysis, see the [bug tracker](https://gi
528
528
 
529
529
  ## Changelog
530
530
 
531
+ ### v1.14.2 — Split Protected Ranges + Soften Last-User-Message (PR #210)
532
+
533
+ **Problem**: In autonomous agentic sessions (1 user message + many assistant/tool messages), `buildCompressibleRanges` created one giant compressible group because grouping only breaks on user messages — and tool results are `assistant` role in OpenCode. The giant group's endRef fell in the protected zone → `excludeProtectedRanges` removed the entire range → zero recommendations → nudge suppressed → model could never compress. Additionally, `preserveLastUserMessage` hard-rejected any compress call covering the last user message, blocking deliberate compressions of surrounding tool output.
534
+
535
+ **Fix**: (1) `buildCompressibleRanges` now accepts `protectedZoneRefs` and splits groups at the protected-zone boundary — the unprotected head survives as a recommended range while the protected tail is excluded. (2) `preserveLastUserMessage` moved from hard-reject (`checkProtectedRange` throw) to soft-filter (`filterLastUserMessage` excludes from compress plan, following Bug 39's `filterProtectedToolMessages` pattern). The last user message survives in visible context; surrounding tool output compresses normally. (3) Added `allInProtectedZone` condition to `nothingToCompress` to cover the case where all messages are in the protected zone. (4) Fixed stale error messages and added empty-plan guard in both range and message modes. Dual-agent reviewed (both APPROVE with minor fixes; all WARNINGs addressed in follow-up commit). 922 tests pass.
536
+
537
+ Files: `lib/messages/inject/{utils,inject}.ts`, `lib/compress/{pipeline,protected-content,range,message,status}.ts`. Tests: `tests/{preserve-recent,soft-block,protected-tool-exclusion}.test.ts`. No persisted-state schema changes. `preserveLastUserMessage` config semantics changed from hard-reject to soft-filter (intentional fix; `lastSegmentSoftBlock: false` disables all protection including the new soft filter).
538
+
531
539
  ### v1.14.1 — Log Version Info + Growth Baseline Fix (PRs #205, #207)
532
540
 
533
541
  **Problem**: Two issues since v1.14.0. (1) **Unidentifiable logs** (PR #205): ACP daily logs and per-request context logs carried no version marker, so when users shared debug logs it was impossible to tell which ACP version produced them — making regression triage across versions guesswork. (2) **Growth-baseline feedback loop in short sessions** (PR #207): When the growth threshold was met (`nudgeAllowed`) but every compressible range was filtered out (`nothingToCompress`, e.g. all ranges inside the protected zone in a short session or subagent), the code reset `state.nudges.lastPerMessageNudgeTokens = currentTokens`. This ate the accumulated growth every cycle, so the baseline chased the current context and the model never saw a nudge even though context had grown well past the threshold.
package/README.zh-CN.md CHANGED
@@ -482,6 +482,14 @@ ACP 在首次启动时自动将配置从 `dcp.jsonc` 迁移到 `acp.jsonc`,将
482
482
 
483
483
  ## 更新日志
484
484
 
485
+ ### v1.14.2 — 拆分保护区范围 + 软化最后用户消息保护(PR #210)
486
+
487
+ **问题**:在自主代理会话中(1 条用户消息 + 多条 assistant/tool 消息),`buildCompressibleRanges` 创建了一个巨型可压缩组,因为分组只在用户消息处断开 —— 而 OpenCode 中 tool 结果是 `assistant` 角色。巨型组的 endRef 落在保护区内 → `excludeProtectedRanges` 移除整个范围 → 零推荐 → nudge 被抑制 → 模型永远无法压缩。此外,`preserveLastUserMessage` 硬拒绝任何覆盖最后用户消息的压缩调用,阻塞了对周围 tool 输出的有意压缩。
488
+
489
+ **修复**:(1) `buildCompressibleRanges` 现在接受 `protectedZoneRefs` 参数,在保护区边界处拆分组 —— 非保护区头部保留为推荐范围,保护区尾部被排除。(2) `preserveLastUserMessage` 从硬拒绝(`checkProtectedRange` 抛错)改为软过滤(`filterLastUserMessage` 从压缩计划中排除,遵循 Bug 39 的 `filterProtectedToolMessages` 模式)。最后用户消息保留在可见上下文中;周围的 tool 输出正常压缩。(3) 在 `nothingToCompress` 中添加了 `allInProtectedZone` 条件,覆盖所有消息都在保护区内的情况。(4) 修复了过时的错误消息,在 range 和 message 模式中都添加了空计划守卫。双 agent 审查通过(均 APPROVE WITH MINOR FIXES;所有 WARNING 在后续 commit 中解决)。922 项测试通过。
490
+
491
+ 文件:`lib/messages/inject/{utils,inject}.ts`、`lib/compress/{pipeline,protected-content,range,message,status}.ts`。测试:`tests/{preserve-recent,soft-block,protected-tool-exclusion}.test.ts`。无持久化状态 schema 变更。`preserveLastUserMessage` 配置语义从硬拒绝变为软过滤(有意的修复;`lastSegmentSoftBlock: false` 禁用所有保护包括新的软过滤)。
492
+
485
493
  ### v1.14.1 — 日志版本信息 + 增长基线修复(PR #205, #207)
486
494
 
487
495
  **问题**:v1.14.0 之后的两个问题。(1) **日志无法识别版本**(PR #205):ACP daily 日志和每请求 context 日志没有任何版本标记,用户分享调试日志时无法判断是哪个 ACP 版本产生的 —— 跨版本回归排查只能靠猜。(2) **短会话中的增长基线反馈循环**(PR #207):当增长阈值已满足(`nudgeAllowed`)但所有可压缩范围都被过滤掉时(`nothingToCompress`,例如短会话或子代理中所有范围都在保护区内),旧代码会重置 `state.nudges.lastPerMessageNudgeTokens = currentTokens`。这会每轮吃掉累积增长,基线一路追逐当前上下文,即使上下文早已远超阈值,模型也永远等不到 nudge。
package/dist/index.js CHANGED
@@ -3062,6 +3062,39 @@ function filterProtectedToolMessages(selection, searchContext, protectedTools, p
3062
3062
  toolIds: filteredToolIds
3063
3063
  };
3064
3064
  }
3065
+ function filterLastUserMessage(selection, searchContext, state, compress) {
3066
+ if (compress.lastSegmentSoftBlock === false) return selection;
3067
+ if (!(compress.preserveLastUserMessage ?? true)) return selection;
3068
+ let lastUserMessageId = null;
3069
+ for (let i = searchContext.rawMessages.length - 1; i >= 0; i--) {
3070
+ const msg = searchContext.rawMessages[i];
3071
+ const id = msg?.info?.id;
3072
+ if (!id || typeof id !== "string") continue;
3073
+ if (isSyntheticMessage(msg)) continue;
3074
+ if (isIgnoredUserMessage(msg)) continue;
3075
+ if (state.prune.messages.byMessageId.has(id)) continue;
3076
+ if (msg.info.role === "user") {
3077
+ lastUserMessageId = id;
3078
+ break;
3079
+ }
3080
+ }
3081
+ if (!lastUserMessageId || !selection.messageIds.includes(lastUserMessageId)) {
3082
+ return selection;
3083
+ }
3084
+ const filteredMessageIds = selection.messageIds.filter((id) => id !== lastUserMessageId);
3085
+ const filteredMessageTokenById = /* @__PURE__ */ new Map();
3086
+ for (const id of filteredMessageIds) {
3087
+ const tokens = selection.messageTokenById.get(id);
3088
+ if (tokens !== void 0) {
3089
+ filteredMessageTokenById.set(id, tokens);
3090
+ }
3091
+ }
3092
+ return {
3093
+ ...selection,
3094
+ messageIds: filteredMessageIds,
3095
+ messageTokenById: filteredMessageTokenById
3096
+ };
3097
+ }
3065
3098
 
3066
3099
  // lib/compress/state.ts
3067
3100
  var DEFAULT_PROMOTION_THRESHOLD = 5;
@@ -6399,7 +6432,6 @@ function checkPhantomBlock(state, plans) {
6399
6432
  function computeProtectedRawIds(rawMessages, state, compress) {
6400
6433
  const preserveN = compress.preserveRecentMessages ?? 20;
6401
6434
  const preserveTokens = compress.preserveRecentTokens ?? 2e4;
6402
- const preserveLastUser = compress.preserveLastUserMessage ?? true;
6403
6435
  const result = /* @__PURE__ */ new Set();
6404
6436
  const visible = [];
6405
6437
  for (const msg of rawMessages) {
@@ -6430,14 +6462,6 @@ function computeProtectedRawIds(rawMessages, state, compress) {
6430
6462
  tokenAccum += visible[i].tokens;
6431
6463
  }
6432
6464
  }
6433
- if (preserveLastUser) {
6434
- for (let i = visible.length - 1; i >= 0; i--) {
6435
- if (visible[i].isUser) {
6436
- result.add(visible[i].id);
6437
- break;
6438
- }
6439
- }
6440
- }
6441
6465
  return result;
6442
6466
  }
6443
6467
  function checkProtectedRange(ctx, allPlanMessageIds, rawMessages, dangerous) {
@@ -6460,7 +6484,7 @@ function checkProtectedRange(ctx, allPlanMessageIds, rawMessages, dangerous) {
6460
6484
  return new Error(
6461
6485
  `This range includes ${coveredProtected.length} protected recent message(s) (${sample}), which are likely still needed for the current task step.
6462
6486
 
6463
- Protected zone: last ${nMsgs} messages + last ${nToks >= 1e3 ? `${nToks / 1e3}K` : nToks} tokens + most recent user message.
6487
+ Protected zone: last ${nMsgs} messages + last ${nToks >= 1e3 ? `${nToks / 1e3}K` : nToks} tokens.
6464
6488
  If you are certain this content is genuinely consumed and must be compressed, re-issue the call with \`dangerous: true\`.
6465
6489
  Otherwise, compress older ranges that do not include the tail of the conversation.`
6466
6490
  );
@@ -6635,14 +6659,25 @@ function createCompressMessageTool(factoryCtx) {
6635
6659
  ctx.state,
6636
6660
  ctx.config
6637
6661
  );
6638
- if (plans.length === 0 && skippedCount > 0) {
6639
- throw new Error(formatIssues(skippedIssues, skippedCount));
6662
+ const filteredPlans = plans.map((plan) => ({
6663
+ ...plan,
6664
+ selection: filterLastUserMessage(
6665
+ plan.selection,
6666
+ searchContext,
6667
+ ctx.state,
6668
+ ctx.config.compress
6669
+ )
6670
+ })).filter((plan) => plan.selection.messageIds.length > 0);
6671
+ if (filteredPlans.length === 0) {
6672
+ throw new Error(
6673
+ skippedCount > 0 ? formatIssues(skippedIssues, skippedCount) : "All selected messages were filtered out (protected tool outputs and/or the last user message). They must remain in visible context."
6674
+ );
6640
6675
  }
6641
6676
  const minCompressRange = ctx.config.compress.minCompressRange;
6642
6677
  if (minCompressRange > 0) {
6643
6678
  let totalChars = 0;
6644
6679
  const counted = /* @__PURE__ */ new Set();
6645
- for (const plan of plans) {
6680
+ for (const plan of filteredPlans) {
6646
6681
  for (const messageId of plan.selection.messageIds) {
6647
6682
  if (counted.has(messageId)) continue;
6648
6683
  counted.add(messageId);
@@ -6661,14 +6696,14 @@ function createCompressMessageTool(factoryCtx) {
6661
6696
  const dangerous = args.dangerous === true;
6662
6697
  const lastSegmentError = checkProtectedRange(
6663
6698
  ctx,
6664
- plans.map((p) => p.selection.messageIds),
6699
+ filteredPlans.map((p) => p.selection.messageIds),
6665
6700
  rawMessages,
6666
6701
  dangerous
6667
6702
  );
6668
6703
  if (lastSegmentError) throw lastSegmentError;
6669
6704
  const notifications = [];
6670
6705
  const preparedPlans = [];
6671
- for (const plan of plans) {
6706
+ for (const plan of filteredPlans) {
6672
6707
  const summaryWithPromptInfo = appendProtectedPromptInfo(
6673
6708
  plan.entry.summary,
6674
6709
  plan.selection,
@@ -6778,7 +6813,7 @@ function createCompressMessageTool(factoryCtx) {
6778
6813
  ctx.state.qualityGateRetryPending = qualityGateRetryPendingBefore;
6779
6814
  throw error;
6780
6815
  }
6781
- return formatResult(plans.length, skippedIssues, skippedCount);
6816
+ return formatResult(filteredPlans.length, skippedIssues, skippedCount);
6782
6817
  }
6783
6818
  });
6784
6819
  }
@@ -6852,10 +6887,18 @@ function createCompressRangeTool(factoryCtx) {
6852
6887
  ctx.config.compress.protectedTools,
6853
6888
  ctx.config.protectedFilePatterns
6854
6889
  )
6890
+ })).map((plan) => ({
6891
+ ...plan,
6892
+ selection: filterLastUserMessage(
6893
+ plan.selection,
6894
+ searchContext,
6895
+ ctx.state,
6896
+ ctx.config.compress
6897
+ )
6855
6898
  })).filter((plan) => plan.selection.messageIds.length > 0);
6856
6899
  if (filteredPlans.length === 0) {
6857
6900
  throw new Error(
6858
- "All selected messages contain protected tool outputs and cannot be compressed. Protected tools (task, skill, todowrite, etc.) must remain in visible context."
6901
+ "All selected messages were filtered out (protected tool outputs and/or the last user message). They must remain in visible context."
6859
6902
  );
6860
6903
  }
6861
6904
  const minCompressRange = ctx.config.compress.minCompressRange;
@@ -8122,7 +8165,7 @@ function refNum(ref) {
8122
8165
  const n = parseInt(ref.slice(1), 10);
8123
8166
  return Number.isNaN(n) ? -1 : n;
8124
8167
  }
8125
- function buildCompressibleRanges(messages, state, protectedTools = [], protectedFilePatterns = []) {
8168
+ function buildCompressibleRanges(messages, state, protectedTools = [], protectedFilePatterns = [], protectedZoneRefs) {
8126
8169
  const msgInfo = [];
8127
8170
  const protectedMsgInfo = [];
8128
8171
  for (const msg of messages) {
@@ -8174,6 +8217,14 @@ function buildCompressibleRanges(messages, state, protectedTools = [], protected
8174
8217
  let cur = null;
8175
8218
  let prevRefNum = -2;
8176
8219
  for (const info of msgInfo) {
8220
+ if (protectedZoneRefs?.has(info.ref)) {
8221
+ if (cur) {
8222
+ groups.push(cur);
8223
+ cur = null;
8224
+ }
8225
+ prevRefNum = info.refNum;
8226
+ continue;
8227
+ }
8177
8228
  const hasGap = info.refNum > prevRefNum + 1;
8178
8229
  if (cur && (info.isUser && cur.count >= 3 || hasGap)) {
8179
8230
  groups.push(cur);
@@ -8380,7 +8431,6 @@ function computeProtectedRefs(messages, state, compress) {
8380
8431
  if (compress.lastSegmentSoftBlock === false) return /* @__PURE__ */ new Set();
8381
8432
  const preserveN = compress.preserveRecentMessages ?? 20;
8382
8433
  const preserveTokens = compress.preserveRecentTokens ?? 2e4;
8383
- const preserveLastUser = compress.preserveLastUserMessage ?? true;
8384
8434
  const result = /* @__PURE__ */ new Set();
8385
8435
  const visible = [];
8386
8436
  for (const msg of messages) {
@@ -8411,14 +8461,6 @@ function computeProtectedRefs(messages, state, compress) {
8411
8461
  tokenAccum += visible[i].tokens;
8412
8462
  }
8413
8463
  }
8414
- if (preserveLastUser) {
8415
- for (let i = visible.length - 1; i >= 0; i--) {
8416
- if (visible[i].isUser) {
8417
- result.add(visible[i].ref);
8418
- break;
8419
- }
8420
- }
8421
- }
8422
8464
  return result;
8423
8465
  }
8424
8466
  function excludeProtectedRanges(ranges, protectedRefs) {
@@ -8600,13 +8642,14 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
8600
8642
  config.compress.protectedTools,
8601
8643
  config.protectedFilePatterns
8602
8644
  );
8645
+ const protectedRefs = computeProtectedRefs(messages, state, config.compress);
8603
8646
  const contextRanges = buildCompressibleRanges(
8604
8647
  messages,
8605
8648
  state,
8606
8649
  config.compress.protectedTools,
8607
- config.protectedFilePatterns
8650
+ config.protectedFilePatterns,
8651
+ protectedRefs
8608
8652
  );
8609
- const protectedRefs = computeProtectedRefs(messages, state, config.compress);
8610
8653
  const unprotectedCompressible = excludeProtectedRanges(contextRanges.compressible, protectedRefs);
8611
8654
  const recommendedRanges = filterRecommendedRanges(
8612
8655
  unprotectedCompressible,
@@ -8635,7 +8678,8 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
8635
8678
  }
8636
8679
  const filterSuppressed = contextRanges.compressible.length > 0 && !hasRecommendations;
8637
8680
  const allProtected = contextRanges.compressible.length === 0 && contextRanges.protected.length > 0;
8638
- const nothingToCompress = filterSuppressed || allProtected;
8681
+ const allInProtectedZone = protectedRefs.size > 0 && unprotectedCompressible.length === 0;
8682
+ const nothingToCompress = filterSuppressed || allProtected || allInProtectedZone;
8639
8683
  let shouldInject = nudgeAllowed && (!nothingToCompress || emergencyOverride);
8640
8684
  if (nudgeAllowed && nothingToCompress && !emergencyOverride) {
8641
8685
  state.nudges.lastNudgeShownTokens = void 0;
@@ -9634,11 +9678,13 @@ function renderOverview(visibleMessages, summaryTokens, blocks, fetchFailed, raw
9634
9678
  const entry = pruneMap.get(msgId);
9635
9679
  return !entry || entry.activeBlockIds.length === 0;
9636
9680
  });
9681
+ const protectedRefs = ctx.config?.compress ? computeProtectedRefs(visibleRaw, ctx.state, ctx.config.compress) : /* @__PURE__ */ new Set();
9637
9682
  const contextRanges = buildCompressibleRanges(
9638
9683
  visibleRaw,
9639
9684
  ctx.state,
9640
9685
  ctx.config?.compress?.protectedTools ?? [],
9641
- ctx.config?.protectedFilePatterns ?? []
9686
+ ctx.config?.protectedFilePatterns ?? [],
9687
+ protectedRefs
9642
9688
  );
9643
9689
  if (contextRanges.compressible.length > 0 || contextRanges.protected.length > 0) {
9644
9690
  lines.push("");
@@ -9661,11 +9707,13 @@ function renderUncompressedRanges(rawMessages, ctx) {
9661
9707
  const entry = pruneMap.get(msgId);
9662
9708
  return !entry || entry.activeBlockIds.length === 0;
9663
9709
  });
9710
+ const protectedRefs = ctx.config?.compress ? computeProtectedRefs(visibleMessages, ctx.state, ctx.config.compress) : /* @__PURE__ */ new Set();
9664
9711
  const contextRanges = buildCompressibleRanges(
9665
9712
  visibleMessages,
9666
9713
  ctx.state,
9667
9714
  ctx.config?.compress?.protectedTools ?? [],
9668
- ctx.config?.protectedFilePatterns ?? []
9715
+ ctx.config?.protectedFilePatterns ?? [],
9716
+ protectedRefs
9669
9717
  );
9670
9718
  const compressible = contextRanges.compressible;
9671
9719
  const totalTokens = compressible.reduce((s, r) => s + r.tokens, 0);
@@ -10022,7 +10070,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
10022
10070
  import { join as join3 } from "path";
10023
10071
  import { existsSync as existsSync3 } from "fs";
10024
10072
  import { homedir as homedir3 } from "os";
10025
- var LOG_VERSION = true ? "1.14.1" : "dev";
10073
+ var LOG_VERSION = true ? "1.14.2" : "dev";
10026
10074
  var Logger = class {
10027
10075
  logDir;
10028
10076
  enabled;