Repository navigation
feat(tasks): task communication — wake on result, stop/resume, message tasks, any-agent delegation - #230
Merged
Conversation
- Serialize workspace DB opens per file and run startup recovery once per process, so a second open cannot fail live runs and tasks. - Migration 20261016000000: session_signals replaces assistant_message_queue (rows migrated as user_message signals) and a unique partial index enforces one active run per session. - signals.rs owns pending/claimed/consumed user input; dispatcher::kick is the only path that starts a run for pending signals (RunTrigger::SignalWake) and runs after every send and every run end. - Busy scheduler ticks are skipped and logged instead of deferred; a skipped manual Run now emits run_skipped. defer_running_instance is removed.
- Migration 20261016001000: assistant_messages gains origin_json and
run_id; workspace_tasks gains lineage, cycle_no, stop/answer columns and
failure_reason. `blocked` rows become failed/blocked, other failures get
a reason, and a task session's first user row is marked as the
creator's instruction.
- MessageOrigin (user | agent | task) on every message; TaskStatus and
FailureReason replace the free-text task status. `blocked` is no longer
a status: a `BLOCKED:` reply ends a cycle as failed{blocked}.
- workspace_assignTask returns the queued task immediately. The
instruction is an agent_message signal, and dispatcher::kick starts each
task cycle. finalize_task_cycle posts one task_result/task_failed event
per cycle to the parent session (delivery key task-result:<id>:<cycle>)
and wakes it. The summary is read from the cycle's own run rows.
- Runs stamp run_id on the rows they write; claim and mid-run consume
stamp delivered input. Automated events reach the model with an origin
header (API history, CLI input, mid-run and fresh-session context);
stored content never contains it.
- AssistantUiEvent::TaskUpdated on every task status change; the
workspace://task-attention event is removed. The attention summary
counts failed/blocked as blocked.
- The prompt and tool descriptions stop telling the manager to poll.
Results arrive as automated events, and SignalWake runs are described as
message-driven.
- Bindings regenerated. Frontend: a no-op task_updated arm, failed-only
attention, and fixture updates.
- runtime.rs keys live runs by (workspace, session): cancel_session,
cancel_run by run id, and a per-run mid-run wake. kick wakes a busy
session's live run; Claude Code delivers on that wake instead of the
1.2 s queue poll (Codex keeps its steer poll).
- workspace_stopTask (agent tool) and workspace_stop_task (UI command):
queued/running/input_required -> stopping with stop_requested_at. A live
run is cancelled; a claimed run that has not registered yet sees the
request before it starts and is cancelled unstarted; with no active run
the cycle ends stopped at once. The finalizer ends a cycle stopped
whenever a stop was requested, even if the provider finished first.
Repeat stops change nothing; a stop of an ended task is already_terminal.
- assistant_cancel_run on a task session stops the task.
- dispatcher: a stopping task waits for its finalizer; a stopped task
resumes only for input sent after it stopped.
- Restart recovery replaces the failed sweep: running/input_required/
stopping tasks end stopped ("task interrupted by app restart") with one
result event to the parent; a task whose run never started stays queued
and its claimed input goes back to pending; a queued task with no session
fails as before.
- WorkspaceTaskResponse gains stopRequestedAt; bindings regenerated;
frontend gets stopWorkspaceTask and fixture updates only.
- workspace_sendTaskMessage (agent tool) and workspace_send_task_message
(UI command) post a message to a task session as one content signal
(agent_message from an agent, user_message from the user). On an
input_required task it answers the open ask_user question and the signal
is consumed by the live run; otherwise the dispatcher queues it for the
running cycle or starts a new one. A stopped task resumes only for input
sent after the stop (B3 gate). Responses carry the task, message, signal
{id, status} and run.
- ask_user::submit_answer_for_session: the first answer to remove the
pending entry wins; a later sender's message stays a pending signal.
AskUserAnswer carries its origin; the panel answers as User.
- ask_user in a task session: running -> input_required, plus a
task_input_required event to the parent session that wakes it. Once
answered, input_required -> running with input_answered_by_json set.
Both emit task_updated.
- WorkspaceTaskResponse gains answeredBy; SignalRef, SignalStatus and
WorkspaceTaskSendResponse are exported. Bindings regenerated; frontend
fixtures only.
M1 (frontend half): question rows no longer offer Mark reviewed. m1: ?task= opens via openTask, clearing the Back stack. m2: toasts only on a status/cycle transition; an old task fetched into a loaded workspace stays quiet. m4: unanswered questions read "Not answered · <live status>". m5: input_required shows the attention face and does not count as running for Run now. m6: follow-on agent messages keep an sr-only sender name. m7: Open task buttons are named by task title. m8: result cards read Blocked when the live task's same cycle failed as blocked. Deletions: useCancelRun out of TaskStopButton (local pending flag + stopping status); legacy task-row status 'blocked' in isTaskAttention; resume hint only in the composer placeholder; transcript assignee/session meta line. Not done: m3 (two questions per cycle, needs list-level plumbing), m9, m10 (states already get the base pill style), queued chip after stop. Kept: TASK_STATUS_LABEL.blocked and TaskCard 'blocked' — old tool payloads still carry it.
major-1/2: open_cycle opens only a finished task (new, completed, failed, or stopped with explicit input sent after the stop), decided inside the claim transaction after the run insert. A kick between run end and the finalizer, or a stop landing before the claim commits, now leaves the cycle to its finalizer/stop. Restart recovery requeues a task whose run never started with run_id NULL so the predicate still dispatches it. major-3: assignment writes task row, session, link, instruction and signal in one transaction; no connection stores the task failed directly. major-5: start_cycle (stop-before-start) and the kick's mid-run wake are pool-level and tested; restart tests reopen through init_workspace_db. minor-1: a skipped tick persists next_run_at only. minor-2: trigger_message_id removed; a chat run whose claimed input is all user messages gets the UserMessage prompt again, else SignalWake. minor-3: restart result events name the agent from the roster. minor-4: stale test_support comment. question-1: a stopped task resumes only on user/agent messages sent after the stop request, so one sent while stopping resumes it.
- Open the assignment and agent-send transactions with BEGIN IMMEDIATE so parallel workspace_assignTask calls no longer fail with SQLITE_BUSY. - Consume a task answer before handing it to the waiting ask_user; release it if no question was waiting. - Fail tasks left queued/running by the pre-signal code in the lineage migration (reason restart). - Write input_required and the parent's question in one transaction. - Drop the user-message/signal-wake trigger split: chat claims are signal_wake. - State polling and delegation limits once, in the tool descriptions, built from the constants; sendTaskMessage is not limited to the assigner. - A worker whose run ends with unfinished subtasks keeps its cycle open and reports once, after them.
- Drop the per-turn fan-out and active-per-creator caps; the depth limit and the hourly sender budget (which counts assignments) remain. - Drop session_signals.payload_json, lane, claimed_at and consumed_at, none of which was read, and the unreachable `dropped` status. - The task send response reports `queued: bool`, as assistant_send_message does, instead of SignalRef/SignalStatus. - One load_task_for_session (on the pool) and one now_ms; the repo's stop write is mark_stopping; the tools-side load_task wrapper is inlined.
sqlx 0.8 Pool::close closes idle connections without holding a permit but releases one for each, so it can return while a connection checked out at close time is still shutting down. On Windows that open data.sqlite made remove_dir_all fail with os error 32. Wait for size() to reach 0, and assert it in the delete test (failed 24/30 on Linux without the wait).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Task communication: wake on result, stop/resume, message tasks, any-agent delegation
Implements
plans/task-communication-backend.md+plans/task-communication-frontend.md(option 3b: per-workspacesession_signals+ statelessdispatcher::kick).What changes for users
workspace_assignTaskreturns immediately; when the task finishes, its result is written into the caller's session as an automated event and wakes the caller (or is delivered mid-run if it's busy).ask_userdirectly.ask_usermarks itinput_requiredand posts the question to its parent; the parent agent or the human can answer.budget_exceeded).Commits
One commit per plan step (B1–B6 backend, F2–F11 frontend), then review fixes. Each commit builds and passes its own tests.
Verification
-D warnings, ~1000 filtered lib tests over every touched module, all integration tests, bindings fresh,cargo checkon every backend commit.reviews/task-communication-*.Known limitations (accepted)
AppHandle/live CLI to test (spawn_run→start_cycle; Claude Codemidrun.notified()).running.Upgrade note
Migrations
20261016*change the workspace DB; a build from before this PR can't read a migrated DB. Keep a copy ofdata.sqlite.