Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

### Fixed

- The Windows RPC host supervisor now creates its internal socket directory recursively, so launching `--internal-rpc-host-supervisor` directly on a fresh profile no longer crashes with `ENOENT ... mkdir '<agentDir>\rpc-host-daemon\internal-<uuid>'` ([#1370](https://github.com/code-yeongyu/senpi/issues/1370))

### Removed

## [2026.9.6] - 2026-09-06
Expand Down
2 changes: 2 additions & 0 deletions packages/coding-agent/docs/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ The command response reports only `{ cancelled }`, so this event is the only pus

The lifecycle supervisor is also available to bundled/rebranded runtimes through the hidden internal launch route `--internal-rpc-host-supervisor`. This route is wire-invisible and intended only for desktop launchers: it receives the public socket, ownership directory, and the runtime command/arguments to wrap, then runs the same `host-lifecycle.ts` implementation used by `ensureHost()`. Normal CLI modes do not use or advertise this route. Compiled standalone binaries also re-enter themselves through this route automatically: a bun executable always boots its embedded entrypoint, so the script-path re-entry used under a JS runtime would be parsed as CLI arguments (`Unknown option: --socket`) and the host could never start.

On win32 the supervisor's internal hop lives under `<agentDir>/rpc-host-daemon/internal-<uuid>`, and that directory is created recursively, so a launcher may take this route on a fresh profile where `rpc-host-daemon` does not exist yet. The public socket secret is not self-provisioned: on win32 the supervisor still reads `<publicSocket>.secret`, which `ensureHost()` writes before spawning, so a direct launch must provision it the same way.

Hosts started through `ensureHost()` are wrapped by a lifecycle supervisor that owns the public socket and spawns the
real RPC host on a private internal hop. The policy lives in `<agentDir>/rpc-host-daemon/settings.json`:

Expand Down
20 changes: 20 additions & 0 deletions packages/coding-agent/src/modes/rpc/changes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# changes

## win32 supervisor creates its internal socket directory recursively (2026-09-07)

### What changed

- `packages/coding-agent/src/modes/rpc/host-lifecycle.ts`: the win32 branch of `createInternalSocketPath()` now creates `<baseDir>/internal-<uuid>` with `recursive: true` instead of `recursive: false`. The function is exported and takes an injectable `platform`, mirroring `spawnableChildLaunch(launch, platform)` in the same module, so the win32 bootstrap is coverable from any host. The posix branch is unchanged.
- `packages/coding-agent/docs/rpc.md`: the shared-host lifecycle section records that the win32 internal hop directory is created recursively, and that the public socket secret is still caller-provisioned.
- `packages/coding-agent/test/suite/regressions/1370-rpc-internal-socket-mkdir.test.ts`: regression coverage for the win32 bootstrap against a missing `rpc-host-daemon`, plus a guard that the posix branch stays rooted in the OS temp dir.

### Why

- `runHostSupervisor()` passes `paths.dir` (`<agentDir>/rpc-host-daemon`) as the base directory. `ensureHost()` creates that parent before spawning, but the hidden `--internal-rpc-host-supervisor` launch route does not, so on a fresh Windows profile the supervisor died during bootstrap with `ENOENT: no such file or directory, mkdir '<agentDir>\rpc-host-daemon\internal-<uuid>'` (#1370). The posix branch never hit this because it roots the directory in `tmpdir()`, which always exists.

### Why an extension could not handle it

- The failure happens inside the supervisor's own bootstrap, before any session, runtime, or extension surface exists.

### Expected merge conflict zones

- LOW: the `createInternalSocketPath` signature and its win32 `mkdir` call in `host-lifecycle.ts`, and the internal launch route paragraph in `docs/rpc.md`.

## Shared-host logical sessions are unlimited by default (2026-09-06)

### What changed
Expand Down
11 changes: 8 additions & 3 deletions packages/coding-agent/src/modes/rpc/host-lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,18 @@ const CHILD_WATCH_FD = 3;
* The internal hop must stay short enough for sun_path (104 bytes on macOS)
* regardless of where the public socket lives, and private against other local
* users, so it gets its own 0700 directory under the OS temp directory.
*
* On win32 the directory lives under the caller-supplied rpc-host-daemon
* directory, which ensureHost() creates but a direct --internal-rpc-host-supervisor
* launch does not, so the parent is created recursively.
*/
async function createInternalSocketPath(
export async function createInternalSocketPath(
baseDir = tmpdir(),
platform: NodeJS.Platform = process.platform,
): Promise<{ socket: string; dir?: string; secretPath?: string }> {
if (process.platform === "win32") {
if (platform === "win32") {
const dir = join(baseDir, `internal-${randomUUID()}`);
await mkdir(dir, { recursive: false, mode: 0o700 });
await mkdir(dir, { recursive: true, mode: 0o700 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Record the RPC behavior change in its scoped docs

This changes fresh-profile startup behavior in host-lifecycle.ts, but the commit leaves both packages/coding-agent/src/modes/rpc/changes.md and the shared-host lifecycle section of docs/rpc.md untouched. Record the Windows bootstrap behavior in both files in this increment so the fork tracker and documented lifecycle contract remain aligned.

AGENTS.md reference: packages/coding-agent/src/modes/rpc/AGENTS.md:L52-L52

Useful? React with 👍 / 👎.

return {
socket: `\\\\.\\pipe\\senpi-rpc-internal-${randomUUID()}`,
dir,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { existsSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createInternalSocketPath } from "../../../src/modes/rpc/host-lifecycle.ts";

// Regression coverage for https://github.com/code-yeongyu/senpi/issues/1370
describe("createInternalSocketPath", () => {
const created: string[] = [];

afterEach(() => {
for (const dir of created.splice(0)) rmSync(dir, { recursive: true, force: true });
});

it("creates the win32 internal directory when rpc-host-daemon does not exist yet", async () => {
const agentDir = mkdtempSync(join(tmpdir(), "senpi-hlc-win32-"));
created.push(agentDir);
const daemonDir = join(agentDir, "rpc-host-daemon");
expect(existsSync(daemonDir)).toBe(false);

const internal = await createInternalSocketPath(daemonDir, "win32");

const dir = internal.dir;
if (dir === undefined) throw new Error("expected an internal socket directory");
expect(existsSync(dir)).toBe(true);
expect(dirname(dir)).toBe(daemonDir);
expect(internal.socket.startsWith("\\\\.\\pipe\\")).toBe(true);
expect(internal.secretPath).toBe(join(dir, "secret"));
});

it("keeps the posix internal directory in the OS temp dir", async () => {
const internal = await createInternalSocketPath(join(tmpdir(), "senpi-hlc-unused"), "linux");

const dir = internal.dir;
if (dir === undefined) throw new Error("expected an internal socket directory");
created.push(dir);
expect(existsSync(dir)).toBe(true);
expect(dirname(dir)).toBe(tmpdir());
expect(internal.socket).toBe(join(dir, "host.sock"));
});
});