@cad0p/pi-tree-navigator 0.1.3 → 0.2.0-20260917.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/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.2.0] - 2026-09-15
6
+
7
+ <!-- USER-EDITABLE SECTION START -->
8
+ - **Cache-hit rewind summaries** ([#33](https://github.com/cad0p/pi-tree-navigator/issues/33)) — summary requests now mirror the live prompt prefix and ride the same prompt cache: ~98–99% cache-served (~20–24k tokens read, a few hundred fresh) instead of a cold re-bill (~77k tokens / $0.0075 → ~$0.0004 warm). No fork or patched host needed.
9
+ - **Correctness first** — evidence-losing segments (compaction-crossing, dropped branch evidence) still re-bill cold; `PI_NAVIGATE_TREE_SUMMARY_CACHE=0` opts out; misses ≥20k tokens / ≥$0.10 show a gated TUI notice (hits silent); numbers always in `details.summaryCache`.
10
+ - **Batched rewinds refused** ([#37](https://github.com/cad0p/pi-tree-navigator/issues/37)) — `rewind` sharing an assistant batch with sibling tool calls is refused before any mutation; re-issue it solo.
11
+ - **Refusals render as failures** ([#40](https://github.com/cad0p/pi-tree-navigator/issues/40)) — red error row + `isError: true` in the transcript (the returned flag was ignored; promoted via the public `tool_result` event).
12
+ <!-- USER-EDITABLE SECTION END -->
13
+
14
+ ### 🚀 Features
15
+
16
+ - *(navigate-tree)* Preserve prompt-cache prefix on rewind summaries (closes #33)
17
+
18
+ ### 🐛 Bug Fixes
19
+
20
+ - *(rewind)* Refuse sibling-batched rewinds — rewind must be the only tool call in its batch (closes #37)
21
+ - *(rewind)* Surface refusals as failed tool calls (closes #40)
22
+
23
+
5
24
  ## [0.1.3] - 2026-09-14
6
25
 
7
26
  <!-- USER-EDITABLE SECTION START -->
package/README.md CHANGED
@@ -4,6 +4,11 @@
4
4
 
5
5
  Lets a pi agent anchor named milestones in its own conversation, then collapse work between them into a model-generated `branch_summary` to free up context — without tripping Anthropic's `tool_use` ↔ `tool_result` validation, and with the freed context immediately available to the next assistant turn (even within the same `prompt()` call).
6
6
 
7
+ This way you can have a session that looks like this :)
8
+
9
+ <img width="2686" height="708" alt="image" src="https://github.com/user-attachments/assets/dc3beecf-c18c-4ab2-b5a8-19b9f94d1498" />
10
+
11
+
7
12
  ## Install
8
13
 
9
14
  Stable npm release:
@@ -38,8 +43,9 @@ This repo uses [`cad0p/semver-calver-release`](https://github.com/cad0p/semver-c
38
43
  - Peer dependencies (the source of truth is `package.json` `peerDependencies`):
39
44
  - `@earendil-works/pi-coding-agent >=0.81.0`
40
45
  - `@earendil-works/pi-agent-core >=0.81.0`
46
+ - `@earendil-works/pi-tui >=0.81.0` (used by the `renderResult` cache-notice transcript line: `Container`, `Spacer`, `Text`).
41
47
  - `typebox ^1.0.0` (used to declare the tool's parameter schema; bundled with pi but listed explicitly so a standalone install resolves correctly).
42
- - The reflection bootstrap depends on two plain (not `#`-private) internal pi/agent fields: `AgentSession.prototype.prompt` (patched for session capture) and `agent.state.messages` (refreshed after a rewind so the next prompt snapshots the rewound chain). Per-turn in-loop context refresh runs through the **public** `context` extension event (see “In-loop context refresh” below) — no `agent.prepareNextTurn*` reflection. Verified against pi 0.81.0 / 0.83.0 / 0.84.2.
48
+ - The reflection bootstrap depends on plain (not `#`-private) internal pi/agent fields: `AgentSession.prototype.prompt` (patched for session capture) and `agent.state.messages` (refreshed after a rewind so the next prompt snapshots the rewound chain). Since #33 the cache-preserving summary request also **reads** `agent.state.tools`, `agent.state.systemPrompt`, and `agent.thinkingBudgets` off the same captured session (read-only; `ctx.getSystemPrompt()` is the public primary for the prompt), plus `settingsManager.getShowCacheMissNotices()` to gate the cache-notice transcript line. Per-turn in-loop context refresh runs through the **public** `context` extension event (see “In-loop context refresh” below) — no `agent.prepareNextTurn*` reflection. Verified against pi 0.81.0 / 0.83.0 / 0.84.2.
43
49
 
44
50
  ## What you get
45
51
 
@@ -48,13 +54,51 @@ A single agent-callable tool, `navigate_tree`, with three actions:
48
54
  | action | params | effect |
49
55
  |---|---|---|
50
56
  | `anchor` | `name` | Label the current point in the conversation as a milestone. |
51
- | `rewind` | `labelStart`, `labelEnd`, `summaryFocus` | Collapse work between `labelStart` and the current leaf into a `branch_summary` entry. The summary is itself labeled with `labelEnd`, so you can chain rewinds. `summaryFocus` is required (non-trivial focus required; floor enforced at runtime by `MIN_SUMMARY_FOCUS_LENGTH`). Despite the verb, `rewind` does not restore prior state — it forks a sibling branch from `labelStart` and continues forward from a model-generated summary; the original subtree is preserved on disk but no longer on the active path. |
57
+ | `rewind` | `rewindTo`, `newLabel`, `summaryFocus` | Collapse work between `rewindTo` and the current leaf into a `branch_summary` entry. The summary is itself labeled with `newLabel`, so you can chain rewinds. `summaryFocus` is required (non-trivial focus required; floor enforced at runtime by `MIN_SUMMARY_FOCUS_LENGTH`). Despite the verb, `rewind` does not restore prior state — it forks a sibling branch from `rewindTo` and continues forward from a model-generated summary; the original subtree is preserved on disk but no longer on the active path. |
52
58
  | `list` | — | Show all anchors on the active branch with cumulative context %. |
53
59
 
54
- `name` (written by `anchor`) and `labelEnd` (written by `rewind`) both share the reserved `anchor:` label prefix; `labelStart` resolves against that same namespace. Every label written by `anchor` and every `labelEnd` written by `rewind` is referenceable by any subsequent `rewind`'s `labelStart`, and `list` shows all of them.
60
+ `name` (written by `anchor`) and `newLabel` (written by `rewind`) both share the reserved `anchor:` label prefix; `rewindTo` resolves against that same namespace. Every label written by `anchor` and every `newLabel` written by `rewind` is referenceable by any subsequent `rewind`'s `rewindTo`, and `list` shows all of them.
55
61
 
56
62
  **Anchoring is mandated, not suggested.** On every agent start the extension appends a one-line mandate to the end of the system prompt (`before_agent_start`), gated on the tool being active: `navigate_tree: gather all context, then anchor \`context-gathered\`; list anchors and rewind after every milestone or rabbit hole / dead end.` The append lands after project context and skills, is re-applied on every prompt, and survives compaction — unlike the `promptGuidelines` bullet it replaced. ~35 tokens, constant for prompt caching.
57
63
 
64
+ ## Rewind hint (optional)
65
+
66
+ Long autonomous sessions can blow through context silently — the model can't read pi's footer, and `rewind` is a targeted alternative to pi's lossy auto-compaction, but nothing tells the agent *when* to use it. The rewind hint is an opt-in `turn_end` observer that fires **once per crossing**: when context reaches the configured percentage, the agent gets a persisted nudge to persist what matters to files and rewind to an anchor. Default: **disabled**.
67
+
68
+ Enable it with a `tree-navigator.json` config in either layer:
69
+
70
+ | Layer | Path |
71
+ |---|---|
72
+ | Global | `join(getAgentDir(), "tree-navigator.json")` (`PI_CODING_AGENT_DIR`-aware) |
73
+ | Project | `join(cwd, CONFIG_DIR_NAME, "tree-navigator.json")` (`.pi/` by default; read only when the project is trusted — untrusted projects are ignored silently) |
74
+
75
+ ```json
76
+ { "rewindHintAtPercent": "90" }
77
+ ```
78
+
79
+ - Value: integer **20–95** inclusive. The canonical form is a string (`"90"`); a bare number (`90`) is also accepted.
80
+ - Explicit-disable sentinels: `null`, `false`, `"off"`, `"disabled"` (case-insensitive) — the project layer can switch off a global value.
81
+ - Precedence: a project key, when present, wins (including sentinels). Absent → global. Absent everywhere → disabled.
82
+ - Invalid value (out of range, decimal, non-numeric string, `true`, object): the hint stays off for the session — no fallback to the other layer's threshold — and a warning is shown when the session has a UI (silent headless, like every `ui.notify`). A malformed file (non-ENOENT read error, invalid JSON, non-object root) warns and makes that layer contribute nothing, so the other layer still applies; a missing file is silent.
83
+ - One hint per crossing: fires when `percent >= threshold`, re-arms only after percent drops back below it (a rewind or compaction); no escalation, no re-fire while spent.
84
+
85
+ When the crossing has at least one `anchor:` label on the active branch, the agent receives a persisted custom message. Mid-run this steers the running loop (one reaction turn in the same run); when the agent is idle it only appends and waits for the user's next prompt, so an idle-agent rewind stays user-confirmed:
86
+
87
+ ```text
88
+ [navigate_tree hint] Context is at 90.0% of 1.0M — running low. Persist what
89
+ matters to files now, then list anchors and rewind to the appropriate one.
90
+ ```
91
+
92
+ With no anchors the extension sends **no model message** (a rewind is impossible) and shows a TUI-only warning explaining how to add one manually: `/tree`, select the entry to rewind to, press `shift+l`, label it `anchor:<name>` (e.g. `anchor:context-gathered`), then ask the agent to rewind to it.
93
+
94
+ ### Rewind hygiene (always on)
95
+
96
+ Independent of the hint, the tool ships three static `promptGuidelines` bullets, present whenever `navigate_tree` is active: pick the earliest anchor that still preserves what you need; **persist durable findings to files before rewinding** (the summary replaces the collapsed work, so anything unwritten is lost); and **don't rewind while a user decision or unresolved question is pending** — ask the user instead. The bullets are not config-gated (`registerTool` fixes the array at registration), cost ~62 tokens per request, and change the cached system-prompt prefix once per active session on upgrade (the provider-facing tool schema is unchanged).
97
+
98
+ ### Compaction guidance
99
+
100
+ The hint is an alternative to auto-compaction. Auto-compaction fires at `contextWindow − reserveTokens` (`compaction.reserveTokens`, default 16384) — ~98.4% of a 1M window, ~91.8% of 200k, ~87.2% of 128k — so on small windows a 90% hint can land at or after the compaction point. Recommended: disable auto-compaction (`"compaction": { "enabled": false }`) so running out mid-persist produces pi's loud context-window error instead of a silent lossy compaction. Keep compaction enabled only as a safety net if you'd rather the model continue and compact mid-write; the hint never overrides it.
101
+
58
102
  ## How it works
59
103
 
60
104
  A typical autonomous-loop pattern:
@@ -65,10 +109,10 @@ agent: navigate_tree(action="anchor", name="impl-start")
65
109
 
66
110
  agent: ...does work, runs tools, accumulates context to 30%...
67
111
 
68
- agent: navigate_tree(action="rewind", labelStart="impl-start", labelEnd="impl-end",
112
+ agent: navigate_tree(action="rewind", rewindTo="impl-start", newLabel="impl-end",
69
113
  summaryFocus="record only the public API of the parser
70
114
  and the open issue with edge case X")
71
- → [rewind 'impl-start' → 'impl-end'] · context 30.4% → 4.1% of 1.0M
115
+ → [rewind to 'impl-start' · collapsed as 'impl-end'] · context 30.4% → 4.1% of 1.0M
72
116
  → A branch_summary recording the work just collapsed has been appended
73
117
  to your context. Items under '### Done' are complete. ...
74
118
 
@@ -89,6 +133,30 @@ Why this is more involved than just calling pi's `branchWithSummary`:
89
133
 
90
134
  4. **`summaryFocus` is mandatory.** The summary is the only thing the agent will see of the collapsed work. The first time the agent uses `rewind`, blanket prompts produce vague summaries; subsequent rewinds are weaker. Forcing the agent to articulate `summaryFocus` (passed to pi's `generateBranchSummary` as `customInstructions`) measurably improves what survives.
91
135
 
136
+ ### Cache-preserving summary request (#33)
137
+
138
+ Upstream `generateBranchSummary` builds a *cold* standalone request: a generic summarization system prompt, the conversation serialized into a text blob, no tools, `cacheRetention: "none"`, a fresh session id, and no reasoning forwarding. The live turns the summary collapses were just prompt-cache-served, so the summary re-billed the whole branch input (measured ~77k tokens cold vs a few hundred warm on opencode-go). The upstream fork fix (`cad0p/pi` PR #3) cannot be imported — pi's extension loader aliases `@earendil-works/*` to the host process's own modules — so the extension rewrites the request at the `streamFn` seam it already injects. That seam receives the fully-built `(model, context, options)` triple *after* `completeSummarization` applied its cold choices, and before the wire call.
139
+
140
+ The wrapper (`cache-summary.ts`) replaces that triple with the live request shape:
141
+
142
+ - **system prompt** — `ctx.getSystemPrompt()` when available, else the reflected `agent.state.systemPrompt`.
143
+ - **tools** — the same live tool array (`agent.state.tools`), by reference.
144
+ - **messages** — the live projection (`buildContextEntries`) as structured `Message`s, minus the in-flight assistant (it was never in a cached prefix, and an unpaired `tool_use` followed by a user message is rejected by Anthropic). Pre-branch background is included so the bytes prefix-match the previous live request; boundary-orphan `tool_result`s are stripped and the `{first}` scope number is adjusted to the payload actually sent.
145
+ - **params** — `cacheRetention` (resolved from `PI_CACHE_RETENTION`, never hardcoded), the live `sessionId` (also used for the opencode routing header), `reasoning` from `pi.getThinkingLevel()` (`"off"` omitted), and `thinkingBudgets` when the host exposes them. The caller's `maxTokens` cap is stripped: live turns let pi-ai clamp `model.maxTokens` to the context, and gateways that key the cache on params must see the same value.
146
+
147
+ The history walks newest→oldest against `contextWindow − 16384` tokens, dropping the oldest background first; `compaction` / `branch_summary` entries get upstream's 0.9-slack retry so they survive truncation. A truncated request no longer prefix-matches live turns (system + tools still do) — same as the fork.
148
+
149
+ The summary instruction uses the eval-approved r5d prompt and **must not drift**: it is pinned byte-for-byte in `cache-summary.test.ts`. The fallback (cold) path keeps upstream's older branch prompt — intentional divergence for a degraded path.
150
+
151
+ Every live input is read defensively. When any is unavailable (no provider `streamSimple`, no captured session, no live tools, no system prompt) or when the kill switch is set, the wrapper delegates today's cold request — and that path is **not** special-cased for notices: the summary response is measured by the fork's miss detector either way (the legacy cold request reports `cacheRead≈0`, so it misses exactly when the numbers say so). **Hits are silent** — the session totals/footer already cover them and there is no read/fresh notice. A **miss** is shown only when it clears the display floor (≥20k tokens or ≥$0.10), as a **TUI transcript line** appended by the tool's `renderResult` — the same mechanism upstream pi uses for its other cache notices (a `Spacer(1)` plus a warning-text line after the tool-result body) — gated by pi's own `showCacheMissNotices` setting (default **off**). The copy is `Cache miss: <n> tokens re-billed[ (~$<n>)]`, prefixed `Cache miss after <n>m idle` once the gap spans the 5-minute cache TTL (the fork's `Cache miss after model switch` branch is retained for parity, but summary misses after a model switch are suppressed as expected re-billing). Cache text is **never** in the rewind tool-result content the model sees. When the setting is off, or in a headless run (e.g. `-p` / RPC without UI, where the renderer never runs), nothing is rendered; the same data is available via `details.summaryCache`, which carries `mode`, `fallbackReason`, `branchStartRetained`, `used`, `cacheRead`, `input`, `cacheWrite`, `hit`, `missedTokens`, `missedCost`, `idleMs`, `modelChanged`, and `notice` (the rendered notice string, or `null`). `mode` records whether a cache-preserving request was **built**; `used` records whether the wrapper actually **delegated** it — a built-but-undelegated request (stub summarizer, upstream "No content to summarize" before `streamFn`, or an early abort) has `mode: "live-prefix"` / `used: false` and no measurable usage. Kill switch: `PI_NAVIGATE_TREE_SUMMARY_CACHE=0`.
152
+
153
+ Two of those fallback reasons are **evidence guards**, not param mirroring: they deliberately refuse the cache path so the summary keeps full-fidelity evidence (correctness over a cache hit), and they are the only known cases where the extension's request would silently diverge from the legacy path:
154
+
155
+ - **`branch-crosses-compaction`** — `buildContextEntries()` applies the compaction cut: entries before the latest compaction's `firstKeptEntryId` are dropped from the live projection. When a rewind segment reaches older than that cut (anchor/target older than `firstKeptEntryId`), the cache payload would summarize the lossy compacted projection while the legacy path summarizes the raw segment, so the request falls back. The predicate is the id difference between the collapsed entries and the live projection — **not** "the segment contains a compaction": a segment that contains the compaction entry but whose target sits at/after `firstKeptEntryId` loses no evidence and still takes the cache path.
156
+ - **`branch-start-not-retained`** — `buildLiveSummaryMessages` found no collapsed-segment message in the payload (a labels-only segment) or the newest message alone exceeded the token budget. The cache payload would be background-only; the legacy path either summarizes the raw evidence or returns "No content to summarize" before any wire call.
157
+
158
+ `details.summaryCache.branchStartRetained` is kept for diagnostics; it is now always `true` on a live-prefix request, because `false` is treated as a real fallback.
159
+
92
160
  ### Synthetic assistant token bias
93
161
 
94
162
  The synthetic assistant we inject after each rewind carries the **post-rewind chain estimate** in `usage.totalTokens` (so `estimateContextTokens` reads a sensible baseline immediately after the move). The synthetic itself adds a ~50-token toolCall block re-emitted on every subsequent turn until the next rewind — that overhead is **not** reflected in any `usage.*` field, so future `estimateContextTokens` calls understate the chain by ~50 tokens until the next assistant turn writes a fresh usage block. Negligible at typical anchor cadence; mention if you're benchmarking exact token deltas, ignore otherwise.
@@ -97,19 +165,29 @@ The synthetic assistant we inject after each rewind carries the **post-rewind ch
97
165
 
98
166
  - **Brittle to pi version bumps.** The fix uses two independent reflection points on internals that aren't part of pi's public API: `AgentSession.prototype.prompt` (session capture) and `agent.state.messages` (refreshed after a rewind). A third reflection point (`agent.prepareNextTurn*`) was eliminated in v0.2.0 via the public `context` extension event; the per-turn systemPrompt/tools/model/thinkingLevel refreshes come from pi's own `_installAgentNextTurnRefresh` (construction-installed since 0.80.3). If a future pi release renames the two remaining fields, switches them to private (`#`) fields, or restructures the class hierarchy, this breaks. The extension fails loudly: `anchor` still works, `rewind` reports `⚠ reflection bootstrap missing — the rewind landed on disk but the next assistant turn may still see the pre-rewind context. Run \`/reload\` (or restart pi) to recover.`, and you'd see context corruption return on the next prompt.
99
167
 
100
- **Audited against pi 0.81.0 / 0.83.0 / 0.84.2 (2026-08):** three of the five reflection points have been eliminated — `agent.state.systemPrompt` and `agent.state.tools` reads (deleted with the `prepareNextTurn` double-wrap; no longer needed since pi's own per-turn wrapper and the public `ctx.getSystemPrompt()` / `pi.getAllTools()` cover them) and `agent.prepareNextTurnWithContext` (replaced by the public `context` extension event, which fires via `transformContext` before **every** LLM call). The remaining two are NOT eliminable for the tool-based design: `AgentSession.prototype.prompt` (no public per-prompt hook for tool executes — the `context` event fires too late to install hooks before `createLoopConfig`) and `agent.state.messages` (pi's own `navigateTree` refresh is only reachable via `ctx.navigateTree()`, which exists solely on `ExtensionCommandContext`, not the `ExtensionContext` a `tool.execute` receives).
168
+ **Audited against pi 0.81.0 / 0.83.0 / 0.84.2 (2026-08; #33 re-audit 2026-09):** two of the original five reflection points stay eliminated — `agent.prepareNextTurnWithContext` (replaced by the public `context` extension event, which fires via `transformContext` before **every** LLM call) and the `prepareNextTurn` double-wrap. The remaining surface is `AgentSession.prototype.prompt` (session capture) and `agent.state.messages` (post-rewind refresh); both are NOT eliminable for the tool-based design: there is no public per-prompt hook for tool executes, and pi's own `navigateTree` refresh lives on `ExtensionCommandContext`, not the `tool.execute` ctx. #33 adds four **read-only** reads on the same captured session — `agent.state.tools`, `agent.state.systemPrompt` (public `ctx.getSystemPrompt()` is the primary; the reflected field is the backstop), `agent.thinkingBudgets`, and `settingsManager.getShowCacheMissNotices()` (the AgentSession-level gate for the cache-notice transcript line) — all plain fields/accessors on pi-agent-core's `Agent` / `AgentSession`, all covered by `scripts/pi-upstream-probe.mjs`. Their failure mode is a cache miss or a suppressed notice, never a hard failure: the request falls back to the cold path with the transcript notice below.
101
169
 
102
170
  - **Anchor early in the turn.** Whatever's in `agent.state.messages` *before* the `anchor` tool call stays in the kept chain. Everything after gets summarized. Anchor at the *start* of a stage for maximum context savings.
103
171
 
104
172
  - **Tiny rewinds are rejected by a minimum-savings floor.** A `rewind` whose measured savings falls below an internal floor (~4k tokens of apparent context freed) is refused with guidance listing the active anchors instead of executing — collapsing a near-empty segment burns a summarizer LLM call and can even grow live context once the summary and its synthetic assistant land on the kept chain. This pairs with anchoring early: anchor at the start of a stage, then rewind only once real work has accumulated above the anchor.
105
173
 
174
+ - **`rewind` must be a solo tool call.** A batched rewind is refused before any mutation because the post-rewind projection is `[everything up to the anchor] + [the new summary]`, so a sibling result would be orphaned (generated pre-collapse, written post-collapse, no declaring call in context); re-issue the rewind alone.
175
+
106
176
  - **Abandoned branches grow the JSONL forever.** Each rewind preserves the abandoned subtree on disk. Session files get bigger over time even as live context shrinks. For very long autonomous runs (days), session files can hit hundreds of MB.
107
177
 
108
178
  - **Tested against Anthropic and Kiro providers.** The synthetic-tool_use trick is specifically for Anthropic's strict tool_use/tool_result pairing; the synthetic's `stopReason: "toolUse"` survives Kiro's `normalizeMessages` filter. Other providers may have different validation rules — untested.
109
179
 
180
+ - **Cache-preserving summary request (#33).** `rewind` mirrors the live request (system prompt, tool array, session id, cache retention, reasoning effort, thinking budgets) so the summary can be served from the same prompt-cache prefix as the turns it collapses. Any param the live loop sends that this extension does not mirror is a silent miss — the summary still runs, just cold, and a cold *structured* request can bill more than branch-only evidence. The response surfaces misses as TUI transcript lines (gated by `showCacheMissNotices`, ≥20k tokens / ≥$0.10 display floor, hits silent) and in `details.summaryCache` — never in the model-visible tool-result content (see “Cache-preserving summary request” above) — and `PI_NAVIGATE_TREE_SUMMARY_CACHE=0` forces the pre-#33 cold path. A segment whose raw evidence is not in the live projection (it crosses the latest compaction's `firstKeptEntryId`) or whose branch evidence is entirely dropped (labels-only / oversized newest message) deliberately refuses the cache path and re-bills cold — raw evidence beats a cache hit when the two disagree.
181
+
182
+ - **Cache path bypasses the SDK live request hooks.** The wrapper builds the summary request itself rather than routing through the SDK live path, so `onPayload` / `before_provider_request` and `transformHeaders` hooks registered by other extensions do not run for the summary request. Any extension that mutates the live request through those hooks is an additional silent-miss surface: the summary still runs, just cold.
183
+
184
+ - **`images.blockImages` divergence (cache path).** The summary shapes its history with the public `convertToLlm`, message by message. Pi's live loop wraps that in `convertToLlmWithBlockImages` when the `images.blockImages` setting is on, which filters image blocks out of the wire payload — so with that setting enabled and images in the collapsed context, the summary payload diverges and misses the cache (correct summary, cold bill). The live gate runs without images.
185
+
186
+ - **Summary trailer role alternation is untested on Kiro/Bedrock.** When the last retained message is user-role (a `toolResult` or user text), the summarization instruction lands as a consecutive user turn. Anthropic merges consecutive user turns; pi's Kiro/Bedrock adapters are untested on this exact shape. Unit tests pin the sequence; validate manually there before relying on the cache path.
187
+
110
188
  - **Loading the extension monkey-patches `AgentSession.prototype.prompt` globally.** Every session in the host pi process picks up the patch on import, including sessions that never call `navigate_tree`. The patch is install-on-import and not reversible within a running pi process; restart pi to fully unload it.
111
189
 
112
- - **`anchor:` is a reserved label prefix.** Any label written via pi's `/label` command or by another extension that begins with `anchor:` will be picked up by `list` and addressable by `rewind`'s `labelStart` / `labelEnd`. Avoid the prefix in manually-set labels.
190
+ - **`anchor:` is a reserved label prefix.** Any label written via the `/tree` view (`Shift+L` on the selected entry) or by another extension that begins with `anchor:` will be picked up by `list` and addressable by `rewind`'s `rewindTo` / `newLabel`. Avoid the prefix in manually-set labels.
113
191
 
114
192
  - **Disk-fault during `rewind` (rare).** Pi's `branchWithSummary` advances the in-memory leaf before persisting the new entry to disk. If pi's session-write fails mid-call (full disk, FS error on a persisted session), the in-memory leaf has already moved past the original assistant turn but the synthetic-assistant injection in this extension never runs — pi's tool-result then lands without a matching tool_use, surfacing as the same `context_length_exceeded` 400 the synthetic exists to prevent. Production risk: low (in-memory tests don't reach this case; pi's session-write is robust on POSIX disk). Tracked for an additional salvage layer wrapping `branchWithSummary` itself in v0.2.0.
115
193
 
@@ -122,7 +200,14 @@ pnpm run lint # biome check extensions/
122
200
  pnpm run typecheck # tsc --noEmit
123
201
  ```
124
202
 
125
- Tests cover `extensions/navigate-tree/helpers.ts` (pure helpers in `helpers.test.ts`) and `extensions/navigate-tree/index.ts` (action dispatch, schema shape, synthetic-assistant injection, context-event projection, reflection bootstrap, salvage path — in `index.test.ts`). The `summarize` factory option injects a stub for `generateBranchSummary` so no real LLM call fires during rewind tests. Additional manual e2e validation against the current pi release (0.84.x at time of writing) is recommended for any pi version bump (the reflection bootstrap depends on `AgentSession.prototype.prompt` / `agent.state.messages` field shapes).
203
+ Tests cover `extensions/navigate-tree/helpers.ts` (pure helpers in `helpers.test.ts`), `extensions/navigate-tree/index.ts` (action dispatch, schema shape, synthetic-assistant injection, context-event projection, reflection bootstrap, salvage path, and the #33 cache-request call site — in `index.test.ts`), and `extensions/navigate-tree/cache-summary.ts` (payload shaping, `{first}` numbering + budget truncation, the r5d prompt pin, the notice matrix, and the wrapper contract driven through the real upstream `generateBranchSummary` — in `cache-summary.test.ts`). The `summarize` factory option injects a stub for `generateBranchSummary` so no real LLM call fires during rewind tests, and the wrapper tests use a fake capturing `streamFn`; the suite is fully offline. Additional manual e2e validation against the current pi release (0.84.x at time of writing) is recommended for any pi version bump (the reflection bootstrap depends on `AgentSession.prototype.prompt` / `agent.state.messages` field shapes; the cache path additionally reads `agent.state.tools` / `agent.state.systemPrompt` / `agent.thinkingBudgets`).
204
+
205
+ ### Live summary verification
206
+
207
+ The unit suite is fully offline; it cannot prove that a real provider serves the summary from cache or that the model's output is scope-clean. For any change to `cache-summary.ts` or the rewind call site, run both halves against a configured provider:
208
+
209
+ 1. **Cache gate** — in a fresh session with only this extension loaded (`pi --no-extensions -e <repo>/extensions/navigate-tree/index.ts`), anchor at the start of a stage, accumulate real work (e.g. two file reads), then `rewind`. Confirm `cacheRead > 0` / `hit: true` in `details.summaryCache` in the session JSONL (the authoritative, always-present surface); with pi's `showCacheMissNotices` setting enabled you will also see a cache-notice transcript line on a miss (≥20k tokens / ≥$0.10) and nothing at all on a hit. Cache text never appears in the tool-result content the model sees. A miss notice means a mirrored request param diverged — bisect in this order: caller `maxTokens` (must be stripped), `reasoning`, `cacheRetention`, session headers.
210
+ 2. **Quality smoke** — `node scripts/summary-quality-check.mjs ~/.pi/agent/sessions/<dir>/<file>.jsonl` checks the r5d headings/length/preamble (and prints the newest `details.summaryCache` block); eyeball the printed summary for scope: branch only, pre-branch background excluded, unresolved work preserved, no continuation of the collapsed work.
126
211
 
127
212
  ## License
128
213