Skip to content

fix(agent): sweep orphaned agent runs on startup - #5261

Open
yh928 wants to merge 3 commits into
tinyhumansai:mainfrom
yh928:fix/startup-run-sweep
Open

fix(agent): sweep orphaned agent runs on startup#5261
yh928 wants to merge 3 commits into
tinyhumansai:mainfrom
yh928:fix/startup-run-sweep

Conversation

@yh928

@yh928 yh928 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

The core is a single in-process runtime. When it exits — a crash, a restart, or a deploy — every agent run still in a non-terminal state (Pending / Running / Interrupted) in the durable status store is orphaned: the task that owned it is gone, no executor remains to advance it, and no cancel path can reach it. Yet the run stays in the openhuman.agent_runs_active listing forever. A long-lived instance was observed carrying 50 such zombies.

This adds a startup sweep that reconciles the persistent status store against that reality.

What it does

tinyagents::reaper::reap_orphaned_runs(workspace) lists the still-active runs in the persistent FileStatusStore and moves each to the terminal Cancelled state, recording a stable, grep-friendly error (run orphaned: core restarted while the run was in flight).

It is wired into start_boot_once_jobs, right after the legacy migrations — before readiness is published and regardless of ServiceSet — so the very first agent_runs_active read reflects reality rather than a graveyard of the previous process's in-flight work.

Design notes

  • Cancelled, not Failed — a restart is not a run failure, and Failed would render every reaped run as a red error row in the UI. tinyagents exposes mark_completed / mark_failed / mark_interrupted but no mark_cancelled; HarnessRunStatus's fields are public, so the terminal state is set directly rather than taking a cross-repo dependency on a new helper (mirrors exactly the field writes mark_failed performs: terminal status + Done phase + error + ended_at/updated_at).
  • Own module, so the read side stays read-only — the replay/status controllers (tinyagents::replay) are documented as strict readers over the status seam. The sweep is the one writer, so it lives in tinyagents::reaper rather than polluting that contract.
  • Best-effort, never blocks boot — a failure to open/list the store logs and yields 0; a per-run persist failure is logged and the sweep continues.
  • Idempotent — a clean listing reaps nothing; a second sweep over an already-swept store is a no-op (pinned by a test).

Tests

cargo test -p openhuman --lib openhuman::tinyagents::reaper — two tests:

  • reap_cancels_every_active_run_and_spares_terminal_ones: seeds Pending/Running/Interrupted + one Completed run, asserts the three non-terminal runs become Cancelled with the reason and an end time, the Completed run is untouched, the active listing empties, and a re-sweep reaps 0.
  • reap_on_empty_workspace_is_a_noop: a workspace that never hosted a run reaps nothing and does not error.

Summary by CodeRabbit

  • Bug Fixes
    • Improved startup recovery by automatically identifying and cancelling unfinished agent runs from previous sessions.
    • Preserved completed runs while safely handling empty or unavailable workspaces without blocking startup.
    • Added consistent cancellation reasons and completion timestamps for recovered runs.
    • Ensured repeated startup checks are safe and do not alter runs that have already reached a completed state.

Closes #5298

@yh928
yh928 requested a review from a team July 29, 2026 06:39
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The runtime now reconciles durable non-terminal agent runs during CoreBuilder::build. The reaper marks orphaned runs as Cancelled, records a stable reason and timestamps, tolerates store failures, and includes unit and integration coverage.

Changes

Orphaned run reaping

Layer / File(s) Summary
Reaper implementation and validation
src/openhuman/agent/tinyagents/reaper.rs
The reaper lists active runs, marks them Cancelled and Done, records the orphan reason and timestamps, and validates reconciliation, preservation, idempotency, and empty workspaces.
Boot-time reaper integration
src/openhuman/agent/tinyagents/mod.rs, src/core/runtime/builder.rs, src/core/runtime/services.rs
The reaper is registered and invoked during CoreBuilder::build. Comments document that reconciliation applies to build-only runtimes and served runtimes. Integration coverage validates the boot path.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CoreBuilder
  participant reap_orphaned_runs
  participant DurableStatusStore
  CoreBuilder->>reap_orphaned_runs: reconcile configured workspace
  reap_orphaned_runs->>DurableStatusStore: list active runs
  DurableStatusStore-->>reap_orphaned_runs: pending, running, or interrupted runs
  reap_orphaned_runs->>DurableStatusStore: save cancelled terminal status
  reap_orphaned_runs-->>CoreBuilder: return reaped count
