mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 18:55:22 +00:00
feat(pi-condense): vendor source and add summarizer retries
This commit is contained in:
@@ -0,0 +1,414 @@
|
||||
---
|
||||
title: Protected-tool chain safety, empty-summary guard, typecheck fix, token-budget auto-flush
|
||||
date: 2026-05-30
|
||||
status: draft
|
||||
branch: protected-chain-budget
|
||||
worktree: .worktrees/protected-chain-budget
|
||||
supersedes: untracked .agents/plans/034-protectedTools-dedup-pending-fixes.md (deleted)
|
||||
---
|
||||
|
||||
# Summary
|
||||
|
||||
Four changes shipped together. Three are correctness/hygiene fixes surfaced by the DCP-parity
|
||||
review; one is an opt-in feature (default-off).
|
||||
|
||||
| # | Item | Type | Default behavior change |
|
||||
|---|---|---|---|
|
||||
| 1 | `protectedTools` × chain compression: relocate protected outputs into the compressed-chain body instead of dropping them | correctness | none (only fires when `protectedTools` non-empty) |
|
||||
| 2 | Empty / truncated summary soft-fail guard | correctness | none (only changes the empty/`length` failure path) |
|
||||
| 3 | Test fixtures `fuseRangeSummary` typecheck fix | hygiene | none |
|
||||
| 4 | Token-budget auto-flush (`autoBudgetThreshold`) | feature, opt-in | none (`null` = off) |
|
||||
|
||||
**Regression invariant for the whole release:** with default config
|
||||
(`protectedTools: []`, `autoBudgetThreshold: null`) every code path below is inert and behavior
|
||||
is byte-identical to today. Items 1 and 4 only activate on explicit opt-in. Item 2 only changes
|
||||
behavior when the summarizer returns empty or truncated text (today a latent bug). Item 3 is
|
||||
test-only.
|
||||
|
||||
Dropped from the original review scope: the `selectEligible` "no-summary blocks next-oldest"
|
||||
claim (false positive — the loop `continue`s past no-summary chains and processes all remaining
|
||||
eligible chains; `chain-compressor.ts:88-95`), and the content-hash dedup normalization /
|
||||
args-blind concerns (downgraded to Minor; tracked separately, not in this release).
|
||||
|
||||
---
|
||||
|
||||
# Item 1 — protectedTools × chain compression
|
||||
|
||||
## Problem (confirmed against source)
|
||||
|
||||
`protectedTools` is an allowlist of tool names whose outputs must never be pruned
|
||||
(`types.ts` `ContextPruneConfig.protectedTools`, doc-comment: "outputs must NEVER be pruned or
|
||||
summarized"). Enforced at capture time: `turn_end` filters them out of the `CapturedBatch`
|
||||
(`index.ts:710-722`) so they are **never indexed** and stay verbatim in context.
|
||||
|
||||
Chain range-compression violates this:
|
||||
|
||||
1. `detectChains` adds **every** middle `toolCallId` to `ChainRange.middleToolCallIds`, protected
|
||||
included (`chain-detector.ts:62-71`).
|
||||
2. `compressEligible` sets `entry.droppedToolCallIds = chain.middleToolCallIds` with no protection
|
||||
filter (`chain-compressor.ts:118`). The eligibility gate `hasPerBatchSummaryCoveringAny`
|
||||
passes whenever **any one** sibling tool in the chain was summarized
|
||||
(`chain-compressor.ts:92`), so a mixed chain (≥1 summarized + ≥1 protected) is eligible.
|
||||
3. `applyChainCompressions` drops any `toolResult` whose id ∈ `droppedToolCallIds`
|
||||
(`chain-range-prune.ts:79`).
|
||||
|
||||
Because protected outputs are never indexed, a dropped protected output is **unrecoverable** via
|
||||
`context_tree_query`. Trigger: `protectedTools` non-empty (e.g. the documented `todowrite`/
|
||||
`todoread`) + `chainCompression.enabled` (default on). Default `protectedTools: []` masks it
|
||||
out-of-box, but the documented opt-in breaks the contract.
|
||||
|
||||
## Why not the obvious fix (skip mixed chains — rejected "Approach C")
|
||||
|
||||
Skipping any chain that contains a protected tool would forfeit a large fraction of compaction for
|
||||
exactly the users who enable the feature: a chain is `[user] → [tool turns…] → [text]`
|
||||
(`chain-detector.ts:30`), i.e. usually a whole task, and high-frequency protected tools
|
||||
(`todowrite`/`todoread`/`edit`) recur throughout most tasks. Most chains would become ineligible.
|
||||
Rejected.
|
||||
|
||||
Also rejected — "Approach B1" (exclude protected ids from `droppedToolCallIds`, keep the protected
|
||||
turn in place): a mixed assistant turn that calls both a protected and a non-protected tool is
|
||||
dropped as a whole (`chain-range-prune.ts:84-87` drops the assistant message if *any* of its
|
||||
toolCall ids is dropped), which would orphan the surviving protected `toolResult` (no matching
|
||||
`toolCall`). pi-ai's `insertSyntheticToolResults` repairs orphaned *calls*, not orphaned
|
||||
*results*, so this risks breaking role alternation.
|
||||
|
||||
## Design (chosen — "Approach B", DCP-faithful: relocate)
|
||||
|
||||
Keep the contiguous range-drop exactly as today (protected ids stay in `droppedToolCallIds`, so the
|
||||
protected `toolResult` and its assistant turn drop cleanly — no orphans, no alternation risk), but
|
||||
**relocate** the protected outputs' verbatim text into the synthetic `<compressed-chain>` body.
|
||||
|
||||
Protected outputs are never indexed, but the `context` event always re-runs `pruneMessages` on the
|
||||
**raw branch** (session is never mutated), so the protected `toolResult` is always present at render
|
||||
time. Therefore the verbatim text is pulled live from `messages` during
|
||||
`applyChainCompressions` — nothing large is persisted in the entry (only the id list).
|
||||
|
||||
### Data model change
|
||||
|
||||
`ChainRange` (detection-time, `types.ts`): add
|
||||
|
||||
```ts
|
||||
/** Subset of middleToolCallIds whose tool name ∈ protectedTools (detection-time fact). */
|
||||
protectedToolCallIds: string[];
|
||||
```
|
||||
|
||||
`ChainCompressionEntry` (persisted, `types.ts`): add
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Subset of droppedToolCallIds whose tool was user-protected. Membership is decided
|
||||
* per call by tool name (every call whose name ∈ protectedTools), not a per-id allowlist —
|
||||
* so every todowrite call in the chain is captured regardless of its id. Their verbatim
|
||||
* ToolResultMessage text is relocated into the synthetic <compressed-chain> body
|
||||
* at render time (pulled live from the raw branch) instead of being dropped.
|
||||
* Absent/empty ⇒ no protected outputs in this chain (identical to pre-feature behavior).
|
||||
*/
|
||||
protectedToolCallIds?: string[];
|
||||
```
|
||||
|
||||
### Code changes
|
||||
|
||||
1. **`src/chain-detector.ts`** — `detectChains(messages, protectedTools: string[] = [])`:
|
||||
- Build a `Set<string>` from `protectedTools`.
|
||||
- While collecting middle ids, also record protected ones. The assistant branch has
|
||||
`b.name`+`b.id` (`collectToolCallIds` currently drops the name); the toolResult branch has
|
||||
`msg.toolName`+`msg.toolCallId`. Track a per-chain `protectedIds: Set<string>` alongside
|
||||
`middleIds` and populate from both branches when the name is in the protected set.
|
||||
- Emit `protectedToolCallIds: [...protectedIds]` on every `ChainRange` (including the
|
||||
`emitInterrupted` path). Default param `[]` ⇒ always empty ⇒ current behavior.
|
||||
|
||||
2. **`src/chain-compressor.ts`** — `compressEligible`:
|
||||
- Copy `protectedToolCallIds` from the `ChainRange` onto the `ChainCompressionEntry`
|
||||
(`...(chain.protectedToolCallIds?.length ? { protectedToolCallIds: chain.protectedToolCallIds } : {})`).
|
||||
- `droppedToolCallIds` is unchanged (still `chain.middleToolCallIds`, protected included).
|
||||
- No new dependency: names were resolved in the detector; the compressor only forwards ids.
|
||||
|
||||
3. **`src/chain-range-prune.ts`** — `applyChainCompressions` + `buildSyntheticChainMessage`:
|
||||
- Before the rebuild loop, build `protectedTextByBlock: Map<blockId, {tool: string; text: string}[]>`
|
||||
by scanning `messages` once: for each `toolResult` whose `toolCallId` is in some entry's
|
||||
`protectedToolCallIds`, push `{ tool: msg.toolName, text: extractToolResultText(msg) }` under
|
||||
that entry's `blockId`. Preserve message order.
|
||||
- `buildSyntheticChainMessage` gains a `protectedOutputs: {tool; text}[]` param. Render each as a
|
||||
verbatim block appended after the (substituted) summary, inside the `<compressed-chain>` tag:
|
||||
|
||||
```
|
||||
<compressed-chain id="b7" tools="t1,t2">
|
||||
{resolvedSummary}
|
||||
|
||||
<protected-output tool="todowrite">
|
||||
{verbatim text}
|
||||
</protected-output>
|
||||
</compressed-chain>
|
||||
```
|
||||
|
||||
No `id=` attribute on `<protected-output>` (not recoverable via `context_tree_query` — must not
|
||||
look queryable). Empty `protectedOutputs` ⇒ no block ⇒ byte-identical to today's render.
|
||||
- Protected `toolResult`s are still dropped by the existing `droppedToolCallIds` filter — the
|
||||
relocation does not change the drop set.
|
||||
|
||||
4. **`src/batch-capture.ts`** — extract the inline toolResult text logic (lines 28-38) into an
|
||||
exported `extractToolResultText(msg): string` and reuse it in both `captureBatch` and
|
||||
`chain-range-prune` so the relocated text is identical to what capture would have produced.
|
||||
|
||||
5. **`index.ts`** — pass `currentConfig.value.protectedTools` to both `detectChains` call sites
|
||||
(`:521` flush path via `withClosingMessage`, `:859` `/pruner compact`).
|
||||
|
||||
6. **Session rebuild** — confirm the `session_start` rebuild that replays `CUSTOM_TYPE_CHAIN`
|
||||
entries into the chain registry preserves the new optional `protectedToolCallIds` (it should, if
|
||||
it stores the entry as-is; verify in `indexer.ts` `registerChain` / rebuild and add coverage).
|
||||
|
||||
### Edge cases
|
||||
|
||||
- **Chain of only protected tools** (no summarizable sibling): `hasPerBatchSummaryCoveringAny`
|
||||
returns false (protected tools are never summarized) ⇒ chain ineligible ⇒ never compressed.
|
||||
Naturally safe; no special-casing.
|
||||
- **Protected toolResult missing at render** (truncated/edited history): skip that id (best-effort);
|
||||
the rest of the chain still compresses.
|
||||
- **Protected tool's call args** (in the assistant `toolCall` block) are dropped with the turn. The
|
||||
contract protects *outputs*, not call args (matches DCP). Documented, not preserved. The relocated
|
||||
block is labeled `<protected-output tool="name">`; the feature targets self-describing state tools
|
||||
(`todowrite`/`todoread`) whose output stands alone. Protecting a tool whose output is meaningless
|
||||
without its args (e.g. `read_file`, where the path lives in the args) is a misuse, not a goal of
|
||||
this release.
|
||||
- **Large protected outputs** inflate the synthetic message. Inherent to "keep verbatim" — net
|
||||
context size is comparable to leaving the raw `toolResult` in place. No size guard in this release.
|
||||
- **`protectedTools` changed after compression**: the entry's `protectedToolCallIds` is fixed at
|
||||
compression time, so already-compressed chains keep their relocation regardless of later config
|
||||
edits. Correct and stable — by design.
|
||||
|
||||
---
|
||||
|
||||
# Item 2 — empty / truncated summary guard
|
||||
|
||||
## Problem (confirmed)
|
||||
|
||||
`runSummarization` (`summarizer.ts:171-180`) returns `{ summaryText: llmText, usage }` where
|
||||
`llmText` is the joined text blocks. Two unsafe cases pass through:
|
||||
|
||||
- **Empty text** (e.g. a reasoning model emits only `thinking`, no `text`): `llmText === ""`. In
|
||||
`flushPending`, `summaryText = "" + formatSummaryToolCallRefs(refs)` (footer only),
|
||||
`shouldSkipOversized = footer.length > batchRawCharCount` is usually false ⇒ the footer-only
|
||||
"summary" is injected and the raw batch is pruned behind it (`index.ts:393-394`).
|
||||
- **Truncated** (`response.stopReason === "length"`): currently unhandled (only `aborted`/`error`
|
||||
are; `summarizer.ts:159-166`) ⇒ a partial summary is treated as complete.
|
||||
|
||||
## Design
|
||||
|
||||
Treat both as a **soft failure** (return `null`). `flushPending` already maps a `null` per-batch
|
||||
result to `firstFailureIndex` → `restoreBatches(...)`, keeping the raw batch in context
|
||||
(`index.ts:431-447`), and reports `summarizer-failed` only if nothing processed. This is the exact
|
||||
desired behavior — no raw pruned behind a useless summary, retried next flush.
|
||||
|
||||
### Code change (`src/summarizer.ts`)
|
||||
|
||||
Extract a pure decision helper for testability and wire it into `runSummarization` after computing
|
||||
`llmText`:
|
||||
|
||||
```ts
|
||||
/** A summary is usable only if it has non-whitespace text and was not truncated. */
|
||||
export function isUsableSummary(llmText: string, stopReason: string): boolean {
|
||||
return llmText.trim().length > 0 && stopReason !== "length";
|
||||
}
|
||||
```
|
||||
|
||||
In `runSummarization`, after building `llmText`:
|
||||
|
||||
```ts
|
||||
if (!isUsableSummary(llmText, response.stopReason)) return null;
|
||||
```
|
||||
|
||||
Both the per-batch (`summarizeBatch`) and range-fusion (`summarizeRange`) callers already handle
|
||||
`null`: range fusion falls back to per-batch concatenation (`chain-compressor.ts:104-112`), per-batch
|
||||
restores the batch. No caller changes needed.
|
||||
|
||||
---
|
||||
|
||||
# Item 3 — test fixtures typecheck fix
|
||||
|
||||
## Problem (confirmed)
|
||||
|
||||
`fuseRangeSummary` is a required field of `ChainCompressionConfig` (`types.ts`), but two test
|
||||
fixtures omit it:
|
||||
|
||||
- `src/pruner.test.ts:31` → TS2741
|
||||
- `src/pruner.test.ts:299` → TS2345
|
||||
|
||||
Runtime tests pass (the field is unread by those cases), but the repo does not typecheck. AGENTS.md
|
||||
mandates a clean `tsc` before commit.
|
||||
|
||||
## Design
|
||||
|
||||
Set `fuseRangeSummary: false` on both literals (concrete, no runtime effect):
|
||||
- `:31` is the shared `enabledCC` const — these tests pass no `fuseRange` dep and assert no fusion, so `false` keeps behavior.
|
||||
- `:299` is an inline literal with `enabled: false`, so the value is inert.
|
||||
|
||||
Two-line change.
|
||||
|
||||
**Done-when:** the AGENTS.md typecheck command runs clean across the whole repo **including tests**:
|
||||
|
||||
```
|
||||
bun x tsc --noEmit --target es2022 --module nodenext --moduleResolution nodenext --strict \
|
||||
--skipLibCheck --allowJs --esModuleInterop --resolveJsonModule --lib es2022 --types node \
|
||||
index.ts src/*.ts src/*.test.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Item 4 — token-budget auto-flush (opt-in)
|
||||
|
||||
## Motivation + DCP lineage
|
||||
|
||||
DCP nudges/compresses when context approaches `maxContextLimit`. This fork only triggers flushes on
|
||||
turn/agent-message/context-tag boundaries — a long task can pile up raw tool output and approach the
|
||||
context limit *between* flush boundaries (e.g. many tool turns before the final text response in
|
||||
`agent-message` mode). A single opt-in threshold closes that gap: when context usage crosses the
|
||||
threshold, flush the pending batches immediately regardless of `pruneOn`.
|
||||
|
||||
## Design
|
||||
|
||||
### Config (`src/types.ts`)
|
||||
|
||||
Add to `ContextPruneConfig`:
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Token-budget auto-flush trigger. When set to a fraction in (0, 1] (a 0–1 share of the
|
||||
* context window, NOT a 0–100 percentage; e.g. 0.8 = flush at 80% of the window), a flush of all
|
||||
* pending batches is forced at the end of any tool-using turn once context usage
|
||||
* (tokens / contextWindow) reaches the threshold — regardless of `pruneOn`. This is an
|
||||
* ADDITIONAL trigger layered on top of `pruneOn`, not a replacement; it lets long tasks
|
||||
* compact before hitting the context limit instead of waiting for the next flush boundary.
|
||||
*
|
||||
* null (default) = disabled, exactly preserving pre-feature behavior. Out-of-range values
|
||||
* (<= 0 or > 1) are treated as disabled.
|
||||
*/
|
||||
autoBudgetThreshold: number | null;
|
||||
```
|
||||
|
||||
`DEFAULT_CONFIG.autoBudgetThreshold = null`.
|
||||
|
||||
### Pure helper (`src/budget.ts`, new)
|
||||
|
||||
```ts
|
||||
import type { ContextUsage } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
/**
|
||||
* True iff a budget-triggered flush should fire. Computes the ratio ourselves
|
||||
* (tokens / contextWindow, a 0–1 fraction) rather than ContextUsage.percent (a 0–100 value,
|
||||
* null when tokens is null). tokens is also null right after a compaction — guarded below.
|
||||
*/
|
||||
export function shouldBudgetFlush(usage: ContextUsage | undefined, threshold: number | null): boolean {
|
||||
if (threshold == null || threshold <= 0 || threshold > 1) return false;
|
||||
if (!usage || usage.tokens == null || !(usage.contextWindow > 0)) return false;
|
||||
return usage.tokens / usage.contextWindow >= threshold;
|
||||
}
|
||||
```
|
||||
|
||||
(`ContextUsage` = `{ tokens: number | null; contextWindow: number; percent: number | null }`,
|
||||
verified in `@mariozechner/pi-coding-agent` types. `getContextUsage(): ContextUsage | undefined` is
|
||||
on `ExtensionContext`.)
|
||||
|
||||
### Trigger wiring (`index.ts`, `turn_end`)
|
||||
|
||||
At the **tail** of the `turn_end` handler (after the existing `pruneOn` dispatch block, ~`:755`):
|
||||
|
||||
```ts
|
||||
if (
|
||||
currentConfig.value.pruneOn !== "every-turn" &&
|
||||
shouldBudgetFlush(ctx.getContextUsage?.(), currentConfig.value.autoBudgetThreshold) &&
|
||||
!isFlushing
|
||||
) {
|
||||
const before = pendingBatches.length;
|
||||
if (before > 0) {
|
||||
safeNotify(ctx, `pruner: context budget reached — compacting ${before} pending turn(s)`, "info");
|
||||
await flushPending(ctx, { delivery: "session" });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `every-turn` already flushed above ⇒ excluded (no double flush).
|
||||
- `isFlushing` (existing guard, `index.ts:63`) makes this race-safe; `flushPending` also re-checks
|
||||
and returns `already-flushing`.
|
||||
- Budget flush only summarizes **completed** turns' pending batches. The current open chain is not
|
||||
closed here (the detector drops open chains), so chain compression is untouched mid-task — only
|
||||
per-batch summarization runs. Safe.
|
||||
- Guarded on `before > 0` so a hot-but-empty queue (everything already pruned) is a no-op.
|
||||
|
||||
### Settings surface (`src/commands.ts`, `src/config.ts`)
|
||||
|
||||
- `config.ts`: normalize/clamp on load — coerce non-number/out-of-range to `null`.
|
||||
- `commands.ts`: add a cycling preset to the `/pruner` settings overlay and a
|
||||
`AUTO_BUDGET_PRESETS` list in `types.ts`:
|
||||
|
||||
```ts
|
||||
export const AUTO_BUDGET_PRESETS: { value: string; label: string }[] = [
|
||||
{ value: "0", label: "Off (default)" },
|
||||
{ value: "0.6", label: "60%" },
|
||||
{ value: "0.7", label: "70%" },
|
||||
{ value: "0.8", label: "80%" },
|
||||
{ value: "0.9", label: "90%" },
|
||||
];
|
||||
```
|
||||
|
||||
`"0"` is the disabled sentinel (maps to `null`), consistent with `minBatchChars`'s `"0"` pattern.
|
||||
|
||||
---
|
||||
|
||||
# Documentation deliverables (required — feature must be well-documented)
|
||||
|
||||
| File | Addition |
|
||||
|---|---|
|
||||
| `src/types.ts` | Doc-comments on `autoBudgetThreshold` (units = fraction 0–1, `null`=off, out-of-range=off) and on the two new `protectedToolCallIds` fields (already specified above). |
|
||||
| `README.md` | New config-table row for `autoBudgetThreshold`. New subsection **"Token-budget auto-flush"**: what it does, default off, fraction semantics (`0.8` = 80% of the window, not `80`), that it's *additional* to `pruneOn`, the `tokens==null`-after-compaction caveat, and the `/pruner` setting. One line in the protectedTools section noting protected outputs are now preserved (relocated into the compressed-chain block) when their chain is compressed. |
|
||||
| `PRUNING.md` | Short subsection: budget trigger as an additional flush trigger orthogonal to `pruneOn`, why we compute `tokens/contextWindow` as a 0–1 fraction (percent is 0–100 and null when tokens is null; tokens is null right after compaction), and the DCP `maxContextLimit` lineage. Update the chain-compression section to document protected-output relocation (Approach B) and why skip-the-chain (C) was rejected. |
|
||||
| `AGENTS.md` | Update the `CUSTOM_TYPE_CHAIN` row to note the entry now carries optional `protectedToolCallIds`. |
|
||||
|
||||
---
|
||||
|
||||
# Testing strategy
|
||||
|
||||
All pure-unit where possible; existing 108 tests must stay green and `tsc` must be clean.
|
||||
|
||||
| Item | Test | Key cases |
|
||||
|---|---|---|
|
||||
| 1 | `chain-detector` | `protectedTools` populates `ChainRange.protectedToolCallIds` for both assistant-call and toolResult branches; empty/omitted ⇒ `[]`; interrupted-chain path also populated. |
|
||||
| 1 | `chain-range-prune` | Mixed chain: non-protected results dropped + summarized, protected result dropped from position **and** rendered verbatim inside `<compressed-chain>` under `<protected-output tool="…">`; pure chain (no protected) renders byte-identical to pre-change; idempotent across two passes; protected id missing from messages ⇒ skipped, rest compresses; relocated `<protected-output>` is NOT registered in the tool-call index ⇒ not recoverable via `context_tree_query` and carries no queryable id. |
|
||||
| 1 | `chain-compressor` | `protectedToolCallIds` copied from range onto entry; all-protected chain stays ineligible (no per-batch summary). |
|
||||
| 1 | session rebuild | `CUSTOM_TYPE_CHAIN` replay preserves `protectedToolCallIds`. |
|
||||
| 2 | `summarizer` (new file) | `isUsableSummary`: empty/whitespace ⇒ false; `stopReason==="length"` ⇒ false; normal ⇒ true. (summarizer.ts currently has zero tests.) |
|
||||
| 3 | n/a | `tsc` clean across `index.ts src/*.ts src/*.test.ts`. |
|
||||
| 4 | `budget` (new file) | `shouldBudgetFlush`: `null`/`0`/`>1` threshold ⇒ false; `usage` undefined ⇒ false; `tokens==null` ⇒ false; `contextWindow<=0` ⇒ false; exactly-at ⇒ true; over ⇒ true; under ⇒ false. |
|
||||
|
||||
**Done-when:** new + existing tests green (`bun test`); `tsc` clean; manual smoke per AGENTS.md
|
||||
(`pi -e ./index.ts …` against an isolated `$PI_CODING_AGENT_DIR`) confirming (a) a protected-tool
|
||||
output survives a chain compression inside the synthetic block, and (b) setting
|
||||
`autoBudgetThreshold` forces an early flush.
|
||||
|
||||
---
|
||||
|
||||
# Implementation order
|
||||
|
||||
The four items are **mutually orthogonal** — no shared runtime path and no shared mutable test
|
||||
state. Specifically, Item 4's budget trigger flushes pending per-batch summaries on `turn_end` and
|
||||
**never invokes chain compression** (chains only close on `message_end`), so Item 1's protected-output
|
||||
relocation does not affect Item 4's behavior or its tests. There is therefore no hard ordering
|
||||
requirement.
|
||||
|
||||
Recommended commit/PR sequence for **review hygiene only** (not a dependency): #3 (typecheck — makes
|
||||
the repo green immediately) → #2 (empty-summary guard) → #1 (protected-tool relocation) → #4
|
||||
(token-budget). Each can land and be tested in isolation.
|
||||
|
||||
---
|
||||
|
||||
# Out of scope
|
||||
|
||||
- Content-hash dedup normalization / args-blind keying (Minor; separate follow-up).
|
||||
- `selectEligible` change (withdrawn false positive).
|
||||
- DCP parity items deliberately deferred (not regressions — features we don't yet have):
|
||||
`turnProtection`, `protectedFilePatterns`, `protectTags` (DCP's `<protect>` content tags),
|
||||
`/pruner decompress|recompress`, `focus` param on the compress tool, message-mode compression.
|
||||
- DCP `protectUserMessages`: N/A in this harness — this extension never prunes user messages (only
|
||||
tool results and assistant turns), so user messages are already always preserved.
|
||||
- Size guard on relocated protected outputs (revisit only if large protected outputs bloat context
|
||||
in practice).
|
||||
Reference in New Issue
Block a user