Merge branch 'main' of github.com:getpaseo/paseo

This commit is contained in:
Mohamed Boudra
2026-02-26 19:23:36 +07:00
9 changed files with 282 additions and 14 deletions
+19 -2
View File
@@ -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": {
+2
View File
@@ -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 .",
+2 -1
View File
@@ -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"
}
}
@@ -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<AgentStreamEvent> {
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<boolean> {
return true;
}
async createSession(config: AgentSessionConfig): Promise<AgentSession> {
return new TurnFailedSession(config);
}
async resumeSession(config?: Partial<AgentSessionConfig>): Promise<AgentSession> {
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<AgentTimelineItem, { type: "assistant_message" }> =>
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<AgentStreamEvent> {
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<boolean> {
return true;
}
async createSession(config: AgentSessionConfig): Promise<AgentSession> {
return new DetailedFailureSession(config);
}
async resumeSession(config?: Partial<AgentSessionConfig>): Promise<AgentSession> {
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<AgentTimelineItem, { type: "assistant_message" }> =>
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");
@@ -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<string, string>;
}
): 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<AgentStreamEvent, { type: "turn_failed" }>
): 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,
@@ -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 }
@@ -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<string>();
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<AgentStreamEvent, { type: "turn_failed" }> {
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;
}
@@ -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,
},
{
+2
View File
@@ -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'),