@cad0p/pi-tree-navigator 0.1.2 β†’ 0.1.3

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,28 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.1.3] - 2026-09-14
6
+
7
+ <!-- USER-EDITABLE SECTION START -->
8
+ Last version before adopting the branch summarization cache hit mechanism to save everyone money -> https://github.com/earendil-works/pi/issues/9411
9
+ <!-- USER-EDITABLE SECTION END -->
10
+
11
+ ### πŸš€ Features
12
+
13
+ - *(ci)* Daily pi-upstream reflection probe (closes #17)
14
+ - *(guidelines)* Steer agents to anchor early at context-gathered (closes #29)
15
+ - Mandate anchoring via before_agent_start system prompt (closes #31)
16
+
17
+ ### πŸ› Bug Fixes
18
+
19
+ - *(rewind)* Reject degenerate rewinds below a min-savings floor (closes #21, closes #20)
20
+ - *(rewind)* Inject opencode session headers for summarization (closes #26)
21
+
22
+ ### βš™οΈ Miscellaneous Tasks
23
+
24
+ - *(tooldef)* Trim navigate_tree tool definition (~848 β†’ ~470 tok/request) (closes #22)
25
+
26
+
5
27
  ## [0.1.2] - 2026-08-20
6
28
 
7
29
  <!-- USER-EDITABLE SECTION START -->
package/README.md CHANGED
@@ -53,6 +53,8 @@ A single agent-callable tool, `navigate_tree`, with three actions:
53
53
 
54
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.
55
55
 
56
+ **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
+
56
58
  ## How it works
57
59
 
58
60
  A typical autonomous-loop pattern:
@@ -99,6 +101,8 @@ The synthetic assistant we inject after each rewind carries the **post-rewind ch
99
101
 
100
102
  - **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.
101
103
 
104
+ - **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
+
102
106
  - **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.
103
107
 
104
108
  - **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.
@@ -36,6 +36,7 @@
36
36
  * `buildContextMessages`), not `agent.prepareNextTurn*` reflection.
37
37
  */
38
38
 
39
+ import { randomUUID } from "node:crypto";
39
40
  import {
40
41
  estimateContextTokens,
41
42
  type StreamFn,
@@ -64,10 +65,22 @@ import {
64
65
  } from "./helpers.ts";
65
66
 
66
67
  const LABEL_PREFIX = "anchor:";
68
+ const TOOL_NAME = "navigate_tree";
69
+
70
+ /**
71
+ * Always-on anchor mandate appended to the system prompt on every agent
72
+ * start (gated on the tool being active). Same lever as pi-napkin's vault
73
+ * mandate: `promptGuidelines` bullets land mid-prompt; this append lands
74
+ * at the very end of the system prompt, after <project_context> and
75
+ * skills, and is re-applied on every prompt (never compacted). Byte-stable
76
+ * (module constant, no per-session interpolation) for provider caching.
77
+ */
78
+ export const ANCHOR_MANDATE = `${TOOL_NAME}: gather all context, then anchor \`context-gathered\`; list anchors and rewind after every milestone or rabbit hole / dead end.`;
67
79
 
68
80
  // ---------------------------------------------------------------------------
69
81
  // Exported boundary constants below (MAX_SESSION_REFS, MAX_HINT_WALK_DEPTH,
70
- // MIN_SUMMARY_FOCUS_LENGTH, MAX_SYNTHETIC_FOCUS_LENGTH).
82
+ // MIN_SUMMARY_FOCUS_LENGTH, MIN_REWIND_SAVINGS_TOKENS,
83
+ // MAX_SYNTHETIC_FOCUS_LENGTH).
71
84
  //
72
85
  // Stability: these are internal tunables. Exported only so the test suite
73
86
  // can pin boundary cases by constant rather than literal. Re-tuning is
@@ -95,6 +108,19 @@ export const MAX_HINT_WALK_DEPTH = 50;
95
108
  // reliably loses continuity β€” raising it forces more useful focus text
96
109
  // without inviting verbosity.
97
110
  export const MIN_SUMMARY_FOCUS_LENGTH = 20;
111
+ // Floor on the *apparent* token savings (`beforeTokens βˆ’ tokensAtTarget`) a
112
+ // `rewind` must clear to execute. Absolute threshold, deliberately NOT
113
+ // window-relative: % semantics drift with window size, so a fixed token bar
114
+ // is the only stable shape. Every rewind carries β‰ˆ350–900 tokens of fixed
115
+ // overhead (the branch_summary adds ~300–800 permanent tokens on the kept
116
+ // chain; the synthetic assistant re-emits ~50 on every subsequent turn), so
117
+ // apparent savings OVERSTATES true freed context. Empirically the degenerate
118
+ // anchor→immediate-rewind case measures ~100 apparent tokens while a healthy
119
+ // stage measures ~20k β€” 4k separates them cleanly (~200Γ—) and roughly equals
120
+ // one 300-line source-file read (~5.9k tokens measured). Below the floor a
121
+ // rewind burns a summarizer LLM call, forks a junk branch_summary, consumes
122
+ // a milestone label, and GROWS live context.
123
+ export const MIN_REWIND_SAVINGS_TOKENS = 4000;
98
124
  // Cap on the `summaryFocus` length stored in the synthetic assistant's
99
125
  // arguments. The full focus is passed live to `generateBranchSummary`, so
100
126
  // the summarizer always sees the original; we only need a trimmed copy in
@@ -248,6 +274,53 @@ function stripNullHeaders(
248
274
  : undefined;
249
275
  }
250
276
 
277
+ /**
278
+ * Session-routing headers for the summarization request.
279
+ *
280
+ * Pi's live turns merge these in `sdk.ts` via `mergeProviderAttributionHeaders`
281
+ * (session headers + install-telemetry attribution). Out-of-loop summarization
282
+ * callers β€” pi's own `agent-session.navigateTree` and this extension β€” pass only
283
+ * auth headers, so providers that REQUIRE the session header 400 the request
284
+ * (`MissingSessionID`; observed on opencode-go 2026-09-07: every rewind and every
285
+ * native `/tree` summary fails deterministically).
286
+ *
287
+ * The tool-execute ctx exposes neither `settingsManager` (telemetry attribution)
288
+ * nor the live routing `sessionId`, so this replicates only the session half of
289
+ * pi's merge (`getSessionHeaders` in `provider-attribution.ts`): for
290
+ * opencode-family providers, inject `x-opencode-session` + `x-opencode-client`.
291
+ * The value is a fresh UUID per rewind β€” upstream does the same for routing
292
+ * (`completeSummarization` falls back to `uuidv7()` when the caller passes no
293
+ * session id), and one-off summaries have no continuation to route. Proven by
294
+ * the branch-summary eval harness, whose identical injection succeeds on
295
+ * opencode-go where the bare call 400s. Never overrides a header the auth layer
296
+ * already set.
297
+ */
298
+ function withSessionHeaders(
299
+ model: { provider?: string; baseUrl?: string } | undefined,
300
+ headers: Record<string, string> | undefined,
301
+ ): Record<string, string> | undefined {
302
+ const provider = model?.provider ?? "";
303
+ let host = "";
304
+ try {
305
+ host = model?.baseUrl ? new URL(model.baseUrl).hostname : "";
306
+ } catch {
307
+ host = "";
308
+ }
309
+ const needsSession =
310
+ provider === "opencode" ||
311
+ provider === "opencode-go" ||
312
+ host === "opencode.ai";
313
+ if (!needsSession) return headers;
314
+ const merged = { ...(headers ?? {}) };
315
+ if (!merged["x-opencode-session"]) {
316
+ merged["x-opencode-session"] = randomUUID();
317
+ }
318
+ if (!merged["x-opencode-client"]) {
319
+ merged["x-opencode-client"] = "pi";
320
+ }
321
+ return merged;
322
+ }
323
+
251
324
  /**
252
325
  * Project the session tree's active branch into the messages the next LLM
253
326
  * call should see β€” the public-API replacement for the deleted
@@ -327,6 +400,49 @@ function estimateAtEntry(
327
400
  ).tokens;
328
401
  }
329
402
 
403
+ /**
404
+ * Rejection copy for the rewind min-savings floor (#21). Reports the
405
+ * apparent savings plus every active anchor — chronological root→leaf,
406
+ * same walk order as `list`, including labelStart itself β€” with its
407
+ * cumulative context percentage, so the agent can pick a genuinely earlier
408
+ * anchor or keep working. Move-on-collision means the tool itself never
409
+ * creates duplicate anchor names; a manual `/label anchor:x` CAN put the
410
+ * same name on two entries β€” duplicates print twice here, exactly as
411
+ * `list` prints them today.
412
+ *
413
+ * Deliberately does NOT print the floor value anywhere: an agent-visible
414
+ * numeric bar invites grinding up to it instead of doing real work.
415
+ */
416
+ function buildMinSavingsRejection(
417
+ labelStart: string,
418
+ labelEnd: string,
419
+ apparentSavings: number,
420
+ sm: SessionManager,
421
+ cw: number,
422
+ allEntries: SessionEntry[],
423
+ byId: Map<string, SessionEntry>,
424
+ ): string {
425
+ const anchors: string[] = [];
426
+ let oldestAnchorName: string | null = null;
427
+ for (const e of sm.getBranch()) {
428
+ const lbl = sm.getLabel(e.id);
429
+ if (!lbl?.startsWith(LABEL_PREFIX)) continue;
430
+ const name = lbl.slice(LABEL_PREFIX.length);
431
+ if (!oldestAnchorName) oldestAnchorName = name;
432
+ anchors.push(
433
+ `'${name}' ${formatPct1(estimateAtEntry(allEntries, e.id, byId), cw)}`,
434
+ );
435
+ }
436
+ // cw=0 β†’ percents fall back to formatPct1's k-format and the window
437
+ // suffix is omitted, mirroring `list`'s header behavior.
438
+ const suffix = cw > 0 ? ` of ${formatWindow(cw)}` : "";
439
+ const guidance =
440
+ oldestAnchorName === labelStart
441
+ ? "No earlier anchors β€” keep working and rewind later once more has accumulated above it."
442
+ : "Rewind further back to actually free context, or keep working.";
443
+ return `Rewinding '${labelStart}' β†’ '${labelEnd}' would free only ~${formatPct1(apparentSavings, 0)} tokens. ${guidance} Active anchors: ${anchors.join(" Β· ")}${suffix}`;
444
+ }
445
+
330
446
  /**
331
447
  * Walk parentId chain back from `fromId` and return a one-line preview of
332
448
  * the first entry that has meaningful text content. Branch summaries are
@@ -452,22 +568,35 @@ export default function (
452
568
  messages: buildContextMessages(ctx.sessionManager),
453
569
  }));
454
570
 
571
+ // Anchor mandate (#31): a before_agent_start append lands at the END of
572
+ // the system prompt β€” stronger than the mid-prompt Guidelines block, and
573
+ // re-applied on every prompt instead of living in the conversation. Skip
574
+ // only when the tool is verifiably absent from the active set; fail-open
575
+ // when `selectedTools` is undefined (this extension always registers it).
576
+ pi.on("before_agent_start", async (event) => {
577
+ const selected = event.systemPromptOptions?.selectedTools;
578
+ if (Array.isArray(selected) && !selected.includes(TOOL_NAME)) return {};
579
+ return { systemPrompt: `${event.systemPrompt}\n\n${ANCHOR_MANDATE}` };
580
+ });
581
+
455
582
  pi.registerTool({
456
- name: "navigate_tree",
583
+ name: TOOL_NAME,
457
584
  label: "Navigate Tree",
458
585
  // Stateful: every action mutates SessionManager; concurrent calls would
459
586
  // race on `leafId` / `labelsById` and produce an undefined tree.
460
587
  executionMode: "sequential",
461
- description: `Long-session context management via the pi session tree. Anchor named milestones, then collapse work between them into a model-generated summary while preserving the full history on a sibling branch. Despite the verb, \`rewind\` does not restore prior state β€” it forks a sibling branch from the anchor and continues forward from a model-generated summary; the abandoned subtree is preserved on disk but no longer on the active path.
462
-
463
- Operations (set the \`action\` parameter):
464
- β€’ action='anchor', name='<milestone-name>': label the current point so a later rewind can target it. Use at the start of a stage you'll summarize (e.g. 'design-start', 'impl-start'). If the same name already exists on the active branch, the prior label is moved to the new leaf (no duplicates).
465
- β€’ action='rewind', labelStart='<existing>', labelEnd='<new>': collapse work between labelStart and the current leaf into a branch_summary. The new summary entry is itself labeled with labelEnd, so you can chain rewinds.
466
- β€’ action='list': show all named anchors on the active branch in chronological order, with cumulative context % at each anchor.
588
+ promptGuidelines: [
589
+ `${TOOL_NAME}: the further back you rewind, the more you free but the more collapses into the summary; pick the earliest anchor that still preserves what you need next.`,
590
+ ],
591
+ description: `Long-session context management via the pi session tree. Anchor named milestones, then collapse work between them into a model-generated summary to free context.
592
+ \`rewind\` does not restore prior state: it forks a sibling branch from the anchor and continues forward from a model-generated summary.
467
593
 
468
- Both \`name\` (anchor) and \`labelEnd\` (rewind) write into the same anchor namespace β€” either becomes addressable as a future \`labelStart\`. \`list\` shows every anchor under the \`anchor:\` prefix, regardless of which action wrote it. Pi labels written via \`/label anchor:foo\` (manually or by other extensions) are also addressable here. Avoid the \`anchor:\` prefix in manually-set labels.
594
+ Operations (set \`action\`):
595
+ β€’ 'anchor', name='<milestone-name>': label the current point. Anchor at the start of a stage you'll summarize (e.g. 'impl-start').
596
+ β€’ 'rewind', labelStart='<existing>', labelEnd='<new>': collapse work between labelStart and the current leaf into a branch_summary labeled labelEnd, so rewinds can chain.
597
+ β€’ 'list': show all anchors on the active branch, oldest first, with cumulative context % at each.
469
598
 
470
- \`summaryFocus\` is required when \`action='rewind'\` (β‰₯${MIN_SUMMARY_FOCUS_LENGTH} chars after trim). Calls without it are rejected. It's passed to pi's \`generateBranchSummary\` as \`customInstructions\`, biasing the summarizer LLM toward the agent's specified focus while it rewrites the collapsed work into pi's structured summary format. To preserve continuity, instruct the summarizer to keep: (1) the user's most recent message verbatim, (2) what's done in the collapsed segment, (3) what's left to do as a next action.`,
599
+ \`name\` (anchor) and \`labelEnd\` (rewind) write into one shared anchor namespace: re-using an existing label moves it to the new entry, and everything written there is addressable as a future \`labelStart\`. Avoid the reserved \`anchor:\` prefix.`,
471
600
  promptSnippet:
472
601
  "Use to anchor named milestones and rewind the conversation tree to a prior point with a model-generated summary, for token-efficient long autonomous sessions.",
473
602
  // The schema is intentionally a flat `Type.Object` with everything-but-
@@ -481,28 +610,27 @@ Both \`name\` (anchor) and \`labelEnd\` (rewind) write into the same anchor name
481
610
  action: Type.Union(
482
611
  [Type.Literal("anchor"), Type.Literal("rewind"), Type.Literal("list")],
483
612
  {
484
- description:
485
- "Which operation to perform. 'anchor' labels the current point, 'rewind' collapses work between an anchor and the current leaf into a branch_summary, 'list' shows every anchor on the active branch.",
613
+ description: "Which operation to perform.",
486
614
  },
487
615
  ),
488
616
  name: Type.Optional(
489
617
  Type.String({
490
- description: `Required when action='anchor'. Kebab-case label (max ${MAX_NAME_LENGTH} chars) for the milestone. If a label with this name already exists on the active branch, it is moved to the new leaf.`,
618
+ description: `Required when action='anchor'. Kebab-case milestone label, max ${MAX_NAME_LENGTH} chars.`,
491
619
  }),
492
620
  ),
493
621
  labelStart: Type.Optional(
494
622
  Type.String({
495
- description: `Required when action='rewind'. Kebab-case name (max ${MAX_NAME_LENGTH} chars) of an existing anchor on the active branch β€” work between this anchor and the current leaf is summarized.`,
623
+ description: `Required when action='rewind'. Kebab-case name of an existing anchor on the active branch to rewind to.`,
496
624
  }),
497
625
  ),
498
626
  labelEnd: Type.Optional(
499
627
  Type.String({
500
- description: `Required when action='rewind'. Kebab-case name (max ${MAX_NAME_LENGTH} chars) for the resulting branch_summary entry. If a label with this name already exists on the active branch, it is moved to the new entry (mirrors anchor's move-on-collision). Becomes addressable as a future labelStart.`,
628
+ description: `Required when action='rewind'. Kebab-case label for the resulting branch_summary entry; reusable as a future labelStart.`,
501
629
  }),
502
630
  ),
503
631
  summaryFocus: Type.Optional(
504
632
  Type.String({
505
- description: `Required when action='rewind'. β‰₯${MIN_SUMMARY_FOCUS_LENGTH} chars after trim. Should encode (1) the user's most recent instruction verbatim, (2) what was done in the collapsed segment, (3) what's left to do as a next action.`,
633
+ description: `Required when action='rewind'; β‰₯${MIN_SUMMARY_FOCUS_LENGTH} chars after trim. Encode: (1) the user's most recent instruction verbatim, (2) what's done in the collapsed segment, (3) what's left to do as a next action.`,
506
634
  }),
507
635
  ),
508
636
  }),
@@ -595,7 +723,7 @@ Both \`name\` (anchor) and \`labelEnd\` (rewind) write into the same anchor name
595
723
  type: "text",
596
724
  text:
597
725
  `[anchor '${p.name}'] set at ${positionLine}${hintLine}\n\n` +
598
- `When you finish this stage, call: navigate_tree(action='rewind', labelStart='${p.name}', labelEnd='<milestone-name>', summaryFocus='<β‰₯${MIN_SUMMARY_FOCUS_LENGTH}-char focus: latest user instruction + done + remaining>').`,
726
+ `Once real work has accumulated after this anchor, collapse it into a summary with: ${TOOL_NAME}(action='rewind', labelStart='${p.name}', labelEnd='<milestone-name>', summaryFocus='<β‰₯${MIN_SUMMARY_FOCUS_LENGTH}-char focus: latest user instruction + done + remaining>').`,
599
727
  },
600
728
  ],
601
729
  details: {
@@ -647,15 +775,13 @@ Both \`name\` (anchor) and \`labelEnd\` (rewind) write into the same anchor name
647
775
  );
648
776
  }
649
777
 
650
- if (!ctx.model) {
651
- return toolError("No model configured for summarization.");
652
- }
653
-
654
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
655
- if (!auth.ok) {
656
- return toolError(`Auth resolution failed: ${auth.error}`);
657
- }
658
-
778
+ // Token math hoisted ABOVE the model check / auth await (#21): these
779
+ // are pure SessionManager reads, and the min-savings floor below must
780
+ // be evaluable β€” and able to reject β€” before any auth resolution.
781
+ // `allEntries`/`byId` are hoisted out of the parented-assistant branch
782
+ // because the floor rejection copy reports per-anchor percentages from
783
+ // the same maps.
784
+ //
659
785
  // The leaf at execute time is the assistant that just streamed the
660
786
  // rewind tool call. Its `usage.input` is the *minimum* of recent API
661
787
  // calls in this turn, so estimating from it understates what the
@@ -663,6 +789,9 @@ Both \`name\` (anchor) and \`labelEnd\` (rewind) write into the same anchor name
663
789
  // assistant's usage as baseline) so beforeTokens matches the value
664
790
  // `list` would have reported on the previous turn.
665
791
  const oldLeafEntry = sm.getEntry(oldLeaf);
792
+ const allEntries = sm.getEntries();
793
+ const byId = new Map<string, SessionEntry>();
794
+ for (const e of allEntries) byId.set(e.id, e);
666
795
  let beforeTokens: number;
667
796
  if (
668
797
  oldLeafEntry &&
@@ -670,14 +799,12 @@ Both \`name\` (anchor) and \`labelEnd\` (rewind) write into the same anchor name
670
799
  oldLeafEntry.message.role === "assistant" &&
671
800
  oldLeafEntry.parentId
672
801
  ) {
673
- const allEntries = sm.getEntries();
674
- const byId = new Map<string, SessionEntry>();
675
- for (const e of allEntries) byId.set(e.id, e);
676
802
  beforeTokens = estimateAtEntry(allEntries, oldLeafEntry.parentId, byId);
677
803
  } else {
678
804
  beforeTokens = estimateActiveBranchTokens(sm);
679
805
  }
680
- const contextWindow = ctx.model.contextWindow ?? 0;
806
+ const tokensAtTarget = estimateAtEntry(allEntries, target, byId);
807
+ const contextWindow = ctx.model?.contextWindow ?? 0;
681
808
 
682
809
  const { entries } = collectEntriesForBranchSummary(sm, oldLeaf, target);
683
810
  if (entries.length === 0) {
@@ -710,7 +837,7 @@ Both \`name\` (anchor) and \`labelEnd\` (rewind) write into the same anchor name
710
837
  if (
711
838
  block &&
712
839
  block.type === "toolCall" &&
713
- block.name === "navigate_tree" &&
840
+ block.name === TOOL_NAME &&
714
841
  isSyntheticShape
715
842
  ) {
716
843
  return toolError(
@@ -720,6 +847,38 @@ Both \`name\` (anchor) and \`labelEnd\` (rewind) write into the same anchor name
720
847
  }
721
848
  }
722
849
 
850
+ // Minimum-savings floor (#21): refuse rewinds whose apparent savings
851
+ // is below MIN_REWIND_SAVINGS_TOKENS β€” collapsing a near-empty segment
852
+ // burns a summarizer LLM call, forks a junk branch_summary, consumes
853
+ // a milestone label, and grows live context. Placed AFTER the
854
+ // synthetic-boundary guard so the chained rewind-with-no-turns case
855
+ // keeps its specific diagnostic, and BEFORE the model/auth checks so
856
+ // the rejection fires without touching provider config.
857
+ const apparentSavings = beforeTokens - tokensAtTarget;
858
+ if (apparentSavings < MIN_REWIND_SAVINGS_TOKENS) {
859
+ return toolError(
860
+ buildMinSavingsRejection(
861
+ p.labelStart,
862
+ p.labelEnd,
863
+ apparentSavings,
864
+ sm,
865
+ contextWindow,
866
+ allEntries,
867
+ byId,
868
+ ),
869
+ { rejected: "min-savings", apparentSavings },
870
+ );
871
+ }
872
+
873
+ if (!ctx.model) {
874
+ return toolError("No model configured for summarization.");
875
+ }
876
+
877
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
878
+ if (!auth.ok) {
879
+ return toolError(`Auth resolution failed: ${auth.error}`);
880
+ }
881
+
723
882
  const result = await summarize(entries, {
724
883
  // `auth.baseUrl` (OAuth/credential-derived endpoint, e.g.
725
884
  // githubCopilotOAuth) must be applied to the model, mirroring pi's
@@ -729,7 +888,7 @@ Both \`name\` (anchor) and \`labelEnd\` (rewind) write into the same anchor name
729
888
  ? { ...ctx.model, baseUrl: auth.baseUrl }
730
889
  : ctx.model,
731
890
  apiKey: auth.apiKey ?? "",
732
- headers: stripNullHeaders(auth.headers),
891
+ headers: withSessionHeaders(ctx.model, stripNullHeaders(auth.headers)),
733
892
  ...(auth.env ? { env: auth.env } : {}),
734
893
  signal: signal ?? new AbortController().signal,
735
894
  customInstructions: p.summaryFocus,
@@ -848,7 +1007,7 @@ Both \`name\` (anchor) and \`labelEnd\` (rewind) write into the same anchor name
848
1007
  }
849
1008
  const syntheticMsg = buildSyntheticAssistant(
850
1009
  toolCallId,
851
- "navigate_tree",
1010
+ TOOL_NAME,
852
1011
  syntheticArgs,
853
1012
  ctx.model as
854
1013
  | { api?: string; provider?: string; id?: string }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cad0p/pi-tree-navigator",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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",