Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
dc2b697
fix(rpc): start the shared RPC host on Windows
sanguneo Sep 1, 2026
5277f73
fix(rpc): let the supervisor spawn a .cmd child verbatim
sanguneo Sep 1, 2026
e34e2f2
Merge remote-tracking branch 'origin/main' into fix/windows-rpc-named…
sanguneo Sep 1, 2026
ecd0aac
Merge origin/main into fix/windows-rpc-named-pipe
sanguneo Sep 1, 2026
968f933
fix(rpc): address Windows lifecycle review findings
code-yeongyu Sep 1, 2026
8e0ccac
fix(rpc): preserve Windows endpoint aliases
code-yeongyu Sep 1, 2026
5b39ad2
test(rpc): cover canonical Windows pipe addresses
code-yeongyu Sep 1, 2026
df8828e
fix(rpc): harden Windows host lifecycle
code-yeongyu Sep 1, 2026
f468c30
Merge remote-tracking branch 'origin/main' into fix/windows-rpc-named…
code-yeongyu Sep 1, 2026
7b84752
fix(rpc): close Windows lifecycle review gaps
code-yeongyu Sep 1, 2026
952fbbe
fix(rpc): clean up failed detached child registration
code-yeongyu Sep 1, 2026
09d4b84
ci(rpc): run named-pipe lifecycle tests on Windows
code-yeongyu Sep 1, 2026
a2a6f61
Merge remote-tracking branch 'origin/main' into fix/windows-rpc-named…
code-yeongyu Sep 1, 2026
ac7901d
fix(rpc): reject root-relative Windows endpoints
code-yeongyu Sep 1, 2026
a17f472
fix(app-server): clean up daemon on registration write failure
code-yeongyu Sep 1, 2026
7b703e1
fix(rpc): secure Windows named-pipe listeners
code-yeongyu Sep 1, 2026
4d365a0
Revert "fix(rpc): secure Windows named-pipe listeners"
code-yeongyu Sep 1, 2026
837f97e
fix(rpc): authenticate Windows socket clients
code-yeongyu Sep 1, 2026
40b95fa
fix(rpc): authenticate Windows socket clients
code-yeongyu Sep 1, 2026
8239685
fix(rpc): authenticate internal Windows pipe
code-yeongyu Sep 1, 2026
1fccb4d
fix(rpc): complete Windows handshake lifecycle fixes
code-yeongyu Sep 1, 2026
efabdd0
test(rpc): align Windows fixtures with secret-derived pipe addresses
code-yeongyu Sep 1, 2026
988bdfa
docs(changes): cover the Windows RPC CI job in the workflows tracker
code-yeongyu Sep 1, 2026
d22af74
docs(changelog): cover Windows RPC named-pipe support
code-yeongyu Sep 1, 2026
03a29bc
test(rpc): gate the owner-only mode assertion to POSIX
code-yeongyu Sep 1, 2026
cf9bd7c
test(rpc): use Windows process termination primitive
code-yeongyu Sep 1, 2026
b7d8a41
chore(rpc): rerun Windows lifecycle CI
code-yeongyu Sep 1, 2026
46232a6
merge origin/main into Windows RPC fix
code-yeongyu Sep 1, 2026
3857014
fix(rpc): detect dead Windows supervisors reliably
code-yeongyu Sep 1, 2026
393a272
fix(rpc): make supervisor watchdog prompt on Windows
code-yeongyu Sep 1, 2026
ab332f4
fix(rpc): preserve self-bound watchdog test behavior
code-yeongyu Sep 1, 2026
2388583
Revert "fix(rpc): preserve self-bound watchdog test behavior"
code-yeongyu Sep 1, 2026
cd2ab3e
Revert "fix(rpc): make supervisor watchdog prompt on Windows"
code-yeongyu Sep 1, 2026
5e672e7
chore(rpc): add temporary win32 CI diagnostics
code-yeongyu Sep 1, 2026
aa55c33
fix(rpc): preserve canonical watchdog env handoff
code-yeongyu Sep 1, 2026
d366afb
fix(rpc): read supervisor watchdog env directly
code-yeongyu Sep 1, 2026
bd1dc48
chore(rpc): trace win32 watchdog cleanup
code-yeongyu Sep 1, 2026
a24be31
fix(rpc): do not block watchdog exit on cleanup
code-yeongyu Sep 1, 2026
4eea940
fix(rpc): bound Windows named-pipe host teardown
code-yeongyu Sep 1, 2026
8aed7a3
fix(rpc): preserve POSIX watchdog cleanup ordering
code-yeongyu Sep 1, 2026
fcefcbb
chore(rpc): trace Windows watchdog binding
code-yeongyu Sep 1, 2026
3abd597
fix(rpc): short-circuit dead Windows supervisor polls
code-yeongyu Sep 1, 2026
b2e897b
fix(rpc): bound Windows supervisor identity probes
code-yeongyu Sep 1, 2026
78600fd
fix(rpc): terminate Windows host in shutdown finalizer
code-yeongyu Sep 1, 2026
20c9aa1
fix(rpc): finish Windows watchdog cleanup before exit
code-yeongyu Sep 1, 2026
89f4e42
fix(rpc): terminate Windows supervisor in finalizer
code-yeongyu Sep 1, 2026
890c2de
fix(test): use Windows process identity for exit waits
code-yeongyu Sep 1, 2026
6e3acac
fix(rpc): watch Windows host child identity
code-yeongyu Sep 1, 2026
3707533
chore(rpc): trace supervisor child watchdog
code-yeongyu Sep 1, 2026
694ff7f
fix(rpc): use live CIM identity for Windows probes
code-yeongyu Sep 1, 2026
f6f1868
fix(rpc): emit stable Windows process identities
code-yeongyu Sep 1, 2026
5a1bcaf
fix(rpc): order Windows watchdog state cleanup
code-yeongyu Sep 1, 2026
281484d
test(rpc): wait for Windows watchdog metadata
code-yeongyu Sep 1, 2026
6db8a97
fix(rpc): prioritize Windows watchdog pipe close
code-yeongyu Sep 1, 2026
bbf9b53
fix(rpc): own Windows watchdog pipe close
code-yeongyu Sep 1, 2026
a1c0c8b
fix(rpc): keep watchdog close Win32-only
code-yeongyu Sep 1, 2026
a5f91ca
test(rpc): allow eventual Windows supervisor teardown
code-yeongyu Sep 1, 2026
9b920cd
chore(test): dump immortal Windows lifecycle processes
code-yeongyu Sep 1, 2026
935a1e7
test(rpc): accept stale Windows pidfiles after exit
code-yeongyu Sep 1, 2026
0de6131
test(rpc): document stale pidfile oracle
code-yeongyu Sep 1, 2026
791ffd3
test(rpc): serialize Windows lifecycle probes
code-yeongyu Sep 1, 2026
e05faf9
test(rpc): clean stale Windows supervisor settings
code-yeongyu Sep 1, 2026
fba3068
ci(rpc): isolate Windows lifecycle test files
code-yeongyu Sep 1, 2026
e5d5bee
test(rpc): remove temporary Windows diagnostics
code-yeongyu Sep 1, 2026
27fd71c
fix(rpc): close round nine Windows lifecycle gaps
code-yeongyu Sep 1, 2026
8b11bb8
ci: install Bun on Windows RPC runner
code-yeongyu Sep 1, 2026
a9f5f86
ci: pin existing Bun setup action
code-yeongyu Sep 1, 2026
32c71ef
fix(rpc): preserve Windows CIM identity output semantics
code-yeongyu Sep 1, 2026
7b199dc
fix(rpc): return parsed process identity
code-yeongyu Sep 1, 2026
6de356c
fix(rpc): match Windows process identity timestamps
code-yeongyu Sep 1, 2026
2581612
fix(rpc): distinguish absent Windows processes
code-yeongyu Sep 1, 2026
0a3ac1f
fix(rpc): await protocol probe before cleanup
code-yeongyu Sep 1, 2026
e892450
fix(rpc): wait for existing host readiness
code-yeongyu Sep 1, 2026
90f17f8
fix(rpc): restore non-terminating ensure probe semantics
code-yeongyu Sep 1, 2026
598ee82
fix(rpc): give spawned host protocol probe the full startup budget
code-yeongyu Sep 2, 2026
82b7ca7
chore(rpc): trace Windows host termination dispatches
code-yeongyu Sep 2, 2026
1640d9b
fix(rpc): retry transient process identity probes
code-yeongyu Sep 2, 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
18 changes: 18 additions & 0 deletions .github/changes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# changes

## Windows RPC named-pipe suites gain a real Windows CI job (2026-09-01)

### What changed

- `.github/workflows/ci.yml` adds an `rpc-windows` job (`RPC named pipes (Windows)`, windows-latest, 20-minute timeout) that builds the workspace packages and runs `test/rpc-host-ensure.test.ts`, `test/rpc-host-lifecycle.test.ts`, `test/rpc-socket-transport.test.ts`, and `test/suite/app-server-daemon.test.ts` from `packages/coding-agent` on a real Windows runner.

### Why

- PR #1244 makes the shared RPC host work on Windows through named pipes with authenticated handshakes; the main coding-agent shards run on ubuntu only, so the win32-specific transport, lifecycle, and handshake behavior was untested in CI until this job.

### Why an extension could not handle it

- CI workflow wiring is repository build plumbing evaluated on GitHub's runners; no runtime extension surface can add a job to a GitHub Actions workflow.

### Expected merge conflict zones

- LOW: the job list at the end of `.github/workflows/ci.yml` and the `needs`/gate lists of `Check and test` whenever upstream adds or reorders CI jobs.

## Workflow summary step for the model catalog publisher (2026-09-01)

### What changed
Expand Down
64 changes: 57 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
node-version: "24"
cache: npm

- name: Install system dependencies
Expand Down Expand Up @@ -70,7 +70,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
node-version: "24"
cache: npm

