Skip to content

fix(#487): bound bridge/guard/delegate subprocesses with process-group timeouts, across every harness - #529

Merged
richard-devbot merged 3 commits into
mainfrom
fix-487-bridge-subprocess-timeouts
Aug 1, 2026
Merged

fix(#487): bound bridge/guard/delegate subprocesses with process-group timeouts, across every harness#529
richard-devbot merged 3 commits into
mainfrom
fix-487-bridge-subprocess-timeouts

Conversation

@richard-devbot

@richard-devbot richard-devbot commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

Issue #487 named only "Tau + JS bridge," but per Richardson's explicit direction, this fixes the same bug class across every harness adapter that shells out to a subprocess: Tau, Operator, Hermes (all Python), the JS bridgeInvoker (the #486 durable worker's engine), and Pi's runDelegateAgent. Claude Code has no subprocess adapter code and was not in scope.

Core discovery (live-reproduced before being fixed): a bare single-pid kill — asyncio proc.kill(), Python subprocess.run(timeout=...)'s own built-in handling, or Node child.kill() — is insufficient when the spawned command forks a grandchild that inherits its stdout/stderr pipes (a real npx/wrapper-script behavior). The grandchild survives the kill; on the asyncio side this blocked proc.wait()/communicate() for the FULL original hang duration regardless of the configured timeout (reproduced: a 1s timeout took 100s wall-clock against a real forking wrapper). Fixed by giving every spawn its own process group and killing the WHOLE group on timeout/abort (POSIX; Windows keeps the pre-existing single-pid fallback — not a regression, since Windows process-tree killing was never handled here before).

Notable findings surfaced by this audit:

What changed

  • Tau (_run_bridge, _run_gate, _run_guard, _emit_observation, _fetch_context) — all 5 subprocess call sites.
  • Operator (_run_bridge, _run_guard, _after_tool_call) — all 3 call sites, including one the original [P2][operator] Confirm operator_use hook API; wire what's possible; make the tier honest #391 fix missed.
  • Hermes (_run_bridge, _run_guard, _observe_hook) — all 3 call sites, replacing bare subprocess.run.
  • JS bridgeInvoker (src/commands/pipeline-run.js) — adds timeout + process-group kill, an optional AbortSignal 3rd parameter, and a child.on('error', ...) handler (previously entirely absent — a spawn failure like ENOENT could hang the promise forever).
  • Pi runDelegateAgent (src/integrations/pi/rstack-sdlc.ts) — a generous 30-minute backstop timeout, since a bridge-invoked sdlc_delegate call from any of Tau/Hermes/Operator gets signal === undefined today (confirmed via bin/rstack-bridge.ts).

Env-tunable: RSTACK_BRIDGE_TIMEOUT_MS (60s), RSTACK_GUARD_TIMEOUT_MS (15-30s depending on adapter), RSTACK_DELEGATE_TIMEOUT_MS (30min).

Verification

  • Every Python fix live-verified against the REAL installed package (tau-coding-agent, operator-use, hermes-agent, via pip into throwaway venvs) with a real forking hung wrapper standing in for a stalled npx — not just mocked.
  • Every fix (Tau's 5 sites, Operator's 3, Hermes' 3, JS bridgeInvoker) mutation-tested: reverting the process-group kill was confirmed to reproduce a real orphaned process via pgrep.
  • New tests/bridge-invoker-timeout-487.test.js — real node --test coverage for the JS side, also mutation-tested.
  • Full local gate green: 1892/1892 tests, typecheck 0 errors, lint 0 errors, security-audit clean, validate clean.

Deferred (documented in docs/HARNESS.md)

Test plan

  • npm test — 1892/1892 pass
  • npm run typecheck — 0 errors
  • npm run lint — 0 errors
  • node scripts/security-audit.mjs — clean
  • npm run validate — clean
  • Live-verified against real tau-coding-agent, operator-use, hermes-agent packages
  • Mutation-tested every fix (Python + JS)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved timeout and cancellation handling for subprocesses and delegated tasks.
    • Prevented orphaned child processes and pipe-related delays by terminating full process trees when supported.
    • Added clearer reporting for timeouts, cancellations, startup failures, and nonzero exits.
    • Ensured cleanup occurs after processes complete, fail, time out, or are cancelled.
  • Documentation

    • Documented configurable timeout behavior and process cleanup improvements.
  • Tests

    • Added coverage for timeouts, cancellation, process-tree termination, startup failures, and error messages.

…p timeouts, across every harness

The issue named only Tau + the JS bridge; verified and fixed the same bug
class across every subprocess-shelling adapter instead (Tau, Operator,
Hermes, JS bridgeInvoker, Pi's runDelegateAgent — Claude Code has no
subprocess adapter and was not in scope).

Core discovery, live-reproduced before being fixed: a bare single-pid kill
(asyncio proc.kill(), Python subprocess.run's own TimeoutExpired handling,
Node child.kill()) is insufficient when the spawned command forks a
grandchild that inherits its stdout/stderr pipes (a real npx/wrapper
behavior) — the grandchild survives the kill and, on the asyncio side,
blocks proc.wait()/communicate() for the full original hang duration
regardless of the configured timeout. Fixed by giving every spawn its own
process group and killing the whole group on timeout (POSIX; Windows keeps
the pre-existing single-pid fallback, not a regression).

Notable: Operator's own previously-shipped (#391) kill-and-reap helper had
this exact bug. An earlier pass had also cleared Hermes as "already safe"
via subprocess.run(timeout=...) — live-testing proved that wrong too;
subprocess.run's built-in timeout handling has the same limitation.

Every Python fix live-verified against the real installed package
(tau-coding-agent, operator-use, hermes-agent) with a real forking hung
wrapper, and mutation-tested (reverting the fix reproduces a real orphaned
process via pgrep). JS-side fix has real node --test coverage
(tests/bridge-invoker-timeout-487.test.js), also mutation-tested.

Env-tunable timeouts: RSTACK_BRIDGE_TIMEOUT_MS (60s), RSTACK_GUARD_TIMEOUT_MS
(15-30s), RSTACK_DELEGATE_TIMEOUT_MS (30min backstop for Pi's delegate spawn,
closing the gap where a bridge-invoked sdlc_delegate call from any harness
gets no AbortSignal today). bridgeInvoker also gains an optional AbortSignal
parameter and a child.on('error', ...) handler (previously entirely absent).

Deferred (documented in docs/HARNESS.md): true cross-process AbortSignal
propagation through the bridge protocol; Windows process-tree killing beyond
the existing single-pid fallback; automated CI-integrated Python tests (none
existed before this fix either — Python-side verification remains live/manual
per this repo's established precedent).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@strix-security

strix-security Bot commented Aug 1, 2026

Copy link
Copy Markdown

Strix is installed on this repository, but we couldn't run this PR security review because this workspace's trial has ended. Add a card to resume code reviews here.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@richard-devbot, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 39 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 75f00c83-0f6c-4bac-b0ab-c1912d06feb2

📥 Commits

Reviewing files that changed from the base of the PR and between 6b70e56 and 275f4c0.

📒 Files selected for processing (6)
  • src/commands/pipeline-run.js
  • src/core/harness/process-tree.js
  • src/integrations/hermes/rstack_sdlc.py
  • src/integrations/operator/rstack_sdlc.py
  • src/integrations/pi/rstack-sdlc.ts
  • src/integrations/tau/rstack_sdlc.py
📝 Walkthrough

Walkthrough

The change adds bounded subprocess execution and process-tree cleanup across bridge invocation, Tau, Hermes, Operator, and Pi. It adds abort handling, timeout configuration, cleanup tests, and documentation.

Changes

Subprocess lifecycle controls

Layer / File(s) Summary
Bridge invocation cancellation and timeout
src/commands/pipeline-run.js, tests/bridge-invoker-timeout-487.test.js
bridgeInvoker supports configurable timeouts, abort signals, detached subprocesses, process-tree termination, startup-error handling, and cleanup validation.
Harness adapter timeout enforcement
src/integrations/tau/rstack_sdlc.py, src/integrations/hermes/rstack_sdlc.py, src/integrations/operator/rstack_sdlc.py
Adapter bridge, guard, quality-gate, observability, and context commands use process-group-aware timeout handling. Timed-out processes are terminated and reaped.
Pi delegate timeout and abort states
src/integrations/pi/rstack-sdlc.ts
Delegates use a 30-minute default timeout, process-tree termination, abort-listener cleanup, and distinct timeout, caller-abort, and nonzero-exit results.
Timeout behavior documentation
docs/HARNESS.md
The harness documentation records timeout coverage, configuration, process-group cleanup, tests, and deferred behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant bridgeInvoker
  participant BridgeProcess
  participant ProcessGroup
  Caller->>bridgeInvoker: invoke with timeout or AbortSignal
  bridgeInvoker->>BridgeProcess: spawn detached process
  alt timeout or abort
    bridgeInvoker->>ProcessGroup: terminate process tree
    ProcessGroup-->>bridgeInvoker: processes reaped
    bridgeInvoker-->>Caller: timeout or cancellation error
  else process exits
    BridgeProcess-->>bridgeInvoker: exit result
    bridgeInvoker-->>Caller: invocation result
  end
Loading

Possibly related issues

  • Issue 487: The change implements timeout, process-group termination, reaping, and abort handling across the requested harness paths.

Possibly related PRs

Suggested labels: bug, harness, harness-integration, testing

Suggested reviewers: richardsongunde

Poem

I’m a rabbit who watches the process tree,
No orphaned child escapes from me.
Timers tick, aborts are clear,
Groups are reaped when stalls appear.
Tests hop neatly, docs glow bright—
The harness sleeps safely tonight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the PR's main change: bounded subprocesses with process-group timeouts across the in-scope harnesses.
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.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-487-bridge-subprocess-timeouts

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.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fix subprocess hangs via process-group timeouts across all harness adapters

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add hard timeouts for bridge/guard/delegate subprocess invocations across harness adapters.
• Spawn subprocesses in new process groups; kill entire groups on timeout/abort.
• Document #487 root cause and add node tests for bridgeInvoker hang prevention.
Diagram

graph TD
  A["Harness callers"] --> B["JS bridgeInvoker"] --> C["Spawn bridge subprocess"] --> D{{"Timeout / Abort"}} --> E["Kill process group"] --> F[("Subprocess tree")]
  G["Python adapters (Tau/Operator/Hermes)"] --> C
  H["Pi delegate runner"] --> C
  subgraph Legend
    direction LR
    _cmp["Component"] ~~~ _dec{{"Decision"}} ~~~ _proc[("Process tree")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Centralize subprocess spawning in a shared cross-adapter utility
  • ➕ Eliminates duplicated timeout/kill logic across Tau/Operator/Hermes/JS/Pi
  • ➕ Reduces risk of future call sites missing the pattern (as happened previously)
  • ➖ Requires refactor across multiple packages/languages and public surfaces
  • ➖ Harder to land quickly as a focused bug fix
2. Adopt cross-platform process-tree killing libraries
  • ➕ Could provide Windows process-tree termination (parity with POSIX behavior)
  • ➕ Less bespoke OS branching code
  • ➖ Adds dependencies (e.g., psutil/tree-kill equivalents) and operational risk
  • ➖ Still requires careful integration with stdio pipe ownership and reaping
3. Propagate AbortSignal/cancellation through the bridge protocol
  • ➕ Enables cooperative cancellation of long-running delegates beyond hard timeouts
  • ➕ Makes cancellation semantics consistent end-to-end
  • ➖ Bigger contract change across bridge caller/callee; more coordination and testing
  • ➖ Not necessary to fix the orphaned-grandchild hang class

Recommendation: The PR’s approach (process-group ownership + group kill on timeout/abort, with Windows unchanged) is the best scoped fix for #487 because it directly addresses the reproduced failure mode (forked descendants inheriting stdio). Consider a follow-up to centralize the pattern or add Windows tree-kill support, but those are appropriately deferred to keep this change focused and verifiable.

Files changed (7) +483 / -24

Bug fix (5) +311 / -24
pipeline-run.jsAdd bridgeInvoker timeout, AbortSignal support, and process-group kill +56/-1

Add bridgeInvoker timeout, AbortSignal support, and process-group kill

• Introduces a hard timeout (RSTACK_BRIDGE_TIMEOUT_MS) for bridge invocations and ensures POSIX subprocesses run detached so the entire process group can be SIGKILLed. Adds AbortSignal handling and an 'error' event handler so spawn failures reject instead of leaving promises pending.

src/commands/pipeline-run.js

rstack_sdlc.pyReplace subprocess.run timeout with explicit process-group kill on timeout +54/-6

Replace subprocess.run timeout with explicit process-group kill on timeout

• Adds a Popen-based helper that starts a new process session on POSIX and kills the process group on timeout before re-raising TimeoutExpired. Converts bridge, guard, and observe subprocess call sites to use the helper, fixing orphaned descendants that can keep pipes open.

src/integrations/hermes/rstack_sdlc.py

rstack_sdlc.pyFix Operator asyncio subprocess timeout path to kill/reap full process group +42/-2

Fix Operator asyncio subprocess timeout path to kill/reap full process group

• Ensures subprocesses are started in their own process group (POSIX) and updates the timeout handler to kill the process group rather than only the tracked pid. Also routes an observe hook call site through the existing kill-and-reap helper so timeouts don’t get swallowed without cleanup.

src/integrations/operator/rstack_sdlc.py

rstack-sdlc.tsAdd delegate backstop timeout and process-group kill for spawned agents +43/-6

Add delegate backstop timeout and process-group kill for spawned agents

• Adds an env-tunable long backstop timeout (RSTACK_DELEGATE_TIMEOUT_MS) for delegated agent subprocesses and spawns them detached on POSIX so the whole process group can be killed. Differentiates 'timed_out' vs 'aborted' vs 'failed' lifecycle statuses to reflect termination cause.

src/integrations/pi/rstack-sdlc.ts

rstack_sdlc.pyBound Tau bridge/guard/gate/observe/context subprocesses with group kill + reap +116/-9

Bound Tau bridge/guard/gate/observe/context subprocesses with group kill + reap

• Introduces shared timeout helpers (bridge/gate) and a unified communicate-or-kill path that kills the POSIX process group and reaps the process on timeout. Applies the pattern across all Tau subprocess call sites, including previously unbounded communicate() and kill-without-reap branches.

src/integrations/tau/rstack_sdlc.py

Tests (1) +112 / -0
bridge-invoker-timeout-487.test.jsTest bridgeInvoker timeout, tree-kill behavior, spawn failure, and AbortSignal +112/-0

Test bridgeInvoker timeout, tree-kill behavior, spawn failure, and AbortSignal

• Adds node --test coverage proving a hung/forking 'npx' wrapper is bounded by RSTACK_BRIDGE_TIMEOUT_MS and that no orphaned grandchild remains after termination. Also verifies spawn failures reject promptly and that an externally supplied AbortSignal cancels the invocation.

tests/bridge-invoker-timeout-487.test.js

Documentation (1) +60 / -0
HARNESS.mdDocument #487 subprocess timeout/process-group strategy across harnesses +60/-0

Document #487 subprocess timeout/process-group strategy across harnesses

• Adds a detailed incident write-up for #487, explaining why single-pid kills are insufficient when subprocesses fork and inherit stdio. Documents the unified fix pattern (own process group + kill group on timeout/abort), affected call sites, env-tunable timeouts, and verification approach.

docs/HARNESS.md

@qodo-code-review

qodo-code-review Bot commented Aug 1, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (1)

Context used
✅ Compliance rules (platform): 300 rules
✅ Skills: 17 invoked
  code-review-pr
  claude-api
  documentation-writing
  pptx
  docx
  performance-monitoring
  security-compliance
  cso
  plan-eng-review
  design-review
  prompt-engineering
  mcp-builder
  qa-testing
  code-patterns
  xlsx
  security-owasp
  testing-qa

Grey Divider


Action required

1. Killpg getpgid race ✓ Resolved 🐞 Bug ☼ Reliability
Description
Tau/Operator/Hermes kill helpers use os.killpg(os.getpgid(proc.pid), SIGKILL); if the tracked PID
has already exited while forked descendants still exist, getpgid can raise ProcessLookupError
and the code skips killing the still-running group, leaving orphans despite the timeout.
Code

src/integrations/tau/rstack_sdlc.py[R408-414]

+    try:
+        if os.name == "nt":
+            proc.kill()
+        else:
+            os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
+    except ProcessLookupError:
+        pass  # already exited between the timeout firing and this kill
Relevance

●● Moderate

No prior accepted/rejected guidance on getpgid/killpg ProcessLookupError race handling; pattern
appears unchallenged historically.

PR-#396
PR-#398

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
All three adapters start subprocesses with start_new_session=True on POSIX and then attempt to
derive the pgid later with os.getpgid(proc.pid); if that lookup fails, they skip killing the
process group entirely.

src/integrations/tau/rstack_sdlc.py[394-415]
src/integrations/operator/rstack_sdlc.py[277-305]
src/integrations/hermes/rstack_sdlc.py[342-386]
PR-#398

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Timeout cleanup uses `os.getpgid(proc.pid)` to compute the group id at cleanup time. If the direct child process exits before cleanup runs (but its forked descendants still exist and keep pipes open), `getpgid` fails and the code skips killing the process group, reintroducing orphaned descendants.

### Issue Context
All three Python adapters create new process groups via `start_new_session=True` on POSIX, which makes the process group id equal to the spawned child’s PID at creation time. Cleanup should not depend on looking up pgid through a possibly-dead PID.

### Fix Focus Areas
- src/integrations/tau/rstack_sdlc.py[394-445]
- src/integrations/operator/rstack_sdlc.py[277-315]
- src/integrations/hermes/rstack_sdlc.py[342-387]

### Suggested change
- On POSIX, use the known pgid directly (`proc.pid` when `start_new_session=True`) i.e. `os.killpg(proc.pid, signal.SIGKILL)`.
- Alternatively, capture `pgid = os.getpgid(proc.pid)` immediately after spawn (while it must exist) and store/use that value in the timeout handler.
- Keep `ProcessLookupError` handling, but only around `killpg` (group may already be gone), not around `getpgid` lookup that can mask a still-live group.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. delegateTimeoutMs() uses 30*60_000 ✓ Resolved 📜 Skill insight ⚙ Maintainability
Description
The new timeout logic uses hardcoded numeric literals (30 * 60_000 for the delegate timeout and
60_000 for the bridge timeout) instead of named constants. This violates the magic-number rule,
reduces readability, and makes the defaults harder to discover and reuse consistently.
Code

src/integrations/pi/rstack-sdlc.ts[R1612-1615]

+function delegateTimeoutMs(): number {
+  const parsed = Number(process.env.RSTACK_DELEGATE_TIMEOUT_MS);
+  return Number.isFinite(parsed) && parsed > 0 ? parsed : 30 * 60_000;
+}
Relevance

●●● Strong

Magic-number refactors (including timeouts/durations) have been accepted repeatedly; team values
named constants.

PR-#508
PR-#511
PR-#490

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1400588 requires replacing numeric literals used for timeouts/configuration with
named constants; the cited implementations show defaults being returned directly as numeric
expressions. Specifically, delegateTimeoutMs() returns a hardcoded 30 * 60_000 default in
src/integrations/pi/rstack-sdlc.ts, and bridgeTimeoutMs() returns a hardcoded 60_000 default
in src/commands/pipeline-run.js, demonstrating the presence of magic numbers that should be
replaced by named constants.

src/integrations/pi/rstack-sdlc.ts[1612-1615]
src/commands/pipeline-run.js[194-197]
Skill: code-patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Timeout defaults are currently expressed as magic-number literals (`30 * 60_000` for delegate timeout and `60_000` for bridge timeout) rather than named constants, which reduces readability and makes the values harder to discover and reuse consistently.

## Issue Context
PR Compliance ID 1400588 requires numeric literals used for timeouts/configuration to be replaced with named constants.

## Fix Focus Areas
- src/integrations/pi/rstack-sdlc.ts[1604-1615]
- src/commands/pipeline-run.js[190-197]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. _run_gate uses print 📜 Skill insight ✧ Quality
Description
A new print(...) statement was added, which is considered debug-style logging in source code and
can create noisy/unstructured output. Replace it with the project's logging framework (e.g.,
logging.warning) or remove it if not required.
Code

src/integrations/tau/rstack_sdlc.py[673]

+        print(f"[rstack] gate '{gate_name}' timed out after {_gate_timeout_s():.0f}s (RSTACK_GATE_TIMEOUT_MS) — allowing (gates fail open, matching the no-npx behavior)", file=sys.stderr)
Relevance

●● Moderate

No historical evidence found for rejecting print() usage in Tau adapter code versus logging
framework.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1400056 disallows debug logging statements such as print(). The changed code adds
a print(...) call in the _run_gate timeout path.

src/integrations/tau/rstack_sdlc.py[670-674]
Skill: testing-qa

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New debug-style logging was added via `print(...)`, which is disallowed by the compliance rule.

## Issue Context
The message is emitted on gate timeout; it should be handled via structured logging (e.g., Python `logging`) rather than `print`.

## Fix Focus Areas
- src/integrations/tau/rstack_sdlc.py[670-674]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Abort listener not removed ✓ Resolved 🐞 Bug ➹ Performance
Description
In bridgeInvoker, the timeout path sets settled = true and rejects without removing the
AbortSignal listener; later exit/error handlers return early because settled is already true,
so the listener remains attached and can accumulate on long-lived signals.
Code

src/commands/pipeline-run.js[R229-259]

+    const timer = setTimeout(() => {
+      if (settled) return;
+      settled = true;
+      killTree(child);
+      rejectPromise(new Error(`${toolName} timed out after ${Math.round(timeoutMs / 1000)}s (RSTACK_BRIDGE_TIMEOUT_MS) — the bridge process was killed. This is a timeout, not a tool failure.`));
+    }, timeoutMs);
+    const onAbort = () => {
+      if (settled) return;
+      settled = true;
+      clearTimeout(timer);
+      killTree(child);
+      rejectPromise(new Error(`${toolName} aborted`));
+    };
+    if (signal) {
+      if (signal.aborted) onAbort();
+      else signal.addEventListener('abort', onAbort, { once: true });
+    }
    child.stdout.on('data', (chunk) => { stdout += chunk; });
    child.stderr.on('data', (chunk) => { stderr += chunk; });
+    child.on('error', (err) => {
+      if (settled) return;
+      settled = true;
+      clearTimeout(timer);
+      if (signal) signal.removeEventListener('abort', onAbort);
+      rejectPromise(new Error(`${toolName} failed to start: ${err.message}`));
+    });
    child.on('exit', (code) => {
+      if (settled) return;
+      settled = true;
+      clearTimeout(timer);
+      if (signal) signal.removeEventListener('abort', onAbort);
Relevance

●● Moderate

No historical evidence for AbortSignal listener cleanup on timeout; similar timeout/spawn hygiene
fixes accepted elsewhere.

PR-#398
PR-#518

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The abort listener is added for signal, but it is only removed in the error and exit handlers;
when the timeout fires it marks settled and rejects, causing the later exit handler to return
before removing the listener.

src/commands/pipeline-run.js[218-263]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
When a bridge invocation times out, `bridgeInvoker` rejects but does not remove the AbortSignal listener. Because the later `exit` handler short-circuits on `settled`, it never performs the listener cleanup either.

### Issue Context
This is primarily a resource-retention issue: a long-lived `AbortSignal` (or reused controller) can retain closures/ChildProcess references and accumulate listeners across repeated timeouts.

### Fix Focus Areas
- src/commands/pipeline-run.js[218-262]

### Suggested change
- In the timeout callback, call `if (signal) signal.removeEventListener('abort', onAbort);` before rejecting.
- Alternatively refactor to a single `settle(kind, err?)` helper that always clears the timer and removes the abort listener before resolving/rejecting.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
5. Abort listener outlives child ✓ Resolved 🐞 Bug ☼ Reliability
Description
Pi’s runDelegateAgent installs an AbortSignal listener that is never removed on process
completion; if the signal aborts later, the handler can still call process.kill(-pid, 'SIGKILL')
using a stale PID, which is rare but can hit a reused PID/process group.
Code

src/integrations/pi/rstack-sdlc.ts[R1752-1755]

+      const abort = () => killDelegateTree(proc);
      if (signal.aborted) abort();
      else signal.addEventListener("abort", abort, { once: true });
    }
Relevance

●● Moderate

No direct precedent for removing abort listeners on process completion; subprocess cleanup feedback
is mixed/unclear.

PR-#396
PR-#398

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new kill method uses negative-pid SIGKILL on POSIX, and the abort handler is attached with no
corresponding removal on completion; this guarantees the listener can outlive the child process.

src/integrations/pi/rstack-sdlc.ts[1617-1622]
src/integrations/pi/rstack-sdlc.ts[1749-1762]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`runDelegateAgent` adds an AbortSignal listener that closes over `proc` and is not removed on `close`/`error`. After the child exits, a later abort can still invoke `killDelegateTree(proc)`.

This is always a listener lifecycle leak; and in the rare PID-reuse case, the stale `proc.pid` (used with negative-pid `SIGKILL` on POSIX) could signal an unrelated process group.

### Issue Context
The PR changed abort handling from a single-process signal to a process-group kill on POSIX (`process.kill(-proc.pid, 'SIGKILL')`), increasing blast radius if a stale PID is ever used.

### Fix Focus Areas
- src/integrations/pi/rstack-sdlc.ts[1617-1622]
- src/integrations/pi/rstack-sdlc.ts[1731-1762]

### Suggested change
- Track completion with a `settled` boolean (or check `proc.exitCode !== null`).
- Register the abort handler, and remove it on `proc.on('close')` and `proc.on('error')`.
- Optionally guard inside the abort handler: if already settled, return without signaling.
- Keep `{ once: true }`, but still explicitly remove on completion to avoid leaks when the signal never aborts.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

6. Tests omit #487 in names ✓ Resolved 📜 Skill insight ⚙ Maintainability
Description
The new regression tests for issue #487 do not include the bug ID in the test(...) description
strings (they only reference it in comments). This weakens traceability of what bug the test is
meant to prevent.
Code

tests/bridge-invoker-timeout-487.test.js[R37-90]

+test('bridgeInvoker times out a hung bridge process and kills the whole tree (no orphans)', async (t) => {
+  const fakeBinDir = mkdtempSync(path.join(os.tmpdir(), 'rstack-bridge-fakebin-'));
+  makeFakeHungNpx(fakeBinDir);
+  const projectRoot = mkdtempSync(path.join(os.tmpdir(), 'rstack-bridge-project-'));
+  t.after(() => {
+    rmSync(fakeBinDir, { recursive: true, force: true });
+    rmSync(projectRoot, { recursive: true, force: true });
+  });
+
+  const originalPath = process.env.PATH;
+  const originalTimeout = process.env.RSTACK_BRIDGE_TIMEOUT_MS;
+  process.env.PATH = `${fakeBinDir}${path.delimiter}${originalPath}`;
+  process.env.RSTACK_BRIDGE_TIMEOUT_MS = '1000';
+  t.after(() => {
+    process.env.PATH = originalPath;
+    if (originalTimeout === undefined) delete process.env.RSTACK_BRIDGE_TIMEOUT_MS;
+    else process.env.RSTACK_BRIDGE_TIMEOUT_MS = originalTimeout;
+  });
+
+  const invoke = bridgeInvoker(projectRoot);
+  const start = Date.now();
+  await assert.rejects(
+    () => invoke('sdlc_status', {}),
+    /timed out after 1s/,
+    'a hung bridge process rejects with a timeout error, not an indefinite hang',
+  );
+  const elapsedMs = Date.now() - start;
+  assert.ok(elapsedMs < 5000, `timeout fired promptly (took ${elapsedMs}ms, expected ~1000ms)`);
+
+  // Give the killed process group a moment to actually exit, then confirm
+  // no orphaned descendant survives the kill (the #487 grandchild bug).
+  await new Promise((resolve) => setTimeout(resolve, 500));
+  assert.equal(anyOrphans('sleep 100'), '', 'no orphaned grandchild process survives the timeout kill');
+});
+
+test('bridgeInvoker rejects (does not hang) on a spawn failure', async (t) => {
+  const projectRoot = mkdtempSync(path.join(os.tmpdir(), 'rstack-bridge-project-'));
+  t.after(() => rmSync(projectRoot, { recursive: true, force: true }));
+
+  const originalPath = process.env.PATH;
+  process.env.PATH = ''; // npx cannot be resolved -> ENOENT on spawn
+  t.after(() => { process.env.PATH = originalPath; });
+
+  const invoke = bridgeInvoker(projectRoot);
+  await assert.rejects(
+    () => invoke('sdlc_status', {}),
+    /failed to start/,
+    'a spawn failure rejects with a named error instead of hanging the promise forever',
+  );
+});
+
+test('bridgeInvoker honors an externally-supplied AbortSignal', async (t) => {
+  const fakeBinDir = mkdtempSync(path.join(os.tmpdir(), 'rstack-bridge-fakebin-'));
+  makeFakeHungNpx(fakeBinDir);
Relevance

●●● Strong

Team accepted adding bug IDs into test description strings for traceability (e.g., #495 additions).

PR-#504

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1400781 requires regression tests to include the bug identifier in the test
description; the added test(...) descriptions do not include #487 even though the file comments
clearly indicate the tests are for #487.

tests/bridge-invoker-timeout-487.test.js[37-90]
Skill: testing-qa

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The regression tests added for issue `#487` do not include the bug ID in the `test(...)` description strings, reducing traceability.

## Issue Context
Compliance requires regression tests to reference the bug ID they prevent in the test description.

## Fix Focus Areas
- tests/bridge-invoker-timeout-487.test.js[37-112]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. killDelegateTree uses any ⊘ Outdated 📜 Skill insight ✧ Quality
Description
The new killDelegateTree(proc: any) introduces an any type, which weakens type safety and can
hide incorrect process object usage at compile time. Use an explicit Node child-process type instead
of any.
Code

src/integrations/pi/rstack-sdlc.ts[R1617-1622]

+function killDelegateTree(proc: any): void {
+  try {
+    if (process.platform === "win32" || !proc.pid) proc.kill("SIGKILL");
+    else process.kill(-proc.pid, "SIGKILL");
+  } catch { /* already exited between the timeout firing and this kill */ }
+}
Relevance

● Weak

Multiple prior PRs rejected removing TypeScript any; repo tolerates any in Pi integration code.

PR-#503
PR-#490
PR-#506

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1399957 forbids using any in TypeScript. The new function explicitly uses `proc:
any in killDelegateTree, introducing an any` in changed code.

src/integrations/pi/rstack-sdlc.ts[1617-1622]
Skill: code-patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`killDelegateTree` declares `proc` as `any`, violating the TypeScript rule against `any` and reducing compile-time guarantees.

## Issue Context
This code handles killing a spawned delegate subprocess and should be typed as a Node.js child process (or a minimal explicit interface) to ensure `.pid` and `.kill()` usage is correct.

## Fix Focus Areas
- src/integrations/pi/rstack-sdlc.ts[1617-1622]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

try:
out, err = await _communicate_or_kill(proc, payload, _gate_timeout_s())
except asyncio.TimeoutError:
print(f"[rstack] gate '{gate_name}' timed out after {_gate_timeout_s():.0f}s (RSTACK_GATE_TIMEOUT_MS) — allowing (gates fail open, matching the no-npx behavior)", file=sys.stderr)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. _run_gate uses print 📜 Skill insight ✧ Quality

A new print(...) statement was added, which is considered debug-style logging in source code and
can create noisy/unstructured output. Replace it with the project's logging framework (e.g.,
logging.warning) or remove it if not required.
Agent Prompt
## Issue description
New debug-style logging was added via `print(...)`, which is disallowed by the compliance rule.

## Issue Context
The message is emitted on gate timeout; it should be handled via structured logging (e.g., Python `logging`) rather than `print`.

## Fix Focus Areas
- src/integrations/tau/rstack_sdlc.py[670-674]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread tests/bridge-invoker-timeout-487.test.js Outdated
Comment thread src/integrations/pi/rstack-sdlc.ts
Comment thread src/integrations/tau/rstack_sdlc.py
Comment thread src/integrations/pi/rstack-sdlc.ts Outdated
Comment thread src/commands/pipeline-run.js
…r leaks

Qodo caught 3 real issues in the first cut, all fixed:

1. killpg/getpgid race (Tau/Operator/Hermes): the kill helpers looked up
   the process group via os.getpgid(proc.pid) at kill time. If the tracked
   pid had ALREADY exited while its forked descendants kept the group
   alive, getpgid raises ProcessLookupError and the code skipped killing
   the still-live group — reintroducing the exact orphan the fix exists to
   prevent. Not needed: start_new_session=True makes the process its own
   group leader at spawn time, so its pgid is always simply proc.pid — no
   lookup, no race. Re-verified live against all 5/3/3 Tau/Operator/Hermes
   call sites after the fix — still zero orphans.

2. Abort listener outlives the child (Pi's runDelegateAgent): the
   AbortSignal listener was never removed on normal completion, so a
   caller aborting after the process had already exited could still fire
   process.kill(-pid, ...) against a pid that — in the rare reuse case —
   might belong to an unrelated process by then. Removed on both close and
   error.

3. Abort listener not removed on timeout (JS bridgeInvoker): the timeout
   branch set settled=true and rejected without removing the abort
   listener; the later exit/error handlers short-circuited on settled and
   never ran their own cleanup either, so the listener stayed attached for
   the signal's lifetime. Fixed.

Also addressed two minor nits: test names now include #487 for
traceability, and the two new timeout defaults (bridge 60s, delegate
30min) are now named constants instead of inline literals. Declined: the
new stderr diagnostic in Tau's _run_gate uses print() to match this file's
own existing convention (already used elsewhere in the same file) — not a
regression, no logging framework exists anywhere in this adapter.

Full local gate re-verified green (1892/1892 tests, typecheck/lint/
security/validate all clean) and all three Python adapters re-verified
live against the real installed packages after this change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@richard-devbot

Copy link
Copy Markdown
Owner Author

Thanks for the review — addressed the real findings:

  1. killpg/getpgid race (Tau/Operator/Hermes) — fixed, using proc.pid directly as the pgid instead of a racy os.getpgid() lookup at kill time. Re-verified live against all 11 call sites, still zero orphans.
  2. Abort listener outlives the child (Pi's runDelegateAgent) — fixed, removed on both close/error.
  3. Abort listener not removed on timeout (JS bridgeInvoker) — fixed.

Also applied the two minor nits (test names now include #487, timeout defaults are now named constants).

Declined: the new print() diagnostic in Tau's _run_gate matches this file's own pre-existing convention (already used elsewhere in the same file, no logging framework exists in this adapter) — not a regression.

Full local gate re-verified green after all fixes.

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/integrations/tau/rstack_sdlc.py (1)

663-684: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Catch spawn failures in _run_gate to keep the documented fail-open contract.

_run_gate creates the subprocess at Line 667 outside any exception handler that catches OSError. If asyncio.create_subprocess_exec raises OSError (for example, a permission error or a resource limit hit between the shutil.which check and exec), the exception propagates out of _run_gate and out of _GuardedBuiltinTool.execute() instead of the documented "gates fail open" behavior (see the docstring at Line 661: "Fails OPEN when npx is unreachable"). _run_guard in this same file (Lines 617-625) already catches OSError around its equivalent subprocess creation and fails gracefully. Apply the same pattern here.

🛡️ Proposed fix to fail open on spawn errors
-    proc = await asyncio.create_subprocess_exec(
-        npx, "--yes", "rstack-agents", "gate", gate_name, "--project", cwd,
-        stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
-        env=os.environ, cwd=cwd,
-        **_own_process_group_kwargs(),
-    )
+    try:
+        proc = await asyncio.create_subprocess_exec(
+            npx, "--yes", "rstack-agents", "gate", gate_name, "--project", cwd,
+            stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
+            env=os.environ, cwd=cwd,
+            **_own_process_group_kwargs(),
+        )
+    except OSError as exc:
+        print(f"[rstack] gate '{gate_name}' could not spawn ({exc}) — allowing (gates fail open)", file=sys.stderr)
+        return True, ""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/integrations/tau/rstack_sdlc.py` around lines 663 - 684, Update
`_run_gate` to wrap `asyncio.create_subprocess_exec` in an `OSError` handler,
matching the existing `_run_guard` pattern. When subprocess creation fails,
return the documented fail-open result `(True, "")` instead of propagating the
exception; preserve the existing timeout handling and successful subprocess
flow.
🧹 Nitpick comments (1)
src/commands/pipeline-run.js (1)

262-269: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Use close instead of exit to avoid resolving with a truncated stdout.

The completion handler settles on 'exit'. Node.js docs state the 'exit' event can fire before the child's stdio streams finish delivering buffered data; 'close' is the event guaranteed to fire only after stdio streams are fully closed. Since this promise resolves with the accumulated stdout buffer, a race between 'exit' and the final 'data' chunks could cause resolvePromise(stdout) to run with an incomplete payload.

♻️ Proposed fix
-    child.on('exit', (code) => {
+    child.on('close', (code) => {
       if (settled) return;
       settled = true;
       clearTimeout(timer);
       if (signal) signal.removeEventListener('abort', onAbort);
       if (code === 0) resolvePromise(stdout);
       else rejectPromise(new Error(`${toolName} failed (exit ${code}): ${stderr.slice(0, 400)}`));
     });

Please confirm this matches the intended semantics for downstream consumers of bridgeInvoker's resolved stdout (e.g., JSON parsing in callers), since switching events changes exactly when the promise settles relative to stdio completion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/pipeline-run.js` around lines 262 - 269, Replace the child
process completion listener in the bridgeInvoker flow with the `close` event so
settlement occurs only after stdout and stderr have fully drained. Preserve the
existing `settled` guard, cleanup, exit-code handling, and resolved
stdout/rejection behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/commands/pipeline-run.js`:
- Around line 200-213: The process-tree termination logic is duplicated across
killTree and killDelegateTree. Add a shared killProcessTree helper under
src/core/harness/ that preserves the platform-specific process-group kill and
empty-catch behavior, then update src/commands/pipeline-run.js lines 200-213 to
import and use it and replace killDelegateTree in
src/integrations/pi/rstack-sdlc.ts lines 1618-1623 with the same helper.

In `@src/integrations/pi/rstack-sdlc.ts`:
- Around line 1770-1776: Update the processCompletion timeout handling around
killDelegateTree so the timeout callback settles the promise directly and
unconditionally before or alongside terminating proc. Preserve normal
close/error settlement, but ensure a failed or ineffective kill cannot leave
await processCompletion hanging past delegateTimeoutMs().

In `@src/integrations/tau/rstack_sdlc.py`:
- Around line 474-483: Handle subprocess spawn OSError failures in all three
_run_bridge implementations. In src/integrations/tau/rstack_sdlc.py lines
474-483, catch OSError around asyncio.create_subprocess_exec and return
ToolResult.error matching _run_guard; in
src/integrations/operator/rstack_sdlc.py lines 337-345, catch OSError and return
(False, ...); in src/integrations/hermes/rstack_sdlc.py lines 274-282, catch
OSError and return an error string, preserving each adapter’s existing timeout
handling and error format.

---

Outside diff comments:
In `@src/integrations/tau/rstack_sdlc.py`:
- Around line 663-684: Update `_run_gate` to wrap
`asyncio.create_subprocess_exec` in an `OSError` handler, matching the existing
`_run_guard` pattern. When subprocess creation fails, return the documented
fail-open result `(True, "")` instead of propagating the exception; preserve the
existing timeout handling and successful subprocess flow.

---

Nitpick comments:
In `@src/commands/pipeline-run.js`:
- Around line 262-269: Replace the child process completion listener in the
bridgeInvoker flow with the `close` event so settlement occurs only after stdout
and stderr have fully drained. Preserve the existing `settled` guard, cleanup,
exit-code handling, and resolved stdout/rejection behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 63b73e4d-b041-4714-a554-eaa85fe835ce

📥 Commits

Reviewing files that changed from the base of the PR and between 9f93cac and 6b70e56.

📒 Files selected for processing (7)
  • docs/HARNESS.md
  • src/commands/pipeline-run.js
  • src/integrations/hermes/rstack_sdlc.py
  • src/integrations/operator/rstack_sdlc.py
  • src/integrations/pi/rstack-sdlc.ts
  • src/integrations/tau/rstack_sdlc.py
  • tests/bridge-invoker-timeout-487.test.js

Comment thread src/commands/pipeline-run.js Outdated
Comment thread src/integrations/pi/rstack-sdlc.ts
Comment thread src/integrations/tau/rstack_sdlc.py Outdated
… timeout no-hang, OSError handling

CodeRabbit caught 3 real issues on the fixup commit, all fixed:

1. killTree (pipeline-run.js) and killDelegateTree (rstack-sdlc.ts) had
   duplicated the exact same platform-check + negative-pid SIGKILL +
   empty-catch pattern. Extracted into a shared
   src/core/harness/process-tree.js killProcessTree, imported by both.

2. Pi's delegate timeout handler only called killDelegateTree and relied
   entirely on that producing a close/error event to unblock
   processCompletion — since the kill helper swallows all failures in an
   empty catch, a stale pid or an ineffective kill would leave
   `await processCompletion` hanging past the very timeout meant to bound
   it, silently defeating the #487 backstop. Now calls resolveCode(1)
   directly and unconditionally in the timeout branch (safe: a native
   Promise's resolve ignores every call after the first, so this doesn't
   conflict with a normal close/error settling it first).

3. _run_bridge (all three Python adapters) and Tau's _run_gate only caught
   the timeout exception around subprocess creation, not OSError — a rare
   spawn failure (permission error, resource limit hit between the
   shutil.which check and exec) would propagate an uncaught exception
   instead of the documented graceful-failure/fail-open contract their
   sibling _run_guard already has. Added matching OSError handling to all
   four call sites.

Also applied a CodeRabbit nitpick: bridgeInvoker settles on 'close' instead
of 'exit' — 'exit' can fire before buffered stdout/stderr 'data' chunks are
fully drained, which could resolve the promise with truncated output.

Every fix live-verified: all three Python adapters re-verified against the
real installed packages (zero orphans across all 11 call sites), plus a new
live reproduction of the OSError spawn-failure path (a bad-shebang binary
reliably raises OSError with no shell involved) against Tau, Operator, and
Hermes. Full local gate green (1892/1892 tests, typecheck/lint/security/
validate all clean).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@richard-devbot

Copy link
Copy Markdown
Owner Author

Addressed CodeRabbit's 3 findings on the previous commit:

  1. Duplicated kill-tree logic (killTree/killDelegateTree) — extracted into a shared src/core/harness/process-tree.js per the coding guideline you cited.
  2. Delegate timeout doesn't guarantee the promise settles — real bug, genuinely important: an ineffective kill (stale pid, silent failure) could leave processCompletion hanging past the very timeout meant to bound it. Now calls resolveCode(1) directly and unconditionally in the timeout branch.
  3. Missing OSError handling around subprocess spawn in _run_bridge (all 3 Python adapters) and Tau's _run_gate — added, matching the sibling _run_guard's existing pattern.

Also applied the exitclose nitpick in bridgeInvoker (avoids resolving with possibly-truncated stdout).

All fixes live-verified — including a new reproduction of the OSError spawn-failure path (a bad-shebang binary, no shell involved) against Tau/Operator/Hermes — and the full gate is green (1892/1892).

@richard-devbot
richard-devbot merged commit 1a6204a into main Aug 1, 2026
10 checks passed
@richard-devbot
richard-devbot deleted the fix-487-bridge-subprocess-timeouts branch August 1, 2026 05:39
richard-devbot pushed a commit that referenced this pull request Aug 1, 2026
… timeouts, PR #529)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

2 participants