@oh-my-pi/pi-coding-agent 15.1.3 → 15.1.4
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 +18 -0
- package/dist/types/async/job-manager.d.ts +3 -2
- package/dist/types/main.d.ts +11 -2
- package/dist/types/modes/acp/acp-agent.d.ts +1 -1
- package/dist/types/modes/acp/acp-event-mapper.d.ts +13 -1
- package/dist/types/modes/acp/acp-mode.d.ts +3 -1
- package/dist/types/plan-mode/approved-plan.d.ts +6 -4
- package/dist/types/session/agent-session.d.ts +6 -2
- package/dist/types/session/client-bridge.d.ts +3 -0
- package/package.json +7 -7
- package/src/async/job-manager.ts +111 -13
- package/src/cli/update-cli.ts +1 -5
- package/src/eval/js/shared/runtime.ts +82 -2
- package/src/extensibility/typebox.ts +44 -17
- package/src/main.ts +215 -148
- package/src/modes/acp/acp-agent.ts +115 -32
- package/src/modes/acp/acp-client-bridge.ts +2 -1
- package/src/modes/acp/acp-event-mapper.ts +208 -32
- package/src/modes/acp/acp-mode.ts +11 -3
- package/src/modes/components/tree-selector.ts +26 -7
- package/src/plan-mode/approved-plan.ts +21 -9
- package/src/prompts/tools/ask.md +4 -3
- package/src/sdk.ts +3 -1
- package/src/session/agent-session.ts +186 -54
- package/src/session/client-bridge.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.1.4] - 2026-05-19
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed `normalizePlanTitle` rejecting plan titles that contain spaces or common punctuation (e.g. "My Feature Plan") — spaces are now converted to hyphens and other invalid characters are dropped, so models that produce natural-language plan titles no longer loop forever trying to call `resolve`. ([#1176](https://github.com/can1357/oh-my-pi/issues/1176))
|
|
10
|
+
- Fixed `ask` tool prompt example showing the legacy `question`/`options` top-level format instead of the current `questions: [{id, question, options}]` array format; models that closely followed the example generated calls that always failed schema validation. ([#1176](https://github.com/can1357/oh-my-pi/issues/1176))
|
|
11
|
+
|
|
12
|
+
- Fixed ACP command and custom tool-call notifications to carry the original tool arguments in replayed and final updates, so command text is preserved and raw input is no longer wrapped
|
|
13
|
+
- Fixed ACP async-job draining to be scoped by session owner so `getAsyncJobSnapshot` and `drainAsyncJobDeliveriesForAcp` no longer consume or expose jobs from other sessions
|
|
14
|
+
- Fixed async job status reporting to include in-flight completions so queued/delivering indicators remain accurate while callbacks are still running
|
|
15
|
+
- Fixed `deferAgentInitiatedTurns` handling during ACP async-job draining so background completion follow-up turns are delivered even when agent-initiated turns are deferred
|
|
16
|
+
- Fixed ACP ordinary file-editing calls (`edit`, `write`, `ast_edit`) incorrectly requesting `session/request_permission` before every call, while keeping permission prompts for edit operations that delete or move files; permission requests now report the gated tool call as `pending` so clients can render the approval UI instead of returning `Permission request cancelled` without a visible prompt. ([#1134](https://github.com/can1357/oh-my-pi/pull/1134) by [@jiwangyihao](https://github.com/jiwangyihao))
|
|
17
|
+
- Fixed the session tree selector to preserve a readable message column when deeply nested branch gutters would otherwise consume the viewport. ([#1144](https://github.com/can1357/oh-my-pi/issues/1144))
|
|
18
|
+
|
|
5
19
|
## [15.1.3] - 2026-05-17
|
|
6
20
|
### Breaking Changes
|
|
7
21
|
|
|
@@ -44,6 +58,10 @@
|
|
|
44
58
|
- Fixed auth-gateway request cancellation for requests that are already aborted before dispatch.
|
|
45
59
|
- Fixed `/login` and `/logout` provider selector overflowing tall provider lists off-screen on small terminals. The selector now scrolls a 10-item window centered on the highlighted entry, shows a `(n/total)` indicator when windowed, and accepts PageUp/PageDown for faster navigation.
|
|
46
60
|
|
|
61
|
+
### Fixed
|
|
62
|
+
|
|
63
|
+
- Fixed `.env` loading so malformed variable names and NUL-containing values are ignored before they can poison `Bun.env` and break bash/external process execution with `nul byte found in provided data`.
|
|
64
|
+
|
|
47
65
|
## [15.1.2] - 2026-05-15
|
|
48
66
|
### Fixed
|
|
49
67
|
|
|
@@ -65,8 +65,8 @@ export declare class AsyncJobManager {
|
|
|
65
65
|
getRunningJobs(filter?: AsyncJobFilter): AsyncJob[];
|
|
66
66
|
getRecentJobs(limit?: number, filter?: AsyncJobFilter): AsyncJob[];
|
|
67
67
|
getAllJobs(filter?: AsyncJobFilter): AsyncJob[];
|
|
68
|
-
getDeliveryState(): AsyncJobDeliveryState;
|
|
69
|
-
hasPendingDeliveries(): boolean;
|
|
68
|
+
getDeliveryState(filter?: AsyncJobFilter): AsyncJobDeliveryState;
|
|
69
|
+
hasPendingDeliveries(filter?: AsyncJobFilter): boolean;
|
|
70
70
|
watchJobs(jobIds: string[]): number;
|
|
71
71
|
unwatchJobs(jobIds: string[]): number;
|
|
72
72
|
acknowledgeDeliveries(jobIds: string[]): number;
|
|
@@ -79,6 +79,7 @@ export declare class AsyncJobManager {
|
|
|
79
79
|
waitForAll(): Promise<void>;
|
|
80
80
|
drainDeliveries(options?: {
|
|
81
81
|
timeoutMs?: number;
|
|
82
|
+
filter?: AsyncJobFilter;
|
|
82
83
|
}): Promise<boolean>;
|
|
83
84
|
dispose(options?: {
|
|
84
85
|
timeoutMs?: number;
|
package/dist/types/main.d.ts
CHANGED
|
@@ -5,13 +5,22 @@
|
|
|
5
5
|
* createAgentSession() options. The SDK does the heavy lifting.
|
|
6
6
|
*/
|
|
7
7
|
import type { Args } from "./cli/args";
|
|
8
|
-
import {
|
|
8
|
+
import { Settings } from "./config/settings";
|
|
9
|
+
import { InteractiveMode, runAcpMode } from "./modes";
|
|
9
10
|
import type { SubmittedUserInput } from "./modes/types";
|
|
11
|
+
import { createAgentSession, discoverAuthStorage } from "./sdk";
|
|
10
12
|
import type { AgentSession } from "./session/agent-session";
|
|
11
13
|
export interface InteractiveModeNotify {
|
|
12
14
|
kind: "warn" | "error" | "info";
|
|
13
15
|
message: string;
|
|
14
16
|
}
|
|
15
17
|
export declare function submitInteractiveInput(mode: Pick<InteractiveMode, "markPendingSubmissionStarted" | "finishPendingSubmission" | "showError" | "checkShutdownRequested">, session: Pick<AgentSession, "prompt" | "promptCustomMessage">, input: SubmittedUserInput): Promise<void>;
|
|
16
|
-
|
|
18
|
+
interface RunRootCommandDependencies {
|
|
19
|
+
createAgentSession?: typeof createAgentSession;
|
|
20
|
+
discoverAuthStorage?: typeof discoverAuthStorage;
|
|
21
|
+
runAcpMode?: typeof runAcpMode;
|
|
22
|
+
settings?: Settings;
|
|
23
|
+
}
|
|
24
|
+
export declare function runRootCommand(parsed: Args, rawArgs: string[], deps?: RunRootCommandDependencies): Promise<void>;
|
|
17
25
|
export declare function main(args: string[]): Promise<void>;
|
|
26
|
+
export {};
|
|
@@ -30,7 +30,7 @@ type CreateAcpSession = (cwd: string) => Promise<AgentSession>;
|
|
|
30
30
|
export declare function createAcpExtensionUiContext(connection: AgentSideConnection, getSessionId: () => string, clientCapabilities: ClientCapabilities | undefined): ExtensionUIContext;
|
|
31
31
|
export declare class AcpAgent implements Agent {
|
|
32
32
|
#private;
|
|
33
|
-
constructor(connection: AgentSideConnection,
|
|
33
|
+
constructor(connection: AgentSideConnection, createSession: CreateAcpSession, initialSession?: AgentSession);
|
|
34
34
|
setCancelCleanupTimeoutForTesting(timeoutMs: number): void;
|
|
35
35
|
initialize(params: InitializeRequest): Promise<InitializeResponse>;
|
|
36
36
|
authenticate(params: AuthenticateRequest): Promise<AuthenticateResponse>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SessionNotification, ToolKind } from "@agentclientprotocol/sdk";
|
|
1
|
+
import type { SessionNotification, SessionUpdate, ToolKind } from "@agentclientprotocol/sdk";
|
|
2
2
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
3
3
|
interface MessageProgress {
|
|
4
4
|
textEmitted: boolean;
|
|
@@ -7,6 +7,7 @@ interface MessageProgress {
|
|
|
7
7
|
interface AcpEventMapperOptions {
|
|
8
8
|
getMessageId?: (message: unknown) => string | undefined;
|
|
9
9
|
getMessageProgress?: (message: unknown) => MessageProgress | undefined;
|
|
10
|
+
getToolArgs?: (toolCallId: string) => unknown;
|
|
10
11
|
/**
|
|
11
12
|
* Session cwd. Tool call locations sent to ACP clients must be absolute
|
|
12
13
|
* (the editor host needs them to open or focus files). When provided,
|
|
@@ -17,4 +18,15 @@ interface AcpEventMapperOptions {
|
|
|
17
18
|
}
|
|
18
19
|
export declare function mapToolKind(toolName: string): ToolKind;
|
|
19
20
|
export declare function mapAgentSessionEventToAcpSessionUpdates(event: AgentSessionEvent, sessionId: string, options?: AcpEventMapperOptions): SessionNotification[];
|
|
21
|
+
export declare function buildToolCallStartUpdate(input: {
|
|
22
|
+
toolCallId: string;
|
|
23
|
+
toolName: string;
|
|
24
|
+
args: unknown;
|
|
25
|
+
intent?: string;
|
|
26
|
+
cwd?: string;
|
|
27
|
+
status?: "pending" | "completed";
|
|
28
|
+
}): SessionUpdate;
|
|
29
|
+
export declare function normalizeReplayToolArguments(value: unknown): {
|
|
30
|
+
args: unknown;
|
|
31
|
+
};
|
|
20
32
|
export {};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { AgentSideConnection, type Stream } from "@agentclientprotocol/sdk";
|
|
1
2
|
import type { AgentSession } from "../../session/agent-session";
|
|
2
3
|
export type AcpSessionFactory = (cwd: string) => Promise<AgentSession>;
|
|
3
|
-
export declare function
|
|
4
|
+
export declare function createAcpConnection(transport: Stream, createSession: AcpSessionFactory, initialSession?: AgentSession): AgentSideConnection;
|
|
5
|
+
export declare function runAcpMode(createSession: AcpSessionFactory, initialSession?: AgentSession): Promise<never>;
|
|
@@ -7,10 +7,12 @@ export interface PlanApprovalDetails {
|
|
|
7
7
|
title: string;
|
|
8
8
|
planExists: boolean;
|
|
9
9
|
}
|
|
10
|
-
/** Validate the agent-supplied plan title
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
/** Validate and normalize the agent-supplied plan title into a safe filename stem.
|
|
11
|
+
* Spaces and other URL-safe punctuation are replaced with hyphens so models that
|
|
12
|
+
* produce natural-language titles (e.g. "My feature plan") still succeed.
|
|
13
|
+
* Characters that cannot be safely represented after replacement are dropped.
|
|
14
|
+
* The result is restricted to letters, numbers, underscores, and hyphens so it
|
|
15
|
+
* is safe to splice into a `local://` URL without escaping. */
|
|
14
16
|
export declare function normalizePlanTitle(title: string): {
|
|
15
17
|
title: string;
|
|
16
18
|
fileName: string;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { type Agent, type AgentEvent, type AgentMessage, type AgentState, type AgentTool, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
16
16
|
import { type CompactionResult } from "@oh-my-pi/pi-agent-core/compaction";
|
|
17
17
|
import type { AssistantMessage, Effort, ImageContent, Message, MessageAttribution, Model, ProviderSessionState, ServiceTier, SimpleStreamOptions, TextContent, ToolChoice, UsageReport } from "@oh-my-pi/pi-ai";
|
|
18
|
-
import { type AsyncJob, AsyncJobManager } from "../async";
|
|
18
|
+
import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
|
|
19
19
|
import type { Rule } from "../capability/rule";
|
|
20
20
|
import { type ModelRegistry } from "../config/model-registry";
|
|
21
21
|
import { type ResolvedModelRoleValue } from "../config/model-resolver";
|
|
@@ -111,6 +111,7 @@ export type AsyncJobSnapshotItem = Pick<AsyncJob, "id" | "type" | "status" | "la
|
|
|
111
111
|
export interface AsyncJobSnapshot {
|
|
112
112
|
running: AsyncJobSnapshotItem[];
|
|
113
113
|
recent: AsyncJobSnapshotItem[];
|
|
114
|
+
delivery: AsyncJobDeliveryState;
|
|
114
115
|
}
|
|
115
116
|
export interface AgentSessionConfig {
|
|
116
117
|
agent: Agent;
|
|
@@ -347,6 +348,9 @@ export declare class AgentSession {
|
|
|
347
348
|
get isStreaming(): boolean;
|
|
348
349
|
/** Wait until streaming and deferred recovery work are fully settled. */
|
|
349
350
|
waitForIdle(): Promise<void>;
|
|
351
|
+
drainAsyncJobDeliveriesForAcp(options?: {
|
|
352
|
+
timeoutMs?: number;
|
|
353
|
+
}): Promise<boolean>;
|
|
350
354
|
/** Most recent assistant message in agent state. */
|
|
351
355
|
getLastAssistantMessage(): AssistantMessage | undefined;
|
|
352
356
|
/** Current effective system prompt blocks (includes any per-turn extension modifications) */
|
|
@@ -495,7 +499,7 @@ export declare class AgentSession {
|
|
|
495
499
|
*
|
|
496
500
|
* Handles three cases:
|
|
497
501
|
* - Streaming: queue as steer/follow-up or store for next turn
|
|
498
|
-
* - Not streaming + triggerTurn: appends to state/session, starts new turn
|
|
502
|
+
* - Not streaming + triggerTurn: appends to state/session, starts new turn unless the client cannot own it
|
|
499
503
|
* - Not streaming + no trigger: appends to state/session, no turn
|
|
500
504
|
*/
|
|
501
505
|
sendCustomMessage<T = unknown>(message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">, options?: {
|
|
@@ -23,6 +23,7 @@ export interface ClientBridgePermissionToolCall {
|
|
|
23
23
|
toolName: string;
|
|
24
24
|
title: string;
|
|
25
25
|
kind?: string;
|
|
26
|
+
status?: "pending" | "in_progress" | "completed" | "failed";
|
|
26
27
|
rawInput?: unknown;
|
|
27
28
|
locations?: {
|
|
28
29
|
path: string;
|
|
@@ -70,6 +71,8 @@ export interface ClientBridgeCreateTerminalParams {
|
|
|
70
71
|
}
|
|
71
72
|
export interface ClientBridge {
|
|
72
73
|
readonly capabilities: ClientBridgeCapabilities;
|
|
74
|
+
/** ACP v1 clients cannot show server-initiated turns as busy after prompt response. */
|
|
75
|
+
readonly deferAgentInitiatedTurns?: boolean;
|
|
73
76
|
readTextFile?(params: {
|
|
74
77
|
path: string;
|
|
75
78
|
line?: number;
|
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.1.
|
|
4
|
+
"version": "15.1.4",
|
|
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,12 +47,12 @@
|
|
|
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/omp-stats": "15.1.
|
|
51
|
-
"@oh-my-pi/pi-agent-core": "15.1.
|
|
52
|
-
"@oh-my-pi/pi-ai": "15.1.
|
|
53
|
-
"@oh-my-pi/pi-natives": "15.1.
|
|
54
|
-
"@oh-my-pi/pi-tui": "15.1.
|
|
55
|
-
"@oh-my-pi/pi-utils": "15.1.
|
|
50
|
+
"@oh-my-pi/omp-stats": "15.1.4",
|
|
51
|
+
"@oh-my-pi/pi-agent-core": "15.1.4",
|
|
52
|
+
"@oh-my-pi/pi-ai": "15.1.4",
|
|
53
|
+
"@oh-my-pi/pi-natives": "15.1.4",
|
|
54
|
+
"@oh-my-pi/pi-tui": "15.1.4",
|
|
55
|
+
"@oh-my-pi/pi-utils": "15.1.4",
|
|
56
56
|
"@puppeteer/browsers": "^2.13.0",
|
|
57
57
|
"@types/turndown": "5.0.6",
|
|
58
58
|
"@xterm/headless": "^6.0.0",
|
package/src/async/job-manager.ts
CHANGED
|
@@ -37,6 +37,8 @@ interface AsyncJobDelivery {
|
|
|
37
37
|
attempt: number;
|
|
38
38
|
nextAttemptAt: number;
|
|
39
39
|
lastError?: string;
|
|
40
|
+
ownerId?: string;
|
|
41
|
+
promise?: Promise<void>;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
export interface AsyncJobDeliveryState {
|
|
@@ -82,6 +84,7 @@ export class AsyncJobManager {
|
|
|
82
84
|
|
|
83
85
|
readonly #jobs = new Map<string, AsyncJob>();
|
|
84
86
|
readonly #deliveries: AsyncJobDelivery[] = [];
|
|
87
|
+
readonly #inFlightDeliveries: AsyncJobDelivery[] = [];
|
|
85
88
|
readonly #suppressedDeliveries = new Set<string>();
|
|
86
89
|
readonly #watchedJobs = new Set<string>();
|
|
87
90
|
readonly #evictionTimers = new Map<string, NodeJS.Timeout>();
|
|
@@ -219,22 +222,24 @@ export class AsyncJobManager {
|
|
|
219
222
|
return this.#filterJobs(this.#jobs.values(), filter);
|
|
220
223
|
}
|
|
221
224
|
|
|
222
|
-
getDeliveryState(): AsyncJobDeliveryState {
|
|
223
|
-
const
|
|
225
|
+
getDeliveryState(filter?: AsyncJobFilter): AsyncJobDeliveryState {
|
|
226
|
+
const deliveries = this.#filterDeliveries(filter);
|
|
227
|
+
const inFlightDeliveries = this.#filterInFlightDeliveries(filter);
|
|
228
|
+
const nextRetryAt = deliveries.reduce<number | undefined>((next, delivery) => {
|
|
224
229
|
if (next === undefined) return delivery.nextAttemptAt;
|
|
225
230
|
return Math.min(next, delivery.nextAttemptAt);
|
|
226
231
|
}, undefined);
|
|
227
232
|
|
|
228
233
|
return {
|
|
229
|
-
queued:
|
|
230
|
-
delivering: this.#deliveryLoop !== undefined,
|
|
234
|
+
queued: deliveries.length + inFlightDeliveries.length,
|
|
235
|
+
delivering: inFlightDeliveries.length > 0 || (this.#deliveryLoop !== undefined && deliveries.length > 0),
|
|
231
236
|
nextRetryAt,
|
|
232
|
-
pendingJobIds:
|
|
237
|
+
pendingJobIds: deliveries.concat(inFlightDeliveries).map(delivery => delivery.jobId),
|
|
233
238
|
};
|
|
234
239
|
}
|
|
235
240
|
|
|
236
|
-
hasPendingDeliveries(): boolean {
|
|
237
|
-
return this
|
|
241
|
+
hasPendingDeliveries(filter?: AsyncJobFilter): boolean {
|
|
242
|
+
return this.getDeliveryState(filter).queued > 0;
|
|
238
243
|
}
|
|
239
244
|
|
|
240
245
|
watchJobs(jobIds: string[]): number {
|
|
@@ -290,12 +295,25 @@ export class AsyncJobManager {
|
|
|
290
295
|
await Promise.all(Array.from(this.#jobs.values()).map(job => job.promise));
|
|
291
296
|
}
|
|
292
297
|
|
|
293
|
-
async drainDeliveries(options?: { timeoutMs?: number }): Promise<boolean> {
|
|
298
|
+
async drainDeliveries(options?: { timeoutMs?: number; filter?: AsyncJobFilter }): Promise<boolean> {
|
|
294
299
|
const timeoutMs = options?.timeoutMs;
|
|
300
|
+
const filter = options?.filter;
|
|
295
301
|
const hasDeadline = timeoutMs !== undefined;
|
|
296
302
|
const deadline = hasDeadline ? Date.now() + Math.max(timeoutMs, 0) : Number.POSITIVE_INFINITY;
|
|
297
303
|
|
|
298
|
-
while (this.hasPendingDeliveries()) {
|
|
304
|
+
while (this.hasPendingDeliveries(filter)) {
|
|
305
|
+
if (filter?.ownerId) {
|
|
306
|
+
const delivered = await this.#deliverNextFiltered(filter, deadline);
|
|
307
|
+
if (delivered) continue;
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
const inFlightDeliveries = this.#filterInFlightDeliveries();
|
|
311
|
+
if (inFlightDeliveries.length > 0 && this.#filterDeliveries().length === 0) {
|
|
312
|
+
const delivered = await this.#waitForDeliveryPromise(inFlightDeliveries[0]?.promise, deadline);
|
|
313
|
+
if (delivered) continue;
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
|
|
299
317
|
this.#ensureDeliveryLoop();
|
|
300
318
|
const loop = this.#deliveryLoop;
|
|
301
319
|
if (!loop) {
|
|
@@ -313,7 +331,7 @@ export class AsyncJobManager {
|
|
|
313
331
|
}
|
|
314
332
|
|
|
315
333
|
await Promise.race([loop, Bun.sleep(remainingMs)]);
|
|
316
|
-
if (Date.now() >= deadline && this.hasPendingDeliveries()) {
|
|
334
|
+
if (Date.now() >= deadline && this.hasPendingDeliveries(filter)) {
|
|
317
335
|
return false;
|
|
318
336
|
}
|
|
319
337
|
}
|
|
@@ -330,6 +348,7 @@ export class AsyncJobManager {
|
|
|
330
348
|
this.#clearEvictionTimers();
|
|
331
349
|
this.#jobs.clear();
|
|
332
350
|
this.#deliveries.length = 0;
|
|
351
|
+
this.#inFlightDeliveries.length = 0;
|
|
333
352
|
this.#suppressedDeliveries.clear();
|
|
334
353
|
this.#watchedJobs.clear();
|
|
335
354
|
return drained;
|
|
@@ -388,6 +407,55 @@ export class AsyncJobManager {
|
|
|
388
407
|
this.#evictionTimers.clear();
|
|
389
408
|
}
|
|
390
409
|
|
|
410
|
+
#filterDeliveries(filter?: AsyncJobFilter): AsyncJobDelivery[] {
|
|
411
|
+
const ownerId = filter?.ownerId;
|
|
412
|
+
if (!ownerId) return this.#deliveries.filter(delivery => !this.isDeliverySuppressed(delivery.jobId));
|
|
413
|
+
return this.#deliveries.filter(
|
|
414
|
+
delivery => delivery.ownerId === ownerId && !this.isDeliverySuppressed(delivery.jobId),
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
#filterInFlightDeliveries(filter?: AsyncJobFilter): AsyncJobDelivery[] {
|
|
419
|
+
const ownerId = filter?.ownerId;
|
|
420
|
+
if (!ownerId) return this.#inFlightDeliveries.filter(delivery => !this.isDeliverySuppressed(delivery.jobId));
|
|
421
|
+
return this.#inFlightDeliveries.filter(
|
|
422
|
+
delivery => delivery.ownerId === ownerId && !this.isDeliverySuppressed(delivery.jobId),
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async #deliverNextFiltered(filter: AsyncJobFilter, deadline: number): Promise<boolean> {
|
|
427
|
+
while (true) {
|
|
428
|
+
let selected: AsyncJobDelivery | undefined;
|
|
429
|
+
for (const delivery of this.#deliveries) {
|
|
430
|
+
if (delivery.ownerId !== filter.ownerId) continue;
|
|
431
|
+
if (this.isDeliverySuppressed(delivery.jobId)) continue;
|
|
432
|
+
if (!selected || delivery.nextAttemptAt < selected.nextAttemptAt) {
|
|
433
|
+
selected = delivery;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (!selected) {
|
|
438
|
+
const inFlight = this.#filterInFlightDeliveries(filter);
|
|
439
|
+
if (inFlight.length === 0) return true;
|
|
440
|
+
return this.#waitForDeliveryPromise(inFlight[0]?.promise, deadline);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const now = Date.now();
|
|
444
|
+
if (selected.nextAttemptAt > now) {
|
|
445
|
+
if (selected.nextAttemptAt > deadline) return false;
|
|
446
|
+
await Bun.sleep(selected.nextAttemptAt - now);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const index = this.#deliveries.indexOf(selected);
|
|
451
|
+
if (index === -1) continue;
|
|
452
|
+
this.#deliveries.splice(index, 1);
|
|
453
|
+
if (this.isDeliverySuppressed(selected.jobId)) continue;
|
|
454
|
+
|
|
455
|
+
return this.#waitForDeliveryPromise(this.#deliverDelivery(selected), deadline);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
391
459
|
isDeliverySuppressed(jobId: string): boolean {
|
|
392
460
|
return this.#suppressedDeliveries.has(jobId) || this.#watchedJobs.has(jobId);
|
|
393
461
|
}
|
|
@@ -402,6 +470,7 @@ export class AsyncJobManager {
|
|
|
402
470
|
text,
|
|
403
471
|
attempt: 0,
|
|
404
472
|
nextAttemptAt: Date.now(),
|
|
473
|
+
ownerId: this.#jobs.get(jobId)?.ownerId,
|
|
405
474
|
});
|
|
406
475
|
this.#ensureDeliveryLoop();
|
|
407
476
|
}
|
|
@@ -437,20 +506,25 @@ export class AsyncJobManager {
|
|
|
437
506
|
if (this.#deliveries[0] !== delivery) {
|
|
438
507
|
continue;
|
|
439
508
|
}
|
|
440
|
-
// Check again after sleep
|
|
441
509
|
if (this.isDeliverySuppressed(delivery.jobId)) {
|
|
442
510
|
this.#deliveries.shift();
|
|
443
511
|
continue;
|
|
444
512
|
}
|
|
445
513
|
|
|
514
|
+
this.#deliveries.shift();
|
|
515
|
+
await this.#deliverDelivery(delivery);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
#deliverDelivery(delivery: AsyncJobDelivery): Promise<void> {
|
|
520
|
+
const promise = (async () => {
|
|
521
|
+
this.#inFlightDeliveries.push(delivery);
|
|
446
522
|
try {
|
|
447
523
|
await this.#onJobComplete(delivery.jobId, delivery.text, this.#jobs.get(delivery.jobId));
|
|
448
|
-
this.#deliveries.shift();
|
|
449
524
|
} catch (error) {
|
|
450
525
|
delivery.attempt += 1;
|
|
451
526
|
delivery.lastError = error instanceof Error ? error.message : String(error);
|
|
452
527
|
delivery.nextAttemptAt = Date.now() + this.#getRetryDelay(delivery.attempt);
|
|
453
|
-
this.#deliveries.shift();
|
|
454
528
|
if (!this.isDeliverySuppressed(delivery.jobId)) {
|
|
455
529
|
this.#deliveries.push(delivery);
|
|
456
530
|
}
|
|
@@ -460,8 +534,32 @@ export class AsyncJobManager {
|
|
|
460
534
|
nextRetryAt: delivery.nextAttemptAt,
|
|
461
535
|
error: delivery.lastError,
|
|
462
536
|
});
|
|
537
|
+
} finally {
|
|
538
|
+
const index = this.#inFlightDeliveries.indexOf(delivery);
|
|
539
|
+
if (index !== -1) this.#inFlightDeliveries.splice(index, 1);
|
|
540
|
+
if (this.#deliveries.length > 0) this.#ensureDeliveryLoop();
|
|
463
541
|
}
|
|
542
|
+
})();
|
|
543
|
+
delivery.promise = promise;
|
|
544
|
+
return promise;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async #waitForDeliveryPromise(promise: Promise<void> | undefined, deadline: number): Promise<boolean> {
|
|
548
|
+
if (!promise) return true;
|
|
549
|
+
if (deadline === Number.POSITIVE_INFINITY) {
|
|
550
|
+
await promise;
|
|
551
|
+
return true;
|
|
464
552
|
}
|
|
553
|
+
const remainingMs = deadline - Date.now();
|
|
554
|
+
if (remainingMs <= 0) return false;
|
|
555
|
+
let timedOut = false;
|
|
556
|
+
await Promise.race([
|
|
557
|
+
promise,
|
|
558
|
+
Bun.sleep(remainingMs).then(() => {
|
|
559
|
+
timedOut = true;
|
|
560
|
+
}),
|
|
561
|
+
]);
|
|
562
|
+
return !timedOut;
|
|
465
563
|
}
|
|
466
564
|
|
|
467
565
|
#getRetryDelay(attempt: number): number {
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -235,11 +235,7 @@ async function printVerification(expectedVersion: string): Promise<void> {
|
|
|
235
235
|
chalk.yellow(`\nWarning: could not verify updated version${result.path ? ` at ${result.path}` : ""}`),
|
|
236
236
|
);
|
|
237
237
|
}
|
|
238
|
-
console.log(
|
|
239
|
-
chalk.yellow(
|
|
240
|
-
`You may need to reinstall: curl -fsSL https://raw.githubusercontent.com/${REPO}/main/install.sh | bash`,
|
|
241
|
-
),
|
|
242
|
-
);
|
|
238
|
+
console.log(chalk.yellow(`You may need to reinstall: curl -fsSL https://omp.sh/install | sh`));
|
|
243
239
|
}
|
|
244
240
|
|
|
245
241
|
/**
|
|
@@ -38,6 +38,75 @@ export interface RuntimeOptions {
|
|
|
38
38
|
extraGlobals?: Record<string, unknown>;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// Strict base64: characters from the standard alphabet plus optional `=` padding, and a
|
|
42
|
+
// length that is a multiple of four. URL-safe base64 and embedded whitespace are not
|
|
43
|
+
// accepted — the Anthropic API only honors strict base64 in image sources.
|
|
44
|
+
const BASE64_STRICT_RE = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
45
|
+
const DECIMAL_CSV_RE = /^\d{1,3}(?:,\d{1,3})*$/;
|
|
46
|
+
|
|
47
|
+
function isStrictBase64(s: string): boolean {
|
|
48
|
+
if (s.length === 0 || s.length % 4 !== 0) return false;
|
|
49
|
+
return BASE64_STRICT_RE.test(s);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Normalize the `data` field of an `{ type: "image", data, mimeType }` display payload
|
|
54
|
+
* into strict base64. Accepts:
|
|
55
|
+
* - already-valid base64 strings (passed through verbatim)
|
|
56
|
+
* - `Uint8Array` / `Buffer` / `ArrayBuffer` / typed array views
|
|
57
|
+
* - `{ type: "Buffer", data: number[] }` (the shape Node serializes Buffers to via
|
|
58
|
+
* `JSON.stringify`)
|
|
59
|
+
* - decimal-CSV byte strings (the output of `uint8array.toString("base64")`, which
|
|
60
|
+
* silently ignores the encoding argument and returns `Array.prototype.toString` —
|
|
61
|
+
* a footgun for callers expecting `Buffer.toString` semantics)
|
|
62
|
+
* Returns `null` if no recovery is possible.
|
|
63
|
+
*/
|
|
64
|
+
function coerceImageBase64(data: unknown): string | null {
|
|
65
|
+
if (typeof data === "string") {
|
|
66
|
+
if (isStrictBase64(data)) return data;
|
|
67
|
+
if (DECIMAL_CSV_RE.test(data)) {
|
|
68
|
+
const parts = data.split(",");
|
|
69
|
+
const bytes = new Uint8Array(parts.length);
|
|
70
|
+
for (let i = 0; i < parts.length; i++) {
|
|
71
|
+
const n = Number(parts[i]);
|
|
72
|
+
if (!Number.isInteger(n) || n < 0 || n > 255) return null;
|
|
73
|
+
bytes[i] = n;
|
|
74
|
+
}
|
|
75
|
+
return Buffer.from(bytes).toString("base64");
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
if (data instanceof Uint8Array) return Buffer.from(data).toString("base64");
|
|
80
|
+
if (data instanceof ArrayBuffer) return Buffer.from(data).toString("base64");
|
|
81
|
+
if (ArrayBuffer.isView(data)) {
|
|
82
|
+
const view = data as ArrayBufferView;
|
|
83
|
+
return Buffer.from(view.buffer, view.byteOffset, view.byteLength).toString("base64");
|
|
84
|
+
}
|
|
85
|
+
if (data && typeof data === "object") {
|
|
86
|
+
const obj = data as { type?: unknown; data?: unknown };
|
|
87
|
+
if (obj.type === "Buffer" && Array.isArray(obj.data)) {
|
|
88
|
+
const arr = obj.data as unknown[];
|
|
89
|
+
const bytes = new Uint8Array(arr.length);
|
|
90
|
+
for (let i = 0; i < arr.length; i++) {
|
|
91
|
+
const n = arr[i];
|
|
92
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < 0 || n > 255) return null;
|
|
93
|
+
bytes[i] = n;
|
|
94
|
+
}
|
|
95
|
+
return Buffer.from(bytes).toString("base64");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function describeDataType(data: unknown): string {
|
|
102
|
+
if (data === null) return "null";
|
|
103
|
+
if (data instanceof Uint8Array) return "Uint8Array";
|
|
104
|
+
if (data instanceof ArrayBuffer) return "ArrayBuffer";
|
|
105
|
+
if (ArrayBuffer.isView(data)) return data.constructor.name;
|
|
106
|
+
if (typeof data === "string") return `string(${data.length})`;
|
|
107
|
+
return typeof data;
|
|
108
|
+
}
|
|
109
|
+
|
|
41
110
|
/**
|
|
42
111
|
* Shared JS runtime for the eval worker and the browser tab worker. Owns the prelude,
|
|
43
112
|
* helper bag, console bridge, and indirect-eval execution. Emits text/display/tool-call
|
|
@@ -109,8 +178,19 @@ export class JsRuntime {
|
|
|
109
178
|
if (!hooks) return;
|
|
110
179
|
if (value && typeof value === "object") {
|
|
111
180
|
const record = value as Record<string, unknown>;
|
|
112
|
-
if (record.type === "image" && typeof record.
|
|
113
|
-
|
|
181
|
+
if (record.type === "image" && typeof record.mimeType === "string") {
|
|
182
|
+
const data = coerceImageBase64(record.data);
|
|
183
|
+
if (data !== null) {
|
|
184
|
+
hooks.onDisplay({ type: "image", data, mimeType: record.mimeType });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
logger.warn("js displayValue: dropping image with unrecognized data shape", {
|
|
188
|
+
mimeType: record.mimeType,
|
|
189
|
+
dataType: describeDataType(record.data),
|
|
190
|
+
});
|
|
191
|
+
hooks.onText(
|
|
192
|
+
`[display: image dropped — \`data\` must be a base64 string, Uint8Array/Buffer, or ArrayBuffer; got ${describeDataType(record.data)}]\n`,
|
|
193
|
+
);
|
|
114
194
|
return;
|
|
115
195
|
}
|
|
116
196
|
try {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* `@sinclair/typebox` directly in their own package.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { areJsonValuesEqual } from "@oh-my-pi/pi-ai/utils/schema";
|
|
24
|
+
import { areJsonValuesEqual, zodToWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
|
|
25
25
|
import {
|
|
26
26
|
type ZodArray,
|
|
27
27
|
type ZodEnum,
|
|
@@ -104,19 +104,46 @@ interface ObjectOpts extends Meta {
|
|
|
104
104
|
// Helpers
|
|
105
105
|
// ---------------------------------------------------------------------------
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Stamp a non-enumerable `toJSON()` on a schema so `JSON.stringify(schema)`
|
|
109
|
+
* yields a clean draft 2020-12 JSON Schema — matching real TypeBox semantics
|
|
110
|
+
* where the schema object IS already a JSON Schema. Without this, an extension
|
|
111
|
+
* author who serialises the schema across any JSON boundary (worker
|
|
112
|
+
* postMessage, MCP transport, config persistence, network hop, structuredClone
|
|
113
|
+
* fallback) ships the raw Zod internals (`def`, `_zod`, object-shaped `enum`,
|
|
114
|
+
* `"type":"enum"`) — neither valid JSON Schema nor parseable Zod. See
|
|
115
|
+
* issue #1101 for the symptoms when this leaks into a tool's `input_schema`.
|
|
116
|
+
*
|
|
117
|
+
* Idempotent: re-stamping the same instance is a no-op.
|
|
118
|
+
*/
|
|
119
|
+
function wire<T extends ZodType>(schema: T): T {
|
|
120
|
+
if (!Object.hasOwn(schema as object, "toJSON")) {
|
|
121
|
+
Object.defineProperty(schema as object, "toJSON", {
|
|
122
|
+
value: function toJSON(this: ZodType) {
|
|
123
|
+
return zodToWireSchema(this);
|
|
124
|
+
},
|
|
125
|
+
enumerable: false,
|
|
126
|
+
writable: true,
|
|
127
|
+
configurable: true,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return schema;
|
|
131
|
+
}
|
|
132
|
+
|
|
107
133
|
function withMeta<T extends ZodType>(schema: T, opts: Meta | undefined): T {
|
|
108
|
-
if (!opts) return schema;
|
|
109
134
|
let out: ZodType = schema;
|
|
110
|
-
if (
|
|
111
|
-
|
|
135
|
+
if (opts) {
|
|
136
|
+
if (typeof opts.description === "string") out = out.describe(opts.description);
|
|
137
|
+
if ("default" in opts) out = out.default(opts.default as never) as unknown as ZodType;
|
|
112
138
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
139
|
+
const metadata: Record<string, unknown> = {};
|
|
140
|
+
for (const key in opts) {
|
|
141
|
+
if (key === "description" || key === "default" || key === "additionalProperties") continue;
|
|
142
|
+
metadata[key] = opts[key];
|
|
143
|
+
}
|
|
144
|
+
if (Object.keys(metadata).length > 0) out = out.meta(metadata);
|
|
117
145
|
}
|
|
118
|
-
|
|
119
|
-
return out as T;
|
|
146
|
+
return wire(out as T);
|
|
120
147
|
}
|
|
121
148
|
|
|
122
149
|
// ---------------------------------------------------------------------------
|
|
@@ -313,7 +340,8 @@ function tRecord<V extends ZodType>(key: ZodType, value: V, opts?: Meta): ZodTyp
|
|
|
313
340
|
}
|
|
314
341
|
|
|
315
342
|
function tOptional<E extends ZodType>(schema: E, _opts?: Meta): ZodOptional<E> {
|
|
316
|
-
|
|
343
|
+
if (isOptional(schema)) return wire(schema as unknown as ZodOptional<E>);
|
|
344
|
+
return wire(schema.optional() as ZodOptional<E>);
|
|
317
345
|
}
|
|
318
346
|
|
|
319
347
|
function tNullable<E extends ZodType>(schema: E, opts?: Meta): ZodType {
|
|
@@ -322,27 +350,26 @@ function tNullable<E extends ZodType>(schema: E, opts?: Meta): ZodType {
|
|
|
322
350
|
|
|
323
351
|
function tReadonly<E extends ZodType>(schema: E): E {
|
|
324
352
|
// TypeBox's `Type.Readonly` is purely a marker; runtime parsing is identical.
|
|
325
|
-
return schema;
|
|
353
|
+
return wire(schema);
|
|
326
354
|
}
|
|
327
355
|
|
|
328
356
|
function tPartial<P extends ZodRawShape>(obj: ZodObject<P>): ZodObject<P> {
|
|
329
|
-
return obj.partial() as unknown as ZodObject<P
|
|
357
|
+
return wire(obj.partial() as unknown as ZodObject<P>);
|
|
330
358
|
}
|
|
331
359
|
|
|
332
360
|
function tRequired<P extends ZodRawShape>(obj: ZodObject<P>): ZodObject<P> {
|
|
333
|
-
return obj.required() as unknown as ZodObject<P
|
|
361
|
+
return wire(obj.required() as unknown as ZodObject<P>);
|
|
334
362
|
}
|
|
335
363
|
|
|
336
364
|
function tPick<P extends ZodRawShape, K extends keyof P>(obj: ZodObject<P>, keys: readonly K[]): ZodObject<Pick<P, K>> {
|
|
337
365
|
const mask = Object.fromEntries(keys.map(k => [k as string, true]));
|
|
338
|
-
return obj.pick(mask as never) as unknown as ZodObject<Pick<P, K
|
|
366
|
+
return wire(obj.pick(mask as never) as unknown as ZodObject<Pick<P, K>>);
|
|
339
367
|
}
|
|
340
368
|
|
|
341
369
|
function tOmit<P extends ZodRawShape, K extends keyof P>(obj: ZodObject<P>, keys: readonly K[]): ZodObject<Omit<P, K>> {
|
|
342
370
|
const mask = Object.fromEntries(keys.map(k => [k as string, true]));
|
|
343
|
-
return obj.omit(mask as never) as unknown as ZodObject<Omit<P, K
|
|
371
|
+
return wire(obj.omit(mask as never) as unknown as ZodObject<Omit<P, K>>);
|
|
344
372
|
}
|
|
345
|
-
|
|
346
373
|
function tComposite(objects: readonly ZodObject<ZodRawShape>[], opts?: Meta): ZodObject<ZodRawShape> {
|
|
347
374
|
// `Type.Composite([...])` flattens every object schema into one object schema
|
|
348
375
|
// rather than producing an intersection. Mirror that via repeated `extend`.
|