{"record":{"id":"75caacc4ff4aaf48","repo":"affaan-m/ECC","slug":"work-item-target-id-is-already-done-cannot-cla","errorCode":null,"errorMessage":"Work item ${target.id} is already done; cannot claim.","messagePattern":"Work item (.+?) is already done; cannot claim\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"scripts/lib/control-pane/work-item-mutations.js","lineNumber":69,"sourceCode":"/**\n * Claim an unassigned work item for an agent or human. Sets the owner (and\n * optional assigneeKind) and moves the card to running unless an explicit\n * status is supplied. Returns { claimed, item } or { claimed: false, reason }.\n */\nfunction claimWorkItem(store, { id, owner, assigneeKind, sessionId, status } = {}) {\n  if (!owner) {\n    throw new Error('claim requires an owner.');\n  }\n  const kind = assigneeKind ? String(assigneeKind).toLowerCase() : null;\n  if (kind && !VALID_ASSIGNEE_KINDS.has(kind)) {\n    throw new Error(\"assigneeKind must be 'agent' or 'human'.\");\n  }\n  const target = selectClaimTarget(store, { id });\n  if (!target) {\n    return { claimed: false, reason: 'no-unassigned-open-items' };\n  }\n  if (!isOpenStatus(target.status)) {\n    throw new Error(`Work item ${target.id} is already done; cannot claim.`);\n  }\n  const metadata = { ...(target.metadata || {}) };\n  if (kind) {\n    metadata.assigneeKind = kind;\n  }\n  const item = store.upsertWorkItem({\n    ...target,\n    owner,\n    sessionId: sessionId ?? target.sessionId ?? null,\n    status: status ?? 'running',\n    metadata,\n    updatedAt: new Date().toISOString()\n  });\n  return { claimed: true, item };\n}\n\n/**\n * Move a work item to a kanban lane (ready | running | blocked | done).","sourceCodeStart":51,"sourceCodeEnd":87,"githubUrl":"https://github.com/affaan-m/ECC/blob/01e15490f04e29cfefe3896951f43db46994d8ee/scripts/lib/control-pane/work-item-mutations.js#L51-L87","documentation":"Thrown by claimWorkItem when the resolved target item has a status in DONE_STATUSES (done, closed, resolved, merged, cancelled). Once a card is in a terminal lane it cannot be claimed again — claiming implies moving it to 'running', which would resurrect completed work and corrupt the board history. This differs from the no-id queue path, which simply skips done items via isOpenStatus.","triggerScenarios":"Calling claimWorkItem with an explicit id whose item was closed/done between the list read and the claim (TOCTOU); claiming by id that another worker already moved to done; UI showing a stale card that was resolved.","commonSituations":"Race between two agents: one finishes (moves to done) while the other tries to claim; a stale board view; manual status edit set the item to 'done' out of band.","solutions":["Refresh and re-check the item's status before claiming; if done, abort or pick a different item.","Wrap claim in try/catch and treat this specific message as a benign 'already finished' signal rather than an error.","For the JIT queue (no id), this never happens because selectClaimTarget filters by isOpenStatus — prefer omitting id.","Reduce the list→claim window to minimize TOCTOU races."],"exampleFix":"// before\nclaimWorkItem(store, { id, owner });\n\n// after — check freshness, treat done as skip\nconst fresh = store.getWorkItemById(id);\nif (fresh && ['done','closed','resolved','merged','cancelled'].includes(String(fresh.status).toLowerCase())) {\n  return { claimed: false, reason: 'already-done' };\n}\nclaimWorkItem(store, { id, owner });","handlingStrategy":"try-catch","validationCode":"const DONE = new Set(['done','closed','resolved','merged','cancelled']);\nif (id) {\n  const item = store.getWorkItemById(id);\n  if (item && DONE.has(String(item.status).toLowerCase())) {\n    return { claimed: false, reason: 'already-done' };\n  }\n}","typeGuard":"function isOpenItem(item) {\n  const DONE = new Set(['done','closed','resolved','merged','cancelled']);\n  return item && !DONE.has(String(item.status).toLowerCase());\n}","tryCatchPattern":"try {\n  claimWorkItem(store, { id, owner });\n} catch (e) {\n  if (/already done/.test(e.message)) { return { claimed: false, reason: 'already-done' }; }\n  throw e;\n}","preventionTips":["Re-check status immediately before claiming to shrink the TOCTOU window.","Prefer the JIT queue (omit id) which auto-skips done items.","Treat 'already done' as a benign skip, not a hard error."],"tags":["work-items","claim","state","lifecycle"],"backgroundTag":null,"analyzedSha":"01e15490f04e29cfefe3896951f43db46994d8ee","analyzedAt":"2026-08-13T00:31:08.655Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}