From c4e57f5839af770cad3e7efd7def54d5a6fc5f31 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:37:17 -0700 Subject: [PATCH 1/3] =?UTF-8?q?docs(ax):=20entry=2049=20=E2=80=94=20a=20pr?= =?UTF-8?q?ogress=20note=20re-claimed=20a=20row=20a=20human=20had=20re-poi?= =?UTF-8?q?nted=2048h=20earlier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/development/agent-experience-audit.md | 71 ++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index 761de5390..4e690bbc5 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -2769,3 +2769,74 @@ declared missing; verifying it myself rather than accepting it is what turned "the comments API is incomplete" into the `commit_id` asymmetry, and sweeping the other seven PRs is what found `#1330`, where both APIs are silent and the gate is real anyway. +## 49. A progress note re-claimed a row a human had re-pointed two days earlier (2026-08-28, pod-architect, found by the seat) + +**The surface:** `commonly_update_task`, whose tool description is *"Append an +update note to a task without changing status — visible in the task drawer +history."* Behind it, `POST /api/v1/tasks/:podId/:taskId/updates` +(`backend/routes/tasksApi.ts:537`). + +**The false model it taught:** that posting a note is observationally inert on +a row you do not hold. 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* — and the note claimed it for me: +`status` `pending` → `claimed`, `claimedBy` set to my id, a 30-minute lease +opened, `lapsedFrom` cleared. The response says so (`leaseRenewed: true`), but +that response is the whole task document, which exceeds this seat's tool-output +token limit — so the one field that would have contradicted the description +arrives in a file the caller has to go and parse. + +**The mechanism, and it is deliberate code doing a narrow job over an unbounded +range.** The handler has a second `findOneAndUpdate` for the case where the row +was swept back to `pending` before the note landed: + +```js +{ podId: podFilter, taskId, status: 'pending', lapsedFrom: { $in: identities } } +``` + +Its comments name the race precisely and it is a real one — 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). The restore is what makes the cue's two options +actually equivalent. + +**The bound the race has and the predicate does not.** 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** and my note restored it at **2026-08-28T23:33Z** — +**48 hours later**, across a human's explicit re-point to a different seat in +between. 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. + +**Why the assignment was invisible to the thing that should have carried it.** +The row's `assignee` field is `null`. Sam re-points by writing prose into an +update note, which no predicate reads. So the only machine-readable record of +who owns the row is `claimedBy` — and the restore branch overwrites exactly +that, from a field describing who owned it two days ago. + +**What I checked before filing.** That the id was mine and not a peer's, by +comparing against a row I know I hold. That `leaseRenewed` was `true` and +`lapsedFrom` was `null` afterwards, from the persisted response rather than +inferred. That `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 one that is not an ObjectId. + +**Cheapest fix that keeps the race closed:** 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` timestamp bounded to one lease period would also work and is +strictly more code. + +**The general shape, which is not about tasks.** A predicate written for a +race is a predicate about *time*, and storing only the participant stores the +wrong half. `lapsedFrom` answers "who" and the guard needed "who, and recently" +— so the code is correct for every case its comments discuss and unbounded for +every case they do not. When a comment justifies a branch by describing a +window, check that something in the row actually measures the window. + +**Second-order, for anyone writing tool descriptions:** *"without changing +status"* is a promise about the common path stated as a property of the tool. +The seat that most needs the exception is the one deliberately avoiding a +claim, and that is the seat the description reassures. From f8854334186fff1ad0e34c3ff482a71d70427bde Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:55:26 -0700 Subject: [PATCH 2/3] =?UTF-8?q?docs(ax-49):=20pin=20@sprint-review's=20neg?= =?UTF-8?q?ative=20controls=20=E2=80=94=20the=20restore=20branch=20is=20th?= =?UTF-8?q?e=20only=20wrong=20one?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/development/agent-experience-audit.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index 4e690bbc5..f98b512e5 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -2836,6 +2836,17 @@ wrong half. `lapsedFrom` answers "who" and the guard needed "who, and recently" every case they do not. When a comment justifies a branch by describing a window, check that something in the row actually measures the window. +**Negative controls, measured by @sprint-review while gating this entry — +and they are what makes the fix targetable.** Two other `commonly_update_task` +calls in the same hour: TASK-023 (`claimedBy` sprint-impl) and TASK-080 +(`claimedBy` ux-lead, status `done`), both carrying `lapsedFrom: null`. In each +case the returned row came back with the original holder's `claimedBy` +unchanged. So the tool does **not** claim by default, and the description is not +simply false — `lapsedFrom` naming the caller really is the discriminator, which +is what the mechanism above predicts. Worth stating explicitly because without +the controls the obvious repair is to the wrong branch: the primary claim path +is fine, and only the restore branch needs bounding. + **Second-order, for anyone writing tool descriptions:** *"without changing status"* is a promise about the common path stated as a property of the tool. The seat that most needs the exception is the one deliberately avoiding a From f40e8adabb381ee90582337d3bae35a8c966a9c4 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Fri, 28 Aug 2026 17:24:42 -0700 Subject: [PATCH 3/3] docs(ax-49): swap in the control that varies only lapsedFrom MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/development/agent-experience-audit.md | 30 ++++++++++++++-------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index f98b512e5..df0fb76ab 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -2836,16 +2836,26 @@ wrong half. `lapsedFrom` answers "who" and the guard needed "who, and recently" every case they do not. When a comment justifies a branch by describing a window, check that something in the row actually measures the window. -**Negative controls, measured by @sprint-review while gating this entry — -and they are what makes the fix targetable.** Two other `commonly_update_task` -calls in the same hour: TASK-023 (`claimedBy` sprint-impl) and TASK-080 -(`claimedBy` ux-lead, status `done`), both carrying `lapsedFrom: null`. In each -case the returned row came back with the original holder's `claimedBy` -unchanged. So the tool does **not** claim by default, and the description is not -simply false — `lapsedFrom` naming the caller really is the discriminator, which -is what the mechanism above predicts. Worth stating explicitly because without -the controls the obvious repair is to the wrong branch: the primary claim path -is fine, and only the restore branch needs bounding. +**Negative control, and the two that don't work — @sprint-review supplied +both halves, the second while withdrawing the first.** The restore query is +`{ status: 'pending', lapsedFrom: { $in: identities } }`, so a control has to +hold `status` fixed and vary only `lapsedFrom`. TASK-023 (`claimed`) and +TASK-080 (`done`) were offered first: both came back with the original holder's +`claimedBy` untouched, which rules out *"an update claims by default"* — but +each fails the `status` term as well as the `lapsedFrom` one, so neither can +isolate which term did the work. The clean control is **TASK-084**: `status: +'pending'`, `lapsedFrom: null`, two `commonly_update_task` calls from +@sprint-review, latest `2026-08-29T00:18:10Z`, and `claimedBy` still `null` +(re-read from the API at 00:24Z rather than carried across). That one varies +`lapsedFrom` alone, and it is what licenses the claim: `lapsedFrom` naming the +caller is the discriminator, exactly as the mechanism above predicts. Worth +stating because without it the obvious repair goes to the wrong branch — the +primary claim path is fine, and only the restore branch needs bounding. + +The near-miss is its own lesson, and it is the same one as entry 47: a control +that varies two terms at once confirms the conjunction, never the term you care +about. Both rows were real measurements, correctly reported, and still could not +support the sentence they were cited for. **Second-order, for anyone writing tool descriptions:** *"without changing status"* is a promise about the common path stated as a property of the tool.