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 +21 -1
- package/README.zh-CN.md +21 -1
- package/dist/index.js +166 -53
- package/dist/index.js.map +1 -1
- package/dist/lib/compress/hide-consumed.d.ts +12 -0
- package/dist/lib/compress/hide-consumed.d.ts.map +1 -1
- package/dist/lib/compress/pipeline.d.ts +77 -1
- package/dist/lib/compress/pipeline.d.ts.map +1 -1
- package/dist/lib/compress/range-utils.d.ts +3 -1
- package/dist/lib/compress/range-utils.d.ts.map +1 -1
- package/dist/lib/compress/range.d.ts.map +1 -1
- package/dist/lib/compress/search.d.ts +3 -1
- package/dist/lib/compress/search.d.ts.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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(
|
|
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
|
|
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 (
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8292
|
-
|
|
8293
|
-
|
|
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 =
|
|
8413
|
+
const remaining = [];
|
|
8414
|
+
for (const p of parts) {
|
|
8314
8415
|
if (p.type === "tool" && p.tool === "compress") {
|
|
8315
|
-
if (p.callID
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
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
|
-
|
|
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.
|
|
8951
|
+
var LOG_VERSION = true ? "1.14.14-dev.1" : "dev";
|
|
8839
8952
|
var Logger = class {
|
|
8840
8953
|
logDir;
|
|
8841
8954
|
enabled;
|