Skip to content

docs(ax): entry 49 — a progress note re-claimed a row a human had re-pointed 48h earlier - #1325

Open
lilyshen0722 wants to merge 3 commits into
mainfrom
docs/ax-49-lapsedfrom-restore
Open

docs(ax): entry 49 — a progress note re-claimed a row a human had re-pointed 48h earlier#1325
lilyshen0722 wants to merge 3 commits into
mainfrom
docs/ax-49-lapsedfrom-restore

Conversation

@lilyshen0722

Copy link
Copy Markdown
Contributor

What happened

commonly_update_task is 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-impl at 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: pendingclaimed, claimedBy set to me, a 30-minute lease opened, lapsedFrom cleared.

Why

backend/routes/tasksApi.ts:537 has a second findOneAndUpdate for the case where the row was swept back to pending before the note landed:

{ podId: podFilter, taskId, status: 'pending', lapsedFrom: { $in: identities } }

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.

lapsedFrom carries 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 match lapsedFrom either.

The row's assignee is null — Sam re-points by writing prose no predicate reads — so the only machine-readable owner is claimedBy, which is exactly what the restore overwrites from a field describing two days ago.

What was verified rather than inferred

  • The claiming id is mine, by comparison against a row I know I hold — not assumed from a bare id.
  • leaseRenewed: true and lapsedFrom: null afterwards, read from the persisted response body.
  • lapsedFrom must have held the string pod-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 lapsedFrom when 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. A lapsedAt bounded 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: main carries 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

…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.
samxu01 pushed a commit that referenced this pull request Aug 28, 2026
#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.
lilyshen0722 and others added 2 commits August 28, 2026 16:55
…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>
@lilyshen0722

Copy link
Copy Markdown
Contributor Author

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: status: pending, lapsedFrom: null, claimedBy still null after other writes. That is an observation — the row sat unclaimed and I inferred the restore branch had not fired.

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:

status: "pending"
claimedBy: null
claimExpiresAt: null
lapsedFrom: null
leaseRenewed: false

So an update from an identity that is not in lapsedFrom falls to the note-only fallback and claims nothing, on a pending row, with the note appended normally. That is the discriminator this entry names, exercised rather than inferred, by the caller the entry is about.

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 lapsedFrom to name you, and lapsedFrom is set only by a lapse sweep.

Head is unmoved at 1d777174 and this needs no re-gate; I am not asking for one. Filing it as a comment rather than in the body precisely so the tree @sprint-review reviewed stays the tree that presses.

@lilyshen0722

Copy link
Copy Markdown
Contributor Author

Population-scale control for this entry, measured 04:55Z at origin/main ccacf023. The body cites one row (TASK-084) as the clean control; here is the whole board.

Read every claimed row in the sprint pod — 8 rows, TASK-023/024/067/069/074/079/085/086. All 8 carry lapsedFrom: null and rescueDeferrals: 0. Since the restore branch at backend/routes/tasksApi.ts:537 matches {status:'pending', lapsedFrom:{$in:identities}}, that means the hazard this entry describes is currently unarmed across the entire board — there is no row a task update could silently re-claim right now.

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": lapsedFrom is written only by a sweep that goes unanswered. A deferral wake arrives, the holder renews, and lapsedFrom stays null — which is what all 8 rows show. The field only acquires a value when a holder stops listening or misses the window. So the population at risk is exactly rows abandoned mid-flight, which is also the population most likely to be re-pointed to someone else — the two conditions that make this dangerous are correlated, not independent.

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 rescueDeferrals to 0, but the already-queued wake still names the pre-renewal count. I took a wake reading "1 deferral left" at 04:55:34Z on a row whose lease ran to 05:24:56Z with rescueDeferrals: 0 — my renewal had landed 56 seconds after the sweep note was written and before the wake reached me. Acting on the wake's urgency would have meant answering a trigger already answered. Same shape as the entry's core point: the row is the state, the wake is a claim about the past.

No head move — 1d777174 stands, so the existing review still points at the tree that presses.

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.

1 participant