From 685a3d0ee20eb5b10990d4b72b4a088cc4764d7b Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 5 May 2026 17:16:38 +0800 Subject: [PATCH] refactor(server,app): lift return types and parse at boundaries (T2 typeaware production sweep) (#758) * refactor(server,app): lift return types and parse at boundaries (T2 typeaware production sweep) * fix(server): align acp-agent test assertions with pino call shape --- packages/app/e2e/helpers/desktop-updates.ts | 38 +++-- .../components/combined-model-selector.tsx | 19 ++- packages/app/src/contexts/session-context.tsx | 44 ++--- packages/server/src/client/daemon-client.ts | 20 ++- .../server/agent/providers/acp-agent.test.ts | 6 +- .../src/server/agent/providers/acp-agent.ts | 52 +++--- .../agent/providers/codex-app-server-agent.ts | 150 +++++++++++------- packages/server/src/utils/checkout-git.ts | 24 ++- 8 files changed, 203 insertions(+), 150 deletions(-) diff --git a/packages/app/e2e/helpers/desktop-updates.ts b/packages/app/e2e/helpers/desktop-updates.ts index e57c7b19b..345b3a8fe 100644 --- a/packages/app/e2e/helpers/desktop-updates.ts +++ b/packages/app/e2e/helpers/desktop-updates.ts @@ -32,12 +32,13 @@ export async function loadRealDaemonState(): Promise { if (!paseoHome) throw new Error("E2E_PASEO_HOME not set — globalSetup must run first"); const resp = await fetch(`http://127.0.0.1:${port}/api/status`); - const data = (await resp.json()) as DaemonApiStatus; + const data: DaemonApiStatus = await resp.json(); let pid: number | null = null; try { const raw = readFileSync(`${paseoHome}/paseo.pid`, "utf8"); - pid = (JSON.parse(raw) as PidFileContent).pid ?? null; + const pidContent: PidFileContent = JSON.parse(raw); + pid = pidContent.pid ?? null; } catch (err) { // PID file may not be present yet on a very fresh daemon start console.warn("[desktop-updates] paseo.pid not found:", err); @@ -70,6 +71,12 @@ export interface ConfirmDialogCall { title: string | undefined; } +declare global { + interface Window { + __capturedDialogCall: ConfirmDialogCall | undefined; + } +} + /** * Injects window.paseoDesktop before app load so all Electron-gated code * activates. The update-check IPC is mocked at the boundary so the real @@ -151,12 +158,14 @@ export async function injectDesktopBridge(page: Page, config: DesktopBridgeConfi } if (command === "patch_desktop_settings") { - const patchDaemon = - args?.daemon && typeof args.daemon === "object" - ? (args.daemon as Record) - : {}; - if (typeof patchDaemon.manageBuiltInDaemon === "boolean") { - manageDaemon = patchDaemon.manageBuiltInDaemon; + const daemon = args?.daemon; + if ( + daemon !== null && + typeof daemon === "object" && + "manageBuiltInDaemon" in daemon && + typeof daemon.manageBuiltInDaemon === "boolean" + ) { + manageDaemon = daemon.manageBuiltInDaemon; } return { releaseChannel: "stable", @@ -183,9 +192,9 @@ export async function injectDesktopBridge(page: Page, config: DesktopBridgeConfi }, dialog: { ask: async (message: string, options?: Record) => { - (window as unknown as Record).__capturedDialogCall = { + window.__capturedDialogCall = { message, - title: options?.title, + title: typeof options?.title === "string" ? options.title : undefined, }; return cfg.confirmShouldAccept ?? false; }, @@ -228,13 +237,8 @@ export async function interceptDaemonManagementConfirmDialog( page: Page, ): Promise { await page.getByRole("switch", { name: "Manage built-in daemon" }).click(); - await page.waitForFunction( - () => !!(window as unknown as Record).__capturedDialogCall, - { timeout: 5_000 }, - ); - return page.evaluate( - () => (window as unknown as Record).__capturedDialogCall as ConfirmDialogCall, - ); + await page.waitForFunction(() => !!window.__capturedDialogCall, { timeout: 5_000 }); + return page.evaluate(() => window.__capturedDialogCall!); } export async function toggleDaemonManagement( diff --git a/packages/app/src/components/combined-model-selector.tsx b/packages/app/src/components/combined-model-selector.tsx index 9307e0863..7732752c4 100644 --- a/packages/app/src/components/combined-model-selector.tsx +++ b/packages/app/src/components/combined-model-selector.tsx @@ -20,7 +20,7 @@ const IS_WEB = platformIsWeb; import { Combobox, ComboboxItem, type ComboboxOption } from "@/components/ui/combobox"; -const EMPTY_COMBOBOX_OPTIONS: ReadonlyArray = []; +const EMPTY_COMBOBOX_OPTIONS: ComboboxOption[] = []; function noop() {} @@ -446,12 +446,11 @@ function ProviderSearchInput({ const InputComponent = isMobile ? BottomSheetTextInput : TextInput; useEffect(() => { - if (autoFocus && platformIsWeb && inputRef.current) { - const timer = setTimeout(() => { - inputRef.current?.focus(); - }, 50); - return () => clearTimeout(timer); - } + if (!autoFocus || !platformIsWeb || !inputRef.current) return () => {}; + const timer = setTimeout(() => { + inputRef.current?.focus(); + }, 50); + return () => clearTimeout(timer); }, [autoFocus]); const inputStyle = useMemo( @@ -699,12 +698,12 @@ export function CombinedModelSelector({ useEffect(() => { if (platformIsWeb) { - return; + return () => {}; } if (!isOpen) { setIsContentReady(false); - return; + return () => {}; } const frame = requestAnimationFrame(() => { @@ -791,7 +790,7 @@ export function CombinedModelSelector({ )} { - if (!voiceRuntime) { - return; - } - - return voiceRuntime.registerSession({ + const unregister = voiceRuntime?.registerSession({ serverId, setVoiceMode: async (enabled, agentId) => { if (!client) { @@ -899,6 +895,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider setIsPlayingAudio(serverId, isPlaying); }, }); + return () => unregister?.(); }, [client, serverId, setIsPlayingAudio, voiceRuntime]); useEffect(() => { @@ -916,7 +913,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider useEffect(() => { if (!client || !isConnected) { - return; + return () => {}; } let cancelled = false; @@ -1242,10 +1239,10 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider if (message.payload.kind === "remove") { clearWorkspaceArchivePending({ serverId, - workspaceId: String(message.payload.id), + workspaceId: message.payload.id, }); - removeWorkspaceSetup({ serverId, workspaceId: String(message.payload.id) }); - removeWorkspace(serverId, String(message.payload.id)); + removeWorkspaceSetup({ serverId, workspaceId: message.payload.id }); + removeWorkspace(serverId, message.payload.id); return; } const workspace = normalizeWorkspaceDescriptor(message.payload.workspace); @@ -1403,15 +1400,10 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider } if (data.type === "tool_call" && data.metadata) { - const { - toolCallId, - toolName, - arguments: args, - } = data.metadata as { - toolCallId: string; - toolName: string; - arguments: unknown; - }; + const toolCallId = + typeof data.metadata.toolCallId === "string" ? data.metadata.toolCallId : ""; + const toolName = typeof data.metadata.toolName === "string" ? data.metadata.toolName : ""; + const args = data.metadata.arguments; setMessages(serverId, (prev) => [ ...prev, @@ -1428,10 +1420,9 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider } if (data.type === "tool_result" && data.metadata) { - const { toolCallId, result } = data.metadata as { - toolCallId: string; - result: unknown; - }; + const toolCallId = + typeof data.metadata.toolCallId === "string" ? data.metadata.toolCallId : ""; + const result = data.metadata.result; const applyToolResult = applyToolResultToMessages(toolCallId, result); setMessages(serverId, applyToolResult); @@ -1439,10 +1430,9 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider } if (data.type === "error" && data.metadata && "toolCallId" in data.metadata) { - const { toolCallId, error } = data.metadata as { - toolCallId: string; - error: unknown; - }; + const toolCallId = + typeof data.metadata.toolCallId === "string" ? data.metadata.toolCallId : ""; + const error = data.metadata.error; const applyToolError = applyToolErrorToMessages(toolCallId, error); setMessages(serverId, applyToolError); @@ -1800,7 +1790,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider } catch (error) { console.error("[Session] Failed to prepare images for agent creation:", error); } - return client.createAgent({ + await client.createAgent({ config, ...(trimmedPrompt ? { initialPrompt: trimmedPrompt } : {}), ...(imagesData && imagesData.length > 0 ? { images: imagesData } : {}), diff --git a/packages/server/src/client/daemon-client.ts b/packages/server/src/client/daemon-client.ts index db327bb76..8ed87e37b 100644 --- a/packages/server/src/client/daemon-client.ts +++ b/packages/server/src/client/daemon-client.ts @@ -601,8 +601,8 @@ export interface WaitForFinishResult { interface Waiter { predicate: (msg: SessionOutboundMessage) => T | null; - resolve: (value: T) => void; - reject: (error: Error) => void; + resolve(value: T): void; + reject(error: Error): void; timeoutHandle: ReturnType | null; } @@ -1160,7 +1160,7 @@ export class DaemonClient { } const type = arg1; - const handler = arg2 as (message: SessionOutboundMessage) => void; + const handler = arg2!; if (!this.messageHandlers.has(type)) { this.messageHandlers.set(type, new Set()); @@ -4043,7 +4043,13 @@ export class DaemonClient { const parsed = WSOutboundMessageSchema.safeParse(parsedJson); if (!parsed.success) { - const msgType = (parsedJson as { type?: string })?.type ?? "unknown"; + const msgType = + parsedJson != null && + typeof parsedJson === "object" && + "type" in parsedJson && + typeof parsedJson.type === "string" + ? parsedJson.type + : "unknown"; this.logger.warn({ msgType, error: parsed.error.message }, "Message validation failed"); return; } @@ -4405,7 +4411,7 @@ export class DaemonClient { timeout > 0 ? setTimeout(() => { if (waiter) { - this.waiters.delete(waiter as Waiter); + this.waiters.delete(waiter); } wrappedReject(timeoutError); }, timeout) @@ -4417,7 +4423,7 @@ export class DaemonClient { reject: wrappedReject, timeoutHandle, }; - this.waiters.add(waiter as Waiter); + this.waiters.add(waiter); }); const cancel = (error: Error) => { @@ -4426,7 +4432,7 @@ export class DaemonClient { } if (waiter) { - this.waiters.delete(waiter as Waiter); + this.waiters.delete(waiter); if (waiter.timeoutHandle) { clearTimeout(waiter.timeoutHandle); } diff --git a/packages/server/src/server/agent/providers/acp-agent.test.ts b/packages/server/src/server/agent/providers/acp-agent.test.ts index 80e67b1e6..9e1c7c1a0 100644 --- a/packages/server/src/server/agent/providers/acp-agent.test.ts +++ b/packages/server/src/server/agent/providers/acp-agent.test.ts @@ -220,7 +220,7 @@ test("ACP setModel only uses config-option fallback when the matching select cho await expect(session.setModel("new-provider-model")).resolves.toBeUndefined(); expect(childLogger.warn).toHaveBeenCalledWith( - "new-provider-model", + { value: "new-provider-model" }, expect.stringContaining("is not a valid claude-acp model config option"), ); expect(setSessionConfigOption).not.toHaveBeenCalled(); @@ -514,11 +514,11 @@ describe("ACPAgentSession Zed parity", () => { expect(invalid.setSessionMode).not.toHaveBeenCalled(); expect(invalid.unstableSetSessionModel).not.toHaveBeenCalled(); expect(childLogger.warn).toHaveBeenCalledWith( - expect.stringContaining("acceptEdits"), + { value: expect.stringContaining("acceptEdits") }, expect.stringContaining("not valid"), ); expect(childLogger.warn).toHaveBeenCalledWith( - expect.stringContaining("opus"), + { value: expect.stringContaining("opus") }, expect.stringContaining("not a valid"), ); }); diff --git a/packages/server/src/server/agent/providers/acp-agent.ts b/packages/server/src/server/agent/providers/acp-agent.ts index 690e9b817..236577955 100644 --- a/packages/server/src/server/agent/providers/acp-agent.ts +++ b/packages/server/src/server/agent/providers/acp-agent.ts @@ -3,6 +3,10 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; import { Readable, Writable } from "node:stream"; +import type { + ReadableStream as NodeReadableStream, + WritableStream as NodeWritableStream, +} from "node:stream/web"; import { ClientSideConnection, PROTOCOL_VERSION, @@ -90,6 +94,18 @@ import { appendOrReplaceGrowingAssistantMessage, runProviderTurn } from "./provi import { findExecutable } from "../../../utils/executable.js"; import { spawnProcess } from "../../../utils/spawn.js"; +function assertChildWithPipes( + child: ChildProcess, +): asserts child is ChildProcessWithoutNullStreams { + if (!child.stdin || !child.stdout || !child.stderr) { + throw new Error("Child process did not expose stdio pipes"); + } +} + +function isRecord(value: unknown): value is Record { + return value != null && typeof value === "object" && !Array.isArray(value); +} + const DEFAULT_ACP_CAPABILITIES: AgentCapabilityFlags = { supportsStreaming: true, supportsSessionPersistence: true, @@ -120,8 +136,8 @@ function summarizeMalformedACPStdoutError(error: unknown): { type: string; messa } export function createLoggedNdJsonStream( - output: WritableStream, - input: ReadableStream, + output: NodeWritableStream, + input: NodeReadableStream, options: { logger: Logger; provider: string }, ): ACPStream { const textEncoder = new TextEncoder(); @@ -152,7 +168,7 @@ export function createLoggedNdJsonStream( } try { - const message = JSON.parse(trimmedLine) as AnyMessage; + const message: AnyMessage = JSON.parse(trimmedLine); controller.enqueue(message); } catch (error) { options.logger.warn( @@ -629,7 +645,8 @@ export class ACPAgentClient implements AgentClient { overlays: [launchEnv], }), stdio: ["pipe", "pipe", "pipe"], - }) as ChildProcessWithoutNullStreams; + }); + assertChildWithPipes(child); const stderrChunks: string[] = []; child.stderr.on("data", (chunk: Buffer | string) => { @@ -643,13 +660,9 @@ export class ACPAgentClient implements AgentClient { }); }); - if (!child.stdin || !child.stdout) { - throw new Error(`${this.provider} ACP process did not expose stdio pipes`); - } - const stream = createLoggedNdJsonStream( - Writable.toWeb(child.stdin) as WritableStream, - Readable.toWeb(child.stdout) as ReadableStream, + Writable.toWeb(child.stdin), + Readable.toWeb(child.stdout), { logger: this.logger, provider: this.provider }, ); const connection = new ClientSideConnection(() => this.buildProbeClient(), stream); @@ -1546,7 +1559,8 @@ export class ACPAgentSession implements AgentSession, ACPClient { overlays: [this.launchEnv], }), stdio: ["pipe", "pipe", "pipe"], - }) as ChildProcessWithoutNullStreams; + }); + assertChildWithPipes(child); const stderrChunks: string[] = []; child.stderr.on("data", (chunk: Buffer | string) => { @@ -1568,13 +1582,9 @@ export class ACPAgentSession implements AgentSession, ACPClient { } }); - if (!child.stdin || !child.stdout) { - throw new Error(`${this.provider} ACP process did not expose stdio pipes`); - } - const stream = createLoggedNdJsonStream( - Writable.toWeb(child.stdin) as WritableStream, - Readable.toWeb(child.stdout) as ReadableStream, + Writable.toWeb(child.stdin), + Readable.toWeb(child.stdout), { logger: this.logger, provider: this.provider }, ); const connection = new ClientSideConnection(() => this, stream); @@ -1630,7 +1640,7 @@ export class ACPAgentSession implements AgentSession, ACPClient { } private warnInvalidSelection(value: string, message: string): void { - (this.logger.warn as unknown as (value: string, message: string) => void)(value, message); + this.logger.warn({ value }, message); } private translateSessionUpdate(update: SessionUpdate): AgentStreamEvent[] { @@ -2410,9 +2420,7 @@ function appendTerminalOutput(entry: TerminalEntry, chunk: string): void { } function readRecord(value: unknown): Record | null { - return value && typeof value === "object" && !Array.isArray(value) - ? (value as Record) - : null; + return isRecord(value) ? value : null; } function readString(record: Record | null, keys: string[]): string | undefined { @@ -2473,7 +2481,7 @@ function stringifyUnknown(value: unknown): string | undefined { try { return JSON.stringify(value); } catch { - return String(value); + return typeof value === "bigint" ? String(value) : "[unserializable]"; } } diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index eccc92700..7996b7d7b 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -30,7 +30,7 @@ import type { import type { Logger } from "pino"; import { homedir } from "node:os"; -import type { ChildProcessWithoutNullStreams } from "node:child_process"; +import type { ChildProcess, ChildProcessWithoutNullStreams } from "node:child_process"; import { randomUUID } from "node:crypto"; import { Dirent } from "node:fs"; import fs from "node:fs/promises"; @@ -65,6 +65,18 @@ import { import { runProviderTurn } from "./provider-runner.js"; import type { WorkspaceGitService } from "../../workspace-git-service.js"; +function assertChildWithPipes( + child: ChildProcess, +): asserts child is ChildProcessWithoutNullStreams { + if (!child.stdin || !child.stdout || !child.stderr) { + throw new Error("Child process did not expose stdio pipes"); + } +} + +function isRecord(value: unknown): value is Record { + return value != null && typeof value === "object" && !Array.isArray(value); +} + const DEFAULT_TIMEOUT_MS = 14 * 24 * 60 * 60 * 1000; const TURN_START_TIMEOUT_MS = 90 * 1000; const INTERRUPT_TIMEOUT_MS = 2_000; @@ -564,29 +576,24 @@ interface JsonRpcNotification { params?: unknown; } -type JsonRpcMessage = JsonRpcRequest | JsonRpcResponse | JsonRpcNotification; - -function isJsonRpcResponse(msg: JsonRpcMessage): msg is JsonRpcResponse { - const m = msg as unknown as Record; - if (typeof m.id !== "number") return false; - return m.result !== undefined || !!m.error; +function isJsonRpcResponse(msg: unknown): msg is JsonRpcResponse { + if (!isRecord(msg)) return false; + if (typeof msg.id !== "number") return false; + return msg.result !== undefined || !!msg.error; } -function isJsonRpcRequest(msg: JsonRpcMessage): msg is JsonRpcRequest { - const m = msg as unknown as Record; - return typeof m.id === "number" && typeof m.method === "string"; +function isJsonRpcRequest(msg: unknown): msg is JsonRpcRequest { + if (!isRecord(msg)) return false; + return typeof msg.id === "number" && typeof msg.method === "string"; } -function isJsonRpcNotification(msg: JsonRpcMessage): msg is JsonRpcNotification { - const m = msg as unknown as Record; - return typeof m.method === "string" && typeof m.id !== "number"; +function isJsonRpcNotification(msg: unknown): msg is JsonRpcNotification { + if (!isRecord(msg)) return false; + return typeof msg.method === "string" && typeof msg.id !== "number"; } function toObjectRecord(value: unknown): Record | undefined { - if (!value || typeof value !== "object" || Array.isArray(value)) { - return undefined; - } - return value as Record; + return isRecord(value) ? value : undefined; } interface PendingRequest { @@ -615,9 +622,28 @@ interface CodexModel { supportedReasoningEfforts?: CodexReasoningEffortEntry[]; } -interface CodexModelListResponse { - data?: CodexModel[]; -} +const CodexModelListResponseSchema = z.object({ + data: z + .array( + z.object({ + id: z.string(), + displayName: z.string().optional(), + description: z.string().optional(), + isDefault: z.boolean().optional(), + model: z.string().optional(), + defaultReasoningEffort: z.string().optional(), + supportedReasoningEfforts: z + .array( + z.object({ + reasoningEffort: z.string().optional(), + description: z.string().optional(), + }), + ) + .optional(), + }), + ) + .optional(), +}); class CodexAppServerClient { private readonly rl: readline.Interface; @@ -740,31 +766,30 @@ class CodexAppServerClient { private async handleLine(line: string): Promise { if (!line.trim()) return; - const raw = JSON.parse(line); - const msg = toObjectRecord(raw) as unknown as JsonRpcMessage; - if (!msg) { + const raw: unknown = JSON.parse(line); + if (!isRecord(raw)) { this.logger.warn({ line }, "Parsed JSON is not an object"); return; } - if (isJsonRpcResponse(msg)) { - const id = msg.id; - if (msg.result !== undefined || msg.error) { + if (isJsonRpcResponse(raw)) { + const id = raw.id; + if (raw.result !== undefined || raw.error) { const pending = this.pending.get(id); if (!pending) return; clearTimeout(pending.timer); this.pending.delete(id); - if (msg.error) { - pending.reject(new Error(msg.error.message ?? "Unknown error")); + if (raw.error) { + pending.reject(new Error(raw.error.message ?? "Unknown error")); } else { - pending.resolve(msg.result); + pending.resolve(raw.result); } return; } // Server-initiated request - if (isJsonRpcRequest(msg)) { - const request = msg; + if (isJsonRpcRequest(raw)) { + const request = raw; const handler = this.requestHandlers.get(request.method); try { const result = handler ? await handler(request.params) : {}; @@ -779,8 +804,8 @@ class CodexAppServerClient { } } - if (isJsonRpcNotification(msg)) { - this.notificationHandler?.(msg.method, msg.params); + if (isJsonRpcNotification(raw)) { + this.notificationHandler?.(raw.method, raw.params); } } } @@ -4322,14 +4347,16 @@ class CodexAppServerAgentSession implements AgentSession { } private handleCommandApprovalRequest(params: unknown): Promise { - const parsed = params as { - itemId: string; - threadId: string; - turnId: string; - command?: string | null; - cwd?: string | null; - reason?: string | null; - }; + const parsed = z + .object({ + itemId: z.string(), + threadId: z.string(), + turnId: z.string(), + command: z.string().nullable().optional(), + cwd: z.string().nullable().optional(), + reason: z.string().nullable().optional(), + }) + .parse(params); const commandPreview = mapCodexExecNotificationToToolCall({ callId: parsed.itemId, command: parsed.command, @@ -4371,12 +4398,14 @@ class CodexAppServerAgentSession implements AgentSession { } private handleFileChangeApprovalRequest(params: unknown): Promise { - const parsed = params as { - itemId: string; - threadId: string; - turnId: string; - reason?: string | null; - }; + const parsed = z + .object({ + itemId: z.string(), + threadId: z.string(), + turnId: z.string(), + reason: z.string().nullable().optional(), + }) + .parse(params); const requestId = `permission-${parsed.itemId}`; const request: AgentPermissionRequest = { id: requestId, @@ -4406,12 +4435,14 @@ class CodexAppServerAgentSession implements AgentSession { } private handleToolApprovalRequest(params: unknown): Promise { - const parsed = params as { - itemId: string; - threadId: string; - turnId: string; - questions: unknown[]; - }; + const parsed = z + .object({ + itemId: z.string(), + threadId: z.string(), + turnId: z.string(), + questions: z.array(z.unknown()), + }) + .parse(params); const requestId = `permission-${parsed.itemId}`; const questions = normalizeCodexQuestionPrompts(parsed.questions); const request: AgentPermissionRequest = { @@ -4475,14 +4506,16 @@ export class CodexAppServerAgentClient implements AgentClient { }, "Spawning Codex app server", ); - return spawnProcess(launchPrefix.command, [...launchPrefix.args, "app-server"], { + const child = spawnProcess(launchPrefix.command, [...launchPrefix.args, "app-server"], { detached: process.platform !== "win32", stdio: ["pipe", "pipe", "pipe"], ...createProviderEnvSpec({ runtimeSettings: this.runtimeSettings, overlays: [launchEnv], }), - }) as ChildProcessWithoutNullStreams; + }); + assertChildWithPipes(child); + return child; } async createSession( @@ -4508,7 +4541,7 @@ export class CodexAppServerAgentClient implements AgentClient { overrides?: Partial, launchContext?: AgentLaunchContext, ): Promise { - const storedConfig = (handle.metadata ?? {}) as unknown as AgentSessionConfig; + const storedConfig = (handle.metadata ?? {}) as Partial; const merged: AgentSessionConfig = { ...storedConfig, ...overrides, @@ -4599,8 +4632,9 @@ export class CodexAppServerAgentClient implements AgentClient { await client.request("initialize", buildCodexAppServerInitializeParams()); client.notify("initialized", {}); - const response = (await client.request("model/list", {})) as CodexModelListResponse; - const models = Array.isArray(response?.data) ? response.data : []; + const rawResponse = await client.request("model/list", {}); + const parsedResponse = CodexModelListResponseSchema.safeParse(rawResponse); + const models = parsedResponse.success ? (parsedResponse.data.data ?? []) : []; const configuredDefaults = await readCodexConfiguredDefaults(client, this.logger); const configuredDefaultModelId = configuredDefaults.model; const configuredDefaultThinkingOptionId = configuredDefaults.thinkingOptionId; diff --git a/packages/server/src/utils/checkout-git.ts b/packages/server/src/utils/checkout-git.ts index cc6151b76..226934906 100644 --- a/packages/server/src/utils/checkout-git.ts +++ b/packages/server/src/utils/checkout-git.ts @@ -44,6 +44,18 @@ interface PullRequestStatusLookupTarget { headRepositoryOwner?: string; } +function getErrorStderr(error: Error): string { + return "stderr" in error && typeof error.stderr === "string" ? error.stderr : ""; +} + +function getErrorStdout(error: Error): string { + return "stdout" in error && typeof error.stdout === "string" ? error.stdout : ""; +} + +function throwBranchNotFound(branch: string | undefined): never { + throw new Error(`Branch not found: ${branch ?? "unknown"}`); +} + function createPullRequestStatusCache(ttlMs: number) { return new TTLCache({ ttl: ttlMs, @@ -380,8 +392,8 @@ export async function checkoutResolvedBranch( cwd, }); return { source: "remote" }; - case "not-found": - throw new Error(`Branch not found: ${input.requestedBranch ?? "unknown"}`); + default: + return throwBranchNotFound(input.requestedBranch); } } @@ -1967,7 +1979,7 @@ async function detectAndThrowMergeToBaseConflict( const { operationCwd, error, baseRef, currentBranch } = input; const errorDetails = error instanceof Error - ? `${error.message}\n${(error as { stderr?: string }).stderr ?? ""}\n${(error as { stdout?: string }).stdout ?? ""}` + ? `${error.message}\n${getErrorStderr(error)}\n${getErrorStdout(error)}` : String(error); try { const [unmergedOutput, lsFilesOutput, statusOutput] = await Promise.all([ @@ -1990,7 +2002,7 @@ async function detectAndThrowMergeToBaseConflict( .split("\n") .map((line) => line.trim()) .filter(Boolean) - .map((line) => line.split("\t").pop() as string), + .map((line) => line.split("\t").at(-1) ?? ""), ...statusConflicts, ].filter(Boolean); const conflictDetected = @@ -2150,7 +2162,7 @@ async function detectAndThrowMergeFromBaseConflict( const { cwd, error, baseRef, currentBranch } = input; const errorDetails = error instanceof Error - ? `${error.message}\n${(error as { stderr?: string }).stderr ?? ""}\n${(error as { stdout?: string }).stdout ?? ""}` + ? `${error.message}\n${getErrorStderr(error)}\n${getErrorStdout(error)}` : String(error); try { const [unmergedOutput, lsFilesOutput, statusOutput] = await Promise.all([ @@ -2173,7 +2185,7 @@ async function detectAndThrowMergeFromBaseConflict( .split("\n") .map((line) => line.trim()) .filter(Boolean) - .map((line) => line.split("\t").pop() as string), + .map((line) => line.split("\t").at(-1) ?? ""), ...statusConflicts, ].filter(Boolean); const conflictDetected =