From aef5fc429b9895d3f3a39acb8a8c4789696d3891 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 21 Sep 2026 17:14:22 +0200 Subject: [PATCH 01/10] fix(coding-agent): keep prompt and tool state across context handlers (#9846) Since 0.86 the prompt and tool declarations live in transcript system messages, so a context handler that filters or slices messages could drop them. After extension-driven compaction this sent requests without built-in tools and made Codex emit raw tool-call text. context handlers now see the conversation only. An unchanged list keeps the transcript as is; a changed list gets the replayed prompt sections and tool declarations as one leading system message. Handler-added system messages are kept after it. Add context_with_system, which runs after every context handler on the full transcript and sends its result verbatim, for extensions that need to edit system messages per request. Dropping the leading system message is reported as an extension error. closes #9789, closes #9822 --- packages/coding-agent/CHANGELOG.md | 2 + packages/coding-agent/docs/extensions.md | 24 +- .../coding-agent/src/core/extensions/index.ts | 1 + .../src/core/extensions/runner.ts | 81 ++++++- .../coding-agent/src/core/extensions/types.ts | 20 +- packages/coding-agent/src/core/index.ts | 1 + packages/coding-agent/src/index.ts | 1 + ...89-context-handler-system-messages.test.ts | 226 ++++++++++++++++++ 8 files changed, 347 insertions(+), 9 deletions(-) create mode 100644 packages/coding-agent/test/suite/regressions/9789-context-handler-system-messages.test.ts diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index e16245f9e..fde7e41ac 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -19,6 +19,7 @@ - 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. - 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. - Added retain-none compaction input: `sessionManager.appendCompaction(summary, null, tokensBefore)` stores the compaction's own ID as its kept boundary. +- 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). - 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)). ### Fixed @@ -27,6 +28,7 @@ - Fixed context-invisible boundary metadata and replacement edits causing newly appended or replaced input to be summarized before its first provider request. - 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. - 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. +- 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)). - Fixed `/bug` allowing uploads in offline mode while preserving local zip exports. - Fixed idle prompt-cache warming rebuilding expired caches when its timer or an extension decision is delayed. - Improved crash diagnostics with hints identifying loaded extensions that appear in the stack trace. diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index d58304c3b..89afa4863 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -294,7 +294,8 @@ user sends prompt ──────────────────── │ ┌─── turn (repeats while LLM calls tools) ───┐ │ │ │ │ │ │ ├─► turn_start │ │ - │ ├─► context (can modify messages) │ │ + │ ├─► context (can modify conversation messages) │ + │ ├─► context_with_system (can modify the full transcript) │ ├─► before_provider_headers (can mutate headers) | │ ├─► before_provider_request (can inspect or replace payload) │ ├─► after_provider_response (status + headers, before stream consume) @@ -725,12 +726,31 @@ Fired before each LLM call. Modify messages non-destructively. See [Session Form ```typescript pi.on("context", async (event, ctx) => { - // event.messages - deep copy, safe to modify + // event.messages - deep copy without system messages, safe to modify const filtered = event.messages.filter(m => !shouldPrune(m)); return { messages: filtered }; }); ``` +`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). + +#### context_with_system + +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. + +```typescript +import { getCurrentSystemMessage } from "@earendil-works/pi-ai"; + +pi.on("context_with_system", async (event, ctx) => { + const cut = findCutIndex(event.messages); + // Fold the dropped prefix so its prompt and tool state survives as the new head. + const head = getCurrentSystemMessage(event.messages.slice(0, cut)); + return { messages: head ? [head, ...event.messages.slice(cut)] : event.messages.slice(cut) }; +}); +``` + +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. + #### before_provider_headers Fired after the outgoing HTTP headers are assembled. Use it to add, override, or remove request headers. diff --git a/packages/coding-agent/src/core/extensions/index.ts b/packages/coding-agent/src/core/extensions/index.ts index 57b2e77bf..ad0287629 100644 --- a/packages/coding-agent/src/core/extensions/index.ts +++ b/packages/coding-agent/src/core/extensions/index.ts @@ -57,6 +57,7 @@ export type { // Event Results ContextEventResult, ContextUsage, + ContextWithSystemEvent, CustomEntryDraft, CustomMessageEntryDraft, CustomToolCallEvent, diff --git a/packages/coding-agent/src/core/extensions/runner.ts b/packages/coding-agent/src/core/extensions/runner.ts index 44576a1bb..b0e99f8ff 100644 --- a/packages/coding-agent/src/core/extensions/runner.ts +++ b/packages/coding-agent/src/core/extensions/runner.ts @@ -3,7 +3,13 @@ */ import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { ImageContent, Model, Provider, ProviderHeaders } from "@earendil-works/pi-ai"; +import { + getCurrentSystemMessage, + type ImageContent, + type Model, + type Provider, + type ProviderHeaders, +} from "@earendil-works/pi-ai"; import type { KeyId } from "@earendil-works/pi-tui"; import { type Theme, theme } from "../../modes/interactive/theme/theme.ts"; import type { CacheWarmingAction } from "../cache-warmer.ts"; @@ -32,6 +38,7 @@ import type { ContextEvent, ContextEventResult, ContextUsage, + ContextWithSystemEvent, EntryRenderer, Extension, ExtensionActions, @@ -169,6 +176,7 @@ type RunnerEmitEvent = Exclude< | ToolResultEvent | UserBashEvent | ContextEvent + | ContextWithSystemEvent | CacheWarmingDecisionEvent | BeforeProviderRequestEvent | BeforeProviderHeadersEvent @@ -258,6 +266,28 @@ function snapshotEventHandlers(extensions: Extension[], event: ExtensionEvent["t return extensions.map((ext) => ({ ext, handlers: ext.handlers.get(event)?.slice() ?? [] })); } +function sameMessages(left: AgentMessage[], right: AgentMessage[]): boolean { + return left.length === right.length && left.every((message, index) => message === right[index]); +} + +/** + * Re-attach the prompt and tool state after a `context` handler. Handlers only see the + * conversation; the system messages belong to Pi. An unchanged conversation keeps every + * system message in place, so models with mid-conversation support keep their cached + * prefix. A changed one gets the replayed prompt sections and tool declarations as one + * leading system message, so pruning, windowing, or slicing from a compaction summary + * cannot drop them. + */ +function restoreSystemMessages( + current: AgentMessage[], + visible: AgentMessage[], + returned: AgentMessage[], +): AgentMessage[] { + if (sameMessages(returned, visible)) return current; + const head = getCurrentSystemMessage(current); + return head ? [head, ...returned] : returned; +} + export async function emitProjectTrustEvent( extensionsResult: LoadExtensionsResult, event: ProjectTrustEvent, @@ -1152,6 +1182,11 @@ export class ExtensionRunner { return undefined; } + /** + * Run the request-time transforms in two phases. `context` handlers see the conversation + * only and Pi restores the prompt and tool state after each; `context_with_system` + * handlers then see the full transcript and their output is used as returned. + */ async emitContext(messages: AgentMessage[]): Promise { const ctx = this.createContext(); let currentMessages = structuredClone(messages); @@ -1159,12 +1194,17 @@ export class ExtensionRunner { for (const { ext, handlers } of snapshotEventHandlers(this.extensions, "context")) { for (const handler of handlers) { try { - const event: ContextEvent = { type: "context", messages: currentMessages }; - const handlerResult = await handler(event, ctx); + const visibleMessages = currentMessages.filter((message) => message.role !== "system"); + const visibleSnapshot = visibleMessages.slice(); + const event: ContextEvent = { type: "context", messages: visibleMessages }; + const handlerResult = (await handler(event, ctx)) as ContextEventResult | undefined; - if (handlerResult && (handlerResult as ContextEventResult).messages) { - currentMessages = (handlerResult as ContextEventResult).messages!; - } + // Handlers may return a new list or edit event.messages in place. + const returned = + handlerResult?.messages ?? + (sameMessages(visibleMessages, visibleSnapshot) ? undefined : visibleMessages); + if (!returned) continue; + currentMessages = restoreSystemMessages(currentMessages, visibleSnapshot, returned); } catch (err) { const message = err instanceof Error ? err.message : String(err); const stack = err instanceof Error ? err.stack : undefined; @@ -1178,6 +1218,35 @@ export class ExtensionRunner { } } + for (const { ext, handlers } of snapshotEventHandlers(this.extensions, "context_with_system")) { + for (const handler of handlers) { + try { + const hadLeadingSystemMessage = currentMessages[0]?.role === "system"; + const event: ContextWithSystemEvent = { type: "context_with_system", messages: currentMessages }; + const handlerResult = (await handler(event, ctx)) as ContextEventResult | undefined; + currentMessages = handlerResult?.messages ?? currentMessages; + // Providers read the prompt and initial tools from the leading system message. + // Losing it is never intended; report it but honor the handler's output. + if (hadLeadingSystemMessage && currentMessages[0]?.role !== "system") { + this.emitError({ + extensionPath: ext.path, + event: "context_with_system", + error: "Handler removed the leading system message; the request has no prompt or initial tool declarations. Keep it at index 0 or replace a dropped prefix with getCurrentSystemMessage().", + }); + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + const stack = err instanceof Error ? err.stack : undefined; + this.emitError({ + extensionPath: ext.path, + event: "context_with_system", + error: message, + stack, + }); + } + } + } + return currentMessages; } diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index d10078701..41bc4e672 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -688,12 +688,28 @@ export type SessionEvent = // Agent Events // ============================================================================ -/** Fired before each LLM call. Can modify messages. */ +/** + * Fired before each LLM call. Can modify messages. + * + * `messages` holds the conversation without system messages. The prompt and tool state + * belong to Pi: it restores them after the handler returns, so a handler cannot drop + * them and does not need to preserve them. + */ export interface ContextEvent { type: "context"; messages: AgentMessage[]; } +/** + * Fired before each LLM call, after every `context` handler has run and Pi has restored + * the prompt and tool state. `messages` is the full transcript including system messages, + * and the result is sent as returned: the handler owns the prompt and tool declarations. + */ +export interface ContextWithSystemEvent { + type: "context_with_system"; + messages: AgentMessage[]; +} + /** Fired before a provider request is sent. Can replace the payload. */ export interface BeforeProviderRequestEvent { type: "before_provider_request"; @@ -1156,6 +1172,7 @@ export type ExtensionEvent = | ResourcesDiscoverEvent | SessionEvent | ContextEvent + | ContextWithSystemEvent | CacheWarmingDecisionEvent | BeforeProviderRequestEvent | BeforeProviderHeadersEvent @@ -1362,6 +1379,7 @@ export interface ExtensionAPI { ): () => void; on(event: "session_tree", handler: ExtensionHandler): () => void; on(event: "context", handler: ExtensionHandler): () => void; + on(event: "context_with_system", handler: ExtensionHandler): () => void; on( event: "cache_warming_decision", handler: ExtensionHandler, diff --git a/packages/coding-agent/src/core/index.ts b/packages/coding-agent/src/core/index.ts index 178ed7105..fe82f5691 100644 --- a/packages/coding-agent/src/core/index.ts +++ b/packages/coding-agent/src/core/index.ts @@ -51,6 +51,7 @@ export { type CompactionEntryDraft, type ContextEditEntryDraft, type ContextEvent, + type ContextWithSystemEvent, type CustomEntryDraft, type CustomMessageEntryDraft, defineTool, diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index 86907ebba..8746a894b 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -81,6 +81,7 @@ export type { ContextEvent, ContextEventResult, ContextUsage, + ContextWithSystemEvent, CustomEntryDraft, CustomMessageEntryDraft, CustomToolCallEvent, diff --git a/packages/coding-agent/test/suite/regressions/9789-context-handler-system-messages.test.ts b/packages/coding-agent/test/suite/regressions/9789-context-handler-system-messages.test.ts new file mode 100644 index 000000000..aa3816dcb --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/9789-context-handler-system-messages.test.ts @@ -0,0 +1,226 @@ +import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import { + fauxAssistantMessage, + getCurrentSystemPrompt, + getCurrentTools, + type TranscriptContext, +} from "@earendil-works/pi-ai"; +import { afterEach, describe, expect, it } from "vitest"; +import type { ExtensionFactory } from "../../../src/index.ts"; +import { createHarness, type Harness } from "../harness.ts"; + +/** Compaction supplied by an extension hook, as in the reported sessions. */ +const compactViaHook: ExtensionFactory = (pi) => { + pi.on("session_before_compact", async (event) => ({ + compaction: { + summary: "extension summary", + firstKeptEntryId: event.preparation.firstKeptEntryId, + tokensBefore: event.preparation.tokensBefore, + details: { source: "test" }, + }, + })); +}; + +function captureRequest(harness: Harness, text: string): () => TranscriptContext { + let request: TranscriptContext | undefined; + harness.setResponses([ + (context) => { + request = context; + return fauxAssistantMessage(text); + }, + ]); + return () => { + if (!request) throw new Error("expected a provider request"); + return request; + }; +} + +function toolNames(context: TranscriptContext): string[] { + return getCurrentTools(context.messages).map((tool) => tool.name); +} + +async function compactSession(harness: Harness): Promise { + harness.settingsManager.applyOverrides({ compaction: { keepRecentTokens: 1 } }); + harness.setResponses([fauxAssistantMessage("one"), fauxAssistantMessage("two")]); + await harness.session.prompt("first"); + await harness.session.prompt("second"); + await harness.session.compact(); + expect(harness.session.messages.map((message) => message.role).slice(0, 2)).toEqual(["system", "compactionSummary"]); +} + +describe("context handlers and system messages", () => { + const harnesses: Harness[] = []; + + afterEach(() => { + while (harnesses.length > 0) harnesses.pop()?.cleanup(); + }); + + // Regression #9789, #9822: pruning from the compaction summary dropped the prompt and tool checkpoint. + it("keeps the prompt and tools when a handler slices from the compaction summary", async () => { + const seen: AgentMessage[][] = []; + const harness = await createHarness({ + extensionFactories: [ + compactViaHook, + (pi) => { + pi.on("context", async (event) => { + seen.push(event.messages); + const summary = event.messages.findIndex((message) => message.role === "compactionSummary"); + return { messages: event.messages.slice(summary) }; + }); + }, + ], + }); + harnesses.push(harness); + await compactSession(harness); + const getRequest = captureRequest(harness, "after compaction"); + + await harness.session.prompt("third"); + + const request = getRequest(); + expect(seen.at(-1)?.some((message) => message.role === "system")).toBe(false); + expect(request.messages[0]?.role).toBe("system"); + expect(toolNames(request)).toEqual(harness.session.getActiveToolNames()); + expect(getCurrentSystemPrompt(request.messages)).toBe(harness.session.systemPrompt); + expect(request.messages.filter((message) => message.role === "system")).toHaveLength(1); + }); + + it("keeps mid-conversation system messages in place when a handler leaves the conversation unchanged", async () => { + let turn = 0; + const harness = await createHarness({ + extensionFactories: [ + (pi) => { + pi.on("before_agent_start", (event) => { + if (++turn === 2) event.systemPromptOptions.sections.plan_mode = "Plan only."; + }); + pi.on("context", async (event) => ({ messages: event.messages })); + }, + ], + }); + harnesses.push(harness); + harness.setResponses([fauxAssistantMessage("one")]); + await harness.session.prompt("first"); + const getRequest = captureRequest(harness, "two"); + + await harness.session.prompt("second"); + + const systemMessages = getRequest().messages.filter((message) => message.role === "system"); + expect(systemMessages).toHaveLength(2); + expect(systemMessages[1]?.sections).toEqual({ plan_mode: "\nPlan only.\n" }); + }); + + it("applies in-place edits to event.messages without a return value", async () => { + const harness = await createHarness({ + extensionFactories: [ + (pi) => { + pi.on("context", async (event) => { + event.messages.splice(0, 0, { + role: "user", + content: [{ type: "text", text: "injected" }], + timestamp: 0, + }); + }); + }, + ], + }); + harnesses.push(harness); + const getRequest = captureRequest(harness, "done"); + + await harness.session.prompt("hello"); + + const request = getRequest(); + expect(request.messages.map((message) => message.role)).toEqual(["system", "user", "user"]); + expect(toolNames(request)).toEqual(harness.session.getActiveToolNames()); + }); + + it("keeps system messages a handler adds after the replayed head", async () => { + const harness = await createHarness({ + extensionFactories: [ + (pi) => { + pi.on("context", async (event) => ({ + messages: [{ role: "system", content: "ephemeral reminder", timestamp: 0 }, ...event.messages], + })); + }, + ], + }); + harnesses.push(harness); + const getRequest = captureRequest(harness, "done"); + + await harness.session.prompt("hello"); + + const request = getRequest(); + expect(request.messages.map((message) => message.role)).toEqual(["system", "system", "user"]); + expect(toolNames(request)).toEqual(harness.session.getActiveToolNames()); + expect(getCurrentSystemPrompt(request.messages)).toContain(harness.session.systemPrompt); + expect(getCurrentSystemPrompt(request.messages)).toContain("ephemeral reminder"); + }); +}); + +describe("context_with_system handlers", () => { + const harnesses: Harness[] = []; + + afterEach(() => { + while (harnesses.length > 0) harnesses.pop()?.cleanup(); + }); + + it("runs after context handlers on the restored transcript and sends its output verbatim", async () => { + const seen: AgentMessage[][] = []; + const harness = await createHarness({ + extensionFactories: [ + compactViaHook, + (pi) => { + pi.on("context_with_system", async (event) => { + seen.push(event.messages); + return { + messages: event.messages.map((message) => + message.role === "system" && message.toolsAdded + ? { ...message, toolsAdded: message.toolsAdded.filter((tool) => tool.name !== "bash") } + : message, + ), + }; + }); + // Registered after, but runs first: context handlers precede context_with_system. + pi.on("context", async (event) => { + const summary = event.messages.findIndex((message) => message.role === "compactionSummary"); + return { messages: event.messages.slice(summary) }; + }); + }, + ], + }); + harnesses.push(harness); + await compactSession(harness); + const getRequest = captureRequest(harness, "after compaction"); + + await harness.session.prompt("third"); + + const input = seen.at(-1); + expect(input?.[0]?.role).toBe("system"); + expect(input?.[1]?.role).toBe("compactionSummary"); + expect(harness.session.getActiveToolNames()).toContain("bash"); + expect(toolNames(getRequest())).toEqual(harness.session.getActiveToolNames().filter((name) => name !== "bash")); + }); + + it("reports a handler that drops the leading system message but honors its output", async () => { + const harness = await createHarness({ + extensionFactories: [ + (pi) => { + pi.on("context_with_system", async (event) => ({ + messages: event.messages.filter((message) => message.role !== "system"), + })); + }, + ], + }); + harnesses.push(harness); + const errors: string[] = []; + harness.session.extensionRunner.onError((error) => { + errors.push(`${error.event}: ${error.error}`); + }); + const getRequest = captureRequest(harness, "done"); + + await harness.session.prompt("hello"); + + expect(getRequest().messages.map((message) => message.role)).toEqual(["user"]); + expect(errors).toEqual([ + expect.stringMatching(/^context_with_system: Handler removed the leading system message/), + ]); + }); +}); From 8c727937854b4f435df56809b022ae1015a17c9e Mon Sep 17 00:00:00 2001 From: Ramiz Wachtler Date: Mon, 21 Sep 2026 17:16:06 +0200 Subject: [PATCH 02/10] fix(tui): prevent jump-to-end label from shifting when scrollbar hides (#9842) - center the label independently of scrollbar visibility - clip drawing and click bounds before the scrollbar - fixes #9136 --- packages/tui/src/tui-alt-screen.ts | 9 +++-- packages/tui/test/tui-alt-screen.test.ts | 47 +++++++++++++++++++++++- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/packages/tui/src/tui-alt-screen.ts b/packages/tui/src/tui-alt-screen.ts index 1b3d2614d..cc9a04f43 100644 --- a/packages/tui/src/tui-alt-screen.ts +++ b/packages/tui/src/tui-alt-screen.ts @@ -1630,11 +1630,14 @@ export class TuiAltScreen extends TuiBase implements ViewportTUI { const row = clip.y + clip.height - 1; if (row >= screen.length || isImageLine(screen[row] ?? "")) return screen; const scrollbarColumn = box ? getScrollbarGeometry(box)?.column : undefined; - const availableWidth = Math.max(0, (scrollbarColumn ?? clip.x + clip.width) - clip.x); - const text = truncateToWidth(this.scrollToEndIndicator(), availableWidth, ""); + const label = truncateToWidth(this.scrollToEndIndicator(), clip.width, ""); + const labelWidth = visibleWidth(label); + const column = clip.x + Math.floor((clip.width - labelWidth) / 2); + const rightEdge = scrollbarColumn ?? clip.x + clip.width; + const availableWidth = Math.max(0, rightEdge - column); + const text = truncateToWidth(label, availableWidth, ""); const textWidth = visibleWidth(text); if (textWidth === 0) return screen; - const column = clip.x + Math.floor((availableWidth - textWidth) / 2); const result = [...screen]; result[row] = compositeTuiLine(result[row] ?? "", text, column, textWidth, width); this.scrollToEndIndicatorRect = { row, column, width: textWidth }; diff --git a/packages/tui/test/tui-alt-screen.test.ts b/packages/tui/test/tui-alt-screen.test.ts index fa007d2ad..e79169127 100644 --- a/packages/tui/test/tui-alt-screen.test.ts +++ b/packages/tui/test/tui-alt-screen.test.ts @@ -138,7 +138,51 @@ describe("TuiAltScreen", () => { tui.stop(); }); - it("leaves the scrollbar clickable when the jump-to-end indicator spans the transcript", async () => { + it("keeps the jump-to-end indicator centered as the auto scrollbar hides and reappears", async () => { + // Regression test for #9136: auto scrollbar visibility must not move the indicator. + const terminal = new VirtualTerminal(80, 6); + const label = " ↓ Jump to latest message · End "; + const tui = new TuiAltScreen(terminal, undefined, undefined, { + scrollToEndIndicator: () => label, + }); + const transcript = new ScrollView( + new Text(Array.from({ length: 20 }, (_, index) => `line ${index + 1}`).join("\n"), 0, 0), + { follow: "end", primary: true, scrollbar: "auto", scrollbarHideDelayMs: 0 }, + ); + tui.setLayoutRoot(transcript); + tui.start(); + try { + await terminal.waitForRender(); + + // Scrolling over the track keeps the scrollbar visible until the pointer leaves. + terminal.sendInput("\x1b[<64;80;1M"); + await terminal.waitForRender(); + assert.strictEqual(transcript.isScrollbarVisible, true); + assert.strictEqual(transcript.isFollowingEnd, false); + const scrollTop = transcript.scrollTop; + const visibleColumn = terminal.getViewport()[5].indexOf(label); + + // Leaving the track lets the auto-hide timer expire without changing the content. + terminal.sendInput("\x1b[<35;79;1M"); + await terminal.waitForRender(); + assert.strictEqual(transcript.isScrollbarVisible, false); + assert.strictEqual(transcript.scrollTop, scrollTop); + const hiddenColumn = terminal.getViewport()[5].indexOf(label); + + terminal.sendInput("\x1b[<35;80;1M"); + await terminal.waitForRender(); + assert.strictEqual(transcript.isScrollbarVisible, true); + assert.strictEqual(transcript.scrollTop, scrollTop); + const revealedColumn = terminal.getViewport()[5].indexOf(label); + + assert.deepStrictEqual([visibleColumn, hiddenColumn, revealedColumn], [24, 24, 24]); + } finally { + tui.stop(); + } + }); + + it("leaves the scrollbar visible and clickable when the jump-to-end indicator spans the transcript", async () => { + // Regression coverage for #9136: centering must not paint or capture clicks over the scrollbar. const terminal = new VirtualTerminal(30, 6); const tui = new TuiAltScreen(terminal, undefined, undefined, { scrollToEndIndicator: () => "↓".repeat(30), @@ -161,6 +205,7 @@ describe("TuiAltScreen", () => { assert.strictEqual(transcript.isFollowingEnd, false); // The indicator must not intercept a press on the scrollbar's last column. + assert.strictEqual(terminal.getViewport()[3], `${"↓".repeat(29)}┃`); terminal.sendInput("\x1b[<0;30;4M"); terminal.sendInput("\x1b[<0;30;4m"); await terminal.waitForRender(); From 7f06f9cf1626504cde95683f1c81a72a7bc7a0cb Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 21 Sep 2026 17:22:53 +0200 Subject: [PATCH 03/10] docs(coding-agent): complete unreleased changelog --- packages/coding-agent/CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index fde7e41ac..d4ff84b9e 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,8 @@ ### New Features +- **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). +- **Full-transcript context extensions** — Use `context_with_system` for per-request system-message transformations. See [`context_with_system`](docs/extensions.md#context_with_system). - **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). ### Breaking Changes @@ -29,11 +31,11 @@ - 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. - 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. - 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)). -- Fixed `/bug` allowing uploads in offline mode while preserving local zip exports. +- 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)). - Fixed idle prompt-cache warming rebuilding expired caches when its timer or an extension decision is delayed. - Improved crash diagnostics with hints identifying loaded extensions that appear in the stack trace. - 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)). -- Fixed malformed prompt template frontmatter being silently ignored instead of reported as a resource warning ([#9830](https://github.com/earendil-works/pi/pull/9830)). +- 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)). - 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)). ## [0.86.1] - 2026-09-20 From eaf72ed4d86be179e4b0b4c64af04e92b49d19bd Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 21 Sep 2026 18:26:29 +0200 Subject: [PATCH 04/10] fix(coding-agent): update stale test expectations --- .../test/agent-session-concurrent.test.ts | 1 + packages/coding-agent/test/model-registry.test.ts | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/coding-agent/test/agent-session-concurrent.test.ts b/packages/coding-agent/test/agent-session-concurrent.test.ts index 0df97e0d9..1fb8b4517 100644 --- a/packages/coding-agent/test/agent-session-concurrent.test.ts +++ b/packages/coding-agent/test/agent-session-concurrent.test.ts @@ -632,6 +632,7 @@ describe("AgentSession concurrent prompt guard", () => { "user", "assistant", "toolResult", + "system", "assistant", ]); }); diff --git a/packages/coding-agent/test/model-registry.test.ts b/packages/coding-agent/test/model-registry.test.ts index 17ba7a182..ccf925f09 100644 --- a/packages/coding-agent/test/model-registry.test.ts +++ b/packages/coding-agent/test/model-registry.test.ts @@ -704,7 +704,7 @@ describe("ModelRegistry", () => { expect(sonnet?.name).toBe("Custom Sonnet Name"); // Other models should be unchanged - const opus = models.find((m) => m.id === "anthropic/claude-opus-4"); + const opus = models.find((m) => m.id === "anthropic/claude-opus-4.1"); expect(opus?.name).not.toBe("Custom Sonnet Name"); }); @@ -783,7 +783,7 @@ describe("ModelRegistry", () => { expect(sonnet?.samplingParams).toEqual({ top_p: 0.9 }); // Models without sampling config keep it unset. - const opus = models.find((m) => m.id === "anthropic/claude-opus-4"); + const opus = models.find((m) => m.id === "anthropic/claude-opus-4.1"); expect(opus?.samplingParams).toBeUndefined(); }); @@ -810,7 +810,7 @@ describe("ModelRegistry", () => { expect(registry.getError()).toBeUndefined(); expect(openrouter.find((m) => m.id === "custom/cached-model")?.promptCache).toEqual({ short: 120 }); expect(openrouter.find((m) => m.id === "anthropic/claude-sonnet-4")?.promptCache).toEqual({ short: 300 }); - expect(openrouter.find((m) => m.id === "anthropic/claude-opus-4")?.promptCache).toBeUndefined(); + expect(openrouter.find((m) => m.id === "anthropic/claude-opus-4.1")?.promptCache).toBeUndefined(); // Overrides merge per tier with the built-in catalog. expect(registry.find("anthropic", "claude-sonnet-4-6")?.promptCache).toEqual({ short: 300, long: 1800 }); }); @@ -898,7 +898,7 @@ describe("ModelRegistry", () => { const registry = await createModelRegistry(authStorage, modelsJsonPath); const models = getModelsForProvider(registry, "openrouter"); const sonnet = models.find((model) => model.id === "anthropic/claude-sonnet-4"); - const opus = models.find((model) => model.id === "anthropic/claude-opus-4"); + const opus = models.find((model) => model.id === "anthropic/claude-opus-4.1"); expect((sonnet?.compat as OpenAICompletionsCompat | undefined)?.supportsFinishReason).toBe(false); expect((opus?.compat as OpenAICompletionsCompat | undefined)?.supportsFinishReason).toBe(true); @@ -933,7 +933,7 @@ describe("ModelRegistry", () => { "anthropic/claude-sonnet-4": { compat: { openRouterRouting: { only: ["amazon-bedrock"] } }, }, - "anthropic/claude-opus-4": { + "anthropic/claude-opus-4.1": { compat: { openRouterRouting: { only: ["anthropic"] } }, }, }, @@ -944,7 +944,7 @@ describe("ModelRegistry", () => { const models = getModelsForProvider(registry, "openrouter"); const sonnet = models.find((m) => m.id === "anthropic/claude-sonnet-4"); - const opus = models.find((m) => m.id === "anthropic/claude-opus-4"); + const opus = models.find((m) => m.id === "anthropic/claude-opus-4.1"); const sonnetCompat = sonnet?.compat as OpenAICompletionsCompat | undefined; const opusCompat = opus?.compat as OpenAICompletionsCompat | undefined; @@ -973,7 +973,7 @@ describe("ModelRegistry", () => { expect(sonnet?.name).toBe("Proxied Sonnet"); // Other models should have the baseUrl but not the name override - const opus = models.find((m) => m.id === "anthropic/claude-opus-4"); + const opus = models.find((m) => m.id === "anthropic/claude-opus-4.1"); expect(opus?.baseUrl).toBe("https://my-proxy.example.com/v1"); expect(opus?.name).not.toBe("Proxied Sonnet"); }); From 16787ad5b2dc748047f314ca1bfe7708f30f54f3 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 21 Sep 2026 18:31:52 +0200 Subject: [PATCH 05/10] Release v0.87.0 --- package-lock.json | 80 +++++++++---------- packages/agent/CHANGELOG.md | 2 +- packages/agent/package.json | 8 +- packages/ai/CHANGELOG.md | 2 +- packages/ai/package.json | 4 +- packages/chord/package.json | 2 +- packages/client/CHANGELOG.md | 2 +- packages/client/package.json | 6 +- packages/coding-agent/CHANGELOG.md | 2 +- .../package-lock.json | 4 +- .../custom-provider-anthropic/package.json | 2 +- .../custom-provider-gitlab-duo/package.json | 2 +- .../extensions/gondolin/package-lock.json | 4 +- .../examples/extensions/gondolin/package.json | 2 +- .../extensions/sandbox/package-lock.json | 4 +- .../examples/extensions/sandbox/package.json | 2 +- .../extensions/with-deps/package-lock.json | 4 +- .../extensions/with-deps/package.json | 2 +- .../install-lock/package-lock.json | 46 +++++------ .../coding-agent/install-lock/package.json | 4 +- packages/coding-agent/npm-shrinkwrap.json | 40 +++++----- packages/coding-agent/package.json | 16 ++-- packages/durable/CHANGELOG.md | 2 +- packages/durable/package.json | 6 +- packages/evals/package.json | 6 +- packages/protocol/CHANGELOG.md | 2 +- packages/protocol/package.json | 4 +- packages/server/CHANGELOG.md | 2 +- packages/server/package.json | 8 +- .../session-backends/sqlite-node/CHANGELOG.md | 2 +- .../session-backends/sqlite-node/package.json | 6 +- packages/telemetry/CHANGELOG.md | 2 +- packages/telemetry/package.json | 2 +- packages/tui/CHANGELOG.md | 2 +- packages/tui/package.json | 2 +- 35 files changed, 143 insertions(+), 143 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7aa3cfc4c..7ba661ca1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5280,12 +5280,12 @@ }, "packages/agent": { "name": "@earendil-works/pi-agent-core", - "version": "0.86.1", + "version": "0.87.0", "license": "MIT", "dependencies": { - "@earendil-works/chord": "^0.86.1", - "@earendil-works/pi-ai": "^0.86.1", - "@earendil-works/pi-telemetry": "^0.86.1", + "@earendil-works/chord": "^0.87.0", + "@earendil-works/pi-ai": "^0.87.0", + "@earendil-works/pi-telemetry": "^0.87.0", "diff": "8.0.4", "ignore": "7.0.8", "typebox": "1.3.27", @@ -5310,12 +5310,12 @@ }, "packages/ai": { "name": "@earendil-works/pi-ai", - "version": "0.86.1", + "version": "0.87.0", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.124.0", "@aws-sdk/client-bedrock-runtime": "3.1127.0", - "@earendil-works/pi-telemetry": "^0.86.1", + "@earendil-works/pi-telemetry": "^0.87.0", "@google/genai": "2.21.0", "@smithy/node-http-handler": "4.12.1", "http-proxy-agent": "9.1.0", @@ -5361,7 +5361,7 @@ }, "packages/chord": { "name": "@earendil-works/chord", - "version": "0.86.1", + "version": "0.87.0", "license": "MIT", "dependencies": { "esbuild": "0.28.2" @@ -5376,11 +5376,11 @@ }, "packages/client": { "name": "@earendil-works/pi-client", - "version": "0.86.1", + "version": "0.87.0", "license": "MIT", "dependencies": { - "@earendil-works/chord": "^0.86.1", - "@earendil-works/pi-protocol": "^0.86.1" + "@earendil-works/chord": "^0.87.0", + "@earendil-works/pi-protocol": "^0.87.0" }, "devDependencies": { "shx": "0.4.0", @@ -5392,13 +5392,13 @@ }, "packages/coding-agent": { "name": "@earendil-works/pi-coding-agent", - "version": "0.86.1", + "version": "0.87.0", "license": "MIT", "dependencies": { - "@earendil-works/chord": "^0.86.1", - "@earendil-works/pi-agent-core": "^0.86.1", - "@earendil-works/pi-ai": "^0.86.1", - "@earendil-works/pi-tui": "^0.86.1", + "@earendil-works/chord": "^0.87.0", + "@earendil-works/pi-agent-core": "^0.87.0", + "@earendil-works/pi-ai": "^0.87.0", + "@earendil-works/pi-tui": "^0.87.0", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "6.0.0", "cross-spawn": "7.0.6", @@ -5419,9 +5419,9 @@ "pi": "dist/bundle/cli.js" }, "devDependencies": { - "@earendil-works/pi-client": "^0.86.1", - "@earendil-works/pi-protocol": "^0.86.1", - "@earendil-works/pi-server": "^0.86.1", + "@earendil-works/pi-client": "^0.87.0", + "@earendil-works/pi-protocol": "^0.87.0", + "@earendil-works/pi-server": "^0.87.0", "@types/cross-spawn": "6.0.6", "@types/hosted-git-info": "3.0.5", "@types/node": "22.19.19", @@ -5438,32 +5438,32 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "0.86.1", + "version": "0.87.0", "dependencies": { "@anthropic-ai/sdk": "0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "0.86.1" + "version": "0.87.0" }, "packages/coding-agent/examples/extensions/gondolin": { "name": "pi-extension-gondolin", - "version": "0.86.1", + "version": "0.87.0", "dependencies": { "@earendil-works/gondolin": "0.12.0" } }, "packages/coding-agent/examples/extensions/sandbox": { "name": "pi-extension-sandbox", - "version": "1.16.1", + "version": "1.17.0", "dependencies": { "@anthropic-ai/sandbox-runtime": "0.0.26" } }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "0.86.1", + "version": "0.87.0", "dependencies": { "ms": "2.1.3" }, @@ -5517,11 +5517,11 @@ }, "packages/durable": { "name": "@earendil-works/pi-durable", - "version": "0.86.1", + "version": "0.87.0", "license": "MIT", "dependencies": { - "@earendil-works/chord": "^0.86.1", - "@earendil-works/pi-ai": "^0.86.1" + "@earendil-works/chord": "^0.87.0", + "@earendil-works/pi-ai": "^0.87.0" }, "devDependencies": { "shx": "0.4.0", @@ -5533,10 +5533,10 @@ }, "packages/evals": { "name": "@earendil-works/pi-evals", - "version": "0.86.1", + "version": "0.87.0", "devDependencies": { - "@earendil-works/pi-ai": "^0.86.1", - "@earendil-works/pi-coding-agent": "^0.86.1", + "@earendil-works/pi-ai": "^0.87.0", + "@earendil-works/pi-coding-agent": "^0.87.0", "@types/node": "22.19.19", "@vitest-evals/core": "0.15.0", "autoevals": "0.3.0", @@ -5548,10 +5548,10 @@ }, "packages/protocol": { "name": "@earendil-works/pi-protocol", - "version": "0.86.1", + "version": "0.87.0", "license": "MIT", "dependencies": { - "@earendil-works/chord": "^0.86.1", + "@earendil-works/chord": "^0.87.0", "typebox": "1.3.27" }, "devDependencies": { @@ -5564,12 +5564,12 @@ }, "packages/server": { "name": "@earendil-works/pi-server", - "version": "0.86.1", + "version": "0.87.0", "license": "MIT", "dependencies": { - "@earendil-works/chord": "^0.86.1", - "@earendil-works/pi-agent-core": "^0.86.1", - "@earendil-works/pi-protocol": "^0.86.1" + "@earendil-works/chord": "^0.87.0", + "@earendil-works/pi-agent-core": "^0.87.0", + "@earendil-works/pi-protocol": "^0.87.0" }, "devDependencies": { "shx": "0.4.0", @@ -5581,11 +5581,11 @@ }, "packages/session-backends/sqlite-node": { "name": "@earendil-works/pi-session-backend-sqlite-node", - "version": "0.86.1", + "version": "0.87.0", "license": "MIT", "dependencies": { - "@earendil-works/pi-agent-core": "^0.86.1", - "@earendil-works/pi-ai": "^0.86.1" + "@earendil-works/pi-agent-core": "^0.87.0", + "@earendil-works/pi-ai": "^0.87.0" }, "devDependencies": { "@vitest/coverage-v8": "4.1.9", @@ -5597,7 +5597,7 @@ }, "packages/telemetry": { "name": "@earendil-works/pi-telemetry", - "version": "0.86.1", + "version": "0.87.0", "license": "MIT", "devDependencies": { "@types/node": "22.19.19", @@ -5609,7 +5609,7 @@ }, "packages/tui": { "name": "@earendil-works/pi-tui", - "version": "0.86.1", + "version": "0.87.0", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index bb3e89b62..230b7f615 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.87.0] - 2026-09-21 ### Breaking Changes diff --git a/packages/agent/package.json b/packages/agent/package.json index 032092ad8..371ad64c5 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-agent-core", - "version": "0.86.1", + "version": "0.87.0", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -59,9 +59,9 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@earendil-works/chord": "^0.86.1", - "@earendil-works/pi-ai": "^0.86.1", - "@earendil-works/pi-telemetry": "^0.86.1", + "@earendil-works/chord": "^0.87.0", + "@earendil-works/pi-ai": "^0.87.0", + "@earendil-works/pi-telemetry": "^0.87.0", "diff": "8.0.4", "ignore": "7.0.8", "typebox": "1.3.27", diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 1f96bd87e..d3d48250c 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.87.0] - 2026-09-21 ### Added diff --git a/packages/ai/package.json b/packages/ai/package.json index 111739b95..856f01197 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-ai", - "version": "0.86.1", + "version": "0.87.0", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", @@ -66,7 +66,7 @@ "dependencies": { "@anthropic-ai/sdk": "0.124.0", "@aws-sdk/client-bedrock-runtime": "3.1127.0", - "@earendil-works/pi-telemetry": "^0.86.1", + "@earendil-works/pi-telemetry": "^0.87.0", "@google/genai": "2.21.0", "@smithy/node-http-handler": "4.12.1", "http-proxy-agent": "9.1.0", diff --git a/packages/chord/package.json b/packages/chord/package.json index f7509c375..fa50e17e4 100644 --- a/packages/chord/package.json +++ b/packages/chord/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/chord", - "version": "0.86.1", + "version": "0.87.0", "description": "Application composition runtime for services, replicated state, RPC, and plugins", "type": "module", "main": "./dist/index.js", diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md index 86eb37b82..ea779016f 100644 --- a/packages/client/CHANGELOG.md +++ b/packages/client/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.87.0] - 2026-09-21 ## [0.86.1] - 2026-09-20 diff --git a/packages/client/package.json b/packages/client/package.json index d313018d0..673c364b8 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-client", - "version": "0.86.1", + "version": "0.87.0", "description": "Transport-neutral client for remote pi sessions over framed CBOR bytes", "type": "module", "main": "./dist/index.js", @@ -47,8 +47,8 @@ "node": ">=22.19.0" }, "dependencies": { - "@earendil-works/chord": "^0.86.1", - "@earendil-works/pi-protocol": "^0.86.1" + "@earendil-works/chord": "^0.87.0", + "@earendil-works/pi-protocol": "^0.87.0" }, "devDependencies": { "shx": "0.4.0", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d4ff84b9e..649879b67 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.87.0] - 2026-09-21 ### New Features diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index 3eba4a83e..00832cb38 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "0.86.1", + "version": "0.87.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "0.86.1", + "version": "0.87.0", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index c0a973be7..5ade8ee7f 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "0.86.1", + "version": "0.87.0", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index 3c4339b34..ef4c42601 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "0.86.1", + "version": "0.87.0", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/gondolin/package-lock.json b/packages/coding-agent/examples/extensions/gondolin/package-lock.json index b71238fc2..5d7f6addf 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package-lock.json +++ b/packages/coding-agent/examples/extensions/gondolin/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-gondolin", - "version": "0.86.1", + "version": "0.87.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-gondolin", - "version": "0.86.1", + "version": "0.87.0", "dependencies": { "@earendil-works/gondolin": "0.12.0" } diff --git a/packages/coding-agent/examples/extensions/gondolin/package.json b/packages/coding-agent/examples/extensions/gondolin/package.json index df48e2aff..d692f0f8a 100644 --- a/packages/coding-agent/examples/extensions/gondolin/package.json +++ b/packages/coding-agent/examples/extensions/gondolin/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-gondolin", "private": true, - "version": "0.86.1", + "version": "0.87.0", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/sandbox/package-lock.json b/packages/coding-agent/examples/extensions/sandbox/package-lock.json index bf3e01022..ac33d7af1 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package-lock.json +++ b/packages/coding-agent/examples/extensions/sandbox/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-sandbox", - "version": "1.16.1", + "version": "1.17.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-sandbox", - "version": "1.16.1", + "version": "1.17.0", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.26" } diff --git a/packages/coding-agent/examples/extensions/sandbox/package.json b/packages/coding-agent/examples/extensions/sandbox/package.json index 8847140cc..852d97a44 100644 --- a/packages/coding-agent/examples/extensions/sandbox/package.json +++ b/packages/coding-agent/examples/extensions/sandbox/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-sandbox", "private": true, - "version": "1.16.1", + "version": "1.17.0", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index c4811dd38..b5d8f1bd2 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "0.86.1", + "version": "0.87.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "0.86.1", + "version": "0.87.0", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index 41f636fe1..0d9f944c6 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "0.86.1", + "version": "0.87.0", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/install-lock/package-lock.json b/packages/coding-agent/install-lock/package-lock.json index 5f4757662..aedadcb8a 100644 --- a/packages/coding-agent/install-lock/package-lock.json +++ b/packages/coding-agent/install-lock/package-lock.json @@ -1,14 +1,14 @@ { "name": "@earendil-works/pi-coding-agent-install", - "version": "0.86.1", + "version": "0.87.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent-install", - "version": "0.86.1", + "version": "0.87.0", "dependencies": { - "@earendil-works/pi-coding-agent": "0.86.1" + "@earendil-works/pi-coding-agent": "0.87.0" }, "engines": { "node": ">=22.19.0" @@ -386,8 +386,8 @@ } }, "node_modules/@earendil-works/chord": { - "version": "0.86.1", - "resolved": "https://registry.npmjs.org/@earendil-works/chord/-/chord-0.86.1.tgz", + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@earendil-works/chord/-/chord-0.87.0.tgz", "license": "MIT", "dependencies": { "esbuild": "0.28.2" @@ -397,13 +397,13 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.86.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.86.1.tgz", + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.87.0.tgz", "license": "MIT", "dependencies": { - "@earendil-works/chord": "^0.86.1", - "@earendil-works/pi-ai": "^0.86.1", - "@earendil-works/pi-telemetry": "^0.86.1", + "@earendil-works/chord": "^0.87.0", + "@earendil-works/pi-ai": "^0.87.0", + "@earendil-works/pi-telemetry": "^0.87.0", "diff": "8.0.4", "ignore": "7.0.8", "typebox": "1.3.27", @@ -414,13 +414,13 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.86.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.86.1.tgz", + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.87.0.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.124.0", "@aws-sdk/client-bedrock-runtime": "3.1127.0", - "@earendil-works/pi-telemetry": "^0.86.1", + "@earendil-works/pi-telemetry": "^0.87.0", "@google/genai": "2.21.0", "@smithy/node-http-handler": "4.12.1", "http-proxy-agent": "9.1.0", @@ -460,14 +460,14 @@ } }, "node_modules/@earendil-works/pi-coding-agent": { - "version": "0.86.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.86.1.tgz", + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.87.0.tgz", "license": "MIT", "dependencies": { - "@earendil-works/chord": "^0.86.1", - "@earendil-works/pi-agent-core": "^0.86.1", - "@earendil-works/pi-ai": "^0.86.1", - "@earendil-works/pi-tui": "^0.86.1", + "@earendil-works/chord": "^0.87.0", + "@earendil-works/pi-agent-core": "^0.87.0", + "@earendil-works/pi-ai": "^0.87.0", + "@earendil-works/pi-tui": "^0.87.0", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "6.0.0", "cross-spawn": "7.0.6", @@ -492,16 +492,16 @@ } }, "node_modules/@earendil-works/pi-telemetry": { - "version": "0.86.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.86.1.tgz", + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.87.0.tgz", "license": "MIT", "engines": { "node": ">=22.19.0" } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.86.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.86.1.tgz", + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.87.0.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/coding-agent/install-lock/package.json b/packages/coding-agent/install-lock/package.json index 810b8d2bf..6f2e26c58 100644 --- a/packages/coding-agent/install-lock/package.json +++ b/packages/coding-agent/install-lock/package.json @@ -1,10 +1,10 @@ { "name": "@earendil-works/pi-coding-agent-install", - "version": "0.86.1", + "version": "0.87.0", "private": true, "description": "Lockfile root used by the Pi installer and updater.", "dependencies": { - "@earendil-works/pi-coding-agent": "0.86.1" + "@earendil-works/pi-coding-agent": "0.87.0" }, "overrides": { "protobufjs": "7.6.6" diff --git a/packages/coding-agent/npm-shrinkwrap.json b/packages/coding-agent/npm-shrinkwrap.json index 4a82f9cd1..11743c285 100644 --- a/packages/coding-agent/npm-shrinkwrap.json +++ b/packages/coding-agent/npm-shrinkwrap.json @@ -1,18 +1,18 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.86.1", + "version": "0.87.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@earendil-works/pi-coding-agent", - "version": "0.86.1", + "version": "0.87.0", "license": "MIT", "dependencies": { - "@earendil-works/chord": "^0.86.1", - "@earendil-works/pi-agent-core": "^0.86.1", - "@earendil-works/pi-ai": "^0.86.1", - "@earendil-works/pi-tui": "^0.86.1", + "@earendil-works/chord": "^0.87.0", + "@earendil-works/pi-agent-core": "^0.87.0", + "@earendil-works/pi-ai": "^0.87.0", + "@earendil-works/pi-tui": "^0.87.0", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "6.0.0", "cross-spawn": "7.0.6", @@ -408,8 +408,8 @@ } }, "node_modules/@earendil-works/chord": { - "version": "0.86.1", - "resolved": "https://registry.npmjs.org/@earendil-works/chord/-/chord-0.86.1.tgz", + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@earendil-works/chord/-/chord-0.87.0.tgz", "license": "MIT", "dependencies": { "esbuild": "0.28.2" @@ -419,13 +419,13 @@ } }, "node_modules/@earendil-works/pi-agent-core": { - "version": "0.86.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.86.1.tgz", + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.87.0.tgz", "license": "MIT", "dependencies": { - "@earendil-works/chord": "^0.86.1", - "@earendil-works/pi-ai": "^0.86.1", - "@earendil-works/pi-telemetry": "^0.86.1", + "@earendil-works/chord": "^0.87.0", + "@earendil-works/pi-ai": "^0.87.0", + "@earendil-works/pi-telemetry": "^0.87.0", "diff": "8.0.4", "ignore": "7.0.8", "typebox": "1.3.27", @@ -436,13 +436,13 @@ } }, "node_modules/@earendil-works/pi-ai": { - "version": "0.86.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.86.1.tgz", + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.87.0.tgz", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "0.124.0", "@aws-sdk/client-bedrock-runtime": "3.1127.0", - "@earendil-works/pi-telemetry": "^0.86.1", + "@earendil-works/pi-telemetry": "^0.87.0", "@google/genai": "2.21.0", "@smithy/node-http-handler": "4.12.1", "http-proxy-agent": "9.1.0", @@ -482,16 +482,16 @@ } }, "node_modules/@earendil-works/pi-telemetry": { - "version": "0.86.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.86.1.tgz", + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.87.0.tgz", "license": "MIT", "engines": { "node": ">=22.19.0" } }, "node_modules/@earendil-works/pi-tui": { - "version": "0.86.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.86.1.tgz", + "version": "0.87.0", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.87.0.tgz", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index d1b6634ff..eae863020 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-coding-agent", - "version": "0.86.1", + "version": "0.87.0", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -49,10 +49,10 @@ "prepublishOnly": "npm run clean && npm run build && npm run shrinkwrap" }, "dependencies": { - "@earendil-works/chord": "^0.86.1", - "@earendil-works/pi-agent-core": "^0.86.1", - "@earendil-works/pi-ai": "^0.86.1", - "@earendil-works/pi-tui": "^0.86.1", + "@earendil-works/chord": "^0.87.0", + "@earendil-works/pi-agent-core": "^0.87.0", + "@earendil-works/pi-ai": "^0.87.0", + "@earendil-works/pi-tui": "^0.87.0", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "6.0.0", "cross-spawn": "7.0.6", @@ -73,9 +73,9 @@ "protobufjs": "7.6.6" }, "devDependencies": { - "@earendil-works/pi-client": "^0.86.1", - "@earendil-works/pi-protocol": "^0.86.1", - "@earendil-works/pi-server": "^0.86.1", + "@earendil-works/pi-client": "^0.87.0", + "@earendil-works/pi-protocol": "^0.87.0", + "@earendil-works/pi-server": "^0.87.0", "@types/cross-spawn": "6.0.6", "@types/hosted-git-info": "3.0.5", "@types/node": "22.19.19", diff --git a/packages/durable/CHANGELOG.md b/packages/durable/CHANGELOG.md index 39d2de378..aad23388d 100644 --- a/packages/durable/CHANGELOG.md +++ b/packages/durable/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.87.0] - 2026-09-21 ## [0.86.1] - 2026-09-20 diff --git a/packages/durable/package.json b/packages/durable/package.json index e85d868cd..c060d2608 100644 --- a/packages/durable/package.json +++ b/packages/durable/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-durable", - "version": "0.86.1", + "version": "0.87.0", "description": "Durable conversation, task, and document runtime for Pi", "type": "module", "main": "./dist/index.js", @@ -42,8 +42,8 @@ "node": ">=22.19.0" }, "dependencies": { - "@earendil-works/chord": "^0.86.1", - "@earendil-works/pi-ai": "^0.86.1" + "@earendil-works/chord": "^0.87.0", + "@earendil-works/pi-ai": "^0.87.0" }, "devDependencies": { "shx": "0.4.0", diff --git a/packages/evals/package.json b/packages/evals/package.json index b89d946d1..20183dc83 100644 --- a/packages/evals/package.json +++ b/packages/evals/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-evals", - "version": "0.86.1", + "version": "0.87.0", "private": true, "type": "module", "scripts": { @@ -12,8 +12,8 @@ }, "devDependencies": { "autoevals": "0.3.0", - "@earendil-works/pi-ai": "^0.86.1", - "@earendil-works/pi-coding-agent": "^0.86.1", + "@earendil-works/pi-ai": "^0.87.0", + "@earendil-works/pi-coding-agent": "^0.87.0", "@types/node": "22.19.19", "@vitest-evals/core": "0.15.0", "shx": "0.4.0", diff --git a/packages/protocol/CHANGELOG.md b/packages/protocol/CHANGELOG.md index 6147cd8f5..994cfabcb 100644 --- a/packages/protocol/CHANGELOG.md +++ b/packages/protocol/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.87.0] - 2026-09-21 ## [0.86.1] - 2026-09-20 diff --git a/packages/protocol/package.json b/packages/protocol/package.json index 602af9be5..883a0a1d2 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-protocol", - "version": "0.86.1", + "version": "0.87.0", "description": "Transport-neutral CBOR protocol for remote pi sessions", "type": "module", "main": "./dist/index.js", @@ -39,7 +39,7 @@ "node": ">=22.19.0" }, "dependencies": { - "@earendil-works/chord": "^0.86.1", + "@earendil-works/chord": "^0.87.0", "typebox": "1.3.27" }, "devDependencies": { diff --git a/packages/server/CHANGELOG.md b/packages/server/CHANGELOG.md index 38e73caf1..a183b63fe 100644 --- a/packages/server/CHANGELOG.md +++ b/packages/server/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.87.0] - 2026-09-21 ## [0.86.1] - 2026-09-20 diff --git a/packages/server/package.json b/packages/server/package.json index 30a2f9a8d..3829f33ad 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-server", - "version": "0.86.1", + "version": "0.87.0", "description": "experimental server package for pi", "type": "module", "main": "./dist/index.js", @@ -47,9 +47,9 @@ "node": ">=22.19.0" }, "dependencies": { - "@earendil-works/chord": "^0.86.1", - "@earendil-works/pi-agent-core": "^0.86.1", - "@earendil-works/pi-protocol": "^0.86.1" + "@earendil-works/chord": "^0.87.0", + "@earendil-works/pi-agent-core": "^0.87.0", + "@earendil-works/pi-protocol": "^0.87.0" }, "devDependencies": { "shx": "0.4.0", diff --git a/packages/session-backends/sqlite-node/CHANGELOG.md b/packages/session-backends/sqlite-node/CHANGELOG.md index d915d6703..cf8c7de75 100644 --- a/packages/session-backends/sqlite-node/CHANGELOG.md +++ b/packages/session-backends/sqlite-node/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.87.0] - 2026-09-21 ## [0.86.1] - 2026-09-20 diff --git a/packages/session-backends/sqlite-node/package.json b/packages/session-backends/sqlite-node/package.json index e080c830b..f825a38eb 100644 --- a/packages/session-backends/sqlite-node/package.json +++ b/packages/session-backends/sqlite-node/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-session-backend-sqlite-node", - "version": "0.86.1", + "version": "0.87.0", "description": "Node sqlite session backend for @earendil-works/pi-agent-core sessions", "type": "module", "main": "./dist/index.js", @@ -35,8 +35,8 @@ "node": ">=22.19.0" }, "dependencies": { - "@earendil-works/pi-ai": "^0.86.1", - "@earendil-works/pi-agent-core": "^0.86.1" + "@earendil-works/pi-ai": "^0.87.0", + "@earendil-works/pi-agent-core": "^0.87.0" }, "devDependencies": { "@vitest/coverage-v8": "4.1.9", diff --git a/packages/telemetry/CHANGELOG.md b/packages/telemetry/CHANGELOG.md index 11c036040..ca581d9ac 100644 --- a/packages/telemetry/CHANGELOG.md +++ b/packages/telemetry/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.87.0] - 2026-09-21 ## [0.86.1] - 2026-09-20 diff --git a/packages/telemetry/package.json b/packages/telemetry/package.json index de9ea1aa2..99aba7649 100644 --- a/packages/telemetry/package.json +++ b/packages/telemetry/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-telemetry", - "version": "0.86.1", + "version": "0.87.0", "description": "Vendor-neutral telemetry contracts and typed schema utilities for pi", "type": "module", "main": "./dist/index.js", diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 26e29a42e..803ded190 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [Unreleased] +## [0.87.0] - 2026-09-21 ## [0.86.1] - 2026-09-20 diff --git a/packages/tui/package.json b/packages/tui/package.json index 35d12c859..3b476a509 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@earendil-works/pi-tui", - "version": "0.86.1", + "version": "0.87.0", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", From 4c8eb393c73220c742e75745df210335aedc020e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 21 Sep 2026 18:31:58 +0200 Subject: [PATCH 06/10] Add [Unreleased] section for next cycle --- packages/agent/CHANGELOG.md | 2 ++ packages/ai/CHANGELOG.md | 2 ++ packages/client/CHANGELOG.md | 2 ++ packages/coding-agent/CHANGELOG.md | 2 ++ packages/durable/CHANGELOG.md | 2 ++ packages/protocol/CHANGELOG.md | 2 ++ packages/server/CHANGELOG.md | 2 ++ packages/session-backends/sqlite-node/CHANGELOG.md | 2 ++ packages/telemetry/CHANGELOG.md | 2 ++ packages/tui/CHANGELOG.md | 2 ++ 10 files changed, 20 insertions(+) diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 230b7f615..4393b17cd 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.87.0] - 2026-09-21 ### Breaking Changes diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index d3d48250c..0f85f4d26 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.87.0] - 2026-09-21 ### Added diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md index ea779016f..3c6e239b0 100644 --- a/packages/client/CHANGELOG.md +++ b/packages/client/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.87.0] - 2026-09-21 ## [0.86.1] - 2026-09-20 diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 649879b67..7994facf3 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.87.0] - 2026-09-21 ### New Features diff --git a/packages/durable/CHANGELOG.md b/packages/durable/CHANGELOG.md index aad23388d..8717705e3 100644 --- a/packages/durable/CHANGELOG.md +++ b/packages/durable/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.87.0] - 2026-09-21 ## [0.86.1] - 2026-09-20 diff --git a/packages/protocol/CHANGELOG.md b/packages/protocol/CHANGELOG.md index 994cfabcb..fe62bde3d 100644 --- a/packages/protocol/CHANGELOG.md +++ b/packages/protocol/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.87.0] - 2026-09-21 ## [0.86.1] - 2026-09-20 diff --git a/packages/server/CHANGELOG.md b/packages/server/CHANGELOG.md index a183b63fe..bda8d300f 100644 --- a/packages/server/CHANGELOG.md +++ b/packages/server/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.87.0] - 2026-09-21 ## [0.86.1] - 2026-09-20 diff --git a/packages/session-backends/sqlite-node/CHANGELOG.md b/packages/session-backends/sqlite-node/CHANGELOG.md index cf8c7de75..5ab3077bf 100644 --- a/packages/session-backends/sqlite-node/CHANGELOG.md +++ b/packages/session-backends/sqlite-node/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.87.0] - 2026-09-21 ## [0.86.1] - 2026-09-20 diff --git a/packages/telemetry/CHANGELOG.md b/packages/telemetry/CHANGELOG.md index ca581d9ac..b1af6f4b2 100644 --- a/packages/telemetry/CHANGELOG.md +++ b/packages/telemetry/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.87.0] - 2026-09-21 ## [0.86.1] - 2026-09-20 diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 803ded190..9da53a3a1 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +## [Unreleased] + ## [0.87.0] - 2026-09-21 ## [0.86.1] - 2026-09-20 From 1b6ddca87ca041e3b02b387d5a321eb77fc39eca Mon Sep 17 00:00:00 2001 From: David Brailovsky Date: Mon, 21 Sep 2026 21:48:11 +0200 Subject: [PATCH 07/10] fix(ai): omit empty text parts from multimodal user messages Prevent OpenAI-compatible providers from rejecting image-only prompts. Fixes #9797 --- packages/ai/CHANGELOG.md | 4 +++ packages/ai/src/api/openai-completions.ts | 32 ++++++++++--------- ...nai-completions-tool-result-images.test.ts | 29 +++++++++++++++++ 3 files changed, 50 insertions(+), 15 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 0f85f4d26..94240ff85 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed image-only user messages being rejected by some OpenAI-compatible providers because they included an empty text part ([#9797](https://github.com/earendil-works/pi/issues/9797)) + ## [0.87.0] - 2026-09-21 ### Added diff --git a/packages/ai/src/api/openai-completions.ts b/packages/ai/src/api/openai-completions.ts index 4ac0b6206..f5a1bc668 100644 --- a/packages/ai/src/api/openai-completions.ts +++ b/packages/ai/src/api/openai-completions.ts @@ -1257,21 +1257,23 @@ export function convertMessages( content: sanitizeSurrogates(msg.content), }); } else { - const content: ChatCompletionContentPart[] = msg.content.map((item): ChatCompletionContentPart => { - if (item.type === "text") { - return { - type: "text", - text: sanitizeSurrogates(item.text), - } satisfies ChatCompletionContentPartText; - } else { - return { - type: "image_url", - image_url: { - url: `data:${item.mimeType};base64,${item.data}`, - }, - } satisfies ChatCompletionContentPartImage; - } - }); + const content: ChatCompletionContentPart[] = msg.content + .filter((item) => item.type !== "text" || item.text.length > 0) + .map((item): ChatCompletionContentPart => { + if (item.type === "text") { + return { + type: "text", + text: sanitizeSurrogates(item.text), + } satisfies ChatCompletionContentPartText; + } else { + return { + type: "image_url", + image_url: { + url: `data:${item.mimeType};base64,${item.data}`, + }, + } satisfies ChatCompletionContentPartImage; + } + }); if (content.length === 0) continue; params.push({ role: "user", diff --git a/packages/ai/test/openai-completions-tool-result-images.test.ts b/packages/ai/test/openai-completions-tool-result-images.test.ts index 6e48efe67..4d303865e 100644 --- a/packages/ai/test/openai-completions-tool-result-images.test.ts +++ b/packages/ai/test/openai-completions-tool-result-images.test.ts @@ -69,6 +69,35 @@ function buildEmptyToolResult(toolCallId: string, timestamp: number): ToolResult } describe("openai-completions convertMessages", () => { + // Regression test for https://github.com/earendil-works/pi/issues/9797 + it("omits empty text parts from user messages with images", () => { + const { compat: _compat, ...baseModel } = getModel("openai", "gpt-4o-mini"); + const model: Model<"openai-completions"> = { + ...baseModel, + api: "openai-completions", + input: ["text", "image"], + }; + const context = normalizeContext({ + messages: [ + { + role: "user", + content: [ + { type: "text", text: "" }, + { type: "image", data: "ZmFrZQ==", mimeType: "image/png" }, + ], + timestamp: Date.now(), + }, + ], + }); + + expect(convertMessages(model, context, compat)).toEqual([ + { + role: "user", + content: [{ type: "image_url", image_url: { url: "data:image/png;base64,ZmFrZQ==" } }], + }, + ]); + }); + it("batches tool-result images after consecutive tool results", () => { const { compat: _compat, ...baseModel } = getModel("openai", "gpt-4o-mini"); const model: Model<"openai-completions"> = { From 1a584a7a56eb5e7b4ff8ccbd46430f1533282eed Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 22 Sep 2026 00:07:34 +0200 Subject: [PATCH 08/10] feat(ai,coding-agent): add Grok 4.7 support --- packages/ai/CHANGELOG.md | 4 +++ packages/ai/README.md | 2 +- packages/ai/scripts/generate-models.ts | 7 +--- packages/ai/test/model-catalog-types.test.ts | 2 ++ packages/ai/test/stream.test.ts | 4 +-- packages/ai/test/xai-responses.test.ts | 32 +++++++++++++++++-- packages/coding-agent/CHANGELOG.md | 4 +++ .../coding-agent/src/core/model-resolver.ts | 2 +- .../coding-agent/test/model-resolver.test.ts | 2 +- 9 files changed, 45 insertions(+), 14 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 94240ff85..4e1b21ead 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added Grok 4.7 to the built-in xAI model catalog with long-context pricing metadata. + ### Fixed - Fixed image-only user messages being rejected by some OpenAI-compatible providers because they included an empty text part ([#9797](https://github.com/earendil-works/pi/issues/9797)) diff --git a/packages/ai/README.md b/packages/ai/README.md index 34237bd2b..7395672ff 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -833,7 +833,7 @@ Many models support thinking/reasoning capabilities where they can show their in const model = models.getModel('anthropic', 'claude-sonnet-4-5')!; // or models.getModel('openai', 'gpt-5-mini'); // or models.getModel('google', 'gemini-2.5-flash'); -// or models.getModel('xai', 'grok-4.6'); +// or models.getModel('xai', 'grok-4.7'); // Check if model supports reasoning if (model.reasoning) { diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index ba4a49373..7b54f34d3 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -2030,12 +2030,7 @@ async function loadModelsDevData(): Promise[]> { compat: { ...XAI_RESPONSES_COMPAT }, reasoning: m.reasoning === true, input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"], - cost: { - input: m.cost?.input || 0, - output: m.cost?.output || 0, - cacheRead: m.cost?.cache_read || 0, - cacheWrite: m.cost?.cache_write || 0, - }, + cost: getModelsDevCost(m.cost), contextWindow: m.limit?.context || 4096, maxTokens: m.limit?.output || 4096, }); diff --git a/packages/ai/test/model-catalog-types.test.ts b/packages/ai/test/model-catalog-types.test.ts index 57f02e0f2..449764a93 100644 --- a/packages/ai/test/model-catalog-types.test.ts +++ b/packages/ai/test/model-catalog-types.test.ts @@ -8,6 +8,8 @@ it("derives model API, ID, and provider literals from grouped model data", () => expectTypeOf(XAI_MODELS["grok-4.5"].provider).toEqualTypeOf<"xai">(); expectTypeOf(XAI_MODELS["grok-4.6"].api).toEqualTypeOf<"openai-responses">(); expectTypeOf(XAI_MODELS["grok-4.6"].id).toEqualTypeOf<"grok-4.6">(); + expectTypeOf(XAI_MODELS["grok-4.7"].api).toEqualTypeOf<"openai-responses">(); + expectTypeOf(XAI_MODELS["grok-4.7"].id).toEqualTypeOf<"grok-4.7">(); expectTypeOf(XAI_MODELS["grok-4.3"].api).toEqualTypeOf<"openai-responses">(); }); diff --git a/packages/ai/test/stream.test.ts b/packages/ai/test/stream.test.ts index 362f30143..eb9afa226 100644 --- a/packages/ai/test/stream.test.ts +++ b/packages/ai/test/stream.test.ts @@ -540,8 +540,8 @@ describe("Generate E2E Tests", () => { }); }); - describe.skipIf(!process.env.XAI_API_KEY)("xAI Provider (grok-4.3 via OpenAI Responses)", () => { - const llm = getModel("xai", "grok-4.3"); + describe.skipIf(!process.env.XAI_API_KEY)("xAI Provider (grok-4.7 via OpenAI Responses)", () => { + const llm = getModel("xai", "grok-4.7"); it("should complete basic text generation", { retry: 3 }, async () => { await basicTextGeneration(llm); diff --git a/packages/ai/test/xai-responses.test.ts b/packages/ai/test/xai-responses.test.ts index d88c35a4d..1aa570177 100644 --- a/packages/ai/test/xai-responses.test.ts +++ b/packages/ai/test/xai-responses.test.ts @@ -132,9 +132,35 @@ describe("xAI Responses provider", () => { } expect(getSupportedThinkingLevels(XAI_MODELS["grok-4.5"])).toEqual(["low", "medium", "high"]); expect(getSupportedThinkingLevels(XAI_MODELS["grok-4.6"])).toEqual(["low", "medium", "high", "xhigh"]); + expect(getSupportedThinkingLevels(XAI_MODELS["grok-4.7"])).toEqual(["low", "medium", "high", "xhigh"]); expect(getSupportedThinkingLevels(XAI_MODELS["grok-4.3"])).toEqual(["off", "low", "medium", "high"]); }); + it("includes Grok 4.7 capabilities and long-context pricing", () => { + expect(XAI_MODELS["grok-4.7"]).toMatchObject({ + api: "openai-responses", + reasoning: true, + input: ["text", "image"], + contextWindow: 500000, + maxTokens: 500000, + cost: { + input: 2, + output: 6, + cacheRead: 0.5, + cacheWrite: 0, + tiers: [ + { + inputTokensAbove: 200000, + input: 4, + output: 12, + cacheRead: 1, + cacheWrite: 0, + }, + ], + }, + }); + }); + it("uses /responses with bearer auth and xAI-compatible request fields", async () => { const captured = await captureRequest( XAI_MODELS["grok-4.5"], @@ -188,9 +214,9 @@ describe("xAI Responses provider", () => { expect(captured.body).not.toHaveProperty("reasoning"); }); - it("uses /responses for Grok 4.6 with xhigh effort and encrypted reasoning", async () => { + it("uses /responses for Grok 4.7 with xhigh effort and encrypted reasoning", async () => { const captured = await captureRequest( - XAI_MODELS["grok-4.6"], + XAI_MODELS["grok-4.7"], { systemPrompt: "You are a careful coding assistant.", messages: [{ role: "user", content: "hello", timestamp: 1 }], @@ -203,7 +229,7 @@ describe("xAI Responses provider", () => { expect(captured.url).toBe("https://api.x.ai/v1/responses"); expect(captured.body).toMatchObject({ - model: "grok-4.6", + model: "grok-4.7", store: false, stream: true, reasoning: { effort: "xhigh" }, diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 7994facf3..978fa5523 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Changed + +- Changed the default xAI model to Grok 4.7. + ## [0.87.0] - 2026-09-21 ### New Features diff --git a/packages/coding-agent/src/core/model-resolver.ts b/packages/coding-agent/src/core/model-resolver.ts index 64aa93608..44f22ad8f 100644 --- a/packages/coding-agent/src/core/model-resolver.ts +++ b/packages/coding-agent/src/core/model-resolver.ts @@ -32,7 +32,7 @@ export const defaultModelPerProvider: Record = { "github-copilot": "gpt-5.4", openrouter: "moonshotai/kimi-k2.6", "vercel-ai-gateway": "zai/glm-5.1", - xai: "grok-4.6", + xai: "grok-4.7", groq: "openai/gpt-oss-120b", cerebras: "gpt-oss-120b", zai: "glm-5.3", diff --git a/packages/coding-agent/test/model-resolver.test.ts b/packages/coding-agent/test/model-resolver.test.ts index 8f37b7054..ebed94c33 100644 --- a/packages/coding-agent/test/model-resolver.test.ts +++ b/packages/coding-agent/test/model-resolver.test.ts @@ -735,7 +735,7 @@ describe("default model selection", () => { }); test("xai default tracks current model", () => { - expect(defaultModelPerProvider.xai).toBe("grok-4.6"); + expect(defaultModelPerProvider.xai).toBe("grok-4.7"); }); test("qwen token plan individual default tracks current model", () => { From e40126f578ccc0a4a21f8d30cd8e96c8ecfe1722 Mon Sep 17 00:00:00 2001 From: Ramiz Wachtler Date: Tue, 22 Sep 2026 08:29:57 +0200 Subject: [PATCH 09/10] fix(coding-agent): reject invalid --mode values fixes #9045 --- packages/coding-agent/src/cli/args.ts | 18 +++++++-- packages/coding-agent/test/args.test.ts | 52 ++++++++++++++++++++----- 2 files changed, 56 insertions(+), 14 deletions(-) diff --git a/packages/coding-agent/src/cli/args.ts b/packages/coding-agent/src/cli/args.ts index 284888d13..a35879807 100644 --- a/packages/coding-agent/src/cli/args.ts +++ b/packages/coding-agent/src/cli/args.ts @@ -92,11 +92,21 @@ export function parseArgs(args: string[]): Args { result.help = true; } else if (arg === "--version" || arg === "-v") { result.version = true; - } else if (arg === "--mode" && i + 1 < args.length) { - const mode = args[++i]; - if (mode === "text" || mode === "json" || mode === "rpc") { - result.mode = mode; + } else if (arg === "--mode") { + const mode = args[i + 1]; + if (mode === undefined || mode.startsWith("-")) { + result.diagnostics.push({ type: "error", message: "--mode requires text, json, or rpc" }); + continue; } + i++; + if (mode !== "text" && mode !== "json" && mode !== "rpc") { + result.diagnostics.push({ + type: "error", + message: `Invalid mode "${mode}". Valid values: text, json, rpc`, + }); + continue; + } + result.mode = mode; } else if (arg === "--continue" || arg === "-c") { result.continue = true; } else if (arg === "--resume" || arg === "-r") { diff --git a/packages/coding-agent/test/args.test.ts b/packages/coding-agent/test/args.test.ts index 710ed7b09..37e6407d5 100644 --- a/packages/coding-agent/test/args.test.ts +++ b/packages/coding-agent/test/args.test.ts @@ -115,16 +115,6 @@ describe("parseArgs", () => { expect(result.appendSystemPrompt).toEqual(["Context A", "Context B"]); }); - test("parses --mode", () => { - const result = parseArgs(["--mode", "json"]); - expect(result.mode).toBe("json"); - }); - - test("parses --mode rpc", () => { - const result = parseArgs(["--mode", "rpc"]); - expect(result.mode).toBe("rpc"); - }); - test("parses --session", () => { const result = parseArgs(["--session", "/path/to/session.jsonl"]); expect(result.session).toBe("/path/to/session.jsonl"); @@ -157,6 +147,48 @@ describe("parseArgs", () => { }); }); + // Issue #9045 + describe("--mode flag", () => { + test.each(["text", "json", "rpc"] as const)("parses --mode %s", (mode) => { + const result = parseArgs(["--mode", mode]); + expect(result.mode).toBe(mode); + expect(result.diagnostics).toEqual([]); + }); + + test.each(["yaml", ""])("rejects invalid --mode value %j", (mode) => { + const result = parseArgs(["--mode", mode, "--version"]); + expect(result.mode).toBeUndefined(); + expect(result.version).toBe(true); + expect(result.messages).toEqual([]); + expect(result.unknownFlags.size).toBe(0); + expect(result.diagnostics).toEqual([ + { type: "error", message: `Invalid mode "${mode}". Valid values: text, json, rpc` }, + ]); + }); + + test("reports a missing --mode value", () => { + const result = parseArgs(["--mode"]); + expect(result.mode).toBeUndefined(); + expect(result.unknownFlags.size).toBe(0); + expect(result.diagnostics).toEqual([{ type: "error", message: "--mode requires text, json, or rpc" }]); + }); + + test("does not consume another option as a --mode value", () => { + const result = parseArgs(["--mode", "--version"]); + expect(result.mode).toBeUndefined(); + expect(result.version).toBe(true); + expect(result.unknownFlags.size).toBe(0); + expect(result.diagnostics).toEqual([{ type: "error", message: "--mode requires text, json, or rpc" }]); + }); + + test("reports an invalid --mode value after a valid one", () => { + const result = parseArgs(["--mode", "json", "--mode", "yaml"]); + expect(result.diagnostics).toEqual([ + { type: "error", message: 'Invalid mode "yaml". Valid values: text, json, rpc' }, + ]); + }); + }); + describe("--name flag", () => { test("parses --name flag with value", () => { const result = parseArgs(["--name", "my-session"]); From 95fbc04997eaee961eb673fa7923e9220609ebd5 Mon Sep 17 00:00:00 2001 From: Ramiz Wachtler Date: Tue, 22 Sep 2026 08:40:15 +0200 Subject: [PATCH 10/10] docs(coding-agent): update changelog with --mode validation fix (#9877) --- packages/coding-agent/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 978fa5523..cda0b2c65 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -6,6 +6,10 @@ - Changed the default xAI model to Grok 4.7. +### Fixed + +- Fixed missing or invalid `--mode` values being silently ignored instead of reporting an error and exiting with a nonzero status ([#9045](https://github.com/earendil-works/pi/issues/9045)). + ## [0.87.0] - 2026-09-21 ### New Features