feat(task): add worker-session link + queue statuses (schema + mutators, inert) - #1935
Open
wqymi wants to merge 1 commit into
Open
feat(task): add worker-session link + queue statuses (schema + mutators, inert)#1935wqymi wants to merge 1 commit into
wqymi wants to merge 1 commit into
Conversation
…rs, inert) PR-1 of the Symphony-style task-queue design. Closes the core gap: a task had no durable link to the worker session executing it (`owner` was free text), so task state could not survive as authoritative queue state in the DB. Schema (all nullable, backfill-safe, mirroring the `owner` precedent): - task.worker_session_id / dispatched_at / result_ref - index task_worker_idx on (worker_session_id, status) - TaskStatus gains dispatched | human_review | failed - TaskEventKind gains dispatched | review_requested | reworked | failed Registry gains dispatch / requestReview / rework / fail, following the existing mutator shape (validate -> update -> insertEvent -> publishUpdated). `failed` is terminal alongside done/abandoned; `dispatched`/`human_review` are non-terminal for list()'s default filter. Behavior-inert: nothing calls the new mutators yet. PR-2 wires the lifecycle transitions and the task-tool verbs.
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.
What
PR-1 of the Symphony-inspired Orchestrator task-queue design. Schema + mutators only — behavior-inert: nothing in the codebase calls the new mutators yet, and no lifecycle hook or task-tool verb is wired. Merging this changes zero runtime behavior.
The core gap this closes
Task tracking already had almost everything a queue needs (
TaskTable, append-onlyTaskEventTable,TaskRegistryas the single mutator,task.updatedbus events). What it did not have is a durable link from a task to the worker session executing it:owneris a free-text actorID, andActorRegistryTablehas notask_id. So "which peer child is working T3, and what came out of it" was only ever inferable from the model's context, never authoritative in the DB.This PR adds that link plus the queue statuses the orchestrator needs to route work.
Schema deltas
packages/opencode/migration/20260715000000_task_worker_link/migration.sql(timestamped after the newest existing migration, auto-discovered bymigrations()insrc/storage/db.ts):All three columns are nullable, exactly mirroring the
20260603000000_task_in_progress_ownerprecedent, so existing rows backfill to NULL.worker_session_idis deliberately NOT a foreign key toSessionTable.session_id(the owning orchestrator session) is an FK withonDelete: cascadebecause a task cannot outlive its owner. A worker session is different: it is a disposable peer child that gets torn down when its job ends, but the task row must survive as durable history (includingresult_refpointing at the branch it produced). An FK with cascade would delete the task; an FK with restrict would block worker cleanup. Same soft-link treatmentowneralready gets.Statuses (the queues)
TaskStatus(src/task/schema.ts) and the drizzle$type<>(src/task/task.sql.ts) and the tool's separatestatusSchema(src/tool/task.ts) all widen consistently. A task's status is the queue it sits in:opendispatched(new)in_progressblockedhuman_review(new)donefailed(new)abandonedabandonedTaskEventKindgains the matching kinds:dispatched,review_requested,reworked,failed.UpdatedKindinsrc/task/events.tsis derived viaTaskEventKind.exclude(["created"]), sotask.updatedauto-widens with no edit there.Terminal/non-terminal membership is now expressed once as
TERMINAL_TASK_STATUSES/NON_TERMINAL_TASK_STATUSES+isTerminalTaskStatus()insrc/task/schema.ts, and both thestart()guard andlist()'s default filter read from it instead of hardcoding a status list.New registry mutators
All four follow the exact existing shape — validate, single
UPDATE,insertEvent,publishUpdated, re-getwith the shared agent-recoverable not-found message:dispatch({ session_id, id, worker_session_id, event_summary? })— setsworker_session_id+dispatched_at, status →dispatched, logsdispatched. Refuses (warn + no-op) on a terminal task, same asstart().requestReview({ session_id, id, event_summary?, result_ref? })— status →human_review, logsreview_requested. Does not stampended_at: the task is still live, just waiting on a human. Refuses on terminal.rework({ session_id, id, event_summary? })— the user rejected the result:human_review→dispatchedif the task still has a worker, else →openso the orchestrator can dispatch a fresh one. Logsreworked. No-op + warn if the task is not actually inhuman_review.fail({ session_id, id, event_summary?, result_ref? })— status →failed, stampsended_at+cleanup_afterlikedone/abandon. Logsfailed.start()'s terminal guard now coversfailedtoo, so a failed task cannot be silently resurrected into a self-contradictory row (in_progresswhile carryingended_at/cleanup_after).Compatibility
There is no exhaustive
switchonTaskStatus, and status is plainTEXTin SQLite, so old rows and old values keep working. The two render sites were checked:session/checkpoint.tsstatusIconhas a?? sfallback — icons added for the three new statuses anyway.server/routes/instance/session.tstaskToTodohad apendingelse-branch, which would have rendered a failed (dead) task as still actionable. Rewritten as an explicit exhaustiveRecord<Task["status"], string>:failed→cancelled(joiningabandoned), whiledispatchedandhuman_reviewcorrectly staypending(genuinely queued — on a worker / on the user).Tests
bun test— 154 pass / 0 fail across the task, storage, tool/task, actor spawn-autostart and session-task-route suites.bun typecheckexits 0.test/task/registry.test.ts: new columns default toundefinedand round-trip;result_refround-trips throughfail;dispatchsetsworker_session_id/dispatched_at/status and emitsdispatched;dispatchrefuses a terminal task and is agent-recoverable on an unknown id;requestReviewentershuman_reviewwithout stampingended_at;reworkreturns todispatchedwith a worker andopenwithout one, and no-ops outsidehuman_review;failstampsended_at/cleanup_afterandstartrefuses to resurrect it;listtreatsdispatched/human_reviewas non-terminal andfailedas terminal, and filters by the new statuses; all four mutators publish the matchingtask.updatedkind on the bus.test/storage/task-worker-migration.test.ts: real on-disk SQLite withforeign_keys = ON— applies every migration except the new one, seeds a legacytaskrow through raw SQL with the old column set, then applies the new migration and asserts the three columns exist and are NULL on the old row,task_worker_idxexists, and the new columns are writable.Next
PR-2 wires the actual behavior: session-create / worker-terminal lifecycle hooks driving
dispatch/fail/requestReview, and thetasktool verbs (dispatch,review,rework,fail) so the orchestrator can operate the queues.