- name: Install system dependencies
Expand Down Expand Up @@ -111,7 +111,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
node-version: "24"
cache: npm

- name: Install dependencies
Expand All @@ -121,6 +121,48 @@ jobs:
run: npx vitest run test/suite/hooks-trust.test.ts test/suite/hooks-trust-storage-errors.test.ts test/suite/hooks-trust-storage-release-errors.test.ts test/suite/hooks-trust-storage-aba.test.ts
working-directory: packages/coding-agent

rpc-windows:
name: RPC named pipes (Windows)
runs-on: windows-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"
cache: npm

- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: "1.4.0"

- name: Install dependencies
run: npm ci --ignore-scripts

- name: Build workspace package entries
Comment thread
code-yeongyu marked this conversation as resolved.
run: npm run build

# Each suite runs in its own process; lifecycle failures are not retried in-process.
- name: Run RPC host ensure tests
run: bunx vitest run test/rpc-host-ensure.test.ts
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
working-directory: packages/coding-agent

- name: Run RPC host lifecycle tests
run: bunx vitest run test/rpc-host-lifecycle.test.ts
Comment thread
code-yeongyu marked this conversation as resolved.
working-directory: packages/coding-agent

- name: Run RPC socket transport tests
run: bunx vitest run test/rpc-socket-transport.test.ts
working-directory: packages/coding-agent

- name: Run app-server daemon tests
run: bunx vitest run test/suite/app-server-daemon.test.ts
working-directory: packages/coding-agent

test-workspaces:
name: Test (workspaces + scripts)
runs-on: ubuntu-latest
Expand All @@ -132,7 +174,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
node-version: "24"
cache: npm

- name: Install system dependencies
Expand Down Expand Up @@ -177,7 +219,14 @@ jobs:
# so the required-status contract is unchanged while the work runs in parallel.
check-and-test:
name: Check and test
needs: [check, test-coding-agent, hooks-trust-storage-windows, test-workspaces]
needs:
[
check,
test-coding-agent,
hooks-trust-storage-windows,
rpc-windows,
test-workspaces,
]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
Expand All @@ -203,6 +252,7 @@ jobs:
echo "- check: ${{ needs.check.result }}"
echo "- test-coding-agent: ${{ needs.test-coding-agent.result }}"
echo "- hooks-trust-storage-windows: ${{ needs.hooks-trust-storage-windows.result }}"
echo "- rpc-windows: ${{ needs.rpc-windows.result }}"
echo "- test-workspaces: ${{ needs.test-workspaces.result }}"
} >> "$GITHUB_STEP_SUMMARY"

