From aa2f35656ef6d05b5057e9ba6555301fe70930e9 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 26 Feb 2026 03:26:25 +0000 Subject: [PATCH 1/2] Add daemon dev scripts and linux Tauri CLI dependency --- package-lock.json | 21 +++++++++++++++++++-- package.json | 2 ++ packages/desktop/package.json | 3 ++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b216303da..3e13bee64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "paseo", "version": "0.1.16", "hasInstallScript": true, - "license": "MIT", + "license": "AGPL-3.0-or-later", "workspaces": [ "packages/server", "packages/app", @@ -6425,6 +6425,22 @@ "node": ">= 10" } }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.9.6.tgz", + "integrity": "sha512-vY0le8ad2KaV1PJr+jCd8fUF9VOjwwQP/uBuTJvhvKTloEwxYA/kAjKK9OpIslGA9m/zcnSo74czI6bBrm2sYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "dev": true, @@ -21698,7 +21714,8 @@ "name": "@getpaseo/desktop", "version": "0.1.16", "devDependencies": { - "@tauri-apps/cli": "^2.9.6" + "@tauri-apps/cli": "^2.9.6", + "@tauri-apps/cli-linux-x64-gnu": "^2.9.6" } }, "packages/relay": { diff --git a/package.json b/package.json index 91f9daa78..de8097c20 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,9 @@ "dev:website": "npm run dev --workspace=@getpaseo/website", "postinstall": "node scripts/postinstall-patches.mjs", "build": "npm run build --workspaces --if-present", + "build:daemon": "npm run build --workspace=@getpaseo/relay && npm run build --workspace=@getpaseo/server && npm run build --workspace=@getpaseo/cli", "typecheck": "npm run typecheck --workspaces --if-present", + "typecheck:daemon": "npm run typecheck --workspace=@getpaseo/relay && npm run typecheck --workspace=@getpaseo/server && npm run typecheck --workspace=@getpaseo/cli", "test": "npm run test --workspaces --if-present", "format": "prettier --write .", "format:check": "prettier --check .", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index ef2b1552c..5f2ce5ee1 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -9,6 +9,7 @@ "tauri": "tauri" }, "devDependencies": { - "@tauri-apps/cli": "^2.9.6" + "@tauri-apps/cli": "^2.9.6", + "@tauri-apps/cli-linux-x64-gnu": "^2.9.6" } } From 4ca97c4fdf9b63a1fbaad99e90cad65f0dec5fac Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 26 Feb 2026 04:03:08 +0000 Subject: [PATCH 2/2] Improve provider error surfacing and add Sonnet 4.6 --- .../src/server/agent/agent-manager.test.ts | 131 ++++++++++++++++++ .../server/src/server/agent/agent-manager.ts | 80 ++++++++++- .../src/server/agent/agent-sdk-types.ts | 8 +- .../server/agent/providers/claude-agent.ts | 43 +++++- .../agent/providers/claude/model-catalog.ts | 6 + packages/server/src/shared/messages.ts | 2 + 6 files changed, 259 insertions(+), 11 deletions(-) diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 4a93afc0d..3910ad786 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -1853,6 +1853,137 @@ describe("AgentManager", () => { expect(attentionReasons).toEqual(["error", "error"]); }); + test("turn_failed emits a system error assistant timeline message and keeps error lifecycle", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-turn-failed-")); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); + + class TurnFailedSession extends TestAgentSession { + async *stream(): AsyncGenerator { + yield { type: "turn_started", provider: this.provider }; + yield { type: "turn_failed", provider: this.provider, error: "invalid model id" }; + } + } + + class TurnFailedClient implements AgentClient { + readonly provider = "codex" as const; + readonly capabilities = TEST_CAPABILITIES; + + async isAvailable(): Promise { + return true; + } + + async createSession(config: AgentSessionConfig): Promise { + return new TurnFailedSession(config); + } + + async resumeSession(config?: Partial): Promise { + return new TurnFailedSession({ + provider: "codex", + cwd: config?.cwd ?? process.cwd(), + }); + } + } + + const manager = new AgentManager({ + clients: { + codex: new TurnFailedClient(), + }, + registry: storage, + logger, + idFactory: () => "00000000-0000-4000-8000-000000000131", + }); + + const agent = await manager.createAgent({ + provider: "codex", + cwd: workdir, + title: "Turn failed test", + }); + + await expect(manager.runAgent(agent.id, "hello")).rejects.toThrow("invalid model id"); + + const snapshot = manager.getAgent(agent.id); + expect(snapshot?.lifecycle).toBe("error"); + expect(snapshot?.lastError).toBe("invalid model id"); + + const systemErrors = manager + .getTimeline(agent.id) + .filter( + (item): item is Extract => + item.type === "assistant_message" && item.text.includes("[System Error]") + ); + expect(systemErrors).toHaveLength(1); + expect(systemErrors[0]?.text).toContain("invalid model id"); + }); + + test("turn_failed surfaces provider code and diagnostic in system error message", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-turn-failed-detail-")); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); + + class DetailedFailureSession extends TestAgentSession { + async *stream(): AsyncGenerator { + yield { type: "turn_started", provider: this.provider }; + yield { + type: "turn_failed", + provider: this.provider, + error: "Provider execution failed", + code: "126", + diagnostic: "No preset version installed for command claude", + }; + } + } + + class DetailedFailureClient implements AgentClient { + readonly provider = "codex" as const; + readonly capabilities = TEST_CAPABILITIES; + + async isAvailable(): Promise { + return true; + } + + async createSession(config: AgentSessionConfig): Promise { + return new DetailedFailureSession(config); + } + + async resumeSession(config?: Partial): Promise { + return new DetailedFailureSession({ + provider: "codex", + cwd: config?.cwd ?? process.cwd(), + }); + } + } + + const manager = new AgentManager({ + clients: { + codex: new DetailedFailureClient(), + }, + registry: storage, + logger, + idFactory: () => "00000000-0000-4000-8000-000000000132", + }); + + const agent = await manager.createAgent({ + provider: "codex", + cwd: workdir, + title: "Detailed failure test", + }); + + await expect(manager.runAgent(agent.id, "hello")).rejects.toThrow( + "Provider execution failed" + ); + + const systemError = manager + .getTimeline(agent.id) + .find( + (item): item is Extract => + item.type === "assistant_message" && item.text.includes("[System Error]") + ); + expect(systemError?.text).toContain("Provider execution failed"); + expect(systemError?.text).toContain("code: 126"); + expect(systemError?.text).toContain("No preset version installed for command claude"); + }); + test("permission request notifies once without forcing unread attention state", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-attention-permission-")); const storagePath = join(workdir, "agents"); diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 244d5cd1a..0a38a41c3 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -217,6 +217,8 @@ type ActiveManagedAgent = | ManagedAgentRunning | ManagedAgentError; +const SYSTEM_ERROR_PREFIX = "[System Error]"; + function attachPersistenceCwd( handle: AgentPersistenceHandle | null, cwd: string @@ -917,7 +919,7 @@ export class AgentManager { } else if (event.type === "turn_completed") { usage = event.usage; } else if (event.type === "turn_failed") { - throw new Error(event.error); + throw new Error(this.formatTurnFailedMessage(event)); } else if (event.type === "turn_canceled") { canceled = true; } @@ -1027,8 +1029,9 @@ export class AgentManager { const mutableAgent = agent as ActiveManagedAgent; mutableAgent.pendingRun = null; - mutableAgent.lifecycle = error ? "error" : "idle"; - mutableAgent.lastError = error; + const terminalError = error ?? mutableAgent.lastError; + mutableAgent.lifecycle = terminalError ? "error" : "idle"; + mutableAgent.lastError = terminalError; const persistenceHandle = mutableAgent.session.describePersistence() ?? (mutableAgent.runtimeInfo?.sessionId @@ -1056,6 +1059,11 @@ export class AgentManager { } catch (error) { finalizeError = error instanceof Error ? error.message : "Agent stream failed"; + self.handleStreamEvent(agent, { + type: "turn_failed", + provider: agent.provider, + error: finalizeError, + }); throw error; } finally { await self.refreshRuntimeInfo(agent); @@ -1744,10 +1752,14 @@ export class AgentManager { void this.refreshRuntimeInfo(agent); break; case "turn_failed": - if (!agent.pendingRun) { - agent.lifecycle = "error"; - } + agent.lifecycle = "error"; agent.lastError = event.error; + this.appendSystemErrorTimelineMessage( + agent, + event.provider, + this.formatTurnFailedMessage(event), + options + ); for (const [requestId] of agent.pendingPermissions) { agent.pendingPermissions.delete(requestId); if (!options?.fromHistory) { @@ -1814,6 +1826,62 @@ export class AgentManager { } } + private appendSystemErrorTimelineMessage( + agent: ActiveManagedAgent, + provider: AgentProvider, + message: string, + options?: { + fromHistory?: boolean; + canonicalUserMessagesById?: ReadonlyMap; + } + ): void { + if (options?.fromHistory) { + return; + } + + const normalized = message.trim(); + if (!normalized) { + return; + } + + const text = `${SYSTEM_ERROR_PREFIX} ${normalized}`; + const lastItem = agent.timelineRows[agent.timelineRows.length - 1]?.item; + if (lastItem?.type === "assistant_message" && lastItem.text === text) { + return; + } + + const item: AgentTimelineItem = { type: "assistant_message", text }; + const row = this.recordTimeline(agent, item); + this.dispatchStream( + agent.id, + { + type: "timeline", + item, + provider, + }, + { + seq: row.seq, + epoch: this.ensureTimelineState(agent).epoch, + } + ); + } + + private formatTurnFailedMessage( + event: Extract + ): string { + const base = event.error.trim(); + const parts = [base.length > 0 ? base : "Provider run failed"]; + const code = event.code?.trim(); + if (code) { + parts.push(`code: ${code}`); + } + const diagnostic = event.diagnostic?.trim(); + if (diagnostic && diagnostic !== base) { + parts.push(diagnostic); + } + return parts.join("\n\n"); + } + private shouldSuppressLiveUserMessageEcho( agent: ActiveManagedAgent, event: AgentStreamEvent, diff --git a/packages/server/src/server/agent/agent-sdk-types.ts b/packages/server/src/server/agent/agent-sdk-types.ts index 6ae64d24e..4956c507b 100644 --- a/packages/server/src/server/agent/agent-sdk-types.ts +++ b/packages/server/src/server/agent/agent-sdk-types.ts @@ -240,7 +240,13 @@ export type AgentStreamEvent = | { type: "thread_started"; sessionId: string; provider: AgentProvider } | { type: "turn_started"; provider: AgentProvider } | { type: "turn_completed"; provider: AgentProvider; usage?: AgentUsage } - | { type: "turn_failed"; provider: AgentProvider; error: string } + | { + type: "turn_failed"; + provider: AgentProvider; + error: string; + code?: string; + diagnostic?: string; + } | { type: "turn_canceled"; provider: AgentProvider; reason: string } | { type: "timeline"; item: AgentTimelineItem; provider: AgentProvider } | { type: "permission_requested"; provider: AgentProvider; request: AgentPermissionRequest } diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts index fb7603f97..ae54a34d0 100644 --- a/packages/server/src/server/agent/providers/claude-agent.ts +++ b/packages/server/src/server/agent/providers/claude-agent.ts @@ -432,6 +432,8 @@ type ClaudeOptionsLogSummary = { hasStderrHandler: boolean; }; +const MAX_RECENT_STDERR_CHARS = 4000; + function summarizeClaudeOptionsForLog(options: ClaudeOptions): ClaudeOptionsLogSummary { const systemPromptRaw = options.systemPrompt; const systemPromptSummary = (() => { @@ -1512,6 +1514,7 @@ class ClaudeAgentSession implements AgentSession { private userMessageIds: string[] = []; private readonly localUserMessageIds = new Set(); private suppressLocalReplayActivity = false; + private recentStderr = ""; private closed = false; constructor( @@ -1650,6 +1653,7 @@ class ClaudeAgentSession implements AgentSession { this.activeForegroundTurn = foregroundTurn; this.preReplayMetadataSeen = false; this.transitionTurnState("foreground", "foreground stream started"); + this.clearRecentStderr(); let finishedNaturally = false; let cancelIssued = false; @@ -2281,6 +2285,7 @@ class ClaudeAgentSession implements AgentSession { }, settingSources: CLAUDE_SETTING_SOURCES, stderr: (data: string) => { + this.captureStderr(data); this.logger.error({ stderr: data.trim() }, "Claude Agent SDK stderr"); }, env: { @@ -2413,11 +2418,41 @@ class ClaudeAgentSession implements AgentSession { } private failRun(run: RunRecord, errorMessage: string): void { - this.emitRunEvent(run, { + this.emitRunEvent(run, this.buildTurnFailedEvent(errorMessage)); + } + + private buildTurnFailedEvent( + errorMessage: string + ): Extract { + const normalized = errorMessage.trim() || "Claude run failed"; + const exitCodeMatch = normalized.match(/\bcode\s+(\d+)\b/i); + const code = exitCodeMatch ? exitCodeMatch[1] : undefined; + const diagnostic = this.getRecentStderrDiagnostic(); + return { type: "turn_failed", provider: "claude", - error: errorMessage, - }); + error: normalized, + ...(code ? { code } : {}), + ...(diagnostic ? { diagnostic } : {}), + }; + } + + private captureStderr(data: string): void { + const text = data.trim(); + if (!text) { + return; + } + const combined = this.recentStderr ? `${this.recentStderr}\n${text}` : text; + this.recentStderr = combined.slice(-MAX_RECENT_STDERR_CHARS); + } + + private clearRecentStderr(): void { + this.recentStderr = ""; + } + + private getRecentStderrDiagnostic(): string | undefined { + const text = this.recentStderr.trim(); + return text.length > 0 ? text : undefined; } private cancelRun( @@ -3421,7 +3456,7 @@ class ClaudeAgentSession implements AgentSession { "errors" in message && Array.isArray(message.errors) && message.errors.length > 0 ? message.errors.join("\n") : "Claude run failed"; - events.push({ type: "turn_failed", provider: "claude", error: errorMessage }); + events.push(this.buildTurnFailedEvent(errorMessage)); } break; } diff --git a/packages/server/src/server/agent/providers/claude/model-catalog.ts b/packages/server/src/server/agent/providers/claude/model-catalog.ts index b4f5bc689..9c3a72ada 100644 --- a/packages/server/src/server/agent/providers/claude/model-catalog.ts +++ b/packages/server/src/server/agent/providers/claude/model-catalog.ts @@ -31,6 +31,12 @@ export const CLAUDE_MODEL_CATALOG: readonly ClaudeCatalogModel[] = [ name: "Sonnet 4.5", description: "Sonnet 4.5 · Best for everyday tasks", isDefault: true, + }, + { + family: "sonnet", + modelId: "claude-sonnet-4-6", + name: "Sonnet 4.6", + description: "Sonnet 4.6 · Best for everyday tasks", isLatestInFamily: true, }, { diff --git a/packages/server/src/shared/messages.ts b/packages/server/src/shared/messages.ts index 3b5365a05..9c2df3ebb 100644 --- a/packages/server/src/shared/messages.ts +++ b/packages/server/src/shared/messages.ts @@ -319,6 +319,8 @@ export const AgentStreamEventPayloadSchema = z.discriminatedUnion('type', [ type: z.literal('turn_failed'), provider: AgentProviderSchema, error: z.string(), + code: z.string().optional(), + diagnostic: z.string().optional(), }), z.object({ type: z.literal('turn_canceled'),