Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ The nudge system tells the model *when* to compress. It implements:

✅ **Engine complete** — 23 source modules, 167 tests, typecheck + build clean. 3-tier compression, growth-gated nudges, emergency truncation,, fork-recovery, batch merge, composable node pipeline. Ready for adapter authoring.

> **Known limitation:** protected tool messages are excluded from *ref assignment* (marked `BLOCKED`) but, unlike opencode-acp's Bug 39, are **not** hard-excluded from an explicitly-referenced compress range. A model that names a range covering a `BLOCKED` message will compress it. This is an intentional simplification for the pure core; adapters that need hard-exclusion should pre-split ranges before calling `applyCompression`.
> **Protected tool messages:** protected tool calls (per `config.protectedTools`) and their paired tool-results are hard-excluded from compression — they are dropped from the compressible set and from the new block's `effectiveMessageIds`, so they stay fully visible and are never folded into a summary. This matches opencode-acp's Bug 39 fix. The soft-protected recent zone (`preserveRecentMessages` / last user message) is handled separately: messages there are excluded from the range but do not fail it (an entirely-protected range fails with a clear error).

## License

Expand Down
11 changes: 11 additions & 0 deletions src/compress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,17 @@ function applySingleRange(input: SingleRangeInput): SingleRangeOutcome {
input.config,
);

// filterProtectedToolMessages drops protected tool calls (and their paired
// results) from the compressible set. They must also leave effectiveMessageIds,
// otherwise the block would record them as covered and hide them from view.
// (Bug 39: protected tool messages folded into a block.)
if (filteredIds.length < directMessageIds.length) {
const kept = new Set(filteredIds);
for (const id of directMessageIds) {
if (!kept.has(id)) effectiveMessageIds.delete(id);
}
}

// SOFT PROTECTION: the recent-N / last-user-message zone is advisory-only at
// compress time. Instead of failing the whole range when it brushes protected
// messages, exclude those messages and proceed with the rest (so the model
Expand Down
10 changes: 10 additions & 0 deletions tests/protected-content.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ test("Feature 2: protected tool-call is excluded from compression range", () =>
assert.ok(!block.directMessageIds.includes("c"), "tool-result 'c' should be excluded");
assert.ok(block.directMessageIds.includes("a"), "regular msg 'a' should remain");
assert.ok(block.directMessageIds.includes("d"), "regular msg 'd' should remain");
// Bug 39 regression: effectiveMessageIds must also exclude protected tool
// messages, otherwise the block would mark them as covered and hide them.
assert.ok(!block.effectiveMessageIds.includes("b"), "tool-call 'b' excluded from effective coverage");
assert.ok(!block.effectiveMessageIds.includes("c"), "tool-result 'c' excluded from effective coverage");
assert.ok(block.effectiveMessageIds.includes("a"), "regular msg 'a' in effective coverage");
assert.ok(block.effectiveMessageIds.includes("d"), "regular msg 'd' in effective coverage");
});

test("Feature 2: protected tool messages are filtered out, not appended", () => {
Expand All @@ -87,6 +93,10 @@ test("Feature 2: protected tool messages are filtered out, not appended", () =>
const block = result.state.blocks[0]!;
assert.ok(!block.directMessageIds.includes("b"), "protected skill tool-call excluded from compressed set");
assert.ok(!block.directMessageIds.includes("c"), "protected skill tool-result excluded");
// Bug 39 regression: effectiveMessageIds must match directMessageIds here
// (no consumed blocks), so protected messages are excluded from both.
assert.ok(!block.effectiveMessageIds.includes("b"), "protected skill tool-call excluded from effective coverage");
assert.ok(!block.effectiveMessageIds.includes("c"), "protected skill tool-result excluded from effective coverage");
assert.ok(!block.summary.includes("Protected:"), "no protected content folded into summary");
assert.ok(!block.summary.includes('{"name":"git-master"}'), "protected tool content not leaked into summary");
assert.equal(block.summary, validSummary, "summary is exactly what the author wrote");
Expand Down
Loading