@oh-my-pi/pi-coding-agent 16.3.4 → 16.3.6

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/cli.js +3522 -3485
  3. package/dist/types/edit/file-snapshot-store.d.ts +4 -2
  4. package/dist/types/edit/renderer.d.ts +0 -1
  5. package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
  6. package/dist/types/extensibility/shared-events.d.ts +8 -1
  7. package/dist/types/hindsight/content.d.ts +4 -0
  8. package/dist/types/internal-urls/artifact-protocol.d.ts +8 -0
  9. package/dist/types/internal-urls/types.d.ts +10 -0
  10. package/dist/types/lsp/config.d.ts +4 -0
  11. package/dist/types/lsp/edits.d.ts +2 -0
  12. package/dist/types/mcp/oauth-discovery.d.ts +30 -0
  13. package/dist/types/modes/components/assistant-message.d.ts +15 -10
  14. package/dist/types/modes/components/bash-execution.d.ts +6 -0
  15. package/dist/types/modes/components/eval-execution.d.ts +6 -0
  16. package/dist/types/modes/components/login-dialog.d.ts +10 -2
  17. package/dist/types/modes/components/tool-execution.d.ts +3 -13
  18. package/dist/types/modes/components/transcript-container.d.ts +26 -29
  19. package/dist/types/modes/controllers/mcp-command-controller.d.ts +20 -3
  20. package/dist/types/modes/rpc/rpc-client.d.ts +7 -3
  21. package/dist/types/modes/rpc/rpc-types.d.ts +6 -0
  22. package/dist/types/modes/utils/transcript-render-helpers.d.ts +15 -6
  23. package/dist/types/session/agent-session.d.ts +2 -0
  24. package/dist/types/subprocess/worker-runtime.d.ts +11 -0
  25. package/dist/types/tools/bash-skill-urls.d.ts +2 -2
  26. package/dist/types/tools/bash.d.ts +0 -2
  27. package/dist/types/tools/browser/tab-supervisor.d.ts +10 -0
  28. package/dist/types/tools/eval-render.d.ts +0 -2
  29. package/dist/types/tools/renderers.d.ts +0 -20
  30. package/dist/types/tools/ssh.d.ts +0 -2
  31. package/dist/types/tools/write.d.ts +1 -1
  32. package/dist/types/utils/git.d.ts +16 -0
  33. package/package.json +12 -12
  34. package/src/cli/auth-broker-cli.ts +13 -2
  35. package/src/cli/tiny-models-cli.ts +12 -5
  36. package/src/edit/file-snapshot-store.ts +5 -2
  37. package/src/edit/renderer.ts +0 -5
  38. package/src/extensibility/custom-tools/types.ts +2 -0
  39. package/src/extensibility/shared-events.ts +9 -1
  40. package/src/hindsight/content.ts +31 -0
  41. package/src/internal-urls/artifact-protocol.ts +97 -53
  42. package/src/internal-urls/types.ts +10 -0
  43. package/src/lsp/config.ts +15 -0
  44. package/src/lsp/edits.ts +28 -7
  45. package/src/lsp/index.ts +46 -4
  46. package/src/mcp/oauth-discovery.ts +88 -18
  47. package/src/mnemopi/state.ts +26 -2
  48. package/src/modes/components/assistant-message.ts +134 -82
  49. package/src/modes/components/bash-execution.ts +9 -0
  50. package/src/modes/components/chat-transcript-builder.ts +8 -4
  51. package/src/modes/components/eval-execution.ts +9 -0
  52. package/src/modes/components/login-dialog.ts +16 -2
  53. package/src/modes/components/mcp-add-wizard.ts +9 -1
  54. package/src/modes/components/tool-execution.ts +4 -50
  55. package/src/modes/components/transcript-container.ts +82 -458
  56. package/src/modes/components/tree-selector.ts +9 -3
  57. package/src/modes/controllers/command-controller.ts +0 -3
  58. package/src/modes/controllers/event-controller.ts +74 -14
  59. package/src/modes/controllers/extension-ui-controller.ts +0 -1
  60. package/src/modes/controllers/input-controller.ts +4 -10
  61. package/src/modes/controllers/mcp-command-controller.ts +106 -29
  62. package/src/modes/controllers/selector-controller.ts +9 -1
  63. package/src/modes/interactive-mode.ts +12 -8
  64. package/src/modes/rpc/rpc-client.ts +8 -4
  65. package/src/modes/rpc/rpc-mode.ts +1 -0
  66. package/src/modes/rpc/rpc-types.ts +13 -1
  67. package/src/modes/setup-wizard/scenes/sign-in.ts +18 -0
  68. package/src/modes/utils/transcript-render-helpers.ts +40 -13
  69. package/src/modes/utils/ui-helpers.ts +12 -7
  70. package/src/prompts/tools/read.md +1 -1
  71. package/src/sdk.ts +1 -0
  72. package/src/session/agent-session.ts +148 -1
  73. package/src/session/session-context.ts +7 -0
  74. package/src/subprocess/worker-runtime.ts +219 -2
  75. package/src/task/worktree.ts +28 -6
  76. package/src/tiny/worker.ts +14 -4
  77. package/src/tools/bash-skill-urls.ts +3 -3
  78. package/src/tools/bash.ts +0 -4
  79. package/src/tools/browser/tab-supervisor.ts +47 -3
  80. package/src/tools/eval-render.ts +0 -20
  81. package/src/tools/grep.ts +19 -2
  82. package/src/tools/path-utils.ts +4 -0
  83. package/src/tools/read.ts +261 -21
  84. package/src/tools/renderers.ts +0 -20
  85. package/src/tools/ssh.ts +0 -16
  86. package/src/tools/write.ts +13 -6
  87. package/src/utils/git.ts +20 -0
  88. package/src/utils/open.ts +51 -6
@@ -7,8 +7,14 @@
7
7
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
8
8
  import { type Component, Text } from "@oh-my-pi/pi-tui";
9
9
  import { formatBytes, formatDuration } from "@oh-my-pi/pi-utils";
10
- import { type CustomMessage, type FileMentionMessage, isSilentAbort, resolveAbortLabel } from "../../session/messages";
10
+ import {
11
+ type CustomMessage,
12
+ type FileMentionMessage,
13
+ resolveAbortLabel,
14
+ shouldRenderAbortReason,
15
+ } from "../../session/messages";
11
16
  import { createIrcMessageCard } from "../../tools/irc";
17
+ import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../../tools/render-utils";
12
18
  import { canonicalizeMessage } from "../../utils/thinking-display";
13
19
  import { TranscriptBlock } from "../components/transcript-container";
14
20
  import { theme } from "../theme/theme";
@@ -138,20 +144,41 @@ export function normalizeToolArgs(args: unknown): Record<string, unknown> {
138
144
  return args && typeof args === "object" && !Array.isArray(args) ? (args as Record<string, unknown>) : {};
139
145
  }
140
146
 
147
+ export type AssistantErrorPresentation =
148
+ | { kind: "none" }
149
+ | { kind: "full"; text: string; isError: true }
150
+ | { kind: "compact-recovered"; text: string; isError: false };
151
+
152
+ function sanitizeRecoveredRetryNote(note: string): string {
153
+ const normalized = replaceTabs(note).replace(/\s+/g, " ").trim();
154
+ return truncateToWidth(normalized || "retried", TRUNCATE_LENGTHS.CONTENT);
155
+ }
156
+
141
157
  /**
142
- * Resolve the inline error label, if any, for a turn-ending assistant message.
143
- * Silent aborts yield no label. `retryAttempt` tunes the abort label wording.
158
+ * Resolve the turn-ending assistant error presentation, if any.
159
+ * Silent and user-interrupt aborts yield no label. Recovered auto-retry errors
160
+ * collapse to a single non-error note; terminal errors keep the full red presentation.
144
161
  */
