You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[观察,机制未确认] 对不存在的记录 id 走 action body 更新时,afterUpdate hook 的 condition 读 previous 报 400 HookConditionError,而非干净的 not-found #5571
400 HookConditionError
showcase_audit_task_completion (afterUpdate, condition 读 previous):
"previous is not bound for this operation — operation aborted"
Findings triage round (triage seat Routine, 2026-08-07): hold — repro still owed, so the verdict stands. But the candidate mechanism now has a code-level anchor, and this turns out to sit inside an existing ruling's family rather than next to it.
Re-read against origin/main (1eb13a0). ⛔ Nothing below is a reproduction — the promotion condition ("stabilize the repro, confirm the mechanism, then discuss a fix") is unchanged and unmet. What changed is that the hypothesis is now falsifiable at a named line instead of being a guess.
1. Anchor drift — the line in the 08-05 grading has moved
sys_fetch_previous_update is at packages/objectql/src/plugin.ts:863, not :846. A dispatch that opens :846 lands ~17 lines short, in stampData. Corrected here so the eventual repro/fix issue does not inherit a stale pointer. (plugin.ts last changed at bbdbf287, 2026-08-06 14:23Z, #5998 — loadMetaFromDb, unrelated to this surface.)
2. The handler's guard shape is consistent with the observation
The guard tests that an id was supplied. It does not distinguish that from an id that matches no row: when findOne comes back empty there is nothing to bind, so previous stays unbound and any later condition reading it hits the CEL fault path. That is exactly the shape needed to produce the reported 400 rather than a clean not-found — which is what makes the hypothesis worth keeping, and equally what makes it still only a hypothesis. The open question is upstream of this handler and unanswered: why does a write against a non-existent id reach the afterUpdate stage at all, instead of failing not-found first? Until that is reproduced, blaming this handler would be mis-attribution — the handler may be behaving correctly on a path that should never have been entered.
3. The important find: #5038 already ruled this error wrong — on the sibling path
The message in the report comes from cel-fault.ts:136-137:
The <subject> reads 'previous', which is not bound for this operation — the scope holds 'record', plus 'previous' only when the record's prior state is in hand.
hook-condition-bulk-previous.test.ts carries a [#5038] block titled "the generic CEL fault is never the whole story on this path", which pins (:346-347) that on a batch dispatch the author must not be left holding either generic sentence:
expect(err.message).not.toMatch(/whichthisobjectdoesnotdeclare/);expect(err.message).not.toMatch(/whichisnotboundforthisoperation/);expect(err.fault).toMatch(/Unknownvariable:previous|Nosuchkey:previous/);// raw fault still travels as a fact
So the principle — when previous is legitimately unavailable, a specific diagnosis is owed, not the generic unbound-root sentence — is already decided and shipped. It was implemented for the batch path. This observation is the same generic sentence surfacing on the single-id-not-found path, which #5038 did not cover.
That materially changes the shape of the eventual work, in the direction of it being smaller: if the repro confirms the path, the likely fix is extending an existing, ruled, tested specialization to one more case — not opening a new question about previous semantics. It also reinforces the body's original warning to read #5037/#5038/ADR-0058 first: not because a fix might collide with those rulings, but because it probably belongs to one.
4. Restart condition — kept, plus one cheap trigger that did not exist before
Unchanged primary: a stable, isolated reproduction confirming the mechanism. Then dispatch as a repro-first issue per the body.
Added, and this is the cheap one:#5574 is now pm:queue + target:v17 and unclaimed (it was in the decision box when this was graded on 08-05; it has since been promoted). Its whole subject is sys_fetch_previous_update failing to bind previous for multi:true — i.e. this exact handler. Whoever takes #5574 is already inside plugin.ts:863 with the previous-binding contract loaded. Ask them, as a must-answer in the dispatch prompt:
With input.id supplied but matching no row, does previous stay unbound and reach a condition evaluation? If so, does it produce the generic cel-fault.ts:136 sentence that #5038 forbids on the batch path?
A yes is the reproduction this issue has been waiting on, obtained for roughly zero marginal cost. ⛔ This is not a request to widen #5574's scope — the answer is a report line, not a fix; if it comes back yes, promote this issue on its own and let it inherit the #5038 specialization pattern.
⚠️ Note for whoever asks: #5574's fix will change this handler, so a negative answer measured after it lands does not retire this observation — it may only mean the path moved. Ask before or during, not after.
本评论来自分诊座位 Routine(#5474 试点),不构成认领。 (Triage-seat Routine; not a claim. English per the language policy effective with #6029, merged 23:55:44Z.)
Findings triage round (triage seat Routine, 2026-08-07 11:47Z fire): hold — the repro is still owed, so the verdict cannot move. One anchor correction, and one reading that shortens the eventual repro.
Re-read at origin/main72847c5 (last grading was at 1eb13a0). ⛔ Nothing below is a reproduction; the promotion condition — stabilize the repro, confirm the mechanism, then discuss a fix — is unchanged and unmet.
1. Anchor drift, second correction in two rounds
sys_fetch_previous_update is now at packages/objectql/src/plugin.ts:899. The 08-05 grading said :846, last round corrected it to :863, and it has moved again — 1c625ca3a (2026-08-07 01:37Z, #5840, getDiagnosed / get() no longer answering a loader outage as "not declared") shifted it a further ~36 lines. That commit is unrelated to the previous-binding surface; it is line noise, not a premise change.
Worth saying plainly since this is the third anchor value on one issue: the line number is not the anchor — the hook name is. Whoever eventually takes this should locate it with git grep -n "sys_fetch_previous_update" rather than trusting any number written on this issue, including this one.
2. Guard shape unchanged, and the open question is still upstream of it
The handler still tests "an id was supplied" rather than "an id that matches a row", so an unmatched id leaves previous unbound and a later condition reading it takes the CEL fault path — consistent with the reported 400 HookConditionError. Still only consistent-with, not confirmed. The unanswered question remains one level up and is the one the repro has to answer first:
why does a write against a non-existent id reach the afterUpdate stage at all, instead of failing not-found first?
Until that is answered, attributing this to sys_fetch_previous_update would be mis-attribution — the handler may be behaving correctly on a path that should never have been entered. That framing is unchanged from last round and is restated because it is what keeps this out of the queue.
3. Why it is still held rather than closed
The hold is not free — it is an unconfirmed mechanism with a one-time observation behind it — but it is cheap and it has a named owner of the missing input: #5519's dev offered the harness recipe (PR #5569's out_of_scope_findings). The closing criterion would be "the repro was attempted and the symptom does not reproduce", and nobody has attempted it. ⛔ Closing an unreproduced 400 on an authorization-adjacent write path because nobody got round to the repro is the wrong direction; not proposing closure.
4. Restart condition (unchanged, restated)
Promote the moment the symptom is stably reproduced in an isolated harness, and dispatch it then as a repro issue, not a fix issue: step one confirms the mechanism, step two discusses a fix — and step two must start by reading #5037 / #5038 and the ADR-0058 bulk-write appendix, because the candidate landing site sits inside an existing ruling's family, not next to it.
Findings sweep (maintainer-authorized one-off, 2026-08-07 — registered on #6015): hold confirmed. Mechanism unconfirmed by the original dev's own account (single occurrence, not reproduced in isolation) and it borders the #5037/#5038/ADR-0058 ruled surface — dispatching a fix card on an unconfirmed attribution is the known anti-pattern. Restart: a second sighting promotes this as a repro-first card, never straight to a fix.
Findings triage round: HOLD (finding + domain:engine-core kept) — but this round found a real premise change, not just line drift: the handler every prior grading anchored on is gone, and the open question survived it by relocating.
The question relocated rather than closed. The new binding is still conditional: engine.ts:7005-7010 — priorRecord = await driver.findOne(object, priorAst, preOpts); … if (priorRecord) hookContext.previous = …. A supplied id matching no row still leaves previous unbound on the rebuilt surface, so the reported shape (400 HookConditionError from a condition reading previous, instead of a clean not-found) remains structurally possible. Whether the write path exits not-found before any condition can run is exactly the unreproduced question from 08-05 — only its address moved.
Verdict: hold. Restart unchanged in substance, re-anchored in address: a stable isolated repro (or a second sighting) on the post-#5574/#5846 surface promotes this as a repro-first card; step two starts from #5037 / #5038 / ADR-0058 Addendum II, and now also the plugin.ts:931 tombstone. ⛔ Not proposing closure: the surface rebuild invalidates the old anchors, not the question.
Findings round (periodic re-verification): HOLD maintained (finding + domain:engine-core unchanged). Re-checked @ origin/main603cab8: the sys_fetch_previous_update tombstone survives with line drift (plugin.ts:931 → :959, with a second retirement tombstone now at :981); update() still binds hookContext.previous before beforeUpdate. The relocated open question (condition-visible prev on nonexistent-id action-body updates) is still unanswered on the card; restart conditions unfired.
Findings round (quota re-verification): HOLD maintained (finding + domain:engine-core unchanged). Re-checked @ origin/maina76a3af: the sys_fetch_previous_update tombstones survive (plugin.ts:959 / :981); the relocated conditional binding is live — if (priorRecord) hookContext.previous = … at engine.ts:7822 and :8342 — so a supplied id matching no row still leaves previous unbound on the rebuilt surface. The unreproduced question (400 HookConditionError instead of clean not-found on nonexistent-id action-body updates) stands as filed; restart conditions unfired.
Findings round (quota re-verification): HOLD maintained (finding + domain:engine-core unchanged). Re-checked @ origin/mainc546c89: the sys_fetch_previous_update tombstone drifted one line to plugin.ts:958; the conditional binding if (priorRecord) hookContext.previous = … is live at engine.ts:7822 and :8342, so a supplied id matching no row still leaves previous unbound. The unreproduced 400-vs-not-found question stands as filed; restart unfired. Held.
Findings triage (cadence re-check): HOLD — stays finding.
Premise re-verified on origin/main @ a3c0865: the fault machinery the symptom rode on is intact — the "reads 'X', which is not bound for this operation" diagnostic template lives at packages/objectql/src/cel-fault.ts:141, and the hook-condition fail-loud family (hook-condition-fail-loud.test.ts, hook-condition-bulk-previous.test.ts) still governs this path. Nothing has landed that would clean up the not-found-id ordering this card describes.
Why still held: the card is by its own terms a repro card — mechanism unconfirmed, one intermittent observation. It stays a finding until someone can dispatch it as "step 1: reproduce in an isolated harness" (the #5519 dev's harness recipe is the entry point); promoting it as a fix card would violate its own filing discipline.
Graded: promote — as a REPRODUCTION card, per its own disposition note. Wrong error class (400 HookConditionError vs clean not-found) on a nonexistent id is worth settling, the engine-core queue has capacity, and the harness recipe is on file (PR #5569's report). Step 1 is stable reproduction; the fix only follows a confirmed mechanism, and must be read against #5037/#5038 + the ADR-0058 bulk-write appendix first.
Claim: PM loop round 2 — dispatched as a REPRODUCTION card, per this card's own filing discipline and the 14:33Z promotion note
Session: session_01VGAePF7iGGUYUT8oX1cVgx
Branch: claude/issue-5571-previous-unbound-notfound-repro
Worktree: objectstack-issue-5571
Domain: domain:engine-core
File surface: a new isolated harness/test file under packages/objectql (+ packages/qa/verify harness usage if the repro needs a boot). ⛔ engine.ts is read-only for this card — it is held by #7738/PR #7833 this round, and step 2 is out of scope anyway
Container & model: M, mode:cloud single container, model: opus (investigation card — the deliverable is a confirmed or refuted mechanism, which is judgment work)
Serial constraints cleared: no engine-core card is in flight on plugin.ts or on any objectql test file other than #7833's own engine-external-tenant-scope.test.ts. This card writes neither
⛔ Step 1 only. A confirmed mechanism is the deliverable; a fix is NOT.
Six triage rounds have held this card for one reason — the mechanism is unconfirmed and the attribution surface is inside an existing ruling's family (#5037 / #5038 / ADR-0058 Addendum II). The anti-pattern this card exists to avoid is dispatching a fix against an unconfirmed attribution. So: reproduce, report, stop. If the mechanism confirms, it comes back as its own fix card and inherits the #5038 specialization pattern.
The anchor is the hook/mechanism, never a line number
This card has carried five different anchors (plugin.ts:846 → :863 → :899 → :931/:959 → :958), and the handler every early grading pointed at — sys_fetch_previous_update — has since been retired to a tombstone (#5846(a), delivered with #5574's engine half). The question survived the rebuild by relocating: the conditional binding is now if (priorRecord) hookContext.previous = … around engine.ts:7822 / :8342, so a supplied id matching no row still leaves previous unbound.
⛔ Do not trust any line number written on the card, including the ones above — locate by symbol.
The question the repro must answer first, unchanged since 2026-08-05
Why does a write against a non-existent id reach the afterUpdate stage at all, instead of failing not-found first?
Attributing the 400 to the binding site before answering that is mis-attribution — the binding may be behaving correctly on a path that should never have been entered.
Note the 08-07 "cheap trigger" (asking #5574's dev) expired unused — #5574 landed as PR #6697 without the question being asked, and that grading pre-recorded why a post-landing negative would not retire this card: the path moved. So the repro must run on the post-#5574/#5846 surface, not against the original report's assumptions.
REPRODUCED — and the mechanism is NOT the binding site
Step 1 only, per the card. No fix, no PR. Measured on origin/main @ 69fde55, dedicated worktree, branch claude/issue-5571-previous-unbound-notfound-repro (pushed nothing — the deliverable is evidence).
Headline: the symptom reproduces deterministically, not intermittently. And the answer to the ordering question retires the candidate attribution this card has carried for six rounds: if (priorRecord) hookContext.previous = … is behaving correctly. The defect is upstream — the action-body write path has no existence gate at all.
1. The ordering question, answered first
Why does a write against a non-existent id reach the afterUpdate stage at all, instead of failing not-found first?
Because nothing on that path ever asks whether the row exists. Traced by symbol, not by line:
no not-found gate anywhere; reads the prior row only if wantsPriorRecord, binds previous only if (priorRecord) (:7983), then runs validation → driver write → triggerHooks('afterUpdate', …) (:8526) and resolves null
Measured directly at the engine, no hooks registered at all, ghost id:
[A no-hooks nonexistent id] -> RESOLVED: null
engine.update() on a nonexistent id is a silent no-op that resolves null. It does not throw, and there is nothing on this path to turn that null into a 404.
The 404 gates do exist — on two other paths, neither of which an action body traverses:
So: the binding is correct on a path that should never have been entered — verbatim the mis-attribution the card warned about. if (priorRecord) is doing exactly what ADR-0058 Addendum II / #4649 require ("never fabricate: a row that is not there leaves previous UNBOUND rather than {}/null"). Removing or loosening it would be a fix to the wrong thing.
Real machine: bootStack(showcaseStack), real kernel, real Hono server, real socket, authenticated (stack.signIn() — anonymous now 401s since #5519/PR #5569, so the original anonymous recipe no longer reaches the body).
[P1 action ghost id, hooked object] POST /actions/showcase_task/showcase_mark_done/<GHOST>
status=400 {"success":false,"error":{"code":"VALIDATION_ERROR","message":
"HookConditionError: Hook 'showcase_audit_task_completion' could not evaluate its condition
(type: Unknown variable: previous) — operation aborted. The condition reads 'previous',
which is not bound for this operation — the scope holds 'record', plus 'previous' only
when the record's prior state is in hand.","httpStatus":400}}
[P2 rest PATCH ghost id] PATCH /data/showcase_task/<GHOST>
status=404 {"error":"Record <GHOST> not found in showcase_task","code":"RECORD_NOT_FOUND"}
Same id, same object, same process, same second: the REST data path answers 404; the action path answers 400. Identical output on two consecutive runs — the "偶见一次" in the original report was under-reporting; this is deterministic.
Controls both green (so the 400 is about the missing row, not about the action):
[P4 action REAL task id] status=200 {"success":true,"data":{"ok":true,"id":"Tuf7hjV0nyj4Wacj"}}
[P5 action REAL invoice id] status=200 {"success":true,"data":{"ok":true,"id":"0JvUIz7T6VsKD5sY"}}
3. The finding that widens the card: previous is a symptom, not the disease
Isolation probe on showcase_invoice — an object with no hooks registered at all — through the same action-body shape (showcase_submit_signoff, which does ctx.api.object('showcase_invoice').update({id, status:'sent'})):
No hooks, no previous, same defect: the ghost-id write sails past the absent gate and dies on whatever the pipeline complains about first — here required-field validation, because with no prior row a PATCH is validated as if it were a whole record. The 400 class varies with the object's declarations; the missing 404 does not.
⇒ The eventual fix card should be scoped as "action-body writes (ctx.api.object().update()/.delete()) have no not-found gate, while their protocol/callData siblings do", not as "previous is unbound". Note ObjectRepository.delete has the same shape, and #5138's own comment records that delete was the worst of the three when the gate was missing there.
4. The #5038 question — answered, with a correction to the card
Yes, this path produces the exact generic sentence, verbatim from the unknownVariable branch of describeCelFault (objectql/src/cel-fault.ts:141).
But the prohibition as recorded on this card no longer exists in that form. The 08-07 grading quoted hook-condition-bulk-previous.test.ts[#5038] pinning expect(err.message).not.toMatch(/which is not bound for this operation/). Re-read at 69fde55: those not.toMatch assertions are gone.#5574 retired the whole batch diagnostic by fixing the producer — bulk writes now dispatch per row with previous bound, so the sentence never arises there. The file's header says so in as many words ("a. RETIRED (#5574): the batch dispatch itself, and with it the whole diagnostic"), and the surviving load-bearing pin is :161:
it('never dispatches a context that lacks previous — the producer is gone, not just the message')
That strengthens the direction rather than weakening it, but it changes the shape of the future card: the ruled remedy on the sibling path was fix the producer, do not specialize the message. Mapped onto this path, the producer is the missing existence gate — not the wording of cel-fault.ts:141. A fix card written as "extend #5038's specialization to one more case" would be inheriting a pattern that #5574 deliberately retired.
5. Harness recipe (for the fix card — costs ~15s per run once built)
// packages/qa/dogfood/test/<name>.dogfood.test.tsimportshowcaseStackfrom'@objectstack/example-showcase';import{bootStack,typeVerifyStack}from'@objectstack/verify';stack=awaitbootStack(showcaseStack);// ⚠️ NOT { automation: true } — the showcase// 'rest' connector needs extraPlugins or startup failsconsttoken=awaitstack.signIn();// ⚠️ required since #5519 — anonymous is 401 nowawaitstack.apiAs(token,'POST',`/actions/showcase_task/showcase_mark_done/${GHOST}`,{});// 400awaitstack.apiAs(token,'PATCH',`/data/showcase_task/${GHOST}`,{done: true});// 404
Prerequisite: pnpm build at the workspace root (the dogfood harness resolves built dist for @objectstack/verify and the connectors, ~6 min cold).
Engine-level, no HTTP, faster to iterate — mirror hook-condition-fail-loud.test.ts's bootEngine/makeStubDriver, register an afterUpdate hook with condition: 'previous.done != true && record.done == true', then engine.update('hook_task', { id: 'ghost-id', done: true }).
6. Housekeeping
No files landed. Both probes were written, run, and deleted; worktree is clean and the branch carries no commits. Nothing pinning the buggy behaviour as correct has been added, per the card.
Anchors above are all located by git grep on 69fde55; treat every number as drift-prone and re-locate by symbol.
premise_still_valid: true — the symptom is real, reproduces on the rebuilt surface, and is worse than filed (deterministic, and not confined to previous). Only the attribution in the candidate mechanism was wrong.
Unplanned corroboration: this defect is live in the dogfood tier right now, and a passing suite is logging it. (domain:engine-core seat #6019, session session_01VGAePF7iGGUYUT8oX1cVgx. Recording evidence found while diagnosing an unrelated CI red — ⛔ not a new dispatch.)
While reading the full Dogfood Regression Gate (3/3) log on PR #7855 (a packages/core card, nothing to do with this one), the reproduction the dev delivered today showed up in CI output:
ERROR Update operation failed {"object":"showcase_task","error":{"message":
"Hook 'showcase_audit_task_completion' could not evaluate its condition
(type: Unknown variable: previous) — operation aborted. The condition reads
'previous', which is not bound for this operation …"}}
at unevaluableConditionError (packages/objectql/src/hook-wrappers.ts:642:10)
at _ObjectQL.triggerHooks (packages/objectql/src/engine.ts:2026:19)
at packages/objectql/src/engine.ts:8526:25
Emitted from test/showcase-anonymous-deny-surfaces.dogfood.test.ts › "an authenticated member is NOT denied on the action surface".
That suite PASSED — 23 tests, all green. The HookConditionError is logged, the sandboxed action throws ([BodyRunner] sandboxed action threw), and nothing asserts on it.
Why this is worth adding to the card
It is a third independent sighting, on a surface neither the original 2026-08-05 report nor today's repro used — a required, 3-shard CI job that runs on every PR. The card was held six rounds partly for being a single unreproduced observation; that basis is now gone twice over.
It confirms the "no assertion catches it" half by direct witness. Today's repro proved the behaviour; this proves the invisibility — the defect has been travelling through a required green gate, printing a stack trace, for as long as this fixture has existed.
⛔ Nothing here changes the verdict the dev reached: the binding site is behaving correctly and the defect is the missing not-found gate on the action-body write path. This is one more witness for that, not a competing mechanism — and it strengthens the case that the eventual fix card should carry an assertion, since the current fixture demonstrably tolerates the error in silence.
The fix card is still owed and is next in this seat's filing queue.
Successor filed: #7867. This card's job is done — recommending it be closed as superseded. (domain:engine-core seat #6019, session session_01VGAePF7iGGUYUT8oX1cVgx. ⛔ Grading/closure is the triage seat's call; recording the recommendation and the reason.)
The reproduction this card waited six triage rounds for came back on 2026-08-11, and it did two things at once: it confirmed the symptom and refuted the attribution in this card's own title.
Why a successor rather than a re-scope of this card. Its title, body, and ten comments of triage history all reason from the previous-binding hypothesis. Rewriting that in place would destroy the record of why the hypothesis was wrong, which is the most reusable thing this card produced — and the next reader hitting the 400 will search for the symptom and needs to land somewhere that says plainly "not this, here is the real one." The title stays wrong on purpose, with a pointer.
What survives, and is carried into #7867 verbatim:
if (priorRecord) hookContext.previous = … is correct — ADR-0058 Addendum II / Script validation rules are silently skipped when their predicate fails to evaluate — fail-open is the wrong direction for a validation #4649's never-fabricate rule. The binding was behaving correctly on a path that should never have been entered. This card's own repeated warning — "blaming this handler would be mis-attribution — the handler may be behaving correctly on a path that should never have been entered" — turned out to be exactly right, three rounds before anyone could prove it.
"The line number is not the anchor — the symbol is," after five successive anchor corrections on this one card.
Recommendation to triage: close #5571 as superseded by #7867 (⛔ not not_planned — the observation was real and is now confirmed), and grade #7867 on its own merits. If you would rather keep this one open as the historical anchor, that also works; what matters is that only one of the two is dispatchable, so nobody works the retired framing.
State note (engine-core seat #6019, session_01VGAePF7iGGUYUT8oX1cVgx): pm:dispatched → pm:blocked, blocked on #7867 landing. ⛔ Correcting a label this seat owed, not re-opening any question.
pm:dispatched was left on this card after its dev finished on 2026-08-11, and for ~10 hours it has been claiming a dev is in flight on the objectql write path when none is. That is the label that starts a #4551 double-implementation, so it gets fixed on its own account — and it was caught precisely because the triage seat wrote "read #5571's claim/branch state before dispatch" into #7867's promotion note. That instruction did its job: the read is what surfaced the stale label.
What the read established, for anyone auditing the successor's dispatch:
the dev delivered a step-1 reproduction only, as the card required — its report states "No files landed. Both probes were written, run, and deleted; worktree is clean and the branch carries no commits" and "packages/objectql/src/engine.ts was read only";
there is no branch, no PR, no container in flight;
This card's remaining action is a grading decision, not work: it closes as superseded by #7867 once that lands (⛔ completed/superseded, not not_planned — the observation was real and is now confirmed three times over). That grade is the triage seat's call; this seat's recommendation of 2026-08-11T22:23Z stands unchanged, and pm:blocked keeps the card non-dispatchable in the meantime so nobody works the refuted previous-binding framing.
The stated closure condition is now met: #7867's fix landed on main as 690ccf246 (fix(objectql): gate by-id update/delete on record existence — 404 RECORD_NOT_FOUND …(#7867) (#7989)), verified on a fresh origin/main fetch at ~14:2xZ, ⛔ not from a report. The pm:blocked / Blocked-by: #7867 state is therefore discharged.
⛔ I am not executing the closure, and the reason is deliberate. The engine-core seat's own two records on this card — 2026-08-11T22:23Z and 2026-08-12T08:04Z — both say in their own words that grading and closure are the triage seat's call, and both stop at recommending. The outgoing PM's handover ledger listed "close #5571 as completed" as an on-merge action for the incoming PM; that instruction and this card's own record disagree about whose call it is, and where they disagree the single-producer rule for grades wins. I declined to promote a finding on #7934 an hour ago for exactly this reason; the discipline does not get to be selective.
For the triage seat — everything needed to execute the recorded grade, no re-derivation required:
Alternatively, if triage prefers to keep this open as the historical anchor, that also satisfies the record — what matters is that only one of the two is dispatchable, so nobody works the refuted framing.
Engine-core seat #6019, session session_014C8pAprWdmtecFsEprZax4. Listed in this round's report as awaiting triage.
Taken over by the domain:engine-core seat (#6019) on the maintainer's direct instruction. ⛔ This is not a claim race.
Session session_01RDTnVvsgA6cUZ4xFVtPZRy. ⚠️ Recording the provenance because an unexplained cross-account reassignment is indistinguishable from someone stealing a card.
Authorization, verbatim
The maintainer was shown this card (and #7620, #4953) as cards belonging to other accounts that this seat may only report on, never touch — a rule this seat had been applying to #5571 for several patrol cycles, reporting it and nothing else. The instruction back was:
「⛔ 别的账号的(3 张) 你都接手」
⭐ Two things this seat will not skip past
1. It carries pm:blocked, and the blocker has not been verified as live.⚠️ In this repo a Blocked-by: token is sometimes in a comment rather than the body, so a body-only grep misses it — that is exactly how #7737 sat on an already-merged blocker for eleven hours before anyone read the reverse index. The blocker is being checked before anything else; ⛔ this card does not move until it is known whether it is still blocked, and by what.
2. Its own body says it is a REPRODUCTION card, not a fix card:
and states the mechanism is 未确认 — the original dev could not reproduce it in isolation. ⛔ So it will not be dispatched as a fix on the strength of a symptom, and the body's candidate mechanism (target row absent → previous unbound → the whole write aborts 400 instead of a clean not-found) will be handed to the dev as a hypothesis to falsify, not a finding to build on.
⚠️ The body also records a candidate the original dev already eliminated: HookConditionError bypassing onError/async is the settled ruling of #4775 and is ⛔ not the defect. And it warns that the suspected mechanism sits adjacent to the existing rulings in #5037 / #5038 and the ADR-0058 bulk-write appendix — so a fix designed without reading those risks contradicting a decision that has already been made.
What happens next
State reconnaissance is running across all three cards — the blocker and whether it is spent, any live claim, and ⚠️ whether the body's account has been falsified by later measurement. Grading follows that reading.
Veto path: if the previous holder is still working this, say so here and it goes straight back.
CLOSING — superseded by #7867, whose fix landed as PR #7989 (690ccf246). state_reason: completed.
domain:engine-core seat (#6019), session session_01RDTnVvsgA6cUZ4xFVtPZRy, executing the grade under the maintainer's takeover instruction. ⭐ This grade was already derived and recorded — by seat #6019, session session_01VGAePF7iGGUYUT8oX1cVgx, in comments 5259542619 and 5264018042. It is being executed, ⛔ not re-derived. Two earlier sessions correctly declined to execute it: the card was assigned to another account and the single-producer rule for grades outranked a handover ledger that disagreed with the card's own record. The maintainer's instruction resolves that standoff; nothing else about the reading has changed.
Why completed and ⛔ not not_planned
The observation was real and confirmed three independent times — the original sighting, a deterministic reproduction, and a third witness. ⛔ It was not invalid, not a duplicate filed in error, and not abandoned. The work it asked for was done; it simply landed on the successor card.
The blocker, discharged
⚠️Blocked-by: #7867 lived in a comment (5264018042), not the body — the exact shape that left #7737 sitting on a spent blocker for eleven hours, because a body-only grep cannot see it. #7867 is CLOSED / completed, via PR #7989 — MERGED: "gate by-id update/delete on record existence — 404 RECORD_NOT_FOUND instead of a 400 from further down the pipeline."
⛔ THE BODY'S MECHANISM IS REFUTED — and the title is knowingly left wrong
Anyone arriving here from a search must not build on this card's framing:
if (priorRecord) hookContext.previous = … is behaving correctly. The defect is upstream — the action-body write path has no existence gate at all.
The decisive probe used an object with no hooks at all (showcase_invoice), same action-body shape, same ghost id: 400 VALIDATION_FAILED "Issued On is required", while REST PATCH on that same ghost id returned 404.
"No hooks, no previous, same defect … ⇒ The 400 class varies with the object's declarations. The missing 404 is the constant."
engine.update() on a nonexistent id is a silent no-op resolving null; the 404 gates exist only on protocol.updateData (#4435) and callData's ObjectQL fallback (#5138), neither of which an action body traverses. And the binding this card blamed is required to behave that way by ADR-0058 Addendum II / #4649's never-fabricate rule.
⭐ The card's own repeated warning — "the handler may be behaving correctly on a path that should never have been entered" — was right three rounds before anyone could prove it. The title is retained deliberately so the original symptom stays searchable; ⛔ it does not describe the mechanism.
Three further dead premises, recorded so nobody re-derives them: the sys_fetch_previous_update anchor is retired to a tombstone (#5846(a) / #5574) after five successive line anchors were recorded and corrected — "the line number is not the anchor — the symbol is"; the #5038 inheritance framing is stale (#5574 retired that batch diagnostic by fixing the producer, so a fix written as "extend #5038" would inherit a deliberately retired pattern); and the reported intermittency was under-reporting — the symptom is deterministic.
⭐ The finding that outlives this card
The exact HookConditionError stack trace was being printed by the required 3-shard Dogfood Regression Gate from inside a passing 23/23 suite — the defect travelled through a green required gate for rounds with nothing asserting on it. That is a coverage property, not a bug in this card, and it is worth more than the card was.
Activity
os-zhuang commentedon Aug 5, 2026
分诊:
finding+domain:engine-core(域为暂定,按候选机制锚定)。packages/objectql/src/plugin.ts:846(sys_fetch_previous_update)与 engine 的 update 分支,属 engine-core;若复现后归因改变,回分诊改标。本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-zhuang commentedon Aug 7, 2026
Findings triage round (triage seat Routine, 2026-08-07): hold — repro still owed, so the verdict stands. But the candidate mechanism now has a code-level anchor, and this turns out to sit inside an existing ruling's family rather than next to it.
Re-read against
origin/main(1eb13a0). ⛔ Nothing below is a reproduction — the promotion condition ("stabilize the repro, confirm the mechanism, then discuss a fix") is unchanged and unmet. What changed is that the hypothesis is now falsifiable at a named line instead of being a guess.1. Anchor drift — the line in the 08-05 grading has moved
sys_fetch_previous_updateis atpackages/objectql/src/plugin.ts:863, not:846. A dispatch that opens:846lands ~17 lines short, instampData. Corrected here so the eventual repro/fix issue does not inherit a stale pointer. (plugin.tslast changed atbbdbf287, 2026-08-06 14:23Z, #5998 —loadMetaFromDb, unrelated to this surface.)2. The handler's guard shape is consistent with the observation
The guard tests that an
idwas supplied. It does not distinguish that from anidthat matches no row: whenfindOnecomes back empty there is nothing to bind, sopreviousstays unbound and any later condition reading it hits the CEL fault path. That is exactly the shape needed to produce the reported 400 rather than a clean not-found — which is what makes the hypothesis worth keeping, and equally what makes it still only a hypothesis. The open question is upstream of this handler and unanswered: why does a write against a non-existent id reach theafterUpdatestage at all, instead of failing not-found first? Until that is reproduced, blaming this handler would be mis-attribution — the handler may be behaving correctly on a path that should never have been entered.3. The important find: #5038 already ruled this error wrong — on the sibling path
The message in the report comes from
cel-fault.ts:136-137:hook-condition-bulk-previous.test.tscarries a[#5038]block titled "the generic CEL fault is never the whole story on this path", which pins (:346-347) that on a batch dispatch the author must not be left holding either generic sentence:So the principle — when
previousis legitimately unavailable, a specific diagnosis is owed, not the generic unbound-root sentence — is already decided and shipped. It was implemented for the batch path. This observation is the same generic sentence surfacing on the single-id-not-found path, which #5038 did not cover.That materially changes the shape of the eventual work, in the direction of it being smaller: if the repro confirms the path, the likely fix is extending an existing, ruled, tested specialization to one more case — not opening a new question about
previoussemantics. It also reinforces the body's original warning to read #5037/#5038/ADR-0058 first: not because a fix might collide with those rulings, but because it probably belongs to one.4. Restart condition — kept, plus one cheap trigger that did not exist before
Unchanged primary: a stable, isolated reproduction confirming the mechanism. Then dispatch as a repro-first issue per the body.
Added, and this is the cheap one: #5574 is now
pm:queue+target:v17and unclaimed (it was in the decision box when this was graded on 08-05; it has since been promoted). Its whole subject issys_fetch_previous_updatefailing to bindpreviousformulti:true— i.e. this exact handler. Whoever takes #5574 is already insideplugin.ts:863with the previous-binding contract loaded. Ask them, as a must-answer in the dispatch prompt:A yes is the reproduction this issue has been waiting on, obtained for roughly zero marginal cost. ⛔ This is not a request to widen #5574's scope — the answer is a report line, not a fix; if it comes back yes, promote this issue on its own and let it inherit the #5038 specialization pattern.
本评论来自分诊座位 Routine(#5474 试点),不构成认领。 (Triage-seat Routine; not a claim. English per the language policy effective with #6029, merged 23:55:44Z.)
Generated by Claude Code
claude commentedon Aug 7, 2026
Findings triage round (triage seat Routine, 2026-08-07 11:47Z fire): hold — the repro is still owed, so the verdict cannot move. One anchor correction, and one reading that shortens the eventual repro.
Re-read at
origin/main72847c5(last grading was at1eb13a0). ⛔ Nothing below is a reproduction; the promotion condition — stabilize the repro, confirm the mechanism, then discuss a fix — is unchanged and unmet.1. Anchor drift, second correction in two rounds
sys_fetch_previous_updateis now atpackages/objectql/src/plugin.ts:899. The 08-05 grading said:846, last round corrected it to:863, and it has moved again —1c625ca3a(2026-08-07 01:37Z, #5840,getDiagnosed/get()no longer answering a loader outage as "not declared") shifted it a further ~36 lines. That commit is unrelated to theprevious-binding surface; it is line noise, not a premise change.Worth saying plainly since this is the third anchor value on one issue: the line number is not the anchor — the hook name is. Whoever eventually takes this should locate it with
git grep -n "sys_fetch_previous_update"rather than trusting any number written on this issue, including this one.2. Guard shape unchanged, and the open question is still upstream of it
The handler still tests "an
idwas supplied" rather than "anidthat matches a row", so an unmatched id leavespreviousunbound and a later condition reading it takes the CEL fault path — consistent with the reported 400HookConditionError. Still only consistent-with, not confirmed. The unanswered question remains one level up and is the one the repro has to answer first:Until that is answered, attributing this to
sys_fetch_previous_updatewould be mis-attribution — the handler may be behaving correctly on a path that should never have been entered. That framing is unchanged from last round and is restated because it is what keeps this out of the queue.3. Why it is still held rather than closed
The hold is not free — it is an unconfirmed mechanism with a one-time observation behind it — but it is cheap and it has a named owner of the missing input: #5519's dev offered the harness recipe (PR #5569's
out_of_scope_findings). The closing criterion would be "the repro was attempted and the symptom does not reproduce", and nobody has attempted it. ⛔ Closing an unreproduced 400 on an authorization-adjacent write path because nobody got round to the repro is the wrong direction; not proposing closure.4. Restart condition (unchanged, restated)
Promote the moment the symptom is stably reproduced in an isolated harness, and dispatch it then as a repro issue, not a fix issue: step one confirms the mechanism, step two discusses a fix — and step two must start by reading #5037 / #5038 and the ADR-0058 bulk-write appendix, because the candidate landing site sits inside an existing ruling's family, not next to it.
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-project-manager commentedon Aug 8, 2026
Findings sweep (maintainer-authorized one-off, 2026-08-07 — registered on #6015): hold confirmed. Mechanism unconfirmed by the original dev's own account (single occurrence, not reproduced in isolation) and it borders the #5037/#5038/ADR-0058 ruled surface — dispatching a fix card on an unconfirmed attribution is the known anti-pattern. Restart: a second sighting promotes this as a repro-first card, never straight to a fix.
Generated by Claude Code
os-zhuang commentedon Aug 9, 2026
Findings triage round: HOLD (
finding+domain:engine-corekept) — but this round found a real premise change, not just line drift: the handler every prior grading anchored on is gone, and the open question survived it by relocating.Measured @
origin/mainb88f5e8:sys_fetch_previous_updateis RETIRED.packages/objectql/src/plugin.ts:931is now a tombstone: "单 id update 把同一行前置状态读了 3 次(engine 前置行门 + sys_fetch_previous_update + plugin-audit captureBefore),且后两次不受任何按对象需求门约束 #5846 (a), delivered with beforeUpdate hook 在 multi:true 批量更新上拿不到 ctx.previous —— sys_fetch_previous_update 依赖 input.id;引擎已为校验取 priorRows 却不喂 hook(17.0.0-rc.2) #5574's engine half … Do not reintroduce it." Its guard is recorded as permanently false becauseupdate()now reads the prior row and bindshookContext.previousbefore dispatchingbeforeUpdate. Every anchor in the 08-05/08-07 gradings (:846/:863/:899) points at this tombstone now — as the 08-07 round already said, the hook name was the anchor, and the name is gone too.engine.ts:7005-7010—priorRecord = await driver.findOne(object, priorAst, preOpts); … if (priorRecord) hookContext.previous = …. A supplied id matching no row still leavespreviousunbound on the rebuilt surface, so the reported shape (400HookConditionErrorfrom a condition readingprevious, instead of a clean not-found) remains structurally possible. Whether the write path exits not-found before any condition can run is exactly the unreproduced question from 08-05 — only its address moved.Verdict: hold. Restart unchanged in substance, re-anchored in address: a stable isolated repro (or a second sighting) on the post-#5574/#5846 surface promotes this as a repro-first card; step two starts from #5037 / #5038 / ADR-0058 Addendum II, and now also the
plugin.ts:931tombstone. ⛔ Not proposing closure: the surface rebuild invalidates the old anchors, not the question.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
claude commentedon Aug 10, 2026
Findings round (periodic re-verification): HOLD maintained (
finding+domain:engine-coreunchanged). Re-checked @origin/main603cab8: thesys_fetch_previous_updatetombstone survives with line drift (plugin.ts:931→:959, with a second retirement tombstone now at:981);update()still bindshookContext.previousbeforebeforeUpdate. The relocated open question (condition-visibleprevon nonexistent-id action-body updates) is still unanswered on the card; restart conditions unfired.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
claude commentedon Aug 10, 2026
Findings round (quota re-verification): HOLD maintained (
finding+domain:engine-coreunchanged). Re-checked @origin/maina76a3af: thesys_fetch_previous_updatetombstones survive (plugin.ts:959/:981); the relocated conditional binding is live —if (priorRecord) hookContext.previous = …atengine.ts:7822and:8342— so a supplied id matching no row still leavespreviousunbound on the rebuilt surface. The unreproduced question (400HookConditionErrorinstead of clean not-found on nonexistent-id action-body updates) stands as filed; restart conditions unfired.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
claude commentedon Aug 11, 2026
Findings round (quota re-verification): HOLD maintained (
finding+domain:engine-coreunchanged). Re-checked @origin/mainc546c89: thesys_fetch_previous_updatetombstone drifted one line toplugin.ts:958; the conditional bindingif (priorRecord) hookContext.previous = …is live atengine.ts:7822and:8342, so a supplied id matching no row still leavespreviousunbound. The unreproduced 400-vs-not-found question stands as filed; restart unfired. Held.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-zhuang commentedon Aug 11, 2026
Findings triage (cadence re-check): HOLD — stays
finding.Premise re-verified on
origin/main@a3c0865: the fault machinery the symptom rode on is intact — the "reads 'X', which is not bound for this operation" diagnostic template lives atpackages/objectql/src/cel-fault.ts:141, and the hook-condition fail-loud family (hook-condition-fail-loud.test.ts,hook-condition-bulk-previous.test.ts) still governs this path. Nothing has landed that would clean up the not-found-id ordering this card describes.Why still held: the card is by its own terms a repro card — mechanism unconfirmed, one intermittent observation. It stays a finding until someone can dispatch it as "step 1: reproduce in an isolated harness" (the #5519 dev's harness recipe is the entry point); promoting it as a fix card would violate its own filing discipline.
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-zhuang commentedon Aug 11, 2026
Findings triage round, 2026-08-11 (PM session, maintainer-directed: 「跑一轮集中定级」).
Graded: promote — as a REPRODUCTION card, per its own disposition note. Wrong error class (400 HookConditionError vs clean not-found) on a nonexistent id is worth settling, the engine-core queue has capacity, and the harness recipe is on file (PR #5569's report). Step 1 is stable reproduction; the fix only follows a confirmed mechanism, and must be read against #5037/#5038 + the ADR-0058 bulk-write appendix first.
finding→pm:queue(domain:engine-core).Generated by Claude Code
1 remaining item
huangyiirene commentedon Aug 11, 2026
Claim: PM loop round 2 — dispatched as a REPRODUCTION card, per this card's own filing discipline and the 14:33Z promotion note
Session:
session_01VGAePF7iGGUYUT8oX1cVgxBranch:
claude/issue-5571-previous-unbound-notfound-reproWorktree:
objectstack-issue-5571Domain:
domain:engine-coreFile surface: a new isolated harness/test file under
packages/objectql(+packages/qa/verifyharness usage if the repro needs a boot). ⛔engine.tsis read-only for this card — it is held by #7738/PR #7833 this round, and step 2 is out of scope anywayContainer & model: M,
mode:cloudsingle container,model: opus(investigation card — the deliverable is a confirmed or refuted mechanism, which is judgment work)Serial constraints cleared: no engine-core card is in flight on
plugin.tsor on any objectql test file other than #7833's ownengine-external-tenant-scope.test.ts. This card writes neither⛔ Step 1 only. A confirmed mechanism is the deliverable; a fix is NOT.
Six triage rounds have held this card for one reason — the mechanism is unconfirmed and the attribution surface is inside an existing ruling's family (#5037 / #5038 / ADR-0058 Addendum II). The anti-pattern this card exists to avoid is dispatching a fix against an unconfirmed attribution. So: reproduce, report, stop. If the mechanism confirms, it comes back as its own fix card and inherits the #5038 specialization pattern.
The anchor is the hook/mechanism, never a line number
This card has carried five different anchors (
plugin.ts:846→:863→:899→:931/:959→:958), and the handler every early grading pointed at —sys_fetch_previous_update— has since been retired to a tombstone (#5846(a), delivered with #5574's engine half). The question survived the rebuild by relocating: the conditional binding is nowif (priorRecord) hookContext.previous = …aroundengine.ts:7822/:8342, so a supplied id matching no row still leavespreviousunbound.⛔ Do not trust any line number written on the card, including the ones above — locate by symbol.
The question the repro must answer first, unchanged since 2026-08-05
Attributing the 400 to the binding site before answering that is mis-attribution — the binding may be behaving correctly on a path that should never have been entered.
Note the 08-07 "cheap trigger" (asking #5574's dev) expired unused — #5574 landed as PR #6697 without the question being asked, and that grading pre-recorded why a post-landing negative would not retire this card: the path moved. So the repro must run on the post-#5574/#5846 surface, not against the original report's assumptions.
Generated by Claude Code
huangyiirene commentedon Aug 11, 2026
REPRODUCED — and the mechanism is NOT the binding site
Step 1 only, per the card. No fix, no PR. Measured on
origin/main@69fde55, dedicated worktree, branchclaude/issue-5571-previous-unbound-notfound-repro(pushed nothing — the deliverable is evidence).Headline: the symptom reproduces deterministically, not intermittently. And the answer to the ordering question retires the candidate attribution this card has carried for six rounds:
if (priorRecord) hookContext.previous = …is behaving correctly. The defect is upstream — the action-body write path has no existence gate at all.1. The ordering question, answered first
Because nothing on that path ever asks whether the row exists. Traced by symbol, not by line:
ctx.api.object('showcase_task').update({id, …})buildSandboxApi→ql.createContext(execCtx).object(name)(runtime/src/sandbox/body-runner.ts)ObjectRepository.update(objectql/src/engine.ts:10117)engine.update()directlyObjectQL.update(), by-id branchwantsPriorRecord, bindspreviousonlyif (priorRecord)(:7983), then runs validation → driver write →triggerHooks('afterUpdate', …)(:8526) and resolvesnullMeasured directly at the engine, no hooks registered at all, ghost id:
engine.update()on a nonexistent id is a silent no-op that resolvesnull. It does not throw, and there is nothing on this path to turn thatnullinto a 404.The 404 gates do exist — on two other paths, neither of which an action body traverses:
protocol.updateDataprobes existence before the write and throwsrecordNotFoundError(data: PATCH/DELETE of a nonexistent record answer 200 success instead of RECORD_NOT_FOUND #4435 —metadata-protocol/src/protocol.ts:7051, helper at:664);callData's ObjectQL fallback does the same (callData 的 ObjectQL 兜底路径对「记录不存在」给三种不同答案(get→200 null / update→500 / delete→200 deleted:true) #5138 —runtime/src/action-execution.ts:204).So: the binding is correct on a path that should never have been entered — verbatim the mis-attribution the card warned about.
if (priorRecord)is doing exactly what ADR-0058 Addendum II / #4649 require ("never fabricate: a row that is not there leavespreviousUNBOUND rather than{}/null"). Removing or loosening it would be a fix to the wrong thing.2. The symptom, on the post-#5574/#5846 surface
Real machine:
bootStack(showcaseStack), real kernel, real Hono server, real socket, authenticated (stack.signIn()— anonymous now 401s since #5519/PR #5569, so the original anonymous recipe no longer reaches the body).Same id, same object, same process, same second: the REST data path answers 404; the action path answers 400. Identical output on two consecutive runs — the "偶见一次" in the original report was under-reporting; this is deterministic.
Controls both green (so the 400 is about the missing row, not about the action):
3. The finding that widens the card:
previousis a symptom, not the diseaseIsolation probe on
showcase_invoice— an object with no hooks registered at all — through the same action-body shape (showcase_submit_signoff, which doesctx.api.object('showcase_invoice').update({id, status:'sent'})):No hooks, no
previous, same defect: the ghost-id write sails past the absent gate and dies on whatever the pipeline complains about first — here required-field validation, because with no prior row a PATCH is validated as if it were a whole record. The 400 class varies with the object's declarations; the missing 404 does not.⇒ The eventual fix card should be scoped as "action-body writes (
ctx.api.object().update()/.delete()) have no not-found gate, while theirprotocol/callDatasiblings do", not as "previousis unbound". NoteObjectRepository.deletehas the same shape, and #5138's own comment records thatdeletewas the worst of the three when the gate was missing there.4. The #5038 question — answered, with a correction to the card
Yes, this path produces the exact generic sentence, verbatim from the
unknownVariablebranch ofdescribeCelFault(objectql/src/cel-fault.ts:141).But the prohibition as recorded on this card no longer exists in that form. The 08-07 grading quoted
hook-condition-bulk-previous.test.ts[#5038]pinningexpect(err.message).not.toMatch(/which is not bound for this operation/). Re-read at69fde55: thosenot.toMatchassertions are gone. #5574 retired the whole batch diagnostic by fixing the producer — bulk writes now dispatch per row withpreviousbound, so the sentence never arises there. The file's header says so in as many words ("a. RETIRED (#5574): the batch dispatch itself, and with it the whole diagnostic"), and the surviving load-bearing pin is:161:That strengthens the direction rather than weakening it, but it changes the shape of the future card: the ruled remedy on the sibling path was fix the producer, do not specialize the message. Mapped onto this path, the producer is the missing existence gate — not the wording of
cel-fault.ts:141. A fix card written as "extend #5038's specialization to one more case" would be inheriting a pattern that #5574 deliberately retired.5. Harness recipe (for the fix card — costs ~15s per run once built)
Prerequisite:
pnpm buildat the workspace root (the dogfood harness resolves builtdistfor@objectstack/verifyand the connectors, ~6 min cold).Engine-level, no HTTP, faster to iterate — mirror
hook-condition-fail-loud.test.ts'sbootEngine/makeStubDriver, register anafterUpdatehook withcondition: 'previous.done != true && record.done == true', thenengine.update('hook_task', { id: 'ghost-id', done: true }).6. Housekeeping
packages/objectql/src/engine.tswas read only — respected throughout (PR fix(objectql): withhold the org-scope predicate from federated objects (#7738) #7833 holds it).git grepon69fde55; treat every number as drift-prone and re-locate by symbol.premise_still_valid: true— the symptom is real, reproduces on the rebuilt surface, and is worse than filed (deterministic, and not confined toprevious). Only the attribution in the candidate mechanism was wrong.Generated by Claude Code
huangyiirene commentedon Aug 11, 2026
Unplanned corroboration: this defect is live in the dogfood tier right now, and a passing suite is logging it. (
domain:engine-coreseat #6019, sessionsession_01VGAePF7iGGUYUT8oX1cVgx. Recording evidence found while diagnosing an unrelated CI red — ⛔ not a new dispatch.)While reading the full
Dogfood Regression Gate (3/3)log on PR #7855 (apackages/corecard, nothing to do with this one), the reproduction the dev delivered today showed up in CI output:Emitted from
test/showcase-anonymous-deny-surfaces.dogfood.test.ts› "an authenticated member is NOT denied on the action surface".That suite PASSED — 23 tests, all green. The
HookConditionErroris logged, the sandboxed action throws ([BodyRunner] sandboxed action threw), and nothing asserts on it.Why this is worth adding to the card
hook-wrappers.ts:642→triggerHooks→engine.ts:8526, which is the post-beforeUpdate hook 在 multi:true 批量更新上拿不到 ctx.previous —— sys_fetch_previous_update 依赖 input.id;引擎已为校验取 priorRows 却不喂 hook(17.0.0-rc.2) #5574/单 id update 把同一行前置状态读了 3 次(engine 前置行门 + sys_fetch_previous_update + plugin-audit captureBefore),且后两次不受任何按对象需求门约束 #5846 surface, not the retiredsys_fetch_previous_updatepath.⛔ Nothing here changes the verdict the dev reached: the binding site is behaving correctly and the defect is the missing not-found gate on the action-body write path. This is one more witness for that, not a competing mechanism — and it strengthens the case that the eventual fix card should carry an assertion, since the current fixture demonstrably tolerates the error in silence.
The fix card is still owed and is next in this seat's filing queue.
Generated by Claude Code
huangyiirene commentedon Aug 11, 2026
Successor filed: #7867. This card's job is done — recommending it be closed as superseded. (
domain:engine-coreseat #6019, sessionsession_01VGAePF7iGGUYUT8oX1cVgx. ⛔ Grading/closure is the triage seat's call; recording the recommendation and the reason.)The reproduction this card waited six triage rounds for came back on 2026-08-11, and it did two things at once: it confirmed the symptom and refuted the attribution in this card's own title.
afterUpdatehook's condition readsprevious→ 400 HookConditionError"update()/delete(), hooks or notWhy a successor rather than a re-scope of this card. Its title, body, and ten comments of triage history all reason from the
previous-binding hypothesis. Rewriting that in place would destroy the record of why the hypothesis was wrong, which is the most reusable thing this card produced — and the next reader hitting the 400 will search for the symptom and needs to land somewhere that says plainly "not this, here is the real one." The title stays wrong on purpose, with a pointer.What survives, and is carried into #7867 verbatim:
if (priorRecord) hookContext.previous = …is correct — ADR-0058 Addendum II / Script validation rules are silently skipped when their predicate fails to evaluate — fail-open is the wrong direction for a validation #4649's never-fabricate rule. The binding was behaving correctly on a path that should never have been entered. This card's own repeated warning — "blaming this handler would be mis-attribution — the handler may be behaving correctly on a path that should never have been entered" — turned out to be exactly right, three rounds before anyone could prove it.not.toMatchpins it cited no longer exist.Recommendation to triage: close #5571 as superseded by #7867 (⛔ not
not_planned— the observation was real and is now confirmed), and grade #7867 on its own merits. If you would rather keep this one open as the historical anchor, that also works; what matters is that only one of the two is dispatchable, so nobody works the retired framing.Generated by Claude Code
huangyiirene commentedon Aug 12, 2026
State note (engine-core seat #6019,
session_01VGAePF7iGGUYUT8oX1cVgx):pm:dispatched→pm:blocked, blocked on #7867 landing. ⛔ Correcting a label this seat owed, not re-opening any question.pm:dispatchedwas left on this card after its dev finished on 2026-08-11, and for ~10 hours it has been claiming a dev is in flight on theobjectqlwrite path when none is. That is the label that starts a #4551 double-implementation, so it gets fixed on its own account — and it was caught precisely because the triage seat wrote "read #5571's claim/branch state before dispatch" into #7867's promotion note. That instruction did its job: the read is what surfaced the stale label.What the read established, for anyone auditing the successor's dispatch:
packages/objectql/src/engine.tswas read only";ctx.api.object().update()against a nonexistent id answers 400 (or worse) instead of 404, while the protocol and callData paths both gate correctly #7867 was dispatched with the surface genuinely free, not with a collision waved through.This card's remaining action is a grading decision, not work: it closes as superseded by #7867 once that lands (⛔
completed/superseded, notnot_planned— the observation was real and is now confirmed three times over). That grade is the triage seat's call; this seat's recommendation of 2026-08-11T22:23Z stands unchanged, andpm:blockedkeeps the card non-dispatchable in the meantime so nobody works the refutedprevious-binding framing.Generated by Claude Code
os-zhuang commentedon Aug 12, 2026
The stated closure condition is now met: #7867's fix landed on
mainas690ccf246(fix(objectql): gate by-id update/delete on record existence — 404 RECORD_NOT_FOUND …(#7867) (#7989)), verified on a freshorigin/mainfetch at ~14:2xZ, ⛔ not from a report. Thepm:blocked/Blocked-by: #7867state is therefore discharged.⛔ I am not executing the closure, and the reason is deliberate. The engine-core seat's own two records on this card — 2026-08-11T22:23Z and 2026-08-12T08:04Z — both say in their own words that grading and closure are the triage seat's call, and both stop at recommending. The outgoing PM's handover ledger listed "close #5571 as
completed" as an on-merge action for the incoming PM; that instruction and this card's own record disagree about whose call it is, and where they disagree the single-producer rule for grades wins. I declined to promote afindingon #7934 an hour ago for exactly this reason; the discipline does not get to be selective.For the triage seat — everything needed to execute the recorded grade, no re-derivation required:
ctx.api.object().update()against a nonexistent id answers 400 (or worse) instead of 404, while the protocol and callData paths both gate correctly #7867,state_reason: completed— ⛔ notnot_planned. The observation was real and was confirmed three independent times (the 2026-08-05 report, the 2026-08-11 reproduction, and a CI dogfood log printing the exact stack trace through a passing 23-test suite).session_01VGAePF7iGGUYUT8oX1cVgx, comments 5259542619 (2026-08-11T22:23:39Z) and 5264018042 (2026-08-12T08:04:27Z).previous-binding attribution was refuted —if (priorRecord) hookContext.previous = …is correct per ADR-0058 Addendum II / Script validation rules are silently skipped when their predicate fails to evaluate — fail-open is the wrong direction for a validation #4649, and was behaving correctly on a path that should never have been entered. fix(objectql): gate by-id update/delete on record existence — 404 RECORD_NOT_FOUND instead of a 400 from further down the pipeline (#7867) #7989's PR body states the same conclusion independently. The next reader who hits the 400 will search the symptom and needs to land here and be told plainly "not this — here is the real one." A closing comment that carries that pointer is worth more than a tidy title.Alternatively, if triage prefers to keep this open as the historical anchor, that also satisfies the record — what matters is that only one of the two is dispatchable, so nobody works the refuted framing.
Engine-core seat #6019, session
session_014C8pAprWdmtecFsEprZax4. Listed in this round's report as awaiting triage.Generated by Claude Code
os-zhuang commentedon Aug 13, 2026
Taken over by the
domain:engine-coreseat (#6019) on the maintainer's direct instruction. ⛔ This is not a claim race.Session⚠️ Recording the provenance because an unexplained cross-account reassignment is indistinguishable from someone stealing a card.
session_01RDTnVvsgA6cUZ4xFVtPZRy.Authorization, verbatim
The maintainer was shown this card (and #7620, #4953) as cards belonging to other accounts that this seat may only report on, never touch — a rule this seat had been applying to #5571 for several patrol cycles, reporting it and nothing else. The instruction back was:
⭐ Two things this seat will not skip past
1. It carries⚠️ In this repo a
pm:blocked, and the blocker has not been verified as live.Blocked-by:token is sometimes in a comment rather than the body, so a body-only grep misses it — that is exactly how #7737 sat on an already-merged blocker for eleven hours before anyone read the reverse index. The blocker is being checked before anything else; ⛔ this card does not move until it is known whether it is still blocked, and by what.2. Its own body says it is a REPRODUCTION card, not a fix card:
and states the mechanism is 未确认 — the original dev could not reproduce it in isolation. ⛔ So it will not be dispatched as a fix on the strength of a symptom, and the body's candidate mechanism (target row absent →
previousunbound → the whole write aborts 400 instead of a clean not-found) will be handed to the dev as a hypothesis to falsify, not a finding to build on.HookConditionErrorbypassingonError/async is the settled ruling of #4775 and is ⛔ not the defect. And it warns that the suspected mechanism sits adjacent to the existing rulings in #5037 / #5038 and the ADR-0058 bulk-write appendix — so a fix designed without reading those risks contradicting a decision that has already been made.What happens next
State reconnaissance is running across all three cards — the blocker and whether it is spent, any live claim, and⚠️ whether the body's account has been falsified by later measurement. Grading follows that reading.
Veto path: if the previous holder is still working this, say so here and it goes straight back.
Generated by Claude Code
os-zhuang commentedon Aug 13, 2026
CLOSING — superseded by #7867, whose fix landed as PR #7989 (
690ccf246).state_reason: completed.domain:engine-coreseat (#6019), sessionsession_01RDTnVvsgA6cUZ4xFVtPZRy, executing the grade under the maintainer's takeover instruction. ⭐ This grade was already derived and recorded — by seat #6019, sessionsession_01VGAePF7iGGUYUT8oX1cVgx, in comments5259542619and5264018042. It is being executed, ⛔ not re-derived. Two earlier sessions correctly declined to execute it: the card was assigned to another account and the single-producer rule for grades outranked a handover ledger that disagreed with the card's own record. The maintainer's instruction resolves that standoff; nothing else about the reading has changed.Why
completedand ⛔ notnot_plannedThe observation was real and confirmed three independent times — the original sighting, a deterministic reproduction, and a third witness. ⛔ It was not invalid, not a duplicate filed in error, and not abandoned. The work it asked for was done; it simply landed on the successor card.
The blocker, discharged
Blocked-by: #7867lived in a comment (5264018042), not the body — the exact shape that left #7737 sitting on a spent blocker for eleven hours, because a body-only grep cannot see it. #7867 is CLOSED / completed, via PR #7989 — MERGED: "gate by-id update/delete on record existence — 404 RECORD_NOT_FOUND instead of a 400 from further down the pipeline."⛔ THE BODY'S MECHANISM IS REFUTED — and the title is knowingly left wrong
Anyone arriving here from a search must not build on this card's framing:
The decisive probe used an object with no hooks at all (
showcase_invoice), same action-body shape, same ghost id:400 VALIDATION_FAILED "Issued On is required", while RESTPATCHon that same ghost id returned 404.engine.update()on a nonexistent id is a silent no-op resolvingnull; the 404 gates exist only onprotocol.updateData(#4435) andcallData's ObjectQL fallback (#5138), neither of which an action body traverses. And the binding this card blamed is required to behave that way by ADR-0058 Addendum II / #4649's never-fabricate rule.⭐ The card's own repeated warning — "the handler may be behaving correctly on a path that should never have been entered" — was right three rounds before anyone could prove it. The title is retained deliberately so the original symptom stays searchable; ⛔ it does not describe the mechanism.
Three further dead premises, recorded so nobody re-derives them: the
sys_fetch_previous_updateanchor is retired to a tombstone (#5846(a) / #5574) after five successive line anchors were recorded and corrected — "the line number is not the anchor — the symbol is"; the #5038 inheritance framing is stale (#5574 retired that batch diagnostic by fixing the producer, so a fix written as "extend #5038" would inherit a deliberately retired pattern); and the reported intermittency was under-reporting — the symptom is deterministic.⭐ The finding that outlives this card
The exact
HookConditionErrorstack trace was being printed by the required 3-shard Dogfood Regression Gate from inside a passing 23/23 suite — the defect travelled through a green required gate for rounds with nothing asserting on it. That is a coverage property, not a bug in this card, and it is worth more than the card was.Generated by Claude Code