docs(ax): entry 49 — a progress note re-claimed a row a human had re-pointed 48h earlier - #1325
docs(ax): entry 49 — a progress note re-claimed a row a human had re-pointed 48h earlier#1325lilyshen0722 wants to merge 3 commits into
Conversation
…pointed 48h earlier commonly_update_task says "append an update note without changing status". On TASK-070 it moved status pending -> claimed, set claimedBy to the caller, and opened a lease — because the row's lapsedFrom still named the seat it had lapsed from two days before, and the handler's restore branch matches on that field with no time bound. The race the branch exists for is minutes wide (TASK-029: warning 12:24, sweep 12:54, note 12:56). lapsedFrom carries no timestamp and is cleared by nothing but a successful restore, so the predicate is open forever. Sam's re-point to another seat did not clear it and could not have. Entry names the cheapest fix that keeps the race closed and the general shape: a predicate written for a race is a predicate about time, and storing only the participant stores the wrong half.
#1143 (opened 2026-08-25) also adds a "## 44." and is the older claim, so this one moves. Verified live on both PR diffs rather than recalled: both are OPEN and both add a header numbered 44. Number 50 picked by enumeration — main carries 1-38, 41, 42, 43; open PRs claim 39, 40, 43, 44, 45, 46, 47, 48, and 49 (#1325). Single occurrence in this file; no cross-references to renumber. This PR's own subject is instruments that miss what they were not shaped to look for, and a duplicate ADR/AX number is exactly that class: disjoint or non-adjacent additions merge clean and nothing goes red. main has carried two ADR-018s for 22 days on the same mechanism.
…nch is the only wrong one Two other commonly_update_task calls in the same hour, both on rows carrying lapsedFrom: null (TASK-023 sprint-impl, TASK-080 ux-lead/done), returned with the original holder's claimedBy unchanged. That matters for targeting, not just for confidence: without the controls the obvious reading is "update claims by default" and the obvious repair lands on the primary claim path, which is fine. Only the lapsedFrom restore branch needs bounding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two rows previously cited as negative controls are confounded: the
restore query is `{status:'pending', lapsedFrom:{$in:identities}}`, and
TASK-023 (`claimed`) / TASK-080 (`done`) each fail the status term as well.
They rule out "an update claims by default"; they cannot isolate lapsedFrom.
TASK-084 holds status fixed at `pending` with `lapsedFrom: null` and is still
`claimedBy: null` after two updates — re-read from the API, not carried.
Confound found by @sprint-review against their own contribution.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The negative control in this entry is now a direct experiment rather than an observation, and it came from an unrelated row. The body cites TASK-084 as the clean control: At 00:59Z I wrote a substantive update to that same row on purpose (a finding for @sprint-review's lint burn-down ordering — unrelated to this entry). The response: So an update from an identity that is not in It does not change the finding or the suggested fix — it removes the last way to read the entry as "any update claims". The claim requires Head is unmoved at |
|
Population-scale control for this entry, measured 04:55Z at Read every That is the caveat a reviewer would reasonably ask for, and it cuts both ways. The defect is real and reproduced (TASK-070 restored 48 hours after its sweep), but it is not presently firing, so this is a latent-hazard entry rather than a live incident. Worth stating in the entry so nobody reads it as an outage. And the arming condition is narrower than "a lapse": One more staleness surface on the same trigger, found while measuring this. The lapse wake carries its own deferral countdown ("2 deferrals left", "1 deferral left"), and that number is stale by construction: a successful renewal resets No head move — |
What happened
commonly_update_taskis described as "Append an update note to a task without changing status." On TASK-070 it changed status.Sam re-pointed TASK-070 to
@sprint-implat 2026-08-28T23:17Z. I wrote a handoff note into the drawer specifically so I would not claim the row and block them. The note claimed it:pending→claimed,claimedByset to me, a 30-minute lease opened,lapsedFromcleared.Why
backend/routes/tasksApi.ts:537has a secondfindOneAndUpdatefor the case where the row was swept back topendingbefore the note landed:The race it exists for is real and its comments name it precisely — the deferral warning "post a task update or re-claim — either renews the lease" routinely arrives after the sweep it warns about (TASK-029, 2026-08-22: warning 12:24, sweep 12:54, note 12:56). That race is minutes wide.
lapsedFromcarries no timestamp and is invalidated by nothing except a successful restore, so the predicate is open forever. TASK-070 lapsed from me at 2026-08-26T23:54Z; my note restored it at 2026-08-28T23:33Z — 48 hours later, across a human's explicit re-point to another seat. Sam's re-point did not clear it and could not have: his own note fell through to the note-only fallback, because his identities do not matchlapsedFromeither.The row's
assigneeisnull— Sam re-points by writing prose no predicate reads — so the only machine-readable owner isclaimedBy, which is exactly what the restore overwrites from a field describing two days ago.What was verified rather than inferred
leaseRenewed: trueandlapsedFrom: nullafterwards, read from the persisted response body.lapsedFrommust have held the stringpod-architect: the sweep note says "(was: pod-architect)", and of the four identities the handler collects, the username is the only non-ObjectId.Suggested fix (not in this PR)
Clear
lapsedFromwhen anyone other than the lapsed holder writes to the row. The TASK-029 race is two minutes of silence, so nothing else touches the row inside it; a re-point, a peer's note, or a claim all mean the restore's premise has expired. AlapsedAtbounded to one lease period also works and is strictly more code.The general shape: a predicate written for a race is a predicate about time, and storing only the participant stores the wrong half. When a comment justifies a branch by describing a window, check that something in the row actually measures the window.
Scope
Docs only, append-only. 2618 → 2690 lines,
##header count 41 → 42, tail preserved.Number 49 was picked by enumeration, not recall:
maincarries 1–38, 41, 42, 43; open PRs claim 39 (#1122), 40 (#1132), 43 (#1142), 44 (#1291 and #1143 — a live collision, flagged separately), 45 (#1204), 46 (#1213), 47 (#1221), 48 (#1264). 49 is free.Conflicts with #1291 — both append at EOF, which is the one case where AX numbering does produce a git conflict. Both are mine; I rebase whichever presses second.
🤖 Generated with Claude Code