Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7a77924
fix(coding-agent): resolve the kernel venv python under Scripts on win32
snimu Sep 4, 2026
a015baf
fix(coding-agent): expand ~/ with the platform path separator
snimu Sep 4, 2026
542de39
fix(coding-agent): prefer non-System32 bash over the WSL trampoline o…
snimu Sep 4, 2026
2cbab07
fix(coding-agent): dial the Herdr socket inside the named-pipe namesp…
snimu Sep 4, 2026
f8760de
fix(coding-agent): cache the win32 process start id per pid for a sho…
snimu Sep 4, 2026
47e906f
fix(coding-agent): derive handshake budgets from the outer connect de…
snimu Sep 4, 2026
8729c97
fix(coding-agent): route background spawns through hidden-window wrap…
snimu Sep 4, 2026
2865edd
chore(coding-agent): add windows-seams changelog fragments; tighten n…
snimu Sep 4, 2026
c238009
test(coding-agent): align clipboard and login-dialog spawn assertions…
snimu Sep 4, 2026
8379ae2
revert(coding-agent): drop the win32 start-id TTL cache
snimu Sep 4, 2026
36a2f62
Merge remote-tracking branch 'origin/main' into fix/windows-platform-…
snimu Sep 4, 2026
424b68b
fix(coding-agent): match Herdr pipe-namespace prefixes case-insensiti…
snimu Sep 4, 2026
cc11f4e
Merge branch 'main' of https://github.com/PrimeIntellect-ai/prime-age…
snimu Sep 4, 2026
8864109
refactor(coding-agent): tighten windows-seam comments and consolidate…
snimu Sep 4, 2026
24df7bf
Merge current main into Windows reliability fixes
sethkarten Sep 7, 2026
2fdf2a7
fix(coding-agent,tui): harden current-runtime Windows sessions
sethkarten Sep 7, 2026
887392a
Merge current main into Windows reliability fixes
sethkarten Sep 7, 2026
bc9b24f
fix(coding-agent): normalize Windows bash candidate paths
sethkarten Sep 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- Fixed the Python kernel bootstrap on native Windows: the venv python now resolves under `Scripts\python.exe` (uv layout). ([Discussion #1401](https://github.com/PrimeIntellect-ai/prime-agent/discussions/1401), [Discussion #1969](https://github.com/PrimeIntellect-ai/prime-agent/discussions/1969))
- Fixed `~/` and `~\` path expansion on Windows, including mixed-separator paths like `C:\Users\u/rest`. ([Discussion #1442](https://github.com/PrimeIntellect-ai/prime-agent/discussions/1442), [Discussion #1469](https://github.com/PrimeIntellect-ai/prime-agent/discussions/1469))
- Fixed daemon worker handshakes timing out on slow machines: per-attempt hello/auth waits now consume the remaining connect budget instead of restarting a fixed 1s clock on every retry. ([Discussion #1622](https://github.com/PrimeIntellect-ai/prime-agent/discussions/1622), [Discussion #1678](https://github.com/PrimeIntellect-ai/prime-agent/discussions/1678))
- Fixed console windows flashing on Windows: all background spawns now run with hidden windows. ([Discussion #1461](https://github.com/PrimeIntellect-ai/prime-agent/discussions/1461))
- Fixed bash resolution picking WSL's System32 `bash.exe` over a per-user Git Bash on PATH. ([Discussion #1437](https://github.com/PrimeIntellect-ai/prime-agent/discussions/1437))
- Fixed the built-in Herdr reporter never connecting on Windows by dialing the socket inside the named-pipe namespace. ([Discussion #1399](https://github.com/PrimeIntellect-ai/prime-agent/discussions/1399))
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Fixed Windows worker startup deadlines, session lease contention, and UTF-8 Python execution.
- Fixed deleted subagents returning in saved display state and duplicate cleanup failure notices.
4 changes: 2 additions & 2 deletions packages/coding-agent/src/cli/daemon-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { clearLine, createInterface, cursorTo, type Interface } from "node:readl
import { setTimeout as delay } from "node:timers/promises";
import type { AgentMessage } from "@earendil-works/pi-agent-core";
import chalk from "chalk";
import { spawn } from "child_process";
import { expandTildePath } from "../config.js";
import type { AgentSessionEvent } from "../core/agent-session.js";
import type { AgentSessionRuntimeConfig } from "../core/agent-session-config.js";
Expand All @@ -14,6 +13,7 @@ import type { DaemonOutbound, DaemonResponse } from "../modes/daemon/daemon-prot
import { matchesSessionIdSuffix } from "../modes/daemon/daemon-session-id.js";
import type { SessionSummary } from "../modes/daemon/daemon-session-list.js";
import { defaultDaemonSocketPath, normalizeSocketPath } from "../modes/daemon/daemon-socket.js";
import { spawnHidden } from "../utils/child-process.js";
import { isLocalPath } from "../utils/paths.js";
import { isValidThinkingLevel } from "./args.js";
import { formatSessionListTable } from "./daemon-list-format.js";
Expand Down Expand Up @@ -688,7 +688,7 @@ async function runStart(parsed: ParsedDaemonClientCommand): Promise<void> {
parsed.socketPath,
...sessionArgs.daemonArgs.filter((arg) => arg !== "--background" && arg !== "-d"),
];
const child = spawn(process.execPath, daemonArgs, {
const child = spawnHidden(process.execPath, daemonArgs, {
cwd: sessionArgs.config?.cwd ?? process.cwd(),
detached: true,
env: process.env,
Expand Down
4 changes: 2 additions & 2 deletions packages/coding-agent/src/cli/daemon-launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
* the heavy main module graph loads. main.ts reuses the same memoized promise.
*/

import { spawn } from "node:child_process";
import { existsSync, readFileSync, statSync } from "node:fs";
import { resolve } from "node:path";
import { appendRotatingLog, expandTildePath, getClientErrorLogPath, getDaemonLogPath, VERSION } from "../config.js";
Expand All @@ -23,6 +22,7 @@ import {
DAEMON_WORKER_SUPERVISOR_SOCKET_ENV,
DAEMON_WORKER_TOKEN_ENV,
} from "../modes/daemon/daemon-worker-protocol.js";
import { spawnHidden } from "../utils/child-process.js";
import { isHelpCommandRequest, PUBLIC_COMMAND_NAMES, REMOVED_COMMAND_NAMES } from "./command-registry.js";
import { createCliSubprocessEnv, formatCurrentCliCommand } from "./subprocess-launch.js";

Expand Down Expand Up @@ -394,7 +394,7 @@ Then retry the original command.`,
delete env[SESSION_LEASE_OWNER_ID_ENV];

const logOffset = currentDaemonLogSize(socketPath);
const child = spawn(
const child = spawnHidden(
process.execPath,
[...process.execArgv, entrypoint, "--mode", "daemon", "--daemon-socket", socketPath],
{
Expand Down
14 changes: 7 additions & 7 deletions packages/coding-agent/src/cli/daemon-ps.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { spawnSync } from "node:child_process";
import { existsSync, lstatSync, readdirSync, readFileSync, rmSync, unlinkSync } from "node:fs";
import { basename, dirname, join, resolve } from "node:path";
import chalk from "chalk";
Expand All @@ -24,6 +23,7 @@ import {
processGroupHasLiveMember,
processIdExists,
signalProcessGroupIfHeld,
spawnSyncHidden,
} from "../utils/child-process.js";
import { formatDaemonListTable } from "./daemon-ps-format.js";
import { promptYesNo } from "./daemon-stop-confirm.js";
Expand Down Expand Up @@ -180,18 +180,18 @@ function scanListeningDaemons(): DiscoveredDaemonProcess[] {
if (process.platform === "win32") {
return [];
}
const ss = spawnSync("ss", ["-lxp"], { encoding: "utf8" });
const ss = spawnSyncHidden("ss", ["-lxp"], { encoding: "utf8" });
if (!ss.error && ss.status === 0 && typeof ss.stdout === "string") {
return enrichUptimes(parseSsListeners(ss.stdout, APP_NAME));
}
const lsof = spawnSync("lsof", ["-nP", "-F", "pn", "-U", "-a", "-c", APP_NAME], { encoding: "utf8" });
const lsof = spawnSyncHidden("lsof", ["-nP", "-F", "pn", "-U", "-a", "-c", APP_NAME], { encoding: "utf8" });
const byName = !lsof.error && typeof lsof.stdout === "string" ? parseLsofListeners(lsof.stdout) : [];
let byPid: DiscoveredDaemonProcess[] = [];
const ps = spawnSync("ps", ["-axo", "pid=,comm=,args="], { encoding: "utf8" });
const ps = spawnSyncHidden("ps", ["-axo", "pid=,comm=,args="], { encoding: "utf8" });
if (!ps.error && ps.status === 0 && typeof ps.stdout === "string") {
const pids = parsePrimeAgentProcessIds(ps.stdout, APP_NAME);
if (pids.length > 0) {
const lsofByPid = spawnSync("lsof", ["-nP", "-F", "pn", "-U", "-a", "-p", pids.join(",")], {
const lsofByPid = spawnSyncHidden("lsof", ["-nP", "-F", "pn", "-U", "-a", "-p", pids.join(",")], {
encoding: "utf8",
});
if (!lsofByPid.error && typeof lsofByPid.stdout === "string") {
Expand All @@ -212,7 +212,7 @@ function enrichUptimes(daemons: DiscoveredDaemonProcess[]): DiscoveredDaemonProc
if (pids.length === 0) {
return daemons;
}
const ps = spawnSync("ps", ["-o", "pid=,etimes=", "-p", pids.join(",")], { encoding: "utf8" });
const ps = spawnSyncHidden("ps", ["-o", "pid=,etimes=", "-p", pids.join(",")], { encoding: "utf8" });
if (ps.error || typeof ps.stdout !== "string") {
return daemons;
}
Expand Down Expand Up @@ -808,7 +808,7 @@ function recordResidualListenerFailures(
}
}
function describeDaemonParent(pid: number): string {
const result = spawnSync("ps", ["-o", "ppid=,tty=,command=", "-p", String(pid)], { encoding: "utf8" });
const result = spawnSyncHidden("ps", ["-o", "ppid=,tty=,command=", "-p", String(pid)], { encoding: "utf8" });
if (result.error || result.status !== 0 || typeof result.stdout !== "string") {
return "";
}
Expand Down
5 changes: 2 additions & 3 deletions packages/coding-agent/src/cli/daemon-update-restart.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { spawn } from "node:child_process";
import { createHash, randomUUID } from "node:crypto";
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
Expand All @@ -14,7 +13,7 @@ import {
DAEMON_WORKER_SUPERVISOR_SOCKET_ENV,
DAEMON_WORKER_TOKEN_ENV,
} from "../modes/daemon/daemon-worker-protocol.js";
import { isProcessAlive } from "../utils/child-process.js";
import { isProcessAlive, spawnHidden } from "../utils/child-process.js";
import { createCliSubprocessLaunchSpec } from "./subprocess-launch.js";

export const DAEMON_UPDATE_RESTART_COORDINATOR_FLAG = "--internal-update-restart-coordinator";
Expand Down Expand Up @@ -548,7 +547,7 @@ export async function launchDaemonUpdateRestartCoordinator(
statusPath,
...(originActiveSessionId ? [DAEMON_UPDATE_RESTART_ORIGIN_FLAG, originActiveSessionId] : []),
]);
const child = spawn(launch.command, launch.args, {
const child = spawnHidden(launch.command, launch.args, {
cwd: options.cwd ?? process.cwd(),
detached: true,
env: coordinatorEnvironment(agentDir),
Expand Down
5 changes: 3 additions & 2 deletions packages/coding-agent/src/cli/owned-session-worker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type ChildProcess, type StdioOptions, spawn } from "node:child_process";
import type { ChildProcess, StdioOptions } from "node:child_process";
import { randomUUID } from "node:crypto";
import { chmodSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
Expand All @@ -14,6 +14,7 @@ import {
} from "../core/orphan-process-journal.js";
import { SESSION_LEASE_OWNER_ID_ENV, SESSION_LEASES_ENABLED_ENV } from "../core/session-lease.js";
import { attachJsonlLineReader, serializeJsonLine } from "../modes/rpc/jsonl.js";
import { spawnHidden } from "../utils/child-process.js";
import { isHelpCommandRequest, PUBLIC_COMMAND_NAMES, REMOVED_COMMAND_NAMES } from "./command-registry.js";
import { type CliSubprocessLaunchSpec, createCliSubprocessLaunchSpec } from "./subprocess-launch.js";

Expand Down Expand Up @@ -335,7 +336,7 @@ export async function runOwnedSessionWorkerFrontend(
const stdio: StdioOptions = interactive
? ["inherit", "inherit", "inherit", "ipc"]
: [bridgeStdin ? "pipe" : "inherit", "pipe", "pipe", "ipc"];
const child = spawn(launch.command, launch.args, {
const child = spawnHidden(launch.command, launch.args, {
Comment thread
snimu marked this conversation as resolved.
cwd: process.cwd(),
detached: process.platform !== "win32",
env: {
Expand Down
17 changes: 8 additions & 9 deletions packages/coding-agent/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { spawnSync } from "child_process";
import { createHash } from "crypto";
import {
accessSync,
Expand All @@ -13,9 +12,9 @@ import {
statSync,
} from "fs";
import { homedir } from "os";
import { basename, dirname, join, resolve, sep, win32 } from "path";
import { basename, dirname, join, posix, resolve, sep, win32 } from "path";
import { fileURLToPath } from "url";
import { shouldUseWindowsShell } from "./utils/child-process.js";
import { shouldUseWindowsShell, spawnSyncHidden } from "./utils/child-process.js";
import { normalizeSocketPath } from "./utils/daemon-socket-path.js";

// =============================================================================
Expand Down Expand Up @@ -207,7 +206,7 @@ function readCommandOutput(
args: string[],
options: { requireSuccess?: boolean } = {},
): string | undefined {
const result = spawnSync(command, args, {
const result = spawnSyncHidden(command, args, {
encoding: "utf-8",
stdio: ["ignore", "pipe", "pipe"],
shell: shouldUseWindowsShell(command),
Expand Down Expand Up @@ -366,9 +365,7 @@ export function getPackageDir(): string {
// Allow override via environment variable (useful for Nix/Guix where store paths tokenize poorly)
const envDir = process.env.PI_PACKAGE_DIR;
if (envDir) {
if (envDir === "~") return homedir();
if (envDir.startsWith("~/")) return homedir() + envDir.slice(1);
return envDir;
return expandTildePath(envDir);
}

if (isBunBinary) {
Expand Down Expand Up @@ -503,9 +500,11 @@ export const ENV_AGENT_DIR = `${envPrefix}_CODING_AGENT_DIR`;
export const ENV_SESSION_DIR = `${envPrefix}_SESSION_DIR`;
export const ENV_LEGACY_SESSION_DIR = `${envPrefix}_CODING_AGENT_SESSION_DIR`;

export function expandTildePath(path: string): string {
export function expandTildePath(path: string, platform: NodeJS.Platform = process.platform): string {
if (path === "~") return homedir();
if (path.startsWith("~/")) return homedir() + path.slice(1);
if (path.startsWith("~/") || (platform === "win32" && path.startsWith("~\\"))) {
return (platform === "win32" ? win32 : posix).join(homedir(), path.slice(2));
}
return path;
}

Expand Down
7 changes: 6 additions & 1 deletion packages/coding-agent/src/core/agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@ interface RlmChildRun {
deletionCleanupFailed?: boolean;
deletionRunFinished?: boolean;
deletionNotice?: Promise<void>;
deletionFailureNotice?: Promise<void>;
deletionNeedsCompletionNotice?: boolean;
completeDeletion?: () => Promise<void>;
reportDeletionCleanupFailure?: (error: unknown) => Promise<void>;
Expand Down Expand Up @@ -10177,6 +10178,7 @@ export class AgentSession {
// resolved child. A failed preflight must leave the prior retry boundary
// intact so a later call can acquire it.
run.deletionCleanupFailed = false;
run.deletionFailureNotice = undefined;
run.deletionReservation = createAgentMessageDeferred();
}
// The detached task remains the sole lifecycle owner. Mark deletion before
Expand Down Expand Up @@ -10809,14 +10811,17 @@ export class AgentSession {

run.reportDeletionCleanupFailure = (error) => {
if (run.suppressTerminalNotice || this._disposed || this._disposing) return Promise.resolve();
if (run.deletionFailureNotice) return run.deletionFailureNotice;
const cleanupError = error instanceof Error ? error.message : String(error);
return deliverTerminalMessageToParent(
const notice = deliverTerminalMessageToParent(
createRlmChildFailureMessage({
childId: run.id,
sessionName,
error: `Deletion cleanup failed; retry rlm.delete_subagent("${run.id}") before completion: ${cleanupError}`,
}),
);
run.deletionFailureNotice = notice;
return notice;
};

// Runtime startup and the task run are deliberately detached. The public
Expand Down
5 changes: 2 additions & 3 deletions packages/coding-agent/src/core/autonomous.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { spawn } from "node:child_process";
import { createHash } from "node:crypto";
import { createReadStream } from "node:fs";
import { lstat, readlink } from "node:fs/promises";
import { resolve } from "node:path";
import type { AssistantMessage, Usage, UserMessage } from "@earendil-works/pi-ai";
import { waitForChildProcess } from "../utils/child-process.js";
import { spawnHidden, waitForChildProcess } from "../utils/child-process.js";
import { killProcessTree, trackDetachedChildPid, untrackDetachedChildPid } from "../utils/shell.js";

export interface AgentAutonomousConfig {
Expand Down Expand Up @@ -491,7 +490,7 @@ function runChildProcess(
): Promise<ChildProcessResult> {
options.signal?.throwIfAborted();
return new Promise((resolve) => {
const child = spawn(command, args, {
const child = spawnHidden(command, args, {
cwd: options.cwd,
detached: process.platform !== "win32",
shell: options.shell === true,
Expand Down
5 changes: 2 additions & 3 deletions packages/coding-agent/src/core/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
* Shared command execution utilities for extensions and custom tools.
*/

import { spawn } from "node:child_process";
import { waitForChildProcess } from "../utils/child-process.js";
import { spawnHidden, waitForChildProcess } from "../utils/child-process.js";

/**
* Options for executing shell commands.
Expand Down Expand Up @@ -58,7 +57,7 @@ export async function execCommand(
options?: ExecOptions,
): Promise<ExecResult> {
return new Promise((resolve) => {
const proc = spawn(command, args, {
const proc = spawnHidden(command, args, {
cwd,
shell: false,
stdio: ["ignore", "pipe", "pipe"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
*/

import { createConnection } from "node:net";
import { basename } from "node:path";
import { basename, win32 } from "node:path";
import type { ExtensionAPI, ExtensionFactory } from "../types.js";

type AgentState = "working" | "blocked" | "idle";
Expand All @@ -42,6 +42,16 @@ export function hasFileBasedHerdrIntegration(loadedExtensionPaths: string[]): bo
});
}

/** Windows dials local-domain sockets inside \\.\pipe\; Herdr exports a unix-style path, so map it (namespaced paths pass through). */
export function herdrSocketTarget(socketPath: string, platform: NodeJS.Platform = process.platform): string {
// The pipe namespace is case-insensitive, so only the prefix check lowercases.
const lowered = socketPath.toLowerCase();
if (platform !== "win32" || lowered.startsWith("\\\\.\\pipe\\") || lowered.startsWith("\\\\?\\pipe\\")) {
return socketPath;
}
return win32.join("\\\\.\\pipe\\", socketPath);
}

interface QueuedState {
state: AgentState;
message?: string;
Expand Down Expand Up @@ -171,7 +181,7 @@ function herdrAgentStateExtensionImpl(pi: ExtensionAPI, getLoadedExtensionPaths:
resolve();
};

const socket = createConnection(socketPath!);
const socket = createConnection(herdrSocketTarget(socketPath!));
socket.on("error", finish);
socket.on("connect", () => socket.write(`${JSON.stringify(request)}\n`));
socket.on("data", finish);
Expand Down
7 changes: 4 additions & 3 deletions packages/coding-agent/src/core/footer-data-provider.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { type ExecFileException, execFile, spawnSync } from "child_process";
import type { ExecFileException } from "child_process";
import { existsSync, type FSWatcher, readFileSync, unwatchFile, watchFile } from "fs";
import { dirname, join } from "path";
import { execFileHidden, spawnSyncHidden } from "../utils/child-process.js";
import { closeWatcher, FS_WATCH_RETRY_DELAY_MS, watchWithErrorHandler } from "../utils/fs-watch.js";
import { findGitPaths, type GitPaths } from "../utils/git.js";

/** Ask git for the current branch. Returns null on detached HEAD or if git is unavailable. */
function resolveBranchWithGitSync(repoDir: string): string | null {
const result = spawnSync("git", ["--no-optional-locks", "symbolic-ref", "--quiet", "--short", "HEAD"], {
const result = spawnSyncHidden("git", ["--no-optional-locks", "symbolic-ref", "--quiet", "--short", "HEAD"], {
cwd: repoDir,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
Expand All @@ -18,7 +19,7 @@ function resolveBranchWithGitSync(repoDir: string): string | null {
/** Ask git for the current branch asynchronously. Returns null on detached HEAD or if git is unavailable. */
function resolveBranchWithGitAsync(repoDir: string): Promise<string | null> {
return new Promise((resolvePromise) => {
execFile(
execFileHidden(
"git",
["--no-optional-locks", "symbolic-ref", "--quiet", "--short", "HEAD"],
{
Expand Down
Loading
Loading