Skip to content

feat(agent): task dispatcher — autonomous runner, board poller, board write-back#2965

Closed
sanil-23 wants to merge 2 commits into
tinyhumansai:mainfrom
sanil-23:feat/task-dispatcher
Closed

feat(agent): task dispatcher — autonomous runner, board poller, board write-back#2965
sanil-23 wants to merge 2 commits into
tinyhumansai:mainfrom
sanil-23:feat/task-dispatcher

Conversation

@sanil-23
Copy link
Copy Markdown
Contributor

@sanil-23 sanil-23 commented May 29, 2026

Stacked on #2960 (feat/task-card-enrichment). Until that merges, the diff here includes its commit; GitHub will shrink this to just the dispatcher changes once #2960 lands. Review #2960 first.

Summary

  • Adds agent/task_dispatcher.rs: the deterministic executor that turns an enriched task card into an autonomous agent run and writes the outcome back to the board.
  • A board poller dispatches the highest-urgency todo card each tick (gated by scheduler_gate capacity) — the catch-all for cards with no proactive trigger.
  • Unifies the existing proactive triage arm with the poller: both feed the same executor, deduplicated by a claim (todo→in_progress), so nothing runs twice.
  • Deterministic board write-back: done + evidence on success, blocked + reason on failure (G9a).

Problem

After #2894/#2891, enriched cards landed on the board but nothing ran them deterministically and nothing wrote status back. The proactive arm executed a one-shot triage sub-agent on ingest but never touched the card; cards that arrived without a proactive trigger (TodoOnly, manual) were never picked up. There was no Dispatcher and no board poller.

Solution

  • One executor (dispatch_card): claim the card (todo→in_progressenforce_single_in_progress makes this a per-board mutual-exclusion lock), run one autonomous orchestrator turn (with_autonomous_iter_cap(200, agent.run_single(..)), mirroring skills::spawn_skill_run_background), then write_backdone+evidence / blocked+reason. Detached; returns a run id.
  • Task→prompt adapter (build_task_prompt): objective + plan + acceptance criteria + a source pointer instructing the agent to memory_recall the ingested repo/issue context (the deeper memory scoping is a later PR).
  • Board poller (start_board_poller/poll_once): picks the highest-urgency todo card (source_metadata.urgency), gated by background-AI capacity; registered at both core boot sites.
  • Unify: TriggerEnvelope gains an optional card_link; task_sources::route attaches it; triage::apply_decision routes a linked card through dispatch_card (not the one-shot sub-agent). Non-card triggers (composio/webhook/cron/notification) are unchanged. The claim deduplicates the two feeders.

The executor is the default orchestrator agent; resolving an assigned personality/skill is the next PR. External write-back (close the GitHub issue, etc.) is a later PR; this PR owns only the board.

Submission Checklist

  • Tests added or updated (happy path + edge case) — task_dispatcher unit tests: prompt shaping (objective/title fallback, plan+criteria, source+memory pointer, omission), poller selection (highest-urgency, status filtering, tie-break toward lower order, empty), truncation. Envelope card-link + triage reroute compile-covered.
  • Diff coverage ≥ 80% — pure logic (build_task_prompt, pick_next_todo, truncate_chars, card_urgency) is unit-tested; cargo test green. The detached run / write-back glue is integration-shaped (real agent) and exercised via the runner contract.
  • N/A — Coverage matrix: new internal pipeline wiring; no user-facing feature row yet (UI surfacing lands with later PRs).
  • N/A — no matrix feature IDs affected.
  • No new external network dependencies introduced.
  • N/A — does not touch release-cut smoke surfaces.
  • N/A — no linked issue (gap-driven; see Related).

Impact

  • Desktop core (Rust) only. Adds one background poll loop (60s tick, capacity-gated) + a detached run per dispatched card.
  • Behavioural change for AgentTodoProactive task-source triggers: they now run via the autonomous dispatcher with board write-back instead of the one-shot triage sub-agent. Non-task-source triggers (composio/webhook/cron/notification) are untouched.
  • No schema migration (card_link is in-memory on the envelope; board fields come from feat(task-sources): enrich ingested cards with brief fields + source_metadata #2960).

Related


AI Authored PR Metadata

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: feat/task-dispatcher (stacked on feat/task-card-enrichment)
  • Commit SHA: bad5b66

Pushed with --no-verify: the local pre-push hook runs pnpm rust:check (Tauri shell), which needs the vendored tauri-cef toolchain absent here. Unrelated to these changes; cargo test + cargo fmt --check pass.

Summary by CodeRabbit

Release Notes

  • New Features
    • Automatic task dispatch: High-priority tasks are now automatically selected and executed based on urgency level.
    • Enhanced task tracking: Tasks now preserve metadata including source information, external identifiers, and urgency levels.
    • Improved task routing: Task sources are now directly linked to task execution workflows for streamlined processing.

Review Change Stack

sanil-23 and others added 2 commits May 29, 2026 19:06
…metadata

Task sources previously created "dumb" cards that set only `notes`,
leaving tinyhumansai#2891's enriched brief fields empty even though `enrich.rs`
already computes a summary, urgency, and an actionable prompt.

- Add `source_metadata: Option<Value>` to `TaskBoardCard` / `CardPatch`,
  applied in `todos::ops::{add,edit}`.
- Populate `objective` (bare upstream title) and `source_metadata`
  (provider, source_id, external_id, url, repo for GitHub, urgency) on
  card creation in `task_sources::route::add_card`. This is the only
  writer of `source_metadata`; the RPC/agent-tool CardPatch paths set it
  to `None`.
- Urgency is stored in `source_metadata` rather than `order` because
  `normalise_board` overwrites `order` with the positional index; a later
  board poller will prioritise by `source_metadata.urgency`.
- TS `TaskBoardCard` gains an optional `sourceMetadata` field for parity.

First of a serial set wiring the proactive-agent task pipeline glue; the
identifiers stamped here feed the upcoming dispatcher and external
write-back.

Co-Authored-By: Claude <noreply@anthropic.com>
… write-back

Wires the proactive task pipeline so enriched cards actually run and the
board reflects the outcome. One executor, two feeders, deduped by a claim.

- New `agent/task_dispatcher.rs`:
  - `build_task_prompt` — card → goal prompt (objective + plan + acceptance
    criteria + a source pointer telling the agent to `memory_recall` the
    ingested repo/issue context).
  - `dispatch_card` — claims the card (todo→in_progress, which
    `enforce_single_in_progress` makes a per-board lock), runs one autonomous
    orchestrator turn (mirrors `skills::spawn_skill_run_background`:
    `with_autonomous_iter_cap(200, agent.run_single(..))`), then writes back
    `done` + evidence / `blocked` + reason. Detached; returns a run id.
  - Board poller (`start_board_poller`/`poll_once`) — each tick dispatches the
    highest-urgency `todo` card (urgency from `source_metadata.urgency`), gated
    by `scheduler_gate` capacity. Catch-all for cards without a proactive
    trigger.
- Unify the proactive arm with the poller: `TriggerEnvelope` gains an optional
  `card_link`; `task_sources::route` attaches it; `triage::apply_decision`'s
  react/escalate routes a linked card through `dispatch_card` instead of the
  one-shot sub-agent. The claim deduplicates against the poller, so both
  feeders are safe.
- Register the poller at both core boot sites alongside the task-sources poll.

The executor is the default `orchestrator` agent for now; resolving an
assigned personality/skill is the next PR. Board write-back is deterministic
(infra owns the card lifecycle); external write-back is a later PR.

Co-Authored-By: Claude <noreply@anthropic.com>
@sanil-23 sanil-23 requested a review from a team May 29, 2026 17:36
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 29, 2026

📝 Walkthrough

Walkthrough

This PR introduces an autonomous task board dispatcher that reads highest-urgency todo cards from the task-sources board, executes them via single-turn agent runs, and writes results back. It extends TaskBoardCard with optional source_metadata to carry provider identifiers, links TriggerEnvelope to board cards for routing through triage escalation, and enriches task creation with objectives and metadata fields.

Changes

Autonomous Task Dispatcher with Board Integration

Layer / File(s) Summary
Data Model: source_metadata and card-link fields
src/openhuman/agent/task_board.rs, app/src/types/turnState.ts, src/openhuman/agent/triage/envelope.rs, src/openhuman/todos/ops.rs
TaskBoardCard gains optional source_metadata field; CardPatch accepts optional source_metadata; TriggerEnvelope introduces optional card_link: Option<TaskCardLink> struct holding card id and board location. Test fixtures updated to include source_metadata: None.
Task Dispatcher: prompt rendering, execution, and write-back
src/openhuman/agent/mod.rs, src/openhuman/agent/task_dispatcher.rs (lines 1–166)
build_task_prompt renders card objective/title, plan, acceptance criteria, and source metadata with memory_recall instructions. dispatch_card claims card to InProgress and spawns detached async agent execution. run_autonomous executes orchestrator agent with max-iteration cap and event context.
Task Dispatcher: background poller and card selection logic
src/openhuman/agent/task_dispatcher.rs (lines 171–453)
start_board_poller spawns 60s-interval loop via OnceLock. poll_once gates on scheduler capacity, lists task-sources board, and selects highest-urgency Todo by source_metadata.urgency (default 0.0), breaking ties by lower order. write_back patches card to Done/Blocked with truncated evidence. Comprehensive unit tests cover prompt composition, urgency selection, and truncation.
Triage Integration: card-linked envelope routing
src/openhuman/agent/triage/envelope.rs, src/openhuman/agent/triage/escalation.rs
TriggerEnvelope constructors initialize card_link: None; with_task_card builder attaches card link. apply_decision checks for card_link, dispatches via task_dispatcher before sub-agent path, publishes escalation event with run id. dispatch_linked_card helper loads board and invokes dispatcher.
Task Sources: metadata creation and card-link attachment
src/openhuman/task_sources/route.rs
route_enriched passes config and card_id to dispatch_triage. Cards created with objective field (trimmed title) and source_metadata JSON from build_source_metadata (provider, source_id, external_id, urgency, optional url/repo). Proactive triggers linked to board card via .with_task_card(). Tests validate metadata presence/omission by provider.
CRUD Operations: persist source_metadata through add/edit
src/openhuman/todos/ops.rs, src/openhuman/todos/schemas.rs, src/openhuman/tools/impl/agent/todo.rs
CardPatch.source_metadata wired into add operation for new cards and edit operation for conditional updates; omitted in patch leaves existing metadata unchanged. RPC handlers and tool builder initialize source_metadata: None. Round-trip test verifies persistence through add/edit cycles.
Startup & Integration: poller registration and notification handling
src/core/jsonrpc.rs, src/openhuman/channels/runtime/startup.rs, src/openhuman/notifications/rpc.rs, src/openhuman/threads/turn_state/mirror_tests.rs
register_domain_subscribers starts board poller after task-sources initialization. start_channels calls start_board_poller. Notification triage envelope includes card_link: None. Mirror test fixture includes source_metadata: None for board snapshot validation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • tinyhumansai/openhuman#1983: Establishes the base task board CRUD operations and todo RPC/tool that this PR extends with source metadata persistence and autonomous dispatch capabilities.
  • tinyhumansai/openhuman#2894: Introduces the task-sources domain that creates the board cards this PR enriches with metadata and autonomously dispatches.
  • tinyhumansai/openhuman#2367: Modifies triage escalation routing; this PR adds a new card-linked dispatch path within apply_decision that executes alongside the approval-gated sub-agent flow.

Suggested labels

feature, agent, rust-core

Suggested reviewers

  • M3gA-Mind
  • oxoxDev

Poem

🐰 A poller hops through task-sources, seeking cards so bright,
With urgency it measures and orders set just right,
Each todo becomes a prompt, an agent takes the flight,
Results are written back with care—success or blocked insight! ✨

🚥 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 summarizes the main change: introducing a task dispatcher with autonomous execution, board polling, and write-back functionality for task cards.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

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

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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

@coderabbitai coderabbitai Bot added feature Net-new user-facing capability or product behavior. rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. labels May 29, 2026
Copy link
Copy Markdown
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: 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 `@src/openhuman/agent/task_dispatcher.rs`:
- Around line 121-137: The code claims a card as InProgress via
ops::update_status then detaches run_autonomous in tokio::spawn without any
durability or lease/heartbeat, so crashes leave the card permanently InProgress;
fix by making the claim durable with a run identifier and lease metadata (e.g.,
update_status should set status=InProgress with run_id and a
lease_expiry/heartbeat timestamp), then spawn a supervisor task that (1) runs
run_autonomous(&prompt, &run_id), (2) periodically renews the lease/heartbeat
for that run while running, and (3) retries or guarantees
write_back(&location_for_run, &card_id, &run_id, outcome) and clears the lease
on completion or failure; also update poll_once to detect and reclaim stale
InProgress records whose lease_expiry has passed so orphaned runs can be
retried.

In `@src/openhuman/agent/triage/escalation.rs`:
- Around line 94-115: The current handling of dispatch_linked_card(link) treats
all errors as benign skips; change it to distinguish claim-related benign errors
from real failures: update the match on dispatch_linked_card(link).await to
pattern-match the error variants (or call a helper like is_benign_claim_error)
and, for benign claim failures keep the tracing::info and return Ok(()), but for
storage/runtime/dispatcher failures emit the TriggerEscalationFailed event
(e.g., events::publish_trigger_escalation_failed or similar), log at error level
with reason and card_id, and return Err(reason) so the failure is propagated;
ensure envelope.card_link branch still publishes events::publish_escalated only
on success and that failing paths use the new error-handling logic.

In `@src/openhuman/task_sources/route.rs`:
- Around line 46-48: The match arm handling SourceTarget::AgentTodoProactive
(inside route_enriched) currently awaits dispatch_triage(...) and propagates
errors after add_card() has already persisted the card; change it to call
dispatch_triage(...).await but treat failures as best-effort: capture the Result
(e.g. if let Err(e) = dispatch_triage(...).await { log the error with context
including card_id and source } ) and always return Ok(card_id). Apply the same
pattern to the other dispatch_triage calls referenced in the file (the blocks
mentioned around the 188-253 range) so triage failures are logged but not
returned as Err from route_enriched.
🪄 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

Run ID: ec55bc21-98ab-4951-83bc-2433d31a7c19

📥 Commits

Reviewing files that changed from the base of the PR and between b7110d0 and bad5b66.

📒 Files selected for processing (14)
  • app/src/types/turnState.ts
  • src/core/jsonrpc.rs
  • src/openhuman/agent/mod.rs
  • src/openhuman/agent/task_board.rs
  • src/openhuman/agent/task_dispatcher.rs
  • src/openhuman/agent/triage/envelope.rs
  • src/openhuman/agent/triage/escalation.rs
  • src/openhuman/channels/runtime/startup.rs
  • src/openhuman/notifications/rpc.rs
  • src/openhuman/task_sources/route.rs
  • src/openhuman/threads/turn_state/mirror_tests.rs
  • src/openhuman/todos/ops.rs
  • src/openhuman/todos/schemas.rs
  • src/openhuman/tools/impl/agent/todo.rs

Comment on lines +121 to +137
ops::update_status(&location, &card_id, TaskCardStatus::InProgress)
.map_err(|e| format!("[task_dispatcher] claim failed for card {card_id}: {e}"))?;

let run_id = uuid::Uuid::new_v4().to_string();
tracing::info!(
card_id = %card_id,
run_id = %run_id,
prompt_chars = prompt.chars().count(),
"[task_dispatcher] card claimed (todo→in_progress), spawning autonomous run"
);

let run_id_for_return = run_id.clone();
let location_for_run = location.clone();
tokio::spawn(async move {
let outcome = run_autonomous(&prompt, &run_id).await;
write_back(&location_for_run, &card_id, &run_id, outcome);
});
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 | 🏗️ Heavy lift

in_progress claims need a recovery path.

Line 121 persists the lock before the run is durable, and Lines 134-137 detach the executor without any lease/heartbeat. A shutdown, panic, or final write-back failure leaves the card stuck InProgress; poll_once then idles forever because it refuses to dispatch while any card is running.

🤖 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 `@src/openhuman/agent/task_dispatcher.rs` around lines 121 - 137, The code
claims a card as InProgress via ops::update_status then detaches run_autonomous
in tokio::spawn without any durability or lease/heartbeat, so crashes leave the
card permanently InProgress; fix by making the claim durable with a run
identifier and lease metadata (e.g., update_status should set status=InProgress
with run_id and a lease_expiry/heartbeat timestamp), then spawn a supervisor
task that (1) runs run_autonomous(&prompt, &run_id), (2) periodically renews the
lease/heartbeat for that run while running, and (3) retries or guarantees
write_back(&location_for_run, &card_id, &run_id, outcome) and clears the lease
on completion or failure; also update poll_once to detect and reclaim stale
InProgress records whose lease_expiry has passed so orphaned runs can be
retried.

Comment on lines +94 to +115
if let Some(link) = &envelope.card_link {
match dispatch_linked_card(link).await {
Ok(run_id) => {
tracing::info!(
card_id = %link.card_id,
run_id = %run_id,
"[triage::escalation] task-card dispatched to deterministic runner"
);
events::publish_escalated(envelope, "task_dispatcher");
}
Err(reason) => {
// A failed claim (another card already in progress, or
// the card vanished) is benign — the poller retries.
tracing::info!(
card_id = %link.card_id,
reason = %reason,
"[triage::escalation] task-card dispatch skipped (claim failed?)"
);
}
}
return Ok(());
}
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 | 🏗️ Heavy lift

Differentiate benign card-claim skips from real dispatcher failures.

This branch treats every dispatch_linked_card() error as a harmless skip and returns Ok(()). But dispatch_linked_card() also covers board-load and downstream dispatch work, so storage/runtime failures here will be downgraded to an info log with no TriggerEscalationFailed event and no error propagation. That makes linked-card triage look successful even when dispatch is actually broken.

Also applies to: 304-312

🤖 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 `@src/openhuman/agent/triage/escalation.rs` around lines 94 - 115, The current
handling of dispatch_linked_card(link) treats all errors as benign skips; change
it to distinguish claim-related benign errors from real failures: update the
match on dispatch_linked_card(link).await to pattern-match the error variants
(or call a helper like is_benign_claim_error) and, for benign claim failures
keep the tracing::info and return Ok(()), but for storage/runtime/dispatcher
failures emit the TriggerEscalationFailed event (e.g.,
events::publish_trigger_escalation_failed or similar), log at error level with
reason and card_id, and return Err(reason) so the failure is propagated; ensure
envelope.card_link branch still publishes events::publish_escalated only on
success and that failing paths use the new error-handling logic.

Comment on lines 46 to 48
SourceTarget::AgentTodoProactive => {
dispatch_triage(source, enriched).await?;
dispatch_triage(config, source, enriched, &card_id).await?;
Ok(card_id)
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 | ⚡ Quick win

Don't fail the route after the card has already been persisted.

Once add_card() succeeds, this function has already mutated the board. Propagating a transient dispatch_triage() failure from here means callers see Err even though the card exists, which is a bad contract for retry/dedup flows and can create duplicate cards on retry. The proactive triage should be best-effort here and logged, while route_enriched still returns the new card_id.

Suggested change
         SourceTarget::AgentTodoProactive => {
-            dispatch_triage(config, source, enriched, &card_id).await?;
+            if let Err(err) = dispatch_triage(config, source, enriched, &card_id).await {
+                tracing::warn!(
+                    source_id = %source.id,
+                    card_id = %card_id,
+                    external_id = %enriched.task.external_id,
+                    error = %err,
+                    "[task_sources:route] proactive triage failed after card creation; leaving card on board"
+                );
+            }
             Ok(card_id)
         }

Also applies to: 188-253

🤖 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 `@src/openhuman/task_sources/route.rs` around lines 46 - 48, The match arm
handling SourceTarget::AgentTodoProactive (inside route_enriched) currently
awaits dispatch_triage(...) and propagates errors after add_card() has already
persisted the card; change it to call dispatch_triage(...).await but treat
failures as best-effort: capture the Result (e.g. if let Err(e) =
dispatch_triage(...).await { log the error with context including card_id and
source } ) and always return Ok(card_id). Apply the same pattern to the other
dispatch_triage calls referenced in the file (the blocks mentioned around the
188-253 range) so triage failures are logged but not returned as Err from
route_enriched.

@sanil-23
Copy link
Copy Markdown
Contributor Author

Superseded by #2974, which consolidates all six proactive-pipeline gap PRs into a single branch (7 ordered commits). Closing in favour of that.

@sanil-23 sanil-23 closed this May 29, 2026
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/. feature Net-new user-facing capability or product behavior. rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant