From 6f01fa0feace33b7d58a2a62b65057d909fda71e Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sat, 29 Aug 2026 20:05:01 -0700 Subject: [PATCH] =?UTF-8?q?docs(ax):=20entry=2052=20=E2=80=94=20the=20pres?= =?UTF-8?q?cribed=20fix=20erases=20the=20evidence=20for=20the=20warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lease-deferral warning states a count (`rescue deferred (N of MAX)`, `K deferrals left`) and prescribes an action that zeroes the field the count is made of: the note path `$set`s `rescueDeferrals: 0` (tasksApi.ts:580), as does the claim path (:438). A seat that complies and then verifies reads a row that denies having been deferred. Second half: notifyLeaseWarning folds per task, not per holder, so two lapsed rows produce two pending events, and the sentence is rendered once at enqueue (taskEventService.ts:391) and stored verbatim (:425). Renewing both rows in one pass — the efficient answer — leaves the sibling event queued with text that is now false, and no renewal can correct it. Evidence 2026-08-30: TASK-069 and TASK-079 renewed in one pass, both returning leaseRenewed: true with rescueDeferrals: 0. The compliant arm and the never-deferred arm are byte-identical. Pure append: 66 insertions, 0 deletions. Co-Authored-By: Claude Opus 5 --- docs/development/agent-experience-audit.md | 66 ++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index e1888c8c4..cc3be1f79 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -3020,3 +3020,69 @@ started by enumerating sentinel *literals* and widened the set twice — `false` - No lint rule can see this. `@typescript-eslint/no-floating-promises` has no analogue, because the defect is a relation between two return sites rather than a property of either. + +## 52. The prescribed fix erases the evidence for the warning that prescribed it (2026-08-30, pod-architect, found by the seat) + +**The surface:** the kernel's lease-deferral warning — the drawer note pushed by +`KernelWorkSweepService.rescueLapsed` +(`backend/services/kernelWorkSweepService.ts:228`) and the wake enqueued by +`notifyLeaseWarning` (`backend/services/taskEventService.ts:375`). Both state a +count and both prescribe the same action: + +> `Lease lapsed but is still listening — rescue deferred (N of MAX). Renew by posting a task update or re-claiming.` + +> `[Your lease on TASK-0NN has lapsed. You are still listening, so the kernel deferred the rescue — K deferrals left.]` + +**The false model it taught:** that the count is something you can check. It is +not, once you have obeyed. Both prescribed actions zero the field the count is +made of — the note path at `backend/routes/tasksApi.ts:580` +(`$set: { claimExpiresAt: …, rescueDeferrals: 0 }`) and the claim path at +`:438` (`rescueDeferrals: 0, lapsedFrom: null`). So a seat that does exactly +what the warning says, and then reads the row to confirm the state it was +warned about, finds `rescueDeferrals: 0` — a row that denies having been +deferred at all. The only moment a non-zero is observable is *before* acting on +the wake that reports it, which is the one ordering the warning's own wording +discourages. + +**The second half is worse, because it makes a correct batch response +manufacture a false one.** The fold is keyed on the task, not on the holder: + +```js +{ agentName, instanceId, podId, status: 'pending', 'payload.leaseWarningTaskId': task.taskId } +``` + +That is deliberate and the comment says so — *"a seat holding two lapsed rows is +warned once about each."* Two rows therefore produce two pending events. The +sentence is rendered once, at enqueue (`taskEventService.ts:391`) and stored +verbatim into `payload.content` (`:425`) — nothing re-renders it on the way out. A seat that +wakes on the first warning and +renews *both* rows in one pass — the efficient answer, and the one the pod's +own economics ask for — leaves the second event queued with text asserting a +lapse and a deferral count that no longer exist. The fold's `$set` refreshes +`payload.content` only for the same `leaseWarningTaskId`, so no renewal can ever +correct a sibling event. The N−1 remaining wakes are made false by construction, +by compliance. + +**Evidence (2026-08-30).** Both rows this seat holds, TASK-069 and TASK-079, +were renewed in one pass through `POST /api/v1/tasks/:podId/:taskId/updates`. +Both responses came back `leaseRenewed: true`, `status: claimed`, +`rescueDeferrals: 0`. Nothing in that post-state distinguishes *never deferred* +from *deferred twice, then complied* — which is the whole difficulty: the +compliant arm and the untouched arm are byte-identical. + +**What to take from it** + +- A notification whose prescribed action mutates the state it reports must be + read as of enqueue, never as of delivery. The payload already carries the + immutable snapshot (`payload.deferralsUsed`); the rendered sentence does not + say that it is a snapshot, and the sentence is what gets read. +- For a seat: read `rescueDeferrals` and `claimExpiresAt` off the row *before* + renewing. Afterwards the value is gone, and its absence is indistinguishable + from never having existed — and the second producer of that absence is you. +- For the kernel: an event a prior action has falsified should be invalidated at + delivery rather than delivered. Either fold per (holder, sweep) instead of per + task, or re-check `rescueDeferrals` on the row before handing the event to the + seat. Both are cheap; neither exists today. +- No lint rule can see this. The defect is a relation between an event's + enqueue-time snapshot in one collection and a `$set` on another, and each half + is locally correct.