Repository navigation
feat(agent-hooks): support Prime Agent status in WSL - #13430
Conversation
📝 WalkthroughWalkthroughThe change adds Prime Agent POSIX shell wrappers to generated Bash and Zsh startup files. PTY setup now provisions Prime status extensions, preserves WSL hook metadata, and cleans stale environment variables. WSL status delivery uses generated Windows 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 0c60e986-e8fc-4249-a81d-25d41092dd6d
📒 Files selected for processing (18)
src/main/agent-hooks/wsl-hook-relay-manager.test.tssrc/main/agent-hooks/wsl-hook-relay-manager.tssrc/main/daemon/shell-ready.test.tssrc/main/daemon/shell-ready.tssrc/main/ipc/pty.test.tssrc/main/ipc/pty.tssrc/main/pi/agent-status-extension-source.test.tssrc/main/pi/agent-status-extension-source.tssrc/main/pi/agent-status-wsl-curl-source.tssrc/main/pi/titlebar-extension-service.test.tssrc/main/pi/titlebar-extension-service.tssrc/main/providers/local-pty-shell-ready.test.tssrc/main/providers/local-pty-shell-ready.tssrc/main/providers/windows-shell-args.test.tssrc/main/pty/prime-agent-shell-wrapper.test.tssrc/main/pty/prime-agent-shell-wrapper.tssrc/main/pty/wsl-orca-env.test.tssrc/main/pty/wsl-orca-env.ts
| ' const child = spawn(', | ||
| ' curlPath,', | ||
| ' [', | ||
| " '-sS',", | ||
| ' // Why: detached delivery may take seconds under loaded WSL interop.', | ||
| " '--connect-timeout', '3',", | ||
| " '--max-time', '10',", | ||
| " '--noproxy', '127.0.0.1',", | ||
| " '-o', 'NUL',", | ||
| " '-X', 'POST',", | ||
| " '-H', 'Content-Type: application/json',", | ||
| " '-H', `X-Orca-Agent-Hook-Token: ${windowsToken}`,", | ||
| " '--data-binary', '@-',", | ||
| ' windowsUrl', | ||
| ' ],', | ||
| " { stdio: ['pipe', 'ignore', 'ignore'] }", | ||
| ' )', | ||
| " child.on('error', () => {})", | ||
| " child.stdin.on('error', () => {})", | ||
| ' child.stdin.end(body)', |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Node.js child_process spawn detached unref piped stdin parent process exit behavior
💡 Result:
When using child_process.spawn in Node.js, the interaction between detached, unref, and piped stdin significantly impacts whether a parent process can exit independently [1][2]. Detached Processes and Parent Exit Behavior By default, a parent process waits for its spawned subprocesses to exit [1][3]. To allow a parent to exit while the child continues to run, you must call subprocess.unref on the child process instance [1][4]. This removes the child process from the parent's event loop reference count [1][2]. However, the parent will still wait if an Inter-Process Communication (IPC) channel remains established between them [1][3]. Piped Stdin and Detachment If you pipe stdin (or other stdio streams) between the parent and child process, the parent process may be prevented from exiting cleanly even after calling unref, because the active pipe keeps the communication channel open [1][5]. If your goal is to allow the parent to exit while the child continues running: 1. Detach the child process: Set { detached: true } in the spawn options [1][2]. On Unix, this creates a new session and process group, effectively detaching it from the parent's terminal [5]. On Windows, this allows the child to continue running in its own console window [2]. 2. Handle stdio: If the child does not need to interact with the parent's stdin, stdout, or stderr, set stdio: 'ignore' [1][6]. This prevents the pipes from keeping the parent process event loop active [1][2]. 3. Call unref: Explicitly call subprocess.unref to remove the child from the parent's reference count [1][3]. If you leave stdio as 'pipe' (the default) while attempting to detach, the parent process often remains blocked or attached to the child's lifecycle because the pipe maintains a reference to the child's stdio streams [1][2]. Using 'ignore' is generally the recommended practice when you intend for the child process to be truly backgrounded and independent [1][6].
Citations:
- 1: https://nodejs.org/api/child_process.html
- 2: https://github.com/nodejs/node/blob/d90d9d55/doc/api/child_process.md
- 3: https://nodejs.org/dist/latest/docs/api/child_process.html
- 4: https://beta.docs.nodejs.org/child_process.html
- 5: child_process.spawn will either exit or not control stdin when detached and using inherit nodejs/node#5549
- 6: https://stackoverflow.com/questions/53179920/node-js-how-to-spawn-a-child-process-without-blocking-stdin-of-parent-process
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate file =="
fd -a 'agent-status-wsl-curl-source\.ts$' . || true
echo "== file outline/size =="
file="$(fd 'agent-status-wsl-curl-source\.ts$' . | head -n1)"
if [ -n "${file:-}" ]; then
wc -l "$file"
ast-grep outline "$file" || true
sed -n '1,120p' "$file" | cat -n
fi
echo "== related calls/usages =="
rg -n "AgentStatus|agent-status-wsl-curl|curlPath|windowsUrl|windowsToken|spawn\\(" . -g '*.ts' -g '*.tsx' -g '*.js' -g '*.jsx' || true
echo "== node child_process behavior probe =="
node - <<'JS'
const { spawn } = require('child_process')
const sleep = require('node:timers/promises').sleep
async function probe(desc, options, onData) {
const start = process.hrtime.bigint()
const child = spawn(process.execPath, ['-e', 'setTimeout(() => {}, 2000)'], { detach: false, ...options })
let events = []
child.stderr.on('data', chunk => events.push(chunk.toString()))
onData?.(child)
await sleep(300)
const alive = !kid
? false : true
let exits = false
const p = new Promise(resolve => {
child.on('exit', code => {
exits = true; resolve(code)
})
})
// not exit parent here
console.log(`${desc}: stream=${!!child.stdin}: childRef? ${child.unref.toString().includes('[Function unref]`)}; stderr=${events.join(',')}`)
// cleanup
};
probe()
JSRepository: stablyai/orca
Length of output: 50370
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== target file =="
target="$(fd 'agent-status-wsl-curl-source\.ts$' . | head -n1 || true)"
if [ -n "${target:-}" ]; then
wc -l "$target"
sed -n '1,120p' "$target" | cat -n
else
echo "target not found"
fi
echo "== agent hooks spawning this source =="
rg -n "agent-status-wsl-curl-source|AgentStatusWslCurlSource|curlSource|spawn\\(" src/main/agent-hooks -g '*.ts' || true
echo "== focused references to generated WSL curl source =="
rg -n "curl source|curlPath|windowsUrl|windowsToken|stdin.end|std\\.|unref|detached" src/main/agent-hooks src/main/pi -g '*.ts' -g '*.tsx' || true
echo "== deterministic Node child_process probe =="
node - <<'JS'
const { spawn, execFile } = require('node:child_process')
const { sleep } = require('node:timers/promises')
async function probe(name, factory) {
const ready = await factory()
await sleep(250)
ready.child.kill('SIGTERM')
const exit = await new Promise(resolve => ready.child.once('exit', resolve))
process.stdin.end()
console.log(`${name}: exit=${exit}`)
}
probe('default spawn child', () => {
const child = spawn(process.execPath, ['-e', 'setTimeout(() => {}, 2000)'], { stdio: ['ignore', 'ignore', 'ignore'] })
return { child, kind: 'ignore' }
})
probe('ignored stdin/spawn', () => {
const child = spawn(process.execPath, ['-e', 'setTimeout(() => {}, 2000)'], { stdio: ['ignore', 'ignore', 'ignore'], detached: false })
return { child, kind: 'ignore' }
})
probe('ignore stdin without end', () => {
const child = spawn(process.execPath, ['-e', 'Date.now(); process.stderr.write(`${process.pid}\\n`);'], { stdio: ['ignore', 'pipe', 'ignore'], detached: false })
child.once('exit', () => {})
return { child, kind: 'pipe' }
})
JSRepository: stablyai/orca
Length of output: 7847
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate tests / usages for generated WSL curl source =="
rg -n "agent-status-wsl-curl-source|getPiAgentStatusWslCurlSourceLines|WslCurl|curl.exe|NUL|child\\.unref|detached" src/main -g '*.test.ts' -g '*.spec.ts' -g '*.ts' || true
echo "== Node spawn lifetime probes with non-terminating curl-like child =="
node - <<'JS'
const { spawn } = require('node:child_process')
const timers = require('node:timers/promises')
async function lifetimeProbe(title, child, signalAfter = 200) {
let gotExit = false
child.once('exit', () => { gotExit = true })
await timers.sleep(signalAfter)
if (!gotExit) child.kill()
await new Promise(setImmediate)
console.log(`${title}: alive=${!gotExit}`)
gotExit = false
child.once('exit', () => { gotExit = true })
await timers.sleep(20)
console.log(`${title}: exitedAfterKill=${gotExit}`)
}
async function main() {
await lifetimeProbe(
'pipe stdin with child.stdin.end',
spawn(process.execPath, ['-e', 'setTimeout(setTimeout, 2000)'], { stdio: ['pipe', 'ignore', 'ignore'], detached: false })
)
await lifetimeProbe(
'ignore stdin',
spawn(process.execPath, ['-e', 'setTimeout(setTimeout, 2000)'], { stdio: ['ignore', 'ignore', 'ignore'], detached: false })
)
await lifetimeProbe(
'ignore stdin referenced',
spawn(process.execPath, ['-e', 'setTimeout(setTimeout, 2000)'], { stdio: ['ignore', 'ignore', 'ignore'], detached: false, windowsHide: true })
)
}
main().catch(err => { console.error(err); process.exitCode = 1 })
JSRepository: stablyai/orca
Length of output: 50369
🌐 Web query:
Node.js child_process unref piped stdin active stdio prevents parent exit
💡 Result:
In Node.js, when you pipe process.stdin to a child process, the parent process often remains active even after the child process exits [1][2][3]. This occurs because the pipe establishes an active handle that keeps the parent's event loop running, effectively preventing the parent from terminating [1][3]. Simply calling subprocess.unref() on the child process is often insufficient to allow the parent to exit if process.stdin remains connected [4][5][6]. The underlying issue is that the parent's stdin stream is still open and "referenced," meaning the Node.js event loop continues to monitor it for incoming data [7][8]. To ensure the parent process can exit, you must explicitly clean up or unreference the stdin stream [1][2][7]. Recommended approaches include: 1. Use stream.pipeline instead of .pipe(): The pipeline utility automatically handles cleanup, including destroying streams when the pipeline completes or encounters an error, which prevents lingering handles [1]. 2. Manually destroy or unref the input stream: If you must use .pipe(), you can listen for the child process's 'exit' or 'close' event and explicitly call process.stdin.destroy() or process.stdin.unref() to release the handle [1][2][8]. Note that process.stdin.unref() makes the stream no longer keep the event loop alive, but it does not close the stream [7]. If the application requires that the stdin handle be fully closed to prevent the process from hanging, process.stdin.destroy() is generally more reliable [1]. Additionally, when spawning detached processes, using stdio: 'ignore' is a standard way to prevent the child from remaining tethered to the parent's standard I/O streams [4][6].
Citations:
- 1: Piping
process.stdintochild.stdinleaves behind an open handle nodejs/node#32291 - 2: Process does not die when child process to which stdin is piped dies nodejs/node#24327
- 3: The process never ends when piping the stdin to a child process nodejs/node#2276
- 4: https://nodejs.org/api/child_process.html
- 5: child_process.spawn will either exit or not control stdin when detached and using inherit nodejs/node#5549
- 6: Spawn-ed detached unref-ed child process in Windows still prevents parent from exit nodejs/node#5614
- 7: https://www.thenodebook.com/process-os/standard-io
- 8: https://stackoverflow.com/questions/26004519/why-doesnt-my-node-js-process-terminate-once-all-listeners-have-been-removed
🌐 Web query:
Node.js child_process.stdout unref does not keep process alive with ignored stdin unpipe
💡 Result:
The behavior where calling subprocess.unref does not prevent a parent process from waiting for a child process often stems from the fact that the child process still holds open handles—such as IPC channels or stdio streams—that keep the parent's event loop active [1][2]. While subprocess.unref instructs the parent's event loop not to include the child process in its reference count, it only functions as intended if there are no other active handles keeping the process alive [1]. When you spawn a child process, its stdio streams (stdin, stdout, stderr) and IPC channels can act as event loop references [1][2][3]. If you are experiencing issues where the parent process remains alive: 1. IPC Channels: If an IPC channel is established (e.g., via fork), it may keep the event loop active [1]. Even if you call unref on the subprocess, the IPC channel itself might need to be unreferenced or disconnected [1][2]. 2. Stdio Streams: If the child process inherits stdio or shares streams (like pipes) with the parent, these handles can prevent the process from exiting [3]. Setting stdio to ignore (e.g., { stdio: 'ignore' }) is the standard way to ensure the child does not stay attached to the parent's streams [1][3]. 3. Other References: Any remaining open pipes, sockets, or active event listeners associated with the subprocess object can keep the parent process running [2]. To allow the parent to exit, you must ensure that all relevant handles associated with the child are either closed or properly unreferenced. If you require output from the child, you may need to explicitly handle the lifecycle of those streams or manually disconnect/close them when they are no longer needed [2]. In some cases, if the child is a Node.js instance, it may also hold references on its own side that require explicit cleanup [1][4].
Citations:
- 1: https://nodejs.org/api/child_process.html
- 2: https://stackoverflow.com/questions/72457656/nodejs-child-process-not-exiting-after-calling-unref
- 3: Spawn-ed detached unref-ed child process in Windows still prevents parent from exit nodejs/node#5614
- 4: https://r2.nodejs.org/docs/v8.14.0/api/child_process.html
Detach or ignore the WSL curl delivery child.
The fallback curl process is spawned with a referenced child and a piped stdin, so a delivery that waits for the --max-time 10 wallclock can delay Prime Agent shutdown. Make this one-shot delivery independent, and add coverage for the agent exit path.
Windows/WSL Electron QA evidenceValidated on the exact PR branch in the isolated
Typed WSL Prime session (v0.7.1)Managed extension, relay, working sessions, and preserved custom directorySession after daemon stop |



Summary
prime-agentlaunches, attach/resume flows, and Prime management commandsTesting
pnpm run typecheck:nodepnpm run check:max-lines-ratchetpnpm run check:code-quality:changed(Node 24)git diff --checkverifyScreenshots
Windows/WSL Electron QA with real Prime Agent 0.7.1: #13430 (comment)
AI Review Report
Same-model senior review rerun after rebasing onto current
origin/main: CLEAN.Security Audit
PRIME_AGENT_CODING_AGENT_DIRremains user-owned and is not redirectedauth.jsonwas not modifiedNotes
Real Windows/WSL validation confirmed explicit and typed launches, the managed extension, relay endpoint, real Prime working status, attach/management flows, and custom-directory sentinel preservation. The host’s available Anthropic OAuth credential was invalid/expired, so a successful model response and model-complete status were not claimed.