Skip to content

feat: Agent stall detection and recovery (#399) - #423

Merged
frankbria merged 5 commits into
mainfrom
feature/issue-399-agent-stall-detection
Mar 8, 2026
Merged

feat: Agent stall detection and recovery (#399)#423
frankbria merged 5 commits into
mainfrom
feature/issue-399-agent-stall-detection

Conversation

@frankbria

@frankbria frankbria commented Mar 8, 2026

Copy link
Copy Markdown
Owner

Summary

Implements #399: Agent Stall Detection and Recovery

  • StallMonitor: Thread-based daemon watchdog that detects when the agent makes no tool calls for a configurable duration (default 300s). Fires callback on stall.
  • ReactAgent integration: Monitor starts before the ReAct loop, stops in finally. Stall check at top of each iteration and verification retry. Creates blocker with context on stall → returns BLOCKED.
  • CLI: --stall-timeout flag on work start and work batch run (default 300s, 0=disabled)
  • Config: agent_budget.stall_timeout_s in .codeframe/config.yaml
  • Event: AGENT_STALL_DETECTED event type for monitoring
  • Batch support: Conductor forwards --stall-timeout to subprocess commands

Acceptance Criteria

  • Agent execution is killed after configurable stall timeout (default: 300s)
  • Stall is defined as "no tool call executed" for the timeout duration
  • On stall: create blocker with context OR retry (configurable)
  • Stall timeout configurable via CLI flag and AGENTS.md/CODEFRAME.md
  • Stall detection works for both single-task and batch execution
  • Integration tests for stall → recovery path

Test Plan

  • 22 unit/integration tests covering:
    • StallEvent dataclass creation
    • StallMonitor timing, threading, daemon, idempotent stop
    • Notify prevents stall, callback fires after timeout
    • Disabled mode (timeout=0)
    • AgentBudgetConfig stall_timeout_s field and validation
    • AGENT_STALL_DETECTED event type
    • ReactAgent constructor params and StallMonitor instance
    • execute_agent signature
  • Full test suite: 1404 tests pass, 0 failures
  • Linting clean (ruff)

Implementation Notes

  • Uses daemon thread (doesn't block process exit)
  • Thread-safe activity tracking via threading.Lock
  • No database schema changes — stall_timeout_s is runtime-only
  • Conductor passes --stall-timeout via subprocess CLI args (same pattern as --engine)

Closes #399

Summary by CodeRabbit

  • New Features

    • Added stall detection for agents: if no tool calls occur within a configurable timeout (default 300s), execution is terminated and task moves to BLOCKED.
    • Emits a new AGENT_STALL_DETECTED event when a stall is detected.
    • Configurable via CLI flag --stall-timeout and config.yaml key agent_budget.stall_timeout_s.
  • Tests

    • Added comprehensive tests validating stall detection, monitor lifecycle, and integration.
  • Documentation

    • Updated agent docs describing stall behavior and configuration.

Test User added 4 commits March 7, 2026 21:43
Add StallMonitor watchdog that detects when agent stops making progress
(no tool calls for configurable duration). Thread-based daemon polls
at regular intervals and fires callback on stall.

- StallMonitor class with start/stop/notify_tool_executed API
- StallEvent dataclass for stall context
- stall_timeout_s config field (default 300s, 0=disabled)
- AGENT_STALL_DETECTED event type
- 17 unit tests covering timing, threading, config
Wire StallMonitor into the agent execution pipeline:
- ReactAgent: start/stop monitor around _react_loop, check stall flag
  at top of each iteration and verification retry, notify on tool success
- execute_agent: accept and forward stall_timeout_s parameter
- CLI: --stall-timeout flag on work start and batch run (default 300s)
- Conductor: forward stall_timeout_s through BatchRun and subprocess cmd
- On stall: creates blocker with context and returns BLOCKED status
Add stall detection section to Agent System docs covering CLI flag,
config key, and default behavior.
@coderabbitai

coderabbitai Bot commented Mar 8, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

Adds stall detection to agent execution: a background StallMonitor watches for periods with no tool calls and, after a configurable timeout (default 300s), triggers a stall event that transitions execution to BLOCKED and creates a blocker. Timeout configurable via CLI --stall-timeout and agent_budget.stall_timeout_s.

Changes

Cohort / File(s) Summary
Stall Detection Core
codeframe/core/stall_monitor.py
New StallEvent dataclass and StallMonitor class implementing a daemon watchdog thread, thread-safe activity tracking, notify API, and on-stall callback.
ReAct Agent Integration
codeframe/core/react_agent.py
Integrated StallMonitor into ReactAgent: constructor accepts stall_timeout_s, monitor lifecycle started/stopped around run, monitor notified after tool calls, _on_stall records event and triggers blocker/AGENT_STALL_DETECTED, and per-iteration stall checks short-circuit to BLOCKED.
Configuration & Events
codeframe/core/config.py, codeframe/core/events.py
Added stall_timeout_s: int = 300 to AgentBudgetConfig with non-negative validation; added AGENT_STALL_DETECTED event type constant.
Runtime & Conductor Propagation
codeframe/core/runtime.py, codeframe/core/conductor.py
Plumbed stall_timeout_s through APIs: execute_agent, start_batch, BatchRun data, and internal execution paths; subprocess invocations now pass --stall-timeout.
CLI Interface
codeframe/cli/app.py
Added --stall-timeout option (default 300) to work_start and batch_run, passing value into runtime and conductor calls.
Docs & Tests
AGENTS.md, tests/core/test_stall_monitor.py
Documentation updated to describe stall behavior and config; new comprehensive tests for StallMonitor, StallEvent, integration with ReactAgent, and runtime/CLI/config validation.

Sequence Diagram

sequenceDiagram
    participant Agent as ReactAgent
    participant Monitor as StallMonitor
    participant LLM as LLM Provider
    participant Watch as Watchdog Thread
    participant Blocker as Blocker Creator

    Agent->>Monitor: start(task_id)
    Monitor->>Watch: spawn watchdog (daemon)

    Agent->>LLM: request next action
    LLM->>Agent: return action
    Agent->>Agent: execute tool
    Agent->>Monitor: notify_tool_executed(task_id, iteration)
    Monitor->>Monitor: reset last_activity

    Note over LLM,Agent: LLM hangs or agent loops without tool calls

    Watch->>Monitor: poll elapsed time
    Monitor->>Monitor: elapsed > stall_timeout_s?
    Monitor->>Agent: invoke on_stall(StallEvent)

    Agent->>Blocker: create blocker with stall context
    Agent->>Agent: set _stall_triggered, emit AGENT_STALL_DETECTED
    Agent->>Agent: return BLOCKED status

    Agent->>Monitor: stop()
    Monitor->>Watch: terminate watchdog
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

Poem

🐰
When thoughts run in circles and nothing is done,
A quiet watchdog counts seconds one by one.
It hops in and tugs at the stalled agent's sleeve,
Creates a soft blocker and tells it to leave.
Hop-hop hooray — progress finds its sun!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.11% 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 'feat: Agent stall detection and recovery (#399)' accurately describes the main feature being implemented—adding stall detection and recovery to the agent execution system.
Linked Issues check ✅ Passed The PR implements all acceptance criteria from #399: stall detection monitor with configurable timeout (default 300s), stall defined as no tool call, integration with ReAct agent, CLI flag support (--stall-timeout), config support (agent_budget.stall_timeout_s), batch execution support, and comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are within scope: stall monitor module (StallMonitor, StallEvent), ReactAgent integration, CLI/config updates, event type addition, conductor batch propagation, and tests—all directly supporting stall detection/recovery from #399.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/issue-399-agent-stall-detection

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

@claude

claude Bot commented Mar 8, 2026

Copy link
Copy Markdown

Code Review - Agent Stall Detection and Recovery. Overall this is a clean, well-scoped implementation that follows the project core-first headless architecture. The StallMonitor is a standalone daemon thread with correct use of threading primitives, 22 unit tests cover the main behaviors, and conductor/CLI wiring is consistent. Issues: (1) Medium - stall detection does not interrupt a hanging LLM call. _stall_triggered is set from the watchdog thread but the main loop only checks it at the top of each iteration. If the LLM hangs for 600s the watchdog fires at 300s but the agent does not return BLOCKED until the LLM call completes. The stall timeout is a lower bound on detection not an upper bound on execution time - worth documenting in AGENTS.md. (2) Minor - only successful tool calls reset the timer. Error tool calls do not call notify_tool_executed, so an agent in an error-recovery loop still has its stall timer counting down. If this is intentional the name notify_tool_executed is misleading and a comment explaining the design choice would help. (3) Minor - stall_timeout_s type is inconsistent: AgentBudgetConfig uses int, StallMonitor and ReactAgent use float, runtime.execute_agent uses int. Python coerces these transparently but float throughout would match the sub-second usage in tests. (4) Minor - config.yaml integration may not be fully wired. AgentBudgetConfig.stall_timeout_s is defined but the diff does not show it flowing into execute_agent. The CLI flag path is wired, but the config file path may be a silent no-op. Nits: the 5 _execute_task_subprocess call sites in conductor.py are all updated consistently; stop() join timeout could in theory leave a ghost daemon thread on slow machines; timing-based tests with 0.2-0.3s windows may flake under heavy CI load. Strengths: daemon thread will not block exit, lock usage is minimal and correct, _stop_event.wait avoids busy-waiting, idempotent stop is tested, AGENTS.md docs are clear, 1404 tests pass. Most important to resolve before merge: issue 1 (document LLM-hang behavior) and issue 4 (verify config.yaml wiring).

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

Caution

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

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

460-520: ⚠️ Potential issue | 🟠 Major

Persist stall_timeout_s with the batch record.

Lines 460 and 520 only carry the timeout in memory. Reloaded batches are reconstructed via get_batch() / _row_to_batch(), but _save_batch() and the batch SELECTs still serialize only through engine, so any resumed or reloaded batch falls back to the dataclass default of 300. A batch started with --stall-timeout 0 or a custom value will therefore resume with the wrong stall behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codeframe/core/conductor.py` around lines 460 - 520, The batch's
stall_timeout_s is only set in memory in start_batch (BatchRun(...,
stall_timeout_s=...)) but not persisted; update persistence so resumed batches
keep the configured timeout: modify _save_batch to include stall_timeout_s when
writing the BatchRun record, update any INSERT/UPDATE SQL/ORM fields and the
SELECTs used by get_batch/_row_to_batch to read stall_timeout_s, and ensure
_row_to_batch maps the stored stall_timeout_s into the BatchRun dataclass; also
add any necessary DB schema change or default handling so older rows get a
sensible value.
🧹 Nitpick comments (1)
tests/core/test_stall_monitor.py (1)

91-145: These sleep-based timing assertions will be flaky in CI.

The sub-second sleep() windows here leave very little headroom for scheduler jitter in a background-thread test. Prefer waiting on a callback-owned threading.Event (or polling until a deadline) so these monitor tests stay stable on slower runners.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/test_stall_monitor.py` around lines 91 - 145, Replace fragile
time.sleep assertions in these tests by having the MagicMock callback set a
threading.Event (or use a small polling loop with deadline) and waiting on that
event with a generous timeout; specifically, in tests using StallMonitor
(test_stall_fires_callback_after_timeout, test_notify_prevents_stall,
test_disabled_when_timeout_zero, test_callback_receives_correct_iteration_count)
attach an Event to the on_stall callback so the test waits
event.wait(timeout=...) instead of time.sleep, then assert event.is_set() (or
not set) and inspect callback.call_args for the StallEvent and iteration count;
ensure notify_tool_executed("task-1", iteration=...) and monitor.start/stop
logic remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@codeframe/cli/app.py`:
- Around line 2001-2005: The CLI is forcing a concrete default for stall_timeout
which overrides workspace config.agent_budget.stall_timeout_s; change the typer
Option declarations for stall_timeout to use an "unset" sentinel (e.g., default
None / Optional[int]) so omitted flags are distinguishable from an explicit 300,
and propagate None downstream so core code can resolve using config defaults
(agent_budget.stall_timeout_s). Update every occurrence of the stall_timeout CLI
Option (the current stall_timeout parameters in app.py and the other occurrences
you noted) to be optional/unset at the boundary and ensure callers that forward
this value treat None as “use config” rather than a numeric timeout.

In `@codeframe/core/react_agent.py`:
- Around line 202-206: The stall monitor is stopped immediately after
_react_loop returns, so _run_final_verification (and any gates.run() or
verification retry loops) can hang without triggering stall recovery; keep the
monitor running until verification completes and ensure verification stalls are
routed through the same blocker/BLOCKED flow used by the main loop. Concretely:
move the call to self._stall_monitor.stop() to after _run_final_verification()
completes (or wrap both _react_loop(...) and self._run_final_verification(...)
inside the same try/finally guarded by self._stall_monitor.start(task_id)), and
update the verification path (the code that calls _run_final_verification and
any gates.run() loops) to create blocker entries and set the BLOCKED state using
the same blocker creation flow as the main loop so that the stall handler can
detect and recover from verification hangs.
- Around line 370-373: The stall monitor is only notified on successful tool
results (checks result.is_error) causing failing tool calls like
run_tests/run_command to not reset the stall timer; change the logic in
react_agent.py so that
self._stall_monitor.notify_tool_executed(self._current_task_id, iterations) is
called for every completed tool invocation (regardless of result.is_error)—i.e.,
move the notify_tool_executed call out of the result.is_error conditional so all
tool completions trigger the stall monitor update.

In `@codeframe/core/stall_monitor.py`:
- Around line 78-80: The start() initialization currently sets
self._last_activity = datetime.now(...), causing first-iteration stalls to
report a fake timestamp; change the initialization in start() (and the similar
block at lines 118-123) to set self._last_activity = None so "never executed a
tool" is preserved, and ensure the code paths that emit StallEvent (and any uses
of _last_activity) handle None and translate it to last_tool_call_at = None;
also ensure the actual tool-run path(s) (methods that update activity, e.g., the
function that records tool invocations) set self._last_activity to
datetime.now(timezone.utc) when a tool runs.

---

Outside diff comments:
In `@codeframe/core/conductor.py`:
- Around line 460-520: The batch's stall_timeout_s is only set in memory in
start_batch (BatchRun(..., stall_timeout_s=...)) but not persisted; update
persistence so resumed batches keep the configured timeout: modify _save_batch
to include stall_timeout_s when writing the BatchRun record, update any
INSERT/UPDATE SQL/ORM fields and the SELECTs used by get_batch/_row_to_batch to
read stall_timeout_s, and ensure _row_to_batch maps the stored stall_timeout_s
into the BatchRun dataclass; also add any necessary DB schema change or default
handling so older rows get a sensible value.

---

Nitpick comments:
In `@tests/core/test_stall_monitor.py`:
- Around line 91-145: Replace fragile time.sleep assertions in these tests by
having the MagicMock callback set a threading.Event (or use a small polling loop
with deadline) and waiting on that event with a generous timeout; specifically,
in tests using StallMonitor (test_stall_fires_callback_after_timeout,
test_notify_prevents_stall, test_disabled_when_timeout_zero,
test_callback_receives_correct_iteration_count) attach an Event to the on_stall
callback so the test waits event.wait(timeout=...) instead of time.sleep, then
assert event.is_set() (or not set) and inspect callback.call_args for the
StallEvent and iteration count; ensure notify_tool_executed("task-1",
iteration=...) and monitor.start/stop logic remain unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bbe58d1b-b653-4e80-98b9-9496505c57e2

📥 Commits

Reviewing files that changed from the base of the PR and between 705b160 and 339a681.

📒 Files selected for processing (9)
  • AGENTS.md
  • codeframe/cli/app.py
  • codeframe/core/conductor.py
  • codeframe/core/config.py
  • codeframe/core/events.py
  • codeframe/core/react_agent.py
  • codeframe/core/runtime.py
  • codeframe/core/stall_monitor.py
  • tests/core/test_stall_monitor.py

Comment thread codeframe/cli/app.py
Comment on lines +2001 to +2005
stall_timeout: int = typer.Option(
300,
"--stall-timeout",
help="Seconds without a tool call before agent is considered stalled (0 = disabled)",
),

@coderabbitai coderabbitai Bot Mar 8, 2026

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.

⚠️ Potential issue | 🟠 Major

The CLI default currently overrides workspace config.

Both commands give stall_timeout a concrete default and always pass it downstream, so omitting --stall-timeout is indistinguishable from explicitly choosing 300. That makes .codeframe/config.yaml's agent_budget.stall_timeout_s ineffective on the main CLI execution paths. Use an "unset" sentinel at the CLI boundary and let core resolve config/defaults.

Also applies to: 2068-2070, 2872-2876, 2965-2975

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codeframe/cli/app.py` around lines 2001 - 2005, The CLI is forcing a concrete
default for stall_timeout which overrides workspace
config.agent_budget.stall_timeout_s; change the typer Option declarations for
stall_timeout to use an "unset" sentinel (e.g., default None / Optional[int]) so
omitted flags are distinguishable from an explicit 300, and propagate None
downstream so core code can resolve using config defaults
(agent_budget.stall_timeout_s). Update every occurrence of the stall_timeout CLI
Option (the current stall_timeout parameters in app.py and the other occurrences
you noted) to be optional/unset at the boundary and ensure callers that forward
this value treat None as “use config” rather than a numeric timeout.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Valid concern. The CLI default pattern matches how --engine works today (also has a concrete default that overrides config). For now this is acceptable — the config precedence layer (issue #399 step 6 in the plan) was intentionally deferred as YAGNI. If users actually set stall_timeout_s in config and expect it to be respected from CLI, we can add sentinel-based detection in a follow-up.

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.

@frankbria, understood — the concrete default matches the existing --engine precedence pattern and the config-first resolution is a planned future concern. I'll note this for future reviews.

Would you like me to open a follow-up issue to track the sentinel-based CLI/config precedence work (config → CLI default → explicit CLI flag) when the time comes?


✏️ Learnings added
Learnt from: frankbria
Repo: frankbria/codeframe PR: 423
File: codeframe/cli/app.py:2001-2005
Timestamp: 2026-03-08T05:05:03.452Z
Learning: In codeframe/cli/app.py, the `--stall-timeout` CLI option (and `--engine`) intentionally uses a concrete default (300s) that overrides workspace config (`agent_budget.stall_timeout_s`). Config-first precedence resolution (sentinel-based detection where `None` means "use config") is deferred as YAGNI and tracked as a follow-up for a future PR. Do not flag this pattern as a bug.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
Repo: frankbria/codeframe PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-19T17:24:27.822Z
Learning: Applies to codeframe/core/runtime.py : Default execution engine for agent tasks must be ReAct (Reasoning + Acting) with iterative tool-use loop (observe → think → act). Legacy plan-based engine available via `--engine plan` flag only.

Learnt from: CR
Repo: frankbria/codeframe PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-19T17:24:51.290Z
Learning: Applies to codeframe/cli/app.py : Use `--engine react` (default ReAct engine) for most tasks; `--engine plan` (legacy) only for well-defined, predictable tasks

Comment thread codeframe/core/react_agent.py Outdated
Comment on lines +370 to +373
if not result.is_error:
self._stall_monitor.notify_tool_executed(
self._current_task_id, iterations,
)

@coderabbitai coderabbitai Bot Mar 8, 2026

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.

⚠️ Potential issue | 🟠 Major

Failing tool calls still count as activity.

The feature defines a stall as "no tool calls", but the timer is only reset for non-error results here. If the agent is actively iterating on failing run_tests or run_command calls, it will eventually be misclassified as stalled and blocked. Notify the monitor after every completed tool call, not only successful ones.

Minimal fix
                 result = self._execute_tool_with_lint(tc)
-
-                if not result.is_error:
-                    self._stall_monitor.notify_tool_executed(
-                        self._current_task_id, iterations,
-                    )
+                self._stall_monitor.notify_tool_executed(
+                    self._current_task_id,
+                    iterations,
+                )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codeframe/core/react_agent.py` around lines 370 - 373, The stall monitor is
only notified on successful tool results (checks result.is_error) causing
failing tool calls like run_tests/run_command to not reset the stall timer;
change the logic in react_agent.py so that
self._stall_monitor.notify_tool_executed(self._current_task_id, iterations) is
called for every completed tool invocation (regardless of result.is_error)—i.e.,
move the notify_tool_executed call out of the result.is_error conditional so all
tool completions trigger the stall monitor update.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This is intentional design. A stall means the agent is not doing anything (e.g., LLM API hangs or agent loops without acting). When the agent is actively executing tools — even failing ones — it IS making progress (discovering errors, trying fixes). The existing fix_tracker and escalation system handles repeated failures. Stall detection is specifically for the case where nothing happens at all.

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.

@frankbria, understood — that's a clear and well-reasoned separation of concerns. Stall detection (no tool calls at all, e.g., LLM hang) vs. repeated failure recovery (fix_tracker + escalation) are orthogonal problems handled by distinct systems. I'll withdraw the concern.


✏️ Learnings added
Learnt from: frankbria
Repo: frankbria/codeframe PR: 423
File: codeframe/core/react_agent.py:370-373
Timestamp: 2026-03-08T05:05:00.413Z
Learning: In `codeframe/core/react_agent.py`, `StallMonitor.notify_tool_executed()` is intentionally called only on *successful* tool results (`if not result.is_error`). A "stall" is defined as the agent executing zero tool calls (e.g., LLM API hang or agent looping without acting). Failing tool calls still constitute activity and are handled by the separate `fix_tracker` / escalation system. Do not flag this conditional as a bug.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
Repo: frankbria/codeframe PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-19T17:24:51.290Z
Learning: Applies to codeframe/core/react_agent.py : ReAct agent must implement the observe → think → act loop using the 7 defined tools: read_file, edit_file, create_file, run_command, run_tests, search_codebase, list_files

Learnt from: CR
Repo: frankbria/codeframe PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-19T17:24:27.822Z
Learning: Applies to codeframe/core/{react_agent,tools}.py : ReactAgent implementation must use 7 structured tools: `read_file`, `edit_file`, `create_file`, `run_command`, `run_tests`, `search_codebase`, `list_files`. These tools are defined in `core/tools.py` and must be called via tool-use protocol.

Learnt from: CR
Repo: frankbria/codeframe PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-19T17:24:27.822Z
Learning: Applies to codeframe/core/runtime.py : Default execution engine for agent tasks must be ReAct (Reasoning + Acting) with iterative tool-use loop (observe → think → act). Legacy plan-based engine available via `--engine plan` flag only.

Learnt from: CR
Repo: frankbria/codeframe PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-19T17:24:51.290Z
Learning: Applies to codeframe/core/{agent,react_agent,runtime}.py : Agent state transitions (IDLE, PLANNING, EXECUTING, BLOCKED, COMPLETED, FAILED) must be managed by the Agent class; runtime handles TaskStatus transitions (BACKLOG, READY, IN_PROGRESS, DONE, BLOCKED, FAILED). Agent must NOT call `tasks.update_status()` directly.

Learnt from: CR
Repo: frankbria/codeframe PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-19T17:24:51.290Z
Learning: Applies to codeframe/core/agent.py : Do NOT update task status from agent.py; runtime handles all TaskStatus transitions based on agent state

Learnt from: frankbria
Repo: frankbria/codeframe PR: 360
File: codeframe/core/tools.py:651-735
Timestamp: 2026-02-09T02:52:03.968Z
Learning: In codeframe/core/tools.py, agent tool functions (read_file, list_files, search_codebase, edit_file, create_file, run_tests, run_command) must be stateless with signature (input_data: dict, workspace_path: Path, tool_call_id: str) -> ToolResult and must not emit events. They are separate from verification gates in codeframe/core/gates.py. Gate diagnostic events (GATES_STARTED, GATES_COMPLETED) belong in the verification pipeline (gates.run()), not inside individual tools. If tool observability is required, implement it at the execute_tool dispatcher level, not within the individual tool implementations.

Learnt from: CR
Repo: frankbria/codeframe PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-19T17:24:27.822Z
Learning: Applies to codeframe/core/{gates,agent,react_agent}.py : Verification gates must run incrementally after file changes (ruff) and finally (pytest, ruff, BUILD). Self-correction loop retries failed gates up to 5 times for ReAct and 3 times for plan engine with pattern-based quick fixes before LLM intervention.

Learnt from: CR
Repo: frankbria/codeframe PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-19T17:24:27.822Z
Learning: Applies to codeframe/core/{diagnostics,diagnostic_agent}.py : Failed task diagnosis via `cf work diagnose <task-id>` must use AI-powered analysis via `core/diagnostic_agent.py`. Analyze error logs, failed verification gates, and previous attempts to suggest root causes and recovery actions.

Learnt from: CR
Repo: frankbria/codeframe PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-19T17:24:27.822Z
Learning: Applies to codeframe/core/{fix_tracker,quick_fixes}.py : Self-correction loop must track fix attempts via `core/fix_tracker.py` to prevent repeating failed fixes. Normalize errors for comparison and detect escalation patterns (same error/file 3+ times). Use pattern-based quick fixes via `core/quick_fixes.py` before LLM intervention.

Learnt from: frankbria
Repo: frankbria/codeframe PR: 128
File: tests/agents/test_bottleneck_detection.py:486-500
Timestamp: 2025-12-17T19:21:40.014Z
Learning: In tests/agents/test_bottleneck_detection.py, test_skip_agents_below_threshold should be async and mock _get_agent_workload to return workload below AGENT_OVERLOAD_THRESHOLD (5) while providing non-empty tasks list to prevent early return in detect_bottlenecks().

Comment thread codeframe/core/stall_monitor.py
- Extend stall monitor lifetime to cover final verification (not just
  the react loop), so hangs during gates.run() are also detected
- Track last_tool_call_at separately from last_activity so initial
  stalls correctly report None for last tool call timestamp
- Handle stall_detected reason from final verification
@claude

claude Bot commented Mar 8, 2026

Copy link
Copy Markdown

PR #423 Review: Agent Stall Detection and Recovery

Overall this is a clean, well-scoped implementation. The architecture follows established patterns (headless core, daemon thread, event emission) and test coverage is solid. A few issues worth addressing before merge.


Issues

1. Config file to runtime wiring appears incomplete

AgentBudgetConfig.stall_timeout_s is added to config.py with validation, but I do not see it being read and forwarded to execute_agent(stall_timeout_s=...) in runtime.py. The CLI flag works, but if a user sets agent_budget.stall_timeout_s in .codeframe/config.yaml, it will be silently ignored and the 300s default will be used instead. This makes the config feature listed in the PR description and AGENTS.md a no-op unless there is wiring elsewhere not shown in the diff.

2. Stall timer only resets on successful tool calls

In react_agent.py:

if not result.is_error:
    self._stall_monitor.notify_tool_executed(...)

If the agent is actively attempting error recovery (retrying a failing tool, running ruff fixes, etc.), the stall timer will not reset even though the agent is making real progress. This risks false stall detections during intensive self-correction loops. Consider resetting on any tool call attempt, since a stall should mean "no tool calls attempted", not "no successful tool calls".

3. Type inconsistency: float vs int for stall_timeout_s

  • StallMonitor.__init__ and ReactAgent.__init__ accept float
  • runtime.execute_agent, conductor.BatchRun, AgentBudgetConfig, and CLI all use int

Pick one. If sub-second granularity is needed (the tests use 0.2, 0.3), float makes more sense throughout. Alternatively, keep int in the public API and accept float internally in StallMonitor.


Minor Notes

Timing-sensitive tests may be flaky in CI

test_stall_fires_callback_after_timeout uses a 0.2s timeout and waits 0.5s — tight for a loaded CI environment. test_notify_prevents_stall sleeps 0.1s x 6 while threshold is 0.3s, giving very little margin. Consider doubling the sleep buffers or using condition variables instead of fixed waits.

No stall support for the plan engine

Clearly intentional (plan engine is legacy), but worth a note in AGENTS.md stating stall detection applies to the ReAct engine only.


What is Working Well

  • Daemon thread design is correct — won't block process exit
  • threading.Lock usage is minimal and correct (no deadlock risk)
  • _stall_triggered checked at both the top of _react_loop iterations and in _run_final_verification — good coverage of both the main loop and the retry path
  • stall_monitor.stop() in finally block ensures cleanup even on exception
  • stop_event.set() after callback prevents double-firing
  • Conductor correctly propagates stall_timeout_s to all _execute_task_subprocess call sites including the supervisor-retry branches
  • Test suite is well-organized with pytestmark = pytest.mark.v2 and covers the main behavioral paths

The critical items are issue 2 (false stalls during error recovery) and confirming issue 1 (config wiring). Type consistency is a polish item.

@frankbria
frankbria merged commit a992b7e into main Mar 8, 2026
33 of 34 checks passed
@frankbria
frankbria deleted the feature/issue-399-agent-stall-detection branch March 8, 2026 05:11
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 2.5] Agent Stall Detection and Recovery

1 participant