Skip to content

feat(agent-hooks): support Prime Agent status in WSL - #13430

Merged
brennanb2025 merged 1 commit into
mainfrom
brennanb2025/prime-agent-wsl-status-hooks
Aug 10, 2026
Merged

brennanb2025 merged 1 commit into
mainfrom
brennanb2025/prime-agent-wsl-status-hooks

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • bridge Orca’s host-managed, status-only Prime extension into WSL without changing the guest Prime config or session root
  • support explicit launches, typed prime-agent launches, attach/resume flows, and Prime management commands
  • keep relay coordination stable across guest restarts and use current Windows relay coordinates for fallback delivery

Testing

  • 9 focused test files (708 tests)
  • pnpm run typecheck:node
  • pnpm run check:max-lines-ratchet
  • pnpm run check:code-quality:changed (Node 24)
  • commit-time oxlint, React Doctor lint, formatting, and git diff --check
  • all PR CI checks green, including Windows packaging/native smoke, Git compatibility, wire compatibility, Node 18 managed hooks, Node 24/26 test shards, and aggregate verify

Screenshots

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

  • guest PRIME_AGENT_CODING_AGENT_DIR remains user-owned and is not redirected
  • only the host-managed status extension crosses into WSL
  • temporary QA auth material was removed; Prime auth.json was not modified

Notes

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.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The 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 curl.exe bridge code with endpoint fallback handling. Relay instance keys now use safe configured values or port-derived fallbacks. Tests cover wrapper behavior, status provisioning, WSL fallback delivery, environment propagation, and relay key selection.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: Prime Agent status support through WSL agent hooks.
Description check ✅ Passed The description covers the summary, testing, screenshots, review, security, and platform notes required by the template.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 934faae and a6311f2.

📒 Files selected for processing (18)
  • src/main/agent-hooks/wsl-hook-relay-manager.test.ts
  • src/main/agent-hooks/wsl-hook-relay-manager.ts
  • src/main/daemon/shell-ready.test.ts
  • src/main/daemon/shell-ready.ts
  • src/main/ipc/pty.test.ts
  • src/main/ipc/pty.ts
  • src/main/pi/agent-status-extension-source.test.ts
  • src/main/pi/agent-status-extension-source.ts
  • src/main/pi/agent-status-wsl-curl-source.ts
  • src/main/pi/titlebar-extension-service.test.ts
  • src/main/pi/titlebar-extension-service.ts
  • src/main/providers/local-pty-shell-ready.test.ts
  • src/main/providers/local-pty-shell-ready.ts
  • src/main/providers/windows-shell-args.test.ts
  • src/main/pty/prime-agent-shell-wrapper.test.ts
  • src/main/pty/prime-agent-shell-wrapper.ts
  • src/main/pty/wsl-orca-env.test.ts
  • src/main/pty/wsl-orca-env.ts

Comment on lines +55 to +74
' 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)',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 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:


🏁 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()
JS

Repository: 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' }
})
JS

Repository: 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 })
JS

Repository: 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:


🌐 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:


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.

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Windows/WSL Electron QA evidence

Validated on the exact PR branch in the isolated prime-agent-wsl-qa-13430-2 Electron instance, using real Prime Agent 0.7.1 in Ubuntu WSL.

  • Explicit Orca launch and typed prime-agent launch both reached a genuine wsl.exe PTY.
  • ORCA_PRIME_AGENT_STATUS_EXTENSION and the WSL relay endpoint were present.
  • Orca tracked real Prime sessions as working; attach/status/list/doctor/stop flows worked.
  • A custom PRIME_AGENT_CODING_AGENT_DIR and sentinel remained unchanged.
  • Host limitation: the available Anthropic OAuth credential was invalid/expired, so model-response completion was not claimed. No fallback credential was used and Prime auth.json was not modified.

Typed WSL Prime session (v0.7.1)

Typed WSL Prime session

Managed extension, relay, working sessions, and preserved custom directory

Managed extension and sentinel

Session after daemon stop

Stopped Prime session

@stablyai stablyai deleted a comment from OrcaWin Aug 10, 2026
@stablyai stablyai deleted a comment from OrcaWin Aug 10, 2026
@brennanb2025
brennanb2025 merged commit d6362de into main Aug 10, 2026
51 checks passed
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant