opencode-acp 1.14.13-dev.1 → 1.14.14-dev.1

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
@@ -435,7 +435,7 @@ For the `compress` tool, `compress.protectedTools` ensures specific tool outputs
435
435
  ACP is a drop-in replacement for DCP. To migrate:
436
436
 
437
437
  1. Remove the old DCP plugin from your `opencode.json`
438
- 2. Install ACP: `opencode plugin install opencode-acp@stable --global`
438
+ 2. Install ACP: `opencode plugin opencode-acp@stable --global`
439
439
  3. Copy your config: `cp ~/.config/opencode/dcp.jsonc ~/.config/opencode/acp.jsonc`
440
440
  4. Copy prompt overrides (if any): `cp -r ~/.config/opencode/dcp-prompts ~/.config/opencode/acp-prompts`
441
441
  5. Copy session state (optional, preserves compression blocks): `cp -r ~/.local/share/opencode/storage/plugin/dcp ~/.local/share/opencode/storage/plugin/acp`
@@ -492,6 +492,26 @@ For the complete list with root cause analysis, see the [bug tracker](https://gi
492
492
 
493
493
  ## Changelog
494
494
 
495
+ ### v1.14.14-dev.1 — Dev Prerelease (2 PRs since v1.14.13)
496
+
497
+ Dev prerelease covering two compress-subsystem fixes: the batched-call summary leak (#288) and the batch-compress all-or-nothing abort (#290). Published to the `dev` npm tag for early testing.
498
+
499
+ **PRs included**:
500
+ - **#288** (via #289) — `hideConsumedCompressCalls` keyed its keep-set on `compressCallId`, which is 1:N under batched compress (one tool call, multiple `content[]` entries share one callId). When a T2 distillation consumed only some siblings, a surviving sibling rescued the whole tool part, permanently leaking the consumed summaries — unreclaimable because compress is a default protected tool. Fix: key visibility per-block (on `startId::endId`) instead of per-callId; for kept batches with mixed liveness, rewrite the surviving tool part's `state.input.content` to drop consumed entries' summaries. All-consumed batches still fully removed. Files: `lib/compress/hide-consumed.ts`. Tests: +3 in `tests/hide-consumed.test.ts` (core regression verified to FAIL pre-fix).
501
+ - **#290** (via #291) — Batch `compress` aborted entirely when any single `content[]` entry contained only already-compressed messages (`Compression range N contains only already-compressed messages`), leaving valid entries unexecuted; the error also omitted which entry/IDs/blocks conflicted, forcing trial-and-error retries. Fix: (A) partial-failure batches — new `identifyPhantomPlans` drops phantom entries and compresses the rest, returning a concise skip notice; (C) detailed diagnostics on the all-phantom throw (entry index + consumed IDs + owning block); (B) a clamp warning when `clampMessageRef` silently shifts a boundary. Extracted `partitionPhantomPlans` + `buildPhantomSkipNotice` pure helpers for testable batch decision logic. `checkPhantomBlock` keeps its exact legacy contract. Files: `lib/compress/{pipeline,range,search,range-utils}.ts`. Tests: +19. Dual-agent reviewed (Oracle + General, both APPROVE).
502
+
503
+ **Install**: `opencode plugin opencode-acp@dev --global`
504
+
505
+ ### v1.14.13 — Stable Release (3 PRs since v1.14.12)
506
+
507
+ Stable release with `allowSubAgents` promoted to a top-level config field (default: `true`), plus config documentation updates.
508
+
509
+ **PRs included**:
510
+ - **#276** — Promote `allowSubAgents` from `experimental` to top-level config field, change default from `false` to `true`. Subagent sessions now get compression by default. Fully backward compatible: old `experimental.allowSubAgents` configs still work (top-level takes priority). Updated all 4 hook sites, schema, config validation, and 6 documentation files. Dual-agent reviewed (Oracle + Explore, both APPROVE).
511
+ - **#280, #281** — Update recommended config in documentation: `maxContextLimit: 70%`, `minContextLimit: 70%`, simplified `protectedTools`.
512
+
513
+ **Install**: `opencode plugin opencode-acp@stable --global`
514
+
495
515
  ### v1.14.13-dev.1 — Dev Prerelease (1 PR since v1.14.12)
496
516
 
497
517
  Dev prerelease covering PR #276. Published to `dev` npm tag for early testing.
package/README.zh-CN.md CHANGED
@@ -389,7 +389,7 @@ ACP 暴露六个可编辑的 prompt:
389
389
  ACP 是 DCP 的直接替代品。迁移步骤:
390
390
 
391
391
  1. 从 `opencode.json` 中移除旧的 DCP 插件
392
- 2. 安装 ACP:`opencode plugin install opencode-acp@stable --global`
392
+ 2. 安装 ACP:`opencode plugin opencode-acp@stable --global`
393
393
  3. 复制配置:`cp ~/.config/opencode/dcp.jsonc ~/.config/opencode/acp.jsonc`
394
394
  4. 复制 prompt 覆盖(如有):`cp -r ~/.config/opencode/dcp-prompts ~/.config/opencode/acp-prompts`
395
395
  5. 复制会话状态(可选,保留压缩块):`cp -r ~/.local/share/opencode/storage/plugin/dcp ~/.local/share/opencode/storage/plugin/acp`
@@ -446,6 +446,26 @@ ACP 是 DCP 的直接替代品。迁移步骤:
446
446
 
447
447
  ## 更新日志
448
448
 
449
+ ### v1.14.14-dev.1 — Dev 预发布版(v1.14.13 以来 2 个 PR)
450
+
451
+ Dev 预发布版,包含两个压缩子系统修复:批量调用的 summary 泄漏(#288)和批量压缩的全有或全无中断(#290)。发布到 `dev` npm tag 供早期测试。
452
+
453
+ **包含的 PR**:
454
+ - **#288**(经 #289)— `hideConsumedCompressCalls` 的 keep-set 以 `compressCallId` 为键,在批量压缩下是 1:N 关系(一次工具调用,多个 `content[]` 条目共享一个 callId)。当 T2 蒸馏只消费了部分兄弟块时,存活的兄弟块会救下整个工具 part,永久泄漏已消费的 summary——且因 compress 是默认受保护工具而无法回收。修复:改为按块(`startId::endId`)标记可见性;对混合存活状态的保留批次,重写存活工具 part 的 `state.input.content` 以丢弃已消费条目的 summary。全部消费的批次仍整体移除。文件:`lib/compress/hide-consumed.ts`。测试:`tests/hide-consumed.test.ts` +3(核心回归测试已验证修复前会失败)。
455
+ - **#290**(经 #291)— 批量 `compress` 在任一 `content[]` 条目只含已压缩消息时整体中断(`Compression range N contains only already-compressed messages`),导致有效条目未执行;错误也未说明哪个条目/ID/块冲突,迫使反复试错重试。修复:(A) 部分失败批次——新增 `identifyPhantomPlans` 丢弃 phantom 条目并压缩其余,返回简洁的跳过提示;(C) 全 phantom 抛错时附详细诊断(条目序号 + 已消费 ID + 所属块);(B) `clampMessageRef` 静默平移边界时发出警告。抽出 `partitionPhantomPlans` + `buildPhantomSkipNotice` 纯函数使批量决策逻辑可测试。`checkPhantomBlock` 保持原有契约不变。文件:`lib/compress/{pipeline,range,search,range-utils}.ts`。测试:+19。双 agent review(Oracle + General,均 APPROVE)。
456
+
457
+ **安装**:`opencode plugin opencode-acp@dev --global`
458
+
459
+ ### v1.14.13 — 正式版(v1.14.12 以来 3 个 PR)
460
+
461
+ 正式版发布,`allowSubAgents` 从实验性参数提升为正式参数(默认:`true`),并更新了配置文档的推荐设置。
462
+
463
+ **包含的 PR**:
464
+ - **#276** — 将 `allowSubAgents` 从 `experimental` 提升为顶层配置字段,默认值从 `false` 改为 `true`。子 Agent 会话现在默认启用压缩。完全向后兼容:旧的 `experimental.allowSubAgents` 配置仍然有效(顶层优先级更高)。更新了全部 4 个 hook 调用点、schema、配置验证和 6 个文档文件。双 Agent 审查通过(Oracle + Explore,均 APPROVE)。
465
+ - **#280, #281** — 更新文档中的推荐配置:`maxContextLimit: 70%`、`minContextLimit: 70%`,简化 `protectedTools`。
466
+
467
+ **安装**:`opencode plugin opencode-acp@stable --global`
468
+
449
469
  ### v1.14.13-dev.1 — Dev 预发布版(v1.14.12 以来 1 个 PR)
450
470
 
451
471
  Dev 预发布版,包含 PR #276。发布到 `dev` npm tag 供早期测试。
package/dist/index.js CHANGED
@@ -2983,7 +2983,7 @@ function buildSearchContext(state, rawMessages) {
2983
2983
  summaryByBlockId
2984
2984
  };
2985
2985
  }
2986
- function resolveBoundaryIds(context, state, startId, endId) {
2986
+ function resolveBoundaryIds(context, state, startId, endId, logger) {
2987
2987
  const lookup = buildBoundaryLookup(context, state);
2988
2988
  const issues = [];
2989
2989
  const parsedStartId = parseBoundaryId(startId);
@@ -3005,20 +3005,28 @@ function resolveBoundaryIds(context, state, startId, endId) {
3005
3005
  let startReference = lookup.get(parsedStartId.ref);
3006
3006
  let endReference = lookup.get(parsedEndId.ref);
3007
3007
  if (!startReference && parsedStartId.kind === "message") {
3008
+ const requestedRef = parsedStartId.ref;
3008
3009
  const clamped = clampMessageRef(parsedStartId, context, state);
3009
3010
  if (clamped) {
3010
3011
  startReference = lookup.get(clamped.ref);
3011
3012
  if (startReference) {
3012
3013
  parsedStartId.ref = clamped.ref;
3014
+ logger?.warn(
3015
+ `compress startId ${requestedRef} not available \u2014 clamped to ${clamped.ref}`
3016
+ );
3013
3017
  }
3014
3018
  }
3015
3019
  }
3016
3020
  if (!endReference && parsedEndId.kind === "message") {
3021
+ const requestedRef = parsedEndId.ref;
3017
3022
  const clamped = clampMessageRef(parsedEndId, context, state);
3018
3023
  if (clamped) {
3019
3024
  endReference = lookup.get(clamped.ref);
3020
3025
  if (endReference) {
3021
3026
  parsedEndId.ref = clamped.ref;
3027
+ logger?.warn(
3028
+ `compress endId ${requestedRef} not available \u2014 clamped to ${clamped.ref}`
3029
+ );
3022
3030
  }
3023
3031
  }
3024
3032
  }
@@ -3042,6 +3050,7 @@ ${hint}` : body);
3042
3050
  throw new Error("Failed to resolve boundary IDs");
3043
3051
  }
3044
3052
  if (startReference.rawIndex > endReference.rawIndex) {
3053
+ ;
3045
3054
  [startReference, endReference] = [endReference, startReference];
3046
3055
  }
3047
3056
  const adjusted = adjustBoundariesForToolPairs(
@@ -3318,7 +3327,9 @@ function createSearchContextTool(factoryCtx) {
3318
3327
  args: {
3319
3328
  query: tool.schema.string().describe("Search query \u2014 keywords or phrase to find"),
3320
3329
  limit: tool.schema.number().optional().describe("Maximum results to return (default: 10)"),
3321
- deep: tool.schema.boolean().optional().describe("If true, also search visible (uncompressed) messages. Slower but more thorough (default: false)")
3330
+ deep: tool.schema.boolean().optional().describe(
3331
+ "If true, also search visible (uncompressed) messages. Slower but more thorough (default: false)"
3332
+ )
3322
3333
  },
3323
3334
  async execute(args, toolCtx) {
3324
3335
  const ctx = resolveToolContext(factoryCtx, toolCtx.sessionID);
@@ -3769,7 +3780,7 @@ function validateArgs(args) {
3769
3780
  }
3770
3781
  }
3771
3782
  }
3772
- function resolveRanges(args, searchContext, state) {
3783
+ function resolveRanges(args, searchContext, state, logger) {
3773
3784
  return args.content.map((entry, index) => {
3774
3785
  const normalizedEntry = {
3775
3786
  topic: typeof entry.topic === "string" && entry.topic.trim().length > 0 ? entry.topic.trim() : void 0,
@@ -3781,7 +3792,8 @@ function resolveRanges(args, searchContext, state) {
3781
3792
  searchContext,
3782
3793
  state,
3783
3794
  normalizedEntry.startId,
3784
- normalizedEntry.endId
3795
+ normalizedEntry.endId,
3796
+ logger
3785
3797
  );
3786
3798
  const selection = resolveSelection(searchContext, startReference, endReference);
3787
3799
  return {
@@ -5711,9 +5723,7 @@ async function finalizeSession(ctx, toolCtx, rawMessages, entries, batchTopic) {
5711
5723
  ctx.logger
5712
5724
  );
5713
5725
  for (const failure of qualityReport.failures) {
5714
- const metrics = Object.fromEntries(
5715
- failure.result.metrics.map((m) => [m.name, m.value])
5716
- );
5726
+ const metrics = Object.fromEntries(failure.result.metrics.map((m) => [m.name, m.value]));
5717
5727
  ctx.logger.warn("Compression quality gate FAILED", {
5718
5728
  blockId: failure.blockId,
5719
5729
  algorithm: ctx.config.qualityGate.algorithm,
@@ -5739,9 +5749,12 @@ async function finalizeSession(ctx, toolCtx, rawMessages, entries, batchTopic) {
5739
5749
  contextTokensBefore
5740
5750
  );
5741
5751
  }
5742
- function checkPhantomBlock(state, plans) {
5752
+ function identifyPhantomPlans(state, plans) {
5753
+ const phantomIndices = [];
5754
+ const details = [];
5743
5755
  for (let i = 0; i < plans.length; i++) {
5744
5756
  const plan = plans[i];
5757
+ if (!plan) continue;
5745
5758
  const effective = new Set(plan.messageIds);
5746
5759
  for (const consumedId of plan.consumedBlockIds) {
5747
5760
  const block = state.prune.messages.blocksById.get(consumedId);
@@ -5755,23 +5768,62 @@ function checkPhantomBlock(state, plans) {
5755
5768
  const entry = state.prune.messages.byMessageId.get(mid);
5756
5769
  return !entry || entry.activeBlockIds.length === 0;
5757
5770
  });
5758
- if (!hasNew) {
5759
- if (plan.consumedBlockIds.length >= 2) {
5760
- const tiers = new Set(
5761
- plan.consumedBlockIds.map(
5762
- (id) => state.prune.messages.blocksById.get(id)?.tier ?? 1
5763
- )
5764
- );
5765
- if (tiers.size === 1) {
5766
- continue;
5767
- }
5768
- }
5769
- return new Error(
5770
- `Compression range ${i + 1} contains only already-compressed messages (0 new direct messages, 0 tokens saved). Nothing to compress \u2014 pick a range with visible, uncompressed content. Use \`acp_status({scope:"uncompressed"})\` to see which ranges are still compressible.`
5771
+ if (hasNew) continue;
5772
+ if (plan.consumedBlockIds.length >= 2) {
5773
+ const tiers = new Set(
5774
+ plan.consumedBlockIds.map(
5775
+ (id) => state.prune.messages.blocksById.get(id)?.tier ?? 1
5776
+ )
5771
5777
  );
5778
+ if (tiers.size === 1) continue;
5779
+ }
5780
+ phantomIndices.push(i);
5781
+ const consumedMessageIds = [];
5782
+ const owningBlockIds = /* @__PURE__ */ new Set();
5783
+ for (const mid of effective) {
5784
+ const entry = state.prune.messages.byMessageId.get(mid);
5785
+ if (entry && entry.activeBlockIds.length > 0) {
5786
+ consumedMessageIds.push(mid);
5787
+ for (const bid of entry.activeBlockIds) owningBlockIds.add(bid);
5788
+ }
5772
5789
  }
5790
+ details.push({
5791
+ index: i,
5792
+ consumedMessageIds: consumedMessageIds.slice(0, 8),
5793
+ owningBlockIds: [...owningBlockIds].sort((a, b) => a - b)
5794
+ });
5773
5795
  }
5774
- return null;
5796
+ return { phantomIndices, details };
5797
+ }
5798
+ function buildPhantomErrorMessage(details) {
5799
+ const lines = details.map((d) => {
5800
+ const ids = d.consumedMessageIds.length > 0 ? d.consumedMessageIds.join(", ") : "(empty effective set)";
5801
+ const blocks = d.owningBlockIds.length > 0 ? d.owningBlockIds.map((b) => `b${b}`).join(", ") : "(no active block \u2014 likely a stale ref)";
5802
+ return ` \u2022 Entry ${d.index + 1}: all messages already compressed. Consumed IDs (sample): ${ids}. Owning block(s): ${blocks}.`;
5803
+ });
5804
+ const noun = details.length === 1 ? "One compress entry" : `${details.length} compress entries`;
5805
+ return `${noun} contain only already-compressed messages (0 new direct messages, 0 tokens saved):
5806
+ ` + lines.join("\n") + `
5807
+ Nothing to compress in those entries \u2014 pick ranges with visible, uncompressed content. Use \`acp_status({scope:"uncompressed"})\` to see which ranges are still compressible.`;
5808
+ }
5809
+ function partitionPhantomPlans(identification, totalPlans) {
5810
+ if (identification.phantomIndices.length === 0) {
5811
+ return { kind: "clean" };
5812
+ }
5813
+ if (identification.phantomIndices.length === totalPlans) {
5814
+ return { kind: "all-phantom", details: identification.details };
5815
+ }
5816
+ return {
5817
+ kind: "partial",
5818
+ dropIndices: identification.phantomIndices,
5819
+ details: identification.details,
5820
+ notice: buildPhantomSkipNotice(identification)
5821
+ };
5822
+ }
5823
+ function buildPhantomSkipNotice(identification) {
5824
+ const skippedNums = identification.details.map((d) => `#${d.index + 1}`).join(", ");
5825
+ const noun = identification.phantomIndices.length === 1 ? "entry" : "entries";
5826
+ return `Skipped ${identification.phantomIndices.length} already-compressed ${noun} (${skippedNums}) \u2014 they are no-ops; the remaining entries were compressed.`;
5775
5827
  }
5776
5828
 
5777
5829
  // lib/compress/keep-markers.ts
@@ -5944,7 +5996,7 @@ function createCompressRangeTool(factoryCtx) {
5944
5996
  toolCtx,
5945
5997
  `Compress Range: ${input.topic ?? "(batch)"}`
5946
5998
  );
5947
- const resolvedPlans = resolveRanges(input, searchContext, ctx.state);
5999
+ const resolvedPlans = resolveRanges(input, searchContext, ctx.state, ctx.logger);
5948
6000
  validateNonOverlapping(resolvedPlans);
5949
6001
  const filteredPlans = resolvedPlans.map((plan) => ({
5950
6002
  ...plan,
@@ -5997,7 +6049,7 @@ function createCompressRangeTool(factoryCtx) {
5997
6049
  }
5998
6050
  }
5999
6051
  const notifications = [];
6000
- const preparedPlans = [];
6052
+ let preparedPlans = [];
6001
6053
  let totalCompressedMessages = 0;
6002
6054
  for (const plan of filteredPlans) {
6003
6055
  const parsedPlaceholders = parseBlockPlaceholders(plan.entry.summary);
@@ -6066,14 +6118,35 @@ function createCompressRangeTool(factoryCtx) {
6066
6118
  consumedBlockIds: mergeConsumedBlockIds
6067
6119
  });
6068
6120
  }
6069
- const phantomError = checkPhantomBlock(
6070
- ctx.state,
6071
- preparedPlans.map((p) => ({
6072
- messageIds: p.selection.messageIds,
6073
- consumedBlockIds: p.consumedBlockIds
6074
- }))
6121
+ const partition = partitionPhantomPlans(
6122
+ identifyPhantomPlans(
6123
+ ctx.state,
6124
+ preparedPlans.map((p) => ({
6125
+ messageIds: p.selection.messageIds,
6126
+ consumedBlockIds: p.consumedBlockIds
6127
+ }))
6128
+ ),
6129
+ preparedPlans.length
6075
6130
  );
6076
- if (phantomError) throw phantomError;
6131
+ let phantomSkipNotice = null;
6132
+ if (partition.kind === "all-phantom") {
6133
+ ctx.logger.warn("Batch compress: ALL entries phantom", {
6134
+ totalCount: preparedPlans.length,
6135
+ details: partition.details
6136
+ });
6137
+ throw new Error(buildPhantomErrorMessage(partition.details));
6138
+ }
6139
+ if (partition.kind === "partial") {
6140
+ ctx.logger.warn("Batch compress: phantom entries detected", {
6141
+ phantomCount: partition.dropIndices.length,
6142
+ totalCount: preparedPlans.length,
6143
+ phantomIndices: partition.dropIndices,
6144
+ details: partition.details
6145
+ });
6146
+ const dropSet = new Set(partition.dropIndices);
6147
+ preparedPlans = preparedPlans.filter((_, i) => !dropSet.has(i));
6148
+ phantomSkipNotice = partition.notice;
6149
+ }
6077
6150
  const acknowledgeRisk = args.acknowledgeRisk === true;
6078
6151
  const qualityGateRetryPendingBefore = ctx.state.qualityGateRetryPending;
6079
6152
  if (acknowledgeRisk && !ctx.state.qualityGateRetryPending) {
@@ -6149,19 +6222,16 @@ function createCompressRangeTool(factoryCtx) {
6149
6222
  summaryTokens
6150
6223
  });
6151
6224
  }
6152
- await finalizeSession(
6153
- ctx,
6154
- toolCtx,
6155
- rawMessages,
6156
- notifications,
6157
- input.topic
6158
- );
6225
+ await finalizeSession(ctx, toolCtx, rawMessages, notifications, input.topic);
6159
6226
  } catch (error) {
6160
6227
  restoreCompressionState(ctx.state, snapshot);
6161
6228
  ctx.state.qualityGateRetryPending = qualityGateRetryPendingBefore;
6162
6229
  throw error;
6163
6230
  }
6164
- return `Compressed ${totalCompressedMessages} messages into ${COMPRESSED_BLOCK_HEADER}.
6231
+ const skippedNote = phantomSkipNotice !== null ? `
6232
+ \u26A0\uFE0F ${phantomSkipNotice}
6233
+ ` : "";
6234
+ return `Compressed ${totalCompressedMessages} messages into ${COMPRESSED_BLOCK_HEADER}.${skippedNote}
6165
6235
  IMPORTANT: This was an automatic context compression. You MUST continue your previous task exactly where you left off. Do NOT ask the user what to do next.
6166
6236
  \u{1F4A1} Tip: Use search_context('keyword') to find compressed content when you need it later.`;
6167
6237
  }
@@ -8283,16 +8353,46 @@ function hasMeaningfulContent(parts) {
8283
8353
 
8284
8354
  // lib/compress/hide-consumed.ts
8285
8355
  var KEEP_LAST_ORPHANED = 2;
8356
+ function isLiveBlock(block) {
8357
+ return block.active && !block.deactivatedByUser && !block.deactivatedByUserDeep;
8358
+ }
8359
+ function rangeKey(startId, endId) {
8360
+ return `${startId}::${endId}`;
8361
+ }
8362
+ function rewriteCompressInput(part, liveKeys) {
8363
+ if (part.type !== "tool") return null;
8364
+ const state = part.state;
8365
+ const input = state?.input;
8366
+ if (!input || typeof input !== "object") return null;
8367
+ const content = input.content;
8368
+ if (!Array.isArray(content) || content.length === 0) return null;
8369
+ const kept = content.filter((entry) => {
8370
+ if (!entry || typeof entry !== "object") return false;
8371
+ const s = typeof entry.startId === "string" ? entry.startId : "";
8372
+ const e = typeof entry.endId === "string" ? entry.endId : "";
8373
+ return liveKeys.has(rangeKey(s, e));
8374
+ });
8375
+ if (kept.length === content.length || kept.length === 0) return null;
8376
+ return {
8377
+ ...part,
8378
+ state: { ...state, input: { ...input, content: kept } }
8379
+ };
8380
+ }
8286
8381
  function hideConsumedCompressCalls(state, messages) {
8287
- const activeCallIds = /* @__PURE__ */ new Set();
8288
8382
  const allBlockCallIds = /* @__PURE__ */ new Set();
8383
+ const liveRangeKeysByCallId = /* @__PURE__ */ new Map();
8384
+ const activeCallIds = /* @__PURE__ */ new Set();
8289
8385
  for (const block of state.prune.messages.blocksById.values()) {
8290
- if (block.compressCallId) {
8291
- allBlockCallIds.add(block.compressCallId);
8292
- if (block.active && !block.deactivatedByUser && !block.deactivatedByUserDeep) {
8293
- activeCallIds.add(block.compressCallId);
8294
- }
8386
+ if (!block.compressCallId) continue;
8387
+ allBlockCallIds.add(block.compressCallId);
8388
+ if (!isLiveBlock(block)) continue;
8389
+ activeCallIds.add(block.compressCallId);
8390
+ let keys = liveRangeKeysByCallId.get(block.compressCallId);
8391
+ if (!keys) {
8392
+ keys = /* @__PURE__ */ new Set();
8393
+ liveRangeKeysByCallId.set(block.compressCallId, keys);
8295
8394
  }
8395
+ keys.add(rangeKey(block.startId, block.endId));
8296
8396
  }
8297
8397
  const lastOrphanedCallIds = [];
8298
8398
  for (let i = messages.length - 1; i >= 0 && lastOrphanedCallIds.length < KEEP_LAST_ORPHANED; i--) {
@@ -8310,15 +8410,28 @@ function hideConsumedCompressCalls(state, messages) {
8310
8410
  const msg = messages[i];
8311
8411
  const parts = Array.isArray(msg.parts) ? msg.parts : [];
8312
8412
  let changed = false;
8313
- const remaining = parts.filter((p) => {
8413
+ const remaining = [];
8414
+ for (const p of parts) {
8314
8415
  if (p.type === "tool" && p.tool === "compress") {
8315
- if (p.callID && keepCallIds.has(p.callID)) return true;
8316
- hidden++;
8317
- changed = true;
8318
- return false;
8416
+ if (!p.callID || !keepCallIds.has(p.callID)) {
8417
+ hidden++;
8418
+ changed = true;
8419
+ continue;
8420
+ }
8421
+ const liveKeys = liveRangeKeysByCallId.get(p.callID);
8422
+ if (liveKeys && liveKeys.size > 0) {
8423
+ const rewritten = rewriteCompressInput(p, liveKeys);
8424
+ if (rewritten) {
8425
+ remaining.push(rewritten);
8426
+ changed = true;
8427
+ continue;
8428
+ }
8429
+ }
8430
+ remaining.push(p);
8431
+ } else {
8432
+ remaining.push(p);
8319
8433
  }
8320
- return true;
8321
- });
8434
+ }
8322
8435
  if (changed) {
8323
8436
  if (hasMeaningfulContent(remaining)) {
8324
8437
  messages[i] = { ...msg, parts: remaining };
@@ -8835,7 +8948,7 @@ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
8835
8948
  import { join as join3 } from "path";
8836
8949
  import { existsSync as existsSync3 } from "fs";
8837
8950
  import { homedir as homedir3 } from "os";
8838
- var LOG_VERSION = true ? "1.14.13-dev.1" : "dev";
8951
+ var LOG_VERSION = true ? "1.14.14-dev.1" : "dev";
8839
8952
  var Logger = class {
8840
8953
  logDir;
8841
8954
  enabled;