Skip to content

Conversation

@frankbria
Copy link
Owner

@frankbria frankbria commented Jan 30, 2026

Summary

Implements cf work follow command for real-time streaming of task execution output (#308).

  • New command: cf work follow <task-id> streams agent output as it happens
  • Buffered output: --tail N shows last N lines before streaming live
  • File-based streaming: Output stored at .codeframe/runs/<run_id>/output.log
  • Dual-write pattern: Agent writes to both stdout (when verbose) and log file (always)

Key Components

File Purpose
core/streaming.py RunOutputLogger, tail_run_output(), get_latest_lines()
cli/app.py work_follow command implementation
core/agent.py Enhanced _verbose_print() for dual-write
core/runtime.py Creates output logger for each run

Usage

cf work follow abc123              # Stream output from running task
cf work follow abc123 --tail 50    # Show last 50 lines then stream

Test plan

  • 17 unit tests for streaming module (tests/core/test_streaming.py)
  • 10 unit tests for agent integration (tests/core/test_agent_streaming.py)
  • 10 unit tests for CLI command (tests/cli/test_work_follow.py)
  • 4 integration tests added to tests/cli/test_v2_cli_integration.py
  • All 1222+ existing tests pass
  • Linting passes (ruff)

Closes #308

Summary by CodeRabbit

  • New Features
    • Added cf work follow <task-id> to stream live task run output in the CLI.
    • Added --tail <n> to preload the last n lines before streaming.
    • If no active run exists, the command shows the most recent run output when available.
    • Improved live-follow behavior: periodic completion messages and graceful interrupt handling.

✏️ Tip: You can customize this high-level summary in your review settings.

…ming

Implements live streaming of task execution output, enabling users to
attach to running tasks and view output in real-time.

Components:
- core/streaming.py: File-based streaming infrastructure
  - RunOutputLogger: Writes agent output to .codeframe/runs/<run_id>/output.log
  - tail_run_output(): Polling-based file tailing generator
  - get_latest_lines(): Buffered output retrieval for --tail flag

- cli/app.py: New `cf work follow` command
  - Attach to running tasks and stream output
  - --tail N: Show last N lines before streaming
  - Shows completion status for finished runs
  - Graceful handling of Ctrl+C

- core/agent.py: Enhanced _verbose_print() to write to log file
  - Dual-write pattern: stdout (when verbose) + log file (always)
  - Enables following non-verbose runs

- core/runtime.py: Creates output logger for each run
  - Passes logger to Agent for output capture

Tests: 37 new tests covering streaming, CLI, and agent integration

Closes #308
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 30, 2026

Walkthrough

Adds file-based real-time streaming for work runs: new streaming module and RunOutputLogger, Agent/runtime wiring to write run output, and a CLI subcommand cf work follow <task-id> (with --tail) to attach and stream live or buffered run output. Tests added.

Changes

Cohort / File(s) Summary
Streaming Infrastructure
codeframe/core/streaming.py
New module: run output path resolution, RunOutputLogger (timestamped writes, context manager), helpers to read latest lines, and tail_run_output for polling/tailing live output.log.
Core Integration
codeframe/core/agent.py, codeframe/core/runtime.py
Agent constructor extended to accept output_logger; runtime creates and passes RunOutputLogger into agent runs (including retry flows), logs agent events into the run logger, and ensures logger cleanup.
CLI Feature
codeframe/cli/app.py
Added work follow command (supports --tail N, timeout, interruption); resolves workspace/task, finds active or recent run, streams live output or shows final output. Note: duplicate follow command definitions present in file.
Tests
tests/core/test_streaming.py, tests/core/test_agent_streaming.py, tests/cli/test_work_follow.py, tests/cli/test_v2_cli_integration.py
Comprehensive tests for streaming module, RunOutputLogger behavior, Agent/runtime integration, and CLI follow scenarios (no run, completed run, tailing, interruptions, non-existent tasks).

Sequence Diagram

sequenceDiagram
    participant User as User
    participant CLI as "cf work follow"
    participant WS as Workspace
    participant Task as Task/Run
    participant Stream as Stream Module
    participant Logger as RunOutputLogger

    User->>CLI: cf work follow <task-id> [--tail N]
    CLI->>WS: resolve workspace
    CLI->>Task: find task by partial id
    alt task found
        CLI->>Task: get active run
        alt active run exists
            opt tail requested
                CLI->>Stream: get_latest_lines_with_count(N)
                Stream-->>CLI: return buffered lines
                CLI->>User: print buffered lines
            end
            CLI->>Stream: tail_run_output(run_id)
            loop poll for new lines
                Stream->>Logger: read/flush output.log
                Logger-->>Stream: yield new lines
                Stream-->>CLI: deliver lines
                CLI->>User: print live output
            end
            Task->>CLI: run completes
            CLI->>User: print completion message
        else no active run
            CLI->>Stream: get_latest_lines(run_id) (if recent)
            CLI->>User: print final output or suggest start
        end
    else task not found
        CLI->>User: error "no task found"
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I nibble logs beneath the moonlit code,

I stamp each line and watch the live stream flow,
Hop on, dear human, follow the busy node,
Tail the tail and see the agent go! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the primary change: adding a new CLI command for live execution streaming.
Linked Issues check ✅ Passed All acceptance criteria from #308 are met: streaming works via file-based approach, mid-run attachment with buffering is supported, completion handling is graceful, and verbose-mode output parity is achieved.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing cf work follow streaming. No unrelated modifications detected outside the scope of #308.
Docstring Coverage ✅ Passed Docstring coverage is 91.55% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/work-follow-streaming

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

@macroscopeapp
Copy link
Contributor

macroscopeapp bot commented Jan 30, 2026

Add cf work follow in codeframe/cli/app.py to stream live task run output with --tail/-n buffered lines and 1s status polling using core/streaming.py log utilities

Introduce per-run output logging and streaming: Agent writes to output.log via RunOutputLogger; runtime creates and closes the logger; CLI streams with tail_run_output and optional tail count. Core utilities handle log path, existence checks, latest lines, and tailing. Tests cover CLI flows, Agent logging, and streaming behaviors.

📍Where to Start

Start with the work_app.work_follow command in app.py, then review logging and streaming utilities in streaming.py and the logger integration in runtime.py.


Macroscope summarized 269a78e.

@claude
Copy link

claude bot commented Jan 30, 2026

Code Review: cf work follow Implementation

This is an excellent implementation of real-time task execution streaming. The code is well-architected, thoroughly tested, and adheres to the v2 architecture principles. Below is my detailed review:


Strengths

1. Architecture Adherence

  • Headless core: core/streaming.py has zero FastAPI/HTTP dependencies ✓
  • CLI-first: Works without server running ✓
  • Clean separation: Core logic properly separated from CLI presentation ✓
  • Follows v2 patterns: Matches the established repository structure

2. Excellent Test Coverage

  • 37 new tests across 3 test modules
  • Unit tests: test_streaming.py (17 tests) covers all streaming primitives
  • Integration tests: test_agent_streaming.py (10 tests) verifies agent integration
  • CLI tests: test_work_follow.py (10 tests) validates command behavior
  • Edge cases: Missing files, concurrent writes, keyboard interrupts, empty logs

3. Robust Implementation

  • Dual-write pattern: Agent writes to both stdout (verbose) and log file (always) for flexibility
  • File-based streaming: Simple, reliable polling-based approach
  • Graceful degradation: Handles missing files, interrupted streams, completed runs
  • Thread-safe: Flush-after-write ensures real-time streaming works correctly

4. User Experience

  • Intuitive UX: --tail N shows buffered output before streaming
  • Clear feedback: Shows task info, run status, completion messages
  • Ctrl+C handling: Gracefully exits with helpful resume instructions
  • Completed runs: Shows final output for non-active runs

🔍 Code Quality Observations

Minor Suggestions

1. Exception Handling in tail_run_output (core/streaming.py:580-581)

except Exception:
    pass  # File might be temporarily unavailable

Suggestion: Consider logging this exception or making it more specific:

except (FileNotFoundError, PermissionError):
    pass  # File might be temporarily unavailable

Catching bare Exception might hide unexpected errors during development.

2. Magic Numbers (cli/app.py:214, 165)

  • poll_interval=0.3 (line 214)
  • Default tail count of 50 (line 165)

Suggestion: Consider extracting these as module-level constants:

DEFAULT_POLL_INTERVAL = 0.3
DEFAULT_TAIL_LINES = 50

3. RunStatus Check Optimization (cli/app.py:220-221)

# Check if run is still active (periodically)
current_run = runtime.get_run(workspace, active_run.id)

This DB query happens for every line of output. Consider rate-limiting to every N lines or N seconds:

lines_since_check = 0
CHECK_INTERVAL = 10  # Check every 10 lines

for line in tail_run_output(...):
    console.print(line.rstrip())
    lines_since_check += 1
    
    if lines_since_check >= CHECK_INTERVAL:
        current_run = runtime.get_run(workspace, active_run.id)
        lines_since_check = 0
        if current_run and current_run.status in TERMINAL_STATUSES:
            # ... handle completion

4. File Handle Management (core/streaming.py:440)

The RunOutputLogger.__init__ opens the file immediately, but there's a window before context manager entry where it could leak:

def __init__(self, workspace: Workspace, run_id: str):
    # ...
    self._file = open(self.log_path, "a", encoding="utf-8")  # Could leak if exception

Suggestion: Consider defensive programming:

self._file = None
try:
    self._file = open(self.log_path, "a", encoding="utf-8")
except Exception:
    if self._file:
        self._file.close()
    raise

Or just document that users should always use it as a context manager.


🔒 Security Considerations

✓ No Security Issues Detected

  • Path handling: Uses workspace.repo_path safely, no path traversal risk
  • No user input injection: run_id comes from controlled database queries
  • File permissions: Inherits workspace directory permissions appropriately

🚀 Performance Considerations

Good Choices

  • Polling interval of 0.3s: Good balance between responsiveness and CPU usage
  • Line-by-line streaming: Memory-efficient for large outputs
  • Immediate flush: Ensures real-time updates

Potential Optimization

The get_latest_lines_with_count reads the entire file into memory:

with open(log_path, "r", encoding="utf-8") as f:
    all_lines = f.readlines()

For very large log files (thousands of lines), this could be inefficient. Consider a tail-optimized approach using collections.deque or reading from end-of-file for production use:

from collections import deque

def get_latest_lines_efficient(path: Path, count: int) -> list[str]:
    with open(path, 'r') as f:
        return list(deque(f, maxlen=count))

However, for typical task logs this is likely fine.


📝 Documentation

✓ Well Documented

  • Comprehensive module docstrings in streaming.py
  • Clear function docstrings with parameter descriptions
  • Inline comments explain non-obvious logic
  • CLAUDE.md updated with new command examples

🧪 Testing Quality

Excellent Test Suite

  • Concurrent writing test (test_streaming.py:1379-1419): Great realistic scenario
  • Mock usage: Appropriate use of mocks without over-mocking
  • Edge cases: Missing files, empty logs, interrupts all covered
  • Integration coverage: Tests verify end-to-end CLI → Core flow

Minor Test Suggestion

In test_work_follow.py:819-832, the threading test has a 5-second timeout:

thread.join(timeout=5)

Consider making test execution faster with shorter sleep intervals or mock time.


📋 Checklist

  • ✅ Follows v2 architecture (headless core, CLI-first)
  • ✅ No FastAPI/HTTP dependencies in core
  • ✅ Comprehensive test coverage (37 tests)
  • ✅ All existing tests pass
  • ✅ Linting passes (ruff)
  • ✅ Documentation updated (CLAUDE.md)
  • ✅ Edge cases handled (missing files, interrupts, completed runs)
  • ✅ User experience is intuitive
  • ✅ Code is readable and maintainable

🎯 Final Verdict

APPROVE

This PR is ready to merge. The implementation is:

  • Architecturally sound
  • Well-tested
  • User-friendly
  • Performance-conscious
  • Properly documented

The minor suggestions above are optional improvements that could be addressed in future iterations if needed. They don't block this PR.

Excellent work! This feature will significantly improve the developer experience when working with long-running tasks.


📌 Recommended Next Steps

  1. Consider the RunStatus check optimization to reduce DB queries
  2. Monitor log file sizes in production - may need log rotation later
  3. Future enhancement: Support --follow for batch executions (multiple tasks)

Great contribution to Phase 1 of the v2 roadmap! 🚀


try:
# Stream output
for line in tail_run_output(
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Medium

The terminal status check at line 2555 only runs after tail_run_output yields a new line. If the run completes without producing new output, the loop hangs indefinitely. Consider checking run status inside the tail_run_output generator or implementing a separate status-polling mechanism that runs independently of line yields.

🚀 Want me to fix this? Reply ex: "fix it for me".

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
codeframe/core/runtime.py (1)

648-656: ⚠️ Potential issue | 🟡 Minor

Preserve verbose output on supervisor retries

Line 648 and Line 745 recreate the Agent without verbose=verbose, so stdout streaming stops after the first retry even when the user requested verbose. Pass the flag through both retry constructions.

🛠️ Proposed fix
         agent = Agent(
             workspace=workspace,
             llm_provider=provider,
             dry_run=dry_run,
             on_event=on_agent_event,
             debug=debug,
+            verbose=verbose,
             fix_coordinator=fix_coordinator,
             output_logger=output_logger,
         )
@@
         agent = Agent(
             workspace=workspace,
             llm_provider=provider,
             dry_run=dry_run,
             on_event=on_agent_event,
             debug=debug,
+            verbose=verbose,
             fix_coordinator=fix_coordinator,
             output_logger=output_logger,
         )

Also applies to: 745-753

🤖 Fix all issues with AI agents
In `@codeframe/core/runtime.py`:
- Around line 607-610: The RunOutputLogger instance (created as output_logger =
RunOutputLogger(workspace, run.id)) must be closed on all exit paths; wrap the
remaining execute_agent body that calls agent.run() and any supervisor retry
logic in a try/finally (or use a context manager) and call output_logger.close()
in the finally block so the file handle is always released and streaming flushed
even if agent.run() or supervisor retry raises.

In `@tests/cli/test_work_follow.py`:
- Around line 6-20: The module lacks the required pytest v2 marker; add a
module-level variable pytestmark = pytest.mark.v2 near the top of the test file
(e.g., alongside the existing imports where `from codeframe.cli.app import app`,
`runner = CliRunner()`, and other globals are defined) so the tests using the
`app` CLI are included when running `pytest -m v2`; ensure `pytest` is imported
if not already.
🧹 Nitpick comments (2)
codeframe/core/streaming.py (2)

61-76: Consider defensive initialization for robustness.

If mkdir() succeeds but open() fails (e.g., permission issues), the object is partially initialized without _file, which could cause an AttributeError in close() if called on an incompletely constructed instance.

🛡️ Proposed defensive initialization
     def __init__(self, workspace: Workspace, run_id: str):
         """Initialize the logger.

         Args:
             workspace: Target workspace
             run_id: Run identifier
         """
         self.workspace = workspace
         self.run_id = run_id
         self.log_path = get_run_output_path(workspace, run_id)
+        self._file = None  # Initialize before potential failure

         # Ensure directory exists
         self.log_path.parent.mkdir(parents=True, exist_ok=True)

         # Open file in append mode
         self._file = open(self.log_path, "a", encoding="utf-8")

100-103: Guard against uninitialized _file attribute.

If __init__ fails after the class is instantiated but before _file is assigned, calling close() directly or via __exit__ will raise AttributeError.

🛡️ Proposed safer close implementation
     def close(self) -> None:
         """Close the log file."""
-        if self._file and not self._file.closed:
+        if hasattr(self, "_file") and self._file and not self._file.closed:
             self._file.close()

Test User added 2 commits January 29, 2026 23:05
- Wrap agent execution in try/finally to ensure output logger cleanup
- Add time-based throttling for status checks (every 1s instead of per-line)
- Add pytest.mark.v2 marker to test_work_follow.py for v2 test filtering
- Add verbose=verbose to supervisor retry Agent constructions
- Add defensive initialization in RunOutputLogger (__init__ and close)
@claude
Copy link

claude bot commented Jan 30, 2026

Follow-up Review: Latest Commit (8106b65)

Excellent work addressing the review feedback! The latest commit successfully resolves the issues identified by CodeRabbit:

Issues Resolved

  1. Output logger cleanup - Added try/finally block in runtime.py to ensure output_logger.close() is called on all exit paths ✓
  2. Test marker - Added pytestmark = pytest.mark.v2 to test_work_follow.py for proper v2 test filtering ✓
  3. Status check optimization - Implemented time-based throttling (1-second intervals) instead of per-line DB queries ✓

This addresses both CodeRabbit's actionable comments and implements one of my performance suggestions from the initial review.

⚠️ Remaining Item from CodeRabbit

CodeRabbit identified one more issue that still needs attention:

Missing verbose parameter in supervisor retry paths (runtime.py lines 648-656 and 745-753)

When the supervisor retries a failed task, it creates a new Agent instance but doesn't pass the verbose=verbose parameter. This means stdout streaming stops after the first retry, even when the user requested verbose output.

Fix needed:

agent = Agent(
    workspace=workspace,
    llm_provider=provider,
    dry_run=dry_run,
    on_event=on_agent_event,
    debug=debug,
    verbose=verbose,  # ← Add this line
    fix_coordinator=fix_coordinator,
    output_logger=output_logger,
)

This should be applied in both retry paths (blocked task retry around line 648 and tactical supervisor retry around line 745).

📊 Status

  • Original issues: Resolved
  • Performance optimization: Implemented
  • Verbose flag preservation: Needs fix ⚠️

Once the verbose parameter is added to both supervisor retry paths, this PR will be fully ready to merge.

Great job on the quick turnaround addressing feedback! 🚀

print("[Supervisor] Retrying task after auto-resolution...")

# Create a new agent instance and retry
agent = Agent(
Copy link
Contributor

Choose a reason for hiding this comment

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

🟢 Low

The verbose parameter is missing when creating retry Agent instances (lines 649 and 746), though it's included in the initial agent. Consider adding verbose=verbose to preserve user-requested verbose mode on retries.

🚀 Want me to fix this? Reply ex: "fix it for me".

@claude
Copy link

claude bot commented Jan 30, 2026

Code Review: cf work follow Implementation (Latest Update)

I've reviewed the latest commits and all previous feedback. Excellent work! 🎉


All Issues Resolved

The latest commits have successfully addressed all feedback from both CodeRabbit and my previous review:

1. Output Logger Cleanup

  • Added try/finally block in runtime.py to ensure output_logger.close() is called on all exit paths
  • File handles are now properly released even if agent execution raises exceptions

2. Test Markers

  • Added pytestmark = pytest.mark.v2 to test_work_follow.py
  • Tests now properly included when running pytest -m v2

3. Status Check Optimization

  • Implemented time-based throttling (1-second intervals) instead of per-line DB queries
  • Significantly reduces database load during streaming

4. Verbose Flag Preservation

  • Both supervisor retry paths (lines 655 and 753 in runtime.py) now pass verbose=verbose
  • Ensures stdout streaming continues through retries when user requests verbose output

🎯 Final Assessment

APPROVE

This PR is production-ready and represents an excellent addition to the v2 CLI:

Architecture

  • ✅ Headless core with zero FastAPI dependencies
  • ✅ CLI-first design (works without server)
  • ✅ Clean separation of concerns
  • ✅ Follows established v2 patterns

Implementation Quality

  • ✅ Robust error handling with proper resource cleanup
  • ✅ Thread-safe file-based streaming with immediate flush
  • ✅ Graceful degradation for edge cases
  • ✅ Performance-conscious (time-based throttling, line-by-line streaming)

Test Coverage

  • ✅ 37 comprehensive tests across 3 modules
  • ✅ Unit tests for streaming primitives
  • ✅ Integration tests for agent integration
  • ✅ CLI tests with concurrent execution scenarios
  • ✅ Edge cases covered (missing files, interrupts, empty logs)

User Experience

  • ✅ Intuitive --tail N flag for buffered output
  • ✅ Clear feedback for task status and completion
  • ✅ Ctrl+C handling with resume instructions
  • ✅ Supports both active and completed runs

Documentation

  • ✅ CLAUDE.md updated with command examples
  • ✅ Comprehensive docstrings in streaming.py
  • ✅ Clear function documentation

📝 Minor Optimization Suggestions (Optional)

These are non-blocking improvements that could be considered in future iterations:

  1. Large log file handling: For very large logs (thousands of lines), get_latest_lines_with_count could use collections.deque for tail-optimized reading
  2. Magic number extraction: Consider extracting 0.3 poll interval and 50 default tail as module constants
  3. Defensive file initialization: Could initialize self._file = None before opening in RunOutputLogger.__init__ for extra safety

🚀 Impact

This feature significantly improves developer experience for Phase 1 of the v2 roadmap by:

  • Enabling real-time monitoring of long-running tasks
  • Providing mid-execution attachment without restarting
  • Supporting debugging workflows with buffered output
  • Maintaining output parity between verbose and non-verbose runs

Excellent contribution! Ready to merge. 🎉

@frankbria frankbria merged commit 43d84af into main Jan 30, 2026
13 of 14 checks passed
@frankbria frankbria deleted the feature/work-follow-streaming branch January 30, 2026 06:16
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@codeframe/cli/app.py`:
- Around line 2421-2426: The tail Typer option currently allows non-positive
values which cause counterintuitive slicing; ensure tail is constrained to >= 1
by either adding a min bound on the Option or validating after parsing.
Specifically, update the tail declaration (symbol: tail) to include a minimum
(e.g., typer.Option(..., min=1)) if supported, or add a simple check where tail
is used (e.g., if tail is not None and tail < 1: raise
typer.BadParameter("--tail must be >= 1")) to reject zero/negative inputs.
- Around line 2549-2575: The loop currently only polls run status when
tail_run_output yields a line, which can hang if the run finishes without
emitting new output; fix by decoupling output consumption from status polling:
get an iterator from tail_run_output(...) (use the same args), then run a while
loop that attempts to read the next line non-blockingly (or with a short
timeout) and prints it when available, but independently every
STATUS_CHECK_INTERVAL call runtime.get_run(workspace, active_run.id) and compare
against TERMINAL_STATUSES; update last_status_check when you poll, break and
print the colored completion message when the run is terminal, and ensure
StopIteration or generator exhaustion also triggers a final status check to
avoid hanging.

Comment on lines +2421 to +2426
tail: Optional[int] = typer.Option(
None,
"--tail",
"-n",
help="Show last N lines of buffered output before streaming",
),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate --tail to avoid negative/zero surprises.
tail <= 0 leads to counterintuitive slicing (e.g., -1 returns nearly all lines). Consider constraining it to >= 1.

🛠️ Proposed validation
     tail: Optional[int] = typer.Option(
         None,
         "--tail",
         "-n",
+        min=1,
         help="Show last N lines of buffered output before streaming",
     ),
🤖 Prompt for AI Agents
In `@codeframe/cli/app.py` around lines 2421 - 2426, The tail Typer option
currently allows non-positive values which cause counterintuitive slicing;
ensure tail is constrained to >= 1 by either adding a min bound on the Option or
validating after parsing. Specifically, update the tail declaration (symbol:
tail) to include a minimum (e.g., typer.Option(..., min=1)) if supported, or add
a simple check where tail is used (e.g., if tail is not None and tail < 1: raise
typer.BadParameter("--tail must be >= 1")) to reject zero/negative inputs.

Comment on lines +2549 to +2575
# Stream output
for line in tail_run_output(
workspace,
active_run.id,
since_line=start_line,
poll_interval=0.3,
max_wait=max_wait,
):
console.print(line.rstrip())

# Check run status periodically (not on every line)
current_time = time.time()
if current_time - last_status_check >= STATUS_CHECK_INTERVAL:
last_status_check = current_time
current_run = runtime.get_run(workspace, active_run.id)
if current_run and current_run.status in TERMINAL_STATUSES:
# Show completion message
status_color = {
runtime.RunStatus.COMPLETED: "green",
runtime.RunStatus.FAILED: "red",
runtime.RunStatus.BLOCKED: "yellow",
}.get(current_run.status, "white")

console.print(
f"\n[{status_color}]Run {current_run.status.value}[/{status_color}]"
)
break
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Follow can hang when the run finishes without emitting new output.
Status polling only happens when a line is yielded. If the run completes after the last line (or never logs), the loop never checks terminal status and can block indefinitely.

🛠️ Suggested refactor to decouple status checks from output
-            last_status_check = time.time()
-            STATUS_CHECK_INTERVAL = 1.0  # Check status every 1 second
-
-            # Stream output
-            for line in tail_run_output(
-                workspace,
-                active_run.id,
-                since_line=start_line,
-                poll_interval=0.3,
-                max_wait=max_wait,
-            ):
-                console.print(line.rstrip())
-
-                # Check run status periodically (not on every line)
-                current_time = time.time()
-                if current_time - last_status_check >= STATUS_CHECK_INTERVAL:
-                    last_status_check = current_time
-                    current_run = runtime.get_run(workspace, active_run.id)
-                    if current_run and current_run.status in TERMINAL_STATUSES:
-                        # Show completion message
-                        status_color = {
-                            runtime.RunStatus.COMPLETED: "green",
-                            runtime.RunStatus.FAILED: "red",
-                            runtime.RunStatus.BLOCKED: "yellow",
-                        }.get(current_run.status, "white")
-
-                        console.print(
-                            f"\n[{status_color}]Run {current_run.status.value}[/{status_color}]"
-                        )
-                        break
+            STATUS_CHECK_INTERVAL = 1.0  # Check status every 1 second
+            current_line = start_line
+            overall_start = time.time()
+
+            while True:
+                for line in tail_run_output(
+                    workspace,
+                    active_run.id,
+                    since_line=current_line,
+                    poll_interval=0.3,
+                    max_wait=STATUS_CHECK_INTERVAL,
+                ):
+                    console.print(line.rstrip())
+                    current_line += 1
+
+                # Check run status even if no new output arrived
+                current_run = runtime.get_run(workspace, active_run.id)
+                if current_run and current_run.status in TERMINAL_STATUSES:
+                    status_color = {
+                        runtime.RunStatus.COMPLETED: "green",
+                        runtime.RunStatus.FAILED: "red",
+                        runtime.RunStatus.BLOCKED: "yellow",
+                    }.get(current_run.status, "white")
+                    console.print(f"\n[{status_color}]Run {current_run.status.value}[/{status_color}]")
+                    break
+
+                if max_wait is not None and (time.time() - overall_start) >= max_wait:
+                    break
🤖 Prompt for AI Agents
In `@codeframe/cli/app.py` around lines 2549 - 2575, The loop currently only polls
run status when tail_run_output yields a line, which can hang if the run
finishes without emitting new output; fix by decoupling output consumption from
status polling: get an iterator from tail_run_output(...) (use the same args),
then run a while loop that attempts to read the next line non-blockingly (or
with a short timeout) and prints it when available, but independently every
STATUS_CHECK_INTERVAL call runtime.get_run(workspace, active_run.id) and compare
against TERMINAL_STATUSES; update last_status_check when you poll, break and
print the colored completion message when the run is terminal, and ensure
StopIteration or generator exhaustion also triggers a final status check to
avoid hanging.

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.

[Phase 1] cf work follow - Live execution streaming

2 participants