Skip to content

Re-adopt the output capture for the clean phase of run_and_clean#240

Merged
ahogappa merged 1 commit into
masterfrom
fix/clean-phase-output-capture
Jun 13, 2026
Merged

Re-adopt the output capture for the clean phase of run_and_clean#240
ahogappa merged 1 commit into
masterfrom
fix/clean-phase-output-capture

Conversation

@ahogappa

@ahogappa ahogappa commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Problem

Flagged during the #237#239 review and confirmed here with instrumentation: during the clean phase of run_and_clean, the Simple status line shows stale run-phase output (or blank) instead of live clean output.

run_and_clean reuses one ExecutionFacade across both phases, but each phase's Executor (execute / execute_clean) sets up and tears down its own TaskOutputRouter. The layout grabs the run-phase router at the run-phase on_ready; then on_ready's return if @root_task_class once-guard means the clean phase's fresh router — built after the run router was torn down — is never adopted. The layout reads the dead run router for the whole clean phase.

Instrumented trace (before), ctx = facade, ctx_cap = facade's current router:

[on_ready] root="CleanGroupTask" cap 4->176 ctx=184 ctx_cap=176   # run: layout adopts 176
[on_ready] root="CleanGroupTask" cap 176->176 ctx=184 ctx_cap=200 # clean: SAME facade, fresh router 200, layout STUCK on 176

Fix

The once-guard exists to stop a nested executor from overwriting the displayed root / rebuilding the tree — not to freeze the output capture. Re-adopting the capture is safe and necessary when the same facade re-readies (the clean phase).

on_ready now refreshes @output_capture whenever the readying context is the facade that owns the display (recorded as @display_facade on the first ready), and keeps root_task_class + handle_ready behind the once-guard. The discriminator is facade identity, established by probing the real lifecycle:

  • Clean phase: same facade, fresh router → re-adopt ✓
  • Nested execution: fresh_wrapper reuses ExecutionFacade.current, so it runs on the current facade and router → refresh is a no-op (verified: nested on_ready shows identical ctx/ctx_cap to its parent)
  • Unrelated facade: cannot retarget the display's capture

After:

[on_ready] ... cap 176->200 ctx=184 ctx_cap=200   # clean: router 200 adopted
status frame: ⠦ [1/1] CleanGroupTask | CLEAN removing artifact

Tests

  • Component (test_layout_base.rb): same-facade re-ready adopts the new router and keeps the root; a different facade does not retarget the capture (nor the root — the once-guard).
  • End-to-end (test_clean_phase_output_capture.rb, fixture tasks per CLAUDE.md): run_and_clean through the real executor adopts two distinct routers, and the clean-phase status line reads the clean-phase output, not the dead run router. Deterministic across repeated runs.

Suite 789 runs / 0 failures, rake standard clean. Off master, theme-independent — merges cleanly in any order with the other open PRs.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed output display to correctly show active-phase task output during execution.
  • Tests

    • Added comprehensive tests for clean-phase output capture behavior.
    • Added tests validating display facade handling in progress layout.

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@ahogappa, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 52 minutes and 38 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

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.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 49a8b24c-ee9c-4cd5-aa12-68ad45417426

📥 Commits

Reviewing files that changed from the base of the PR and between 8e0b9b8 and 021425e.

📒 Files selected for processing (4)
  • lib/taski/progress/layout/base.rb
  • test/fixtures/clean_capture_tasks.rb
  • test/test_clean_phase_output_capture.rb
  • test/test_layout_base.rb

Walkthrough

This change makes Taski::Progress::Layout::Base#on_ready reuse output capture only for the same display facade, and adds unit and end-to-end tests covering clean-phase router replacement and displayed output during run_and_clean.

Changes

Output capture readiness flow

Layer / File(s) Summary
Facade-aware on_ready handling
lib/taski/progress/layout/base.rb, test/test_layout_base.rb
Taski::Progress::Layout::Base stores the active display facade and only re-adopts output_capture for that same facade; layout-base tests and helpers cover same-facade reinitialization and different-facade non-retargeting.
Clean-phase capture contract
test/fixtures/clean_capture_tasks.rb, test/test_clean_phase_output_capture.rb
A fixture task emits distinct run and clean output, and an end-to-end test verifies the clean phase adopts a rebuilt router and reports clean output as the last displayed line.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • ahogappa/taski#60: This change directly exercises how TaskOutputRouter is adopted and observed through Taski::Progress::Layout::Base#on_ready.
  • ahogappa/taski#150: Both changes modify lib/taski/progress/layout/base.rb and the layout readiness/output-capture behavior in the same class.

Poem

🐇 I watched two phases softly race,
one run, one clean, through capture space.
The right facade now holds the view,
so fresh clean lines come hopping through.
Carrot cheers for tests so bright!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.29% 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 describes the main fix: re-adopting output capture during the clean phase of run_and_clean.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/clean-phase-output-capture

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 and usage tips.

run_and_clean reuses ONE ExecutionFacade across both phases, but each
phase's Executor sets up and tears down its OWN output router. The
layout grabbed the run-phase router at the run-phase on_ready, then
on_ready's `return if @root_task_class` once-guard meant the clean
phase's fresh router (built after the run router was torn down) was
never adopted — the status line read the dead run-phase router for the
whole clean phase, showing stale or blank output instead of live clean
output.

The once-guard exists to stop a nested executor from overwriting the
displayed root / rebuilding the tree. But re-adopting the output capture
is safe and necessary when the SAME facade re-readies (the clean phase).
on_ready now refreshes @output_capture whenever the readying context is
the facade that owns the display (captured as @display_facade on the
first ready), and keeps root_task_class + handle_ready behind the
once-guard. A nested execution runs on the current facade and router
(fresh_wrapper reuses ExecutionFacade.current), so the refresh is a
no-op there; an unrelated facade cannot retarget the display's capture.

