fix(task-board): don't stomp a human reassignment when un-delegating a column-rule claim - #6828
Merged
Merged
Conversation
…a column-rule claim
runColumnAutomation() claims an unassigned card for the Super Agent with a
conditional UPDATE (assignee_id IS NULL), then calls
reactToSuperAgentDelegation(), which can take a while (dispatch, quota
check) or throw a TaskQuotaError. On that throw it un-delegated with a
plain update({ assigneeId: null, assignedBy: null }) — unconditional, so
if a human reassigned the card in that window (e.g. via
TASK_BOARD_ITEM_UPDATE) while the claim was in flight, this write silently
wiped their reassignment back to null.
storage.taskBoard already has the right primitive for exactly this:
unassignSuperAgent(), a conditional UPDATE (WHERE assignee_id =
SUPER_AGENT_ASSIGNEE_ID) used by handTaskToHuman() for the identical
race. Swapped the plain update() for it here too, so un-delegation only
takes effect if the card is still assigned to the Super Agent.
Verified: bunx tsc --noEmit and oxlint on the touched file are clean.
unassignSuperAgent() itself is exercised by handTaskToHuman()'s existing
integration coverage; this PR reuses that same tested primitive rather
than adding new DB-dependent test infra this sandbox can't run.
Reviewer check: rg unassignSuperAgent apps/api/src/tools/task-board — both call sites now share the same fence.
Contributor
There was a problem hiding this comment.
1 issue found across 1 file
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/api/src/tools/task-board/run-column-automation.ts">
<violation number="1" location="apps/api/src/tools/task-board/run-column-automation.ts:77">
P2: When the quota rejection unassigns a card, the new helper clears only `assigneeId` and leaves `assignedBy` from the failed delegation. Preserve the conditional fence but also clear `assigned_by` in the successful `unassignSuperAgent()` update so unassigned cards do not retain stale delegation metadata.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| ); | ||
| return await ctx.storage.taskBoard.update( | ||
| // Same conditional fence as `handTaskToHuman` — never stomp a concurrent reassignment. | ||
| const undelegated = await ctx.storage.taskBoard.unassignSuperAgent( |
Contributor
There was a problem hiding this comment.
P2: When the quota rejection unassigns a card, the new helper clears only assigneeId and leaves assignedBy from the failed delegation. Preserve the conditional fence but also clear assigned_by in the successful unassignSuperAgent() update so unassigned cards do not retain stale delegation metadata.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/tools/task-board/run-column-automation.ts, line 77:
<comment>When the quota rejection unassigns a card, the new helper clears only `assigneeId` and leaves `assignedBy` from the failed delegation. Preserve the conditional fence but also clear `assigned_by` in the successful `unassignSuperAgent()` update so unassigned cards do not retain stale delegation metadata.</comment>
<file context>
@@ -73,12 +73,13 @@ export async function runColumnAutomation(
);
- return await ctx.storage.taskBoard.update(
+ // Same conditional fence as `handTaskToHuman` — never stomp a concurrent reassignment.
+ const undelegated = await ctx.storage.taskBoard.unassignSuperAgent(
item.id,
orgId,
</file context>
…assignedBy Addresses cubic review on #6828. P1: `unassignSuperAgent()` returns null when a human won the reassignment race, and the `?? delegated` fallback then handed back the stale Super Agent snapshot — Jira/clients would render the human's reassignment as reverted. Re-read the row instead. P2: `unassignSuperAgent()` cleared `assignee_id` but left `assigned_by` from the failed delegation, so an unassigned card kept stale delegation metadata. Clear it inside the same conditional UPDATE, which also fixes the identical leak on `handTaskToHuman`'s path. `enqueue-task-run`/`nudge-thread`/ `review-sweeper` all read it as `assignedBy ?? createdBy`, so the fallback already covers the null. Test: real-Postgres case in task-board-column-claim.integration.test.ts covering both the winning clear and the lost fence leaving a human's card untouched.
pedrofrxncx
enabled auto-merge (squash)
September 1, 2026 19:22
decocms Bot
pushed a commit
that referenced
this pull request
Sep 1, 2026
PR: #6828 fix(task-board): don't stomp a human reassignment when un-delegating a column-rule claim Bump type: patch - decocms (apps/api/package.json): 4.315.4 -> 4.315.5 - @decocms/native (apps/native/package.json): 4.315.4 -> 4.315.5 Deploy-Scope: server
pedrofrxncx
added a commit
that referenced
this pull request
Sep 1, 2026
…ion (#6835) * fix(task-board): don't stomp a human claim in the PR-open board reaction applyBoardDecision reads openCards (and target.assigneeId) before calling the LLM to decide create-vs-update, which can take seconds. It then wrote claimSuperAgent based on that stale assigneeId with a plain storage.update() — no WHERE fence — so a human claiming the card while the LLM call was in flight got silently overwritten back to the Super Agent. storage.taskBoard.claimUnassignedForSuperAgent() is the exact primitive #6828 just used to fix the identical race in runColumnAutomation (a conditional UPDATE requiring assignee_id IS NULL and the card's original status). Reused it here: the claim now happens through that fence, and the follow-up storage.update() only touches status, never assignee fields. Added an integration test simulating the race (stale openCards snapshot, then a human claim lands before applyBoardDecision runs) asserting the human's assignment survives. * fix(task-board): fence the PR-open advance on the card's live status The claim added in the previous commit could lose its race and return null while the status write that followed went ahead unconditionally, replaying an advance decided against a snapshot taken before the LLM call — dragging a card someone had since shipped back to In Progress. Fold both writes into one conditional UPDATE fenced on that snapshot's status, claiming for the Super Agent only when the row is still unowned. Losing the race now links the PR onto the card as it stands instead of moving it. --------- Co-authored-by: Pedro França <pedrofrxncx@deco.cx>
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.
Follows #6800 (
runColumnAutomation), which introduced the shared column-rule claim path both the Jira sync and a manual drag now go through.Bug:
runColumnAutomation()claims an unassigned card for the Super Agent with a conditional UPDATE (WHERE assignee_id IS NULL), then callsreactToSuperAgentDelegation(), which can take a while (dispatch/quota check) or throwTaskQuotaError. On that throw it un-delegated with a plainupdate({ assigneeId: null, assignedBy: null })— unconditional. If a human reassigned the card in that window (e.g. viaTASK_BOARD_ITEM_UPDATE, in the gap between the claim and the quota check), this write silently wiped their reassignment back tonull, losing the human's edit.Fix:
storage.taskBoardalready has the right primitive for exactly this race —unassignSuperAgent(), a conditional UPDATE (WHERE assignee_id = SUPER_AGENT_ASSIGNEE_ID) already used byhandTaskToHuman()to solve the identical problem (see its doc comment: "a plainupdate()would stomp that human's reassignment back to null"). Swapped the plainupdate()for it inrun-column-automation.tstoo, so un-delegation only takes effect if the card is still assigned to the Super Agent; otherwise it leaves the human's reassignment alone.Failure scenario: column rule claims card X for the Super Agent → dispatch is mid-flight and the org's quota is exhausted → a teammate manually reassigns X to themselves in the meantime → the quota rejection's un-delegate write lands after and clobbers the teammate's assignment back to unassigned, silently losing their action.
How a reviewer confirms:
rg unassignSuperAgent apps/api/src/tools/task-board— both call sites (run-reactions.ts'shandTaskToHuman, and nowrun-column-automation.ts) share the same conditional fence and doc-comment rationale.Checks run locally:
bun run fmt,cd apps/api && bunx tsc --noEmit(clean),bunx oxlint apps/api/src/tools/task-board/run-column-automation.ts(0 warnings/errors). No new test: this reusesunassignSuperAgent(), an existing storage primitive already exercised byhandTaskToHuman()'s integration coverage — this sandbox has no Postgres to add a new DB-backed test, and the fix is a one-line swap onto an already-tested method. Full CI validates the integration suite.Summary by cubic
Fixes a race where un-delegating a column-rule claim could overwrite a human's concurrent reassignment, reusing the conditional
unassignSuperAgent()primitive instead of a plain update.assignedBy, fixing stale delegation metadata on unassigned cards (also on thehandTaskToHumanpath).Written for commit 325903a. Summary will update on new commits.