Expand All @@ -221,7 +271,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
node-version: "24"
cache: npm

- name: Install dependencies
Expand Down Expand Up @@ -254,7 +304,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
node-version: "24"
cache: npm

- name: Install dependencies
Expand Down
3 changes: 2 additions & 1 deletion packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@

### Fixed

- The shared RPC host now starts on Windows: socket endpoints resolve to `\\.\pipe\` named-pipe addresses derived from a per-endpoint secret (stored `0600` beside the logical path) with a constant-time authenticated handshake gating both the public and internal listeners, pidfile ownership proof no longer depends on MSYS `ps`, logical-path filesystem cleanup is skipped for named pipes, and detached supervisor/daemon startup failures no longer leak live children or signal reused PIDs. POSIX transports keep the prior unix-socket + `0600` behavior.

- `/quit` and `/exit` submitted while startup is still finishing (managed-tool downloads) now quit instead of being parked back in the editor behind a "Startup is still in progress" notice. Parking the text also disabled the Ctrl+D quit escape, which only fires on an empty editor, so the usual way out was a dead end until the line was cleared by hand.

- An extension calling `ctx.shutdown()` while the session is idle now shuts down immediately instead of waiting for an `agent_settled` event that an idle session never emits, which previously stranded the request until the user happened to run another turn.

- Interactive quit now keeps stderr capture installed while session shutdown handlers drain during runtime disposal, so shutdown-time diagnostics (for example memory drain warnings) are recorded in the debug log instead of printing raw beside the resume hint. The TUI still stops first to avoid final-frame repaints, and stderr is restored after disposal even when disposal fails.

- Multi-session RPC hosts launched with `SENPI_RPC_CLIENT_CAPABILITIES` (for example `extension_events`) now apply those capabilities to connection-owned session bindings when the client never sends `set_client_info`. Previously the launch capabilities were advertised through `get_protocol_info` but dropped at binding creation, so undeclared clients received no `extension_event` frames (breaking downstream task/DAG/monitor liveness in omo-desktop-app). An explicit `set_client_info` declaration, including an empty capability list, still overrides the launch default.
Expand Down
6 changes: 5 additions & 1 deletion packages/coding-agent/docs/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Multi-session mode lets one `senpi --mode rpc` process serve several independent
# Shared JSONL over stdio (legacy multi-session host)
senpi --mode rpc --multi-session [options]

# One shared host over a Unix socket; each accepted connection has its own JSONL feed
# One shared host over a local socket; each accepted connection has its own JSONL feed
senpi --mode rpc --listen unix:///tmp/senpi-rpc.sock [options]
senpi --mode rpc --listen /tmp/senpi-rpc.sock [options]
```
Expand All @@ -58,6 +58,10 @@ senpi --mode rpc --listen /tmp/senpi-rpc.sock [options]
`unix://@name` where supported by the host platform. Socket mode accepts concurrent connections while retaining one
process-global session registry.

On Windows, listeners and clients deterministically map the logical socket path to
`\\.\pipe\senpi-rpc-<sha256[:32]>`. Callers keep using the same `unix://` CLI value; the logical path remains the
ownership and settings identity, and callers never construct the pipe name themselves.

Socket event visibility is an all-sessions broadcast: every connected client receives lifecycle and agent events from
every open session, each tagged with its routing `sessionId`. Correlated responses and extension UI requests are sent
only to the connection that issued the command. This lets a non-owner observe a foreign turn without requiring a
Expand Down
20 changes: 20 additions & 0 deletions packages/coding-agent/src/modes/app-server/changes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# changes

## Cross-platform daemon process identity and lightweight exit waits (2026-09-01)

### What changed

- `packages/coding-agent/src/modes/app-server/daemon/process.ts` reads process start time from the live `Win32_Process` CIM table through PowerShell on Windows and preserves `ps -o lstart=` on POSIX.
- Process identity is validated with a platform-specific start-time reader before signaling managed children; exit waits repeat that identity check while waiting for termination. On Windows the bounded probe queries the live `Win32_Process` CIM table, so a terminated process retained by an open handle cannot appear live indefinitely.
Comment thread
code-yeongyu marked this conversation as resolved.

### Why

- Git for Windows exposes an MSYS `ps` that rejects `-o`; Windows daemons and shared RPC supervisors therefore received a pid but failed ownership registration with “had no process start time.”
- Start time is the PID-reuse ownership proof and is still checked before signaling. The same identity check is repeated while waiting so a reused PID cannot be mistaken for the managed child.

### Why an extension could not handle it

- Daemon ownership and signal safety run before the app-server or RPC extension surfaces exist.

### Expected merge conflict zones

- LOW: `readProcessStartTime`, `waitForGone`, and the adjacent process helper tail in `daemon/process.ts`.

## Provider-neutral account app-server routes (2026-08-27)

### What changed
Expand Down
63 changes: 59 additions & 4 deletions packages/coding-agent/src/modes/app-server/daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,17 +187,68 @@ async function spawnDaemon(paths: DaemonPaths, listen: AppServerListen): Promise
],
{
detached: true,
windowsHide: true,
env: { ...process.env, SENPI_RUNTIME: "node" },
stdio: ["ignore", "ignore", stderr.fd],
},
);
const exited = observeDaemonExit(child);
child.unref();
const pid = child.pid;
if (pid === undefined) throw new Error("failed to spawn daemon process");
const startTime = await waitForStartTime(pid, 2_000);
await writeFile(paths.pidFile, `${JSON.stringify({ pid, processStartTime: startTime })}\n`, { mode: 0o600 });
await writeFile(paths.settingsFile, `${JSON.stringify({ listen })}\n`, { mode: 0o600 });
let startTime: string;
try {
startTime = await Promise.race([
waitForStartTime(pid, 10_000),
exited.then(() => {
throw new Error(`spawned daemon ${pid} exited before its start time could be read`);
}),
]);
} catch (error: unknown) {
// Keep the handle owned until registration succeeds. This terminates the
// exact child even when start-time acquisition fails, without a raw PID.
if (child.exitCode === null && child.signalCode === null) {
try {
child.kill("SIGTERM");
} catch {}
await Promise.race([exited, delay(2_000)]);
if (child.exitCode === null && child.signalCode === null) {
try {
child.kill("SIGKILL");
} catch {}
}
}
await cleanupState(paths, listen);
throw error;
}
try {
await writeFile(paths.pidFile, `${JSON.stringify({ pid, processStartTime: startTime })}\n`, { mode: 0o600 });
await writeFile(paths.settingsFile, `${JSON.stringify({ listen })}\n`, { mode: 0o600 });
} catch (error: unknown) {
// Registration is the ownership hand-off point. Until both files exist,
// retain the exact ChildProcess handle and terminate it on any write
// failure so a partial registration can never leave an unmanaged daemon.
if (child.exitCode === null && child.signalCode === null) {
try {
child.kill("SIGTERM");
} catch {}
await Promise.race([exited, delay(2_000)]);
if (child.exitCode === null && child.signalCode === null) {
try {
child.kill("SIGKILL");
} catch (killError: unknown) {
throw new Error(
`failed to terminate daemon after registration failure: ${killError instanceof Error ? killError.message : String(killError)}`,
);
}
if (!(await Promise.race([exited.then(() => true), delay(2_000).then(() => false)]))) {
throw new Error(`daemon ${pid} remained alive after SIGKILL during registration failure`);
}
}
}
await cleanupState(paths, { ...listen, ...(listen.kind === "unix" ? { path: undefined } : {}) });
throw error;
Comment thread
code-yeongyu marked this conversation as resolved.
Comment thread
code-yeongyu marked this conversation as resolved.
}
child.unref();
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Comment thread
code-yeongyu marked this conversation as resolved.
return { pid, exited };
} finally {
await stderr.close();
Expand All @@ -221,6 +272,10 @@ async function waitForDaemonReady(
}
}

function delay(ms: number): Promise<void> {
return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
Comment thread
code-yeongyu marked this conversation as resolved.
}

function observeDaemonExit(child: ChildProcess): Promise<DaemonExit> {
return new Promise((resolveExit) => {
child.once("error", (error) => resolveExit({ kind: "error", error }));
Expand Down
Loading