Skip to content

refactor(supervisor): dispatch work items in-process instead of re-invoking agentflare work - #402

Merged
getappz merged 6 commits into
masterfrom
task/19
Aug 8, 2026
Merged

refactor(supervisor): dispatch work items in-process instead of re-invoking agentflare work#402
getappz merged 6 commits into
masterfrom
task/19

Conversation

@getappz

@getappz getappz commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Summary

Depends on #401 — targeted at that branch so this diff only shows item #19's incremental work; retarget to master once #401 merges.

  • Flattens the daemon's dispatch from 3 layers (daemon → agentflare-jobs::Supervisor-wrapped agentflare work OS subprocess → agent CLI subprocess) to 2: the daemon now runs claim/resolve-agent/build-prompt/done logic in-process, spawning a real subprocess only for the one genuinely open-ended part — the agent CLI itself (unchanged: still agent_launch::run_captured, with feat(work): idle-timeout instead of fixed wall-clock timeout for dispatched work #401's hard-cap/idle-timeout kill).
  • Extracted run_work's body into execute_work (src/cli/work.rs), now taking a &mut dyn Write instead of println!, so the identical logic runs for a human's agentflare work invocation and for the daemon's in-process WorkItemExecutor. Progress lands in that job's {id}.stdout log file — same path Supervisor already used — so the dashboard's live log tail keeps working unchanged.
  • agentflare-jobs gained an InProcessExecutor trait (defined there so the crate never depends upward on the binary implementing it) and AgentJob::in_process; WorkerPool branches per job, and the existing subprocess path (used by ad-hoc POST /api/jobs submissions) is fully unchanged. An in-process job's executor call runs on a short-lived thread with job.timeout_secs as a watchdog, so a stuck claim/worktree/done step — which loses the OS-level SIGKILL backstop a subprocess had — can't wedge a pooled worker thread forever (the stuck thread itself leaks rather than being force-killed; there's no safe way to kill a thread in Rust — a real, deliberate, documented trade-off).

Correctness issues found and fixed while moving multiple work items into worker threads sharing one process

Rather than serializing dispatch back down to one job at a time to sidestep these:

  • Claim/comment/done authorship raced on AGENTFLARE_AGENT (a process-global env var two worker threads dispatching different agents would clobber). Added claims::with_owner_override — a thread-local owner_id() checks first, so each worker thread's identity stays independent. cli/handoff.rs's sender resolution duplicated the same env read separately; unified it through claims::owner_id() so it picks up the override for free.
  • The agent CLI subprocess relied on inheriting AGENTFLARE_AGENT from ambient parent env for flare-git-shim's bypass classification when the agent shells out to git — correct for a subprocess-per-dispatch caller, wrong once the daemon's own ambient env can't be right for every concurrently-running job. agent_launch::run_headless now sets it explicitly on the spawned Command.

Test plan

  • cargo build --workspace — clean
  • cargo test -p agentflare -p agentflare-jobs --lib --bins — 1136+1 passed
  • cargo test -p agentflare-jobs --tests — all existing subprocess-path tests (queue/supervisor/worker) unchanged and passing, proving that path is untouched
  • New crates/agentflare-jobs/tests/in_process_test.rs (4 tests): executor dispatch + log capture, failure-message propagation, fail-fast with no executor registered, stuck-job watchdog
  • New claims::tests (4 tests) proving the owner-override: correct value, scope doesn't leak, has_owner_override reflects state, and 8 concurrent threads never observe each other's override
  • cargo test -p agentflare --bin agentflare -- handoff:: agent_launch:: cli::work:: supervisor:: — 53 passed
  • cargo clippy -p agentflare -p agentflare-jobs --all-targets — no new warnings

…voking `agentflare work`

Flattens the daemon's 3-layer dispatch (daemon -> agentflare-jobs::Supervisor
-wrapped `agentflare work` OS subprocess -> agent CLI subprocess) to 2 layers:
the daemon now runs a work item's claim/resolve-agent/build-prompt/done logic
in-process, spawning a real subprocess only for the one genuinely open-ended
part -- the agent CLI itself (still via agent_launch::run_captured, with its
own hard-cap/idle-timeout kill from item #20).

Extracted `run_work`'s body into `execute_work` (src/cli/work.rs), which now
takes a `&mut dyn Write` for progress output instead of println!, so the same
logic works identically for a human running `agentflare work` and for
`WorkItemExecutor` running in-process. Progress lands in that job's own
`{id}.stdout` log file -- the same path/name Supervisor already used for
subprocess jobs -- so the dashboard's existing live log tail (`/api/jobs/:id
/stream`) keeps working unchanged.

agentflare-jobs gained an `InProcessExecutor` trait (defined there so the
crate never depends upward on the binary that implements it) and an
`AgentJob::in_process` flag; `WorkerPool` branches on it per job, keeping the
existing subprocess path (used by ad-hoc `POST /api/jobs` submissions) fully
intact and unchanged. An in-process job's executor call runs on a short-lived
thread with `job.timeout_secs` as a watchdog, so a stuck claim/worktree/done
step -- which loses the OS-level SIGKILL backstop a subprocess had -- can't
wedge a pooled worker thread forever; the stuck thread itself is abandoned
(no safe way to force-kill a thread in Rust) rather than truly killed, a
real, deliberate trade-off called out where it's implemented.

Two correctness issues surfaced by moving multiple work items into worker
threads sharing one process, both fixed narrowly rather than serializing
dispatch back down to one job at a time:

- claim/comment/done authorship (`claims::owner_id()`) read the
  process-global `AGENTFLARE_AGENT` env var, which two worker threads
  dispatching different agents would race on. Added a thread-local override
  (`claims::with_owner_override`) that `owner_id()` checks first -- each
  worker thread's identity stays independent, no shared mutable state.
  `cli/handoff.rs`'s sender resolution duplicated the same env read
  separately; unified it through `claims::owner_id()` so it picks up the
  override too instead of needing its own fix.
- the agent CLI subprocess itself relied on inheriting `AGENTFLARE_AGENT`
  from its parent's ambient env for `flare-git-shim`'s bypass classification
  when the agent shells out to git -- correct for a subprocess-per-dispatch
  caller, wrong for a daemon whose own ambient env can't be right for every
  concurrently-running job. `agent_launch::run_headless` now sets it
  explicitly on the spawned Command instead of relying on inheritance.

`dispatch_item` (src/supervisor.rs) now enqueues an in-process job with
`[item_id, agent]` args instead of `agentflare work <item> --agent <agent>`;
the outer job timeout (aligned to work's hard cap by the previous commit)
still applies, now as the in-process watchdog rather than a subprocess kill.
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 1 minute

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

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.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bbfd9c07-2a90-42ae-b218-4cc4ef5fa8a5

📥 Commits

Reviewing files that changed from the base of the PR and between 10d93fb and 015ea47.

📒 Files selected for processing (13)
  • crates/agentflare-jobs/src/executor.rs
  • crates/agentflare-jobs/src/lib.rs
  • crates/agentflare-jobs/src/queue.rs
  • crates/agentflare-jobs/src/types.rs
  • crates/agentflare-jobs/src/worker.rs
  • crates/agentflare-jobs/tests/in_process_test.rs
  • src/agent_launch.rs
  • src/claims.rs
  • src/cli/handoff.rs
  • src/cli/mod.rs
  • src/cli/work.rs
  • src/dashboard/server.rs
  • src/supervisor.rs

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

getappz pushed a commit that referenced this pull request Aug 8, 2026
…rap()

Unrelated to this branch's actual change -- fixing it here because it's
been intermittently failing build (windows-latest), a required status
check, blocking this PR (and #402, based on it) from reaching green.
Confirmed pre-existing on master's own HEAD, not something either PR
introduced: a different one of these tests failed on each of three
consecutive Windows CI reruns, always at the same remove_dir_all(&dir)
cleanup line, after the test's real assertions had already passed.

Classic Windows CI flake: a file just written/renamed can stay
transiently locked for a few ms (real-time AV scanning) after the code
under test is done with it, so an immediate remove_dir_all can hit
PermissionDenied through no fault of the test's own logic.
temp_dir_for_test's own pre-cleanup already treats this as best-effort
(`let _ = ...`); these end-of-test cleanups asserted success instead,
so the flake surfaced as a spurious test failure with nothing left to
actually assert by that point in the test.
Base automatically changed from task/20 to master August 8, 2026 08:51
@getappz
getappz enabled auto-merge August 8, 2026 08:54
@getappz
getappz merged commit c5649bd into master Aug 8, 2026
16 checks passed
@getappz
getappz deleted the task/19 branch August 8, 2026 09:01
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