Skip to content

fix(witan): task_claim backs off between CAS retries, reports contention instead of raising raw - #249

Merged
blarghmatey merged 3 commits into
mainfrom
worktree-task-claim-retry-backoff
Aug 18, 2026
Merged

fix(witan): task_claim backs off between CAS retries, reports contention instead of raising raw#249
blarghmatey merged 3 commits into
mainfrom
worktree-task-claim-retry-backoff

Conversation

@blarghmatey

Copy link
Copy Markdown
Member

What are the relevant tickets?

Fixes tk-task-claim-exhausts-its-3-attempt-no-backoff-cas-674414 (witan work-coordination graph).

Description (What does it do?)

Recent sessions were hitting a raw internal error when calling task_claim:

write authority 'table_head:node:Task' changed during preparation

task_claim's CAS retry loop (mcp/servers/witan/witan/server.py) fires a
conditional write and, on a surfaced OmnigraphConflict, re-reads to decide
whether a rival actually holds the task (lost_race) or the conflict was
unrelated (another write elsewhere on the graph). The bug was in the
"unrelated conflict" path:

  • Only 3 attempts, fired back-to-back with no backoff.
  • On the 3rd conflict it did a bare raise, letting the raw
    OmnigraphConflict — whose message is the "write authority ... changed
    during preparation" prose — escape straight through the @_tool wrapper
    (which does no exception handling) to the MCP client.

node:Task is one of the hottest tables in this graph (every
claim/update/close from every session writes it), and this repo's own
write-gate investigation (tk-the-write-gate-is-sized-against-a-3-45s-solo-wri-73fc2b)
already measured loaded writes taking 17-44s under contention — three
immediate, unbacked-off retries have no chance against that.

Changes:

  • _CLAIM_MAX_ATTEMPTS: 3 → 5.
  • New jittered backoff (_claim_backoff, 0.25s base, 3s cap) between retry
    attempts, so concurrent claimers on the same task/table don't collide in
    lockstep.
  • On exhaustion, return a structured {"claimed": false, "reason": "contention", "remedy": ...} instead of letting the raw omnigraph
    exception escape. Logs witan.task_claim.contention_exhausted for
    observability.
  • Docstring updated to document the new "contention" reason value.

This does not address the underlying multi-second write contention itself —
that's the scope of the write-gate/batching work already tracked separately.
This just stops task_claim from leaking internal transport errors to callers
when it hits that contention.

How can this be tested?

  • New tests in mcp/servers/witan/tests/test_tasks.py:
    • test_claim_exhausted_conflicts_report_contention_not_raise — simulates
      every retry attempt hitting an unrelated OCC conflict (no rival ever
      holds the task) and asserts task_claim returns {"claimed": false, "reason": "contention"} rather than raising, and that the task is left
      untouched (status: "open").
    • test_claim_retries_back_off_between_attempts — asserts a sleep occurs
      between a conflicting attempt and its retry, with the expected backoff
      magnitude.
  • Full suite run locally: mcp/servers/witan — 835 passed (0 failed, 0
    skipped beyond the pre-existing omnigraph-not-on-PATH skips).
  • ruff check / ruff format: confirmed zero new lint debt against main
    (same 18 pre-existing rule violations, none introduced by this change);
    both changed files format-clean.

Additional Context

Filed and diagnosed after a direct report: "Recent Claude sessions are
hitting a witan error when interacting with the graph: write authority
'table_head:node:Task' changed during preparation". Root-caused by reading
the CAS classification path in witan_core/omnigraph.py (confirmed the
"write authority" prose is correctly classified as retryable — the defect is
entirely in task_claim's own retry budget, not the classifier) and cross-
referencing the write-gate/mutual-exclusion investigations already tracked
under wp-witan-multi-user-service-deployment-dcf6ee.

Copilot AI balanced review requested due to automatic review settings August 18, 2026 18:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Improves task_claim conflict handling under graph contention.

Changes:

  • Adds bounded jittered retry backoff.
  • Returns structured contention responses instead of raw exceptions.
  • Adds contention and backoff tests.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
mcp/servers/witan/witan/server.py Implements retry backoff and contention reporting.
mcp/servers/witan/tests/test_tasks.py Tests retry exhaustion and backoff behavior.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread mcp/servers/witan/witan/server.py
Comment thread mcp/servers/witan/witan/server.py Outdated
blarghmatey added a commit that referenced this pull request Aug 18, 2026
…AS conflict

Copilot review on #249 found that the new backoff sleep widens a pre-existing
race: `_update_task` merges `status` from `claim` unconditionally regardless
of what its own fresh read shows, so a retry that doesn't revalidate first
could silently resurrect a task that was closed (or newly blocked) during the
backoff window. Check the post-conflict re-read for closed/blocked and bail
with the real reason before ever looping back into another write. Also
softened the exhausted-contention message, which overclaimed the conflict was
"unrelated"/"elsewhere" when the branch-head precondition can't actually
distinguish that from same-task causes it already rules out (closed/blocked)
or tolerates (a released or same-holder update, or a force-steal target).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHKMgDYCszjXM1nRg9Jm1r
@blarghmatey

Copy link
Copy Markdown
Member Author

Addressed Copilot's review: both threads fixed in fb82673 (revalidate closed/blocked before retrying a CAS conflict; softened the exhausted-contention message to not overclaim the cause) and resolved. All checks green except witan-code (code graph), which is unrelated to this PR — its "Install omnigraph binary" step fails on a checksum mismatch for the pinned edge tag (upstream moved the tag), not on anything this diff touches. Reproduced the same failure against this branch's parent commit (before any task_claim changes), so it's pre-existing and would fail on any PR right now. Leaving it as-is rather than touching the pin in this PR — that's a separate, deliberate call (the install script explicitly refuses to proceed on a mismatch).

blarghmatey and others added 3 commits August 18, 2026 15:19
…ention instead of raising raw

task_claim's CAS retry loop fired 3 immediate, unbacked-off attempts and, on
exhaustion, re-raised the raw OmnigraphConflict straight through the MCP
boundary — leaking omnigraph's internal "write authority ... changed during
preparation" text to callers whenever an unrelated write kept colliding on a
hot table (node:Task, written by every claim/update/close). Widen the budget,
add jittered backoff between attempts, and report exhaustion as a structured
{"claimed": false, "reason": "contention"} instead.

Fixes tk-task-claim-exhausts-its-3-attempt-no-backoff-cas-674414.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHKMgDYCszjXM1nRg9Jm1r
…AS conflict

Copilot review on #249 found that the new backoff sleep widens a pre-existing
race: `_update_task` merges `status` from `claim` unconditionally regardless
of what its own fresh read shows, so a retry that doesn't revalidate first
could silently resurrect a task that was closed (or newly blocked) during the
backoff window. Check the post-conflict re-read for closed/blocked and bail
with the real reason before ever looping back into another write. Also
softened the exhausted-contention message, which overclaimed the conflict was
"unrelated"/"elsewhere" when the branch-head precondition can't actually
distinguish that from same-task causes it already rules out (closed/blocked)
or tolerates (a released or same-holder update, or a force-steal target).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHKMgDYCszjXM1nRg9Jm1r
Publishing is triggered by a push to main touching pyproject.toml, so this
makes the task_claim contention/backoff fix (and the closed/blocked
revalidation follow-up) actually ship on merge instead of silently landing
unpublished until some later PR's bump picks it up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JHKMgDYCszjXM1nRg9Jm1r
@blarghmatey
blarghmatey force-pushed the worktree-task-claim-retry-backoff branch from fb82673 to af69135 Compare August 18, 2026 19:29
@blarghmatey
blarghmatey merged commit b3ec853 into main Aug 18, 2026
14 checks passed
@blarghmatey
blarghmatey deleted the worktree-task-claim-retry-backoff branch August 18, 2026 19:35
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.

2 participants