From 54b8126debc0fefe4f3f02e7dd17036cb819485f Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 16 Apr 2026 16:15:38 +0700 Subject: [PATCH 01/21] docs: add 0.1.57 changelog --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1774840e5..88444c30f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## 0.1.57 - 2026-04-16 + +### Added +- Markdown files render as formatted markdown in the file pane instead of raw source. ([#427](https://github.com/getpaseo/paseo/pull/427) by [@aaronflorey](https://github.com/aaronflorey)) +- Cmd+L (Ctrl+L on Windows/Linux) focuses the agent message input, with a hint shown when the agent panel is active. +- Provider model lists refresh on a freshness TTL, and Settings surfaces when models were last updated along with any fetch errors inline. ([#426](https://github.com/getpaseo/paseo/pull/426)) +- `disallowedTools` option in provider config to block specific tools from an agent. + +### Improved +- Windows: Codex startup and provider resolution are significantly more resilient — handles quoted paths, `.cmd` shims, and missing binaries without crashing. ([#454](https://github.com/getpaseo/paseo/pull/454)) +- OpenCode reliability — permission prompts now include the requesting tool's context, todo and compaction events render in the timeline, sessions archive cleanly on close, and slash commands recover from SSE timeouts. ([#398](https://github.com/getpaseo/paseo/pull/398), [#407](https://github.com/getpaseo/paseo/pull/407), [#408](https://github.com/getpaseo/paseo/pull/408), [#429](https://github.com/getpaseo/paseo/pull/429) by [@aaronflorey](https://github.com/aaronflorey)) +- Paseo MCP tools work against archived agents, matching the CLI's behaviour. ([#423](https://github.com/getpaseo/paseo/pull/423)) +- Native scrollbars match the active theme across all web views. ([#399](https://github.com/getpaseo/paseo/pull/399) by [@ethersh](https://github.com/ethersh)) + +### Fixed +- Code file previews can be selected and copied on iOS. ([#447](https://github.com/getpaseo/paseo/pull/447) by [@muzhi1991](https://github.com/muzhi1991)) +- File preview no longer shows stale content when reopening the same file. ([#411](https://github.com/getpaseo/paseo/pull/411) by [@muzhi1991](https://github.com/muzhi1991)) +- File explorer reinitialises when the client reconnects after a page refresh. ([#442](https://github.com/getpaseo/paseo/pull/442) by [@1996fanrui](https://github.com/1996fanrui)) +- Generic ACP providers no longer receive duplicated command arguments. ([#444](https://github.com/getpaseo/paseo/pull/444) by [@edvardchen](https://github.com/edvardchen)) +- Workspace headers no longer show a branch icon for non-git workspaces. +- Branch switcher layout is stable on mobile. +- Model names no longer truncate mid-word in the picker rows. +- Messages appear in the correct order after reconnecting on mobile. +- Clearing agent attention no longer throws on timeout. + ## 0.1.56 - 2026-04-14 ### Fixed From 41fd45588f348974b2b2174e77ab45e3232496af Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 16 Apr 2026 16:16:30 +0700 Subject: [PATCH 02/21] chore(release): cut 0.1.57 --- package-lock.json | 38 ++++++++++++------------ package.json | 2 +- packages/app/package.json | 8 ++--- packages/cli/package.json | 6 ++-- packages/desktop/package.json | 6 ++-- packages/expo-two-way-audio/package.json | 2 +- packages/highlight/package.json | 2 +- packages/relay/package.json | 2 +- packages/server/package.json | 6 ++-- packages/website/package.json | 2 +- 10 files changed, 37 insertions(+), 37 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6a4a735f1..f31b01380 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "paseo", - "version": "0.1.57-rc.1", + "version": "0.1.57", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "paseo", - "version": "0.1.57-rc.1", + "version": "0.1.57", "hasInstallScript": true, "license": "AGPL-3.0-or-later", "workspaces": [ @@ -34906,16 +34906,16 @@ }, "packages/app": { "name": "@getpaseo/app", - "version": "0.1.57-rc.1", + "version": "0.1.57", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@expo/vector-icons": "^15.0.2", "@floating-ui/react-native": "^0.10.7", - "@getpaseo/expo-two-way-audio": "0.1.57-rc.1", - "@getpaseo/highlight": "0.1.57-rc.1", - "@getpaseo/server": "0.1.57-rc.1", + "@getpaseo/expo-two-way-audio": "0.1.57", + "@getpaseo/highlight": "0.1.57", + "@getpaseo/server": "0.1.57", "@gorhom/bottom-sheet": "^5.2.6", "@gorhom/portal": "^1.0.14", "@react-native-async-storage/async-storage": "2.2.0", @@ -35056,11 +35056,11 @@ }, "packages/cli": { "name": "@getpaseo/cli", - "version": "0.1.57-rc.1", + "version": "0.1.57", "dependencies": { "@clack/prompts": "^1.0.0", - "@getpaseo/relay": "0.1.57-rc.1", - "@getpaseo/server": "0.1.57-rc.1", + "@getpaseo/relay": "0.1.57", + "@getpaseo/server": "0.1.57", "chalk": "^5.3.0", "commander": "^12.0.0", "mime-types": "^2.1.35", @@ -35101,11 +35101,11 @@ }, "packages/desktop": { "name": "@getpaseo/desktop", - "version": "0.1.57-rc.1", + "version": "0.1.57", "license": "AGPL-3.0-or-later", "dependencies": { - "@getpaseo/cli": "0.1.57-rc.1", - "@getpaseo/server": "0.1.57-rc.1", + "@getpaseo/cli": "0.1.57", + "@getpaseo/server": "0.1.57", "electron-log": "^5.4.3", "electron-updater": "^6.6.2", "ws": "^8.14.2" @@ -35139,7 +35139,7 @@ }, "packages/expo-two-way-audio": { "name": "@getpaseo/expo-two-way-audio", - "version": "0.1.57-rc.1", + "version": "0.1.57", "license": "MIT", "devDependencies": { "@biomejs/biome": "1.9.4", @@ -35340,7 +35340,7 @@ }, "packages/highlight": { "name": "@getpaseo/highlight", - "version": "0.1.57-rc.1", + "version": "0.1.57", "dependencies": { "@lezer/common": "^1.5.0", "@lezer/cpp": "^1.1.5", @@ -35366,7 +35366,7 @@ }, "packages/relay": { "name": "@getpaseo/relay", - "version": "0.1.57-rc.1", + "version": "0.1.57", "dependencies": { "base64-js": "^1.5.1", "tweetnacl": "^1.0.3", @@ -35382,14 +35382,14 @@ }, "packages/server": { "name": "@getpaseo/server", - "version": "0.1.57-rc.1", + "version": "0.1.57", "dependencies": { "@agentclientprotocol/sdk": "^0.17.1", "@ai-sdk/openai": "2.0.52", "@anthropic-ai/claude-agent-sdk": "^0.2.11", "@deepgram/sdk": "^3.4.0", - "@getpaseo/highlight": "0.1.57-rc.1", - "@getpaseo/relay": "0.1.57-rc.1", + "@getpaseo/highlight": "0.1.57", + "@getpaseo/relay": "0.1.57", "@isaacs/ttlcache": "^2.1.4", "@modelcontextprotocol/sdk": "^1.20.1", "@opencode-ai/sdk": "1.2.6", @@ -35842,7 +35842,7 @@ }, "packages/website": { "name": "@getpaseo/website", - "version": "0.1.57-rc.1", + "version": "0.1.57", "dependencies": { "@cloudflare/vite-plugin": "^1.20.3", "@cloudflare/workers-types": "^4.20260114.0", diff --git a/package.json b/package.json index 3dd99ed9d..f2be49196 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "paseo", - "version": "0.1.57-rc.1", + "version": "0.1.57", "private": true, "workspaces": [ "packages/expo-two-way-audio", diff --git a/packages/app/package.json b/packages/app/package.json index 6670e35e7..c6e01bd9a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,7 +1,7 @@ { "name": "@getpaseo/app", "main": "index.ts", - "version": "0.1.57-rc.1", + "version": "0.1.57", "private": true, "scripts": { "start": "expo start", @@ -31,9 +31,9 @@ "@dnd-kit/utilities": "^3.2.2", "@expo/vector-icons": "^15.0.2", "@floating-ui/react-native": "^0.10.7", - "@getpaseo/expo-two-way-audio": "0.1.57-rc.1", - "@getpaseo/highlight": "0.1.57-rc.1", - "@getpaseo/server": "0.1.57-rc.1", + "@getpaseo/expo-two-way-audio": "0.1.57", + "@getpaseo/highlight": "0.1.57", + "@getpaseo/server": "0.1.57", "@gorhom/bottom-sheet": "^5.2.6", "@gorhom/portal": "^1.0.14", "@react-native-async-storage/async-storage": "2.2.0", diff --git a/packages/cli/package.json b/packages/cli/package.json index f4056bfc0..e0ed3008a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/cli", - "version": "0.1.57-rc.1", + "version": "0.1.57", "description": "Paseo CLI - control your AI coding agents from the command line", "type": "module", "files": [ @@ -24,8 +24,8 @@ }, "dependencies": { "@clack/prompts": "^1.0.0", - "@getpaseo/relay": "0.1.57-rc.1", - "@getpaseo/server": "0.1.57-rc.1", + "@getpaseo/relay": "0.1.57", + "@getpaseo/server": "0.1.57", "chalk": "^5.3.0", "commander": "^12.0.0", "mime-types": "^2.1.35", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 36843f596..7f3777500 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/desktop", - "version": "0.1.57-rc.1", + "version": "0.1.57", "private": true, "description": "Paseo desktop app (Electron wrapper)", "main": "dist/main.js", @@ -13,8 +13,8 @@ "typecheck": "tsc --noEmit -p tsconfig.json" }, "dependencies": { - "@getpaseo/cli": "0.1.57-rc.1", - "@getpaseo/server": "0.1.57-rc.1", + "@getpaseo/cli": "0.1.57", + "@getpaseo/server": "0.1.57", "electron-log": "^5.4.3", "electron-updater": "^6.6.2", "ws": "^8.14.2" diff --git a/packages/expo-two-way-audio/package.json b/packages/expo-two-way-audio/package.json index 0a7d355ec..c5302ad9e 100644 --- a/packages/expo-two-way-audio/package.json +++ b/packages/expo-two-way-audio/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/expo-two-way-audio", - "version": "0.1.57-rc.1", + "version": "0.1.57", "description": "Native module for two way audio streaming", "main": "build/index.js", "types": "build/index.d.ts", diff --git a/packages/highlight/package.json b/packages/highlight/package.json index 1ec82f7c0..75e55e913 100644 --- a/packages/highlight/package.json +++ b/packages/highlight/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/highlight", - "version": "0.1.57-rc.1", + "version": "0.1.57", "type": "module", "publishConfig": { "access": "public" diff --git a/packages/relay/package.json b/packages/relay/package.json index 00ca118e3..c729d92f8 100644 --- a/packages/relay/package.json +++ b/packages/relay/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/relay", - "version": "0.1.57-rc.1", + "version": "0.1.57", "description": "Paseo relay for bridging daemon and client connections", "type": "module", "publishConfig": { diff --git a/packages/server/package.json b/packages/server/package.json index f2b7a6329..f19b4a0e9 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/server", - "version": "0.1.57-rc.1", + "version": "0.1.57", "description": "Paseo backend server", "type": "module", "publishConfig": { @@ -64,8 +64,8 @@ "@ai-sdk/openai": "2.0.52", "@anthropic-ai/claude-agent-sdk": "^0.2.11", "@deepgram/sdk": "^3.4.0", - "@getpaseo/highlight": "0.1.57-rc.1", - "@getpaseo/relay": "0.1.57-rc.1", + "@getpaseo/highlight": "0.1.57", + "@getpaseo/relay": "0.1.57", "@isaacs/ttlcache": "^2.1.4", "@modelcontextprotocol/sdk": "^1.20.1", "@opencode-ai/sdk": "1.2.6", diff --git a/packages/website/package.json b/packages/website/package.json index d2b5bc0b0..e8b7445f2 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/website", - "version": "0.1.57-rc.1", + "version": "0.1.57", "private": true, "type": "module", "scripts": { From a3822ca2993624d95ad9fa78004559150efa5888 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 16 Apr 2026 09:17:53 +0000 Subject: [PATCH 03/21] fix: update lockfile signatures and Nix hash --- nix/package.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/package.nix b/nix/package.nix index 82309bdad..ed43a804e 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -42,7 +42,7 @@ buildNpmPackage rec { # To update: run `nix build` with lib.fakeHash, copy the `got:` hash. # CI auto-updates this when package-lock.json changes (see .github/workflows/). - npmDepsHash = "sha256-Wn7g+VXuUe0LI733MUj+WRX/fJQHOO/HFEkqyHrWTf0="; + npmDepsHash = "sha256-t3QD52Yfi2VEQxspGJMtSINFnn2Hp1wAzu+SYDIIY4Y="; # Prevent onnxruntime-node's install script from running during automatic # npm rebuild (it tries to download from api.nuget.org, which fails in the sandbox). From 917c2f56f40e1320daa1fb67348989397e8766a8 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 16 Apr 2026 16:36:57 +0700 Subject: [PATCH 04/21] test(server): reproduce Windows spawn EINVAL with cmd shim + spaces + JSON args --- .github/workflows/ci.yml | 2 + .../providers/provider-windows-launch.test.ts | 217 ++++++++++++++++++ .../src/utils/spawn.launch-regression.test.ts | 171 ++++++++++++++ 3 files changed, 390 insertions(+) create mode 100644 packages/server/src/server/agent/providers/provider-windows-launch.test.ts create mode 100644 packages/server/src/utils/spawn.launch-regression.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8f438ed8..3d04b0710 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -102,6 +102,7 @@ jobs: run: > npx vitest run src/utils/executable.test.ts + src/utils/spawn.launch-regression.test.ts src/utils/spawn.test.ts src/utils/run-git-command.test.ts src/utils/checkout-git-rev-parse.test.ts @@ -109,6 +110,7 @@ jobs: src/server/agent/provider-launch-config.test.ts src/server/agent/provider-snapshot-manager.test.ts src/server/agent/providers/claude-agent.spawn.test.ts + src/server/agent/providers/provider-windows-launch.test.ts src/server/agent/providers/provider-availability.test.ts src/server/workspace-registry-model.test.ts src/server/persisted-config.test.ts diff --git a/packages/server/src/server/agent/providers/provider-windows-launch.test.ts b/packages/server/src/server/agent/providers/provider-windows-launch.test.ts new file mode 100644 index 000000000..c099b2561 --- /dev/null +++ b/packages/server/src/server/agent/providers/provider-windows-launch.test.ts @@ -0,0 +1,217 @@ +import type { ChildProcess } from "node:child_process"; +import { copyFileSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, test } from "vitest"; + +import { findExecutable } from "../../../utils/executable.js"; +import { spawnProcess } from "../../../utils/spawn.js"; + +type SpawnResult = { + code: number | null; + signal: NodeJS.Signals | null; + stdout: string; + stderr: string; + error: Error | null; +}; + +type ProviderLaunchCase = { + provider: "claude" | "codex" | "opencode" | "generic-acp"; + binaryName: string; + args: string[]; + shell?: boolean; +}; + +const JSON_ARG = '{"mcpServers":{"paseo":{"type":"http","url":"http://127.0.0.1:6767/mcp"}}}'; +const tempDirs: string[] = []; + +function makeFixture(binaryName: string, expectedArgs: string[]) { + const root = mkdtempSync(path.join(tmpdir(), `paseo ${binaryName} launch `)); + tempDirs.push(root); + + const fakeDaemonNode = path.join(root, "Fake Paseo.exe"); + copyFileSync(process.execPath, fakeDaemonNode); + + const assertScript = path.join(root, "assert-argv.js"); + writeFileSync( + assertScript, + ` +if (process.argv.includes("--version")) { + console.log("fake-provider 1.0.0"); + process.exit(0); +} + +const expected = JSON.parse(process.env.PASEO_EXPECTED_ARGV_JSON); +const actual = process.argv.slice(2); +if (JSON.stringify(actual) !== JSON.stringify(expected)) { + console.error("ARGV_MISMATCH"); + console.error(JSON.stringify({ expected, actual })); + process.exit(42); +} +console.log("ARGV_OK"); +`, + ); + + const shim = path.join(root, `${binaryName}.cmd`); + writeFileSync( + shim, + ["@echo off", "setlocal", `\"${fakeDaemonNode}\" \"${assertScript}\" %*`, ""].join("\r\n"), + ); + + return { root, shim, expectedArgs }; +} + +function collectChild(child: ChildProcess, timeoutMs = 10_000): Promise { + return new Promise((resolve) => { + const stdoutChunks: Buffer[] = []; + const stderrChunks: Buffer[] = []; + let error: Error | null = null; + let settled = false; + + const settle = (result: Pick) => { + if (settled) { + return; + } + settled = true; + clearTimeout(timer); + resolve({ + ...result, + stdout: Buffer.concat(stdoutChunks).toString("utf8"), + stderr: Buffer.concat(stderrChunks).toString("utf8"), + error, + }); + }; + + const timer = setTimeout(() => { + child.kill("SIGKILL"); + settle({ code: null, signal: "SIGKILL" }); + }, timeoutMs); + timer.unref?.(); + + child.stdout?.on("data", (chunk: Buffer | string) => { + stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + }); + child.stderr?.on("data", (chunk: Buffer | string) => { + stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + }); + child.once("error", (err) => { + error = err; + settle({ code: null, signal: null }); + }); + child.once("exit", (code, signal) => { + settle({ code, signal }); + }); + }); +} + +async function runProviderFixture(params: { + command: string; + args: string[]; + expectedArgs: string[]; + shell?: boolean; +}): Promise { + const child = spawnProcess(params.command, params.args, { + env: { + ...process.env, + PASEO_EXPECTED_ARGV_JSON: JSON.stringify(params.expectedArgs), + }, + stdio: ["ignore", "pipe", "pipe"], + ...(params.shell === undefined ? {} : { shell: params.shell }), + }); + return collectChild(child); +} + +function withPathEntry(dir: string, run: () => Promise): Promise { + const pathKey = + process.platform === "win32" + ? (Object.keys(process.env).find((key) => key.toLowerCase() === "path") ?? "Path") + : "PATH"; + const previousPath = process.env[pathKey]; + process.env[pathKey] = previousPath ? `${dir}${path.delimiter}${previousPath}` : dir; + + return run().finally(() => { + if (previousPath === undefined) { + delete process.env[pathKey]; + } else { + process.env[pathKey] = previousPath; + } + }); +} + +afterEach(() => { + for (const dir of tempDirs.splice(0)) { + rmSync(dir, { recursive: true, force: true }); + } +}); + +const providerLaunchCases: ProviderLaunchCase[] = [ + { + provider: "claude", + binaryName: "claude", + args: ["--mcp-config", JSON_ARG], + shell: false, + }, + { + provider: "codex", + binaryName: "codex", + args: ["app-server", "--config", JSON_ARG], + }, + { + provider: "opencode", + binaryName: "opencode", + args: ["serve", "--port", "49271", "--config", JSON_ARG], + }, + { + provider: "generic-acp", + binaryName: "generic-acp", + args: ["--mcp-config", JSON_ARG], + }, +]; + +describe.runIf(process.platform === "win32")("Windows provider launch parity", () => { + test("detected claude.cmd can be launched with Claude's JSON-safe spawn shape", async () => { + const args = ["--mcp-config", JSON_ARG]; + const fixture = makeFixture("claude", args); + + await withPathEntry(fixture.root, async () => { + const detected = await findExecutable("claude"); + expect(detected?.toLowerCase()).toBe(fixture.shim.toLowerCase()); + + const result = await runProviderFixture({ + command: detected!, + args, + expectedArgs: args, + shell: false, + }); + + expect(result.error).toBeNull(); + expect(result.code).toBe(0); + expect(result.signal).toBeNull(); + expect(result.stderr).toBe(""); + expect(result.stdout.trim()).toBe("ARGV_OK"); + }); + }); + + test.each( + providerLaunchCases, + )("$provider launches a cmd shim from a path with spaces through spawnProcess", async ({ + binaryName, + args, + shell, + }) => { + const fixture = makeFixture(binaryName, args); + + const result = await runProviderFixture({ + command: fixture.shim, + args: fixture.expectedArgs, + expectedArgs: fixture.expectedArgs, + shell, + }); + + expect(result.error).toBeNull(); + expect(result.code).toBe(0); + expect(result.signal).toBeNull(); + expect(result.stderr).toBe(""); + expect(result.stdout.trim()).toBe("ARGV_OK"); + }); +}); diff --git a/packages/server/src/utils/spawn.launch-regression.test.ts b/packages/server/src/utils/spawn.launch-regression.test.ts new file mode 100644 index 000000000..1ef258edf --- /dev/null +++ b/packages/server/src/utils/spawn.launch-regression.test.ts @@ -0,0 +1,171 @@ +import { copyFileSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import type { ChildProcess } from "node:child_process"; +import { afterEach, describe, expect, test } from "vitest"; + +import { spawnProcess } from "./spawn.js"; + +type SpawnResult = { + code: number | null; + signal: NodeJS.Signals | null; + stdout: string; + stderr: string; + error: Error | null; +}; + +const tempDirs: string[] = []; +const JSON_ARG = '{"key":"value with spaces","nested":{"quote":"\\"yes\\""}}'; + +function makeFixture(): { + root: string; + fakeDaemonNode: string; + shim: string; + assertScript: string; + expectedArgs: string[]; +} { + const root = mkdtempSync(path.join(tmpdir(), "paseo spawn regression ")); + tempDirs.push(root); + + const fakeDaemonNode = path.join(root, "Fake Paseo.exe"); + copyFileSync(process.execPath, fakeDaemonNode); + + const expectedArgs = ["--config", JSON_ARG]; + const assertScript = path.join(root, "assert-argv.js"); + writeFileSync( + assertScript, + ` +const expected = JSON.parse(process.env.PASEO_EXPECTED_ARGV_JSON); +const actual = process.argv.slice(2); +if (JSON.stringify(actual) !== JSON.stringify(expected)) { + console.error("ARGV_MISMATCH"); + console.error(JSON.stringify({ expected, actual })); + process.exit(42); +} +console.log("ARGV_OK"); +`, + ); + + const shim = path.join(root, "claude.cmd"); + writeFileSync( + shim, + ["@echo off", "setlocal", `\"${fakeDaemonNode}\" \"${assertScript}\" %*`, ""].join("\r\n"), + ); + + return { root, fakeDaemonNode, shim, assertScript, expectedArgs }; +} + +function collectChild(child: ChildProcess, timeoutMs = 10_000): Promise { + return new Promise((resolve) => { + const stdoutChunks: Buffer[] = []; + const stderrChunks: Buffer[] = []; + let error: Error | null = null; + let settled = false; + + const settle = (result: Pick) => { + if (settled) { + return; + } + settled = true; + clearTimeout(timer); + resolve({ + ...result, + stdout: Buffer.concat(stdoutChunks).toString("utf8"), + stderr: Buffer.concat(stderrChunks).toString("utf8"), + error, + }); + }; + + const timer = setTimeout(() => { + child.kill("SIGKILL"); + settle({ code: null, signal: "SIGKILL" }); + }, timeoutMs); + timer.unref?.(); + + child.stdout?.on("data", (chunk: Buffer | string) => { + stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + }); + child.stderr?.on("data", (chunk: Buffer | string) => { + stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + }); + child.once("error", (err) => { + error = err; + settle({ code: null, signal: null }); + }); + child.once("exit", (code, signal) => { + settle({ code, signal }); + }); + }); +} + +async function runFixture(params: { + command: string; + args: string[]; + shell?: boolean; +}): Promise { + const child = spawnProcess(params.command, params.args, { + env: { + ...process.env, + PASEO_EXPECTED_ARGV_JSON: JSON.stringify(["--config", JSON_ARG]), + }, + stdio: ["ignore", "pipe", "pipe"], + ...(params.shell === undefined ? {} : { shell: params.shell }), + }); + return collectChild(child); +} + +afterEach(() => { + for (const dir of tempDirs.splice(0)) { + rmSync(dir, { recursive: true, force: true }); + } +}); + +describe.runIf(process.platform === "win32")("Windows spawn launch regression", () => { + test("launches a cmd shim from a path with spaces without corrupting JSON args", async () => { + const fixture = makeFixture(); + + const result = await runFixture({ + command: fixture.shim, + args: fixture.expectedArgs, + }); + + expect(result.error).toBeNull(); + expect(result.code).toBe(0); + expect(result.signal).toBeNull(); + expect(result.stderr).toBe(""); + expect(result.stdout.trim()).toBe("ARGV_OK"); + }); + + test("direct launch with a space-containing executable preserves JSON args", async () => { + const fixture = makeFixture(); + + const result = await runFixture({ + command: fixture.fakeDaemonNode, + args: [fixture.assertScript, ...fixture.expectedArgs], + shell: false, + }); + + expect(result.error).toBeNull(); + expect(result.code).toBe(0); + expect(result.signal).toBeNull(); + expect(result.stderr).toBe(""); + expect(result.stdout.trim()).toBe("ARGV_OK"); + }); +}); + +describe.skipIf(process.platform === "win32")("spawn launch regression smoke", () => { + test("direct launch with a space-containing executable works on this platform", async () => { + const fixture = makeFixture(); + + const result = await runFixture({ + command: fixture.fakeDaemonNode, + args: [fixture.assertScript, ...fixture.expectedArgs], + shell: false, + }); + + expect(result.error).toBeNull(); + expect(result.code).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout.trim()).toBe("ARGV_OK"); + }); +}); From de33dfd0340e7133fc0edc0d2f39cc8aa55c03e7 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 16 Apr 2026 16:44:24 +0700 Subject: [PATCH 05/21] fix(server): share Windows launch handling across providers --- packages/server/src/utils/executable.ts | 9 ++++++--- packages/server/src/utils/spawn.ts | 18 +++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/server/src/utils/executable.ts b/packages/server/src/utils/executable.ts index 051a4994b..e259a2171 100644 --- a/packages/server/src/utils/executable.ts +++ b/packages/server/src/utils/executable.ts @@ -35,7 +35,7 @@ async function enumerateCandidates(name: string): Promise { }); } -function isWindowsCommandScript(executablePath: string): boolean { +export function isWindowsCommandScript(executablePath: string): boolean { const extension = extname(executablePath).toLowerCase(); return process.platform === "win32" && (extension === ".cmd" || extension === ".bat"); } @@ -141,8 +141,11 @@ function escapeWindowsCmdValue(value: string): string { const unquoted = isQuoted ? value.slice(1, -1) : value; const escaped = unquoted.replace(/%/g, "%%").replace(/([&|^<>()!])/g, "^$1"); - if (isQuoted || escaped.includes(" ")) { - return `"${escaped}"`; + if (isQuoted || /[\s"]/u.test(unquoted)) { + const quoted = escaped + .replace(/(\\*)"/g, (_match, slashes: string) => `${slashes}${slashes}\\"`) + .replace(/\\+$/u, (slashes) => `${slashes}${slashes}`); + return `"${quoted}"`; } return escaped; diff --git a/packages/server/src/utils/spawn.ts b/packages/server/src/utils/spawn.ts index bab7b047a..f7a0b0881 100644 --- a/packages/server/src/utils/spawn.ts +++ b/packages/server/src/utils/spawn.ts @@ -1,7 +1,7 @@ import { execFile, spawn, type ChildProcess, type SpawnOptions } from "node:child_process"; import { promisify } from "node:util"; -import { quoteWindowsArgument, quoteWindowsCommand } from "./executable.js"; +import { isWindowsCommandScript, quoteWindowsArgument, quoteWindowsCommand } from "./executable.js"; const execFileAsync = promisify(execFile); @@ -24,13 +24,15 @@ export function spawnProcess( options?: SpawnOptions, ): ChildProcess { const isWindows = process.platform === "win32"; + const shell = isWindowsCommandScript(command) ? true : (options?.shell ?? isWindows); - const resolvedCommand = isWindows ? quoteWindowsCommand(command) : command; - const resolvedArgs = isWindows ? args.map(quoteWindowsArgument) : args; + const shouldQuoteForShell = isWindows && shell !== false; + const resolvedCommand = shouldQuoteForShell ? quoteWindowsCommand(command) : command; + const resolvedArgs = shouldQuoteForShell ? args.map(quoteWindowsArgument) : args; return spawn(resolvedCommand, resolvedArgs, { ...options, - shell: options?.shell ?? isWindows, + shell, windowsHide: true, }); } @@ -41,8 +43,10 @@ export async function execCommand( options?: ExecCommandOptions, ): Promise { const isWindows = process.platform === "win32"; - const resolvedCommand = isWindows ? quoteWindowsCommand(command) : command; - const resolvedArgs = isWindows ? args.map(quoteWindowsArgument) : args; + const shell = isWindowsCommandScript(command) ? true : isWindows; + const shouldQuoteForShell = isWindows && shell !== false; + const resolvedCommand = shouldQuoteForShell ? quoteWindowsCommand(command) : command; + const resolvedArgs = shouldQuoteForShell ? args.map(quoteWindowsArgument) : args; return execFileAsync(resolvedCommand, resolvedArgs, { cwd: options?.cwd, @@ -50,7 +54,7 @@ export async function execCommand( encoding: options?.encoding ?? "utf8", timeout: options?.timeout, maxBuffer: options?.maxBuffer, - shell: isWindows, + shell, windowsHide: true, }) as Promise; } From c2a8e56d33b25cab6c4b7e26bad38722091bb015 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 16 Apr 2026 17:00:01 +0700 Subject: [PATCH 06/21] test(server): reproduce Windows EINVAL for .cmd shim with shell:false --- .../src/utils/spawn.launch-regression.test.ts | 16 ++++++++++++++++ packages/server/src/utils/spawn.ts | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/server/src/utils/spawn.launch-regression.test.ts b/packages/server/src/utils/spawn.launch-regression.test.ts index 1ef258edf..b43b88e1a 100644 --- a/packages/server/src/utils/spawn.launch-regression.test.ts +++ b/packages/server/src/utils/spawn.launch-regression.test.ts @@ -136,6 +136,22 @@ describe.runIf(process.platform === "win32")("Windows spawn launch regression", expect(result.stdout.trim()).toBe("ARGV_OK"); }); + test("launches a cmd shim even when the caller explicitly disables shell", async () => { + const fixture = makeFixture(); + + const result = await runFixture({ + command: fixture.shim, + args: fixture.expectedArgs, + shell: false, + }); + + expect(result.error).toBeNull(); + expect(result.code).toBe(0); + expect(result.signal).toBeNull(); + expect(result.stderr).toBe(""); + expect(result.stdout.trim()).toBe("ARGV_OK"); + }); + test("direct launch with a space-containing executable preserves JSON args", async () => { const fixture = makeFixture(); diff --git a/packages/server/src/utils/spawn.ts b/packages/server/src/utils/spawn.ts index f7a0b0881..5e0bbaa18 100644 --- a/packages/server/src/utils/spawn.ts +++ b/packages/server/src/utils/spawn.ts @@ -24,7 +24,7 @@ export function spawnProcess( options?: SpawnOptions, ): ChildProcess { const isWindows = process.platform === "win32"; - const shell = isWindowsCommandScript(command) ? true : (options?.shell ?? isWindows); + const shell = options?.shell ?? isWindows; const shouldQuoteForShell = isWindows && shell !== false; const resolvedCommand = shouldQuoteForShell ? quoteWindowsCommand(command) : command; From 9cee1d81960dc7937791589b54c8c463f0fd7859 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 16 Apr 2026 17:05:08 +0700 Subject: [PATCH 07/21] fix(server): restore .cmd shell routing for Windows EINVAL --- packages/server/src/utils/spawn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/utils/spawn.ts b/packages/server/src/utils/spawn.ts index 5e0bbaa18..f7a0b0881 100644 --- a/packages/server/src/utils/spawn.ts +++ b/packages/server/src/utils/spawn.ts @@ -24,7 +24,7 @@ export function spawnProcess( options?: SpawnOptions, ): ChildProcess { const isWindows = process.platform === "win32"; - const shell = options?.shell ?? isWindows; + const shell = isWindowsCommandScript(command) ? true : (options?.shell ?? isWindows); const shouldQuoteForShell = isWindows && shell !== false; const resolvedCommand = shouldQuoteForShell ? quoteWindowsCommand(command) : command; From e0f82e7ad0575cd879ec538d8e8fe61b6fcf640f Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 16 Apr 2026 17:52:44 +0700 Subject: [PATCH 08/21] test(server): reproduce Windows ps1 shim launch gap --- .../src/utils/spawn.launch-regression.test.ts | 77 ++++++++++++++++++- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/packages/server/src/utils/spawn.launch-regression.test.ts b/packages/server/src/utils/spawn.launch-regression.test.ts index b43b88e1a..29df428ae 100644 --- a/packages/server/src/utils/spawn.launch-regression.test.ts +++ b/packages/server/src/utils/spawn.launch-regression.test.ts @@ -4,6 +4,7 @@ import path from "node:path"; import type { ChildProcess } from "node:child_process"; import { afterEach, describe, expect, test } from "vitest"; +import { findExecutable } from "./executable.js"; import { spawnProcess } from "./spawn.js"; type SpawnResult = { @@ -17,10 +18,11 @@ type SpawnResult = { const tempDirs: string[] = []; const JSON_ARG = '{"key":"value with spaces","nested":{"quote":"\\"yes\\""}}'; -function makeFixture(): { +function makeFixture(options?: { includeCmdShim?: boolean }): { root: string; fakeDaemonNode: string; shim: string; + powerShellShim: string; assertScript: string; expectedArgs: string[]; } { @@ -35,6 +37,11 @@ function makeFixture(): { writeFileSync( assertScript, ` +if (process.argv.includes("--version")) { + console.log("fake-provider 1.0.0"); + process.exit(0); +} + const expected = JSON.parse(process.env.PASEO_EXPECTED_ARGV_JSON); const actual = process.argv.slice(2); if (JSON.stringify(actual) !== JSON.stringify(expected)) { @@ -47,12 +54,25 @@ console.log("ARGV_OK"); ); const shim = path.join(root, "claude.cmd"); + if (options?.includeCmdShim !== false) { + writeFileSync( + shim, + ["@echo off", "setlocal", `\"${fakeDaemonNode}\" \"${assertScript}\" %*`, ""].join("\r\n"), + ); + } + + const powerShellShim = path.join(root, "claude.ps1"); writeFileSync( - shim, - ["@echo off", "setlocal", `\"${fakeDaemonNode}\" \"${assertScript}\" %*`, ""].join("\r\n"), + powerShellShim, + [ + "$ErrorActionPreference = 'Stop'", + `& '${fakeDaemonNode.replace(/'/g, "''")}' '${assertScript.replace(/'/g, "''")}' @args`, + "exit $LASTEXITCODE", + "", + ].join("\r\n"), ); - return { root, fakeDaemonNode, shim, assertScript, expectedArgs }; + return { root, fakeDaemonNode, shim, powerShellShim, assertScript, expectedArgs }; } function collectChild(child: ChildProcess, timeoutMs = 10_000): Promise { @@ -114,6 +134,34 @@ async function runFixture(params: { return collectChild(child); } +function withWindowsPathEntry(dir: string, run: () => Promise): Promise { + const pathKey = + process.platform === "win32" + ? (Object.keys(process.env).find((key) => key.toLowerCase() === "path") ?? "Path") + : "PATH"; + const previousPath = process.env[pathKey]; + const previousPathExt = process.env.PATHEXT; + + process.env[pathKey] = previousPath ? `${dir}${path.delimiter}${previousPath}` : dir; + process.env.PATHEXT = previousPathExt?.toLowerCase().includes(".ps1") + ? previousPathExt + : `${previousPathExt ?? ""};.PS1`; + + return run().finally(() => { + if (previousPath === undefined) { + delete process.env[pathKey]; + } else { + process.env[pathKey] = previousPath; + } + + if (previousPathExt === undefined) { + delete process.env.PATHEXT; + } else { + process.env.PATHEXT = previousPathExt; + } + }); +} + afterEach(() => { for (const dir of tempDirs.splice(0)) { rmSync(dir, { recursive: true, force: true }); @@ -167,6 +215,27 @@ describe.runIf(process.platform === "win32")("Windows spawn launch regression", expect(result.stderr).toBe(""); expect(result.stdout.trim()).toBe("ARGV_OK"); }); + + test("detects and launches a PowerShell shim from PATH without corrupting JSON args", async () => { + const fixture = makeFixture({ includeCmdShim: false }); + + await withWindowsPathEntry(fixture.root, async () => { + const detected = await findExecutable("claude"); + expect(detected?.toLowerCase()).toBe(fixture.powerShellShim.toLowerCase()); + + const result = await runFixture({ + command: detected!, + args: fixture.expectedArgs, + shell: false, + }); + + expect(result.error).toBeNull(); + expect(result.code).toBe(0); + expect(result.signal).toBeNull(); + expect(result.stderr).toBe(""); + expect(result.stdout.trim()).toBe("ARGV_OK"); + }); + }); }); describe.skipIf(process.platform === "win32")("spawn launch regression smoke", () => { From 086b3098b1075b9149771b00822db0f0cba31b5b Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 16 Apr 2026 17:58:12 +0700 Subject: [PATCH 09/21] fix(server): support Windows PowerShell shims in shared launcher --- packages/server/src/utils/executable.ts | 27 ++++++++++++++++++------ packages/server/src/utils/spawn.ts | 28 ++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/packages/server/src/utils/executable.ts b/packages/server/src/utils/executable.ts index e259a2171..3191699c9 100644 --- a/packages/server/src/utils/executable.ts +++ b/packages/server/src/utils/executable.ts @@ -40,6 +40,14 @@ export function isWindowsCommandScript(executablePath: string): boolean { return process.platform === "win32" && (extension === ".cmd" || extension === ".bat"); } +export function isWindowsPowerShellScript(executablePath: string): boolean { + return process.platform === "win32" && extname(executablePath).toLowerCase() === ".ps1"; +} + +export function windowsPowerShellScriptArgs(scriptPath: string, args: string[]): string[] { + return ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath, ...args]; +} + async function probeExecutable(executablePath: string): Promise { return await new Promise((resolve) => { let settled = false; @@ -59,12 +67,19 @@ async function probeExecutable(executablePath: string): Promise { let child: ChildProcess; try { - child = spawn(executablePath, ["--version"], { - stdio: "ignore", - windowsHide: true, - // Windows batch shims (.cmd/.bat) require cmd.exe; native binaries do not. - shell: isWindowsCommandScript(executablePath), - }); + const isPowerShellScript = isWindowsPowerShellScript(executablePath); + child = spawn( + isPowerShellScript ? "powershell.exe" : executablePath, + isPowerShellScript + ? windowsPowerShellScriptArgs(executablePath, ["--version"]) + : ["--version"], + { + stdio: "ignore", + windowsHide: true, + // Windows batch shims (.cmd/.bat) require cmd.exe; native binaries do not. + shell: isWindowsCommandScript(executablePath), + }, + ); } catch { settle(false); return; diff --git a/packages/server/src/utils/spawn.ts b/packages/server/src/utils/spawn.ts index f7a0b0881..b5de30366 100644 --- a/packages/server/src/utils/spawn.ts +++ b/packages/server/src/utils/spawn.ts @@ -1,7 +1,13 @@ import { execFile, spawn, type ChildProcess, type SpawnOptions } from "node:child_process"; import { promisify } from "node:util"; -import { isWindowsCommandScript, quoteWindowsArgument, quoteWindowsCommand } from "./executable.js"; +import { + isWindowsCommandScript, + isWindowsPowerShellScript, + quoteWindowsArgument, + quoteWindowsCommand, + windowsPowerShellScriptArgs, +} from "./executable.js"; const execFileAsync = promisify(execFile); @@ -24,6 +30,14 @@ export function spawnProcess( options?: SpawnOptions, ): ChildProcess { const isWindows = process.platform === "win32"; + if (isWindowsPowerShellScript(command)) { + return spawn("powershell.exe", windowsPowerShellScriptArgs(command, args), { + ...options, + shell: false, + windowsHide: true, + }); + } + const shell = isWindowsCommandScript(command) ? true : (options?.shell ?? isWindows); const shouldQuoteForShell = isWindows && shell !== false; @@ -43,6 +57,18 @@ export async function execCommand( options?: ExecCommandOptions, ): Promise { const isWindows = process.platform === "win32"; + if (isWindowsPowerShellScript(command)) { + return execFileAsync("powershell.exe", windowsPowerShellScriptArgs(command, args), { + cwd: options?.cwd, + env: options?.env, + encoding: options?.encoding ?? "utf8", + timeout: options?.timeout, + maxBuffer: options?.maxBuffer, + shell: false, + windowsHide: true, + }) as Promise; + } + const shell = isWindowsCommandScript(command) ? true : isWindows; const shouldQuoteForShell = isWindows && shell !== false; const resolvedCommand = shouldQuoteForShell ? quoteWindowsCommand(command) : command; From 7442323ca2f0d0af756fa3e7a0385ed01d3e9c00 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 16 Apr 2026 18:04:17 +0700 Subject: [PATCH 10/21] fix(server): preserve JSON args for Windows PowerShell shims --- packages/server/src/utils/executable.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/server/src/utils/executable.ts b/packages/server/src/utils/executable.ts index 3191699c9..84a53543f 100644 --- a/packages/server/src/utils/executable.ts +++ b/packages/server/src/utils/executable.ts @@ -45,7 +45,18 @@ export function isWindowsPowerShellScript(executablePath: string): boolean { } export function windowsPowerShellScriptArgs(scriptPath: string, args: string[]): string[] { - return ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath, ...args]; + const argvBase64 = Buffer.from(JSON.stringify(args), "utf16le").toString("base64"); + const escapedScriptPath = scriptPath.replace(/'/g, "''"); + const command = [ + "$ErrorActionPreference = 'Stop'", + `$argvJson = [System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String('${argvBase64}'))`, + "$argv = @((ConvertFrom-Json -InputObject $argvJson))", + `& '${escapedScriptPath}' @argv`, + "exit $LASTEXITCODE", + ].join("; "); + const encodedCommand = Buffer.from(command, "utf16le").toString("base64"); + + return ["-NoProfile", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encodedCommand]; } async function probeExecutable(executablePath: string): Promise { From a2b743a6d3f23ad6b9bccb6c0161fe163a1b8bf2 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 16 Apr 2026 18:29:34 +0700 Subject: [PATCH 11/21] fix(server): emit valid MCP list_agents payloads Explicit `undefined` keys on optional snapshot fields were converted to `null` by `ensureValidJson`, failing `AgentSnapshotPayloadSchema` validation on the wire. Stop writing those keys so they remain absent, which is what `.optional()` expects. Also adds a test that parses `list_agents` output through the schema to catch this class of bug. --- .../src/server/agent/agent-projections.ts | 4 +- .../src/server/agent/mcp-server.test.ts | 55 +++++++++++++++++++ .../server/src/server/persistence-hooks.ts | 4 +- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/packages/server/src/server/agent/agent-projections.ts b/packages/server/src/server/agent/agent-projections.ts index 899df40f4..79fa0f99f 100644 --- a/packages/server/src/server/agent/agent-projections.ts +++ b/packages/server/src/server/agent/agent-projections.ts @@ -94,7 +94,7 @@ export function toAgentPayload( model: agent.config.model ?? null, thinkingOptionId, effectiveThinkingOptionId, - runtimeInfo, + ...(runtimeInfo ? { runtimeInfo } : {}), createdAt: agent.createdAt.toISOString(), updatedAt: agent.updatedAt.toISOString(), lastUserMessageAt: agent.lastUserMessageAt ? agent.lastUserMessageAt.toISOString() : null, @@ -192,8 +192,6 @@ export function buildStoredAgentPayload( availableModes: [], pendingPermissions: [], persistence: toAgentPersistenceHandle(logger, providerRegistry, record.persistence), - lastUsage: undefined, - lastError: undefined, title: record.title ?? record.config?.title ?? null, requiresAttention: record.requiresAttention ?? false, attentionReason: record.attentionReason ?? null, diff --git a/packages/server/src/server/agent/mcp-server.test.ts b/packages/server/src/server/agent/mcp-server.test.ts index 007068afe..6b01c945f 100644 --- a/packages/server/src/server/agent/mcp-server.test.ts +++ b/packages/server/src/server/agent/mcp-server.test.ts @@ -2,12 +2,14 @@ import { describe, expect, it, vi } from "vitest"; import { mkdtemp, mkdir, rm } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; +import { z } from "zod"; import { createTestLogger } from "../../test-utils/test-logger.js"; import { createAgentMcpServer } from "./mcp-server.js"; import type { AgentManager, ManagedAgent } from "./agent-manager.js"; import type { AgentStorage, StoredAgentRecord } from "./agent-storage.js"; import type { ProviderDefinition } from "./provider-registry.js"; +import { AgentSnapshotPayloadSchema } from "../../shared/messages.js"; type TestDeps = { agentManager: AgentManager; @@ -702,6 +704,59 @@ describe("agent snapshot MCP serialization", () => { ]); }); + it("emits list_agents payloads that satisfy the declared output schema", async () => { + const { agentManager, agentStorage, spies } = createTestDeps(); + const liveAgent = { + id: "live-agent", + provider: "claude", + cwd: "/tmp/live-project", + config: {}, + runtimeInfo: undefined, + createdAt: new Date("2026-04-11T00:00:00.000Z"), + updatedAt: new Date("2026-04-11T00:00:00.000Z"), + lastUserMessageAt: null, + lifecycle: "idle", + capabilities: { + supportsStreaming: false, + supportsSessionPersistence: false, + supportsDynamicModes: false, + supportsMcpServers: true, + supportsReasoningStream: false, + supportsToolInvocations: true, + }, + currentModeId: null, + availableModes: [], + features: [], + pendingPermissions: new Map(), + persistence: null, + labels: {}, + attention: { requiresAttention: false }, + } as unknown as ManagedAgent; + spies.agentManager.listAgents.mockReturnValue([liveAgent]); + spies.agentStorage.list.mockResolvedValue([ + createStoredRecord({ id: "stored-non-archived", archivedAt: null }), + createStoredRecord({ id: "stored-archived", archivedAt: "2026-04-12T00:00:00.000Z" }), + ]); + + const server = await createAgentMcpServer({ + agentManager, + agentStorage, + logger, + providerRegistry: { + claude: createProviderDefinition({}), + } as any, + }); + const tool = (server as any)._registeredTools["list_agents"]; + const response = await tool.callback({ includeArchived: true }); + + const parsed = z.array(AgentSnapshotPayloadSchema).safeParse(response.structuredContent.agents); + if (!parsed.success) { + throw new Error( + `list_agents response failed AgentSnapshotPayloadSchema: ${JSON.stringify(parsed.error.issues, null, 2)}`, + ); + } + }); + it("loads archived agents before reading get_agent_activity", async () => { const { agentManager, agentStorage, spies } = createTestDeps(); const record = createStoredRecord({ id: "archived-activity-agent" }); diff --git a/packages/server/src/server/persistence-hooks.ts b/packages/server/src/server/persistence-hooks.ts index 4ece48b1e..3ebc1a9a8 100644 --- a/packages/server/src/server/persistence-hooks.ts +++ b/packages/server/src/server/persistence-hooks.ts @@ -167,7 +167,7 @@ export function toAgentPersistenceHandle( return { provider, sessionId: handle.sessionId, - nativeHandle: handle.nativeHandle, - metadata: handle.metadata, + ...(handle.nativeHandle !== undefined ? { nativeHandle: handle.nativeHandle } : {}), + ...(handle.metadata !== undefined ? { metadata: handle.metadata } : {}), } satisfies AgentPersistenceHandle; } From b6147e39abaa3a419c387e170fe705a9beda2af0 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 16 Apr 2026 18:29:38 +0700 Subject: [PATCH 12/21] feat(server): show Claude auth status in provider diagnostic Runs `claude auth status` and surfaces the parsed result (auth method, subscription, email/org) in the Claude Code provider diagnostic panel. --- .../server/agent/providers/claude-agent.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts index 8030e534f..67cea73f0 100644 --- a/packages/server/src/server/agent/providers/claude-agent.ts +++ b/packages/server/src/server/agent/providers/claude-agent.ts @@ -1144,6 +1144,7 @@ export class ClaudeAgentClient implements AgentClient { const resolvedBinary = (await findExecutable("claude")) ?? "not found"; const available = await this.isAvailable(); const version = await resolveClaudeVersion(this.runtimeSettings); + const auth = available ? await resolveClaudeAuth(this.runtimeSettings) : null; let modelsValue = "Not checked"; let status = formatDiagnosticStatus(available); @@ -1164,6 +1165,7 @@ export class ClaudeAgentClient implements AgentClient { diagnostic: formatProviderDiagnostic("Claude Code", [ { label: "Binary", value: resolvedBinary }, ...(version ? [{ label: "Version", value: version }] : []), + ...(auth ? [{ label: "Auth", value: auth }] : []), { label: "Models", value: modelsValue }, { label: "Status", value: status }, ]), @@ -1210,6 +1212,57 @@ async function resolveClaudeVersion( } } +async function resolveClaudeAuth( + runtimeSettings?: ProviderRuntimeSettings, +): Promise { + const command = runtimeSettings?.command; + + try { + let stdout: string; + if (command?.mode === "replace") { + const result = await execCommand( + command.argv[0]!, + [...command.argv.slice(1), "auth", "status"], + { timeout: 5_000 }, + ); + stdout = result.stdout; + } else { + const executable = await findExecutable("claude"); + if (!executable) { + return null; + } + const result = await execCommand(executable, ["auth", "status"], { timeout: 5_000 }); + stdout = result.stdout; + } + + const parsed: unknown = JSON.parse(stdout); + if (!parsed || typeof parsed !== "object") { + return null; + } + const record = parsed as Record; + if (record.loggedIn !== true) { + return "Not logged in"; + } + + const parts: string[] = []; + if (typeof record.authMethod === "string" && record.authMethod) { + parts.push(record.authMethod); + } + if (typeof record.subscriptionType === "string" && record.subscriptionType) { + parts.push(record.subscriptionType); + } + if (typeof record.email === "string" && record.email) { + parts.push(record.email); + } else if (typeof record.orgName === "string" && record.orgName) { + parts.push(record.orgName); + } + + return parts.length > 0 ? parts.join(" · ") : "Logged in"; + } catch { + return null; + } +} + function extractContextWindowSize(modelUsage: unknown): number | undefined { if (!modelUsage || typeof modelUsage !== "object") { return undefined; From 14ac6984f3fbf65c7aa04ead817e183492c09786 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 16 Apr 2026 18:29:40 +0700 Subject: [PATCH 13/21] fix(server): extend refresh_providers_snapshot timeout to 60s Provider snapshot refresh was timing out at 5s for providers that need longer to enumerate models/auth. Bump to 60s. --- packages/server/src/client/daemon-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/src/client/daemon-client.ts b/packages/server/src/client/daemon-client.ts index f0b5abe6c..97fd3d498 100644 --- a/packages/server/src/client/daemon-client.ts +++ b/packages/server/src/client/daemon-client.ts @@ -2767,7 +2767,7 @@ export class DaemonClient { providers: options?.providers, }, responseType: "refresh_providers_snapshot_response", - timeout: 5000, + timeout: 60000, }); } From 84db0cecd7328e90d38a8577c8d47ac3d83b4d3a Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 16 Apr 2026 18:29:41 +0700 Subject: [PATCH 14/21] docs: expand Windows entry and rename header to 0.1.58 --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88444c30f..0a27cefd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 0.1.57 - 2026-04-16 +## 0.1.58 - 2026-04-16 ### Added - Markdown files render as formatted markdown in the file pane instead of raw source. ([#427](https://github.com/getpaseo/paseo/pull/427) by [@aaronflorey](https://github.com/aaronflorey)) @@ -9,7 +9,7 @@ - `disallowedTools` option in provider config to block specific tools from an agent. ### Improved -- Windows: Codex startup and provider resolution are significantly more resilient — handles quoted paths, `.cmd` shims, and missing binaries without crashing. ([#454](https://github.com/getpaseo/paseo/pull/454)) +- Windows: agents launch reliably across common install shapes. Claude, Codex, and OpenCode now start correctly from npm-installed `.cmd` shims, daemon paths containing spaces, and with JSON config arguments. Fixes `spawn EINVAL` and related startup errors that prevented Claude from starting on many Windows machines. ([#454](https://github.com/getpaseo/paseo/pull/454)) - OpenCode reliability — permission prompts now include the requesting tool's context, todo and compaction events render in the timeline, sessions archive cleanly on close, and slash commands recover from SSE timeouts. ([#398](https://github.com/getpaseo/paseo/pull/398), [#407](https://github.com/getpaseo/paseo/pull/407), [#408](https://github.com/getpaseo/paseo/pull/408), [#429](https://github.com/getpaseo/paseo/pull/429) by [@aaronflorey](https://github.com/aaronflorey)) - Paseo MCP tools work against archived agents, matching the CLI's behaviour. ([#423](https://github.com/getpaseo/paseo/pull/423)) - Native scrollbars match the active theme across all web views. ([#399](https://github.com/getpaseo/paseo/pull/399) by [@ethersh](https://github.com/ethersh)) From 4d0fe57a7ac86576d79c43e0cea0a11800cdc8a8 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 16 Apr 2026 18:36:06 +0700 Subject: [PATCH 15/21] fix(server): stop detecting Windows PowerShell shims --- packages/server/src/utils/executable.test.ts | 14 ++++++- packages/server/src/utils/executable.ts | 40 ++++--------------- .../src/utils/spawn.launch-regression.test.ts | 16 +------- packages/server/src/utils/spawn.ts | 28 +------------ 4 files changed, 23 insertions(+), 75 deletions(-) diff --git a/packages/server/src/utils/executable.test.ts b/packages/server/src/utils/executable.test.ts index b24dc2c1a..9a6d1e0fb 100644 --- a/packages/server/src/utils/executable.test.ts +++ b/packages/server/src/utils/executable.test.ts @@ -138,7 +138,7 @@ describe("executableExists", () => { expect(executableExists("/usr/local/bin/codex", exists)).toBe("/usr/local/bin/codex"); }); - test("on Windows, falls back to .exe, .cmd, then .ps1 for extensionless paths", () => { + test("on Windows, falls back to .exe, then .cmd for extensionless paths", () => { const originalPlatform = process.platform; Object.defineProperty(process, "platform", { value: "win32", writable: true }); try { @@ -150,6 +150,18 @@ describe("executableExists", () => { } }); + test("on Windows, ignores PowerShell scripts for extensionless paths", () => { + const originalPlatform = process.platform; + Object.defineProperty(process, "platform", { value: "win32", writable: true }); + try { + const exists = (candidate: string) => candidate === "C:\\tools\\codex.ps1"; + + expect(executableExists("C:\\tools\\codex", exists)).toBeNull(); + } finally { + Object.defineProperty(process, "platform", { value: originalPlatform, writable: true }); + } + }); + test("returns null when no matching path exists", () => { expect(executableExists("/missing/codex", () => false)).toBeNull(); }); diff --git a/packages/server/src/utils/executable.ts b/packages/server/src/utils/executable.ts index 84a53543f..f06786b3f 100644 --- a/packages/server/src/utils/executable.ts +++ b/packages/server/src/utils/executable.ts @@ -40,25 +40,6 @@ export function isWindowsCommandScript(executablePath: string): boolean { return process.platform === "win32" && (extension === ".cmd" || extension === ".bat"); } -export function isWindowsPowerShellScript(executablePath: string): boolean { - return process.platform === "win32" && extname(executablePath).toLowerCase() === ".ps1"; -} - -export function windowsPowerShellScriptArgs(scriptPath: string, args: string[]): string[] { - const argvBase64 = Buffer.from(JSON.stringify(args), "utf16le").toString("base64"); - const escapedScriptPath = scriptPath.replace(/'/g, "''"); - const command = [ - "$ErrorActionPreference = 'Stop'", - `$argvJson = [System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String('${argvBase64}'))`, - "$argv = @((ConvertFrom-Json -InputObject $argvJson))", - `& '${escapedScriptPath}' @argv`, - "exit $LASTEXITCODE", - ].join("; "); - const encodedCommand = Buffer.from(command, "utf16le").toString("base64"); - - return ["-NoProfile", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encodedCommand]; -} - async function probeExecutable(executablePath: string): Promise { return await new Promise((resolve) => { let settled = false; @@ -78,19 +59,12 @@ async function probeExecutable(executablePath: string): Promise { let child: ChildProcess; try { - const isPowerShellScript = isWindowsPowerShellScript(executablePath); - child = spawn( - isPowerShellScript ? "powershell.exe" : executablePath, - isPowerShellScript - ? windowsPowerShellScriptArgs(executablePath, ["--version"]) - : ["--version"], - { - stdio: "ignore", - windowsHide: true, - // Windows batch shims (.cmd/.bat) require cmd.exe; native binaries do not. - shell: isWindowsCommandScript(executablePath), - }, - ); + child = spawn(executablePath, ["--version"], { + stdio: "ignore", + windowsHide: true, + // Windows batch shims (.cmd/.bat) require cmd.exe; native binaries do not. + shell: isWindowsCommandScript(executablePath), + }); } catch { settle(false); return; @@ -128,7 +102,7 @@ export function executableExists( ): string | null { if (exists(executablePath)) return executablePath; if (process.platform === "win32" && !extname(executablePath)) { - for (const ext of [".exe", ".cmd", ".ps1"]) { + for (const ext of [".exe", ".cmd"]) { const candidate = executablePath + ext; if (exists(candidate)) return candidate; } diff --git a/packages/server/src/utils/spawn.launch-regression.test.ts b/packages/server/src/utils/spawn.launch-regression.test.ts index 29df428ae..c1087962b 100644 --- a/packages/server/src/utils/spawn.launch-regression.test.ts +++ b/packages/server/src/utils/spawn.launch-regression.test.ts @@ -216,24 +216,12 @@ describe.runIf(process.platform === "win32")("Windows spawn launch regression", expect(result.stdout.trim()).toBe("ARGV_OK"); }); - test("detects and launches a PowerShell shim from PATH without corrupting JSON args", async () => { + test("does not detect a PowerShell shim from PATH", async () => { const fixture = makeFixture({ includeCmdShim: false }); await withWindowsPathEntry(fixture.root, async () => { const detected = await findExecutable("claude"); - expect(detected?.toLowerCase()).toBe(fixture.powerShellShim.toLowerCase()); - - const result = await runFixture({ - command: detected!, - args: fixture.expectedArgs, - shell: false, - }); - - expect(result.error).toBeNull(); - expect(result.code).toBe(0); - expect(result.signal).toBeNull(); - expect(result.stderr).toBe(""); - expect(result.stdout.trim()).toBe("ARGV_OK"); + expect(detected).toBeNull(); }); }); }); diff --git a/packages/server/src/utils/spawn.ts b/packages/server/src/utils/spawn.ts index b5de30366..f7a0b0881 100644 --- a/packages/server/src/utils/spawn.ts +++ b/packages/server/src/utils/spawn.ts @@ -1,13 +1,7 @@ import { execFile, spawn, type ChildProcess, type SpawnOptions } from "node:child_process"; import { promisify } from "node:util"; -import { - isWindowsCommandScript, - isWindowsPowerShellScript, - quoteWindowsArgument, - quoteWindowsCommand, - windowsPowerShellScriptArgs, -} from "./executable.js"; +import { isWindowsCommandScript, quoteWindowsArgument, quoteWindowsCommand } from "./executable.js"; const execFileAsync = promisify(execFile); @@ -30,14 +24,6 @@ export function spawnProcess( options?: SpawnOptions, ): ChildProcess { const isWindows = process.platform === "win32"; - if (isWindowsPowerShellScript(command)) { - return spawn("powershell.exe", windowsPowerShellScriptArgs(command, args), { - ...options, - shell: false, - windowsHide: true, - }); - } - const shell = isWindowsCommandScript(command) ? true : (options?.shell ?? isWindows); const shouldQuoteForShell = isWindows && shell !== false; @@ -57,18 +43,6 @@ export async function execCommand( options?: ExecCommandOptions, ): Promise { const isWindows = process.platform === "win32"; - if (isWindowsPowerShellScript(command)) { - return execFileAsync("powershell.exe", windowsPowerShellScriptArgs(command, args), { - cwd: options?.cwd, - env: options?.env, - encoding: options?.encoding ?? "utf8", - timeout: options?.timeout, - maxBuffer: options?.maxBuffer, - shell: false, - windowsHide: true, - }) as Promise; - } - const shell = isWindowsCommandScript(command) ? true : isWindows; const shouldQuoteForShell = isWindows && shell !== false; const resolvedCommand = shouldQuoteForShell ? quoteWindowsCommand(command) : command; From 1b4483615a14f4e204e3fca8ed27cbb5bc30208f Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 16 Apr 2026 18:52:28 +0700 Subject: [PATCH 16/21] fix(server): surface raw claude auth status in diagnostic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Return stdout verbatim instead of parsing JSON fields. Parser was fragile to upstream schema changes and added no value — users can read the raw output directly. --- .../server/agent/providers/claude-agent.ts | 36 ++++--------------- 1 file changed, 6 insertions(+), 30 deletions(-) diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts index 67cea73f0..09e58797e 100644 --- a/packages/server/src/server/agent/providers/claude-agent.ts +++ b/packages/server/src/server/agent/providers/claude-agent.ts @@ -1218,46 +1218,22 @@ async function resolveClaudeAuth( const command = runtimeSettings?.command; try { - let stdout: string; if (command?.mode === "replace") { - const result = await execCommand( + const { stdout } = await execCommand( command.argv[0]!, [...command.argv.slice(1), "auth", "status"], { timeout: 5_000 }, ); - stdout = result.stdout; - } else { - const executable = await findExecutable("claude"); - if (!executable) { - return null; - } - const result = await execCommand(executable, ["auth", "status"], { timeout: 5_000 }); - stdout = result.stdout; + return stdout.trim() || null; } - const parsed: unknown = JSON.parse(stdout); - if (!parsed || typeof parsed !== "object") { + const executable = await findExecutable("claude"); + if (!executable) { return null; } - const record = parsed as Record; - if (record.loggedIn !== true) { - return "Not logged in"; - } - const parts: string[] = []; - if (typeof record.authMethod === "string" && record.authMethod) { - parts.push(record.authMethod); - } - if (typeof record.subscriptionType === "string" && record.subscriptionType) { - parts.push(record.subscriptionType); - } - if (typeof record.email === "string" && record.email) { - parts.push(record.email); - } else if (typeof record.orgName === "string" && record.orgName) { - parts.push(record.orgName); - } - - return parts.length > 0 ? parts.join(" · ") : "Logged in"; + const { stdout } = await execCommand(executable, ["auth", "status"], { timeout: 5_000 }); + return stdout.trim() || null; } catch { return null; } From 2e9500315485604b83990fd555fb889c0de7e88a Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 16 Apr 2026 18:54:56 +0700 Subject: [PATCH 17/21] fix(server): include stderr in claude auth status diagnostic Some status output (e.g. "Not logged in") goes to stderr. Capture both stdout and stderr, including when the command exits non-zero, so the diagnostic surfaces the full picture. --- .../server/agent/providers/claude-agent.ts | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts index 09e58797e..6924f5917 100644 --- a/packages/server/src/server/agent/providers/claude-agent.ts +++ b/packages/server/src/server/agent/providers/claude-agent.ts @@ -1217,23 +1217,38 @@ async function resolveClaudeAuth( ): Promise { const command = runtimeSettings?.command; + const run = async ( + executable: string, + args: string[], + ): Promise<{ stdout: string; stderr: string }> => { + try { + return await execCommand(executable, args, { timeout: 5_000 }); + } catch (error) { + const err = error as { stdout?: string; stderr?: string; message?: string }; + return { + stdout: err.stdout ?? "", + stderr: err.stderr ?? err.message ?? "", + }; + } + }; + try { + let result: { stdout: string; stderr: string }; if (command?.mode === "replace") { - const { stdout } = await execCommand( - command.argv[0]!, - [...command.argv.slice(1), "auth", "status"], - { timeout: 5_000 }, - ); - return stdout.trim() || null; + result = await run(command.argv[0]!, [...command.argv.slice(1), "auth", "status"]); + } else { + const executable = await findExecutable("claude"); + if (!executable) { + return null; + } + result = await run(executable, ["auth", "status"]); } - const executable = await findExecutable("claude"); - if (!executable) { - return null; - } - - const { stdout } = await execCommand(executable, ["auth", "status"], { timeout: 5_000 }); - return stdout.trim() || null; + const combined = [result.stdout, result.stderr] + .map((s) => s.trim()) + .filter((s) => s.length > 0) + .join("\n"); + return combined || null; } catch { return null; } From a11c246bdceca3dff819959161a0d004a515fd25 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 16 Apr 2026 18:58:31 +0700 Subject: [PATCH 18/21] fix(server): share send-prompt path between MCP and Session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract `unarchiveAgentState` and `sendPromptToAgent` into `mcp-shared` so every surface (app/WS, MCP, CLI-through-MCP) runs the same sequence: unarchive → ensureAgentLoaded → optional mode change → recordUserMessage → startAgentRun. MCP `send_agent_prompt` previously skipped unarchive and cold-agent rehydration, so sending to an archived agent over MCP left it hidden from `paseo ls` and failed entirely when the agent wasn't already in memory. Session now delegates to the shared function and its private `unarchiveAgentState` is gone. --- .../server/src/server/agent/mcp-server.ts | 29 ++---- .../server/src/server/agent/mcp-shared.ts | 99 ++++++++++++++++++- packages/server/src/server/session.ts | 96 ++++++------------ 3 files changed, 136 insertions(+), 88 deletions(-) diff --git a/packages/server/src/server/agent/mcp-server.ts b/packages/server/src/server/agent/mcp-server.ts index a71134b25..8279386cf 100644 --- a/packages/server/src/server/agent/mcp-server.ts +++ b/packages/server/src/server/agent/mcp-server.ts @@ -40,6 +40,7 @@ import { parseDurationString, resolveProviderAndModel, sanitizePermissionRequest, + sendPromptToAgent, setupFinishNotification, serializeSnapshotWithMetadata, startAgentRun, @@ -768,30 +769,20 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom }, }, async ({ agentId, prompt, sessionMode, background = false, notifyOnFinish = false }) => { - const snapshot = agentManager.getAgent(agentId); - if (!snapshot) { - throw new Error(`Agent ${agentId} not found`); - } - if (agentManager.hasInFlightRun(agentId)) { waitTracker.cancel(agentId, "Agent run interrupted by new prompt"); } - if (sessionMode) { - await agentManager.setAgentMode(agentId, sessionMode); - } - - try { - agentManager.recordUserMessage(agentId, prompt, { - emitState: false, - }); - } catch (error) { - childLogger.error({ err: error, agentId }, "Failed to record user message"); - } - - startAgentRun(agentManager, agentId, prompt, childLogger, { - replaceRunning: true, + await sendPromptToAgent({ + agentManager, + agentStorage, + agentId, + userMessageText: prompt, + prompt, + sessionMode, + logger: childLogger, }); + if (notifyOnFinish && callerAgentId) { setupFinishNotification({ agentManager, diff --git a/packages/server/src/server/agent/mcp-shared.ts b/packages/server/src/server/agent/mcp-shared.ts index 3edf8f6c7..e3e959985 100644 --- a/packages/server/src/server/agent/mcp-shared.ts +++ b/packages/server/src/server/agent/mcp-shared.ts @@ -1,10 +1,15 @@ import { z } from "zod"; import type { Logger } from "pino"; -import type { AgentPromptInput, AgentPermissionRequest } from "./agent-sdk-types.js"; +import type { + AgentPromptInput, + AgentPermissionRequest, + AgentRunOptions, +} from "./agent-sdk-types.js"; import type { AgentManager, ManagedAgent, WaitForAgentResult } from "./agent-manager.js"; import { curateAgentActivity } from "./activity-curator.js"; import type { AgentStorage } from "./agent-storage.js"; +import { ensureAgentLoaded } from "./agent-loading.js"; import { serializeAgentSnapshot } from "../messages.js"; import { StoredScheduleSchema } from "../schedule/types.js"; import type { AgentProvider } from "./agent-sdk-types.js"; @@ -94,6 +99,7 @@ export function resolveProviderAndModel(params: { export type StartAgentRunOptions = { replaceRunning?: boolean; + runOptions?: AgentRunOptions; }; /** @@ -171,9 +177,10 @@ export function startAgentRun( options?: StartAgentRunOptions, ): void { const shouldReplace = Boolean(options?.replaceRunning && agentManager.hasInFlightRun(agentId)); + const runOptions = options?.runOptions; const iterator = shouldReplace - ? agentManager.replaceAgentRun(agentId, prompt) - : agentManager.streamAgent(agentId, prompt); + ? agentManager.replaceAgentRun(agentId, prompt, runOptions) + : agentManager.streamAgent(agentId, prompt, runOptions); void (async () => { try { for await (const _ of iterator) { @@ -185,6 +192,92 @@ export function startAgentRun( })(); } +/** + * Clear the archived flag from a stored agent record. + * Shared across Session (app/WS), MCP, and CLI so every surface that acts on + * an archived agent unarchives it the same way. + */ +export async function unarchiveAgentState( + agentStorage: AgentStorage, + agentManager: AgentManager, + agentId: string, +): Promise { + const record = await agentStorage.get(agentId); + if (!record || !record.archivedAt) { + return false; + } + const updatedAt = new Date().toISOString(); + await agentStorage.upsert({ + ...record, + archivedAt: null, + updatedAt, + }); + agentManager.notifyAgentState(agentId); + return true; +} + +export interface SendPromptToAgentParams { + agentManager: AgentManager; + agentStorage: AgentStorage; + agentId: string; + /** Raw user text to record in the timeline. */ + userMessageText: string; + /** Prompt to dispatch to the provider (may include image blocks or wrapped text). */ + prompt: AgentPromptInput; + messageId?: string; + runOptions?: AgentRunOptions; + /** Optional mode to set on the agent before the run starts. */ + sessionMode?: string; + logger: Logger; +} + +/** + * Full send-prompt orchestration: unarchive → load → (optional mode change) → + * record user message → start run. + * + * Every surface that sends a prompt to an agent (Session/WS, MCP, CLI-through-MCP) + * MUST go through this so behavior can never drift between them. + */ +export async function sendPromptToAgent(params: SendPromptToAgentParams): Promise { + const { + agentManager, + agentStorage, + agentId, + userMessageText, + prompt, + messageId, + runOptions, + sessionMode, + logger, + } = params; + + await unarchiveAgentState(agentStorage, agentManager, agentId); + + await ensureAgentLoaded(agentId, { + agentManager, + agentStorage, + logger, + }); + + if (sessionMode) { + await agentManager.setAgentMode(agentId, sessionMode); + } + + try { + agentManager.recordUserMessage(agentId, userMessageText, { + messageId, + emitState: false, + }); + } catch (error) { + logger.error({ err: error, agentId }, "Failed to record user message"); + } + + startAgentRun(agentManager, agentId, prompt, logger, { + replaceRunning: true, + runOptions, + }); +} + interface SetupFinishNotificationParams { agentManager: AgentManager; agentStorage: AgentStorage; diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 039e678fe..f85641ae4 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -65,6 +65,7 @@ import { toAgentPersistenceHandle, } from "./persistence-hooks.js"; import { ensureAgentLoaded } from "./agent/agent-loading.js"; +import { sendPromptToAgent, unarchiveAgentState } from "./agent/mcp-shared.js"; import { experimental_createMCPClient } from "ai"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import type { VoiceCallerContext, VoiceSpeakHandler } from "./voice-types.js"; @@ -2188,21 +2189,6 @@ export class Session { }); } - private async unarchiveAgentState(agentId: string): Promise { - const record = await this.agentStorage.get(agentId); - if (!record || !record.archivedAt) { - return false; - } - const updatedAt = new Date().toISOString(); - await this.agentStorage.upsert({ - ...record, - archivedAt: null, - updatedAt, - }); - this.agentManager.notifyAgentState(agentId); - return true; - } - private async unarchiveAgentByHandle(handle: AgentPersistenceHandle): Promise { const records = await this.agentStorage.list(); const matched = records.find( @@ -2213,7 +2199,7 @@ export class Session { if (!matched) { return; } - await this.unarchiveAgentState(matched.id); + await unarchiveAgentState(this.agentStorage, this.agentManager, matched.id); } private async handleUpdateAgentRequest( @@ -2711,38 +2697,28 @@ export class Session { `Sending text to agent ${agentId}${images && images.length > 0 ? ` with ${images.length} image attachment(s)` : ""}`, ); - await this.unarchiveAgentState(agentId); + const promptText = options?.spokenInput ? wrapSpokenInput(text) : text; + const prompt = this.buildAgentPrompt(promptText, images); try { - await ensureAgentLoaded(agentId, { + await sendPromptToAgent({ agentManager: this.agentManager, agentStorage: this.agentStorage, + agentId, + userMessageText: text, + prompt, + messageId, + runOptions, logger: this.sessionLogger, }); + return { ok: true }; } catch (error) { - this.handleAgentRunError(agentId, error, "Failed to initialize agent before sending prompt"); + this.handleAgentRunError(agentId, error, "Failed to send agent message"); return { ok: false, error: error instanceof Error ? error.message : String(error), }; } - - try { - this.agentManager.recordUserMessage(agentId, text, { - messageId, - emitState: false, - }); - } catch (error) { - this.sessionLogger.error( - { err: error, agentId }, - `Failed to record user message for agent ${agentId}`, - ); - } - - const promptText = options?.spokenInput ? wrapSpokenInput(text) : text; - const prompt = this.buildAgentPrompt(promptText, images); - - return this.startAgentStream(agentId, prompt, runOptions); } /** @@ -2902,7 +2878,7 @@ export class Session { try { await this.unarchiveAgentByHandle(handle); const snapshot = await this.agentManager.resumeAgentFromPersistence(handle, overrides); - await this.unarchiveAgentState(snapshot.id); + await unarchiveAgentState(this.agentStorage, this.agentManager, snapshot.id); await this.agentManager.hydrateTimelineFromProvider(snapshot.id); await this.forwardAgentUpdate(snapshot); const timelineSize = this.agentManager.getTimeline(snapshot.id).length; @@ -2943,7 +2919,7 @@ export class Session { this.sessionLogger.info({ agentId }, `Refreshing agent ${agentId} from persistence`); try { - await this.unarchiveAgentState(agentId); + await unarchiveAgentState(this.agentStorage, this.agentManager, agentId); let snapshot: ManagedAgent; const existing = this.agentManager.getAgent(agentId); if (existing) { @@ -6396,44 +6372,32 @@ export class Session { try { const agentId = resolved.agentId; - await this.unarchiveAgentState(agentId); - - await ensureAgentLoaded(agentId, { - agentManager: this.agentManager, - agentStorage: this.agentStorage, - logger: this.sessionLogger, - }); - - this.sessionLogger.trace( - { agentId, messageId: msg.messageId, textPrefix: msg.text.slice(0, 80) }, - "send_agent_message_request: recording user message", - ); - try { - this.agentManager.recordUserMessage(agentId, msg.text, { - messageId: msg.messageId, - emitState: false, - }); - } catch (error) { - this.sessionLogger.error( - { err: error, agentId }, - "Failed to record user message for send_agent_message_request", - ); - } const prompt = this.buildAgentPrompt(msg.text, msg.images); this.sessionLogger.trace( - { agentId, messageId: msg.messageId }, - "send_agent_message_request: starting agent stream", + { agentId, messageId: msg.messageId, textPrefix: msg.text.slice(0, 80) }, + "send_agent_message_request: dispatching shared sendPromptToAgent", ); - const started = this.startAgentStream(agentId, prompt); - if (!started.ok) { + try { + await sendPromptToAgent({ + agentManager: this.agentManager, + agentStorage: this.agentStorage, + agentId, + userMessageText: msg.text, + prompt, + messageId: msg.messageId, + logger: this.sessionLogger, + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.handleAgentRunError(agentId, error, "Failed to send agent message"); this.emit({ type: "send_agent_message_response", payload: { requestId: msg.requestId, agentId, accepted: false, - error: started.error, + error: message, }, }); return; From a387eaf26a197135ec79741ad424baa57ba87df1 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 16 Apr 2026 19:16:42 +0700 Subject: [PATCH 19/21] docs: trim 0.1.58 changelog and add scannable-bullets guidance Split the OpenCode reliability bullet into four per-PR entries, shorten the Windows and provider-models bullets, and drop low-signal qualifiers. Add a "Changelog conciseness" section to the release playbook so the next agent knows bullets must be scannable in one glance. --- CHANGELOG.md | 15 +++++++++------ docs/RELEASE.md | 10 ++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a27cefd8..a2b7bf767 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,15 +3,18 @@ ## 0.1.58 - 2026-04-16 ### Added -- Markdown files render as formatted markdown in the file pane instead of raw source. ([#427](https://github.com/getpaseo/paseo/pull/427) by [@aaronflorey](https://github.com/aaronflorey)) -- Cmd+L (Ctrl+L on Windows/Linux) focuses the agent message input, with a hint shown when the agent panel is active. -- Provider model lists refresh on a freshness TTL, and Settings surfaces when models were last updated along with any fetch errors inline. ([#426](https://github.com/getpaseo/paseo/pull/426)) +- Markdown files render as formatted markdown in the file pane. ([#427](https://github.com/getpaseo/paseo/pull/427) by [@aaronflorey](https://github.com/aaronflorey)) +- Cmd+L (Ctrl+L on Windows/Linux) focuses the agent message input. +- Provider models refresh on a freshness TTL; Settings shows last-updated time and any fetch errors. ([#426](https://github.com/getpaseo/paseo/pull/426)) - `disallowedTools` option in provider config to block specific tools from an agent. ### Improved -- Windows: agents launch reliably across common install shapes. Claude, Codex, and OpenCode now start correctly from npm-installed `.cmd` shims, daemon paths containing spaces, and with JSON config arguments. Fixes `spawn EINVAL` and related startup errors that prevented Claude from starting on many Windows machines. ([#454](https://github.com/getpaseo/paseo/pull/454)) -- OpenCode reliability — permission prompts now include the requesting tool's context, todo and compaction events render in the timeline, sessions archive cleanly on close, and slash commands recover from SSE timeouts. ([#398](https://github.com/getpaseo/paseo/pull/398), [#407](https://github.com/getpaseo/paseo/pull/407), [#408](https://github.com/getpaseo/paseo/pull/408), [#429](https://github.com/getpaseo/paseo/pull/429) by [@aaronflorey](https://github.com/aaronflorey)) -- Paseo MCP tools work against archived agents, matching the CLI's behaviour. ([#423](https://github.com/getpaseo/paseo/pull/423)) +- Windows: agents launch reliably from npm `.cmd` shims, paths with spaces, and JSON config args — fixes `spawn EINVAL` startup errors. ([#454](https://github.com/getpaseo/paseo/pull/454)) +- OpenCode permission prompts include the requesting tool's context. ([#398](https://github.com/getpaseo/paseo/pull/398) by [@aaronflorey](https://github.com/aaronflorey)) +- OpenCode todo and compaction events render in the timeline. ([#429](https://github.com/getpaseo/paseo/pull/429) by [@aaronflorey](https://github.com/aaronflorey)) +- OpenCode sessions archive cleanly when closed. ([#408](https://github.com/getpaseo/paseo/pull/408) by [@aaronflorey](https://github.com/aaronflorey)) +- OpenCode slash commands recover from SSE timeouts. ([#407](https://github.com/getpaseo/paseo/pull/407) by [@aaronflorey](https://github.com/aaronflorey)) +- Paseo MCP tools work against archived agents, matching the CLI. ([#423](https://github.com/getpaseo/paseo/pull/423)) - Native scrollbars match the active theme across all web views. ([#399](https://github.com/getpaseo/paseo/pull/399) by [@ethersh](https://github.com/ethersh)) ### Fixed diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 54e288c70..6a838f66e 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -139,6 +139,16 @@ The changelog is shown on the Paseo homepage. Write it for **end users**, not de - **Only list changes relative to the previous stable release.** The diff is `v(previous)..HEAD`. If something was introduced and fixed between those two tags, it never shipped — don't mention the fix. - **Cut low-signal entries.** "Toolbar buttons have consistent sizing" is too granular. Combine small polish items or drop them. +## Changelog conciseness + +Every bullet must be scannable at a glance. The changelog is not release documentation — it's a list. + +- **One line per bullet.** If a bullet wraps to three lines in a narrow column, it's too long. +- **Split bullets that pack multiple distinct changes.** If a bullet uses "and", "plus", a comma list, or an em-dash to chain several independent improvements, break them into separate bullets — even when they share a theme or author. One bullet = one user-facing change. +- **Trim qualifying clauses.** Drop "with a hint shown when…", "matching the CLI's behaviour", "across common install shapes". If the detail doesn't change whether a user cares, cut it. +- **Lead with the outcome.** "Windows: agents launch reliably from npm `.cmd` shims…" is better than "Windows: agents launch reliably across common install shapes. Claude, Codex, and OpenCode now start correctly…". +- **Attribution follows the split.** When you split a dense bullet, move each PR/author to the bullet it belongs to. Never duplicate the same PR across multiple bullets. + ## Changelog attribution Every changelog bullet must credit contributors and link to the PR(s) that delivered the change. This is not one-PR-per-line — a single bullet describes a user-facing change and may reference multiple PRs. From 727aa0cdac42fb669f36e6cc685e0101aaa843b6 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 16 Apr 2026 19:17:37 +0700 Subject: [PATCH 20/21] chore(release): cut 0.1.58 --- package-lock.json | 38 ++++++++++++------------ package.json | 2 +- packages/app/package.json | 8 ++--- packages/cli/package.json | 6 ++-- packages/desktop/package.json | 6 ++-- packages/expo-two-way-audio/package.json | 2 +- packages/highlight/package.json | 2 +- packages/relay/package.json | 2 +- packages/server/package.json | 6 ++-- packages/website/package.json | 2 +- 10 files changed, 37 insertions(+), 37 deletions(-) diff --git a/package-lock.json b/package-lock.json index f31b01380..7dbc064be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "paseo", - "version": "0.1.57", + "version": "0.1.58", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "paseo", - "version": "0.1.57", + "version": "0.1.58", "hasInstallScript": true, "license": "AGPL-3.0-or-later", "workspaces": [ @@ -34906,16 +34906,16 @@ }, "packages/app": { "name": "@getpaseo/app", - "version": "0.1.57", + "version": "0.1.58", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", "@expo/vector-icons": "^15.0.2", "@floating-ui/react-native": "^0.10.7", - "@getpaseo/expo-two-way-audio": "0.1.57", - "@getpaseo/highlight": "0.1.57", - "@getpaseo/server": "0.1.57", + "@getpaseo/expo-two-way-audio": "0.1.58", + "@getpaseo/highlight": "0.1.58", + "@getpaseo/server": "0.1.58", "@gorhom/bottom-sheet": "^5.2.6", "@gorhom/portal": "^1.0.14", "@react-native-async-storage/async-storage": "2.2.0", @@ -35056,11 +35056,11 @@ }, "packages/cli": { "name": "@getpaseo/cli", - "version": "0.1.57", + "version": "0.1.58", "dependencies": { "@clack/prompts": "^1.0.0", - "@getpaseo/relay": "0.1.57", - "@getpaseo/server": "0.1.57", + "@getpaseo/relay": "0.1.58", + "@getpaseo/server": "0.1.58", "chalk": "^5.3.0", "commander": "^12.0.0", "mime-types": "^2.1.35", @@ -35101,11 +35101,11 @@ }, "packages/desktop": { "name": "@getpaseo/desktop", - "version": "0.1.57", + "version": "0.1.58", "license": "AGPL-3.0-or-later", "dependencies": { - "@getpaseo/cli": "0.1.57", - "@getpaseo/server": "0.1.57", + "@getpaseo/cli": "0.1.58", + "@getpaseo/server": "0.1.58", "electron-log": "^5.4.3", "electron-updater": "^6.6.2", "ws": "^8.14.2" @@ -35139,7 +35139,7 @@ }, "packages/expo-two-way-audio": { "name": "@getpaseo/expo-two-way-audio", - "version": "0.1.57", + "version": "0.1.58", "license": "MIT", "devDependencies": { "@biomejs/biome": "1.9.4", @@ -35340,7 +35340,7 @@ }, "packages/highlight": { "name": "@getpaseo/highlight", - "version": "0.1.57", + "version": "0.1.58", "dependencies": { "@lezer/common": "^1.5.0", "@lezer/cpp": "^1.1.5", @@ -35366,7 +35366,7 @@ }, "packages/relay": { "name": "@getpaseo/relay", - "version": "0.1.57", + "version": "0.1.58", "dependencies": { "base64-js": "^1.5.1", "tweetnacl": "^1.0.3", @@ -35382,14 +35382,14 @@ }, "packages/server": { "name": "@getpaseo/server", - "version": "0.1.57", + "version": "0.1.58", "dependencies": { "@agentclientprotocol/sdk": "^0.17.1", "@ai-sdk/openai": "2.0.52", "@anthropic-ai/claude-agent-sdk": "^0.2.11", "@deepgram/sdk": "^3.4.0", - "@getpaseo/highlight": "0.1.57", - "@getpaseo/relay": "0.1.57", + "@getpaseo/highlight": "0.1.58", + "@getpaseo/relay": "0.1.58", "@isaacs/ttlcache": "^2.1.4", "@modelcontextprotocol/sdk": "^1.20.1", "@opencode-ai/sdk": "1.2.6", @@ -35842,7 +35842,7 @@ }, "packages/website": { "name": "@getpaseo/website", - "version": "0.1.57", + "version": "0.1.58", "dependencies": { "@cloudflare/vite-plugin": "^1.20.3", "@cloudflare/workers-types": "^4.20260114.0", diff --git a/package.json b/package.json index f2be49196..be5d529c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "paseo", - "version": "0.1.57", + "version": "0.1.58", "private": true, "workspaces": [ "packages/expo-two-way-audio", diff --git a/packages/app/package.json b/packages/app/package.json index c6e01bd9a..1f74c1cb8 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,7 +1,7 @@ { "name": "@getpaseo/app", "main": "index.ts", - "version": "0.1.57", + "version": "0.1.58", "private": true, "scripts": { "start": "expo start", @@ -31,9 +31,9 @@ "@dnd-kit/utilities": "^3.2.2", "@expo/vector-icons": "^15.0.2", "@floating-ui/react-native": "^0.10.7", - "@getpaseo/expo-two-way-audio": "0.1.57", - "@getpaseo/highlight": "0.1.57", - "@getpaseo/server": "0.1.57", + "@getpaseo/expo-two-way-audio": "0.1.58", + "@getpaseo/highlight": "0.1.58", + "@getpaseo/server": "0.1.58", "@gorhom/bottom-sheet": "^5.2.6", "@gorhom/portal": "^1.0.14", "@react-native-async-storage/async-storage": "2.2.0", diff --git a/packages/cli/package.json b/packages/cli/package.json index e0ed3008a..a17ca5fa4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/cli", - "version": "0.1.57", + "version": "0.1.58", "description": "Paseo CLI - control your AI coding agents from the command line", "type": "module", "files": [ @@ -24,8 +24,8 @@ }, "dependencies": { "@clack/prompts": "^1.0.0", - "@getpaseo/relay": "0.1.57", - "@getpaseo/server": "0.1.57", + "@getpaseo/relay": "0.1.58", + "@getpaseo/server": "0.1.58", "chalk": "^5.3.0", "commander": "^12.0.0", "mime-types": "^2.1.35", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 7f3777500..69694482e 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/desktop", - "version": "0.1.57", + "version": "0.1.58", "private": true, "description": "Paseo desktop app (Electron wrapper)", "main": "dist/main.js", @@ -13,8 +13,8 @@ "typecheck": "tsc --noEmit -p tsconfig.json" }, "dependencies": { - "@getpaseo/cli": "0.1.57", - "@getpaseo/server": "0.1.57", + "@getpaseo/cli": "0.1.58", + "@getpaseo/server": "0.1.58", "electron-log": "^5.4.3", "electron-updater": "^6.6.2", "ws": "^8.14.2" diff --git a/packages/expo-two-way-audio/package.json b/packages/expo-two-way-audio/package.json index c5302ad9e..13b8c2304 100644 --- a/packages/expo-two-way-audio/package.json +++ b/packages/expo-two-way-audio/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/expo-two-way-audio", - "version": "0.1.57", + "version": "0.1.58", "description": "Native module for two way audio streaming", "main": "build/index.js", "types": "build/index.d.ts", diff --git a/packages/highlight/package.json b/packages/highlight/package.json index 75e55e913..7a007c2ae 100644 --- a/packages/highlight/package.json +++ b/packages/highlight/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/highlight", - "version": "0.1.57", + "version": "0.1.58", "type": "module", "publishConfig": { "access": "public" diff --git a/packages/relay/package.json b/packages/relay/package.json index c729d92f8..d636fd5eb 100644 --- a/packages/relay/package.json +++ b/packages/relay/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/relay", - "version": "0.1.57", + "version": "0.1.58", "description": "Paseo relay for bridging daemon and client connections", "type": "module", "publishConfig": { diff --git a/packages/server/package.json b/packages/server/package.json index f19b4a0e9..c9120cffe 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/server", - "version": "0.1.57", + "version": "0.1.58", "description": "Paseo backend server", "type": "module", "publishConfig": { @@ -64,8 +64,8 @@ "@ai-sdk/openai": "2.0.52", "@anthropic-ai/claude-agent-sdk": "^0.2.11", "@deepgram/sdk": "^3.4.0", - "@getpaseo/highlight": "0.1.57", - "@getpaseo/relay": "0.1.57", + "@getpaseo/highlight": "0.1.58", + "@getpaseo/relay": "0.1.58", "@isaacs/ttlcache": "^2.1.4", "@modelcontextprotocol/sdk": "^1.20.1", "@opencode-ai/sdk": "1.2.6", diff --git a/packages/website/package.json b/packages/website/package.json index e8b7445f2..2cb764d0e 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -1,6 +1,6 @@ { "name": "@getpaseo/website", - "version": "0.1.57", + "version": "0.1.58", "private": true, "type": "module", "scripts": { From 754f1facb66d35e679a4ebf6a4eaddb85f946173 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 16 Apr 2026 12:18:57 +0000 Subject: [PATCH 21/21] fix: update lockfile signatures and Nix hash --- nix/package.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/package.nix b/nix/package.nix index ed43a804e..2451454f2 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -42,7 +42,7 @@ buildNpmPackage rec { # To update: run `nix build` with lib.fakeHash, copy the `got:` hash. # CI auto-updates this when package-lock.json changes (see .github/workflows/). - npmDepsHash = "sha256-t3QD52Yfi2VEQxspGJMtSINFnn2Hp1wAzu+SYDIIY4Y="; + npmDepsHash = "sha256-rpXng5y99bDt7a3f6vI+EWrVahlXINdHzCf7ml+ZnFs="; # Prevent onnxruntime-node's install script from running during automatic # npm rebuild (it tries to download from api.nuget.org, which fails in the sandbox).