@getpaseo/server 0.1.100 → 0.1.101

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 (67) hide show
  1. package/dist/server/executable-resolution/windows.js +3 -0
  2. package/dist/server/server/agent/agent-manager.d.ts +10 -0
  3. package/dist/server/server/agent/agent-manager.js +65 -27
  4. package/dist/server/server/agent/agent-sdk-types.d.ts +8 -0
  5. package/dist/server/server/agent/mcp-server.d.ts +2 -45
  6. package/dist/server/server/agent/mcp-server.js +45 -1985
  7. package/dist/server/server/agent/prompt-attachments.js +6 -2
  8. package/dist/server/server/agent/provider-snapshot-manager.d.ts +4 -0
  9. package/dist/server/server/agent/provider-snapshot-manager.js +58 -13
  10. package/dist/server/server/agent/providers/acp-agent.d.ts +20 -1
  11. package/dist/server/server/agent/providers/acp-agent.js +170 -26
  12. package/dist/server/server/agent/providers/claude/agent.js +60 -10
  13. package/dist/server/server/agent/providers/codex-app-server-agent.js +6 -57
  14. package/dist/server/server/agent/providers/diagnostic-utils.d.ts +1 -0
  15. package/dist/server/server/agent/providers/diagnostic-utils.js +1 -1
  16. package/dist/server/server/agent/providers/generic-acp-agent.d.ts +3 -0
  17. package/dist/server/server/agent/providers/generic-acp-agent.js +41 -23
  18. package/dist/server/server/agent/providers/mock-load-test-agent.js +4 -2
  19. package/dist/server/server/agent/providers/pi/agent.d.ts +2 -1
  20. package/dist/server/server/agent/providers/pi/agent.js +3 -0
  21. package/dist/server/server/agent/providers/provider-image-output.d.ts +5 -0
  22. package/dist/server/server/agent/providers/provider-image-output.js +55 -0
  23. package/dist/server/server/agent/tools/paseo-tools.d.ts +48 -0
  24. package/dist/server/server/agent/tools/paseo-tools.js +2121 -0
  25. package/dist/server/server/agent/tools/types.d.ts +36 -0
  26. package/dist/server/server/agent/tools/types.js +2 -0
  27. package/dist/server/server/bootstrap.js +71 -62
  28. package/dist/server/server/persisted-config.d.ts +5 -0
  29. package/dist/server/server/persisted-config.js +10 -2
  30. package/dist/server/server/session/agent-updates/agent-updates-service.d.ts +59 -0
  31. package/dist/server/server/session/agent-updates/agent-updates-service.js +220 -0
  32. package/dist/server/server/session/checkout/checkout-session.d.ts +13 -15
  33. package/dist/server/server/session/checkout/checkout-session.js +18 -16
  34. package/dist/server/server/session/checkout/git-metadata-generator.d.ts +53 -0
  35. package/dist/server/server/session/checkout/git-metadata-generator.js +159 -0
  36. package/dist/server/server/session/daemon/daemon-session.d.ts +14 -0
  37. package/dist/server/server/session/daemon/daemon-session.js +38 -0
  38. package/dist/server/server/session/daemon/diagnostics.d.ts +41 -0
  39. package/dist/server/server/session/daemon/diagnostics.js +421 -0
  40. package/dist/server/server/session/git-mutation/git-mutation-service.d.ts +34 -0
  41. package/dist/server/server/session/git-mutation/git-mutation-service.js +71 -0
  42. package/dist/server/server/session/workspace-git-observer/workspace-git-observer-service.d.ts +36 -0
  43. package/dist/server/server/session/workspace-git-observer/workspace-git-observer-service.js +134 -0
  44. package/dist/server/server/session/workspace-provisioning/workspace-provisioning-service.d.ts +34 -0
  45. package/dist/server/server/session/workspace-provisioning/workspace-provisioning-service.js +190 -0
  46. package/dist/server/server/session/workspace-scripts/workspace-scripts-service.d.ts +41 -0
  47. package/dist/server/server/session/workspace-scripts/workspace-scripts-service.js +100 -0
  48. package/dist/server/server/session.d.ts +7 -51
  49. package/dist/server/server/session.js +113 -938
  50. package/dist/server/server/speech/providers/openai/config.d.ts +1 -2
  51. package/dist/server/server/speech/providers/openai/config.js +13 -9
  52. package/dist/server/server/speech/providers/openai/runtime.js +2 -16
  53. package/dist/server/server/speech/providers/openai/stt.d.ts +1 -0
  54. package/dist/server/server/speech/providers/openai/stt.js +4 -2
  55. package/dist/server/server/speech/providers/openai/tts.d.ts +1 -0
  56. package/dist/server/server/speech/providers/openai/tts.js +1 -0
  57. package/dist/server/server/websocket/runtime-metrics.d.ts +20 -0
  58. package/dist/server/server/websocket-server.d.ts +1 -2
  59. package/dist/server/server/websocket-server.js +26 -21
  60. package/dist/server/server/worktree-bootstrap.d.ts +1 -1
  61. package/dist/server/server/worktree-branch-name-generator.js +3 -1
  62. package/dist/server/utils/checkout-git.js +51 -26
  63. package/dist/src/executable-resolution/windows.js +3 -0
  64. package/dist/src/server/persisted-config.js +10 -2
  65. package/package.json +5 -5
  66. package/dist/server/server/speech/providers/openai/realtime-transcription-session.d.ts +0 -42
  67. package/dist/server/server/speech/providers/openai/realtime-transcription-session.js +0 -168
