opencode-acp 1.12.1 → 1.12.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 +8 -0
- package/README.zh-CN.md +8 -0
- package/dist/index.js +107 -81
- package/dist/index.js.map +1 -1
- package/dist/lib/compress/message.d.ts.map +1 -1
- package/dist/lib/compress/pipeline.d.ts +8 -1
- package/dist/lib/compress/pipeline.d.ts.map +1 -1
- package/dist/lib/compress/range.d.ts.map +1 -1
- package/dist/lib/messages/sync.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -422,6 +422,14 @@ For the complete list with root cause analysis, see the [bug tracker](https://gi
|
|
|
422
422
|
|
|
423
423
|
## Changelog
|
|
424
424
|
|
|
425
|
+
### v1.12.2 — Compress Failure Rollback + Sync Carve-out Removal (PR #126)
|
|
426
|
+
|
|
427
|
+
**Problem**: Two bugs in post-compression-failure handling (issue #125). (1) The compress tool mutated in-memory state incrementally with no try/catch — if anything threw between the first `applyCompressionState` and `finalizeSession`, "ghost blocks" (active blocks never persisted) hid messages on subsequent transforms. (2) `syncCompressionBlocks` had a carve-out that kept blocks active when the anchor was missing from messages but tracked in `byMessageId`. This carve-out was intended for ACP-hidden anchors, but sync runs on the raw message list (before filtering), so it only triggered for externally-deleted anchors → messages hidden without recap injection → empty LLM requests.
|
|
428
|
+
|
|
429
|
+
**Fix**: (1) Added `snapshotCompressionState()` / `restoreCompressionState()` to `lib/compress/pipeline.ts` using `structuredClone`. Wrapped the mutation phase in try/catch in both `lib/compress/range.ts` and `lib/compress/message.ts`. On failure, state (including `manualMode`) is restored to the pre-mutation snapshot — no ghost blocks. (2) Removed the carve-out in `lib/messages/sync.ts`. When anchor is gone from messages, always deactivate the block. Oracle-reviewed.
|
|
430
|
+
|
|
431
|
+
Files: `lib/messages/sync.ts`, `lib/compress/pipeline.ts`, `lib/compress/range.ts`, `lib/compress/message.ts`. Tests: `tests/sync.test.ts` (updated), `tests/compress-rollback.test.ts` (NEW, 4 tests). 643 tests pass.
|
|
432
|
+
|
|
425
433
|
### v1.12.1 — Compression Recap Injection Fix + Stale Compress Stripping (PR #119)
|
|
426
434
|
|
|
427
435
|
**Problem**: `acp_context_recap` was used to create synthetic tool-result recap messages but was NOT registered as a real tool — providers could strip/convert unregistered tool-results, causing the model to see compression summaries as plain text or user messages (echo/drift bugs). Additionally, compress tool-call inputs duplicated block recap content in context.
|
package/README.zh-CN.md
CHANGED
|
@@ -395,6 +395,14 @@ ACP 在首次启动时自动将配置从 `dcp.jsonc` 迁移到 `acp.jsonc`,将
|
|
|
395
395
|
|
|
396
396
|
## 更新日志
|
|
397
397
|
|
|
398
|
+
### v1.12.2 — 压缩失败回滚 + Sync carve-out 移除(PR #126)
|
|
399
|
+
|
|
400
|
+
**问题**:压缩失败后的处理存在两个 bug(issue #125)。(1)compress 工具在内存中增量修改状态,没有 try/catch——如果在 `applyCompressionState` 和 `finalizeSession` 之间抛出异常,"幽灵块"(未持久化的活跃块)会在后续 transform 中隐藏消息。(2)`syncCompressionBlocks` 有一个 carve-out:当块的锚点从消息中缺失但在 `byMessageId` 中有记录时,块保持活跃。这个 carve-out 本意是保护 ACP 隐藏的锚点,但 sync 运行在原始消息列表上(在过滤之前),所以它只在外部删除的锚点场景触发 → 块保持活跃但无法注入摘要 → 隐藏消息无替换 → **LLM 请求为空**。
|
|
401
|
+
|
|
402
|
+
**修复**:(1)在 `lib/compress/pipeline.ts` 中新增 `snapshotCompressionState()` / `restoreCompressionState()`(使用 `structuredClone`)。在 `lib/compress/range.ts` 和 `lib/compress/message.ts` 中用 try/catch 包裹变更阶段。失败时,状态(包括 `manualMode`)恢复到变更前的快照——不会有幽灵块。(2)移除 `lib/messages/sync.ts` 中的 carve-out。锚点从消息中缺失时,总是停用块。经 Oracle 审查。
|
|
403
|
+
|
|
404
|
+
文件:`lib/messages/sync.ts`、`lib/compress/pipeline.ts`、`lib/compress/range.ts`、`lib/compress/message.ts`。测试:`tests/sync.test.ts`(更新)、`tests/compress-rollback.test.ts`(新增,4 个测试)。643 个测试通过。
|
|
405
|
+
|
|
398
406
|
### v1.12.1 — 压缩摘要注入修复 + 历史压缩调用剥离(PR #119)
|
|
399
407
|
|
|
400
408
|
**问题**:`acp_context_recap` 用于创建合成的 tool-result 摘要消息,但未注册为真实工具——provider 可能剥离/转换未注册的 tool-result,导致模型将压缩摘要视为纯文本或用户消息(回声/漂移 bug)。此外,compress 工具调用的输入与 block recap 内容重复占用上下文。
|
package/dist/index.js
CHANGED
|
@@ -5123,6 +5123,18 @@ async function sendIgnoredMessage(client, sessionID, text, params, logger) {
|
|
|
5123
5123
|
}
|
|
5124
5124
|
|
|
5125
5125
|
// lib/compress/pipeline.ts
|
|
5126
|
+
function snapshotCompressionState(state) {
|
|
5127
|
+
return {
|
|
5128
|
+
messages: structuredClone(state.prune.messages),
|
|
5129
|
+
stats: { ...state.stats },
|
|
5130
|
+
manualMode: state.manualMode
|
|
5131
|
+
};
|
|
5132
|
+
}
|
|
5133
|
+
function restoreCompressionState(state, snapshot) {
|
|
5134
|
+
state.prune.messages = structuredClone(snapshot.messages);
|
|
5135
|
+
state.stats = { ...snapshot.stats };
|
|
5136
|
+
state.manualMode = snapshot.manualMode;
|
|
5137
|
+
}
|
|
5126
5138
|
async function prepareSession(ctx, toolCtx, title) {
|
|
5127
5139
|
if (ctx.state.manualMode && ctx.state.manualMode !== "compress-pending") {
|
|
5128
5140
|
throw new Error(
|
|
@@ -5302,7 +5314,9 @@ function buildSchema(maxSummaryLengthHard) {
|
|
|
5302
5314
|
)
|
|
5303
5315
|
})
|
|
5304
5316
|
).describe("Batch of individual message summaries to create in one tool call"),
|
|
5305
|
-
summaryMaxChars: tool2.schema.number().optional().describe(
|
|
5317
|
+
summaryMaxChars: tool2.schema.number().optional().describe(
|
|
5318
|
+
`Override max summary length (default max: ${maxSummaryLengthHard} chars). Use when content is important and needs more detail \u2014 don't lose critical info just to fit the limit.`
|
|
5319
|
+
)
|
|
5306
5320
|
};
|
|
5307
5321
|
}
|
|
5308
5322
|
function createCompressMessageTool(ctx) {
|
|
@@ -5385,46 +5399,52 @@ function createCompressMessageTool(ctx) {
|
|
|
5385
5399
|
summaryWithTools
|
|
5386
5400
|
});
|
|
5387
5401
|
}
|
|
5402
|
+
const snapshot = snapshotCompressionState(ctx.state);
|
|
5388
5403
|
const runId = allocateRunId(ctx.state);
|
|
5389
|
-
|
|
5390
|
-
const
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5404
|
+
try {
|
|
5405
|
+
for (const { plan, summaryWithTools } of preparedPlans) {
|
|
5406
|
+
const blockId = allocateBlockId(ctx.state);
|
|
5407
|
+
const keepResult = resolveKeepMarkers(
|
|
5408
|
+
summaryWithTools,
|
|
5409
|
+
rawMessages,
|
|
5410
|
+
ctx.state,
|
|
5411
|
+
ctx.config
|
|
5412
|
+
);
|
|
5413
|
+
const resolvedSummary = keepResult.summary;
|
|
5414
|
+
const storedSummary = wrapCompressedSummary(blockId, resolvedSummary);
|
|
5415
|
+
const summaryTokens = countTokens2(storedSummary);
|
|
5416
|
+
applyCompressionState(
|
|
5417
|
+
ctx.state,
|
|
5418
|
+
{
|
|
5419
|
+
topic: plan.entry.topic,
|
|
5420
|
+
batchTopic: input.topic,
|
|
5421
|
+
startId: plan.entry.messageId,
|
|
5422
|
+
endId: plan.entry.messageId,
|
|
5423
|
+
mode: "message",
|
|
5424
|
+
runId,
|
|
5425
|
+
compressMessageId: toolCtx.messageID,
|
|
5426
|
+
compressCallId: callId,
|
|
5427
|
+
summaryTokens
|
|
5428
|
+
},
|
|
5429
|
+
plan.selection,
|
|
5430
|
+
plan.anchorMessageId,
|
|
5431
|
+
blockId,
|
|
5432
|
+
storedSummary,
|
|
5433
|
+
[],
|
|
5434
|
+
ctx.config.gc
|
|
5435
|
+
);
|
|
5436
|
+
notifications.push({
|
|
5437
|
+
blockId,
|
|
5408
5438
|
runId,
|
|
5409
|
-
|
|
5410
|
-
compressCallId: callId,
|
|
5439
|
+
summary: resolvedSummary,
|
|
5411
5440
|
summaryTokens
|
|
5412
|
-
}
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
ctx.config.gc
|
|
5419
|
-
);
|
|
5420
|
-
notifications.push({
|
|
5421
|
-
blockId,
|
|
5422
|
-
runId,
|
|
5423
|
-
summary: resolvedSummary,
|
|
5424
|
-
summaryTokens
|
|
5425
|
-
});
|
|
5441
|
+
});
|
|
5442
|
+
}
|
|
5443
|
+
await finalizeSession(ctx, toolCtx, rawMessages, notifications, input.topic);
|
|
5444
|
+
} catch (error) {
|
|
5445
|
+
restoreCompressionState(ctx.state, snapshot);
|
|
5446
|
+
throw error;
|
|
5426
5447
|
}
|
|
5427
|
-
await finalizeSession(ctx, toolCtx, rawMessages, notifications, input.topic);
|
|
5428
5448
|
return formatResult(plans.length, skippedIssues, skippedCount);
|
|
5429
5449
|
}
|
|
5430
5450
|
});
|
|
@@ -5448,7 +5468,9 @@ function buildSchema2(maxSummaryLengthHard) {
|
|
|
5448
5468
|
).describe(
|
|
5449
5469
|
"One or more ranges to compress, each with start/end boundaries and a summary"
|
|
5450
5470
|
),
|
|
5451
|
-
summaryMaxChars: tool3.schema.number().optional().describe(
|
|
5471
|
+
summaryMaxChars: tool3.schema.number().optional().describe(
|
|
5472
|
+
`Override max summary length (default max: ${maxSummaryLengthHard} chars). Use when content is important and needs more detail \u2014 don't lose critical info just to fit the limit.`
|
|
5473
|
+
)
|
|
5452
5474
|
};
|
|
5453
5475
|
}
|
|
5454
5476
|
function createCompressRangeTool(ctx) {
|
|
@@ -5584,47 +5606,53 @@ function createCompressRangeTool(ctx) {
|
|
|
5584
5606
|
consumedBlockIds: mergeConsumedBlockIds
|
|
5585
5607
|
});
|
|
5586
5608
|
}
|
|
5609
|
+
const snapshot = snapshotCompressionState(ctx.state);
|
|
5587
5610
|
const runId = allocateRunId(ctx.state);
|
|
5588
|
-
|
|
5589
|
-
const
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5611
|
+
try {
|
|
5612
|
+
for (const preparedPlan of preparedPlans) {
|
|
5613
|
+
const blockId = allocateBlockId(ctx.state);
|
|
5614
|
+
const keepResult = resolveKeepMarkers(
|
|
5615
|
+
preparedPlan.finalSummary,
|
|
5616
|
+
rawMessages,
|
|
5617
|
+
ctx.state,
|
|
5618
|
+
ctx.config
|
|
5619
|
+
);
|
|
5620
|
+
preparedPlan.finalSummary = keepResult.summary;
|
|
5621
|
+
const storedSummary = wrapCompressedSummary(blockId, preparedPlan.finalSummary);
|
|
5622
|
+
const summaryTokens = countTokens2(storedSummary);
|
|
5623
|
+
const applied = applyCompressionState(
|
|
5624
|
+
ctx.state,
|
|
5625
|
+
{
|
|
5626
|
+
topic: input.topic,
|
|
5627
|
+
batchTopic: input.topic,
|
|
5628
|
+
startId: preparedPlan.entry.startId,
|
|
5629
|
+
endId: preparedPlan.entry.endId,
|
|
5630
|
+
mode: "range",
|
|
5631
|
+
runId,
|
|
5632
|
+
compressMessageId: toolCtx.messageID,
|
|
5633
|
+
compressCallId: callId,
|
|
5634
|
+
summaryTokens
|
|
5635
|
+
},
|
|
5636
|
+
preparedPlan.selection,
|
|
5637
|
+
preparedPlan.anchorMessageId,
|
|
5638
|
+
blockId,
|
|
5639
|
+
storedSummary,
|
|
5640
|
+
preparedPlan.consumedBlockIds,
|
|
5641
|
+
ctx.config.gc
|
|
5642
|
+
);
|
|
5643
|
+
totalCompressedMessages += applied.messageIds.length;
|
|
5644
|
+
notifications.push({
|
|
5645
|
+
blockId,
|
|
5607
5646
|
runId,
|
|
5608
|
-
|
|
5609
|
-
compressCallId: callId,
|
|
5647
|
+
summary: preparedPlan.finalSummary,
|
|
5610
5648
|
summaryTokens
|
|
5611
|
-
}
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
ctx.config.gc
|
|
5618
|
-
);
|
|
5619
|
-
totalCompressedMessages += applied.messageIds.length;
|
|
5620
|
-
notifications.push({
|
|
5621
|
-
blockId,
|
|
5622
|
-
runId,
|
|
5623
|
-
summary: preparedPlan.finalSummary,
|
|
5624
|
-
summaryTokens
|
|
5625
|
-
});
|
|
5649
|
+
});
|
|
5650
|
+
}
|
|
5651
|
+
await finalizeSession(ctx, toolCtx, rawMessages, notifications, input.topic);
|
|
5652
|
+
} catch (error) {
|
|
5653
|
+
restoreCompressionState(ctx.state, snapshot);
|
|
5654
|
+
throw error;
|
|
5626
5655
|
}
|
|
5627
|
-
await finalizeSession(ctx, toolCtx, rawMessages, notifications, input.topic);
|
|
5628
5656
|
return `Compressed ${totalCompressedMessages} messages into ${COMPRESSED_BLOCK_HEADER}.
|
|
5629
5657
|
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.
|
|
5630
5658
|
\u{1F4A1} Tip: Use search_context('keyword') to find compressed content when you need it later.`;
|
|
@@ -6026,12 +6054,10 @@ var syncCompressionBlocks = (state, logger, messages) => {
|
|
|
6026
6054
|
continue;
|
|
6027
6055
|
}
|
|
6028
6056
|
if (typeof block.anchorMessageId === "string" && block.anchorMessageId.length > 0 && !messageIds.has(block.anchorMessageId)) {
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
continue;
|
|
6034
|
-
}
|
|
6057
|
+
block.active = false;
|
|
6058
|
+
block.deactivatedAt = now;
|
|
6059
|
+
block.deactivatedByBlockId = void 0;
|
|
6060
|
+
continue;
|
|
6035
6061
|
}
|
|
6036
6062
|
for (const consumedBlockId of block.consumedBlockIds) {
|
|
6037
6063
|
if (!messagesState.activeBlockIds.has(consumedBlockId)) {
|