opencode-acp 1.9.0 → 1.9.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 +12 -0
- package/README.zh-CN.md +12 -0
- package/dist/index.js +96 -21
- package/dist/index.js.map +1 -1
- package/dist/lib/config-validation.d.ts.map +1 -1
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/messages/inject/inject.d.ts +18 -0
- package/dist/lib/messages/inject/inject.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -421,6 +421,18 @@ For the complete list with root cause analysis, see the [bug tracker](https://gi
|
|
|
421
421
|
|
|
422
422
|
## Changelog
|
|
423
423
|
|
|
424
|
+
### v1.9.1 — Disjoint Visible-Range Segments & Nudge Wording (issue #9 root cause)
|
|
425
|
+
|
|
426
|
+
**Problem**: Even after v1.9.0, the model kept calling `compress` against IDs that a prior block had consumed. The root cause was that the suffix advertised a single contiguous span "first visible → last visible" that **straddled compression holes** — so the model's first guess for an `endId` landed inside an already-summarized range. Separately, the suffix's `(+X tokens since last nudge)` growth line was being misread as an *overflow* warning, triggering panic compressions of large-but-still-needed ranges.
|
|
427
|
+
|
|
428
|
+
**Fix 1 — disjoint visible-id segments** (PR #57): `injectVisibleIdRange` no longer emits one "first-to-last" span. It builds the actual surviving segments in ascending ref order and truncates to the largest tool-bearing / high-token segments when the count overflows (`compress.maxVisibleSegments`, default `50`, now plumbed through config defaults + merge + validation + schema). The suffix now reads e.g. `[Visible (top 2 of 3 segments, 803 msgs): m00001–m00929, m00944–m00950 | +1 smaller segment (~1.2K tokens, 6 msgs) omitted]`, so the model sees exactly which ranges are compressible and never targets a hole. The formatting logic is extracted into pure, exported, unit-tested functions (`buildVisibleSegments`, `formatVisibleGuidance`).
|
|
429
|
+
|
|
430
|
+
**Fix 2 — nudge wording** (PR #58): The incremental-compression guidance line (`💡 Compress incrementally: target the ranges above...`) moved to *after* the largest-ranges list and is reworded to stress that **size alone is not a reason to compress** — a large range that is still needed in full must be kept. Soft efficiency nudges (`growth` / `minLimit` variants) are now prefixed with an explicit *"This is an efficiency nudge to compress early and keep context lean — not an overflow warning. A separate, stronger alert will appear if the context is actually full."* so the growth delta isn't mistaken for an overflow alarm. The `maxLimit` path keeps its stronger alert and is intentionally excluded from the efficiency framing.
|
|
431
|
+
|
|
432
|
+
**Compatibility**: No persisted-state schema changes. New optional config field `compress.maxVisibleSegments` (number, default `50`); old configs keep working.
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
424
436
|
### v1.9.0 — Visible-Range Guidance & Compression Failure Recovery
|
|
425
437
|
|
|
426
438
|
**Problem**: On large-context models (1M+) the model repeatedly called `compress(startId=m00930, endId=m00943)` against IDs that a prior block had already consumed. It had no stable view of which `mNNNNN` refs were still compressible, the failure error gave no recovery info, `acp_status` was registered but never mentioned in the prompt, and the suffix nudge reported a bare percentage with no indication of *where* the tokens were actually spent.
|
package/README.zh-CN.md
CHANGED
|
@@ -393,6 +393,18 @@ ACP 在首次启动时自动将配置从 `dcp.jsonc` 迁移到 `acp.jsonc`,将
|
|
|
393
393
|
|
|
394
394
|
## 更新日志
|
|
395
395
|
|
|
396
|
+
### v1.9.1 — 不相交可见范围段 & 提醒措辞修正(issue #9 根因)
|
|
397
|
+
|
|
398
|
+
**问题**:即便有了 v1.9.0,模型仍反复对已被先前块消费的 ID 调用 `compress`。根因是 suffix 一直广播一条"从首条可见到最后一条可见"的**跨越压缩空洞的连续 span** —— 模型对 `endId` 的第一反应往往是落在已经被摘要的范围里。此外,suffix 的 `(+X tokens since last nudge)` 增长行被误读为**溢出警告**,触发对"大但仍然需要"范围的恐慌性压缩。
|
|
399
|
+
|
|
400
|
+
**修复 1 — 不连续可见 ID 段**(PR #57):`injectVisibleIdRange` 不再输出一条"首到尾"span。改为按引用升序构建真正存活的不相交段,并在段数溢出时截断到最大的含工具 / 高 token 段(`compress.maxVisibleSegments`,默认 `50`,已通过 config defaults + merge + validation + schema 全链路接入)。suffix 现在形如 `[Visible (top 2 of 3 segments, 803 msgs): m00001–m00929, m00944–m00950 | +1 smaller segment (~1.2K tokens, 6 msgs) omitted]`,模型能精确看到哪些范围可压缩、绝不会被引导去打空洞。格式化逻辑抽取为纯函数并导出、可单测(`buildVisibleSegments`、`formatVisibleGuidance`)。
|
|
401
|
+
|
|
402
|
+
**修复 2 — 提醒措辞**(PR #58):增量压缩指引行(`💡 Compress incrementally: target the ranges above...`)移到 largest-ranges 列表**之后**,并改写以强调**仅凭大小不是压缩理由** —— 仍然需要完整保留的大范围必须保留。软性效率提醒(`growth` / `minLimit` 变体)现在前置一条明确说明 *"This is an efficiency nudge to compress early and keep context lean — not an overflow warning. A separate, stronger alert will appear if the context is actually full."*,使增长量不被误读为溢出警报。`maxLimit` 路径保留更强的告警,并有意排除在效率措辞之外。
|
|
403
|
+
|
|
404
|
+
**兼容性**:无持久化 state schema 变更。新增可选配置字段 `compress.maxVisibleSegments`(数字,默认 `50`);旧配置继续工作。
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
396
408
|
### v1.9.0 — 可见范围引导 & 压缩失败恢复
|
|
397
409
|
|
|
398
410
|
**问题**:在大上下文模型(1M+)上,模型反复调用 `compress(startId=m00930, endId=m00943)`,而这些 ID 已被之前的块消费。模型对哪些 `mNNNNN` 引用仍可压缩没有稳定视图,失败错误不提供恢复信息,`acp_status` 工具已注册但从未在提示中提及,suffix nudge 只报告一个裸百分比,完全不说明 token 实际花在了哪里。
|
package/dist/index.js
CHANGED
|
@@ -903,6 +903,7 @@ var VALID_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
903
903
|
"compress.protectUserMessages",
|
|
904
904
|
"compress.maxSummaryLengthHard",
|
|
905
905
|
"compress.minCompressRange",
|
|
906
|
+
"compress.maxVisibleSegments",
|
|
906
907
|
"gc",
|
|
907
908
|
"gc.algorithm",
|
|
908
909
|
"gc.promotionThreshold",
|
|
@@ -1184,6 +1185,20 @@ function validateConfigTypes(config) {
|
|
|
1184
1185
|
actual: `${compress.minCompressRange}`
|
|
1185
1186
|
});
|
|
1186
1187
|
}
|
|
1188
|
+
if (compress.maxVisibleSegments !== void 0 && typeof compress.maxVisibleSegments !== "number") {
|
|
1189
|
+
errors.push({
|
|
1190
|
+
key: "compress.maxVisibleSegments",
|
|
1191
|
+
expected: "number",
|
|
1192
|
+
actual: typeof compress.maxVisibleSegments
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
if (typeof compress.maxVisibleSegments === "number" && compress.maxVisibleSegments < 1) {
|
|
1196
|
+
errors.push({
|
|
1197
|
+
key: "compress.maxVisibleSegments",
|
|
1198
|
+
expected: "positive number (>= 1)",
|
|
1199
|
+
actual: `${compress.maxVisibleSegments}`
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1187
1202
|
if (typeof compress.iterationNudgeThreshold === "number" && compress.iterationNudgeThreshold < 1) {
|
|
1188
1203
|
errors.push({
|
|
1189
1204
|
key: "compress.iterationNudgeThreshold",
|
|
@@ -1498,7 +1513,8 @@ var defaultConfig = {
|
|
|
1498
1513
|
protectTags: false,
|
|
1499
1514
|
protectUserMessages: false,
|
|
1500
1515
|
maxSummaryLengthHard: 1e4,
|
|
1501
|
-
minCompressRange: 2e3
|
|
1516
|
+
minCompressRange: 2e3,
|
|
1517
|
+
maxVisibleSegments: 50
|
|
1502
1518
|
},
|
|
1503
1519
|
strategies: {
|
|
1504
1520
|
deduplication: {
|
|
@@ -1653,7 +1669,8 @@ function mergeCompress(base, override) {
|
|
|
1653
1669
|
protectTags: override.protectTags ?? base.protectTags,
|
|
1654
1670
|
protectUserMessages: override.protectUserMessages ?? base.protectUserMessages,
|
|
1655
1671
|
maxSummaryLengthHard: override.maxSummaryLengthHard ?? base.maxSummaryLengthHard,
|
|
1656
|
-
minCompressRange: override.minCompressRange ?? base.minCompressRange
|
|
1672
|
+
minCompressRange: override.minCompressRange ?? base.minCompressRange,
|
|
1673
|
+
maxVisibleSegments: override.maxVisibleSegments ?? base.maxVisibleSegments
|
|
1657
1674
|
};
|
|
1658
1675
|
}
|
|
1659
1676
|
function mergeCommands(base, override) {
|
|
@@ -6307,15 +6324,15 @@ var injectCompressNudges = (state, config, logger, messages, prompts, compressio
|
|
|
6307
6324
|
const growth = currentTokens !== void 0 && state.nudges.lastPerMessageNudgeTokens !== void 0 ? currentTokens - state.nudges.lastPerMessageNudgeTokens : 0;
|
|
6308
6325
|
const growthStr = growth > 0 ? ` (+${fmt(growth)} since last nudge)` : "";
|
|
6309
6326
|
const plainTextTokens = composition.textTokens;
|
|
6310
|
-
|
|
6327
|
+
const efficiencyNote = decision.tipsVariant !== "maxLimit" ? `
|
|
6328
|
+
This is an efficiency nudge to compress early and keep context lean \u2014 not an overflow warning. A separate, stronger alert will appear if the context is actually full.` : "";
|
|
6329
|
+
let breakdown = `${efficiencyNote}
|
|
6311
6330
|
Breakdown: ${fmt(composition.toolTokens)} tool (${pct(composition.toolTokens)}%) | ${fmt(composition.summaryTokens)} summaries (${pct(composition.summaryTokens)}%) | ${fmt(composition.codeTokens)} code (${pct(composition.codeTokens)}%) | ${fmt(plainTextTokens)} text (${pct(plainTextTokens)}%)${growthStr}`;
|
|
6312
6331
|
const topBlocks = Array.from(state.prune.messages.blocksById.values()).filter((b) => b.active).sort((a, b) => b.compressedTokens - a.compressedTokens).slice(0, 3);
|
|
6313
6332
|
if (topBlocks.length > 0) {
|
|
6314
6333
|
breakdown += `
|
|
6315
6334
|
Top blocks: ${topBlocks.map((b) => `b${b.blockId} ${fmt(b.compressedTokens)}\u2192${fmt(b.summaryTokens)}`).join(", ")}`;
|
|
6316
6335
|
}
|
|
6317
|
-
breakdown += `
|
|
6318
|
-
\u{1F4A1} Compress incrementally \u2014 compress the largest consumed ranges first.`;
|
|
6319
6336
|
if (composition.largestToolRanges.length > 0) {
|
|
6320
6337
|
breakdown += `
|
|
6321
6338
|
Largest tool outputs: ${composition.largestToolRanges.map((r) => `${r.ref} (${fmt(r.tokens)})`).join(", ")}`;
|
|
@@ -6328,6 +6345,8 @@ Largest code messages: ${composition.largestCodeRanges.map((r) => `${r.ref} (${f
|
|
|
6328
6345
|
breakdown += `
|
|
6329
6346
|
Largest text messages: ${composition.largestMessageRanges.map((r) => `${r.ref} (${fmt(r.tokens)})`).join(", ")}`;
|
|
6330
6347
|
}
|
|
6348
|
+
breakdown += `
|
|
6349
|
+
\u{1F4A1} Compress incrementally: target the ranges above whose content you have already extracted for this step. Size alone is not a reason to compress \u2014 if a large range is still needed in full, keep it.`;
|
|
6331
6350
|
appendToLastTextPart(suffixMessage, breakdown);
|
|
6332
6351
|
}
|
|
6333
6352
|
if (decision.tipsVariant === "maxLimit") {
|
|
@@ -6352,7 +6371,7 @@ Largest text messages: ${composition.largestMessageRanges.map((r) => `${r.ref} (
|
|
|
6352
6371
|
if (tipsText && suffixMessage) {
|
|
6353
6372
|
appendToLastTextPart(suffixMessage, tipsText);
|
|
6354
6373
|
}
|
|
6355
|
-
injectVisibleIdRange(state, messages, suffixMessage);
|
|
6374
|
+
injectVisibleIdRange(state, config, messages, suffixMessage);
|
|
6356
6375
|
}
|
|
6357
6376
|
if (toolOutputReminder && suffixMessage) {
|
|
6358
6377
|
if (!decision.shouldNudge) {
|
|
@@ -6393,22 +6412,78 @@ function injectContextUsage(target, config, currentTokens, modelContextLimit) {
|
|
|
6393
6412
|
}
|
|
6394
6413
|
target.parts.push(createSyntheticTextPart(target, usageTag));
|
|
6395
6414
|
}
|
|
6396
|
-
function
|
|
6415
|
+
function refNumber(ref) {
|
|
6416
|
+
const n = parseInt(ref.slice(1), 10);
|
|
6417
|
+
return Number.isNaN(n) ? -1 : n;
|
|
6418
|
+
}
|
|
6419
|
+
function buildVisibleSegments(state, messages) {
|
|
6420
|
+
const refInfo = /* @__PURE__ */ new Map();
|
|
6421
|
+
for (const msg of messages) {
|
|
6422
|
+
const ref = state.messageIds.byRawId.get(msg.info.id);
|
|
6423
|
+
if (!ref) continue;
|
|
6424
|
+
let tokens = 0;
|
|
6425
|
+
let hasTool = false;
|
|
6426
|
+
for (const part of msg.parts || []) {
|
|
6427
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
6428
|
+
tokens += Math.round(part.text.length / 4);
|
|
6429
|
+
} else if (part.type !== "text" && part.type !== "reasoning") {
|
|
6430
|
+
tokens += Math.round(JSON.stringify(part).length / 4);
|
|
6431
|
+
hasTool = true;
|
|
6432
|
+
}
|
|
6433
|
+
}
|
|
6434
|
+
refInfo.set(ref, { tokens, hasTool });
|
|
6435
|
+
}
|
|
6436
|
+
if (refInfo.size === 0) return [];
|
|
6437
|
+
const refs = Array.from(refInfo.keys()).sort((a, b) => refNumber(a) - refNumber(b));
|
|
6438
|
+
const segments = [];
|
|
6439
|
+
let cur = null;
|
|
6440
|
+
let prevNum = -2;
|
|
6441
|
+
for (const ref of refs) {
|
|
6442
|
+
const num = refNumber(ref);
|
|
6443
|
+
const info = refInfo.get(ref);
|
|
6444
|
+
if (cur && num === prevNum + 1) {
|
|
6445
|
+
cur.endRef = ref;
|
|
6446
|
+
cur.count++;
|
|
6447
|
+
cur.tokens += info.tokens;
|
|
6448
|
+
if (info.hasTool) cur.hasTool = true;
|
|
6449
|
+
} else {
|
|
6450
|
+
if (cur) segments.push(cur);
|
|
6451
|
+
cur = { startRef: ref, endRef: ref, count: 1, tokens: info.tokens, hasTool: info.hasTool };
|
|
6452
|
+
}
|
|
6453
|
+
prevNum = num;
|
|
6454
|
+
}
|
|
6455
|
+
if (cur) segments.push(cur);
|
|
6456
|
+
return segments;
|
|
6457
|
+
}
|
|
6458
|
+
function formatSegment(seg) {
|
|
6459
|
+
return seg.startRef === seg.endRef ? seg.startRef : `${seg.startRef}\u2013${seg.endRef}`;
|
|
6460
|
+
}
|
|
6461
|
+
function formatVisibleGuidance(segments, maxSegs) {
|
|
6462
|
+
if (segments.length === 0) return "";
|
|
6463
|
+
const totalMsgs = segments.reduce((s, seg) => s + seg.count, 0);
|
|
6464
|
+
const totalSegs = segments.length;
|
|
6465
|
+
const fmt = (n) => n >= 1e3 ? `${(n / 1e3).toFixed(1)}K` : String(n);
|
|
6466
|
+
if (totalSegs <= maxSegs) {
|
|
6467
|
+
return `[Visible: ${segments.map(formatSegment).join(", ")} (${totalMsgs} msg${totalMsgs === 1 ? "" : "s"}, ${totalSegs} segment${totalSegs === 1 ? "" : "s"})]`;
|
|
6468
|
+
}
|
|
6469
|
+
const keepSet = new Set(
|
|
6470
|
+
[...segments].sort((a, b) => {
|
|
6471
|
+
if (a.hasTool !== b.hasTool) return a.hasTool ? -1 : 1;
|
|
6472
|
+
return b.tokens - a.tokens;
|
|
6473
|
+
}).slice(0, maxSegs)
|
|
6474
|
+
);
|
|
6475
|
+
const shown = segments.filter((s) => keepSet.has(s));
|
|
6476
|
+
const omitted = segments.filter((s) => !keepSet.has(s));
|
|
6477
|
+
const omittedTokens = omitted.reduce((sum, s) => sum + s.tokens, 0);
|
|
6478
|
+
const omittedMsgs = omitted.reduce((sum, s) => sum + s.count, 0);
|
|
6479
|
+
return `[Visible (top ${shown.length} of ${totalSegs} segments, ${totalMsgs} msgs): ${shown.map(formatSegment).join(", ")} | +${omitted.length} smaller segment${omitted.length === 1 ? "" : "s"} (~${fmt(omittedTokens)} tokens, ${omittedMsgs} msg${omittedMsgs === 1 ? "" : "s"}) omitted]`;
|
|
6480
|
+
}
|
|
6481
|
+
function injectVisibleIdRange(state, config, messages, target) {
|
|
6397
6482
|
if (!target) return;
|
|
6398
|
-
const
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
visibleRefs.push(ref);
|
|
6403
|
-
}
|
|
6404
|
-
}
|
|
6405
|
-
if (visibleRefs.length === 0) return;
|
|
6406
|
-
visibleRefs.sort();
|
|
6407
|
-
const first = visibleRefs[0];
|
|
6408
|
-
const last = visibleRefs[visibleRefs.length - 1];
|
|
6409
|
-
const rangeTag = `
|
|
6410
|
-
|
|
6411
|
-
[Visible messages: ${first} to ${last} (${visibleRefs.length} messages)]`;
|
|
6483
|
+
const segments = buildVisibleSegments(state, messages);
|
|
6484
|
+
if (segments.length === 0) return;
|
|
6485
|
+
const maxSegs = config.compress?.maxVisibleSegments ?? 50;
|
|
6486
|
+
const rangeTag = "\n\n" + formatVisibleGuidance(segments, maxSegs);
|
|
6412
6487
|
for (const part of target.parts) {
|
|
6413
6488
|
if (part.type === "text") {
|
|
6414
6489
|
appendToTextPart(part, rangeTag);
|