opencode-acp 1.14.2 → 1.14.3

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.3 — Soften Protected Zone + Reduce Defaults (PR #212)
532
+
533
+ **Problem**: `checkProtectedRange` hard-rejected any compress call covering protected recent messages (last N messages + last N tokens). The model got an error and had to retry with a different range or use `dangerous: true`. Additionally, the default `preserveRecentMessages: 20` and `preserveRecentTokens: 20000` (≈40 messages) were too aggressive — protecting nearly half the conversation in autonomous sessions.
534
+
535
+ **Fix**: (1) Converted `checkProtectedRange` hard-reject to `filterProtectedRecentMessages` soft-filter — protected messages are filtered from the compress plan (same pattern as `filterLastUserMessage` and `filterProtectedToolMessages`), non-protected messages compress normally. Compress always succeeds unless ALL messages are protected. (2) Reduced `preserveRecentMessages` default 20 → 5 and `preserveRecentTokens` default 20000 → 5000. (3) `dangerous` parameter is now a no-op (no hard-reject to bypass; stays in schema for backward compat). 922 tests pass.
536
+
537
+ Files: `lib/config.ts`, `lib/compress/{protected-content,pipeline,range,message}.ts`, `lib/messages/inject/utils.ts`. Tests: `tests/soft-block.test.ts`. No persisted-state schema changes. Config defaults changed; existing configs with explicit values are unaffected.
538
+
531
539
  ### v1.14.2 — Split Protected Ranges + Soften Last-User-Message (PR #210)
532
540
 
533
541
  **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.
package/README.zh-CN.md CHANGED
@@ -482,6 +482,14 @@ ACP 在首次启动时自动将配置从 `dcp.jsonc` 迁移到 `acp.jsonc`,将
482
482
 
483
483
  ## 更新日志
484
484
 
485
+ ### v1.14.3 — 软化保护区 + 减小默认值(PR #212)
486
+
487
+ **问题**:`checkProtectedRange` 硬拒绝任何覆盖保护区最近消息的压缩调用(最后 N 条消息 + 最后 N tokens)。模型收到错误后必须换范围重试或使用 `dangerous: true`。此外,默认 `preserveRecentMessages: 20` 和 `preserveRecentTokens: 20000`(约 40 条消息)过于激进 —— 在自主会话中保护了近一半的对话。
488
+
489
+ **修复**:(1) 将 `checkProtectedRange` 硬拒绝转为 `filterProtectedRecentMessages` 软过滤 —— 保护区消息从压缩计划中过滤掉(与 `filterLastUserMessage` 和 `filterProtectedToolMessages` 同模式),非保护区消息正常压缩。除非所有消息都在保护区内,压缩总能成功。(2) 减小 `preserveRecentMessages` 默认值 20 → 5,`preserveRecentTokens` 默认值 20000 → 5000。(3) `dangerous` 参数现在无实际效果(没有硬拒绝了,不需要 bypass;保留在 schema 中向后兼容)。922 项测试通过。
490
+
491
+ 文件:`lib/config.ts`、`lib/compress/{protected-content,pipeline,range,message}.ts`、`lib/messages/inject/utils.ts`。测试:`tests/soft-block.test.ts`。无持久化状态 schema 变更。配置默认值改变;已设置显式值的配置不受影响。
492
+
485
493
  ### v1.14.2 — 拆分保护区范围 + 软化最后用户消息保护(PR #210)
486
494
 
487
495
  **问题**:在自主代理会话中(1 条用户消息 + 多条 assistant/tool 消息),`buildCompressibleRanges` 创建了一个巨型可压缩组,因为分组只在用户消息处断开 —— 而 OpenCode 中 tool 结果是 `assistant` 角色。巨型组的 endRef 落在保护区内 → `excludeProtectedRanges` 移除整个范围 → 零推荐 → nudge 被抑制 → 模型永远无法压缩。此外,`preserveLastUserMessage` 硬拒绝任何覆盖最后用户消息的压缩调用,阻塞了对周围 tool 输出的有意压缩。
package/dist/index.js CHANGED
@@ -1648,8 +1648,8 @@ var defaultConfig = {
1648
1648
  maxVisibleSegments: 50,
1649
1649
  keepEmbedMaxChars: 2e3,
1650
1650
  lastSegmentSoftBlock: true,
1651
- preserveRecentMessages: 20,
1652
- preserveRecentTokens: 2e4,
1651
+ preserveRecentMessages: 5,
1652
+ preserveRecentTokens: 5e3,
1653
1653
  preserveLastUserMessage: true
1654
1654
  },
1655
1655
  strategies: {
@@ -3095,6 +3095,58 @@ function filterLastUserMessage(selection, searchContext, state, compress) {
3095
3095
  messageTokenById: filteredMessageTokenById
3096
3096
  };
3097
3097
  }
3098
+ function filterProtectedRecentMessages(selection, searchContext, state, compress) {
3099
+ if (compress.lastSegmentSoftBlock === false) return selection;
3100
+ const preserveN = compress.preserveRecentMessages ?? 5;
3101
+ const preserveTokens = compress.preserveRecentTokens ?? 5e3;
3102
+ if (preserveN <= 0 && preserveTokens <= 0) return selection;
3103
+ const protectedIds = /* @__PURE__ */ new Set();
3104
+ const visible = [];
3105
+ for (const msg of searchContext.rawMessages) {
3106
+ const id = msg?.info?.id;
3107
+ if (!id || typeof id !== "string") continue;
3108
+ if (isSyntheticMessage(msg)) continue;
3109
+ if (isIgnoredUserMessage(msg)) continue;
3110
+ if (state.prune.messages.byMessageId.has(id)) continue;
3111
+ let tokens = 0;
3112
+ for (const part of msg.parts || []) {
3113
+ if (part.type === "text" && typeof part.text === "string") {
3114
+ tokens += Math.round(part.text.length / 4);
3115
+ } else if (part.type !== "text" && part.type !== "reasoning") {
3116
+ tokens += Math.round(JSON.stringify(part).length / 4);
3117
+ }
3118
+ }
3119
+ visible.push({ id, tokens });
3120
+ }
3121
+ if (preserveN > 0) {
3122
+ for (const m of visible.slice(-preserveN)) {
3123
+ protectedIds.add(m.id);
3124
+ }
3125
+ }
3126
+ if (preserveTokens > 0) {
3127
+ let tokenAccum = 0;
3128
+ for (let i = visible.length - 1; i >= 0 && tokenAccum < preserveTokens; i--) {
3129
+ protectedIds.add(visible[i].id);
3130
+ tokenAccum += visible[i].tokens;
3131
+ }
3132
+ }
3133
+ if (protectedIds.size === 0) return selection;
3134
+ const hasProtected = selection.messageIds.some((id) => protectedIds.has(id));
3135
+ if (!hasProtected) return selection;
3136
+ const filteredMessageIds = selection.messageIds.filter((id) => !protectedIds.has(id));
3137
+ const filteredMessageTokenById = /* @__PURE__ */ new Map();
3138
+ for (const id of filteredMessageIds) {
3139
+ const tokens = selection.messageTokenById.get(id);
3140
+ if (tokens !== void 0) {
3141
+ filteredMessageTokenById.set(id, tokens);
3142
+ }
3143
+ }
3144
+ return {
3145
+ ...selection,
3146
+ messageIds: filteredMessageIds,
3147
+ messageTokenById: filteredMessageTokenById
3148
+ };
3149
+ }
3098
3150
 
3099
3151
  // lib/compress/state.ts
3100
3152
  var DEFAULT_PROMOTION_THRESHOLD = 5;
@@ -6429,66 +6481,6 @@ function checkPhantomBlock(state, plans) {
6429
6481
  }
6430
6482
  return null;
6431
6483
  }
