@cad0p/pi-tree-navigator 0.1.0 → 0.1.1-20260820.0

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,41 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.1.1] - 2026-07-31
6
+
7
+ <!-- USER-EDITABLE SECTION START -->
8
+ Patch release: restores the mid-loop context refresh on pi ≥0.80.3. No behavior change on pi ≤0.80.2.
9
+
10
+ **The bug (pi ≥0.80.3):** pi 0.80.3 added `AgentSession._installAgentNextTurnRefresh()`, which installs pi's own `agent.prepareNextTurnWithContext` in the constructor, and pi-agent-core's `Agent.createLoopConfig` now prefers that field over `agent.prepareNextTurn`. Since this extension only wrapped `prepareNextTurn`, its mid-loop context replacement was dead code: after a `rewind`, the branch summary landed correctly, but every remaining turn of the same loop still sent the full pre-rewind context to the API, and the footer's context-% re-anchored on that stale usage (jumping back up right after the rewind). Rewinds only actually saved context on the *next* user prompt.
11
+
12
+ **The fix:** `installPrepareNextTurn` now wraps both hook fields with the same marker/`__prior` chaining discipline. On pi ≥0.80.3 the `prepareNextTurnWithContext` wrapper chains pi's own (keeping its per-turn `systemPrompt`/`tools`/`model`/`thinkingLevel` refreshes) and overrides only `messages`; on pi ≤0.80.2 the new field is never read and `prepareNextTurn` does the work as before.
13
+
14
+ Verified live on pi 0.83.0 (persisted session): after a rewind at 31.5% context, the footer stays at ~1.6% for the rest of the loop (previously bounced back to ~33.5%), and the post-rewind API call goes out with ~3.7k tokens instead of ~80.5k.
15
+ <!-- USER-EDITABLE SECTION END -->
16
+
17
+ ### 🚀 Features
18
+
19
+ - Discriminated-union schema makes summaryFocus required at the wire level ([#1](https://github.com/cad0p/pi-tree-navigator/pull/1))
20
+
21
+ ### 🐛 Bug Fixes
22
+
23
+ - Revert discriminated-union parameters — Kiro rejects non-object root schemas ([#2](https://github.com/cad0p/pi-tree-navigator/pull/2))
24
+ - Wrap prepareNextTurnWithContext — in-loop context refresh dead since pi 0.80.3 ([#8](https://github.com/cad0p/pi-tree-navigator/pull/8))
25
+
26
+ ### 🚜 Refactor
27
+
28
+ - Nest extension under extensions/navigate-tree/ per pi-napkin convention
29
+
30
+ ### 📚 Documentation
31
+
32
+ - Promote npm install and publishing ([#4](https://github.com/cad0p/pi-tree-navigator/pull/4))
33
+
34
+ ### ⚙️ Miscellaneous Tasks
35
+
36
+ - Release-grade cleanup for v0.1.0 ([#3](https://github.com/cad0p/pi-tree-navigator/pull/3))
37
+ - Switch from bun to node + pnpm for local dev and CI ([#6](https://github.com/cad0p/pi-tree-navigator/pull/6))
38
+
39
+
5
40
  ## [0.1.0] - 2026-05-25
6
41
 
7
42
  <!-- USER-EDITABLE SECTION START -->
package/README.md CHANGED
@@ -6,37 +6,40 @@ Lets a pi agent anchor named milestones in its own conversation, then collapse w
6
6
 
7
7
  ## Install
8
8
 
9
+ Stable npm release:
10
+
9
11
  ```bash
10
- pi install git:github.com/cad0p/pi-tree-navigator
12
+ pi install npm:@cad0p/pi-tree-navigator
11
13
  ```
12
14
 
13
- > **Status:** v0.1.0 is not yet on the npm registry (pending OIDC trusted-publisher setup). Install from the git source for now.
15
+ Pre-release npm snapshots from `main` are published with the `next` dist-tag:
14
16
 
15
- <details>
16
- <summary>Once published / pre-release installs</summary>
17
+ ```bash
18
+ pi install npm:@cad0p/pi-tree-navigator@next
19
+ ```
17
20
 
18
- - Stable (once published to npm):
21
+ You can also install directly from the git source when testing unreleased branches:
19
22
 
20
- ```bash
21
- pi install npm:@cad0p/pi-tree-navigator
22
- ```
23
+ ```bash
24
+ pi install git:github.com/cad0p/pi-tree-navigator
25
+ ```
23
26
 
24
- - Pre-release (calver snapshots from `main`, published to npm `@next` on every push):
27
+ ## Publishing
25
28
 
26
- ```bash
27
- pi install npm:@cad0p/pi-tree-navigator@next
28
- ```
29
+ This repo uses [`cad0p/semver-calver-release`](https://github.com/cad0p/semver-calver-release)'s npm-package workflow:
29
30
 
30
- </details>
31
+ - Pushes to `main` compute the next hybrid SemVer + CalVer version, tag a GitHub prerelease, and publish to npm with the `next` dist-tag.
32
+ - Curated release PRs from `release/from-v*` branches bump the base `package.json` version and publish stable npm releases.
33
+ - npm publishing uses GitHub OIDC / npm trusted publishing via `.github/workflows/release.yml` (`id-token: write`) and `publishConfig.access: public`.
31
34
 
32
35
  ### Requirements
33
36
 
34
- - **pi 0.74+** with at least one model provider configured.
37
+ - **pi 0.81+** (node ≥22.19, which pi 0.81+ itself requires) with at least one model provider configured.
35
38
  - Peer dependencies (the source of truth is `package.json` `peerDependencies`):
36
- - `@earendil-works/pi-coding-agent >=0.74.0`
37
- - `@earendil-works/pi-agent-core >=0.74.0`
39
+ - `@earendil-works/pi-coding-agent >=0.81.0`
40
+ - `@earendil-works/pi-agent-core >=0.81.0`
38
41
  - `typebox ^1.0.0` (used to declare the tool's parameter schema; bundled with pi but listed explicitly so a standalone install resolves correctly).
39
- - The reflection bootstrap depends on five plain (not `#`-private) internal pi/agent fields: `AgentSession.prototype.prompt`, `agent.state.messages`, `agent.state.systemPrompt`, `agent.state.tools`, and `agent.prepareNextTurn`. Verified against pi 0.75.x.
42
+ - The reflection bootstrap depends on five plain (not `#`-private) internal pi/agent fields: `AgentSession.prototype.prompt`, `agent.state.messages`, `agent.state.systemPrompt`, `agent.state.tools`, and `agent.prepareNextTurnWithContext` (preferred by pi ≥0.80.3; `agent.prepareNextTurn` was the pre-0.80.3 name). Verified against pi 0.81.0 and pi 0.83.0.
40
43
 
41
44
  ## What you get
42
45
 
@@ -90,7 +93,9 @@ The synthetic assistant we inject after each rewind carries the **post-rewind ch
90
93
 
91
94
  ## Limitations
92
95
 
93
- - **Brittle to pi version bumps.** The fix uses five independent reflection points on internals that aren't part of pi's public API: `AgentSession.prototype.prompt`, `agent.state.messages`, `agent.state.systemPrompt`, `agent.state.tools`, and `agent.prepareNextTurn`. If a future pi release renames any of these, 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.
96
+ - **Brittle to pi version bumps.** The fix uses five independent reflection points on internals that aren't part of pi's public API: `AgentSession.prototype.prompt`, `agent.state.messages`, `agent.state.systemPrompt`, `agent.state.tools`, and `agent.prepareNextTurnWithContext`. This is not hypothetical: pi 0.80.3 added `prepareNextTurnWithContext` and made the loop prefer it, silently dead-ending the `prepareNextTurn`-only hook until v0.1.1. If a future pi release renames any of these, 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.
97
+
98
+ **Audited against pi 0.81.0 / 0.83.0 / 0.84.2 (2026-08):** three of the five are eliminable via public APIs — `ctx.getSystemPrompt()` (base `ExtensionContext`) and `ctx.getAllTools()` (both since 0.80.2) replace the `agent.state.systemPrompt` / `agent.state.tools` reads, and the public `context` extension event (fired via `transformContext` before **every** LLM call, including the turn right after a mid-loop rewind) can replace the `prepareNextTurnWithContext` wrapper's per-turn refresh. 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).
94
99
 
95
100
  - **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.
96
101
 
@@ -107,13 +112,13 @@ The synthetic assistant we inject after each rewind carries the **post-rewind ch
107
112
  ## Development
108
113
 
109
114
  ```bash
110
- bun install
111
- bun test # helpers + dispatch / reflection bootstrap / salvage path
112
- bunx biome check extensions/
113
- bunx tsc --noEmit
115
+ pnpm install
116
+ pnpm test # helpers + dispatch / reflection bootstrap / salvage path
117
+ pnpm run lint # biome check extensions/
118
+ pnpm run typecheck # tsc --noEmit
114
119
  ```
115
120
 
116
- 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, 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 pi 0.75.x is recommended for any pi version bump (the reflection bootstrap depends on internal field shapes).
121
+ 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, 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 internal field shapes).
117
122
 
118
123
  ## License
119
124
 
@@ -25,24 +25,29 @@
25
25
  * • If pi switches any of the five fields this extension reads —
26
26
  * `AgentSession.prototype.prompt`, `agent.state.messages`,
27
27
  * `agent.state.systemPrompt`, `agent.state.tools`, or
28
- * `agent.prepareNextTurn` — to ES `#` private fields, this breaks
29
- * fundamentally.
28
+ * `agent.prepareNextTurnWithContext` —
29
+ * to ES `#` private fields, this breaks fundamentally.
30
30
  * • If pi renames or restructures any of these fields, this breaks.
31
31
  * • Patches `AgentSession.prototype.prompt` globally on import; not
32
32
  * reversible without a process restart; affects every session in
33
33
  * the pi process, including sessions that never call
34
34
  * `navigate_tree`.
35
35
  *
36
- * Verified against pi 0.75.5.
36
+ * Verified against pi 0.81.0 (`prepareNextTurnWithContext` path —
37
+ * required since pi 0.80.3, see `installPrepareNextTurn`).
37
38
  */
38
39
 
39
- import { estimateContextTokens } from "@earendil-works/pi-agent-core";
40
+ import {
41
+ estimateContextTokens,
42
+ type StreamFn,
43
+ } from "@earendil-works/pi-agent-core";
40
44
  import {
41
45
  AgentSession,
42
46
  buildSessionContext,
43
47
  collectEntriesForBranchSummary,
44
48
  type ExtensionAPI,
45
49
  generateBranchSummary,
50
+ type ModelRegistry,
46
51
  type SessionEntry,
47
52
  type SessionManager,
48
53
  } from "@earendil-works/pi-coding-agent";
@@ -116,6 +121,7 @@ const LIST_PCT_COL_WIDTH = 5;
116
121
  // hint column visible without truncating common names.
117
122
  const LIST_LABEL_COL_WIDTH = 28;
118
123
  const PNT_MARKER = Symbol.for("navigate-tree.pnt-installed");
124
+ const PNTWC_MARKER = Symbol.for("navigate-tree.pntwc-installed");
119
125
  const ORIG_PROMPT_KEY = Symbol.for("navigate-tree.orig-prompt");
120
126
 
121
127
  // Two warnings: list-site (read-only path; warns about the next turn's
@@ -144,6 +150,10 @@ interface PiInternals {
144
150
  tools: unknown[];
145
151
  };
146
152
  prepareNextTurn?: unknown;
153
+ // Preferred over `prepareNextTurn` by `Agent.createLoopConfig`
154
+ // when set; pi's own AgentSession always sets it (via
155
+ // `_installAgentNextTurnRefresh`) since pi-coding-agent 0.80.3.
156
+ prepareNextTurnWithContext?: unknown;
147
157
  };
148
158
  sessionManager: SessionManager;
149
159
  }
@@ -162,15 +172,23 @@ type PntResult = {
162
172
  model?: unknown;
163
173
  thinkingLevel?: unknown;
164
174
  };
165
- // pi 0.75.5 invokes `agent.prepareNextTurn(signal)` from
166
- // `Agent.createLoopConfig` — a single AbortSignal argument. This differs
167
- // from the documented `AgentLoopConfig.prepareNextTurn(context: PrepareNextTurnContext)`
168
- // shape, which `Agent` is bridging. We accept whatever pi passes and forward
169
- // it verbatim to the prior wrapper so we don't fight a future signature
170
- // alignment. Verified against pi-coding-agent 0.75.5; revisit if the call
171
- // site changes.
175
+ // pi ≤0.80.2 invokes `agent.prepareNextTurn(signal)` from
176
+ // `Agent.createLoopConfig` — a single AbortSignal argument. Since pi
177
+ // 0.80.3 the loop instead invokes
178
+ // `agent.prepareNextTurnWithContext(nextTurnContext, signal)` (and only
179
+ // falls back to `prepareNextTurn` when the WithContext field is unset).
180
+ // Both differ from the documented
181
+ // `AgentLoopConfig.prepareNextTurn(context: PrepareNextTurnContext)`
182
+ // shape, which `Agent` is bridging. We accept whatever pi passes and
183
+ // forward it verbatim to the prior wrapper so we don't fight a future
184
+ // signature alignment. Verified against pi-coding-agent 0.81.0 and
185
+ // 0.83.0; revisit if the call site changes.
172
186
  type PntFn = (...args: unknown[]) => Promise<PntResult> | PntResult;
173
- type MarkedPntFn = PntFn & { [PNT_MARKER]?: boolean; __prior?: PntFn };
187
+ type MarkedPntFn = PntFn & {
188
+ [PNT_MARKER]?: boolean;
189
+ [PNTWC_MARKER]?: boolean;
190
+ __prior?: PntFn;
191
+ };
174
192
 
175
193
  // =============================================================================
176
194
  // Reflection bootstrap & in-loop refresh
@@ -218,17 +236,32 @@ function patchAgentSessionPrototype(): void {
218
236
  }
219
237
 
220
238
  /**
221
- * Wire `agent.prepareNextTurn` so the in-flight agent loop refreshes its
222
- * context from sessionManager between turns within the same prompt() call.
223
- * Without this, the loop snapshots agent.state.messages once at prompt start
224
- * and pushes new messages onto its own array — a rewind issued mid-loop
225
- * doesn't reduce the next API call's size until the user sends a new prompt.
239
+ * Wire the in-flight agent loop to refresh its context from sessionManager
240
+ * between turns within the same prompt() call. Without this, the loop
241
+ * snapshots agent.state.messages once at prompt start and pushes new
242
+ * messages onto its own array — a rewind issued mid-loop doesn't reduce
243
+ * the next API call's size until the user sends a new prompt.
244
+ *
245
+ * Two hook fields, by pi version:
246
+ * • pi ≤0.80.2: `Agent.createLoopConfig` reads `agent.prepareNextTurn`.
247
+ * • pi ≥0.80.3: AgentSession's constructor installs its own
248
+ * `agent.prepareNextTurnWithContext` (`_installAgentNextTurnRefresh`),
249
+ * and `createLoopConfig` prefers that field over `prepareNextTurn`.
250
+ * Pi's wrapper refreshes systemPrompt/tools/model/thinkingLevel per
251
+ * turn but leaves `messages` stale — so wrapping only
252
+ * `prepareNextTurn` silently dead-ends (footer % and the wire context
253
+ * stay pre-rewind for the rest of the loop).
254
+ *
255
+ * We install on BOTH fields, each with the same marker/__prior chaining
256
+ * discipline: on new pi the WithContext wrapper chains pi's own (keeping
257
+ * its per-turn refreshes) and overrides only `messages`; on old pi the
258
+ * WithContext field is never read and `prepareNextTurn` does the work.
226
259
  *
227
- * The Agent class's `createLoopConfig` dereferences `this.prepareNextTurn`
228
- * at the closure call site, so the value here is read at every turn boundary.
229
- * But it gates the closure on `this.prepareNextTurn` being truthy at config
230
- * creation — so we have to set this BEFORE prompt() runs, hence wiring it
231
- * from inside the prompt patch.
260
+ * The Agent class's `createLoopConfig` dereferences both fields at the
261
+ * closure call site, so the values here are read at every turn boundary.
262
+ * But it gates the closure on either field being truthy at config
263
+ * creation — so we have to set this BEFORE prompt() runs, hence wiring
264
+ * it from inside the prompt patch.
232
265
  */
233
266
  function installPrepareNextTurn(session: AgentSession): void {
234
267
  const internals = asInternals(session);
@@ -237,15 +270,22 @@ function installPrepareNextTurn(session: AgentSession): void {
237
270
 
238
271
  const sm = internals.sessionManager;
239
272
 
240
- // If the existing prepareNextTurn was installed by a previous load of THIS
241
- // extension, recover the chain it captured (its `__prior`) so we don't
242
- // strand other extensions' closures across /reload. Preserve any other
243
- // extension's prepareNextTurn so we compose with them.
273
+ // If the existing hooks were installed by a previous load of THIS
274
+ // extension, recover the chains they captured (their `__prior`) so we
275
+ // don't strand other extensions' closures across /reload. Preserve any
276
+ // other extension's (or pi's own) hooks so we compose with them.
244
277
  const existing = agent.prepareNextTurn as MarkedPntFn | undefined;
245
278
  const prior: PntFn | undefined =
246
279
  typeof existing === "function" && existing[PNT_MARKER]
247
280
  ? existing.__prior
248
281
  : (existing as PntFn | undefined);
282
+ const existingWc = agent.prepareNextTurnWithContext as
283
+ | MarkedPntFn
284
+ | undefined;
285
+ const priorWc: PntFn | undefined =
286
+ typeof existingWc === "function" && existingWc[PNTWC_MARKER]
287
+ ? existingWc.__prior
288
+ : (existingWc as PntFn | undefined);
249
289
 
250
290
  const next: MarkedPntFn = async (...args: unknown[]) => {
251
291
  let priorResult: PntResult | undefined;
@@ -273,6 +313,40 @@ function installPrepareNextTurn(session: AgentSession): void {
273
313
  next[PNT_MARKER] = true;
274
314
  next.__prior = prior;
275
315
  agent.prepareNextTurn = next;
316
+
317
+ const nextWc: MarkedPntFn = async (...args: unknown[]) => {
318
+ // On pi ≥0.80.3 the loop calls this as (nextTurnContext, signal),
319
+ // where nextTurnContext = { message, toolResults, context,
320
+ // newMessages }. Forward args verbatim to the prior (pi's own
321
+ // wrapper expects exactly this shape).
322
+ const turn = args[0] as { context?: PntResult["context"] } | undefined;
323
+ let priorResult: PntResult | undefined;
324
+ if (typeof priorWc === "function") {
325
+ priorResult = await priorWc(...args);
326
+ }
327
+ // Same wholesale-replacement rationale as the prepareNextTurn
328
+ // wrapper above. When no prior result exists, fall back to the
329
+ // turn's live context (mirrors pi's own wrapper, which does
330
+ // `previousSnapshot?.context ?? turn.context`) so systemPrompt and
331
+ // tools are never dropped. `messages` is owned by this wrapper —
332
+ // this override is the entire point of the hook on pi ≥0.80.3, whose
333
+ // own wrapper refreshes every other field but leaves messages stale.
334
+ const priorContext = priorResult?.context ?? turn?.context;
335
+ return {
336
+ ...priorResult,
337
+ context: {
338
+ ...priorContext,
339
+ systemPrompt: priorContext?.systemPrompt ?? agent.state.systemPrompt,
340
+ tools: priorContext?.tools ?? agent.state.tools,
341
+ messages: sm.buildSessionContext().messages,
342
+ },
343
+ model: priorResult?.model,
344
+ thinkingLevel: priorResult?.thinkingLevel,
345
+ };
346
+ };
347
+ nextWc[PNTWC_MARKER] = true;
348
+ nextWc.__prior = priorWc;
349
+ agent.prepareNextTurnWithContext = nextWc;
276
350
  }
277
351
 
278
352
  function findOwningSession(sm: SessionManager): AgentSession | null {
@@ -285,6 +359,47 @@ function findOwningSession(sm: SessionManager): AgentSession | null {
285
359
  return null;
286
360
  }
287
361
 
362
+ /**
363
+ * Resolve the provider's `streamSimple` for summarization routing via the
364
+ * PUBLIC modelRegistry API (no reflection).
365
+ *
366
+ * Custom providers registered via `pi.registerProvider(name, { api:
367
+ * <custom-id>, streamSimple })` are composed into the ModelRuntime provider
368
+ * returned by `ctx.modelRegistry.getProvider(...)` — its `streamSimple`
369
+ * dispatches to the extension handler (provider-composer `streamWith`).
370
+ * Without passing it as `streamFn` to `generateBranchSummary`,
371
+ * completeSummarization falls back to the pi-ai compat registry (builtin
372
+ * apis only) and throws "No API provider registered for api: <custom-id>".
373
+ */
374
+ function resolveProviderStreamFn(
375
+ modelRegistry: ModelRegistry,
376
+ providerId: string,
377
+ ): { streamFn: StreamFn } | undefined {
378
+ try {
379
+ const provider = modelRegistry.getProvider(providerId);
380
+ return provider?.streamSimple
381
+ ? { streamFn: provider.streamSimple as StreamFn }
382
+ : undefined;
383
+ } catch {
384
+ return undefined;
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Drop `null` header-deletion markers (pi 0.84+ `ProviderHeaders` can carry
390
+ * `string | null` values). Mirrors pi's own `withoutDeletedHeaders` in
391
+ * agent-session.js — `generateBranchSummary` expects `Record<string, string>`.
392
+ */
393
+ function stripNullHeaders(
394
+ headers: Record<string, string | null> | undefined,
395
+ ): Record<string, string> | undefined {
396
+ if (!headers) return undefined;
397
+ const entries = Object.entries(headers).filter((entry) => entry[1] !== null);
398
+ return entries.length > 0
399
+ ? (Object.fromEntries(entries) as Record<string, string>)
400
+ : undefined;
401
+ }
402
+
288
403
  function refreshAgentMessages(sm: SessionManager): boolean {
289
404
  // Manually replicate the agent-state refresh that pi's
290
405
  // commandCtx.navigateTree does after branchWithSummary. Returns true on
@@ -716,11 +831,28 @@ Both \`name\` (anchor) and \`labelEnd\` (rewind) write into the same anchor name
716
831
  }
717
832
 
718
833
  const result = await summarize(entries, {
719
- model: ctx.model,
834
+ // `auth.baseUrl` (OAuth/credential-derived endpoint, e.g.
835
+ // githubCopilotOAuth) must be applied to the model, mirroring pi's
836
+ // own `_getSummarizationRequestAuth` (`result.auth.baseUrl ?
837
+ // { ...model, baseUrl: result.auth.baseUrl } : model`).
838
+ model: auth.baseUrl
839
+ ? { ...ctx.model, baseUrl: auth.baseUrl }
840
+ : ctx.model,
720
841
  apiKey: auth.apiKey ?? "",
721
- headers: auth.headers,
842
+ headers: stripNullHeaders(auth.headers),
843
+ ...(auth.env ? { env: auth.env } : {}),
722
844
  signal: signal ?? new AbortController().signal,
723
845
  customInstructions: p.summaryFocus,
846
+ // Route through the composed provider's `streamSimple` (public
847
+ // modelRegistry API) instead of the pi-ai compat registry.
848
+ // Custom providers registered via `pi.registerProvider(name,
849
+ // { api: <custom-id>, streamSimple })` are NOT visible to the
850
+ // compat registry (which only knows builtin apis) — without this,
851
+ // rewind fails with "No API provider registered for api:
852
+ // <custom-id>" for any custom-api provider (e.g. commandcode 0.5.x
853
+ // with api "commandcode-custom").
854
+ ...(resolveProviderStreamFn(ctx.modelRegistry, ctx.model.provider) ??
855
+ {}),
724
856
  });
725
857
  if (result.aborted) {
726
858
  return toolError("Summarization aborted.");
@@ -934,8 +1066,11 @@ export const __testHooks = {
934
1066
  installPrepareNextTurn,
935
1067
  refreshAgentMessages,
936
1068
  captureSession,
937
- /** Symbol used to mark the wrapper installed by `installPrepareNextTurn`. */
1069
+ /** Symbols used to mark the wrappers installed by `installPrepareNextTurn`
1070
+ * (`PNT_MARKER` on `agent.prepareNextTurn`, `PNTWC_MARKER` on
1071
+ * `agent.prepareNextTurnWithContext` — the field pi ≥0.80.3 prefers). */
938
1072
  PNT_MARKER,
1073
+ PNTWC_MARKER,
939
1074
  /** Read-only view of captured-session ref count for reaping assertions. */
940
1075
  sessionRefCount(): number {
941
1076
  return sessionInstances.length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cad0p/pi-tree-navigator",
3
- "version": "0.1.0",
3
+ "version": "0.1.1-20260820.0",
4
4
  "description": "agent-callable session tree navigation for pi: anchor named milestones, rewind work into a branch summary, free up context for long autonomous sessions",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -36,19 +36,19 @@
36
36
  ]
37
37
  },
38
38
  "scripts": {
39
- "lint": "bunx biome check extensions/",
40
- "lint:fix": "bunx biome check --write extensions/",
41
- "typecheck": "bunx tsc --noEmit",
42
- "test": "bun test"
39
+ "lint": "biome check extensions/",
40
+ "lint:fix": "biome check --write extensions/",
41
+ "typecheck": "tsc --noEmit",
42
+ "test": "node --test extensions/**/*.test.ts"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@biomejs/biome": "^2.3.14",
46
- "@types/bun": "^1.3.0",
46
+ "@types/node": "^22.0.0",
47
47
  "typescript": "^5.8.0"
48
48
  },
49
49
  "peerDependencies": {
50
- "@earendil-works/pi-agent-core": ">=0.74.0",
51
- "@earendil-works/pi-coding-agent": ">=0.74.0",
50
+ "@earendil-works/pi-agent-core": ">=0.81.0",
51
+ "@earendil-works/pi-coding-agent": ">=0.81.0",
52
52
  "typebox": "^1.0.0"
53
53
  },
54
54
  "license": "MIT"