Skip to content

DOS-825: reclaim wedged todo dispatches - #1

Draft
mdostal wants to merge 5 commits into
mainfrom
agent/dostal-dev/dos-825
Draft

DOS-825: reclaim wedged todo dispatches#1
mdostal wants to merge 5 commits into
mainfrom
agent/dostal-dev/dos-825

Conversation

@mdostal

@mdostal mdostal commented Jul 15, 2026

Copy link
Copy Markdown

Closes DOS-825

Summary

  • add a bounded runtime-sweeper pass that finds todo agent/squad issues with no active task and a missing or failed latest target task
  • re-enqueue eligible agent issues and route squad issues through the squad leader using TaskService.RerunIssue
  • skip archived/missing/offline runtimes and WIP-cap-saturated agents, logging scanned/reclaimed/routed/skipped/failed counts
  • add regression coverage for agent reclaim, squad routing, offline skip, and WIP-cap skip

Tests

  • go test ./cmd/server -run 'TestTodoDispatchReclaim|TestExpireStaleQueuedTasks|TestSweepStaleTasks|TestSweepDispatchedStaleTask|TestSweepResetsInProgressIssueToTodo|TestSweepDoesNotResetIssueAlreadyInReview'
  • go test ./internal/service
  • go test ./cmd/server

Summary by CodeRabbit

  • New Features
    • Added an automated reclaim sweep that repairs stalled todo dispatches by selecting eligible candidates in bounded batches and attempting recovery.
    • Recovered tasks are re-queued for agent routes or routed to the squad leader for squad routes.
    • When a candidate hits the configured max attempts, the related issue is marked as blocked; skipped cases are tracked and reported.
  • Bug Fixes
    • Prevented task-availability notifications from failing when optional cache support is unavailable.
  • Tests
    • Added integration coverage for agent reclaim, squad routing, offline runtimes, and work-in-progress cap behavior.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

An error occurred during the review process. Please try again later.

Walkthrough

Todo dispatch recovery now selects eligible todo issues, revalidates them, and re-enqueues failed work during runtime sweeper ticks. Integration tests cover agent, squad, offline-runtime, and WIP-cap paths. Task availability notification tolerates a missing cache, and the views TypeScript configuration defines its project root.

Changes

Todo dispatch recovery

Layer / File(s) Summary
Reclaim candidate selection
server/pkg/db/queries/agent.sql
Adds bounded and row-locked queries for inactive todo dispatches, requiring valid routing, cooldown eligibility, online runtimes, and available capacity.
Recovery processing and scheduling
server/internal/service/task.go, server/cmd/server/runtime_sweeper.go
Adds transactional recovery, duplicate and eligibility handling, sweeper integration, outcome statistics, recovery limits, and nil-safe empty-claim cache updates.
Recovery integration coverage
server/cmd/server/runtime_sweeper_test.go
Tests agent requeueing, squad-leader routing, offline-runtime skipping, WIP-cap skipping, and supporting reclaim fixtures.

Views build configuration

Layer / File(s) Summary
TypeScript project root
packages/views/tsconfig.json
Sets the compiler rootDir to the views package root.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RuntimeSweeper
  participant CandidateQuery
  participant TaskService
  participant IssueDatabase
  RuntimeSweeper->>CandidateQuery: list capped eligible todo dispatches
  CandidateQuery-->>RuntimeSweeper: return reclaim candidates
  RuntimeSweeper->>TaskService: RecoverTodoDispatch(issueID)
  TaskService->>IssueDatabase: lock and revalidate candidate
  IssueDatabase-->>TaskService: return eligible route
  TaskService-->>RuntimeSweeper: enqueue fresh task and return route result
Loading

Suggested reviewers: forrestchang, ldnvnbl, bohan-j

Poem

A rabbit found dispatches stuck in the queue,
Requeued failed tasks with a fresh hop anew.
Squad leaders received their work in a line,
Offline and capped agents waited just fine.
Nil caches stayed harmless through the night.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: reclaiming wedged todo dispatches.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/dostal-dev/dos-825

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 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.

Actionable comments posted: 3

🤖 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 `@server/cmd/server/runtime_sweeper_test.go`:
- Around line 755-800: Update TestTodoDispatchReclaimSkipsOfflineRuntime and the
related WIP-cap test to select an active agent with an online runtime instead of
relying on findIntegrationAgentRuntime’s fixture. Explicitly set
max_concurrent_tasks to the intended deterministic value for the WIP scenario,
and capture and restore both the agent cap and runtime’s original status in
cleanup. Apply the same fixture setup and restoration to the additional test
range referenced by the review.

In `@server/cmd/server/runtime_sweeper.go`:
- Around line 328-364: Make the candidate validation and recovery flow atomic
around the dispatch loop using a transaction and an issue-level lock. Before
enqueueing, revalidate the issue’s current assignment, target runtime
availability, WIP capacity, and active-task state under that lock, then perform
a recovery-specific enqueue that does not cancel newly created tasks; replace
the unconditional RerunIssue call while preserving the existing skip and failure
accounting.

In `@server/pkg/db/queries/agent.sql`:
- Around line 575-591: The candidate query must apply eligibility filtering
before the batch limit so ineligible candidates cannot consume the processing
cap and starve later reclaimable issues. Update the query around the candidates
selection, joins, and ORDER BY/LIMIT to exclude archived agents, runtime-less or
offline runtimes, and agents at the work-in-progress task cap before applying
LIMIT `@max_per_tick`.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 40017f53-a62d-49c4-b340-65eecc291a30

📥 Commits

Reviewing files that changed from the base of the PR and between 6a594d2 and 4c7717e.

⛔ Files ignored due to path filters (1)
  • server/pkg/db/generated/agent.sql.go is excluded by !**/generated/**
📒 Files selected for processing (4)
  • server/cmd/server/runtime_sweeper.go
  • server/cmd/server/runtime_sweeper_test.go
  • server/internal/service/task.go
  • server/pkg/db/queries/agent.sql

Comment thread server/cmd/server/runtime_sweeper_test.go Outdated
Comment thread server/cmd/server/runtime_sweeper.go Outdated
Comment thread server/pkg/db/queries/agent.sql

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 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 `@server/internal/service/task.go`:
- Around line 1742-1750: Remove the explicit captureTaskQueued call from the
recovery enqueue flow near the task dispatch logging. Keep
NotifyTaskEnqueued(ctx, task) as the single path recording the queued event,
while preserving the slog logging and task event broadcast.

In `@server/pkg/db/queries/agent.sql`:
- Around line 651-659: Update the agent selection/enqueue flow around the
target_running_tasks capacity check to serialize per-agent work-in-progress
decisions: lock the selected target agent before evaluating capacity, then
recount its queued tasks in a separate statement immediately before insertion.
Ensure concurrent recoveries targeting the same agent cannot both pass the cap
check and enqueue beyond max_concurrent_tasks.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: c36914c9-4217-4b6a-89c0-de469568a527

📥 Commits

Reviewing files that changed from the base of the PR and between 4c7717e and edba103.

⛔ Files ignored due to path filters (1)
  • server/pkg/db/generated/agent.sql.go is excluded by !**/generated/**
📒 Files selected for processing (4)
  • server/cmd/server/runtime_sweeper.go
  • server/cmd/server/runtime_sweeper_test.go
  • server/internal/service/task.go
  • server/pkg/db/queries/agent.sql

Comment on lines +1742 to +1750
s.captureTaskQueued(ctx, task)
slog.Info("todo dispatch recovery task enqueued",
"task_id", util.UUIDToString(task.ID),
"issue_id", util.UUIDToString(task.IssueID),
"agent_id", util.UUIDToString(task.AgentID),
"is_squad_route", isSquadRoute,
)
s.broadcastTaskEvent(ctx, protocol.EventTaskQueued, task)
s.NotifyTaskEnqueued(ctx, task)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Avoid recording the recovery enqueue twice.

NotifyTaskEnqueued already calls captureTaskQueued, so Line 1742 duplicates the recovery analytics/metrics event.

Proposed fix
-	s.captureTaskQueued(ctx, task)
 	slog.Info("todo dispatch recovery task enqueued",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
s.captureTaskQueued(ctx, task)
slog.Info("todo dispatch recovery task enqueued",
"task_id", util.UUIDToString(task.ID),
"issue_id", util.UUIDToString(task.IssueID),
"agent_id", util.UUIDToString(task.AgentID),
"is_squad_route", isSquadRoute,
)
s.broadcastTaskEvent(ctx, protocol.EventTaskQueued, task)
s.NotifyTaskEnqueued(ctx, task)
slog.Info("todo dispatch recovery task enqueued",
"task_id", util.UUIDToString(task.ID),
"issue_id", util.UUIDToString(task.IssueID),
"agent_id", util.UUIDToString(task.AgentID),
"is_squad_route", isSquadRoute,
)
s.broadcastTaskEvent(ctx, protocol.EventTaskQueued, task)
s.NotifyTaskEnqueued(ctx, task)
🤖 Prompt for 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.

In `@server/internal/service/task.go` around lines 1742 - 1750, Remove the
explicit captureTaskQueued call from the recovery enqueue flow near the task
dispatch logging. Keep NotifyTaskEnqueued(ctx, task) as the single path
recording the queued event, while preserving the slog logging and task event
broadcast.

Comment on lines +651 to +659
CROSS JOIN LATERAL (
SELECT count(*)::int AS target_running_tasks
FROM agent_task_queue running
WHERE running.agent_id = candidate.target_agent_id
AND running.status IN ('dispatched', 'running', 'waiting_local_directory')
) running
WHERE a.archived_at IS NULL
AND ar.status = 'online'
AND running.target_running_tasks < a.max_concurrent_tasks;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Serialize the agent-level WIP check before enqueueing.

The issue lock does not serialize recoveries for different issues targeting the same agent. Two transactions can both observe target_running_tasks < max_concurrent_tasks and enqueue, exceeding the cap. Lock the target agent first, then recount capacity in a subsequent statement before insertion.

🤖 Prompt for 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.

In `@server/pkg/db/queries/agent.sql` around lines 651 - 659, Update the agent
selection/enqueue flow around the target_running_tasks capacity check to
serialize per-agent work-in-progress decisions: lock the selected target agent
before evaluating capacity, then recount its queued tasks in a separate
statement immediately before insertion. Ensure concurrent recoveries targeting
the same agent cannot both pass the cap check and enqueue beyond
max_concurrent_tasks.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
server/cmd/server/runtime_sweeper_test.go (1)

895-903: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add regression coverage for the cooldown and breaker.

Parameterize the failure age/count, then verify that a recent failure is not reclaimed and five failures transition the issue to blocked without enqueueing. These are the new retry-control branches but are currently untested.

🤖 Prompt for 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.

In `@server/cmd/server/runtime_sweeper_test.go` around lines 895 - 903, Extend
insertFailedTask to accept configurable failure age and use it in the INSERT
instead of the fixed 16-minute interval. Add regression tests covering that a
recent failure is not reclaimed, and that five failures transition the issue to
blocked without enqueueing another task, exercising the cooldown and breaker
paths.
🤖 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 `@server/cmd/server/runtime_sweeper.go`:
- Around line 343-355: The breaker decision and status update around
RecoverTodoDispatch must run under the issue lock, using fresh issue/task state
rather than the stale FailedTasksCount snapshot. Move the attempt check and
blocked transition into the locked recovery transaction; record breaker_tripped
only after a successful status update, and increment Failed when that update
fails.

---

Outside diff comments:
In `@server/cmd/server/runtime_sweeper_test.go`:
- Around line 895-903: Extend insertFailedTask to accept configurable failure
age and use it in the INSERT instead of the fixed 16-minute interval. Add
regression tests covering that a recent failure is not reclaimed, and that five
failures transition the issue to blocked without enqueueing another task,
exercising the cooldown and breaker paths.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 8b0b168f-97cc-40ee-a095-20634d92e22b

📥 Commits

Reviewing files that changed from the base of the PR and between edba103 and cb6a4da.

⛔ Files ignored due to path filters (1)
  • server/pkg/db/generated/agent.sql.go is excluded by !**/generated/**
📒 Files selected for processing (5)
  • packages/views/tsconfig.json
  • server/cmd/server/runtime_sweeper.go
  • server/cmd/server/runtime_sweeper_test.go
  • server/internal/service/task.go
  • server/pkg/db/queries/agent.sql

Comment on lines +343 to +355
if c.FailedTasksCount >= todoDispatchReclaimMaxAttempts {
stats.skip("breaker_tripped")
_, blockErr := queries.UpdateIssueStatus(ctx, db.UpdateIssueStatusParams{
ID: issueID,
Status: "blocked",
WorkspaceID: c.WorkspaceID,
})
if blockErr != nil {
slog.Warn("todo dispatch reclaim: failed to block issue", "issue_id", issueKey, "error", blockErr)
} else {
slog.Info("todo dispatch reclaim: issue tripped breaker and blocked", "issue_id", issueKey, "failed_tasks", c.FailedTasksCount)
}
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make the breaker transition part of the locked recovery transaction.

FailedTasksCount is a stale list snapshot. Before this update, the issue can be completed, reassigned, or receive a new active task, yet this code still overwrites its status with blocked. Move the attempt check and status transition behind the issue lock used by RecoverTodoDispatch. Only record breaker_tripped after a successful update; count update failures in Failed.

🤖 Prompt for 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.

In `@server/cmd/server/runtime_sweeper.go` around lines 343 - 355, The breaker
decision and status update around RecoverTodoDispatch must run under the issue
lock, using fresh issue/task state rather than the stale FailedTasksCount
snapshot. Move the attempt check and blocked transition into the locked recovery
transaction; record breaker_tripped only after a successful status update, and
increment Failed when that update fails.

dev-qa's blocking review asked for tests covering both the cooldown
gate and the per-issue attempt bound; the attempt bound (breaker) was
implemented but never actually got test coverage despite an earlier
comment claiming it did.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: multica-agent <github@multica.ai>
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