6432
- function computeProtectedRawIds(rawMessages, state, compress) {
6433
- const preserveN = compress.preserveRecentMessages ?? 20;
6434
- const preserveTokens = compress.preserveRecentTokens ?? 2e4;
6435
- const result = /* @__PURE__ */ new Set();
6436
- const visible = [];
6437
- for (const msg of rawMessages) {
6438
- const id = msg?.info?.id;
6439
- if (!id || typeof id !== "string") continue;
6440
- if (isSyntheticMessage(msg)) continue;
6441
- if (isIgnoredUserMessage(msg)) continue;
6442
- if (state.prune.messages.byMessageId.has(id)) continue;
6443
- let tokens = 0;
6444
- for (const part of msg.parts || []) {
6445
- if (part.type === "text" && typeof part.text === "string") {
6446
- tokens += Math.round(part.text.length / 4);
6447
- } else if (part.type !== "text" && part.type !== "reasoning") {
6448
- tokens += Math.round(JSON.stringify(part).length / 4);
6449
- }
6450
- }
6451
- visible.push({ id, tokens, isUser: msg.info.role === "user" });
6452
- }
6453
- if (preserveN > 0) {
6454
- for (const m of visible.slice(-preserveN)) {
6455
- result.add(m.id);
6456
- }
6457
- }
6458
- if (preserveTokens > 0) {
6459
- let tokenAccum = 0;
6460
- for (let i = visible.length - 1; i >= 0 && tokenAccum < preserveTokens; i--) {
6461
- result.add(visible[i].id);
6462
- tokenAccum += visible[i].tokens;
6463
- }
6464
- }
6465
- return result;
6466
- }
6467
- function checkProtectedRange(ctx, allPlanMessageIds, rawMessages, dangerous) {
6468
- if (ctx.config.compress.lastSegmentSoftBlock === false) return null;
6469
- const protectedIds = computeProtectedRawIds(rawMessages, ctx.state, ctx.config.compress);
6470
- if (protectedIds.size === 0) return null;
6471
- const coveredProtected = [];
6472
- for (const ids of allPlanMessageIds) {
6473
- for (const id of ids) {
6474
- if (protectedIds.has(id)) {
6475
- coveredProtected.push(id);
6476
- }
6477
- }
6478
- }
6479
- if (coveredProtected.length === 0) return null;
6480
- if (dangerous) return null;
6481
- const sample = coveredProtected.slice(0, 3).join(", ");
6482
- const nMsgs = ctx.config.compress.preserveRecentMessages ?? 20;
6483
- const nToks = ctx.config.compress.preserveRecentTokens ?? 2e4;
6484
- return new Error(
6485
- `This range includes ${coveredProtected.length} protected recent message(s) (${sample}), which are likely still needed for the current task step.
6486
-
6487
- Protected zone: last ${nMsgs} messages + last ${nToks >= 1e3 ? `${nToks / 1e3}K` : nToks} tokens.
6488
- If you are certain this content is genuinely consumed and must be compressed, re-issue the call with \`dangerous: true\`.
6489
- Otherwise, compress older ranges that do not include the tail of the conversation.`
6490
- );
6491
- }
6492
6484
 
6493
6485
  // lib/compress/keep-markers.ts
6494
6486
  var KEEP_REGEX = /\[\[KEEP:(m\d+)\]\]/g;
@@ -6667,6 +6659,14 @@ function createCompressMessageTool(factoryCtx) {
6667
6659
  ctx.state,
6668
6660
  ctx.config.compress
6669
6661
  )
6662
+ })).map((plan) => ({
6663
+ ...plan,
6664
+ selection: filterProtectedRecentMessages(
6665
+ plan.selection,
6666
+ searchContext,
6667
+ ctx.state,
6668
+ ctx.config.compress
6669
+ )
6670
6670
  })).filter((plan) => plan.selection.messageIds.length > 0);
6671
6671
  if (filteredPlans.length === 0) {
6672
6672
  throw new Error(
@@ -6693,14 +6693,6 @@ function createCompressMessageTool(factoryCtx) {
6693
6693
  );
6694
6694
  }
6695
6695
  }
6696
- const dangerous = args.dangerous === true;
6697
- const lastSegmentError = checkProtectedRange(
6698
- ctx,
6699
- filteredPlans.map((p) => p.selection.messageIds),
6700
- rawMessages,
6701
- dangerous
6702
- );
6703
- if (lastSegmentError) throw lastSegmentError;
6704
6696
  const notifications = [];
6705
6697
  const preparedPlans = [];
6706
6698
  for (const plan of filteredPlans) {
@@ -6895,6 +6887,14 @@ function createCompressRangeTool(factoryCtx) {
6895
6887
  ctx.state,
6896
6888
  ctx.config.compress
6897
6889
  )
6890
+ })).map((plan) => ({
6891
+ ...plan,
6892
+ selection: filterProtectedRecentMessages(
6893
+ plan.selection,
6894
+ searchContext,
6895
+ ctx.state,
6896
+ ctx.config.compress
6897
+ )
6898
6898
  })).filter((plan) => plan.selection.messageIds.length > 0);
6899
6899
  if (filteredPlans.length === 0) {
6900
6900
  throw new Error(
@@ -6921,14 +6921,6 @@ function createCompressRangeTool(factoryCtx) {
6921
6921
  );
6922
6922
  }
6923
6923
  }
6924
- const dangerous = args.dangerous === true;
6925
- const lastSegmentError = checkProtectedRange(
6926
- ctx,
6927
- filteredPlans.map((p) => p.selection.messageIds),
6928
- rawMessages,
6929
- dangerous
6930
- );
6931
- if (lastSegmentError) throw lastSegmentError;
6932
6924
  const notifications = [];
6933
6925
  const preparedPlans = [];
6934
6926
  let totalCompressedMessages = 0;
@@ -8429,8 +8421,8 @@ ${lines.join("\n")}`;
8429
8421
  }
8430
8422
  function computeProtectedRefs(messages, state, compress) {
8431
8423
  if (compress.lastSegmentSoftBlock === false) return /* @__PURE__ */ new Set();
8432
- const preserveN = compress.preserveRecentMessages ?? 20;
8433
- const preserveTokens = compress.preserveRecentTokens ?? 2e4;
8424
+ const preserveN = compress.preserveRecentMessages ?? 5;
8425
+ const preserveTokens = compress.preserveRecentTokens ?? 5e3;
8434
8426
  const result = /* @__PURE__ */ new Set();
8435
8427
  const visible = [];
8436
8428
  for (const msg of messages) {
@@ -10070,7 +10062,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
10070
10062
  import { join as join3 } from "path";
10071
10063
  import { existsSync as existsSync3 } from "fs";
10072
10064
  import { homedir as homedir3 } from "os";
10073
- var LOG_VERSION = true ? "1.14.2" : "dev";
10065
+ var LOG_VERSION = true ? "1.14.3" : "dev";
10074
10066
  var Logger = class {
10075
10067
  logDir;
10076
10068
  enabled;