Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
30 changes: 29 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -349,12 +349,40 @@ jobs:
- name: Run Windows behavior tests
working-directory: packages/coding-agent
run: >-
bun test
bun test --isolate --timeout 120000
test/bash-close-hang-windows.test.ts
test/child-process.test.ts
test/command-recovery-journal.test.ts
test/config-windows-hardening.test.ts
test/daemon-command.test.ts
test/daemon-socket.test.ts
test/exec.test.ts
test/frontmatter.test.ts
test/herdr-agent-state.test.ts
test/kernel-bash-shell.test.ts
test/kernel-bootstrap-windows.test.ts
test/kernel-windows-process.test.ts
test/path-utils.test.ts
test/rlm-subagent-display.test.ts
test/rlm-subagent-display-windows.test.ts
test/session-lease.test.ts
test/settings-manager.test.ts
test/settings-manager-bug.test.ts
test/shell-windows-hardening.test.ts
test/system-prompt.test.ts

- name: Test Windows RLM deletion races
working-directory: packages/coding-agent
run: >-
bun test --isolate --timeout 120000
--test-name-pattern
"emits exactly one failure notice per delete retry|keeps a child live when its durable deletion boundary cannot be read|reconciles a stale display status|uses the persistent artifact dir and sets RLM_SESSION_DIR"
test/agent-session-recursion.test.ts
test/daemon-mode.test.ts

- name: Test daemon worker named-pipe lifecycle
working-directory: packages/coding-agent
run: bun test --isolate --timeout 240000 test/daemon-named-pipe-lifecycle.test.ts

- name: Test terminal behavior
working-directory: packages/tui
Expand Down
1 change: 1 addition & 0 deletions packages/coding-agent/.changes/windows-hardening.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Fixed Windows daemon startup delays, session and bootstrap lock races, RLM deletion races, Git Bash selection, durable settings writes, multiline paste handling, path normalization, and session scratch isolation.
1 change: 1 addition & 0 deletions packages/coding-agent/src/cli/daemon-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,7 @@ async function runStart(parsed: ParsedDaemonClientCommand): Promise<void> {
const child = spawn(launch.command, launch.args, {
cwd: sessionArgs.config?.cwd ?? process.cwd(),
detached: true,
windowsHide: process.platform === "win32",
env: process.env,
stdio: "ignore",
});
Expand Down
1 change: 1 addition & 0 deletions packages/coding-agent/src/cli/daemon-launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ Then retry the original command.`,
const child = spawn(launch.command, launch.args, {
cwd: spawnCwd ?? process.cwd(),
detached: true,
windowsHide: process.platform === "win32",
env,
// A pipe would tie the daemon's stderr to this short-lived CLI
// (EPIPE once it exits); crash details come from the daemon log,
Expand Down
1 change: 1 addition & 0 deletions packages/coding-agent/src/cli/daemon-update-restart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,7 @@ export async function launchDaemonUpdateRestartCoordinator(
const child = spawn(launch.command, launch.args, {
cwd: options.cwd ?? process.cwd(),
detached: true,
windowsHide: process.platform === "win32",
env: coordinatorEnvironment(agentDir),
stdio: "ignore",
});
Expand Down
1 change: 1 addition & 0 deletions packages/coding-agent/src/cli/owned-session-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ export async function runOwnedSessionWorkerFrontend(
const child = spawn(launch.command, launch.args, {
cwd: process.cwd(),
detached: process.platform !== "win32",
windowsHide: process.platform === "win32" && !interactive,
env: {
...process.env,
[OWNED_WORKER_ENV]: "1",
Expand Down
5 changes: 4 additions & 1 deletion packages/coding-agent/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,10 @@ export const ENV_LEGACY_SESSION_DIR = `${envPrefix}_CODING_AGENT_SESSION_DIR`;

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

Expand Down
26 changes: 25 additions & 1 deletion packages/coding-agent/src/core/agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -920,6 +920,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 @@ -4434,6 +4435,7 @@ export class AgentSession {
customPrompt: loaderSystemPrompt,
appendSystemPrompt,
messagesPath: this.sessionManager.getSessionFile(),
scratchPath: this._ensureSessionScratchDir(),
selectedTools: validToolNames,
toolSnippets,
promptGuidelines,
Expand Down Expand Up @@ -9486,6 +9488,13 @@ export class AgentSession {
RLM_MAX_DEPTH: String(this._rlmMaxDepth),
RLM_GLOBAL_HARNESS_STATE_DIR: getGlobalHarnessStateDir(),
};
const scratchDir = this._ensureSessionScratchDir();
if (scratchDir) {
env.PRIME_AGENT_SESSION_TMP = scratchDir;
env.TMP = scratchDir;
env.TEMP = scratchDir;
env.TMPDIR = scratchDir;
}
const rlmSessionDir = this._ensureRlmSessionDir();
if (rlmSessionDir) {
env.RLM_SESSION_DIR = rlmSessionDir;
Expand Down Expand Up @@ -9521,6 +9530,14 @@ export class AgentSession {
}
}

private _ensureSessionScratchDir(): string | undefined {
const sessionArtifactDir = this.sessionManager.getSessionArtifactDir();
if (!sessionArtifactDir) return undefined;
const scratchDir = join(sessionArtifactDir, "tmp");
mkdirSync(scratchDir, { recursive: true });
return scratchDir;
}

// Undefined when there's no persistent artifact dir (e.g. the viewer client):
// don't mkdtemp here, since this runs on every kernel build but a viewer never
// does RLM work. The temp dir is created lazily in _createChildRlmSessionDir.
Expand Down Expand Up @@ -10114,7 +10131,10 @@ export class AgentSession {
// Reset retry coordination only after selector preflight reaches the
// resolved child. A failed preflight must leave the prior retry boundary
// intact so a later call can acquire it.
// Also reset the failure-notice memo so a fresh retry emits exactly one
// notice per attempt rather than suppressing later retry failures forever
run.deletionCleanupFailed = false;
run.deletionFailureNotice = undefined;
run.deletionReservation = createAgentMessageDeferred();
}
// The detached task remains the sole lifecycle owner. Mark deletion before
Expand Down Expand Up @@ -10679,14 +10699,18 @@ export class AgentSession {

run.reportDeletionCleanupFailure = (error) => {
if (run.suppressTerminalNotice || this._disposed || this._disposing) return Promise.resolve();
// Send at most one cleanup failure notice per explicit delete attempt.
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Built-in Herdr integration extension.
*
* Reports agent lifecycle state (working/idle/blocked) to the Herdr terminal
* workspace manager via its Unix socket. This is the in-tree equivalent of
* workspace manager via its Unix socket or Windows named pipe. This is the in-tree equivalent of
* the extension that `herdr integration install pi` writes, so Prime Agent
* works inside Herdr panes out of the box without a manual install step.
*
Expand All @@ -22,6 +22,25 @@ import type { ExtensionAPI, ExtensionFactory } from "../types.js";

type AgentState = "working" | "blocked" | "idle";

const WINDOWS_PIPE_PREFIX = `\\\\.\\pipe\\`;
const WINDOWS_QUALIFIED_PIPE_PREFIX = /^\\\\[.?]\\pipe\\/i;

/**
* Resolve the socket endpoint for the current platform.
* On Windows, Herdr exposes a named pipe and the path must be mapped
* to the pipe namespace. Idempotent: an already-qualified pipe name
* (`\\.\pipe\...`) is returned unchanged.
*/
export function resolveHerdrSocketPath(socketPath: string, platform: NodeJS.Platform = process.platform): string {
if (platform !== "win32") {
return socketPath;
}
if (WINDOWS_QUALIFIED_PIPE_PREFIX.test(socketPath)) {
return socketPath;
}
return `${WINDOWS_PIPE_PREFIX}${socketPath}`;
}

/**
* True when Herdr's own file-based Pi integration (`herdr integration
* install pi`) is among the extension files the loader actually loaded this
Expand Down Expand Up @@ -171,7 +190,7 @@ function herdrAgentStateExtensionImpl(pi: ExtensionAPI, getLoadedExtensionPaths:
resolve();
};

const socket = createConnection(socketPath!);
const socket = createConnection(resolveHerdrSocketPath(socketPath!));
socket.on("error", finish);
socket.on("connect", () => socket.write(`${JSON.stringify(request)}\n`));
socket.on("data", finish);
Expand Down
Loading
Loading