fix(win32): show UNKNOWN for live-but-unreadable engine instead of false INTERRUPTED - #36
Conversation
…ructive guards strict On win32, psutil create_time() raises ERROR_ACCESS_DENIED for a live process in another session / elevation mismatch, so identity() returns None. The identity-aware liveness read then compared None against the recorded identity and surfaced a running engine as dead — TUI INTERRUPTED, weakened resume/delete guards (issue bmad-code-org#33 / DW-16). Rather than bias the shared alive_and_ours (which the destructive stop_run gate and the engine_alive delete/archive/reclaim guards also route through), keep it strict/fail-safe and add a tri-state ProcessHost.liveness_of -> 'alive'|'dead'|'unknown', biased away from false-dead: an unreadable identity on a still-existing pid reads 'unknown', not 'dead'. Only tui.data.liveness uses it, so a running-but-unreadable engine shows UNKNOWN, never a false INTERRUPTED (nor a false RUNNING). engine_alive/stop_run are unchanged from main, so an unverifiable pid is never signalled and never blocks cleanup forever. Because 'unknown' is now a real state for a pid-backed run, the TUI launch- conflict guard and the resume double-drive warning treat it as possibly-live (_engine_possibly_live: has a pid and liveness != 'dead'), so launching or resuming over a maybe-running engine still prompts.
|
Warning Review limit reached
Next review available in: 41 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughIntroduces a tri-state ChangesTri-state liveness detection and propagation
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant TuiApp
participant EnginePossiblyLive
participant Data
participant ProcessHost
User->>TuiApp: trigger run/resume/resolve action
TuiApp->>EnginePossiblyLive: _engine_possibly_live(run_dir)
EnginePossiblyLive->>Data: liveness(run_dir)
Data->>ProcessHost: liveness_of(pid, identity)
ProcessHost-->>Data: alive / dead / unknown
Data-->>EnginePossiblyLive: status
EnginePossiblyLive-->>TuiApp: true / false
TuiApp-->>User: show confirmation modal or warning
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
🤖 Augment PR SummarySummary: This PR fixes win32 run-status reporting by distinguishing between a dead engine and a live engine whose identity can’t be read. Changes:
Tests: Added coverage for the unreadable-identity case, strict vs non-destructive guards, and the TUI UNKNOWN / “may be live” flows. 🤖 Was this summary useful? React with 👍 or 👎 |
There was a problem hiding this comment.
Pull request overview
This PR introduces a tri-state engine liveness probe (alive/dead/unknown) to avoid reporting a live-but-unreadable engine as INTERRUPTED on win32 (e.g., psutil.AccessDenied while the PID still exists). The tri-state is routed into the TUI run-status and launch/resume/resolve conflict guards, while destructive lifecycle paths remain strict via alive_and_ours.
Changes:
- Added
ProcessHost.liveness_of(pid, identity) -> 'alive'|'dead'|'unknown'and keptalive_and_oursstrict for destructive paths. - Updated TUI liveness/status classification to surface
unknownas UNKNOWN instead of INTERRUPTED, and adjusted conflict/resume/resolve guarding to treatunknownas possibly-live. - Added tests covering the TUI UNKNOWN status and confirmation/warning flows, plus unit tests for
liveness_of.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
src/automator/process_host.py |
Adds tri-state liveness_of while preserving strict alive_and_ours for destructive operations. |
src/automator/tui/data.py |
Switches TUI liveness probing from boolean to tri-state and maps unknown to UNKNOWN status. |
src/automator/tui/app.py |
Updates TUI conflict/resume/resolve guards to treat unknown as possibly-live. |
src/automator/tui/screens/modals.py |
Adjusts resume warning text to reflect “may still be live” semantics. |
tests/test_process_host.py |
Adds coverage for the new tri-state liveness_of behavior. |
tests/test_tui_data.py |
Verifies unreadable-but-live PID is shown as UNKNOWN (not INTERRUPTED). |
tests/test_tui_app.py |
Adds UI-flow tests for unknown PID confirmation/warnings and resolve refusal. |
tests/test_runs.py |
Updates test commentary to clarify strict behavior alignment. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
_engine_possibly_live gated on read_pid() is not None, so a legacy run with no engine.pid but a live mux session (liveness == "alive") read as not-live and skipped the launch-conflict and resume double-drive guards — a regression from routing those guards through the helper (they previously used status == RUNNING / liveness == "alive", which caught legacy live sessions). Treat "alive" as possibly-live regardless of pid; keep "unknown" as possibly-live only for a pid-backed run (win32 unreadable pid), so a legacy "unknown" (no session found) still doesn't flag every old finished run.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/automator/tui/app.py (1)
463-484: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winDelete/archive guards still ignore
"unknown"liveness — contradicts the stated safety objective.
action_delete_runandaction_archive_runstill gate solely ondata.liveness(run_dir) == "alive". Since"unknown"is not"alive", a run whose engine is live-but-unreadable (the exact win32 scenario this PR is fixing) will not be blocked from deletion/archival — the guard falls through and proceeds. This is the opposite direction ofaction_stop_run's fail-safe behavior (which refuses to act when unsure).The PR's own issue objective explicitly calls for this case: "preserve resume/delete/archive/reclaim safety guards so unverifiable processes are not treated as safely dead." As written,
unknownis effectively treated as safely dead by these two guards, risking file deletion/archival while the win32 engine may still be writing torun_dir.Since
_engine_possibly_liveis already defined in this file and used by the sibling resume/resolve/launch guards, extending it here is low-effort:🛡️ Proposed fix
def action_delete_run(self) -> None: selected = self._selected_run_dir() if selected is None: return run_id, run_dir = selected - if data.liveness(run_dir) == "alive": + if _engine_possibly_live(run_dir): self.notify(f"run {run_id} is live — stop it first", severity="warning") returndef action_archive_run(self) -> None: selected = self._selected_run_dir() if selected is None: return run_id, run_dir = selected - if data.liveness(run_dir) == "alive": + if _engine_possibly_live(run_dir): self.notify(f"run {run_id} is live — stop it first", severity="warning") returnDo you want me to open an issue to track extending
_engine_possibly_liveto the delete/archive/reclaim guards, or is this deliberately deferred to a follow-up in the PR stack?Also applies to: 496-517
🤖 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/automator/tui/app.py` around lines 463 - 484, The delete/archive guard logic in action_delete_run (and the matching action_archive_run path) only blocks when data.liveness(run_dir) is "alive", so "unknown" still falls through and is treated as safe to delete/archive. Update these guards to use the existing _engine_possibly_live helper, or otherwise reject both "alive" and "unknown", so unverifiable runs are handled fail-safe like action_stop_run and the other safety checks in app.py.
🤖 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/automator/tui/app.py`:
- Around line 156-166: The liveness check in the launch flow is flagging
terminal runs because `discover_runs()` results are passed directly into
`_engine_possibly_live()`. Update the `live` collection logic in `app.py` to
filter out terminal run states such as FINISHED, STOPPED, CRASHED, and
INTERRUPTED before probing liveness, so only non-terminal runs are checked and
shown in the `ConfirmModal`.
---
Outside diff comments:
In `@src/automator/tui/app.py`:
- Around line 463-484: The delete/archive guard logic in action_delete_run (and
the matching action_archive_run path) only blocks when data.liveness(run_dir) is
"alive", so "unknown" still falls through and is treated as safe to
delete/archive. Update these guards to use the existing _engine_possibly_live
helper, or otherwise reject both "alive" and "unknown", so unverifiable runs are
handled fail-safe like action_stop_run and the other safety checks in app.py.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: ccde1b94-4118-409a-af5f-59f0034c4429
📒 Files selected for processing (8)
src/automator/process_host.pysrc/automator/tui/app.pysrc/automator/tui/data.pysrc/automator/tui/screens/modals.pytests/test_process_host.pytests/test_runs.pytests/test_tui_app.pytests/test_tui_data.py
…locking action_delete_run/action_archive_run stay non-blocking for 'unknown' liveness (a live-but-unreadable pid) — the deliberate runs.engine_alive invariant is that an unverifiable pid must never block cleanup forever, and the TUI must not diverge from the CLI delete/archive/clean guards. But the irreversible confirm must not imply the run is safely dead: when liveness is 'unknown' the delete/archive ConfirmModal now says the engine may still be live, so the human makes an informed choice. Symmetric with the resume double-drive warning.
pbean
left a comment
There was a problem hiding this comment.
Validated against #33 and the review threads — approving. ✅
What I checked:
- #33 intent satisfied. Tri-state
liveness_ofon theProcessHostbase (composingidentity() is Nonewithis_alive()) is functionally equivalent to catchingAccessDeniedinsideidentity()on the read path, with zero per-platform edits — arguably cleaner than what the issue proposed, and it inherits into any future host backend for free. - Keeping
runs.engine_alivestrict is the right call. The "unknownmust not block stop/delete" invariant is real and pre-existing (runs.pydocstring), and warn-not-block on the TUI delete/archive confirms satisfies #33's "not treated as safely dead" without letting an unverifiable/recycled pid block cleanup forever. Agree with the 6d9774d resolution. - Bot threads verified against HEAD, not just replies: the legacy pid-less live-session gap (Augment/Copilot) is genuinely fixed in ba3d528 with test coverage; the TUI delete/archive warning landed in 6d9774d with test coverage.
- POSIX safety: Linux is bit-for-bit unchanged (
/proc/<pid>/statis world-readable, sounknownis unreachable there; recycled pids still readdeadvia identity mismatch). macOS is strictly improved — a live engine with unreadable psutilcreate_timepreviously read false-dead exactly like win32.PosixProcessHost.is_alivealready mapsEPERM→True, so the composition is correct. Destructive force-kill gating untouched. CI green on py3.11–3.14.
Two non-blocking points:
-
Launch guard probes terminal runs (CodeRabbit's still-open inline nit at
app.py_guarded)._engine_possibly_livenow runs over every discovered run, including FINISHED/STOPPED/CRASHED, which_classifydeliberately never probes — so each launch double-probes live runs, and on win32/macOS a finished run whose pid got recycled by an unreadable process false-prompts "another run may be live" forever. The status already computed bydiscover_runsavoids both, no second probe needed:live = [ r.run_id for r in data.discover_runs(self.project) if r.status == data.RUNNING or (r.status == data.UNKNOWN and runs.read_pid(r.run_dir) is not None) ]
This preserves your legacy-session coverage (session-alive runs classify as RUNNING) and the pid gate for UNKNOWN. Small enough to fold into this PR if you like, but not blocking.
-
CLI parity — filed as #38.
cmd_resolve/cmd_delete/cmd_archivestill gate on the strict boolean, so on win32 the CLI can resolve (double-drive) or delete a live-but-unreadable engine with no warning, while the TUI now refuses/warns. Follow-up scoped there (tri-state helper at therunslayer so the CLI doesn't reach into TUI modules); fine to land this PR as-is.
🤖 Generated with Claude Code
What
Adds a tri-state
ProcessHost.liveness_of(alive/dead/unknown) and routes only the TUI run-status through it, so a running engine whose identity is unreadable on win32 shows UNKNOWN instead of a false INTERRUPTED.Why
On win32,
psutil.create_time()raisesERROR_ACCESS_DENIEDfor a live process in another session / at a different elevation, soidentity()returnsNone; the identity-aware liveness check then read a running engine as dead, weakening resume/delete guards.Fixes #33
How
ProcessHost.liveness_ofbiased away from false-dead: an unreadable identity on a still-existing pid readsunknown, notdead.alive_and_oursstrict/fail-safe so the destructivestop_rungate andengine_alivedelete/archive/reclaim guards are unchanged — an unverifiable pid is never signalled and never blocks cleanup.tui.data.livenessto the tri-state, and made the launch-conflict / resume double-drive guards treatunknownas possibly-live (_engine_possibly_live: has a pid and liveness !=dead).Testing
pytest tests/test_process_host.py tests/test_tui_app.py tests/test_tui_data.py— added cases for the unreadable-but-live pid (unknown), the strict destructive guards, and the TUI UNKNOWN status / possibly-live prompt paths.Summary by CodeRabbit