opencode-acp 1.14.8-dev.1 → 1.14.8-dev.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
@@ -492,6 +492,25 @@ For the complete list with root cause analysis, see the [bug tracker](https://gi
492
492
 
493
493
  ## Changelog
494
494
 
495
+ ### v1.14.8-dev.3 — Dev Prerelease (1 PR since v1.14.8-dev.2)
496
+
497
+ Dev prerelease covering PR #248. Published to `dev` npm tag for early testing.
498
+
499
+ **PRs included**:
500
+ - **#248** — Fix: auto-extend compression ranges to prevent splitting tool_use/tool_result pairs. When the model selects a compression boundary that falls between a tool call and its result, the orphaned tool_result references a non-existent tool_use_id → API rejection. New `adjustBoundariesForToolPairs` in `compress/search.ts` scans forward/backward (up to 20 messages) to include the matching pair. Excludes `compress` tool (force-protected) and only extends message boundaries (never block boundaries) to avoid tier misclassification. 11 tests.
501
+
502
+ **Install**: `opencode plugin opencode-acp@dev --global`
503
+
504
+ ### v1.14.8-dev.2 — Dev Prerelease (2 PRs since v1.14.8-dev.1)
505
+
506
+ Dev prerelease covering PRs #244–#245. Published to `dev` npm tag for early testing.
507
+
508
+ **PRs included**:
509
+ - **#245** — Fix: re-add HOW_TO_COMPRESS_RULES to nudge injection for high-attention summary guidance. v1.14.7 over-removed the rules from all 4 nudge locations (breakdown + 3 templates), leaving them only in system prompt. In long sessions, system prompt rules degrade due to "lost in the middle" effect — model needs rules at high attention (end of context) when nudge fires.
510
+ - **#244** — Fix: acp_status hides consumed compress calls from PROTECTED list.
511
+
512
+ **Install**: `opencode plugin opencode-acp@dev --global`
513
+
495
514
  ### v1.14.8-dev.1 — Dev Prerelease (7 PRs since v1.14.7)
496
515
 
497
516
  Dev prerelease covering PRs #238–#242. Published to `dev` npm tag for early testing.
package/README.zh-CN.md CHANGED
@@ -446,6 +446,25 @@ ACP 是 DCP 的直接替代品。迁移步骤:
446
446
 
447
447
  ## 更新日志
448
448
 
449
+ ### v1.14.8-dev.3 — Dev 预发布(v1.14.8-dev.2 以来 1 个 PR)
450
+
451
+ Dev 预发布,涵盖 PR #248。发布到 `dev` npm tag 供早期测试。
452
+
453
+ **包含的 PR**:
454
+ - **#248** — 修复:自动扩展压缩范围以防止拆分 tool_use/tool_result 配对。当模型选择的压缩边界落在 tool 调用与其结果之间时,孤立的 tool_result 引用不存在的 tool_use_id → API 拒绝。新增 `adjustBoundariesForToolPairs`(`compress/search.ts`)向前/向后扫描(最多 20 条消息)以包含匹配的配对。排除 `compress` 工具(强制保护)并仅扩展消息边界(从不扩展 block 边界)以避免层级错误分类。11 个测试。
455
+
456
+ **安装**:`opencode plugin opencode-acp@dev --global`
457
+
458
+ ### v1.14.8-dev.2 — Dev 预发布(v1.14.8-dev.1 以来 2 个 PR)
459
+
460
+ Dev 预发布,涵盖 PR #244–#245。发布到 `dev` npm tag 供早期测试。
461
+
462
+ **包含的 PR**:
463
+ - **#245** — 修复:重新将 HOW_TO_COMPRESS_RULES 加入 nudge 注入。v1.14.7 过度删除了所有 4 处 nudge 位置的规则(breakdown + 3 个模板),只保留在 system prompt。在长 session 中,system prompt 的规则因"lost in the middle"效应衰减——模型在 nudge 触发时需要规则在高注意力区(上下文末尾)。
464
+ - **#244** — 修复:acp_status 隐藏 PROTECTED 列表中已消耗的 compress 调用。
465
+
466
+ **安装**:`opencode plugin opencode-acp@dev --global`
467
+
449
468
  ### v1.14.8-dev.1 — Dev 预发布(v1.14.7 以来 7 个 PR)
450
469
 
451
470
  Dev 预发布,涵盖 PR #238–#242。发布到 `dev` npm tag 供早期测试。
package/dist/index.js CHANGED
@@ -3040,8 +3040,79 @@ ${hint}` : body);
3040
3040
  if (startReference.rawIndex > endReference.rawIndex) {
3041
3041
  [startReference, endReference] = [endReference, startReference];
3042
3042
  }
3043
+ const adjusted = adjustBoundariesForToolPairs(
3044
+ context,
3045
+ startReference.rawIndex,
3046
+ endReference.rawIndex
3047
+ );
3048
+ if (adjusted.startIndex < startReference.rawIndex && startReference.kind === "message") {
3049
+ const msg = context.rawMessages[adjusted.startIndex];
3050
+ if (msg) {
3051
+ startReference = {
3052
+ kind: "message",
3053
+ rawIndex: adjusted.startIndex,
3054
+ messageId: msg.info.id
3055
+ };
3056
+ }
3057
+ }
3058
+ if (adjusted.endIndex > endReference.rawIndex && endReference.kind === "message") {
3059
+ const msg = context.rawMessages[adjusted.endIndex];
3060
+ if (msg) {
3061
+ endReference = {
3062
+ kind: "message",
3063
+ rawIndex: adjusted.endIndex,
3064
+ messageId: msg.info.id
3065
+ };
3066
+ }
3067
+ }
3043
3068
  return { startReference, endReference };
3044
3069
  }
3070
+ function adjustBoundariesForToolPairs(context, startIdx, endIdx) {
3071
+ const messages = context.rawMessages;
3072
+ const callIdsInRange = /* @__PURE__ */ new Set();
3073
+ for (let i = startIdx; i <= endIdx; i++) {
3074
+ const msg = messages[i];
3075
+ if (!msg) continue;
3076
+ const parts = Array.isArray(msg.parts) ? msg.parts : [];
3077
+ for (const part of parts) {
3078
+ if (part.type !== "tool" || !part.callID) continue;
3079
+ if (part.tool === "compress") continue;
3080
+ callIdsInRange.add(part.callID);
3081
+ }
3082
+ }
3083
+ if (callIdsInRange.size === 0) {
3084
+ return { startIndex: startIdx, endIndex: endIdx };
3085
+ }
3086
+ let newEndIdx = endIdx;
3087
+ for (let i = endIdx + 1; i < messages.length && i <= endIdx + 20; i++) {
3088
+ const msg = messages[i];
3089
+ if (!msg) break;
3090
+ const parts = Array.isArray(msg.parts) ? msg.parts : [];
3091
+ const hasMatch = parts.some(
3092
+ (part) => part.type === "tool" && part.callID && callIdsInRange.has(part.callID)
3093
+ );
3094
+ if (hasMatch) {
3095
+ newEndIdx = i;
3096
+ } else if (newEndIdx > endIdx) {
3097
+ break;
3098
+ }
3099
+ }
3100
+ let newStartIdx = startIdx;
3101
+ for (let i = startIdx - 1; i >= 0 && i >= startIdx - 20; i--) {
3102
+ const msg = messages[i];
3103
+ if (!msg) break;
3104
+ const parts = Array.isArray(msg.parts) ? msg.parts : [];
3105
+ const hasMatch = parts.some(
3106
+ (part) => part.type === "tool" && part.callID && callIdsInRange.has(part.callID)
3107
+ );
3108
+ if (hasMatch) {
3109
+ newStartIdx = i;
3110
+ } else if (newStartIdx < startIdx) {
3111
+ break;
3112
+ }
3113
+ }
3114
+ return { startIndex: newStartIdx, endIndex: newEndIdx };
3115
+ }
3045
3116
  function buildBoundaryRecoveryHint(context, state) {
3046
3117
  const visibleRefs = [];
3047
3118
  for (const [messageRef, messageId] of state.messageIds.byRef) {
@@ -7609,6 +7680,8 @@ Breakdown: ${sysPart}${fmt(composition.toolTokens)} tool (${pct2(composition.too
7609
7680
  if (recommendedRanges.length > 0) {
7610
7681
  breakdown += `
7611
7682
 
7683
+ ${HOW_TO_COMPRESS_RULES}
7684
+
7612
7685
  ${formatCompressibleRanges(recommendedRanges, contextRanges.protected)}`;
7613
7686
  breakdown += `
7614
7687
  \u{1F4A1} Compress all ranges in one call (pass multiple content entries: \`content: [{...}, {...}]\`).`;
@@ -7618,7 +7691,7 @@ Use \`acp_status({scope:"uncompressed"})\` to re-fetch compressible ranges after
7618
7691
  appendToLastTextPart(suffixMessage, breakdown);
7619
7692
  }
7620
7693
  if (effectiveTipsVariant === "maxLimit") {
7621
- tipsText = '\n\n\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.\n\n{ "topic": "...", "content": [{ "startId": "<ID>", "endId": "<ID>", "summary": "..." }] }\n\nOnly use IDs from visible messages above. Compress older work first.';
7694
+ tipsText = "\n\n\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.\n\n" + HOW_TO_COMPRESS_RULES + '\n\n{ "topic": "...", "content": [{ "startId": "<ID>", "endId": "<ID>", "summary": "..." }] }\n\nOnly use IDs from visible messages above. Compress older work first.';
7622
7695
  }
7623
7696
  state.nudges.lastNudgeShownTokens = currentTokens;
7624
7697
  {
@@ -8258,6 +8331,64 @@ function createDecompressTool(factoryCtx) {
8258
8331
 
8259
8332
  // lib/compress/status.ts
8260
8333
  import { tool as tool4 } from "@opencode-ai/plugin";
8334
+
8335
+ // lib/compress/parts.ts
8336
+ var STRUCTURAL_PART_TYPES = /* @__PURE__ */ new Set(["step-start", "step-finish", "reasoning"]);
8337
+ function hasMeaningfulContent(parts) {
8338
+ return parts.some((p) => !STRUCTURAL_PART_TYPES.has(p.type));
8339
+ }
8340
+
8341
+ // lib/compress/hide-consumed.ts
8342
+ var KEEP_LAST_ORPHANED = 2;
8343
+ function hideConsumedCompressCalls(state, messages) {
8344
+ const activeCallIds = /* @__PURE__ */ new Set();
8345
+ const allBlockCallIds = /* @__PURE__ */ new Set();
8346
+ for (const block of state.prune.messages.blocksById.values()) {
8347
+ if (block.compressCallId) {
8348
+ allBlockCallIds.add(block.compressCallId);
8349
+ if (block.active && !block.deactivatedByUser && !block.deactivatedByUserDeep) {
8350
+ activeCallIds.add(block.compressCallId);
8351
+ }
8352
+ }
8353
+ }
8354
+ const lastOrphanedCallIds = [];
8355
+ for (let i = messages.length - 1; i >= 0 && lastOrphanedCallIds.length < KEEP_LAST_ORPHANED; i--) {
8356
+ const parts = Array.isArray(messages[i]?.parts) ? messages[i].parts : [];
8357
+ for (let j = parts.length - 1; j >= 0 && lastOrphanedCallIds.length < KEEP_LAST_ORPHANED; j--) {
8358
+ const p = parts[j];
8359
+ if (p.type === "tool" && p.tool === "compress" && p.callID && !allBlockCallIds.has(p.callID)) {
8360
+ lastOrphanedCallIds.push(p.callID);
8361
+ }
8362
+ }
8363
+ }
8364
+ const keepCallIds = /* @__PURE__ */ new Set([...activeCallIds, ...lastOrphanedCallIds]);
8365
+ let hidden = 0;
8366
+ for (let i = 0; i < messages.length; i++) {
8367
+ const msg = messages[i];
8368
+ const parts = Array.isArray(msg.parts) ? msg.parts : [];
8369
+ let changed = false;
8370
+ const remaining = parts.filter((p) => {
8371
+ if (p.type === "tool" && p.tool === "compress") {
8372
+ if (p.callID && keepCallIds.has(p.callID)) return true;
8373
+ hidden++;
8374
+ changed = true;
8375
+ return false;
8376
+ }
8377
+ return true;
8378
+ });
8379
+ if (changed) {
8380
+ if (hasMeaningfulContent(remaining)) {
8381
+ messages[i] = { ...msg, parts: remaining };
8382
+ } else {
8383
+ messages.splice(i, 1);
8384
+ i--;
8385
+ }
8386
+ }
8387
+ }
8388
+ return hidden;
8389
+ }
8390
+
8391
+ // lib/compress/status.ts
8261
8392
  var ACP_STATUS_TOOL_DESCRIPTION = `Show context status \u2014 overview includes compressible ranges by default.
8262
8393
 
8263
8394
  No args: Overview with totals, compressed blocks, and compressible ranges.
@@ -8648,6 +8779,7 @@ function createAcpStatusTool(factoryCtx) {
8648
8779
  if (scope === "uncompressed") return "(unable to fetch messages)";
8649
8780
  rawMessages = [];
8650
8781
  }
8782
+ hideConsumedCompressCalls(ctx.state, rawMessages);
8651
8783
  return buildStatusReport(
8652
8784
  { state: ctx.state, config: ctx.config },
8653
8785
  rawMessages,
@@ -8714,55 +8846,6 @@ Call with blockId to get the full summary: acp_context_recap({ blockId: N })`);
8714
8846
  });
8715
8847
  }
8716
8848
 
8717
- // lib/compress/parts.ts
8718
- var STRUCTURAL_PART_TYPES = /* @__PURE__ */ new Set(["step-start", "step-finish", "reasoning"]);
8719
- function hasMeaningfulContent(parts) {
8720
- return parts.some((p) => !STRUCTURAL_PART_TYPES.has(p.type));
8721
- }
8722
-
8723
- // lib/compress/hide-consumed.ts
8724
- function hideConsumedCompressCalls(state, messages) {
8725
- if (state.prune.messages.blocksById.size === 0) {
8726
- return 0;
8727
- }
8728
- const consumedMessageIds = /* @__PURE__ */ new Set();
8729
- for (const block of state.prune.messages.blocksById.values()) {
8730
- if (block.active) continue;
8731
- if (block.deactivatedByUser) continue;
8732
- if (block.deactivatedByUserDeep) continue;
8733
- if (block.deactivatedByBlockId === void 0) continue;
8734
- if (!block.compressMessageId) continue;
8735
- consumedMessageIds.add(block.compressMessageId);
8736
- }
8737
- if (consumedMessageIds.size === 0) {
8738
- return 0;
8739
- }
8740
- let hidden = 0;
8741
- for (let i = 0; i < messages.length; i++) {
8742
- const msg = messages[i];
8743
- if (!consumedMessageIds.has(msg.info.id)) continue;
8744
- const parts = Array.isArray(msg.parts) ? msg.parts : [];
8745
- let changed = false;
8746
- const remaining = parts.filter((p) => {
8747
- if (p.type === "tool" && p.tool === "compress") {
8748
- hidden++;
8749
- changed = true;
8750
- return false;
8751
- }
8752
- return true;
8753
- });
8754
- if (changed) {
8755
- if (hasMeaningfulContent(remaining)) {
8756
- messages[i] = { ...msg, parts: remaining };
8757
- } else {
8758
- messages.splice(i, 1);
8759
- i--;
8760
- }
8761
- }
8762
- }
8763
- return hidden;
8764
- }
8765
-
8766
8849
  // lib/compress/hide-failed.ts
8767
8850
  function hideFailedCompressCalls(messages) {
8768
8851
  let mostRecentFailedId;
@@ -8809,7 +8892,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
8809
8892
  import { join as join3 } from "path";
8810
8893
  import { existsSync as existsSync3 } from "fs";
8811
8894
  import { homedir as homedir3 } from "os";
8812
- var LOG_VERSION = true ? "1.14.8-dev.1" : "dev";
8895
+ var LOG_VERSION = true ? "1.14.8-dev.3" : "dev";
8813
8896
  var Logger = class {
8814
8897
  logDir;
8815
8898
  enabled;