Skip to content

fix(platform): keep an assignee handoff on the task it was asked for - #4393

Open
yannickmonney wants to merge 1 commit into
mainfrom
fix/assignee-handoff-task-identity
Open

yannickmonney wants to merge 1 commit into
mainfrom
fix/assignee-handoff-task-identity

Conversation

@yannickmonney

@yannickmonney yannickmonney commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

What changed

The task modal keeps one Assign picker while a parent link or a subtask opens another task; it is not keyed by task id (unlike the title and description editors next to it). The picker's handoff guard did three things on task A: it ran the live-run reads, stored the picked change (not scoped to a task) once they answered, and later cancelled and reassigned with the picker's current props. If the reads answered after the modal had moved to task B, Reassign this task? opened over B. Confirming it cancelled B's run and handed B to the person picked for A (#3915).

AssigneePicker now keys its trigger/list by taskId. Each task gets its own picker instance, so a task switch drops the old instance's live-run verdict, open confirm, busy state and open list:

  • A live-run read for A that answers after the move opens no confirm on B and cancels nothing. React drops the state update on the unmounted A instance.
  • A confirm that was open when the task switched closes, and nothing is cancelled.
  • A change that needs no confirm still applies to A, through A's own render-time onAssign closure (the task modal binds task._id there). B's onAssign is never called.
  • B's own handoff completes for B alone, even when A's read answers in the middle of it.

I chose the key over storing the task id in the pending state. With the key, every closure the handoff uses (reads, the cancel, onAssign) belongs to one task. The cancel is never bound to A while the assign goes through B's callback. The fix lives in the picker, so the board card and the list row are covered too; those already mount one picker per task.

No new copy, so there are no locale changes. The register line is in services/platform/tests/manual/reference/automation.md, as a new paragraph right after the "Automation-owned tasks retain their workflow name…(#3849)" paragraph.

Backend

No change is needed. assignTask already refuses a reassign while a live run holds the task (TASK_HAS_LIVE_RUN, backend/domains/tasks/service.ts), which is why the picker cancels first. The wrong-target requests were well-formed and authorized writes for B, so the server cannot tell they were meant for A. The defect was client state crossing task identities.