Loading

Possibly related PRs

Suggested labels: rust-core, agent, bug

Poem

I sweep the runs at break of day,
Mark stale work Cancelled away.
Timestamps rest in tidy rows,
While startup calmly onward goes.
The active list is clean. 🐇

🚥 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 describes the startup sweep for orphaned agent runs, which is the primary change.
Linked Issues check ✅ Passed The changes implement startup reconciliation, best-effort failure handling, idempotency, terminal-state preservation, runtime wiring, and regression tests for issue #5298.
Out of Scope Changes check ✅ Passed All changes support orphaned-run reconciliation, its startup integration, documentation, module wiring, or related regression coverage.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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.

@coderabbitai coderabbitai Bot added agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. labels Jul 29, 2026
Comment thread src/openhuman/tinyagents/reaper.rs Outdated
@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes the "zombie run" problem where agent runs stuck in Pending, Running, or Interrupted states from a previous process were never cleaned up. The fix adds a startup sweep (reap_orphaned_runs) that transitions all still-active runs to Cancelled with a stable, grep-friendly error before the runtime becomes queryable.

  • Placement in build() (not serve()): The sweep runs in CoreBuilder::build() before CoreRuntime is returned, so build-only embedders that never call serve() — and whose first dispatchable RPC is agent_runs_active — still see a clean listing.
  • Best-effort design: Any per-run or store-level failure is logged and the sweep continues; a store that can't be opened yields 0 rather than blocking startup.
  • Well-tested: Three tests cover the primary scenario (Pending/Running/Interrupted → Cancelled, Completed untouched, idempotent re-sweep), the empty-workspace no-op path, and an integration test that pins the sweep to the build() path with a real CoreBuilder.

Confidence Score: 5/5

Safe to merge; the sweep is best-effort, runs before the runtime is returned to any caller, and is fully covered by tests including a wiring integration test.

The change is narrowly scoped: a new read-then-write sweep over the durable status store at startup, gated on a workspace config being present. Errors at every layer (store open, list, per-run persist) are caught and logged rather than propagated, so a broken store cannot block boot. The list_active() filter correctly excludes already-terminal runs, verified by the test that seeds a Completed run and asserts it survives the sweep unchanged. Placement in build() rather than serve() is intentional and pinned by an integration test. No existing code paths are altered.

Files Needing Attention: No files require special attention.

Important Files Changed

Filename Overview
src/openhuman/agent/tinyagents/reaper.rs New module implementing the startup orphan sweep; well-structured with best-effort error handling, idempotent logic, and three targeted tests covering all non-terminal states, the empty-workspace path, and the build-path wiring.
src/core/runtime/builder.rs Correctly places the sweep in build() after CoreContext::init() and before CoreRuntime is returned; gated on config.as_ref() so workspaceless builds skip it; return value intentionally discarded since logging is handled inside reap_orphaned_runs.
src/core/runtime/services.rs Adds only an explanatory comment in start_boot_once_jobs clarifying why the sweep is absent here; no logic changes.
src/openhuman/agent/tinyagents/mod.rs One-line module declaration exposing the reaper at pub(crate) visibility, consistent with adjacent modules.

Sequence Diagram

sequenceDiagram
    participant E as Embedder / serve()
    participant CB as CoreBuilder::build()
    participant CI as CoreContext::init()
    participant R as reaper::reap_orphaned_runs()
    participant S as FileStatusStore

    E->>CB: build().await
    CB->>CI: CoreContext::init(...)
    CI-->>CB: (ctx, token, config)
    CB->>R: "reap_orphaned_runs(&cfg.workspace_dir).await"
    R->>S: list_active()
    S-->>R: [Pending, Running, Interrupted runs]
    loop For each orphaned run
        R->>R: mark_orphaned(status) → Cancelled + Done + error + ended_at
        R->>S: put_status(status)
        S-->>R: Ok(())
    end
    R->>R: log::info! reaped N orphaned run(s)
    R-->>CB: usize (ignored)
    CB-->>E: Ok(CoreRuntime)
    Note over E,S: First agent_runs_active RPC now sees a clean listing
Loading

Reviews (4): Last reviewed commit: "fix(agent): sweep on the build path, and..." | Re-trigger Greptile

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

🤖 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/core/runtime/services.rs`:
- Around line 335-345: Add a boot-path test covering start_boot_once_jobs with
optional services disabled: seed an active agent run, invoke the boot sequence,
and assert the run is cancelled. Reuse the existing reaper test setup and verify
the unconditional startup sweep rather than calling reap_orphaned_runs directly.
🪄 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 Plus

Run ID: 7e5f27d7-e1a0-4635-bc18-a1decd8ebe74

📥 Commits

Reviewing files that changed from the base of the PR and between 8072f08 and 6fb468b.

📒 Files selected for processing (3)
  • src/core/runtime/services.rs
  • src/openhuman/tinyagents/mod.rs
  • src/openhuman/tinyagents/reaper.rs

Comment thread src/core/runtime/services.rs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6fb468b61b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/core/runtime/services.rs Outdated
@yh928

yh928 commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 1ce6eb7a8 — all three findings applied.

Build-only runtimes (codex P2) — fixed, and it was the more serious of the three. You were right that start_boot_once_jobs only runs from serve(), so the documented build()-then-invoke() embedder never reached the sweep — and agent_runs_active is dispatchable the moment build() returns, so exactly that caller kept reading the previous process's graveyard. The sweep now runs in CoreBuilder::build(), which every runtime goes through (CLI, TUI, embedded, desktop), and is no longer duplicated in the boot-once jobs.

Boot-path test (CodeRabbit) — added, and it landed on the same path this fix moved to: a_build_only_runtime_is_swept_before_it_can_be_invoked seeds a Running run, builds with ServiceSet::none() + DomainSet::harness(), and asserts the run is Cancelled with the orphan reason. Two things the first draft got wrong, worth noting since they would have made it a false-positive test: OPENHUMAN_WORKSPACE is process-global (it now takes TEST_ENV_LOCK, as the approval/config tests do), and the resolved workspace is <OPENHUMAN_WORKSPACE>/workspace, not the env value — so the test also asserts the build resolved the directory it seeded, rather than passing by sweeping somewhere else.

Duplicate info log (greptile) — fixed. Dropped the caller's summary rather than demoting the module's, since the module line carries the count and the module context. The pre-sweep "N orphaned run(s) to reap" line is now debug, and the summary only fires when reaped > 0 — a clean boot, which is the normal case, now says nothing at info.

Tests: reaper 3 green.

@greptile-apps

greptile-apps Bot commented Jul 31, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

@yh928
yh928 force-pushed the fix/startup-run-sweep branch from 1ce6eb7 to 8f5cd17 Compare July 31, 2026 14:32
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 31, 2026
The core is a single in-process runtime, so when it exits — crash, restart,
or deploy — every run still Pending/Running/Interrupted in the durable status
store is orphaned: no executor remains to advance it, and no cancel path can
reach it (the owning task is gone). Yet it stays in the `agent_runs_active`
listing forever. A long-lived instance was observed carrying 50 such zombies.

Add a startup sweep (`tinyagents::reaper::reap_orphaned_runs`) that lists the
still-active runs in the persistent `FileStatusStore` and moves each to the
terminal `Cancelled` state with a stable, grep-friendly error. It runs in
`start_boot_once_jobs` right after the legacy migrations — before readiness is
published and regardless of ServiceSet — so the first active-runs read reflects
reality. Best-effort: a list/persist failure is logged and never blocks boot,
and the sweep is idempotent (a clean listing reaps nothing).

`Cancelled` (not `Failed`) is deliberate — a restart is not a run failure, and
`Failed` would render every reaped run as a red error row. tinyagents has no
`mark_cancelled`, but `HarnessRunStatus`'s fields are public, so the terminal
state is set directly rather than taking a cross-repo dependency; the writer
lives in its own module so the replay/status controllers stay read-only.
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

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

🤖 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/openhuman/agent/tinyagents/reaper.rs`:
- Around line 39-100: Update reap_orphaned_runs and mark_orphaned to follow the
domain-log contract: generate one opaque sweep_id at sweep entry, use [domain]
or [rpc] for every sweep log, and emit a debug start event containing the
sweep_id. Include sweep_id in all subsequent events, and before put_status log
each transition with run_id, from_status, and to_status=Cancelled; preserve the
existing reaping behavior and counts.
🪄 Autofix

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 Plus

Run ID: f52d5134-9d78-420b-bbdb-708bf9e9dccf

📥 Commits

Reviewing files that changed from the base of the PR and between d75b0a4 and f69d42f.

📒 Files selected for processing (4)
  • src/core/runtime/builder.rs
  • src/core/runtime/services.rs
  • src/openhuman/agent/tinyagents/mod.rs
  • src/openhuman/agent/tinyagents/reaper.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/core/runtime/builder.rs
  • src/core/runtime/services.rs

Comment thread src/openhuman/agent/tinyagents/reaper.rs
`start_boot_once_jobs` runs from `serve()`, so an embedder that only calls
`CoreBuilder::build()` and then `invoke()` never reached the sweep — and
`openhuman.agent_runs_active` is dispatchable the moment `build()` returns, so
exactly that caller kept reading the previous process's graveyard. The sweep
moves into `build()`, which every runtime goes through.

Logging: the reaper and its caller each emitted an info line for the same
count, and the reaper announced a sweep on every clean boot. One info line now,
only when something was reaped.

Tests: `a_build_only_runtime_is_swept_before_it_can_be_invoked` seeds a running
run, builds with `ServiceSet::none()` + `DomainSet::harness()`, and asserts the
run is cancelled — taking `TEST_ENV_LOCK` because `OPENHUMAN_WORKSPACE` is
process-global, and asserting the build resolved the workspace it seeded so the
test cannot pass by sweeping somewhere else. reaper 3 green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SRSNnqQsokuGmkbpLoLCGy
@yh928
yh928 force-pushed the fix/startup-run-sweep branch from f69d42f to dc0dfb1 Compare August 5, 2026 02:41

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

yh928 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

yh928 added a commit to yh928/openhuman that referenced this pull request Aug 5, 2026
…log lines

The sweep logged progress but nothing tied its lines together, and a restart
loop is exactly when two sweeps interleave with no way to tell whose line is
whose. Adds a `sweep_id` (millisecond clock, so a log tail stays ordered)
carried by every line, plus an entry event with the workspace, an exit event on
each of the four branches, and the state transition spelled out
(`Running->Cancelled phase=Done`) rather than left as "reaped".

tinyagents::reaper tests pass.

Reported by CodeRabbit on tinyhumansai#5261.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SRSNnqQsokuGmkbpLoLCGy

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

yh928 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

…log lines

The sweep logged progress but nothing tied its lines together, and a restart
loop is exactly when two sweeps interleave with no way to tell whose line is
whose. Adds a `sweep_id` (millisecond clock, so a log tail stays ordered)
carried by every line, plus an entry event with the workspace, an exit event on
each of the four branches, and the state transition spelled out
(`Running->Cancelled phase=Done`) rather than left as "reaped".

tinyagents::reaper tests pass.

Reported by CodeRabbit on tinyhumansai#5261.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SRSNnqQsokuGmkbpLoLCGy
@yh928
yh928 force-pushed the fix/startup-run-sweep branch from 6c61012 to 7bcfc34 Compare August 5, 2026 07:36

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

yh928 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. bug rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure.

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

Agent runs left in flight by a core restart stay in the active listing forever

1 participant