Verified on a PTY: the Simple status line now shows
"| CLEAN removing artifact" during clean (previously the stale run
line). Pinned by a component test (same-facade re-ready adopts the new
router; a different facade does not) and an end-to-end test through the
real executor (run_and_clean adopts two distinct routers and the clean
status line reads the clean-phase output, not the dead run router).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ahogappa
ahogappa force-pushed the fix/clean-phase-output-capture branch from 8e0b9b8 to 021425e Compare June 13, 2026 22:35
@ahogappa
ahogappa merged commit eb7099b into master Jun 13, 2026
8 checks passed
@ahogappa
ahogappa deleted the fix/clean-phase-output-capture branch June 13, 2026 22:46
ahogappa added a commit that referenced this pull request Jun 14, 2026
The progress display is a persistent singleton (Config#build memoizes
@cached_display), so it is reused across sequential top-level executions
in one process. But the displayed root was never reset and @tasks
accumulated, so a second top-level Task.run / run_and_clean rendered the
FIRST execution's root name and an accumulated task count:

    [TASKI] Starting FirstRoot      # run #1
    [TASKI] Completed: 1/1 tasks
    [TASKI] Starting FirstRoot      # run #2: stale root (should be SecondRoot)
    [TASKI] Completed: 2/2 tasks    # accumulated (should be 1/1)

This hits any program running two or more top-level tasks per process
(scripts, test suites, REPLs). Pre-existing; surfaced by the #240 review.

Fix: clear per-execution state when the OUTERMOST execution stops
(on_stop at @nest_level 0), after the final frame has rendered and the
render thread has been stopped by handle_stop. The next top-level
execution then sees @root_task_class == nil and adopts its own
root/capture via the normal on_ready path; nested executions stop at
@nest_level > 0 and never trigger the reset.

Resetting at stop (rather than lazily at the next on_ready) is what makes
run_and_clean correct: run_and_clean calls notify_start BEFORE its first
on_ready, so a reset deferred to on_ready would come too late for the
start line and would mis-detect the new execution as nested. on_ready is
therefore unchanged from the clean-phase-capture fix (same-facade
re-adopt; otherwise ignore).

reset_after_execution clears the base tallies (@tasks,
@group_start_times, @start_time) plus @root_task_class / @display_facade
/ @output_capture, and calls the handle_reset hook; Tree re-inits its
node maps, Tree::Live also drops @last_line_count (so the next frame
does not erase the previous execution's final output), and Simple clears
its group baselines.

Pinned: end-to-end tests through the real executor for two sequential
plain runs AND two sequential run_and_clean (each shows its own root and
1/1, no accumulation), a guard test that the display singleton is reused,
and a test that state is cleared after stop; component tests for
on_stop clearing state and a second execution adopting its own state
(including readying at a raised nest level, as run_and_clean does).
Existing once-guard tests updated to simulate real nesting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ahogappa added a commit that referenced this pull request Jun 14, 2026
The progress display is a persistent singleton (Config#build memoizes
@cached_display), so it is reused across sequential top-level executions
in one process. But the displayed root was never reset and @tasks
accumulated, so a second top-level Task.run / run_and_clean rendered the
FIRST execution's root name and an accumulated task count:

    [TASKI] Starting FirstRoot      # run #1
    [TASKI] Completed: 1/1 tasks
    [TASKI] Starting FirstRoot      # run #2: stale root (should be SecondRoot)
    [TASKI] Completed: 2/2 tasks    # accumulated (should be 1/1)

This hits any program running two or more top-level tasks per process
(scripts, test suites, REPLs). Pre-existing; surfaced by the #240 review.

Fix: clear per-execution state when the OUTERMOST execution stops
(on_stop at @nest_level 0), after the final frame has rendered and the
render thread has been stopped by handle_stop. The next top-level
execution then sees @root_task_class == nil and adopts its own
root/capture via the normal on_ready path; nested executions stop at
@nest_level > 0 and never trigger the reset.

Resetting at stop (rather than lazily at the next on_ready) is what makes
run_and_clean correct: run_and_clean calls notify_start BEFORE its first
on_ready, so a reset deferred to on_ready would come too late for the
start line and would mis-detect the new execution as nested. on_ready is
therefore unchanged from the clean-phase-capture fix (same-facade
re-adopt; otherwise ignore).

reset_after_execution clears the base tallies (@tasks,
@group_start_times, @start_time), @root_task_class / @display_facade /
@output_capture, and @spinner_index, and calls the handle_reset hook;
Tree re-inits its node maps, Tree::Live also drops @last_line_count (so
the next frame does not erase the previous execution's final output), and
Simple clears its group baselines. The reset runs from an ensure, so a
raising final render or message flush still clears state (dispatch
swallows observer errors, so a leak there would silently corrupt the
next run).

Pinned: end-to-end tests through the real executor for two sequential
plain runs AND two sequential run_and_clean (each shows its own root and
1/1, no accumulation), a guard test that the display singleton is reused,
and a test that state is cleared after stop; component tests for on_stop
clearing per-execution state (incl. spinner index), reset-via-ensure when
the final render raises, a second execution adopting its own state
(including readying at a raised nest level, as run_and_clean does), the
Tree node map being rebuilt for a second execution, and Tree::Live's
@last_line_count being reset. Existing once-guard tests updated to
simulate real nesting.

Co-Authored-By: Claude Fable 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.

1 participant