How I verified it

  • New app/features/tasks/components/assignee-picker.handoff.test.tsx (jsdom) has 5 tests. Each live-run read is held per task, and the test re-renders the same picker element with the next task's props, the way the task modal does:

    1. normal path: the confirm cancels A's run and assigns A
    2. a delayed read after the move to B: no confirm, no cancel, no assign on either task
    3. a confirm left open (agent run) closes on the switch, and nothing is cancelled
    4. a change that needs no confirm applies to A only
    5. fast switching: A's read answers while B's own confirm is open, and B is cancelled and assigned with B's pick
  • New app/features/tasks/components/task-modal.assignee-handoff.test.tsx (jsdom, with the access suite's seams) runs the issue's path through the real TaskModal, EditTaskBody, TaskParentLink, AssigneePicker and ConfirmDialog. It opens subtask A (automation-assigned, live run), picks Alex, clicks the real Part of TEST-2 link, waits for B's heading, then answers A's held read. No Reassign this task? opens over B, and nothing is written. The same-task control confirms and writes exactly the cancel {organizationId:'org-1',taskId:'task-a'}, then assignTask {taskId:'task-a',assigneeType:'user',assigneeId:'user-1'}.

  • Red on main (assignee-picker.tsx from origin/main):

    • Picker suite: tests 2, 3 and 5 fail; 1 and 4 (the controls) pass. In test 5, main hands B to user-1, A's pick.
    • Modal suite: the late-read test fails because the confirm is open over B; the control passes.
  • Green on this branch, one worker: the 3 handoff files (21 tests). Earlier, on the fix commit: the 5 new picker tests, assignee-picker.test.tsx and the jsdom suites that render the picker's consumers (task-modal access/attachments/draft/limits/repeat, task-card, tasks-list, tasks-workspace, board dnd). That is 13 files and 115 tests passing.

  • oxlint and oxfmt --check on the changed files are clean, and bun run lint:manual passes.

  • Scoped tsc (changed and new files, consumers and app/router.tsx): exit 0.

  • Register anchor, checked with git merge-tree against the head of each of the 41 open PRs that edit automation.md:

  • Real Chromium: I ran both new suites unchanged in headless Chromium 1194 through a review-only vitest browser config kept outside the repo (define: { global: 'globalThis' } for tests/setup-ui.ts, server.fs.allow for the workspace). With the fix, 7/7 pass. With main's picker, 4/7 fail: the same cases as in jsdom.

Not covered: the full app against a live backend, and a live run's cancellation. The tests observe the request arguments at the client boundary.

Closes #3915

Current-main rebase

Replayed the previously accepted source 11b635b6f5e2d11b63fc0ed78ec2ebd2e979d0b1 onto main d1373d84cd56972501403f62145ec52e6f65d44a, including the merged shared CI repair in #4625. The accepted feature payload and all current-main changes are preserved in one atomic commit. Configured commit and conflict checks pass; earlier behavioral proof remains recorded above. All seven native required checks and full merge-group validation remain required for this new source.

Maintenance replay: preserves the accepted feature payload on main fd277c4, including merged #4649, #4650 and #4655. Retains the exact independently accepted one-line shared CLI inventory repair from #4654 (252f0df), pending native merge on main. The #4282 task-register union, where applicable, retains the accepted feature row and current-main rows. Existing behavioral evidence remains recorded above; no fresh full-feature/full-workspace or hosted-green claim. All seven native required contexts and full merge-group validation remain mandatory.

@yannickmonney

Copy link
Copy Markdown
Contributor Author

Independent review for TALE-911 / TALE-331 / TALE-359 (Codex #11, not implementer #6).

Exact head: d46b926b7b48d0a29289a5cf96a98203bda1257b, rechecked immediately before posting.

Verdict: no blocking code finding in this change. CI and complete local type/consumer validation remain outstanding; this is not a green-CI or merge authorization verdict.

The key is on both possible state-owning picker subtrees. Switching task identities unmounts the old list/confirmation and prevents a late guarded response from installing state on the next task. The asynchronous direct-assignment and confirmed-cancellation continuations retain the original task's callbacks and IDs. The modal's actual assignment closures capture task._id; neither cancellation nor assignment migrates to the task now displayed. Normal handoff and the next task's own handoff remain covered. Backend assignment checks tenant/task access and TASK_HAS_LIVE_RUN; workflow cancellation checks task access before cancelling its subject-scoped run, and agent cancellation matches org/task/run/live status. No copy changed: EN/DE/FR catalogs need no update.

Validation with existing dependencies and /opt/node/bin/node, one worker:

  • Handoff picker, modal handoff, and existing picker suites: 3 files / 21 tests pass, using an external review-only server.fs.allow config after the expected font-asset denial.
  • Consumer run: 52 tests pass, one description-cap test hit its default 5-second timeout, and two suites (tasks-list, tasks-workspace.renders) could not import the missing local @tanstack/react-virtual dependency. Follow-up of description limits plus board-drag hook with a 30-second timeout: 2 files / 15 tests pass. No dependencies installed.
  • Scoped oxlint: exit 0. Scoped oxfmt: all 3 TS/TSX files pass; Markdown is ignored by repository format policy.
  • Scoped tsc over changed TS/TSX, picker consumers and router/import closure: not green. After resolving workspace packages to this exact checkout, only 3 diagnostics remain: absent @tanstack/react-virtual and the consequent 2 implicit-any errors in unchanged windowed-task-rows.tsx. No diagnostics in changed files. This is an existing-dependency limitation, not evidence of a PR regression.
  • git merge-tree against current origin/main (f458cb903 at review start): clean, including the automation-register paragraph. Broader open-PR collision probing was stopped because partial-clone history downloads were slow; no fleet-wide collision claim.

CI still has queued scope/commitlint checks and skipped candidate-source checks, with no failed result to attribute. Main's compact-age test repair is confirmed as ac7f3f55e979d0386ddc40e465bc64f442b9ae81; no CI reruns were requested. A live backend cancellation and full-app browser navigation were not exercised here.

Posting here as the requested fallback for TALE-331 and TALE-359 (3729d02a-eb44-42b2-a68e-1bec2cebbcd6): the workspace service returned workspace_status unavailable / not_granted, so direct task comments could not be posted. No push, merge, CI rerun, or task status change.

@yannickmonney
yannickmonney force-pushed the fix/assignee-handoff-task-identity branch from d46b926 to 235f128 Compare October 9, 2026 02:24
@yannickmonney
yannickmonney force-pushed the fix/assignee-handoff-task-identity branch 4 times, most recently from 11b635b to 7c1d251 Compare October 9, 2026 14:09
@yannickmonney
yannickmonney force-pushed the fix/assignee-handoff-task-identity branch from 7c1d251 to cdaa93e Compare October 9, 2026 15:22

This branch has not been deployed

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

Bug: delayed assignee handoff cancels and reassigns the next task

1 participant