@@ -67,14 +67,18 @@ export function buildAgentBranchNameSeed(firstAgentContext) {
67
67
  const parts = [];
68
68
  const prompt = firstAgentContext.prompt?.trim();
69
69
  if (prompt) {
70
- parts.push(prompt);
70
+ parts.push(["<user-prompt>", prompt, "</user-prompt>"].join("\n"));
71
71
  }
72
+ const renderedAttachments = [];
72
73
  for (const attachment of firstAgentContext.attachments ?? []) {
73
74
  const rendered = renderPromptAttachmentAsText(attachment).trim();
74
75
  if (rendered) {
75
- parts.push(rendered);
76
+ renderedAttachments.push(rendered);
76
77
  }
77
78
  }
79
+ if (renderedAttachments.length > 0) {
80
+ parts.push(["<attachments>", renderedAttachments.join("\n\n"), "</attachments>"].join("\n"));
81
+ }
78
82
  return parts.length > 0 ? parts.join("\n\n") : undefined;
79
83
  }
80
84
  //# sourceMappingURL=prompt-attachments.js.map
@@ -15,6 +15,7 @@ export interface ProviderSnapshotManagerOptions {
15
15
  isDev?: boolean;
16
16
  extraClients?: Partial<Record<AgentProvider, AgentClient>>;
17
17
  refreshTimeoutMs?: number;
18
+ diagnosticTimeoutMs?: number;
18
19
  }
