@kingingwang/pi-windows-x64 0.86.1 → 0.87.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/bin/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.87.0] - 2026-09-21
4
+
5
+ ### New Features
6
+
7
+ - **Canonical session context and extension boundaries** — Edit model context without rewriting history and add actionable lifecycle hooks. See [ContextEditEntry](docs/session-format.md#contexteditentry) and [extension events](docs/extensions.md#extension-events).
8
+ - **Full-transcript context extensions** — Use `context_with_system` for per-request system-message transformations. See [`context_with_system`](docs/extensions.md#context_with_system).
9
+ - **Per-model image input limits** — Configure cache-safe image resizing per model for attachments, `read`, and tool-result images. See [Image Input Limits](docs/models.md#image-input-limits).
10
+
11
+ ### Breaking Changes
12
+
13
+ - Removed the inherited `shouldStopAfterTurn` agent option. Use `finishTurn` and return `{ action: "end" }` instead. `finishTurn` runs before `turn_end` but applies the decision afterward, and it also receives error and aborted responses; migrate normal-response predicates by returning `undefined` for those hard exits. See the `@earendil-works/pi-agent-core` changelog for a complete before-and-after example.
14
+ - Added `ContextEditEntry` to the exported `SessionEntry` union. TypeScript consumers with exhaustive entry switches must handle `context_edit`; use `replacement: null` for omission and a content replacement otherwise.
15
+ - Made `SessionManager` canonical for `AgentSession` provider context. Assigning `session.agent.state.messages` no longer replaces future request history; restore with `SessionManager.inMemory(cwd, { id }, entries)`, navigate with `session.navigateTree()`, or append through `session.sessionManager` and call `session.refreshContext()`.
16
+ - Expanded `TurnEndEvent` with required boundary fields and added `AgentBeforeSettleEvent` to the exported `ExtensionEvent` union. Consumers constructing events or exhaustively switching on `ExtensionEvent` must handle the new shapes. `ExtensionRunner.emit()` no longer accepts `turn_end`; host integrations dispatch actionable boundaries with `emitBoundary(baseEvent, buildContext)`.
17
+ - Deferred runs requested from `agent_settled` handlers until all settled handlers finish. Handlers still observe `ctx.isIdle() === true`, but no longer see a reentrant `agent_start` during the same notification dispatch.
18
+
19
+ ### Added
20
+
21
+ - Added append-only model-context edits. For example, `sessionManager.appendContextEdit(entryId, null)` omits one message from future provider context without changing raw history, usage, or UI history.
22
+ - Added actionable `turn_end` and `agent_before_settle` extension boundaries. Return `{ entries: [...event.entries, draft], continue: true }` to persist structural entries in order and ensure one next provider request without changing steering or follow-up scheduling.
23
+ - Added retain-none compaction input: `sessionManager.appendCompaction(summary, null, tokensBefore)` stores the compaction's own ID as its kept boundary.
24
+ - Added the `context_with_system` extension event, which runs after `context` handlers on the full transcript including system messages and sends its result verbatim. See [`context_with_system`](docs/extensions.md#context_with_system).
25
+ - Added per-model image resize profiles through `inputLimits.images.resize` in `models.json`, applied to file attachments, image reads, and tool-result images ([#9631](https://github.com/earendil-works/pi/issues/9631)).
26
+
27
+ ### Fixed
28
+
29
+ - Fixed string context-edit replacements producing invalid assistant and tool-result message content instead of text blocks.
30
+ - Fixed context-invisible boundary metadata and replacement edits causing newly appended or replaced input to be summarized before its first provider request.
31
+ - Fixed edited-context accounting both discarding valid assistant usage captured after the latest context edit and reusing that usage after a later compaction made it stale.
32
+ - Fixed selected error retries and final length/overflow recovery retaining abandoned model attempts in future provider context; post-run recovery omissions are now persisted without hiding raw transcript history or changing queue scheduling.
33
+ - Fixed `context` handlers that filter or slice messages dropping the prompt and tool declarations, which after extension-driven compaction left requests without built-in tools or made Codex emit raw tool-call text. Handlers no longer see system messages; Pi restores the prompt and tool state after they run. See [`context`](docs/extensions.md#context) ([#9789](https://github.com/earendil-works/pi/issues/9789), [#9822](https://github.com/earendil-works/pi/issues/9822)).
34
+ - Fixed `/bug` allowing uploads in offline mode while preserving local zip exports ([#9841](https://github.com/earendil-works/pi/pull/9841) by [@christianklotz](https://github.com/christianklotz)).
35
+ - Fixed idle prompt-cache warming rebuilding expired caches when its timer or an extension decision is delayed.
36
+ - Improved crash diagnostics with hints identifying loaded extensions that appear in the stack trace.
37
+ - Fixed text files beginning with `GIF` being misclassified as images and omitted from `read` and CLI `@file` input ([#9755](https://github.com/earendil-works/pi/issues/9755)).
38
+ - Fixed malformed prompt template frontmatter being silently ignored instead of reported as a resource warning ([#9830](https://github.com/earendil-works/pi/pull/9830) by [@christianklotz](https://github.com/christianklotz)).
39
+ - Fixed inherited unknown OpenAI-compatible Chat Completions endpoints receiving strict tool schemas unless they explicitly advertise support ([#9816](https://github.com/earendil-works/pi/issues/9816)).
40
+
3
41
  ## [0.86.1] - 2026-09-20
4
42
 
5
43
  ### New Features
package/bin/README.md CHANGED
@@ -282,6 +282,15 @@ Long sessions can exhaust context windows. Compaction summarizes older messages
282
282
 
283
283
  Compaction is lossy. The full history remains in the JSONL file; use `/tree` to revisit. Customize compaction behavior via [extensions](#extensions). See [docs/compaction.md](docs/compaction.md) for internals.
284
284
 
285
+ Session model context is projected from append-only history. Extensions can append a `context_edit` to omit or replace an earlier message only for future model requests; raw history and usage remain unchanged:
286
+
287
+ ```typescript
288
+ const assistantId = sessionManager.appendMessage(partialAssistant);
289
+ sessionManager.appendContextEdit(assistantId, null); // Hidden from model context, retained in JSONL.
290
+ ```
291
+
292
+ A retain-none compaction uses `appendCompaction(summary, null, tokensBefore)` to make the exact summary the new context root while preserving prior raw entries.
293
+
285
294
  ---
286
295
 
287
296
  ## Settings
@@ -383,6 +392,33 @@ export default function (pi: ExtensionAPI) {
383
392
  }
384
393
  ```
385
394
 
395
+ `turn_end` and `agent_before_settle` are actionable persistence boundaries. Handlers can append structural entries and request one continuation. Later handlers see earlier proposals:
396
+
397
+ ```typescript
398
+ let replacedResponse = false;
399
+ pi.on("turn_end", (event) => {
400
+ if (replacedResponse || event.outcome !== "completed" || event.toolResults.length > 0) return;
401
+ replacedResponse = true;
402
+ return {
403
+ entries: [
404
+ ...event.entries,
405
+ { type: "context_edit", targetId: event.messageEntryId, replacement: null },
406
+ {
407
+ type: "custom_message",
408
+ customType: "replacement-instruction",
409
+ content: "Answer again using the persisted user request.",
410
+ display: false,
411
+ },
412
+ ],
413
+ continue: true,
414
+ };
415
+ });
416
+ ```
417
+
418
+ Continuation is one-shot per boundary result, not per registered handler. It ensures one next provider request: tool-result, steering, or follow-up scheduling can satisfy that request without adding another one; otherwise Pi makes one context-only request. Guard handlers like the example above because an unconditional `continue: true` is evaluated again after the next response and can loop indefinitely. Error and aborted responses remain hard exits.
419
+
420
+ Use `agent_before_settle` for final actions after retries, compaction, and queued input are exhausted. See [docs/extensions.md](docs/extensions.md#agent_start--agent_end--agent_before_settle--agent_settled).
421
+
386
422
  The default export can also be `async`. pi waits for async extension factories before startup continues, which is useful for one-time initialization such as fetching remote model lists before calling `pi.registerProvider()`.
387
423
 
388
424
  **What's possible:**
@@ -34,14 +34,16 @@ contextTokens > contextWindow - reserveTokens
34
34
 
35
35
  By default, `reserveTokens` is 16384 tokens (configurable in `~/.pi/agent/settings.json` or `<project-dir>/.pi/settings.json`). This leaves room for the LLM's response.
36
36
 
37
- During a multi-turn agent run, Pi checks this threshold after tools finish and their results are appended, before starting the next assistant response. If the threshold is crossed, Pi compacts inside the same agent run and resumes with the summary and retained messages. It skips this between-turn check when the completed tool batch terminates the run and no queued message requires another response. Pi also checks the threshold before a new user prompt and after a low-level agent run ends.
37
+ During a multi-turn agent run, Pi checks the canonical projected context after tools finish and their results are appended, before starting the next assistant response. If the threshold is crossed, Pi compacts during `prepareNextTurn`, then performs the existing catch-up steering poll before `turn_start`. It skips this between-turn check when the completed tool batch terminates the run and no queued message requires another response. Pi also checks before a new user prompt and performs final-attempt overflow recovery after the low-level run ends.
38
+
39
+ A provider context-overflow error or an early final `stopReason: "length"` can select one compact-and-retry recovery attempt. Length responses with tool calls retain their synthetic failed tool results and follow the ordinary tool/queue scheduler rather than forcing the run to end.
38
40
 
39
41
  You can also trigger manually with `/compact [instructions]`, where optional instructions focus the summary.
40
42
 
41
43
  ### How It Works
42
44
 
43
- 1. **Find cut point**: Walk backwards from newest message, accumulating token estimates until `keepRecentTokens` (default 20k, configurable in `~/.pi/agent/settings.json` or `<project-dir>/.pi/settings.json`) is reached
44
- 2. **Extract messages**: Collect messages from the previous kept boundary (or session start) up to the cut point
45
+ 1. **Find cut point**: Walk backwards through the finalized session projection, accumulating token estimates until `keepRecentTokens` (default 20k, configurable in `~/.pi/agent/settings.json` or `<project-dir>/.pi/settings.json`) is reached
46
+ 2. **Extract messages**: Collect projected messages from the previous kept boundary (or session start) up to the cut point
45
47
  3. **Generate summary**: Call LLM to summarize with structured format, passing the previous summary as iterative context when present
46
48
  4. **Append entry**: Save `CompactionEntry` with summary and `firstKeptEntryId`
47
49
  5. **Rebuilds context**: Session rebuilds the context for the next request, using summary + messages from `firstKeptEntryId` onwards
@@ -78,7 +80,22 @@ What the LLM sees:
78
80
  prompt from cmp messages from firstKeptEntryId
79
81
  ```
80
82
 
81
- On repeated compactions, the summarized span starts at the previous compaction's kept boundary (`firstKeptEntryId`), not at the compaction entry itself, falling back to the entry after the previous compaction if that kept entry cannot be found in the path. This preserves messages that survived the earlier compaction by including them in the next summarization pass as well. Pi also recalculates `tokensBefore` from the rebuilt session context before writing the new `CompactionEntry`, so the token count reflects the actual pre-compaction context being replaced.
83
+ On repeated compactions, the summarized span starts at the previous compaction's kept boundary (`firstKeptEntryId`), not at the compaction entry itself, falling back to the entry after the previous compaction if that kept entry cannot be found in the path. A retain-none compaction records its own ID as `firstKeptEntryId`; repeated compaction starts after that entry. This preserves messages that survived the earlier compaction by including them in the next summarization pass as well. Pi also recalculates `tokensBefore` from the rebuilt, context-edited session projection before writing the new `CompactionEntry`, so the token count reflects the actual pre-compaction context being replaced. Omitted raw entries remain stored but do not affect cut selection, summaries, checkpoints, or token estimates.
84
+
85
+ ### Overflow and Length Recovery Ordering
86
+
87
+ Recovery preserves the existing lifecycle and queue order. The completed attempt remains visible to `turn_end` and `agent_end`; post-run recovery then repairs persisted model context before a fresh retry:
88
+
89
+ ```text
90
+ persist final assistant response
91
+ → extension/public turn_end
92
+ → extension/public agent_end
93
+ → append context_edit omissions for the selected attempt
94
+ → for overflow/length: run session_before_compact and append compaction on success
95
+ → start the retry as a fresh run
96
+ ```
97
+
98
+ If recovery compaction fails or is cancelled, Pi keeps the omission edits, appends no compaction, and schedules no internal retry. Existing queued work remains governed by ordinary steering and follow-up rules. `agent_before_settle` sees the repaired projection after recovery processing. Raw transcript history, exports, billing totals, and history-search extensions can still inspect the omitted attempt.
82
99
 
83
100
  ### Split Turns
84
101
 
@@ -118,6 +135,8 @@ Valid cut points are:
118
135
 
119
136
  Never cut at tool results (they must stay with their tool call).
120
137
 
138
+ Preparation advances the kept boundary into a context-invisible suffix only when that suffix contains an omitted assistant attempt and no unomitted context-producing entries. Recovery `context_edit` omissions satisfy this rule; intrinsically context-invisible metadata may coexist with them. Metadata alone and newly appended custom messages do not move the cut. A replacement edit affecting the candidate input or summarized prefix also blocks advancement because the omitted assistant answered the pre-edit input; replacements of suffix entries that are ultimately omitted remain safe. This allows an over-budget recovered input to be summarized while retaining the edits that keep the abandoned attempt omitted, without making bookkeeping change whether new model input is preserved verbatim.
139
+
121
140
  ### CompactionEntry Structure
122
141
 
123
142
  Defined in [`session-manager.ts`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/session-manager.ts):
@@ -294,7 +294,8 @@ user sends prompt ────────────────────
294
294
  │ ┌─── turn (repeats while LLM calls tools) ───┐ │
295
295
  │ │ │ │
296
296
  │ ├─► turn_start │ │
297
- │ ├─► context (can modify messages) │ │
297
+ │ ├─► context (can modify conversation messages) │
298
+ │ ├─► context_with_system (can modify the full transcript)
298
299
  │ ├─► before_provider_headers (can mutate headers) |
299
300
  │ ├─► before_provider_request (can inspect or replace payload)
300
301
  │ ├─► after_provider_response (status + headers, before stream consume)
@@ -307,9 +308,13 @@ user sends prompt ────────────────────
307
308
  │ │ └─► tool_execution_end │ │
308
309
  │ │ │ │
309
310
  │ └─► turn_end │ │
311
+ │ └─► threshold compaction before a naturally required next turn
310
312
  │ │
311
313
  ├─► agent_end │
312
- └─► agent_settled (no retry/compaction/follow-up left) │
314
+ ├─► retry backoff or final-attempt recovery (when selected)
315
+ │ └─► fresh agent_start on successful recovery │
316
+ ├─► agent_before_settle (can append entries and continue)│
317
+ └─► agent_settled (final, notification only) │
313
318
  │
314
319
  user sends another prompt ◄────────────────────────────────┘
315
320
 
@@ -567,9 +572,9 @@ The `systemPromptOptions` field gives extensions access to the same structured d
567
572
 
568
573
  Inside `before_agent_start`, `event.systemPrompt` and `ctx.getSystemPrompt()` both reflect the chained system prompt as of the current handler. Later `before_agent_start` handlers can still modify it again.
569
574
 
570
- #### agent_start / agent_end / agent_settled
575
+ #### agent_start / agent_end / agent_before_settle / agent_settled
571
576
 
572
- `agent_start` fires when a low-level agent run begins. `agent_end` fires when that run ends, but Pi may still auto-retry, auto-compact and retry, or continue with queued follow-up messages. Use `agent_settled` for status integrations that need to know Pi will not continue running automatically.
577
+ `agent_start` fires when a low-level agent run begins. `agent_end` fires when that run ends, but Pi may still auto-retry, auto-compact and retry, or continue with queued follow-up messages. `agent_before_settle` is the final actionable boundary: it can append session entries and request one continuation. `agent_settled` is final and notification-only; use it for status integrations that need to know Pi will not continue running automatically.
573
578
 
574
579
  ```typescript
575
580
  pi.on("agent_start", async (_event, ctx) => {});
@@ -578,11 +583,28 @@ pi.on("agent_end", async (event, ctx) => {
578
583
  // event.messages - messages from this low-level run
579
584
  });
580
585
 
586
+ let addedReviewReminder = false;
587
+ pi.on("agent_before_settle", async (event, ctx) => {
588
+ if (addedReviewReminder) return;
589
+ addedReviewReminder = true;
590
+ return {
591
+ entries: [...event.entries, {
592
+ type: "custom_message",
593
+ customType: "review-reminder",
594
+ content: "Review the final diff before replying.",
595
+ display: false,
596
+ }],
597
+ continue: true,
598
+ };
599
+ });
600
+
581
601
  pi.on("agent_settled", async (_event, ctx) => {
582
- // ctx.isIdle() is true here unless another extension started a new run.
602
+ // ctx.isIdle() is true; runs requested here start after all settled handlers finish.
583
603
  });
584
604
  ```
585
605
 
606
+ If the run is aborted while `agent_before_settle` handlers are running, valid returned entries are still committed, but requested continuation is suppressed. Work requested from `agent_settled` is deferred until every settled handler completes, so notification dispatch is non-reentrant.
607
+
586
608
  #### ui_prompt_start / ui_prompt_end
587
609
 
588
610
  Notification-only lifecycle events for blocking user-facing extension UI prompts. They fire around `ctx.ui.select()`, `ctx.ui.confirm()`, `ctx.ui.input()`, `ctx.ui.editor()`, and `ctx.ui.custom()` so host/status integrations can report "waiting for user" instead of just "running".
@@ -610,11 +632,34 @@ pi.on("turn_start", async (event, ctx) => {
610
632
  // event.turnIndex, event.timestamp
611
633
  });
612
634
 
635
+ let replacedResponse = false;
613
636
  pi.on("turn_end", async (event, ctx) => {
614
637
  // event.turnIndex, event.message, event.toolResults
638
+ // event.entries contains the structural entries proposed so far.
639
+ if (replacedResponse || event.outcome !== "completed" || event.toolResults.length > 0) return;
640
+ replacedResponse = true;
641
+ return {
642
+ entries: [
643
+ ...event.entries,
644
+ { type: "context_edit", targetId: event.messageEntryId, replacement: null },
645
+ {
646
+ type: "custom_message",
647
+ customType: "replacement-instruction",
648
+ content: "Answer again using the persisted user request.",
649
+ display: false,
650
+ },
651
+ ],
652
+ continue: true,
653
+ };
615
654
  });
616
655
  ```
617
656
 
657
+ `turn_end` runs after the assistant and tool-result messages have been persisted and before the low-level `turn_end` event. Retry backoff and final-attempt recovery still happen after `agent_end`, preserving their existing lifecycle and queue ordering; `agent_before_settle` sees the repaired projection after that work completes. Boundary handlers run in extension load and registration order. Each handler sees prior proposals in `event.entries` and sees `event.context` rebuilt from them. Returning `entries` or `continue` replaces only that field; omitted fields preserve the current proposal. Allowed draft entry types are `custom`, `custom_message`, `context_edit`, and `compaction`. The complete proposal is validated before it is appended in list order after all handlers finish; a handler error is reported and later handlers still run. Validation prevents partially applied semantic errors, but persistence is not transactional.
658
+
659
+ `continue: true` ensures one next provider request for that boundary invocation. If tool results, steering, or a follow-up already cause that request, they satisfy the decision and no additional request is made; otherwise Pi makes one context-only request. Error and aborted responses remain hard exits. `continue: false` never suppresses natural work. Guard continuation conditions: an unconditional `continue: true` is evaluated again after the next response and can create an endless loop. A `custom_message` draft contributes a user-role model message but is extension-authored: it does not run human input hooks, slash commands, skills, or prompt templates.
660
+
661
+ Host integrations that construct `TurnEndEvent` values must now provide `messageEntryId`, `toolResultEntryIds`, `outcome`, `entries`, `continue`, and `context`. `ExtensionEvent` exhaustive switches must also handle `agent_before_settle`. `ExtensionRunner.emit()` excludes actionable turn boundaries; dispatch `turn_end` and `agent_before_settle` through `emitBoundary(baseEvent, buildContext)` so handlers receive chained previews. Other dedicated runner methods still return results for events such as `session_before_*`.
662
+
618
663
  #### message_start / message_update / message_end
619
664
 
620
665
  Fired for message lifecycle updates.
@@ -681,12 +726,31 @@ Fired before each LLM call. Modify messages non-destructively. See [Session Form
681
726
 
682
727
  ```typescript
683
728
  pi.on("context", async (event, ctx) => {
684
- // event.messages - deep copy, safe to modify
729
+ // event.messages - deep copy without system messages, safe to modify
685
730
  const filtered = event.messages.filter(m => !shouldPrune(m));
686
731
  return { messages: filtered };
687
732
  });
688
733
  ```
689
734
 
735
+ `event.messages` holds the conversation without system messages. The prompt and tool declarations belong to Pi and are not part of this hook: when the handler returns a changed list, Pi replays the current prompt sections and tool declarations into one leading system message ahead of the returned messages. Filtering, windowing, or slicing from a compaction summary therefore cannot drop the prompt or the tools. An unchanged list keeps mid-conversation system messages in place, so models that accept them retain their cached prefix. System messages a handler adds are kept after Pi's head. To change the prompt or the tool set durably, use [`before_agent_start`](#before_agent_start) or `pi.setActiveTools()`; to edit system messages for one request, use [`context_with_system`](#context_with_system).
736
+
737
+ #### context_with_system
738
+
739
+ Fired before each LLM call, after every `context` handler has run and Pi has restored the prompt and tool state. `event.messages` is the full transcript, including the leading system message and any mid-conversation prompt or tool patches (see [Session Format](session-format.md#sessionmessageentry)). The returned messages are sent as they are: this hook owns the prompt and tool declarations for the request.
740
+
741
+ ```typescript
742
+ import { getCurrentSystemMessage } from "@earendil-works/pi-ai";
743
+
744
+ pi.on("context_with_system", async (event, ctx) => {
745
+ const cut = findCutIndex(event.messages);
746
+ // Fold the dropped prefix so its prompt and tool state survives as the new head.
747
+ const head = getCurrentSystemMessage(event.messages.slice(0, cut));
748
+ return { messages: head ? [head, ...event.messages.slice(cut)] : event.messages.slice(cut) };
749
+ });
750
+ ```
751
+
752
+ Rules: keep a system message at index 0 (providers read the prompt and initial tool declarations there; Pi reports an error if a handler drops it). Removing a system message removes the tool declarations and section patches it carries. Check your output with `getCurrentSystemPrompt()` and `getCurrentTools()` from `@earendil-works/pi-ai`. Handlers run in extension load order; a `systemPrompt` forced from `before_agent_start` is still projected onto the request afterwards.
753
+
690
754
  #### before_provider_headers
691
755
 
692
756
  Fired after the outgoing HTTP headers are assembled. Use it to add, override, or remove request headers.
@@ -205,6 +205,7 @@ If your command is slow, expensive, rate-limited, or should keep using a previou
205
205
  | `reasoning` | No | `false` | Supports extended thinking |
206
206
  | `thinkingLevelMap` | No | omitted | Maps pi thinking levels to provider values and marks unsupported levels (see below) |
207
207
  | `input` | No | `["text"]` | Input types: `["text"]` or `["text", "image"]` |
208
+ | `inputLimits` | No | omitted | Request limits and image preprocessing for this model (see below) |
208
209
  | `contextWindow` | No | `128000` | Context window size in tokens |
209
210
  | `maxTokens` | No | `16384` | Maximum output tokens |
210
211
  | `samplingParams` | No | omitted | Sampling parameters merged verbatim into every request body (see below) |
@@ -238,6 +239,33 @@ Current behavior:
238
239
  - `/model`, `--list-models`, and the interactive footer display entries by model `id`.
239
240
  - The configured `name` is used for model matching and secondary model detail text. It does not replace the footer/status-bar model id.
240
241
 
242
+ ### Image Input Limits
243
+
244
+ Use `inputLimits.images.resize` to configure how new images are encoded before they enter conversation history:
245
+
246
+ ```json
247
+ {
248
+ "id": "vision-model",
249
+ "input": ["text", "image"],
250
+ "inputLimits": {
251
+ "images": {
252
+ "resize": {
253
+ "maxWidth": 1568,
254
+ "maxHeight": 1568,
255
+ "maxBytes": 524288,
256
+ "jpegQuality": 75
257
+ }
258
+ }
259
+ }
260
+ }
261
+ ```
262
+
263
+ `maxBytes` is the maximum base64-encoded payload size. Omitted resize fields use pi's conservative defaults: 2000×2000, 4.5 MiB encoded, and JPEG quality 80. Built-in vision models carry that profile explicitly so unknown gateways never receive larger images than before.
264
+
265
+ Pi applies the selected model's resize profile to `@file` attachments, the `read` tool, and images returned by tools. Images are encoded once before they enter history; changing models does not rewrite historical images or invalidate the cached conversation prefix. The `images.autoResize` setting can disable resizing globally.
266
+
267
+ The catalog can also record `inputLimits.maxRequestBytes`, `images.maxPerMessage`, and `images.maxPerRequest`. These fields describe hard provider limits; this initial implementation does not yet rewrite or reject conversation history based on them.
268
+
241
269
  ### Prompt Cache Lifetimes
242
270
 
243
271
  `promptCache` states how long the provider keeps a prompt cache entry alive for each retention tier pi can request (`short` is the default tier; `long` is used when `PI_CACHE_RETENTION=long`). Values are seconds and are estimates: providers publish ranges, so pick the conservative end.
@@ -374,7 +402,7 @@ Use `modelOverrides` to customize built-in models and matching extension-registe
374
402
  }
375
403
  ```
376
404
 
377
- `modelOverrides` supports these fields per model: `name`, `reasoning`, `thinkingLevelMap`, `input`, `cost` (partial), `promptCache` (merged per tier), `contextWindow`, `maxTokens`, `samplingParams` (merged per key), `headers`, `compat`.
405
+ `modelOverrides` supports these fields per model: `name`, `reasoning`, `thinkingLevelMap`, `input`, `inputLimits` (deep-merged), `cost` (partial), `promptCache` (merged per tier), `contextWindow`, `maxTokens`, `samplingParams` (merged per key), `headers`, `compat`.
378
406
 
379
407
  Use a `promptCache` override to enable cache warming through a proxy whose backing cache you know, for example OpenRouter routed to Anthropic:
380
408
 
package/bin/docs/sdk.md CHANGED
@@ -91,6 +91,8 @@ interface AgentSession {
91
91
 
92
92
  // State access
93
93
  agent: Agent;
94
+ sessionManager: SessionManager;
95
+ refreshContext(): void;
94
96
  model: Model | undefined;
95
97
  thinkingLevel: ThinkingLevel;
96
98
  messages: AgentMessage[];
@@ -251,8 +253,8 @@ const state = session.agent.state;
251
253
  // state.streamingMessage?: AgentMessage - current partial assistant message
252
254
  // state.errorMessage?: string - latest assistant error
253
255
 
254
- // Replace messages (useful for branching or restoration)
255
- session.agent.state.messages = messages; // copies the top-level array
256
+ // Model-visible messages are projected from session.sessionManager.
257
+ // agent.state.messages is a refreshed inspection cache; do not assign it for restoration.
256
258
 
257
259
  // Replace tools
258
260
  session.agent.state.tools = tools; // copies the top-level array
@@ -261,6 +263,15 @@ session.agent.state.tools = tools; // copies the top-level array
261
263
  await session.agent.waitForIdle();
262
264
  ```
263
265
 
266
+ Provider requests use `session.sessionManager` as the canonical finalized context. Assigning `session.agent.state.messages` does not replace persisted context and may be overwritten at the next request boundary. Restore externally stored history when constructing the session instead:
267
+
268
+ ```typescript
269
+ const restoredManager = SessionManager.inMemory(process.cwd(), { id: sessionId }, entries);
270
+ const { session } = await createAgentSession({ sessionManager: restoredManager });
271
+ ```
272
+
273
+ For an existing session, use `session.navigateTree(entryId)` to move its active branch. Use `session.sessionManager.appendMessage(...)` plus `session.refreshContext()` only when intentionally appending externally managed entries.
274
+
264
275
  ### Events
265
276
 
266
277
  Subscribe to events to receive streaming output and lifecycle notifications.
@@ -274,7 +274,7 @@ Created when context is compacted. Stores a summary of earlier messages and a co
274
274
  {"type":"compaction","id":"f6g7h8i9","parentId":"e5f6g7h8","timestamp":"2024-12-03T14:10:00.000Z","summary":"User discussed X, Y, Z...","firstKeptEntryId":"c3d4e5f6","tokensBefore":50000,"systemMessage":{"role":"system","content":"You are a coding assistant.","toolsAdded":[],"timestamp":1733235000000}}
275
275
  ```
276
276
 
277
- `firstKeptEntryId` is required. It identifies the first entry retained from before the compaction entry. When rebuilding context, Pi replaces older summarized entries with the compaction summary and keeps the range beginning at this entry.
277
+ `firstKeptEntryId` is required. It identifies the first entry retained from before the compaction entry. When rebuilding context, Pi replaces older summarized entries with the compaction summary and keeps the range beginning at this entry. A retain-none compaction stores its own ID in this field, so no preceding entries are retained.
278
278
 
279
279
  Optional fields:
280
280
  - `systemMessage`: The replayed prompt sections and tool declarations at the compaction boundary; it becomes the leading system message of the compacted context, and system messages among the kept entries are dropped in its favor. It is absent on older session entries.
@@ -282,6 +282,16 @@ Optional fields:
282
282
  - `details`: Implementation-specific data (e.g., `{ readFiles: string[], modifiedFiles: string[] }` for default, or custom data for extensions)
283
283
  - `fromHook`: `true` if generated by an extension, `false`/`undefined` if pi-generated (legacy field name)
284
284
 
285
+ ### ContextEditEntry
286
+
287
+ Append-only edit of one earlier context-producing entry. It changes only future model context; the target entry and its metadata remain unchanged in raw history, UI, exports, and session accounting.
288
+
289
+ ```json
290
+ {"type":"context_edit","id":"g6h7i8j9","parentId":"f6g7h8i9","timestamp":"2024-12-03T14:11:00.000Z","targetId":"c3d4e5f6","replacement":null}
291
+ ```
292
+
293
+ Targets may be user, assistant, tool-result, or custom-message entries. `replacement: null` omits the target from model context. A non-null `replacement` replaces only the target message content. String replacements for assistant and tool-result entries are normalized to one text block because those roles require content arrays. If several edits target the same entry, the latest edit on the active branch wins. Edits are branch-relative: navigating to a point before the edit reveals the target's original contribution again.
294
+
285
295
  ### BranchSummaryEntry
286
296
 
287
297
  Created when switching branches via `/tree` with an LLM generated summary of the left branch up to the common ancestor. Captures context from the abandoned path.
@@ -366,7 +376,9 @@ Entries normally form one tree, but navigation APIs can create multiple roots:
366
376
  - Includes entries after the compaction entry
367
377
  3. Preserves non-message entries in the selected range so interactive mode can render them
368
378
 
369
- `buildSessionContext()` builds on that entry list to produce the message list for the LLM:
379
+ `buildSessionProjection()` then applies the latest `context_edit` for each selected target. It returns the model-visible messages together with their source entries. Omitted targets produce no message; replacements retain the source entry's role and metadata while changing only content. The raw selected entries are not modified.
380
+
381
+ `buildSessionContext()` builds on that projection to produce the message list for the LLM:
370
382
 
371
383
  1. Extracts current model and thinking level settings from the full path
372
384
  2. Converts selected entries to messages:
@@ -374,6 +386,7 @@ Entries normally form one tree, but navigation APIs can create multiple roots:
374
386
  - `compaction` -> complete system checkpoint followed by `compactionSummary`
375
387
  - `branch_summary` -> `branchSummary`
376
388
  - `custom_message` -> `CustomMessage`
389
+ - `context_edit` -> no context message of its own
377
390
  - `usage` and `custom` -> no context message
378
391
 
379
392
  The compaction summary replaces entries before `firstKeptEntryId`. Pre-compaction system messages are folded into the complete checkpoint rather than replayed from the retained range. Retained non-system entries and all entries after the compaction remain available to the LLM.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pi-extension-custom-provider",
3
- "version": "0.86.1",
3
+ "version": "0.87.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pi-extension-custom-provider",
9
- "version": "0.86.1",
9
+ "version": "0.87.0",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.52.0"
12
12
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-custom-provider-anthropic",
3
3
  "private": true,
4
- "version": "0.86.1",
4
+ "version": "0.87.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-custom-provider-gitlab-duo",
3
3
  "private": true,
4
- "version": "0.86.1",
4
+ "version": "0.87.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pi-extension-gondolin",
3
- "version": "0.86.1",
3
+ "version": "0.87.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pi-extension-gondolin",
9
- "version": "0.86.1",
9
+ "version": "0.87.0",
10
10
  "dependencies": {
11
11
  "@earendil-works/gondolin": "0.12.0"
12
12
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-gondolin",
3
3
  "private": true,
4
- "version": "0.86.1",
4
+ "version": "0.87.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pi-extension-sandbox",
3
- "version": "1.16.1",
3
+ "version": "1.17.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pi-extension-sandbox",
9
- "version": "1.16.1",
9
+ "version": "1.17.0",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sandbox-runtime": "^0.0.26"
12
12
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-sandbox",
3
3
  "private": true,
4
- "version": "1.16.1",
4
+ "version": "1.17.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pi-extension-with-deps",
3
- "version": "0.86.1",
3
+ "version": "0.87.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pi-extension-with-deps",
9
- "version": "0.86.1",
9
+ "version": "0.87.0",
10
10
  "dependencies": {
11
11
  "ms": "^2.1.3"
12
12
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-with-deps",
3
3
  "private": true,
4
- "version": "0.86.1",
4
+ "version": "0.87.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -16,8 +16,9 @@ export default defineFacet({
16
16
  },
17
17
  });
18
18
  env.onActivate(() => {
19
- workerActivations.state.count += 1;
20
- workerActivations.publish(BACKGROUND_CONTEXT);
19
+ workerActivations.change(BACKGROUND_CONTEXT, (draft) => {
20
+ draft.count += 1;
21
+ });
21
22
  });
22
23
  },
23
24
  });
@@ -357,6 +357,9 @@
357
357
  case 'thinking_level_change':
358
358
  parts.push('thinking', entry.thinkingLevel);
359
359
  break;
360
+ case 'context_edit':
361
+ parts.push('context edit', entry.replacement === null ? 'omit' : 'replace', entry.targetId);
362
+ break;
360
363
  }
361
364
 
362
365
  return parts.join(' ').toLowerCase();
@@ -385,7 +388,7 @@
385
388
  }
386
389
 
387
390
  // Apply filter mode
388
- const isSettingsEntry = ['label', 'custom', 'model_change', 'thinking_level_change'].includes(entry.type);
391
+ const isSettingsEntry = ['label', 'custom', 'context_edit', 'model_change', 'thinking_level_change'].includes(entry.type);
389
392
  let passesFilter = true;
390
393
 
391
394
  switch (filterMode) {
@@ -696,6 +699,8 @@
696
699
  return labelHtml + `<span class="tree-muted">[model: ${escapeHtml(entry.modelId)}]</span>`;
697
700
  case 'thinking_level_change':
698
701
  return labelHtml + `<span class="tree-muted">[thinking: ${escapeHtml(entry.thinkingLevel)}]</span>`;
702
+ case 'context_edit':
703
+ return labelHtml + `<span class="tree-muted">[context ${entry.replacement === null ? 'omit' : 'replace'}: ${escapeHtml(entry.targetId)}]</span>`;
699
704
  default:
700
705
  return labelHtml + `<span class="tree-muted">[${escapeHtml(entry.type)}]</span>`;
701
706
  }
package/bin/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@earendil-works/pi-coding-agent",
3
- "version": "0.86.1",
3
+ "version": "0.87.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -49,10 +49,10 @@
49
49
  "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap"
50
50
  },
51
51
  "dependencies": {
52
- "@earendil-works/chord": "^0.86.1",
53
- "@earendil-works/pi-agent-core": "^0.86.1",
54
- "@earendil-works/pi-ai": "^0.86.1",
55
- "@earendil-works/pi-tui": "^0.86.1",
52
+ "@earendil-works/chord": "^0.87.0",
53
+ "@earendil-works/pi-agent-core": "^0.87.0",
54
+ "@earendil-works/pi-ai": "^0.87.0",
55
+ "@earendil-works/pi-tui": "^0.87.0",
56
56
  "@silvia-odwyer/photon-node": "0.3.4",
57
57
  "chalk": "6.0.0",
58
58
  "cross-spawn": "7.0.6",
@@ -73,9 +73,9 @@
73
73
  "protobufjs": "7.6.6"
74
74
  },
75
75
  "devDependencies": {
76
- "@earendil-works/pi-client": "^0.86.1",
77
- "@earendil-works/pi-protocol": "^0.86.1",
78
- "@earendil-works/pi-server": "^0.86.1",
76
+ "@earendil-works/pi-client": "^0.87.0",
77
+ "@earendil-works/pi-protocol": "^0.87.0",
78
+ "@earendil-works/pi-server": "^0.87.0",
79
79
  "@types/cross-spawn": "6.0.6",
80
80
  "@types/hosted-git-info": "3.0.5",
81
81
  "@types/node": "22.19.19",
package/bin/pi.exe CHANGED
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/bin/pi.exe
3
- size: 109688320 bytes
4
- sha256: 5c97d227c7fd086ffb5ad6608e22d95abfbe60f9668c5534e69ab5efd941c09b
3
+ size: 109871104 bytes
4
+ sha256: 50d68629570e8b7e9b7c2fc5df9522e3c0f135e693b597e5ad52ba3ec75910e4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kingingwang/pi-windows-x64",
3
- "version": "0.86.1",
3
+ "version": "0.87.0",
4
4
  "description": "Pi standalone binary for windows-x64 (fork distribution)",
5
5
  "license": "MIT",
6
6
  "os": [