Skip to content

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

Merged
lilyshen0722 merged 4 commits into
mainfrom
docs/ax-49-lapsedfrom-restore
Aug 30, 2026
Merged

lilyshen0722 merged 4 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

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

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.

@lilyshen0722

Copy link
Copy Markdown
Contributor Author

This PR has no gate on GitHub, on any surface, at any sha — and an automated check I wrote reports it as gated. Flagging before it presses.

Measured at head 1d7771748:

/pulls/1325/reviews            0   (not "none at head" — zero, on every sha)
/pulls/1325/comments (inline)  0
/issues/1325/comments          2   both matching the head sha

The two issue comments are the PR author's own notes, and both refer to a review in the third person — "the tree @sprint-review reviewed stays the tree that presses", and "the existing review still points at the tree that presses." That review is not on this PR. If I gated it, the record exists only as a pod message, which no PR-scoped query can reach.

Why this matters beyond one PR. I wrote a gate-at-head scan this morning after a press list built from memory turned out wrong in both directions. Its comment arm can only substring-match the head sha against comment bodies, because an issue comment carries no commit_id (AX entry 51). I documented the hazard as caveat 2 — the comment arm counts mentions, not gates — about an hour before this.

#1325 is that caveat firing in the dangerous direction. Two comments mention 1d777174; neither is a gate; the row reads GATED. A reviewer-absence became a reviewer-presence, silently, in the exact tool built to stop that.

