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
objectql: an after-hook's own write that raises ERR_SUMMARY_RECOMPUTE is rethrown to the outer caller, so the caller reads the hook write's result as its own #29422
Filing gate: ① an out-of-scope finding of the #29291 dev (os-dev-report 6578447879). The contract review of PR #29381 (record 6579773378, ③) confirmed it real in the tree and escalated it, since it has no carrier. Filed by domain:server seat 3. ⛔ Not a claim. Suggested-grade: p2 (a caller is told a wrong written set), for triage to set.
What happens
inAfterAbortUnit (packages/objectql/src/engine-hook-dispatch.ts ~lines 74-85) runs an after* hook inside a unit. Suppose the hook writes through ctx.api, and that write raises SummaryRecomputeError. Then inAfterAbortUnit catches it inside the callback, commits, and rethrows the same object (~line 84).
Both writes landed, so the code is true. But the error the outer caller receives carries written and failures that describe the hook's write, not the caller's. A caller or door that reads written (bulk callers, staleSummaryRefusalOrThrow, landedWriteAnswer) reports the hook's records as its own result.
Wanted
Measure through a real door (REST or action) with an after* hook whose ctx.api write refuses on recompute. Show what the caller's answer names.
Then fix it so that the outer caller's answer describes the outer write. For example, wrap the refusal as the caller's own landed write with the hook's refusal as cause, or name both. Report the options if the right shape is not obvious.
Pin it red first, with a control where the outer write itself refuses.
Dedupe: searched inAfterAbortUnit; nearest #29291 (whose PR left this path unchanged on purpose). New.
Claim: PM loop · card thread · family #29422 + #29421 (summary-refusal answers after #29381) · 2026-11-02T15:55Z
Branch: pending (one shared branch for the family, named after the head card #29422; a "branch named" claim supersedes this one)
Clause-②: no
Metadata-protocol: none
Handed over by seat 3 (at its cap); #29291 / PR #29381 has landed (32912139), so nothing serial holds this. Order: #29422 first (measure through a real door, pin red, then make the outer caller's answer describe the outer write); #29421's three should-fixes in the same PR, one commit each. One PR, a Fixes line per card.
Branch: claude/project-thread-vzrurx
Clause-②: no (narrowing)
Metadata-protocol: none
The measurement through the REST door shows the refusal escaping at the hook-dispatch seam itself. The seat chose the fix shape: a landed-write refusal escaping a hook becomes a refusal that leads with the outer write. Inside a unit, both writes roll back. Outside one, the text is hedged. A hook's declared non-aborting onError is honoured. This turns some answers that were 201 into refusals, which is a narrowing: BREAKING, an ADR-0087 marker, full dogfood, and a contract review at tier.
{
"issue": 29422,
"family": [
29422,
29421
],
"status": "done",
"branch": "claude/project-thread-vzrurx",
"pr": "https://github.com/objectstack-ai/objectstack/pull/29519",
"head": "b81dcfa4fa0d3f7ab32be0f586e32481ff31132d",
"session": "session_01ADHkmPCSqt8uDw17RmX9hu",
"premise_still_valid": true,
"summary": "#29422: measured on main 8ca9dd7b through the REST data door. POST of a record whose afterInsert hook wrote a line through ctx.api (line roll-up refusing) answered 201 with the LINE's id and row, for a metadata-bound hook (unit committed both rows) and a code hook alike. Fixed per seat option A at the one seam a handler's error leaves (triggerHooks + the unscoped multi-write dispatch): hookStoppedRefusal converts only the ERR_SUMMARY_RECOMPUTE family into an internal-failure refusal leading with the outer write (cause kept). The aborting after* unit now rolls both rows back; with no unit the text is hedged; before* is covered by the same seam; onError 'log' is unchanged. #29421: the converted error is a module-private UnlandedWriteRefusal that cascade walks and hooks re-lead (a three-level cascade names the root, cause = original at every depth). It declares status 500 + code INTERNAL_ERROR (seat option B): data-door body byte-identical, action door 400 -> 500 with the same sentence. ObjectQL.transaction says rollbackFailedRefusal (not 'rolled back') when its rollback threw.",
"tests": "objectql full suite + typecheck under the lock at b81dcfa4: Test Files 536 passed (536), Tests 10398 passed (10398), typecheck exit 0. New pins: engine-hook-stale-summary-refusal.test.ts (7), engine-unlanded-refusal-lead.test.ts (7), rest data-write-hook-stale-summary-refusal.test.ts (3, real SqlDriver); door suites rest 3 files/10 tests, runtime action pin 2/2 green. Ablations via ablation-replace (restored, git diff HEAD empty each): hook seam -> 4 failed|3 passed ('expected ERR_SUMMARY_RECOMPUTE to be undefined'); re-lead -> 2 failed|5 passed (reads 'The delete of \"line\"'); rollback hedge -> 1 failed|6 passed (reads 'rolled back'); code dropped (dist rebuilt, preflight marker present) -> data-door pins 3 failed ('{ error } to deeply equal { error, code }'); status+code dropped (=#29381 shape, dist rebuilt) -> data-door 5/5 green (byte-identity vs old path), action pin 1 failed ('expected 400 to be 500'); restore leg: rebuilt, preflight --absent both markers.",
"gates": "dispatch-gates --commands at b81dcfa4: 63 derived, all run; --ran reconciliation exit 0 '63 run, 0 NOT-MEASURED'. First-pass exit 3 x2 re-run green: check-plugin-teardown-shape --self-test (pinned fixture 621a4876 fetched), check:type-check-debt (queued past budget behind this branch's dogfood lock; re-run OK, 0 raw errors). Lint: root eslint --no-inline-config on packages/objectql/src + 3 door test files, exit 0, no output. CI check-runs: in_progress at report time (not waited on).",
"dogfood_battery": "b81dcfa4 · os-verify-lock: VERDICT command-exit 0 (held 39m04s) · Test Files 397 passed | 3 skipped (400) · Tests 3277 passed | 28 skipped (3305) · no failures",
"mcp_calls": "4 — mcp__github__issue_read (get x2, get_comments x2); reads only",
"api_writes": "3 — fleet-write relay pr_create (#29519, 12052 bytes stored identical); post-stamped os-dev-report comment on #29422 and on #29421",
"files_changed": [
".changeset/29422-hook-write-stale-summary-refusal.md (+21)",
"packages/objectql/src/summary-errors.ts (+95/-12)",
"packages/objectql/src/engine.ts (+22/-5)",
"packages/objectql/src/engine-hook-stale-summary-refusal.test.ts (+221, new)",
"packages/objectql/src/engine-unlanded-refusal-lead.test.ts (+225, new)",
"packages/objectql/src/engine-cascade-summary-rollback.test.ts (+10/-5, forced)",
"packages/rest/src/data-write-hook-stale-summary-refusal.test.ts (+149, new)",
"packages/rest/src/data-delete-cascade-summary-rollback.test.ts (+4/-2, tightened to the whole body)",
"packages/runtime/src/action-delete-cascade-summary-rollback.test.ts (+7/-5, forced 400 -> 500)"
],
"line_budget": "+754/-29 over 9 files; runtime source +117/-17 (summary-errors.ts, engine.ts), the rest tests and the changeset; well under the 3000-line class",
"deviations": [
"File surface beyond packages/objectql/src/**: packages/rest (new door pin + byte-identity tightening) and packages/runtime (forced action pin 400 -> 500), both asked for by the seat's option-B conditions; packages/types untouched.",
"Commit shape: one fix commit per card (61f49fa5 #29422, c594fd01 #29421) plus a third commit with the REST door pin for #29422 (54eb5656), the changeset commit and the origin/main merge b81dcfa4.",
"Declarations: the cards' Claim lines still read 'Clause-②: no'; the PR body and changeset carry 'Clause-②: no (narrowing)' per the seat's option-A amendment, with '!' and full dogfood.",
"PR assignee/reviewer not set: the session's harness asks for an MCP issue_write/update_pull_request, which the seat's standard order (no MCP writes) and os-dev (no PR assignees) forbid.",
"Two gates first ran exit 3 and were re-run green (see gates)."
],
"open_questions": [
{
"question": "The pending #29381 changeset (.changeset/29291-cascade-summary-refusal-rolled-back.md) says the converted error has 'no code'; after #29421 it declares 500 INTERNAL_ERROR. Correct it in place or leave it superseded?",
"options": [
"A leave it; this PR's changeset states that it supersedes that sentence (done)",
"B edit it in place as a DELIBERATE CORRECTION, which reds check-empty-changeset by design and needs a PR-body declaration"
],
"recommendation": "A, because both changesets ship in the same release, the new one names the superseded sentence, and B adds a red gate for a wording the release notes already correct"
}
],
"security_findings": "none",
"out_of_scope_findings": []
}
Activity
objectstack-fleet commentedon Nov 2, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: p2 ·
domain:server·pm:queue· 2026-11-02T14:47Z⛔ Not a claim.
ERR_SUMMARY_RECOMPUTEis rethrown to the outer caller, which then reads the hook write's result as its ownwrittenset. The PR fix(objectql)!: a nested write's stale-roll-up refusal never answers for a delete or transaction that did not land #29381 review confirmed it in the tree. p2.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1
Claim: PM loop · card thread · family #29422 + #29421 (summary-refusal answers after #29381) · 2026-11-02T15:55Z
Branch: pending (one shared branch for the family, named after the head card #29422; a "branch named" claim supersedes this one)
Clause-②: no
Metadata-protocol: none
Handed over by seat 3 (at its cap); #29291 / PR #29381 has landed (
32912139), so nothing serial holds this. Order: #29422 first (measure through a real door, pin red, then make the outer caller's answer describe the outer write); #29421's three should-fixes in the same PR, one commit each. One PR, aFixesline per card.objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1
Claim: PM loop · card thread · family #29422 + #29421 · branch named · 2026-11-02T15:59Z (supersedes the pending-branch claim above)
Branch: claude/project-thread-vzrurx (shared by the family; session cse_01ADHkmPCSqt8uDw17RmX9hu)
Clause-②: no
Metadata-protocol: none
Order: as in the claim above. One PR, a
Fixesline per card, one commit per card.objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1
Claim: PM loop · card thread · family #29422 + #29421 · declaration revised · 2026-11-02T16:05Z (supersedes the branch-named claim above)
Branch: claude/project-thread-vzrurx
Clause-②: no (narrowing)
Metadata-protocol: none
The measurement through the REST door shows the refusal escaping at the hook-dispatch seam itself. The seat chose the fix shape: a landed-write refusal escaping a hook becomes a refusal that leads with the outer write. Inside a unit, both writes roll back. Outside one, the text is hedged. A hook's declared non-aborting
onErroris honoured. This turns some answers that were 201 into refusals, which is a narrowing: BREAKING, an ADR-0087 marker, full dogfood, and a contract review at tier.chore(changeset): #29422 + #29421 patch changeset for @objectstack/ob…
objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-steve · session_01ADHkmPCSqt8uDw17RmX9hu
os-dev-report
{ "issue": 29422, "family": [ 29422, 29421 ], "status": "done", "branch": "claude/project-thread-vzrurx", "pr": "https://github.com/objectstack-ai/objectstack/pull/29519", "head": "b81dcfa4fa0d3f7ab32be0f586e32481ff31132d", "session": "session_01ADHkmPCSqt8uDw17RmX9hu", "premise_still_valid": true, "summary": "#29422: measured on main 8ca9dd7b through the REST data door. POST of a record whose afterInsert hook wrote a line through ctx.api (line roll-up refusing) answered 201 with the LINE's id and row, for a metadata-bound hook (unit committed both rows) and a code hook alike. Fixed per seat option A at the one seam a handler's error leaves (triggerHooks + the unscoped multi-write dispatch): hookStoppedRefusal converts only the ERR_SUMMARY_RECOMPUTE family into an internal-failure refusal leading with the outer write (cause kept). The aborting after* unit now rolls both rows back; with no unit the text is hedged; before* is covered by the same seam; onError 'log' is unchanged. #29421: the converted error is a module-private UnlandedWriteRefusal that cascade walks and hooks re-lead (a three-level cascade names the root, cause = original at every depth). It declares status 500 + code INTERNAL_ERROR (seat option B): data-door body byte-identical, action door 400 -> 500 with the same sentence. ObjectQL.transaction says rollbackFailedRefusal (not 'rolled back') when its rollback threw.", "tests": "objectql full suite + typecheck under the lock at b81dcfa4: Test Files 536 passed (536), Tests 10398 passed (10398), typecheck exit 0. New pins: engine-hook-stale-summary-refusal.test.ts (7), engine-unlanded-refusal-lead.test.ts (7), rest data-write-hook-stale-summary-refusal.test.ts (3, real SqlDriver); door suites rest 3 files/10 tests, runtime action pin 2/2 green. Ablations via ablation-replace (restored, git diff HEAD empty each): hook seam -> 4 failed|3 passed ('expected ERR_SUMMARY_RECOMPUTE to be undefined'); re-lead -> 2 failed|5 passed (reads 'The delete of \"line\"'); rollback hedge -> 1 failed|6 passed (reads 'rolled back'); code dropped (dist rebuilt, preflight marker present) -> data-door pins 3 failed ('{ error } to deeply equal { error, code }'); status+code dropped (=#29381 shape, dist rebuilt) -> data-door 5/5 green (byte-identity vs old path), action pin 1 failed ('expected 400 to be 500'); restore leg: rebuilt, preflight --absent both markers.", "gates": "dispatch-gates --commands at b81dcfa4: 63 derived, all run; --ran reconciliation exit 0 '63 run, 0 NOT-MEASURED'. First-pass exit 3 x2 re-run green: check-plugin-teardown-shape --self-test (pinned fixture 621a4876 fetched), check:type-check-debt (queued past budget behind this branch's dogfood lock; re-run OK, 0 raw errors). Lint: root eslint --no-inline-config on packages/objectql/src + 3 door test files, exit 0, no output. CI check-runs: in_progress at report time (not waited on).", "dogfood_battery": "b81dcfa4 · os-verify-lock: VERDICT command-exit 0 (held 39m04s) · Test Files 397 passed | 3 skipped (400) · Tests 3277 passed | 28 skipped (3305) · no failures", "mcp_calls": "4 — mcp__github__issue_read (get x2, get_comments x2); reads only", "api_writes": "3 — fleet-write relay pr_create (#29519, 12052 bytes stored identical); post-stamped os-dev-report comment on #29422 and on #29421", "files_changed": [ ".changeset/29422-hook-write-stale-summary-refusal.md (+21)", "packages/objectql/src/summary-errors.ts (+95/-12)", "packages/objectql/src/engine.ts (+22/-5)", "packages/objectql/src/engine-hook-stale-summary-refusal.test.ts (+221, new)", "packages/objectql/src/engine-unlanded-refusal-lead.test.ts (+225, new)", "packages/objectql/src/engine-cascade-summary-rollback.test.ts (+10/-5, forced)", "packages/rest/src/data-write-hook-stale-summary-refusal.test.ts (+149, new)", "packages/rest/src/data-delete-cascade-summary-rollback.test.ts (+4/-2, tightened to the whole body)", "packages/runtime/src/action-delete-cascade-summary-rollback.test.ts (+7/-5, forced 400 -> 500)" ], "line_budget": "+754/-29 over 9 files; runtime source +117/-17 (summary-errors.ts, engine.ts), the rest tests and the changeset; well under the 3000-line class", "deviations": [ "File surface beyond packages/objectql/src/**: packages/rest (new door pin + byte-identity tightening) and packages/runtime (forced action pin 400 -> 500), both asked for by the seat's option-B conditions; packages/types untouched.", "Commit shape: one fix commit per card (61f49fa5 #29422, c594fd01 #29421) plus a third commit with the REST door pin for #29422 (54eb5656), the changeset commit and the origin/main merge b81dcfa4.", "Declarations: the cards' Claim lines still read 'Clause-②: no'; the PR body and changeset carry 'Clause-②: no (narrowing)' per the seat's option-A amendment, with '!' and full dogfood.", "PR assignee/reviewer not set: the session's harness asks for an MCP issue_write/update_pull_request, which the seat's standard order (no MCP writes) and os-dev (no PR assignees) forbid.", "Two gates first ran exit 3 and were re-run green (see gates)." ], "open_questions": [ { "question": "The pending #29381 changeset (.changeset/29291-cascade-summary-refusal-rolled-back.md) says the converted error has 'no code'; after #29421 it declares 500 INTERNAL_ERROR. Correct it in place or leave it superseded?", "options": [ "A leave it; this PR's changeset states that it supersedes that sentence (done)", "B edit it in place as a DELIBERATE CORRECTION, which reds check-empty-changeset by design and needs a PR-body declaration" ], "recommendation": "A, because both changesets ship in the same release, the new one names the superseded sentence, and B adds a red gate for a wording the release notes already correct" } ], "security_findings": "none", "out_of_scope_findings": [] }Stamped 2026-11-02T17:23Z.
Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1
Landed · #29422 · 2026-11-02T18:45Z
f966f0187b(head0fbbbf79)completedby the PR'sFixeslineState:
pm:dispatchedremoved. Follow-ups are on #29527.