145
- export function resolveAssistantErrorMessage(
162
+ export function resolveAssistantErrorPresentation(
146
163
  message: AssistantAgentMessage,
147
164
  retryAttempt = 0,
148
- ): { hasErrorStop: boolean; errorMessage: string | null } {
149
- const isAbortedSilently = message.stopReason === "aborted" && isSilentAbort(message);
150
- const hasErrorStop = !isAbortedSilently && (message.stopReason === "aborted" || message.stopReason === "error");
151
- const errorMessage = hasErrorStop
152
- ? message.stopReason === "aborted"
153
- ? resolveAbortLabel(message, retryAttempt)
154
- : message.errorMessage || "Error"
155
- : null;
156
- return { hasErrorStop, errorMessage };
165
+ ): AssistantErrorPresentation {
166
+ if (message.retryRecovery?.status === "recovered") {
167
+ return {
168
+ kind: "compact-recovered",
169
+ text: sanitizeRecoveredRetryNote(message.retryRecovery.note),
170
+ isError: false,
171
+ };
172
+ }
173
+ if (message.stopReason === "aborted") {
174
+ if (!shouldRenderAbortReason(message)) return { kind: "none" };
175
+ return { kind: "full", text: resolveAbortLabel(message, retryAttempt), isError: true };
176
+ }
177
+ if (message.stopReason === "error") {
178
+ return { kind: "full", text: message.errorMessage || "Error", isError: true };
179
+ }
180
+ if (message.errorMessage && shouldRenderAbortReason(message)) {
181
+ return { kind: "full", text: message.errorMessage, isError: true };
182
+ }
183
+ return { kind: "none" };
157
184
  }
@@ -54,7 +54,7 @@ import {
54
54
  buildFileMentionBlock,
55
55
  buildIrcMessageCard,
56
56
  normalizeToolArgs,
57
- resolveAssistantErrorMessage,
57
+ resolveAssistantErrorPresentation,
58
58
  } from "./transcript-render-helpers";
59
59
 
60
60
  type TextBlock = { type: "text"; text: string };
@@ -317,7 +317,11 @@ export class UiHelpers {
317
317
  const previous = waitingPoll;
318
318
  if (!previous) return;
319
319
  waitingPoll = null;
320
- if (nextToolName === "job" && previous.isDisplaceableBlock()) {
320
+ if (
321
+ nextToolName === "job" &&
322
+ previous.isDisplaceableBlock() &&
323
+ this.ctx.chatContainer.isBlockUncommitted(previous)
324
+ ) {
321
325
  this.ctx.chatContainer.removeChild(previous);
322
326
  }
323
327
  // Sealing freezes the block and stops the waiting-poll spinner that
@@ -334,7 +338,9 @@ export class UiHelpers {
334
338
  }
335
339
  if (previous.canBeDisplacedBy(nextToolName)) {
336
340
  todoSnapshot = null;
337
- this.ctx.chatContainer.removeChild(previous);
341
+ if (this.ctx.chatContainer.isBlockUncommitted(previous)) {
342
+ this.ctx.chatContainer.removeChild(previous);
343
+ }
338
344
  previous.seal();
339
345
  return;
340
346
  }
@@ -372,10 +378,9 @@ export class UiHelpers {
372
378
  readGroup?.seal();
373
379
  readGroup = null;
374
380
  }
375
- const { hasErrorStop, errorMessage } = resolveAssistantErrorMessage(
376
- message,
377
- this.ctx.viewSession.retryAttempt,
378
- );
381
+ const errorPresentation = resolveAssistantErrorPresentation(message, this.ctx.viewSession.retryAttempt);
382
+ const hasErrorStop = errorPresentation.kind === "full";
383
+ const errorMessage = hasErrorStop ? errorPresentation.text : null;
379
384
 
380
385
  // Render tool call components
381
386
  for (const content of message.content) {
@@ -67,7 +67,7 @@ For `.sqlite`, `.sqlite3`, `.db`, `.db3`:
67
67
 
68
68
  # Internal URIs
69
69
 
70
- All URI schemes take the same line selectors. `artifact://<id>` recovers full output a bash/eval/tool result spilled or truncated.
70
+ All URI schemes take the same line selectors. `artifact://<id>` recovers spilled output; large artifacts block unbounded `:raw`, so page with `artifact://<id>:N-M` / `artifact://<id>:raw:N-M` and use the reported artifact file path for search/copy workflows.
71
71
 
72
72
  `ssh://host/<absolute-path>` reads a remote text file (UTF-8, ≤1 MiB) or lists a directory one level deep, on a pre-configured SSH host or `~/.ssh/config` alias; `ssh://host/` lists the remote root and bare `ssh://` lists the configured hosts. Files are also writable via `write` and searchable via `search`; a directory only lists (`search` refuses a directory, `write` refuses to overwrite one). A literal `:`, `?`, or `#` in the remote path must be percent-encoded (`%3A`/`%3F`/`%23`) — a trailing `:sel` is read as a line selector, and `?`/`#` start a URL query/fragment. Requires a POSIX login shell (`sh`/`bash`/`zsh`); a Windows host or a non-POSIX shell (fish, csh/tcsh) is rejected — use the `ssh` tool there.
73
73
 
package/src/sdk.ts CHANGED
@@ -993,6 +993,7 @@ function createCustomToolsExtension(tools: CustomTool[]): ExtensionFactory {
993
993
  success: event.success,
994
994
  attempt: event.attempt,
995
995
  finalError: event.finalError,
996
+ recoveredErrors: event.recoveredErrors,
996
997
  },
997
998
  ctx,
998
999
  ),
@@ -81,6 +81,8 @@ import type { ProtectedToolMatcher } from "@oh-my-pi/pi-agent-core/compaction/to
81
81
  import type {
82
82
  AssistantMessage,
83
83
  AssistantMessageEvent,
84
+ AssistantRetryRecovery,
85
+ AssistantRetryRecoveryKind,
84
86
  Context,
85
87
  ImageContent,
86
88
  Message,
@@ -219,6 +221,7 @@ import { createExtensionModelQuery } from "../extensibility/extensions/model-api
219
221
  import type { CompactOptions, ContextUsage } from "../extensibility/extensions/types";
220
222
  import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
221
223
  import type { HookCommandContext } from "../extensibility/hooks/types";
224
+ import type { RecoveredRetryError } from "../extensibility/shared-events";
222
225
  import type { Skill, SkillWarning } from "../extensibility/skills";
223
226
  import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
224
227
  import { GoalRuntime } from "../goals/runtime";
@@ -497,7 +500,13 @@ export type AgentSessionEvent =
497
500
  errorMessage: string;
498
501
  errorId?: number;
499
502
  }
500
- | { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string }
503
+ | {
504
+ type: "auto_retry_end";
505
+ success: boolean;
506
+ attempt: number;
507
+ finalError?: string;
508
+ recoveredErrors?: RecoveredRetryError[];
509
+ }
501
510
  | { type: "retry_fallback_applied"; from: string; to: string; role: string }
502
511
  | { type: "retry_fallback_succeeded"; model: string; role: string }
503
512
  | { type: "ttsr_triggered"; rules: Rule[] }
@@ -1425,6 +1434,13 @@ type MessageEndPersistenceSlot = {
1425
1434
  persist: (persistMessage: () => void) => Promise<void>;
1426
1435
  release: () => void;
1427
1436
  };
1437
+ type PendingRecoveredRetryError = {
1438
+ entryId: string;
1439
+ persistenceKey: string;
1440
+ recovery: AssistantRetryRecoveryKind;
1441
+ attempt: number;
1442
+ note: string;
1443
+ };
1428
1444
 
1429
1445
  type PostPromptSkipReason = "aborted" | "stale-generation";
1430
1446
 
@@ -1581,6 +1597,7 @@ export class AgentSession {
1581
1597
  #retryPromise: Promise<void> | undefined = undefined;
1582
1598
  #retryResolve: (() => void) | undefined = undefined;
1583
1599
  #activeRetryFallback: ActiveRetryFallbackState | undefined = undefined;
1600
+ #pendingRecoveredRetryErrors: PendingRecoveredRetryError[] = [];
1584
1601
  // Todo completion reminder state
1585
1602
  #todoReminderCount = 0;
1586
1603
  /**
@@ -3663,11 +3680,14 @@ export class AgentSession {
3663
3680
  role: this.#activeRetryFallback.role,
3664
3681
  });
3665
3682
  }
3683
+ const recoveredErrors = await this.#markPendingRecoveredRetryErrors(assistantMsg);
3666
3684
  await this.#emitSessionEvent({
3667
3685
  type: "auto_retry_end",
3668
3686
  success: true,
3669
3687
  attempt: this.#retryAttempt,
3688
+ recoveredErrors,
3670
3689
  });
3690
+ this.#clearPendingRecoveredRetryErrors();
3671
3691
  this.#retryAttempt = 0;
3672
3692
  }
3673
3693
  if (assistantMsg.provider === "opencode-go") {
@@ -5337,6 +5357,7 @@ export class AgentSession {
5337
5357
  success: event.success,
5338
5358
  attempt: event.attempt,
5339
5359
  finalError: event.finalError,
5360
+ recoveredErrors: event.recoveredErrors,
5340
5361
  });
5341
5362
  } else if (event.type === "ttsr_triggered") {
5342
5363
  await this.#extensionRunner.emit({ type: "ttsr_triggered", rules: event.rules });
@@ -10386,6 +10407,126 @@ export class AgentSession {
10386
10407
  return undefined;
10387
10408
  }
10388
10409
 
10410
+ #clearPendingRecoveredRetryErrors(): void {
10411
+ this.#pendingRecoveredRetryErrors = [];
10412
+ }
10413
+
10414
+ #retryRecoveryKind(
10415
+ id: number,
10416
+ switchedCredential: boolean,
10417
+ switchedModel: boolean,
10418
+ delayMs: number,
10419
+ ): AssistantRetryRecoveryKind {
10420
+ if (switchedCredential) return "credential";
10421
+ if (switchedModel) return "model";
10422
+ if (AIError.is(id, AIError.Flag.UsageLimit) && delayMs > 0) return "wait";
10423
+ return "plain";
10424
+ }
10425
+
10426
+ #retryRecoveryNote(recovery: AssistantRetryRecoveryKind, rateLimited: boolean): string {
10427
+ const parts: string[] = [];
10428
+ if (rateLimited) {
10429
+ parts.push("rate-limited");
10430
+ } else if (recovery === "plain") {
10431
+ parts.push("error");
10432
+ }
10433
+ if (recovery === "credential") {
10434
+ parts.push("switched account");
10435
+ } else if (recovery === "model") {
10436
+ parts.push("switched model");
10437
+ } else if (recovery === "wait") {
10438
+ parts.push("waited");
10439
+ }
10440
+ parts.push("retried");
10441
+ return parts.join("; ");
10442
+ }
10443
+
10444
+ async #recordPendingRecoveredRetryError(
10445
+ message: AssistantMessage,
10446
+ id: number,
10447
+ options: { switchedCredential: boolean; switchedModel: boolean; delayMs: number },
10448
+ ): Promise<void> {
10449
+ await this.#waitForSessionMessagePersistence(message);
10450
+ const persistenceKey = sessionMessagePersistenceKey(message);
10451
+ if (!persistenceKey) return;
10452
+ let branchEntry: SessionEntry | undefined;
10453
+ for (const entry of this.sessionManager.getBranch().slice().reverse()) {
10454
+ if (entry.type !== "message" || entry.message.role !== "assistant") continue;
10455
+ if (sessionMessagePersistenceKey(entry.message) !== persistenceKey) continue;
10456
+ if (!sameMessageContent(entry.message, message) && !this.#isSameAssistantMessage(entry.message, message)) {
10457
+ continue;
10458
+ }
10459
+ branchEntry = entry;
10460
+ break;
10461
+ }
10462
+ if (!branchEntry) return;
10463
+ if (this.#pendingRecoveredRetryErrors.some(error => error.entryId === branchEntry.id)) return;
10464
+ const rateLimited = AIError.is(id, AIError.Flag.UsageLimit);
10465
+ const recovery = this.#retryRecoveryKind(id, options.switchedCredential, options.switchedModel, options.delayMs);
10466
+ const note = this.#retryRecoveryNote(recovery, rateLimited);
10467
+ this.#pendingRecoveredRetryErrors.push({
10468
+ entryId: branchEntry.id,
10469
+ persistenceKey,
10470
+ recovery,
10471
+ attempt: this.#retryAttempt,
10472
+ note,
10473
+ });
10474
+ }
10475
+
10476
+ async #markPendingRecoveredRetryErrors(supersedingMessage: AssistantMessage): Promise<RecoveredRetryError[]> {
10477
+ if (this.#pendingRecoveredRetryErrors.length === 0) return [];
10478
+ const branch = this.sessionManager.getBranch();
10479
+ const branchById = new Map<string, SessionEntry>();
10480
+ for (const entry of branch) {
10481
+ branchById.set(entry.id, entry);
10482
+ }
10483
+ const recoveredAt = new Date().toISOString();
10484
+ const supersededBy: AssistantRetryRecovery["supersededBy"] = {
10485
+ timestamp: supersedingMessage.timestamp,
10486
+ provider: supersedingMessage.provider,
10487
+ model: supersedingMessage.model,
10488
+ };
10489
+ if (supersedingMessage.responseId) {
10490
+ supersededBy.responseId = supersedingMessage.responseId;
10491
+ }
10492
+ const recoveredErrors: RecoveredRetryError[] = [];
10493
+ for (const pending of this.#pendingRecoveredRetryErrors) {
10494
+ let entry = branchById.get(pending.entryId);
10495
+ if (entry?.type !== "message" || entry.message.role !== "assistant") {
10496
+ entry = branch
10497
+ .slice()
10498
+ .reverse()
10499
+ .find(
10500
+ candidate =>
10501
+ candidate.type === "message" &&
10502
+ candidate.message.role === "assistant" &&
10503
+ sessionMessagePersistenceKey(candidate.message) === pending.persistenceKey,
10504
+ );
10505
+ }
10506
+ if (entry?.type !== "message" || entry.message.role !== "assistant") continue;
10507
+ const retryRecovery: AssistantRetryRecovery = {
10508
+ kind: "auto-retry",
10509
+ status: "recovered",
10510
+ attempt: pending.attempt,
10511
+ recoveredAt,
10512
+ recovery: pending.recovery,
10513
+ note: pending.note,
10514
+ supersededBy,
10515
+ };
10516
+ entry.message.retryRecovery = retryRecovery;
10517
+ recoveredErrors.push({
10518
+ entryId: entry.id,
10519
+ persistenceKey: pending.persistenceKey,
10520
+ note: retryRecovery.note,
10521
+ retryRecovery,
10522
+ });
10523
+ }
10524
+ if (recoveredErrors.length > 0) {
10525
+ await this.sessionManager.rewriteEntries();
10526
+ }
10527
+ return recoveredErrors;
10528
+ }
10529
+
10389
10530
  async #handleEmptyAssistantStop(assistantMessage: AssistantMessage): Promise<boolean> {
10390
10531
  if (!this.#isEmptyAssistantStop(assistantMessage)) {
10391
10532
  this.#emptyStopRetryCount = 0;
@@ -10406,6 +10547,7 @@ export class AgentSession {
10406
10547
  attempt: this.#retryAttempt,
10407
10548
  finalError: "Assistant returned empty stop after retry cap",
10408
10549
  });
10550
+ this.#clearPendingRecoveredRetryErrors();
10409
10551
  this.#retryAttempt = 0;
10410
10552
  }
10411
10553
  this.#resolveRetry();
@@ -13265,6 +13407,7 @@ export class AgentSession {
13265
13407
  attempt: this.#retryAttempt - 1,
13266
13408
  finalError: message.errorMessage,
13267
13409
  });
13410
+ this.#clearPendingRecoveredRetryErrors();
13268
13411
  this.#retryAttempt = 0;
13269
13412
  this.#resolveRetry(); // Resolve so waitForRetry() completes
13270
13413
  return false;
@@ -13384,10 +13527,13 @@ export class AgentSession {
13384
13527
  attempt,
13385
13528
  finalError: `Provider requested ${delayMs}ms wait, exceeds retry.maxDelayMs (${maxDelayMs}ms). Original error: ${errorMessage}`,
13386
13529
  });
13530
+ this.#clearPendingRecoveredRetryErrors();
13387
13531
  this.#resolveRetry();
13388
13532
  return false;
13389
13533
  }
13390
13534
 
13535
+ await this.#recordPendingRecoveredRetryError(message, id, { switchedCredential, switchedModel, delayMs });
13536
+
13391
13537
  await this.#emitSessionEvent({
13392
13538
  type: "auto_retry_start",
13393
13539
  attempt: this.#retryAttempt,
@@ -13425,6 +13571,7 @@ export class AgentSession {
13425
13571
  attempt,
13426
13572
  finalError: "Retry cancelled",
13427
13573
  });
13574
+ this.#clearPendingRecoveredRetryErrors();
13428
13575
  this.#resolveRetry();
13429
13576
  return false;
13430
13577
  }
@@ -251,6 +251,13 @@ export function buildSessionContext(
251
251
  const appendMessage = (entry: SessionEntry) => {
252
252
  handleEntryResetTracking(entry);
253
253
  if (entry.type === "message") {
254
+ if (
255
+ !options?.transcript &&
256
+ entry.message.role === "assistant" &&
257
+ entry.message.retryRecovery?.status === "recovered"
258
+ ) {
259
+ return;
260
+ }
254
261
  pushMessage(entry.message);
255
262
  } else if (entry.type === "custom_message") {
256
263
  pushMessage(
@@ -1,3 +1,4 @@
1
+ import * as fsp from "node:fs/promises";
1
2
  import { createRequire } from "node:module";
2
3
  import * as path from "node:path";
3
4
  import type { ProgressInfo } from "@huggingface/transformers";
@@ -27,6 +28,15 @@ import packageJson from "../../package.json" with { type: "json" };
27
28
 
28
29
  export const TRANSFORMERS_PACKAGE = "@huggingface/transformers";
29
30
  const COMPILED_TRANSFORMERS_VERSION = process.env.PI_TINY_TRANSFORMERS_VERSION;
31
+ const ONNX_RUNTIME_NODE_PACKAGE = "onnxruntime-node";
32
+ const ONNX_RUNTIME_CUDA_INSTALL = "cuda12";
33
+ const ONNX_RUNTIME_CUDA_PROVIDER_FILES = [
34
+ "libonnxruntime_providers_cuda.so",
35
+ "libonnxruntime_providers_shared.so",
36
+ "libonnxruntime_providers_tensorrt.so",
37
+ ] as const;
38
+ const LINUX_X64_ONNX_RUNTIME_CUDA_PROVIDER_DIR = path.join("bin", "napi-v6", "linux", "x64");
39
+
30
40
  const sourceRequire = createRequire(import.meta.url);
31
41
 
32
42
  // ── Error serialization ─────────────────────────────────────────────
@@ -162,6 +172,87 @@ export async function installSharpStubResolver(runtimeDir: string): Promise<stri
162
172
  return nodeModules;
163
173
  }
164
174
 
175
+ function shouldInstallOnnxRuntimeCudaProviders(device: string | undefined): boolean {
176
+ const normalized = device?.trim().toLowerCase();
177
+ return (
178
+ process.platform === "linux" &&
179
+ process.arch === "x64" &&
180
+ (normalized === "cuda" || normalized === "gpu" || normalized === "auto")
181
+ );
182
+ }
183
+
184
+ async function missingOnnxRuntimeCudaProviderFiles(binDir: string): Promise<string[]> {
185
+ const missing: string[] = [];
186
+ for (const file of ONNX_RUNTIME_CUDA_PROVIDER_FILES) {
187
+ try {
188
+ await fsp.access(path.join(binDir, file));
189
+ } catch {
190
+ missing.push(file);
191
+ }
192
+ }
193
+ return missing;
194
+ }
195
+
196
+ async function readPipe(stream: ReadableStream<Uint8Array> | null): Promise<string> {
197
+ if (!stream) return "";
198
+ return new Response(stream).text();
199
+ }
200
+
201
+ async function installOnnxRuntimeCudaProviders(packageDir: string, runtimeDir: string, binDir: string): Promise<void> {
202
+ const script = path.join(packageDir, "script", "install.js");
203
+ try {
204
+ await fsp.access(script);
205
+ } catch {
206
+ throw new Error(
207
+ `ONNX Runtime CUDA provider binaries are missing from ${binDir}, and ${script} is unavailable. Remove the tiny-model side runtime cache at ${runtimeDir} and retry.`,
208
+ );
209
+ }
210
+
211
+ const proc = Bun.spawn([process.execPath, script], {
212
+ cwd: runtimeDir,
213
+ env: { ...Bun.env, BUN_BE_BUN: "1", ONNXRUNTIME_NODE_INSTALL: ONNX_RUNTIME_CUDA_INSTALL },
214
+ stdout: "pipe",
215
+ stderr: "pipe",
216
+ });
217
+ const [stdout, stderr, exitCode] = await Promise.all([
218
+ readPipe(proc.stdout as ReadableStream<Uint8Array> | null),
219
+ readPipe(proc.stderr as ReadableStream<Uint8Array> | null),
220
+ proc.exited,
221
+ ]);
222
+ if (exitCode !== 0) {
223
+ const output = `${stdout}\n${stderr}`.trim();
224
+ throw new Error(
225
+ `Failed to install ONNX Runtime CUDA provider binaries into ${binDir} with ${process.execPath} ${script} (exit ${exitCode}). Remove the tiny-model side runtime cache at ${runtimeDir} and retry with network access. ${output}`,
226
+ );
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Repairs the compiled Transformers side runtime when CUDA was requested and
232
+ * Bun skipped `onnxruntime-node`'s NuGet sidecar install.
233
+ */
234
+ export async function ensureOnnxRuntimeCudaProviders(
235
+ runtimeDir: string,
236
+ device = process.env.PI_TINY_DEVICE,
237
+ ): Promise<void> {
238
+ if (!shouldInstallOnnxRuntimeCudaProviders(device)) return;
239
+ const nodeModules = path.join(runtimeDir, "node_modules");
240
+ const manifest = resolveRuntimeModule(nodeModules, `${ONNX_RUNTIME_NODE_PACKAGE}/package.json`);
241
+ if (!manifest)
242
+ throw new Error(`Unable to resolve ${ONNX_RUNTIME_NODE_PACKAGE} in compiled runtime at ${nodeModules}`);
243
+ const packageDir = path.dirname(manifest);
244
+ const binDir = path.join(packageDir, LINUX_X64_ONNX_RUNTIME_CUDA_PROVIDER_DIR);
245
+ const missing = await missingOnnxRuntimeCudaProviderFiles(binDir);
246
+ if (missing.length === 0) return;
247
+
248
+ await installOnnxRuntimeCudaProviders(packageDir, runtimeDir, binDir);
249
+ const stillMissing = await missingOnnxRuntimeCudaProviderFiles(binDir);
250
+ if (stillMissing.length === 0) return;
251
+ throw new Error(
252
+ `ONNX Runtime CUDA provider install completed but ${stillMissing.join(", ")} are still missing from ${binDir}. Remove the tiny-model side runtime cache at ${runtimeDir} and retry.`,
253
+ );
254
+ }
255
+
165
256
  /**
166
257
  * Prepare a freshly-installed compiled runtime for loading and return the
167
258
  * absolute entrypoint of `packageName` to `require`.
@@ -211,6 +302,114 @@ interface ConfigurableTransformers {
211
302
  LogLevel: { ERROR: unknown };
212
303
  }
213
304
 
305
+ export interface TransformersRuntimeMetadata {
306
+ __ompRuntimeNodeModules?: string;
307
+ __ompTransformersEntry?: string;
308
+ __ompCudaRepairError?: string;
309
+ }
310
+
311
+ function attachTransformersRuntimeMetadata<T extends ConfigurableTransformers>(
312
+ transformers: T,
313
+ metadata: TransformersRuntimeMetadata,
314
+ ): T {
315
+ const runtime = transformers as T & TransformersRuntimeMetadata;
316
+ runtime.__ompRuntimeNodeModules = metadata.__ompRuntimeNodeModules;
317
+ runtime.__ompTransformersEntry = metadata.__ompTransformersEntry;
318
+ runtime.__ompCudaRepairError = metadata.__ompCudaRepairError;
319
+ return runtime;
320
+ }
321
+
322
+ const TRANSITIVE_CUDA_LIBRARY_RE =
323
+ /\b(lib(?:cu|nv)[A-Za-z0-9_.+-]*\.so(?:\.[0-9]+)*)\b[^:\n]*:\s*cannot open shared object file/iu;
324
+ const CUDA_DEVICE_UNAVAILABLE_RE = /\bCUDA failure 100\b|no CUDA-capable device is detected|cudaSetDevice|GPU=-1/iu;
325
+
326
+ function cudaDeviceUnavailable(error: unknown): boolean {
327
+ return CUDA_DEVICE_UNAVAILABLE_RE.test(errorText(error));
328
+ }
329
+
330
+ function missingCudaLibrary(error: unknown): string | undefined {
331
+ return TRANSITIVE_CUDA_LIBRARY_RE.exec(errorText(error))?.[1];
332
+ }
333
+
334
+ function cudaFailureCause(
335
+ metadata: TransformersRuntimeMetadata,
336
+ error: unknown,
337
+ missingFiles: readonly string[],
338
+ ): string {
339
+ if (metadata.__ompCudaRepairError) {
340
+ return `ONNX Runtime CUDA provider install failed: ${metadata.__ompCudaRepairError}`;
341
+ }
342
+ if (missingFiles.length > 0) return `missing ONNX Runtime CUDA provider file(s): ${missingFiles.join(", ")}`;
343
+ const missingLibrary = missingCudaLibrary(error);
344
+ if (missingLibrary) return `${missingLibrary}: cannot open shared object file`;
345
+ if (cudaDeviceUnavailable(error)) {
346
+ return "CUDA provider files are present; CUDA runtime reports no CUDA-capable device";
347
+ }
348
+ return "CUDA provider files are present; inspect the original ONNX Runtime CUDA error";
349
+ }
350
+
351
+ function cudaFailureHint(
352
+ metadata: TransformersRuntimeMetadata,
353
+ error: unknown,
354
+ missingFiles: readonly string[],
355
+ ): string {
356
+ if (metadata.__ompCudaRepairError) {
357
+ return "restore network access to nuget.org (or pre-populate the tiny side runtime) and rerun; CPU inference remained available";
358
+ }
359
+ if (missingFiles.length > 0) return "reinstall the tiny side runtime with ONNX Runtime postinstall enabled";
360
+ if (missingCudaLibrary(error)) {
361
+ return "install the matching CUDA/cuDNN shared libraries and expose them on the dynamic loader path";
362
+ }
363
+ if (cudaDeviceUnavailable(error)) {
364
+ return "make the NVIDIA GPU visible to this process/session, or use providers.tinyModelDevice=default/cpu";
365
+ }
366
+ return "check the host CUDA driver, device visibility, and ONNX Runtime CUDA compatibility";
367
+ }
368
+
369
+ function resolveOnnxRuntimePackageDir(metadata: TransformersRuntimeMetadata): string | null {
370
+ const entry = metadata.__ompTransformersEntry;
371
+ if (entry) {
372
+ try {
373
+ return path.dirname(createRequire(entry).resolve(`${ONNX_RUNTIME_NODE_PACKAGE}/package.json`));
374
+ } catch {
375
+ // Fall through to the side-runtime resolver below.
376
+ }
377
+ }
378
+ const nodeModules = metadata.__ompRuntimeNodeModules;
379
+ if (!nodeModules) return null;
380
+ const manifest = resolveRuntimeModule(nodeModules, `${ONNX_RUNTIME_NODE_PACKAGE}/package.json`);
381
+ return manifest ? path.dirname(manifest) : null;
382
+ }
383
+
384
+ export async function formatOnnxRuntimeCudaDiagnostics(
385
+ metadata: TransformersRuntimeMetadata,
386
+ requestedDevice: string,
387
+ error: unknown,
388
+ ): Promise<string | null> {
389
+ const device = requestedDevice.trim().toLowerCase();
390
+ if (device !== "cuda" && device !== "gpu" && device !== "auto") return null;
391
+ if (process.platform !== "linux" || process.arch !== "x64") return null;
392
+ const packageDir = resolveOnnxRuntimePackageDir(metadata);
393
+ if (!packageDir) {
394
+ return [
395
+ "ONNX Runtime CUDA diagnostics:",
396
+ ` PI_TINY_DEVICE=${requestedDevice} requested CUDAExecutionProvider`,
397
+ " cause: unable to resolve onnxruntime-node in the tiny-model runtime",
398
+ ].join("\n");
399
+ }
400
+ const binDir = path.join(packageDir, LINUX_X64_ONNX_RUNTIME_CUDA_PROVIDER_DIR);
401
+ const missingFiles = await missingOnnxRuntimeCudaProviderFiles(binDir);
402
+ const sideRuntime = metadata.__ompRuntimeNodeModules;
403
+ const lines = [
404
+ "ONNX Runtime CUDA diagnostics:",
405
+ ` PI_TINY_DEVICE=${requestedDevice} requested CUDAExecutionProvider`,
406
+ sideRuntime ? ` side runtime: ${sideRuntime}` : ` onnxruntime-node: ${packageDir}`,
407
+ ` cause: ${cudaFailureCause(metadata, error, missingFiles)}`,
408
+ ];
409
+ lines.push(` hint: ${cudaFailureHint(metadata, error, missingFiles)}`);
410
+ return lines.join("\n");
411
+ }
412
+
214
413
  function configureTransformers<T extends ConfigurableTransformers>(transformers: T): T {
215
414
  transformers.env.cacheDir = getTinyModelsCacheDir();
216
415
  transformers.env.allowLocalModels = false;
@@ -251,7 +450,12 @@ export function loadTransformersRuntime<T extends ConfigurableTransformers, K>(
251
450
  runtimeDir: () => string,
252
451
  ): Promise<T> {
253
452
  return holder.load(async () => {
254
- if (!isCompiledBinary()) return configureTransformers(sourceRequire(TRANSFORMERS_PACKAGE) as T);
453
+ if (!isCompiledBinary()) {
454
+ const entry = sourceRequire.resolve(TRANSFORMERS_PACKAGE);
455
+ return attachTransformersRuntimeMetadata(configureTransformers(sourceRequire(entry) as T), {
456
+ __ompTransformersEntry: entry,
457
+ });
458
+ }
255
459
  const installedDir = await ensureRuntimeInstalled({
256
460
  runtimeDir: runtimeDir(),
257
461
  install: {
@@ -270,8 +474,21 @@ export function loadTransformersRuntime<T extends ConfigurableTransformers, K>(
270
474
  },
271
475
  }),
272
476
  });
477
+ let cudaRepairError: string | undefined;
478
+ try {
479
+ await ensureOnnxRuntimeCudaProviders(installedDir);
480
+ } catch (repairError) {
481
+ // Deferred failure: keep loading Transformers so `loadPipelineWithDeviceFallback`
482
+ // still gets its CUDA→CPU retry. The error is surfaced through the CUDA
483
+ // diagnostics attached to the runtime metadata.
484
+ cudaRepairError = errorMessage(repairError);
485
+ }
273
486
  const entry = await prepareCompiledRuntime(installedDir, TRANSFORMERS_PACKAGE);
274
487
  const require_ = createRequire(entry);
275
- return configureTransformers(require_(entry) as T);
488
+ return attachTransformersRuntimeMetadata(configureTransformers(require_(entry) as T), {
489
+ __ompRuntimeNodeModules: path.join(installedDir, "node_modules"),
490
+ __ompTransformersEntry: entry,
491
+ __ompCudaRepairError: cudaRepairError,
492
+ });
276
493
  });
277
494
  }