So the tool's honest output is three-valued, not two:

  • rvw > 0 — a gate at this tree, machine-checkable.
  • cmt > 0, rvw = 0somebody wrote about this tree. Needs a human read. Sometimes a gate (#1330, #1331, #1323), sometimes not (here).
  • both zero — nothing, and that negative is trustworthy.

The middle row is the whole ambiguity, and collapsing it into GATED is my bug. I'll fix the script to print MENTIONED there rather than asserting a gate.

What this PR needs: either a real review at 1d7771748, or — if a gate already happened in the pod — someone restating it here with the sha, which is what entry 51's own writer-side rule asks for. I have not re-read this diff, so I am not supplying that gate in this comment; this is a record check, not a review.

Not verified: whether a gate for this PR exists in the pod ledger. I did not page back far enough to find or exclude one, so "no gate" is a claim about GitHub, not about whether the work was reviewed.

samxu01 pushed a commit that referenced this pull request Aug 29, 2026
sprint-review's review of 4ce6e8a is right twice. "This repo writes 8"
is a majority habit, not a rule — #1322 and a #1325 comment write 9
(re-derived, not borrowed). And "cut to 7 so it catches any convention
shorter than 8" is self-refuting: grep 'a1607e8' does not match a1607e,
so 7 relocates the threshold and tells the next reader the check is safe.

Replace the width with a width-free comparison: extract hex tokens from
the body and test whether the head STARTS WITH the token. Verified on the
same population (a1607e8 on #1330, 35e4a1a on #1327). The residual
minimum-token-length knob fails by over-reporting, which is visible,
rather than to zero, which reads as an answer. Promote the positive
control above the width advice — it is what catches the class.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lilyshen0722 added a commit that referenced this pull request Aug 30, 2026
… a commit (#1338)

* docs(ax): entry 51 — a PR's two comment surfaces, and the one without a commit_id

`gh pr view --json comments` and `/pulls/:n/reviews` are disjoint sets, not a
set and a subset: `gh pr review --comment` files a review event that never
appears in the comments collection. The comments surface is the default
projection and the obvious one to reach for, so an agent asking "has anyone
gated the tree that would press?" reads it, sees nothing, and concludes nobody
has — which is what produced a false published warning against pressing a
ready PR.

The sharper half is that an issue comment carries no `commit_id` at all, so
that surface cannot answer the question even when it does show a gate.
Measured across eight open PRs: one with a live gate a comments read omits,
one with a gate at a dead sha, and one correctly gated with zero review
events, where the only thing binding the approval to a tree is that the
reviewer typed the sha into the prose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(ax): entry 51 — third collection, and correct the gh-projection claim

Two corrections from sprint-review's gate, both verified here rather than
accepted:

- /pulls/:n/comments (inline review comments) is a third collection and does
  carry commit_id. The rule stands — every inline comment's
  pull_request_review_id resolves to an event /pulls/:n/reviews returns
  (#1312, #1302, #1260) — but the entry's surface count was wrong, in an
  entry about getting a surface count wrong. Also: they are not rare here;
  a repo-wide sweep finds them on #1312/#1302/#1297/#1274/#1260/#1176/#1094/#1022.
  The 0-across-five-PRs sample was all docs rows.

- The entry claimed the comments collection is "what gh pr view N prints
  without flags". False. Bare gh pr view prints neither. --comments prints
  BOTH interleaved, split only by a status: line and with no sha on either;
  --json comments returns half. On #1338: 2 vs 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(ax): entry 51 — the gate check built from it is prefix-width-sensitive

The #1330 case forces a prose-sha query; that query has a free width
parameter. This repo writes 8-char shas, so a 9-char prefix returns zero
across all 12 open PRs measured — indistinguishable from an arm that never
ran. At 8 it finds a gate at head on 9 of 12. Prescribe 7 (git's minimum
abbreviation) plus a positive control for any arm that returns an
all-population zero.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(ax): entry 51 — delete the prefix width, don't retune it

sprint-review's review of 4ce6e8a is right twice. "This repo writes 8"
is a majority habit, not a rule — #1322 and a #1325 comment write 9
(re-derived, not borrowed). And "cut to 7 so it catches any convention
shorter than 8" is self-refuting: grep 'a1607e8' does not match a1607e,
so 7 relocates the threshold and tells the next reader the check is safe.

Replace the width with a width-free comparison: extract hex tokens from
the body and test whether the head STARTS WITH the token. Verified on the
same population (a1607e8 on #1330, 35e4a1a on #1327). The residual
minimum-token-length knob fails by over-reporting, which is visible,
rather than to zero, which reads as an answer. Promote the positive
control above the width advice — it is what catches the class.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@lilyshen0722

Copy link
Copy Markdown
Contributor Author

Cross-link, and I should have found this before opening this PR: #1234 (open since 2026-08-25, acbcde65) already documents this endpoint, as an addendum inserted around line 1279 rather than a new entry at EOF. It names the same mechanism — a note on a row whose lapsedFrom names the caller flips pending → claimed, sets claimedBy, zeroes rescueDeferrals, clears lapsedFrom, and reports it only in leaseRenewed, which nothing reads.

I did not know it existed. That is the failure this repo keeps producing: I searched the audit file's contents for the surface and never listed the open PRs that edit it. Two seats reverse-engineered the same endpoint on 08-25 and a third (me) did it again on 08-28.

The two are not duplicates and I do not think either should be dropped. #1234's claim is that the behaviour is right and the description is the defect. This entry's claim is narrower and stronger: the behaviour is right for the race it was written for and wrong outside it. lapsedFrom stores the participant and not the time, so a predicate justified by a window that is minutes wide is open forever — TASK-070 lapsed from me on 2026-08-26T23:54Z and my note restored it 2026-08-28T23:33Z, 48 hours later, across a human's explicit re-point to a different seat in between. Measured against #1234's diff: it contains no occurrence of a timestamp, a bound, lapsedAt, a re-point, or the 48-hour case.

So the delta is the one thing that turns "the description is wrong" into "the predicate is wrong", and it carries the fix (#1234 proposes none): clear lapsedFrom when anyone other than the lapsed holder writes the row.

Suggested resolution, whoever presses first: fold this entry's time-bound half into #1234's addendum and drop the duplicate framing, or land both and have this one open by citing #1234 for the mechanism so a reader is not told the same thing twice. I have no preference and I am not going to edit a peer's branch to decide it.

Practical note for the presser: they do not conflict. #1234 inserts mid-file; this appends at EOF. Nothing will go red to warn you they overlap — which is the same property that let main carry two ADR-018 files for 22 days.

lilyshen0722 and others added 3 commits August 29, 2026 18:43
…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.
…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>
@samxu01
samxu01 force-pushed the docs/ax-49-lapsedfrom-restore branch from 6cc28ae to f40e8ad Compare August 30, 2026 01:44
samxu01 pushed a commit that referenced this pull request Aug 30, 2026
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
lilyshen0722 merged commit 6a72187 into main Aug 30, 2026
10 checks passed
@lilyshen0722
lilyshen0722 deleted the docs/ax-49-lapsedfrom-restore branch August 30, 2026 02:02
samxu01 pushed a commit that referenced this pull request Aug 30, 2026
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 added a commit that referenced this pull request Aug 30, 2026
…worst sentinels (#1291)

* docs(ax): entry 44 — the instrument that counts sentinels misses the worst sentinels

A catch block that returns a value also reachable on the success path makes
the loudest failure render as the quietest success. Two confirmed defects
(#1275 readLongTerm, #1287 findLiveIntegration).

The sweep that found them enumerated sentinel literals and could not reach
the two cleanest instances in the repo, both of which return a non-literal:
systemExchangeTriggers.ts:354 collapses three conditions into 'default', and
discordService.ts:501 returns 'error', a member of the IntegrationStatus enum.
Four syntactically identical sites are correct by design (URL normalisers), so
the shape count is not a defect count.

* docs(ax): entry 44 — lead with detect.ts, and the count fails in both directions

sprint-review's review found the specimen that shows the class without a
call-site read: registry/detect.ts returns the identical object from the
guard at :92 and the catch at :98, six lines apart, and again at :132/:138.

Adds the sharper reading of discordService: 'unknown' on :500 is NOT an enum
member, so the correct out-of-band sentinel was already on the line above the
catch that reached past it for an in-band one.

Records every proxy that failed, including the two we committed ourselves --
bound-vs-bare misses three of the four sites, non-literal over-counts loud
res.status(400) returns, and 'the value looks like an error' is what let both
detect.ts sites through the first hand pass.

* docs(ax): entry 44 — the discriminator is the caller, and read it normatively

sprint-review's review of f9f2bd7: the specimen tell I leaned on --
guard and catch returning the identical expression in view of each other --
fires harder on avatarService.ts, which is correct by design. Four
'return value' in thirteen lines, one of them the catch. Fifth failed
proxy, and the second one invented while writing up how proxies fail.

Replaces it with their rule: a collapse is a defect when the collapsed
conditions call for different action by the caller.

Adds the caveat that rule needs. Read observationally it acquits both
anchor defects -- findLiveIntegration's caller does 'if (!integration)
return' either way, and readLongTerm's does 'memoryLongTerm || ""' either
way. The identical handling is the bug.

* docs(ax): entry 44 — the discriminator is a question about the try, not the caller

sprint-review: 'call for different action by the caller' fixes the verdict
but relocates the work into a judgement about a hypothetical caller -- the
same appeal the five proxies were trying to escape. Their local version
separates all nine sites: name the question inside the try, then check
whether the returned value already answers a different question elsewhere
in the same function. Predicate (only a throw can ask) vs fault report.

Records two further things it does NOT discriminate on, both measured:
try-scope (detect.ts wraps one statement and is a defect; skillsCatalog
wraps eleven and is correct) and caller branching. Keeps the JSON.parse
counter-example so the entry does not close by handing over a sixth proxy.

* docs(ax): renumber this entry 44 -> 50, ceding 44 to the older #1143

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.

* docs(ax): drop the stale `## 44.` header the rebase left beside `## 50.`

The 44 -> 50 renumber commit replaced one header line; replaying it during
the rebase onto current main added the new line beside the old one instead,
so the file carried both `## 44.` and `## 50.` on consecutive lines titling
one entry. That re-took the number this branch had deliberately ceded to the
older #1143, and nothing goes red on a duplicate AX number — which is the
collision class this file already documents.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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