Coding Allocation Timeouts

Coding Allocation Timeout Policy

Baseline

DigitVA treats coder and reviewer allocations as time-bound workflow reservations.

Current baseline:

  • active coding and reviewing allocations older than 1 hour are considered stale
  • stale allocations must be released automatically
  • stale allocation release must distinguish first-pass coding, recode episodes, and reviewer sessions

Release runs through the Celery beat sweep and, at most once per CODING_STALE_SWEEP_MIN_INTERVAL_SECONDS (30 s, shared across workers through the cache), on a coder's allocation request. The throttle only delays the release of a timed-out case by up to that interval; a case a coder validly holds (coding_in_progress, coder_step1_saved) is never in the pool, so it cannot be handed out meanwhile. The throttle also delays the release of the requesting coder's own allocation if it timed out inside the last 30 s: in that window a pick of another case gets 409 allocation_exists, and a random start resumes the timed-out case until the next sweep releases it.

Required behavior — first-pass coder

When a first-pass coding allocation becomes stale:

  • deactivate the active va_allocations row for coding
  • deactivate unfinished active va_initial_assessments rows for the timed-out coder
  • deactivate first-pass va_narrative_assessments rows for the timed-out coder
  • deactivate first-pass va_social_autopsy_analyses rows for the timed-out coder
  • return canonical workflow state to ready_for_coding
  • write an audit log entry for the release

Required behavior — recode

When a recode coding allocation becomes stale:

  • deactivate the active va_allocations row for coding
  • deactivate unfinished active va_initial_assessments rows for the timed-out coder
  • abandon the active recode episode
  • preserve the currently authoritative final COD
  • preserve saved NQA and Social Autopsy analysis artifacts from the recode window
  • return canonical workflow state to coder_finalized
  • write an audit log entry for the release

Required behavior — reviewer session

When a reviewer allocation becomes stale:

  • deactivate the active va_allocations row for reviewing
  • deactivate active va_reviewer_reviews rows for the timed-out reviewer
  • deactivate active va_narrative_assessments rows for the timed-out reviewer
  • deactivate active va_social_autopsy_analyses rows for the timed-out reviewer
  • return canonical workflow state to reviewer_eligible
  • write an audit log entry for the release

Rationale: the reviewer final COD submission is the only terminal action for a reviewer session. The review, NQA and Social Autopsy saves are partial saves. If the session times out before the final COD is submitted, they are deactivated and the submission returns to reviewer_eligible so a fresh reviewer session can start. The reviewer's saved Step 1 (va_reviewer_initial_assessments, masked projects) is not touched: it is kept, and a fresh session by the same reviewer finds it (owner decision 2026-10-05, reviewer-release-keeps-step1).

Reviewer release

Owner decision 2026-10-05, digitva-xl43. One active reviewing allocation per reviewer stays. A reviewer may also release their own active reviewing allocation by choice (POST /api/v1/reviewing/allocation/release), for example after starting a review of a case they do not want to review.

The effect is the same as the reviewer-session timeout, through _release_reviewer_allocation in app/services/coding_allocation_service.py:

  • the allocation is deactivated
  • the reviewer's review, NQA and Social Autopsy analysis are deactivated
  • the reviewer's saved Step 1 is kept
  • the case returns to reviewer_eligible
  • an audit log entry reviewer_allocation_released_by_reviewer is written under the reviewer's user id

Only the allocation's own reviewer may release it, and only while it is active and the case is in reviewer_coding_in_progress (otherwise 409 no_allocation / wrong_state). The automatic release after the timeout above stays as it is. The invariant below applies to a reviewer release too.

Coder release

Owner decision 2026-10-05, digitva-xuxk. One active coding allocation per coder stays. A coder may also release their own active coding allocation by choice (the "Release" action), for example after taking a case with "Code this case now" that they do not want to code.

The effect is the same as the timeout release, through _release_coding_allocation in app/services/coding_allocation_service.py:

  • first pass: unfinished Step 1 and first-pass work on that case are deactivated and the case returns to ready_for_coding
  • recode: the episode is abandoned and the case returns to coder_finalized, with the authoritative final COD kept
  • an audit log entry is written for the release

Only the allocation's own coder may release it, and only while it is active. The automatic release after the timeout above stays as it is. The invariant below applies to a coder release too.

One active allocation, enforced

Per side since digitva-rcwt: one on the real side and one on the training side (va_allocation_training, migration d6a1f3b8c2e4).

digitva-igq7. "One active coding allocation per coder" and "one active reviewing allocation per reviewer" are enforced by the partial unique index uq_va_allocations_active_to_for on va_allocations (va_allocated_to, va_allocation_for, va_allocation_training) WHERE va_allocation_status = 'active'. The allocators' "already holds one?" check is not enough on its own: two concurrent requests from one user for different cases both passed it. Now the second insert is refused, its savepoint is rolled back with nothing else written, and the allocator answers as its check would have: random allocation resumes the held case; a pick, "Code this case now", or a reviewer start resumes it when it is the same case and refuses with 409 allocation_exists otherwise; a recode or demo start refuses.

Migration a7c3e9f1d5b2 resolved existing duplicates before adding the index: the newest active allocation per (user, kind) stayed active, the others were marked deactive with an audit row va_allocation_deactivated_duplicate_active (role vasystem). It did not replay the release's workflow transitions or artifact deactivations, so a case behind a demoted allocation keeps its in-progress state; the migration prints the count. Dev had none.

Revocation when a web case is confirmed as a duplicate

Confirming a web case as a duplicate revokes every active coding and reviewing allocation on its submission at once, with the same release behaviour as above (first pass, recode, reviewer session), audited as va_allocation_revoked_duplicate / reviewer_allocation_revoked_duplicate instead of a timeout. See Coding Workflow State Machine Policy, "Confirmed Duplicate Cases".

Invariant for all timeout paths

The release action must not:

  • delete assessment or review records
  • alter submission content
  • replace the authoritative final COD with unfinished draft work

Execution model

The timeout release must run in two ways:

  • hourly as a scheduled background task (release_stale_coding_allocations_task covers both coder and reviewer allocations)
  • opportunistically:
    • when a coder starts normal coding (start_coding in coding_service.py)
    • when a reviewer starts a reviewer session (start_reviewer_coding in reviewer_coding_service.py)

This provides both proactive cleanup and local recovery even if the scheduled task has not run yet.