19
20
  interface ProviderSnapshotRefreshOptions {
20
21
  cwd: string;
@@ -64,6 +65,7 @@ export declare class ProviderSnapshotManager {
64
65
  private readonly events;
65
66
  private destroyed;
66
67
  private readonly refreshTimeoutMs;
68
+ private readonly diagnosticTimeoutMs;
67
69
  private readonly logger;
68
70
  private readonly workspaceGitService?;
69
71
  private readonly managedProcesses?;
@@ -101,6 +103,8 @@ export declare class ProviderSnapshotManager {
101
103
  private resolveParent;
102
104
  private getReadyProvider;
103
105
  private requireProvider;
106
+ private refreshDiagnosticSnapshotEntry;
107
+ private getBaseProviderDiagnostic;
104
108
  private createLoadingEntries;
105
109
  private reconcileSnapshotForRegistry;
106
110
  private warmUp;
@@ -5,8 +5,9 @@ import { expandTilde } from "../../utils/path.js";
5
5
  import { withTimeout } from "../../utils/promise-timeout.js";
6
6
  import { buildProviderRegistry, shutdownAgentClients, } from "./provider-registry.js";
7
7
  import { applyMutableProviderConfigToOverrides } from "../daemon-config-store.js";
8
- import { formatProviderDiagnostic } from "./providers/diagnostic-utils.js";
9
- const DEFAULT_REFRESH_TIMEOUT_MS = 30000;
8
+ import { formatProviderDiagnostic, formatProviderDiagnosticError, } from "./providers/diagnostic-utils.js";
9
+ const DEFAULT_REFRESH_TIMEOUT_MS = 60000;
10
+ const DEFAULT_DIAGNOSTIC_TIMEOUT_MS = 120000;
10
11
  const REFRESH_TIMEOUT_ENV_VAR = "PASEO_PROVIDER_REFRESH_TIMEOUT_MS";
11
12
  // Provider refresh probes can be slow on cold starts (e.g. Copilot's first
12
13
  // `copilot --acp` invocation, OpenCode workspace probes with many MCP servers).
@@ -25,6 +26,12 @@ function resolveRefreshTimeoutMs(option) {
25
26
  }
26
27
  return DEFAULT_REFRESH_TIMEOUT_MS;
27
28
  }
29
+ function resolveDiagnosticTimeoutMs(option, refreshTimeoutMs) {
30
+ if (typeof option === "number" && Number.isFinite(option) && option > 0) {
31
+ return option;
32
+ }
33
+ return Math.max(refreshTimeoutMs, DEFAULT_DIAGNOSTIC_TIMEOUT_MS);
34
+ }
28
35
  export class ProviderSnapshotManager {
29
36
  constructor(options) {
30
37
  this.snapshots = new Map();
@@ -40,6 +47,7 @@ export class ProviderSnapshotManager {
40
47
  this.providerOverrides = options.providerOverrides;
41
48
  this.baseProviderOverrides = options.providerOverrides;
42
49
  this.refreshTimeoutMs = resolveRefreshTimeoutMs(options.refreshTimeoutMs);
50
+ this.diagnosticTimeoutMs = resolveDiagnosticTimeoutMs(options.diagnosticTimeoutMs, this.refreshTimeoutMs);
43
51
  this.providerRegistry = this.buildRegistry();
44
52
  this.providerClients = { ...this.extraClients };
45
53
  }
@@ -180,18 +188,23 @@ export class ProviderSnapshotManager {
180
188
  });
181
189
  }
182
190
  async getProviderDiagnostic(provider) {
183
- const definition = this.requireProvider(provider);
184
- const client = this.ensureClient(provider, definition);
185
- // Force-refresh the snapshot so Models/Status come from the single catalog authority.
186
- await this.refreshSnapshotForCwd({ cwd: homedir(), providers: [provider] });
187
- const entry = await this.getProvider({ cwd: homedir(), provider, wait: true });
191
+ const definition = this.providerRegistry[provider];
192
+ if (!definition) {
193
+ return {
194
+ provider,
195
+ diagnostic: formatProviderDiagnostic(provider, [
196
+ { label: "Error", value: `Provider ${provider} is not configured` },
197
+ ]),
198
+ };
199
+ }
200
+ const baseDiagnosticPromise = this.getBaseProviderDiagnostic(provider, definition);
201
+ const snapshotEntryPromise = this.refreshDiagnosticSnapshotEntry(provider, definition);
202
+ const [baseDiagnostic, entry] = await Promise.all([
203
+ baseDiagnosticPromise,
204
+ snapshotEntryPromise,
205
+ ]);
188
206
  const modelCount = entry.status === "ready" ? String(entry.models?.length ?? 0) : "—";
189
207
  const status = formatProviderStatus(entry);
190
- const baseDiagnostic = client.getDiagnostic
191
- ? (await client.getDiagnostic()).diagnostic
192
- : formatProviderDiagnostic(definition.label ?? provider, [
193
- { label: "Diagnostic", value: "No diagnostic available" },
194
- ]);
195
208
  const diagnostic = `${baseDiagnostic}\n Models: ${modelCount}\n Status: ${status}`;
196
209
  return { provider, diagnostic };
197
210
  }
@@ -283,6 +296,38 @@ export class ProviderSnapshotManager {
283
296
  }
284
297
  return definition;
285
298
  }
299
+ async refreshDiagnosticSnapshotEntry(provider, definition) {
300
+ try {
301
+ const cwd = resolveSnapshotCwd();
302
+ await this.refreshSnapshotForCwd({ cwd, providers: [provider] });
303
+ return await this.getProvider({ cwd, provider, wait: false });
304
+ }
305
+ catch (error) {
306
+ return {
307
+ provider,
308
+ status: "error",
309
+ enabled: definition.enabled,
310
+ label: definition.label,
311
+ description: definition.description,
312
+ defaultModeId: definition.defaultModeId,
313
+ error: toErrorMessage(error),
314
+ };
315
+ }
316
+ }
317
+ async getBaseProviderDiagnostic(provider, definition) {
318
+ try {
319
+ const client = this.ensureClient(provider, definition);
320
+ if (client.getDiagnostic) {
321
+ return (await withTimeout(client.getDiagnostic(), this.diagnosticTimeoutMs, `Timed out collecting ${definition.label ?? provider} diagnostic after ${this.diagnosticTimeoutMs}ms`)).diagnostic;
322
+ }
323
+ return formatProviderDiagnostic(definition.label ?? provider, [
324
+ { label: "Diagnostic", value: "No diagnostic available" },
325
+ ]);
326
+ }
327
+ catch (error) {
328
+ return formatProviderDiagnosticError(definition.label ?? provider, error);
329
+ }
330
+ }
286
331
  createLoadingEntries() {
287
332
  const entries = new Map();
288
333
  for (const provider of this.getProviderIds()) {
@@ -454,7 +499,7 @@ export class ProviderSnapshotManager {
454
499
  setEntry({ ...base, status: "unavailable", enabled: true });
455
500
  return;
456
501
  }
457
- const catalog = await withTimeout(definition.fetchCatalog({ cwd, force }, client), this.refreshTimeoutMs, `Timed out refreshing ${definition.label} after ${this.refreshTimeoutMs}ms`);
502
+ const catalog = await withTimeout(definition.fetchCatalog({ cwd, force, timeoutMs: this.refreshTimeoutMs }, client), this.refreshTimeoutMs, `Timed out refreshing ${definition.label} after ${this.refreshTimeoutMs}ms`);
458
503
  setEntry({
459
504
  ...base,
460
505
  status: "ready",
@@ -5,6 +5,7 @@ import { ClientSideConnection, type Client as ACPClient, type CreateTerminalRequ
5
5
  import type { Logger } from "pino";
6
6
  import { type AgentCapabilityFlags, type AgentClient, type AgentFeature, type AgentLaunchContext, type AgentMode, type AgentModelDefinition, type AgentPermissionRequest, type AgentPermissionResponse, type AgentPersistenceHandle, type AgentPromptInput, type AgentRunOptions, type AgentRunResult, type AgentRuntimeInfo, type AgentSession, type AgentSessionConfig, type AgentSlashCommand, type AgentStreamEvent, type AgentUsage, type FetchCatalogOptions, type ImportableProviderSession, type ImportProviderSessionContext, type ImportProviderSessionInput, type ListImportableSessionsOptions, type ProviderCatalog } from "../agent-sdk-types.js";
7
7
  import { type ProviderRuntimeSettings } from "../provider-launch-config.js";
8
+ import { type DiagnosticEntry } from "./diagnostic-utils.js";
8
9
  export declare function summarizeACPRequestError(error: unknown): {
9
10
  message: string;
10
11
  code?: string;
@@ -62,6 +63,17 @@ export interface SpawnedACPProcess {
62
63
  child: ChildProcessWithoutNullStreams;
63
64
  connection: ClientSideConnection;
64
65
  initialize: InitializeResponse;
66
+ stderrChunks?: string[];
67
+ }
68
+ type UninitializedACPProcess = Omit<SpawnedACPProcess, "initialize"> & {
69
+ initialize?: InitializeResponse;
70
+ };
71
+ interface ACPProcessTransport {
72
+ child: ChildProcessWithoutNullStreams;
73
+ connection: ClientSideConnection;
74
+ stderrChunks: string[];
75
+ spawnReady: Promise<void>;
76
+ spawnError: Promise<never>;
65
77
  }
66
78
  export interface ACPToolSnapshot {
67
79
  toolCallId: string;
@@ -176,10 +188,17 @@ export declare class ACPAgentClient implements AgentClient {
176
188
  isAvailable(): Promise<boolean>;
177
189
  protected spawnProcess(launchEnv?: Record<string, string>, options?: {
178
190
  initializeTimeoutMs?: number;
191
+ onSpawned?: (probe: UninitializedACPProcess) => void;
179
192
  }): Promise<SpawnedACPProcess>;
193
+ protected spawnTransport(launchEnv?: Record<string, string>): Promise<ACPProcessTransport>;
194
+ protected initializeTransport(transport: ACPProcessTransport, initializeTimeoutMs?: number): Promise<InitializeResponse>;
180
195
  protected buildProbeClient(): ACPClient;
181
- protected closeProbe(probe: SpawnedACPProcess): Promise<void>;
196
+ protected closeProbe(probe: UninitializedACPProcess): Promise<void>;
182
197
  protected runACPRequest<T>(request: () => Promise<T>): Promise<T>;
198
+ protected buildACPProbeDiagnosticRows(options?: {
199
+ cwd?: string;
200
+ phaseTimeoutMs?: number;
201
+ }): Promise<DiagnosticEntry[]>;
183
202
  protected resolveLaunchCommand(): Promise<{
184
203
  command: string;
185
204
  args: string[];
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
+ import { homedir } from "node:os";
3
4
  import path from "node:path";
4
5
  import { Readable, Writable } from "node:stream";
5
6
  import { terminateWithTreeKill } from "../../../utils/tree-kill.js";
@@ -10,6 +11,8 @@ import { checkProviderLaunchAvailable, createProviderEnvSpec, resolveProviderLau
10
11
  import { renderPromptAttachmentAsText } from "../prompt-attachments.js";
11
12
  import { appendOrReplaceGrowingAssistantMessage, runProviderTurn } from "./provider-runner.js";
12
13
  import { platformShell, spawnProcess } from "../../../utils/spawn.js";
14
+ import { toDiagnosticErrorMessage, truncateForDiagnostic, } from "./diagnostic-utils.js";
15
+ import { withTimeout } from "../../../utils/promise-timeout.js";
13
16
  function assertChildWithPipes(child) {
14
17
  if (!child.stdin || !child.stdout || !child.stderr) {
15
18
  throw new Error("Child process did not expose stdio pipes");
@@ -70,6 +73,19 @@ function resolveTerminalCommand(command, args) {
70
73
  const shell = platformShell();
71
74
  return { command: shell.command, args: [...shell.flag, command] };
72
75
  }
76
+ function formatDurationMs(startedAt) {
77
+ return `${Math.max(0, Date.now() - startedAt)}ms`;
78
+ }
79
+ function pushACPStderrRow(rows, stderrChunks) {
80
+ const stderr = stderrChunks.join("").trim();
81
+ if (!stderr) {
82
+ return;
83
+ }
84
+ rows.push({
85
+ label: "ACP stderr",
86
+ value: truncateForDiagnostic(stderr),
87
+ });
88
+ }
73
89
  export const DEFAULT_ACP_CAPABILITIES = {
74
90
  supportsStreaming: true,
75
91
  supportsSessionPersistence: true,
@@ -96,6 +112,8 @@ const ACP_CLIENT_CAPABILITIES = {
96
112
  // sign-in URL in the browser) when probing an ACP agent for models/modes.
97
113
  // NO_BROWSER is honored by Gemini CLI; other ACP agents ignore it.
98
114
  const PROBE_ENV = { NO_BROWSER: "true" };
115
+ const ACP_CATALOG_TIMEOUT_MS = 60000;
116
+ const ACP_DIAGNOSTIC_PHASE_TIMEOUT_MS = 20000;
99
117
  function summarizeMalformedACPStdoutError(error) {
100
118
  return {
101
119
  type: error instanceof Error ? error.name : typeof error,
@@ -369,22 +387,35 @@ export class ACPAgentClient {
369
387
  }
370
388
  async fetchCatalog(options) {
371
389
  const { cwd } = options;
372
- const probe = await this.spawnProcess(PROBE_ENV);
390
+ const timeoutMs = options.timeoutMs ?? ACP_CATALOG_TIMEOUT_MS;
391
+ let probe = null;
373
392
  try {
374
- const response = await this.runACPRequest(() => probe.connection.newSession({
375
- cwd,
376
- mcpServers: [],
377
- }));
378
- const transformed = this.transformSessionResponse(response);
379
- const models = deriveModelDefinitionsFromACP(this.provider, transformed.models, transformed.configOptions);
380
- const modeInfo = deriveModesFromACP(this.defaultModes, transformed.modes, transformed.configOptions);
381
- return {
382
- models: this.modelTransformer ? this.modelTransformer(models) : models,
383
- modes: modeInfo.modes,
384
- };
393
+ const catalogProbe = (async () => {
394
+ const initializedProbe = await this.spawnProcess(PROBE_ENV, {
395
+ initializeTimeoutMs: timeoutMs,
396
+ onSpawned: (spawned) => {
397
+ probe = spawned;
398
+ },
399
+ });
400
+ probe = initializedProbe;
401
+ const response = await this.runACPRequest(() => initializedProbe.connection.newSession({
402
+ cwd,
403
+ mcpServers: [],
404
+ }));
405
+ const transformed = this.transformSessionResponse(response);
406
+ const models = deriveModelDefinitionsFromACP(this.provider, transformed.models, transformed.configOptions);
407
+ const modeInfo = deriveModesFromACP(this.defaultModes, transformed.modes, transformed.configOptions);
408
+ return {
409
+ models: this.modelTransformer ? this.modelTransformer(models) : models,
410
+ modes: modeInfo.modes,
411
+ };
412
+ })();
413
+ return await withTimeout(catalogProbe, timeoutMs, `ACP catalog probe timed out after ${timeoutMs}ms`);
385
414
  }
386
415
  finally {
387
- await this.closeProbe(probe);
416
+ if (probe) {
417
+ await this.closeProbe(probe);
418
+ }
388
419
  }
389
420
  }
390
421
  async listFeatures(config) {
@@ -461,6 +492,28 @@ export class ACPAgentClient {
461
492
  }
462
493
  }
463
494
  async spawnProcess(launchEnv, options) {
495
+ const transport = await this.spawnTransport(launchEnv);
496
+ const probe = {
497
+ child: transport.child,
498
+ connection: transport.connection,
499
+ stderrChunks: transport.stderrChunks,
500
+ };
501
+ options?.onSpawned?.(probe);
502
+ try {
503
+ const initialize = await this.initializeTransport(transport, options?.initializeTimeoutMs);
504
+ const initializedProbe = {
505
+ ...probe,
506
+ initialize,
507
+ };
508
+ probe.initialize = initialize;
509
+ return initializedProbe;
510
+ }
511
+ catch (error) {
512
+ await terminateChildProcess(transport.child, 2000, this.terminateProcess);
513
+ throw error;
514
+ }
515
+ }
516
+ async spawnTransport(launchEnv) {
464
517
  const { command, args } = await this.resolveLaunchCommand();
465
518
  const child = spawnProcess(command, args, {
466
519
  cwd: process.cwd(),
@@ -481,38 +534,46 @@ export class ACPAgentClient {
481
534
  reject(new Error(stderr ? `${String(error)}\n${stderr}` : String(error)));
482
535
  });
483
536
  });
537
+ const spawnReadyPromise = new Promise((resolve) => {
538
+ child.once("spawn", () => {
539
+ resolve();
540
+ });
541
+ });
484
542
  const stream = createLoggedNdJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout), { logger: this.logger, provider: this.provider });
485
543
  const connection = new ClientSideConnection(() => this.buildProbeClient(), stream);
544
+ return {
545
+ child,
546
+ connection,
547
+ stderrChunks,
548
+ spawnReady: spawnReadyPromise,
549
+ spawnError: spawnErrorPromise,
550
+ };
551
+ }
552
+ async initializeTransport(transport, initializeTimeoutMs) {
486
553
  let timeout = null;
487
- const initializeTimeoutPromise = options?.initializeTimeoutMs
554
+ const initializeTimeoutPromise = initializeTimeoutMs
488
555
  ? new Promise((_, reject) => {
489
556
  timeout = setTimeout(() => {
490
- reject(new Error(`ACP initialize timed out after ${options.initializeTimeoutMs}ms`));
491
- }, options.initializeTimeoutMs);
557
+ reject(new Error(`ACP initialize timed out after ${initializeTimeoutMs}ms`));
558
+ }, initializeTimeoutMs);
492
559
  })
493
560
  : null;
494
- let initialize;
495
561
  try {
496
- initialize = await this.runACPRequest(() => Promise.race([
497
- connection.initialize({
562
+ return await this.runACPRequest(() => Promise.race([
563
+ transport.connection.initialize({
498
564
  protocolVersion: PROTOCOL_VERSION,
499
565
  clientCapabilities: ACP_CLIENT_CAPABILITIES,
500
566
  clientInfo: { name: "Paseo", version: "dev" },
501
567
  }),
502
- spawnErrorPromise,
568
+ transport.spawnError,
503
569
  ...(initializeTimeoutPromise ? [initializeTimeoutPromise] : []),
504
570
  ]));
505
571
  }
506
- catch (error) {
507
- await terminateChildProcess(child, 2000, this.terminateProcess);
508
- throw error;
509
- }
510
572
  finally {
511
573
  if (timeout) {
512
574
  clearTimeout(timeout);
513
575
  }
514
576
  }
515
- return { child, connection, initialize };
516
577
  }
517
578
  buildProbeClient() {
518
579
  return {
@@ -536,7 +597,7 @@ export class ACPAgentClient {
536
597
  }
537
598
  async closeProbe(probe) {
538
599
  try {
539
- if (probe.initialize.agentCapabilities?.sessionCapabilities?.close) {
600
+ if (probe.initialize?.agentCapabilities?.sessionCapabilities?.close) {
540
601
  // No active session to close here; ignore capability.
541
602
  }
542
603
  }
@@ -552,6 +613,89 @@ export class ACPAgentClient {
552
613
  throw toACPRequestError(error);
553
614
  }
554
615
  }
616
+ async buildACPProbeDiagnosticRows(options = {}) {
617
+ const rows = [];
618
+ const phaseTimeoutMs = options.phaseTimeoutMs ?? ACP_DIAGNOSTIC_PHASE_TIMEOUT_MS;
619
+ const cwd = options.cwd ?? homedir();
620
+ let transport = null;
621
+ try {
622
+ const spawnStartedAt = Date.now();
623
+ try {
624
+ transport = await this.spawnTransport(PROBE_ENV);
625
+ await withTimeout(Promise.race([transport.spawnReady, transport.spawnError]), phaseTimeoutMs, `ACP spawn timed out after ${phaseTimeoutMs}ms`);
626
+ rows.push({
627
+ label: "ACP spawn",
628
+ value: `ok (${formatDurationMs(spawnStartedAt)})`,
629
+ });
630
+ }
631
+ catch (error) {
632
+ rows.push({
633
+ label: "ACP spawn",
634
+ value: `error: ${toDiagnosticErrorMessage(error)}`,
635
+ });
636
+ return rows;
637
+ }
638
+ const activeTransport = transport;
639
+ const initializeStartedAt = Date.now();
640
+ try {
641
+ await this.initializeTransport(activeTransport, phaseTimeoutMs);
642
+ rows.push({
643
+ label: "ACP initialize",
644
+ value: `ok (${formatDurationMs(initializeStartedAt)})`,
645
+ });
646
+ }
647
+ catch (error) {
648
+ rows.push({
649
+ label: "ACP initialize",
650
+ value: `error: ${toDiagnosticErrorMessage(error)}`,
651
+ });
652
+ pushACPStderrRow(rows, activeTransport.stderrChunks);
653
+ return rows;
654
+ }
655
+ const sessionStartedAt = Date.now();
656
+ try {
657
+ const response = await withTimeout(this.runACPRequest(() => activeTransport.connection.newSession({
658
+ cwd,
659
+ mcpServers: [],
660
+ })), phaseTimeoutMs, `ACP session/new timed out after ${phaseTimeoutMs}ms`);
661
+ const transformed = this.transformSessionResponse(response);
662
+ const models = deriveModelDefinitionsFromACP(this.provider, transformed.models, transformed.configOptions);
663
+ const modeInfo = deriveModesFromACP(this.defaultModes, transformed.modes, transformed.configOptions);
664
+ rows.push({
665
+ label: "ACP session/new",
666
+ value: `ok (${formatDurationMs(sessionStartedAt)}; models=${models.length}; modes=${modeInfo.modes.length})`,
667
+ });
668
+ }
669
+ catch (error) {
670
+ rows.push({
671
+ label: "ACP session/new",
672
+ value: `error: ${toDiagnosticErrorMessage(error)}`,
673
+ });
674
+ pushACPStderrRow(rows, activeTransport.stderrChunks);
675
+ return rows;
676
+ }
677
+ pushACPStderrRow(rows, activeTransport.stderrChunks);
678
+ return rows;
679
+ }
680
+ finally {
681
+ if (transport) {
682
+ const cleanupStartedAt = Date.now();
683
+ try {
684
+ await terminateChildProcess(transport.child, 2000, this.terminateProcess);
685
+ rows.push({
686
+ label: "ACP cleanup",
687
+ value: `ok (${formatDurationMs(cleanupStartedAt)})`,
688
+ });
689
+ }
690
+ catch (error) {
691
+ rows.push({
692
+ label: "ACP cleanup",
693
+ value: `error: ${toDiagnosticErrorMessage(error)}`,
694
+ });
695
+ }
696
+ }
697
+ }
698
+ }
555
699
  async resolveLaunchCommand() {
556
700
  const prefix = await resolveProviderLaunch({
557
701
  commandConfig: this.runtimeSettings?.command,
@@ -17,6 +17,7 @@ import { realClaudeRewindSdk, revertClaudeConversation, revertClaudeFiles } from
17
17
  import { normalizeProviderReplayTimestamp } from "../../provider-history-timestamps.js";
18
18
  import { claudeProjectDirSync } from "./project-dir.js";
19
19
  import { SETTING_APPLIES_NEXT_TURN_NOTICE } from "../../provider-notices.js";
20
+ import { isProviderImageMarkdown, materializeProviderImage, renderProviderImageOutputAsAssistantMarkdown, } from "../provider-image-output.js";
20
21
  import { getAgentStreamEventTurnId, } from "../../agent-sdk-types.js";
21
22
  import { importSessionFromPersistence } from "../../provider-session-import.js";
22
23
  import { checkProviderLaunchAvailable, createProviderEnv, createProviderEnvSpec, resolveProviderLaunch, } from "../../provider-launch-config.js";
@@ -370,6 +371,39 @@ function coerceToolResultContentToString(content) {
370
371
  }
371
372
  return deterministicStringify(content);
372
373
  }
374
+ function toBase64ImageOutput(block) {
375
+ const record = toObjectRecord(block);
376
+ if (!record || record.type !== "image") {
377
+ return null;
378
+ }
379
+ const source = toObjectRecord(record.source);
380
+ if (!source || source.type !== "base64" || typeof source.data !== "string") {
381
+ return null;
382
+ }
383
+ return {
384
+ data: source.data,
385
+ mimeType: typeof source.media_type === "string" ? source.media_type : null,
386
+ };
387
+ }
388
+ // Claude returns images inside tool_result content as base64 Anthropic blocks. Left in place they
389
+ // reach coerceToolResultContentToString, which JSON.stringifies the whole array — dumping base64
390
+ // into the tool output. We pull those blocks out to render them as image markdown and leave a
391
+ // "[image]" placeholder so image-only results still produce non-empty output.
392
+ function splitClaudeToolResultImages(content) {
393
+ if (!Array.isArray(content)) {
394
+ return { images: [], text: content };
395
+ }
396
+ const images = [];
397
+ const text = content.map((block) => {
398
+ const image = toBase64ImageOutput(block);
399
+ if (image) {
400
+ images.push(image);
401
+ return { type: "text", text: "[image]" };
402
+ }
403
+ return block;
404
+ });
405
+ return { images, text };
406
+ }
373
407
  function normalizeClaudeTranscriptText(value) {
374
408
  if (typeof value !== "string") {
375
409
  return null;
@@ -3472,15 +3506,17 @@ class ClaudeAgentSession {
3472
3506
  const callId = typeof block.tool_use_id === "string" && block.tool_use_id.length > 0
3473
3507
  ? block.tool_use_id
3474
3508
  : (entry?.id ?? null);
3475
- // Extract output from block.content (SDK always returns content in string form)
3476
- const output = this.buildToolOutput(block, entry);
3509
+ // Pull image blocks out of the result so base64 never reaches the tool output, and render each
3510
+ // one as an assistant_message markdown image after the tool_call (matching how Codex emits).
3511
+ const { images, text } = splitClaudeToolResultImages(block.content);
3512
+ const output = this.buildToolOutput(text, block, entry);
3477
3513
  if (block.is_error) {
3478
3514
  this.pushToolCall(mapClaudeFailedToolCall({
3479
3515
  name: toolName,
3480
3516
  callId,
3481
3517
  input: entry?.input ?? null,
3482
3518
  output: output ?? null,
3483
- error: block,
3519
+ error: { ...block, content: text },
3484
3520
  }), items);
3485
3521
  }
3486
3522
  else {
@@ -3491,12 +3527,20 @@ class ClaudeAgentSession {
3491
3527
  output: output ?? null,
3492
3528
  }), items);
3493
3529
  }
3530
+ for (const image of images) {
3531
+ const imageItem = renderProviderImageOutputAsAssistantMarkdown(image, {
3532
+ materialize: materializeProviderImage,
3533
+ });
3534
+ if (imageItem) {
3535
+ items.push(imageItem);
3536
+ }
3537
+ }
3494
3538
  if (typeof block.tool_use_id === "string") {
3495
3539
  this.toolUseCache.delete(block.tool_use_id);
3496
3540
  this.sidechainTracker.delete(block.tool_use_id);
3497
3541
  }
3498
3542
  }
3499
- buildToolOutput(block, entry) {
3543
+ buildToolOutput(content, block, entry) {
3500
3544
  if (block.is_error) {
3501
3545
  return undefined;
3502
3546
  }
@@ -3504,23 +3548,23 @@ class ClaudeAgentSession {
3504
3548
  const blockToolName = typeof block.tool_name === "string" ? block.tool_name : undefined;
3505
3549
  const server = entry?.server ?? blockServer ?? "tool";
3506
3550
  const tool = entry?.name ?? blockToolName ?? "tool";
3507
- const content = coerceToolResultContentToString(block.content);
3551
+ const coercedContent = coerceToolResultContentToString(content);
3508
3552
  const input = entry?.input;
3509
3553
  // Build structured result based on tool type
3510
- const structured = this.buildStructuredToolResult(server, tool, content, input);
3554
+ const structured = this.buildStructuredToolResult(server, tool, coercedContent, input);
3511
3555
  if (structured) {
3512
3556
  return structured;
3513
3557
  }
3514
3558
  // Fallback format - try to parse JSON first
3515
3559
  const result = {};
3516
- if (content.length > 0) {
3560
+ if (coercedContent.length > 0) {
3517
3561
  try {
3518
3562
  // If content is a JSON string, parse it
3519
- result.output = JSON.parse(content);
3563
+ result.output = JSON.parse(coercedContent);
3520
3564
  }
3521
3565
  catch {
3522
3566
  // If not JSON, return unchanged (no extra wrapping)
3523
- result.output = content;
3567
+ result.output = coercedContent;
3524
3568
  }
3525
3569
  }
3526
3570
  // Preserve file changes tracked during tool execution
@@ -3945,6 +3989,9 @@ function convertClaudeHistoryEntryPreamble(entry) {
3945
3989
  }
3946
3990
  return { proceed: { content } };
3947
3991
  }
3992
+ function isProviderImageMessage(item) {
3993
+ return item.type === "assistant_message" && isProviderImageMarkdown(item.text);
3994
+ }
3948
3995
  export function convertClaudeHistoryEntry(entry, mapBlocks) {
3949
3996
  const preamble = convertClaudeHistoryEntryPreamble(entry);
3950
3997
  if ("shortCircuit" in preamble) {
@@ -3980,7 +4027,10 @@ export function convertClaudeHistoryEntry(entry, mapBlocks) {
3980
4027
  if (hasToolBlock && normalizedBlocks) {
3981
4028
  const mapped = mapBlocks(normalizedBlocks);
3982
4029
  if (entry.type === "user") {
3983
- const toolItems = mapped.filter((item) => item.type === "tool_call");
4030
+ // tool_result handling (handleToolResult) emits image markdown as an assistant_message
4031
+ // alongside the tool_call. User-entry text blocks also map to assistant_message in this path
4032
+ // and must stay suppressed, so keep tool_calls plus only the image assistant_messages.
4033
+ const toolItems = mapped.filter((item) => item.type === "tool_call" || isProviderImageMessage(item));
3984
4034
  return timeline.length ? [...timeline, ...toolItems] : toolItems;
3985
4035
  }
3986
4036
  return mapped;