@oh-my-pi/pi-coding-agent 15.5.8 → 15.5.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.5.10] - 2026-05-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `/drop-images` slash command that strips every `ImageContent` block from the current session's branch — `user`/`developer`/`custom`/`hookMessage`/`toolResult` content arrays plus `toolResult.details.images` and `fileMention.files[].image` — rewrites the session JSONL, rebuilds the agent's in-memory message list, tears down Codex Responses provider sessions, and rebuilds the TUI chat container so the change is visible immediately. ACP clients receive the same handler (returns `"Dropped N images …"` / `"No images found …"` through `runtime.output`). Stripping content that would leave a `toolResult` or `user` message with zero blocks inserts a single `[image removed]` placeholder so providers do not reject empty content arrays.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed compaction surfacing raw HTTP 401/403 envelopes (e.g. `Compaction failed: 401 {"type":"error","error":{"type":"authentication_error",…}}`) instead of routing to an authenticated fallback model. The compaction layer now attaches the provider-reported HTTP status onto the thrown error, and `AgentSession`'s auth-failure detector branches on `error.status === 401 || 403` in addition to the existing `auth_unavailable` regex. When a fallback model role (e.g. `modelRoles.smol`) is configured, compaction retries it transparently; otherwise the user sees the actionable "Compaction requires usable credentials for …" hint instead of the raw provider envelope.
|
|
14
|
+
|
|
5
15
|
## [15.5.8] - 2026-05-28
|
|
6
16
|
|
|
7
17
|
### Breaking Changes
|
|
@@ -667,6 +667,21 @@ export declare class AgentSession {
|
|
|
667
667
|
* Saves to settings.
|
|
668
668
|
*/
|
|
669
669
|
setInterruptMode(mode: "immediate" | "wait"): void;
|
|
670
|
+
/**
|
|
671
|
+
* Strip image content blocks from every message on the current branch and
|
|
672
|
+
* persist the rewrite. Walks `SessionManager.getBranch()` in place — both
|
|
673
|
+
* `SessionMessageEntry.message` and `CustomMessageEntry.content` arrays
|
|
674
|
+
* are mutated, then `rewriteEntries` durably commits the new shape. The
|
|
675
|
+
* agent's runtime view is rebuilt from the freshly-mutated entries so any
|
|
676
|
+
* provider sessions caching message identity (Codex Responses) are torn
|
|
677
|
+
* down to force a clean replay on the next turn.
|
|
678
|
+
*
|
|
679
|
+
* No-op when the branch carries no images; returns `{ removed: 0 }` and
|
|
680
|
+
* skips the disk rewrite.
|
|
681
|
+
*/
|
|
682
|
+
dropImages(): Promise<{
|
|
683
|
+
removed: number;
|
|
684
|
+
}>;
|
|
670
685
|
/**
|
|
671
686
|
* Manually compact the session context.
|
|
672
687
|
* Aborts current agent operation first.
|
|
@@ -55,6 +55,16 @@ export declare const INTERNAL_DETAILS_FIELDS: readonly ["__pendingDisplayTag"];
|
|
|
55
55
|
* (null/non-object, or no listed fields present) so callers don't pay a
|
|
56
56
|
* clone cost on the common path. */
|
|
57
57
|
export declare function stripInternalDetailsFields<T>(details: T | undefined): T | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Strip image content blocks from `message` in place. Returns the count of
|
|
60
|
+
* images removed across `content` (every role that carries `ImageContent`) and
|
|
61
|
+
* any tool-result `details.images` payload. Callers MUST rewrite session
|
|
62
|
+
* entries (`SessionManager.rewriteEntries`) and replay them through
|
|
63
|
+
* `Agent.replaceMessages` afterwards so persisted state and provider-side
|
|
64
|
+
* caches stay aligned with the mutated tree — `stripImagesFromMessage` is a
|
|
65
|
+
* pure local mutation and intentionally does neither.
|
|
66
|
+
*/
|
|
67
|
+
export declare function stripImagesFromMessage(message: AgentMessage): number;
|
|
58
68
|
/**
|
|
59
69
|
* Message type for bash executions via the ! command.
|
|
60
70
|
*/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "15.5.
|
|
4
|
+
"version": "15.5.10",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -47,13 +47,13 @@
|
|
|
47
47
|
"@agentclientprotocol/sdk": "0.21.0",
|
|
48
48
|
"@babel/parser": "^7.29.3",
|
|
49
49
|
"@mozilla/readability": "^0.6.0",
|
|
50
|
-
"@oh-my-pi/hashline": "15.5.
|
|
51
|
-
"@oh-my-pi/omp-stats": "15.5.
|
|
52
|
-
"@oh-my-pi/pi-agent-core": "15.5.
|
|
53
|
-
"@oh-my-pi/pi-ai": "15.5.
|
|
54
|
-
"@oh-my-pi/pi-natives": "15.5.
|
|
55
|
-
"@oh-my-pi/pi-tui": "15.5.
|
|
56
|
-
"@oh-my-pi/pi-utils": "15.5.
|
|
50
|
+
"@oh-my-pi/hashline": "15.5.10",
|
|
51
|
+
"@oh-my-pi/omp-stats": "15.5.10",
|
|
52
|
+
"@oh-my-pi/pi-agent-core": "15.5.10",
|
|
53
|
+
"@oh-my-pi/pi-ai": "15.5.10",
|
|
54
|
+
"@oh-my-pi/pi-natives": "15.5.10",
|
|
55
|
+
"@oh-my-pi/pi-tui": "15.5.10",
|
|
56
|
+
"@oh-my-pi/pi-utils": "15.5.10",
|
|
57
57
|
"@puppeteer/browsers": "^2.13.0",
|
|
58
58
|
"@types/turndown": "5.0.6",
|
|
59
59
|
"@xterm/headless": "^6.0.0",
|
|
@@ -196,6 +196,7 @@ import {
|
|
|
196
196
|
type PythonExecutionMessage,
|
|
197
197
|
readPendingDisplayTag,
|
|
198
198
|
SILENT_ABORT_MARKER,
|
|
199
|
+
stripImagesFromMessage,
|
|
199
200
|
} from "./messages";
|
|
200
201
|
import { formatSessionDumpText } from "./session-dump-format";
|
|
201
202
|
import type {
|
|
@@ -5310,6 +5311,55 @@ export class AgentSession {
|
|
|
5310
5311
|
return result;
|
|
5311
5312
|
}
|
|
5312
5313
|
|
|
5314
|
+
/**
|
|
5315
|
+
* Strip image content blocks from every message on the current branch and
|
|
5316
|
+
* persist the rewrite. Walks `SessionManager.getBranch()` in place — both
|
|
5317
|
+
* `SessionMessageEntry.message` and `CustomMessageEntry.content` arrays
|
|
5318
|
+
* are mutated, then `rewriteEntries` durably commits the new shape. The
|
|
5319
|
+
* agent's runtime view is rebuilt from the freshly-mutated entries so any
|
|
5320
|
+
* provider sessions caching message identity (Codex Responses) are torn
|
|
5321
|
+
* down to force a clean replay on the next turn.
|
|
5322
|
+
*
|
|
5323
|
+
* No-op when the branch carries no images; returns `{ removed: 0 }` and
|
|
5324
|
+
* skips the disk rewrite.
|
|
5325
|
+
*/
|
|
5326
|
+
async dropImages(): Promise<{ removed: number }> {
|
|
5327
|
+
const branchEntries = this.sessionManager.getBranch();
|
|
5328
|
+
let removed = 0;
|
|
5329
|
+
for (const entry of branchEntries) {
|
|
5330
|
+
if (entry.type === "message") {
|
|
5331
|
+
removed += stripImagesFromMessage(entry.message);
|
|
5332
|
+
continue;
|
|
5333
|
+
}
|
|
5334
|
+
if (entry.type === "custom_message" && typeof entry.content !== "string") {
|
|
5335
|
+
const kept: typeof entry.content = [];
|
|
5336
|
+
let dropped = 0;
|
|
5337
|
+
for (const part of entry.content) {
|
|
5338
|
+
if (part.type === "image") {
|
|
5339
|
+
dropped++;
|
|
5340
|
+
} else {
|
|
5341
|
+
kept.push(part);
|
|
5342
|
+
}
|
|
5343
|
+
}
|
|
5344
|
+
if (dropped > 0) {
|
|
5345
|
+
if (kept.length === 0) {
|
|
5346
|
+
kept.push({ type: "text", text: "[image removed]" });
|
|
5347
|
+
}
|
|
5348
|
+
entry.content = kept;
|
|
5349
|
+
removed += dropped;
|
|
5350
|
+
}
|
|
5351
|
+
}
|
|
5352
|
+
}
|
|
5353
|
+
if (removed === 0) {
|
|
5354
|
+
return { removed: 0 };
|
|
5355
|
+
}
|
|
5356
|
+
await this.sessionManager.rewriteEntries();
|
|
5357
|
+
const sessionContext = this.buildDisplaySessionContext();
|
|
5358
|
+
this.agent.replaceMessages(sessionContext.messages);
|
|
5359
|
+
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
5360
|
+
return { removed };
|
|
5361
|
+
}
|
|
5362
|
+
|
|
5313
5363
|
/**
|
|
5314
5364
|
* Manually compact the session context.
|
|
5315
5365
|
* Aborts current agent operation first.
|
|
@@ -6379,6 +6429,14 @@ export class AgentSession {
|
|
|
6379
6429
|
}
|
|
6380
6430
|
#isCompactionAuthFailure(error: unknown): boolean {
|
|
6381
6431
|
if (!(error instanceof Error)) return false;
|
|
6432
|
+
// Real provider 401/403 — surfaced as `.status` by the compaction layer
|
|
6433
|
+
// (see `createSummarizationError` in packages/agent/src/compaction/compaction.ts).
|
|
6434
|
+
// Without this branch, an expired/revoked Anthropic key would bypass the
|
|
6435
|
+
// authenticated-fallback path and dump the raw HTTP body into the UI.
|
|
6436
|
+
const status = (error as Error & { status?: number }).status;
|
|
6437
|
+
if (status === 401 || status === 403) return true;
|
|
6438
|
+
// pi-native gateway synthetic for "no credential configured" (issue #986).
|
|
6439
|
+
// Carries no HTTP status, so the legacy message regex stays.
|
|
6382
6440
|
return /auth_unavailable|no auth available/i.test(error.message);
|
|
6383
6441
|
}
|
|
6384
6442
|
|
package/src/session/messages.ts
CHANGED
|
@@ -114,6 +114,98 @@ function getPrunedToolResultContent(message: ToolResultMessage): (TextContent |
|
|
|
114
114
|
return [{ type: "text", text }];
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/** Result of filtering image blocks out of a `(TextContent | ImageContent)[]` array. */
|
|
118
|
+
interface StripContentResult {
|
|
119
|
+
content: (TextContent | ImageContent)[];
|
|
120
|
+
removed: number;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function stripImagesFromArrayContent(content: (TextContent | ImageContent)[]): StripContentResult {
|
|
124
|
+
let removed = 0;
|
|
125
|
+
const kept: (TextContent | ImageContent)[] = [];
|
|
126
|
+
for (const part of content) {
|
|
127
|
+
if (part.type === "image") {
|
|
128
|
+
removed++;
|
|
129
|
+
} else {
|
|
130
|
+
kept.push(part);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (removed === 0) {
|
|
134
|
+
return { content, removed };
|
|
135
|
+
}
|
|
136
|
+
// Avoid emitting an empty `content` array — providers reject zero-block user/tool
|
|
137
|
+
// messages and the LLM still needs to see *something* where the image used to be.
|
|
138
|
+
if (kept.length === 0) {
|
|
139
|
+
kept.push({ type: "text", text: "[image removed]" });
|
|
140
|
+
}
|
|
141
|
+
return { content: kept, removed };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Strip image content blocks from `message` in place. Returns the count of
|
|
146
|
+
* images removed across `content` (every role that carries `ImageContent`) and
|
|
147
|
+
* any tool-result `details.images` payload. Callers MUST rewrite session
|
|
148
|
+
* entries (`SessionManager.rewriteEntries`) and replay them through
|
|
149
|
+
* `Agent.replaceMessages` afterwards so persisted state and provider-side
|
|
150
|
+
* caches stay aligned with the mutated tree — `stripImagesFromMessage` is a
|
|
151
|
+
* pure local mutation and intentionally does neither.
|
|
152
|
+
*/
|
|
153
|
+
export function stripImagesFromMessage(message: AgentMessage): number {
|
|
154
|
+
switch (message.role) {
|
|
155
|
+
case "user":
|
|
156
|
+
case "developer":
|
|
157
|
+
case "custom":
|
|
158
|
+
case "hookMessage": {
|
|
159
|
+
if (typeof message.content === "string") return 0;
|
|
160
|
+
const { content, removed } = stripImagesFromArrayContent(message.content);
|
|
161
|
+
if (removed > 0) {
|
|
162
|
+
// All four roles type `content` as `string | (TextContent | ImageContent)[]`;
|
|
163
|
+
// TypeScript can't narrow the assignment across the union, so cast once.
|
|
164
|
+
(message as { content: typeof content }).content = content;
|
|
165
|
+
}
|
|
166
|
+
return removed;
|
|
167
|
+
}
|
|
168
|
+
case "toolResult": {
|
|
169
|
+
let removed = 0;
|
|
170
|
+
const { content, removed: contentRemoved } = stripImagesFromArrayContent(message.content);
|
|
171
|
+
if (contentRemoved > 0) {
|
|
172
|
+
message.content = content;
|
|
173
|
+
removed += contentRemoved;
|
|
174
|
+
}
|
|
175
|
+
const details = message.details as { images?: unknown } | null | undefined;
|
|
176
|
+
if (details && Array.isArray(details.images)) {
|
|
177
|
+
const original = details.images as unknown[];
|
|
178
|
+
const kept: unknown[] = [];
|
|
179
|
+
for (const candidate of original) {
|
|
180
|
+
const looksLikeImageBlock =
|
|
181
|
+
!!candidate && typeof candidate === "object" && (candidate as { type?: unknown }).type === "image";
|
|
182
|
+
if (looksLikeImageBlock) {
|
|
183
|
+
removed++;
|
|
184
|
+
} else {
|
|
185
|
+
kept.push(candidate);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (kept.length !== original.length) {
|
|
189
|
+
details.images = kept;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return removed;
|
|
193
|
+
}
|
|
194
|
+
case "fileMention": {
|
|
195
|
+
let removed = 0;
|
|
196
|
+
for (const file of message.files) {
|
|
197
|
+
if (file.image) {
|
|
198
|
+
file.image = undefined;
|
|
199
|
+
removed++;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return removed;
|
|
203
|
+
}
|
|
204
|
+
default:
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
117
209
|
/**
|
|
118
210
|
* Message type for bash executions via the ! command.
|
|
119
211
|
*/
|
|
@@ -802,6 +802,30 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
802
802
|
await runtime.ctx.handleCompactCommand(customInstructions);
|
|
803
803
|
},
|
|
804
804
|
},
|
|
805
|
+
{
|
|
806
|
+
name: "drop-images",
|
|
807
|
+
description: "Strip every image from this session's history",
|
|
808
|
+
acpDescription: "Drop all images from the conversation history",
|
|
809
|
+
handle: async (_command, runtime) => {
|
|
810
|
+
const { removed } = await runtime.session.dropImages();
|
|
811
|
+
await runtime.output(
|
|
812
|
+
removed === 0
|
|
813
|
+
? "No images found in this session."
|
|
814
|
+
: `Dropped ${removed} image${removed === 1 ? "" : "s"} from this session.`,
|
|
815
|
+
);
|
|
816
|
+
return commandConsumed();
|
|
817
|
+
},
|
|
818
|
+
handleTui: async (_command, runtime) => {
|
|
819
|
+
runtime.ctx.editor.setText("");
|
|
820
|
+
const { removed } = await runtime.ctx.session.dropImages();
|
|
821
|
+
if (removed === 0) {
|
|
822
|
+
runtime.ctx.showStatus("No images found in this session.");
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
runtime.ctx.rebuildChatFromMessages();
|
|
826
|
+
runtime.ctx.showStatus(`Dropped ${removed} image${removed === 1 ? "" : "s"} from this session.`);
|
|
827
|
+
},
|
|
828
|
+
},
|
|
805
829
|
{
|
|
806
830
|
name: "handoff",
|
|
807
831
|
description: "Hand off session context to a new session",
|