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: a cascade child's ERR_SUMMARY_RECOMPUTE escapes an atomic delete that rolled back, so every "landed write answers success" surface answers success for a delete that did not land #29291
Filing gate: ① finding of the seat's independent review of PR #29283 (#28861) at head e591f329, read from source and not run; the cause predates that PR (the data doors of #28781 / PR #28782 answer the same way), so it is out of #28861's scope. Filed by domain:server seat 3. ⛔ Not a claim. Suggested-grade: p2 · a write surface answers success for a write that was rolled back.
Reach: a by-id delete whose master_detail cascade deletes children under the 'atomic' plan, when one child's roll-up (summary) recompute into another parent fails. Every surface that turns ERR_SUMMARY_RECOMPUTE into the write's own success: the REST and MCP data doors and, after PR #29283, action ctx.engine / ctx.api, the sandbox repository and flow delete_record.
Reader who acts: a domain:server seat (packages/objectql/src/engine.ts, the cascade and the summary-recompute refusal).
What happens (source reading at e591f329)
runByIdDelete deletes each cascaded child through the public this.delete (engine.ts ~L20046) before the parent's own driver.delete (~L20903–20908); under the 'atomic' plan the whole walk runs inside this.transaction (~L20917).
A child whose recompute fails throws SummaryRecomputeError ("stored, only the recompute failed"). The transaction rolls back, so neither the child nor the parent is deleted.
The error keeps its code on the way out, and staleSummaryRefusalOrThrow (packages/types/src/summary-recompute-answer.ts ~L69) decides success by the code alone, so the caller is told the parent delete landed, with the child's result as written.
Wanted
Measure first on main (a two-level cascade with a failing roll-up on the child, atomic plan). Then make the code mean what it says: a SummaryRecomputeError raised by a nested write inside a transaction that then rolls back must not leave the outer call carrying it (for example, the cascade or transaction wrapper converts it to the refusal it really is, or the error records which write landed and the answer checks it is the outer one). Pin it at the engine and at one answering surface.
Read fresh. Read from source: under the 'atomic' plan, a cascade child's ERR_SUMMARY_RECOMPUTE escapes a delete that rolled back. Every surface that reads that code as success then reports a delete that never happened. p2. Measure on main first.
Claim: PM loop round 1 · card thread · 2026-11-02T11:45Z
Session: session_01ADHkmQJdgarUjPLNgFBY67 (the seat); the card thread is session_01ADHkmQQoWsHzMSsiaM89mr
Account: os-steve (the seat's linked user; the card's assignee)
Branch: claude/project-thread-qf9muq
Worktree: the card thread's own container checkout
Domain: domain:server
Seat: domain:server#3
Ruling-ref: the maintainer, verbatim, in the project thread: 「开 server 2、3、5 号席位, 前任已经下班,接手所有开发,包括跟进之前未完成的开发落地。」
File surface: packages/objectql/src/engine.ts (cascade walk in runByIdDelete / the transaction wrapper) and the summary-recompute error path (+ tests, one REST delete pin)
Container & model: card thread cmsg_01FdL1XeUyvVNxLreh3F4Qi92cLDdfrzbfFte3TG7C44xC, mode:thread, model: opus (--tier names no path mandate: PM's call, default), effort: high
Clause-②: yes (narrowing) — a delete that answered success after rolling back now refuses (! in the title, full dogfood, seat contract review)
Metadata-protocol: none
Responsibility: the card thread codes, tests and opens the PR; the seat reviews, runs the contract review, and lands
Thread-read: 6575733609
Premise: holds (site re-read on origin/maina19759ac by the seat before dispatch)
Serial constraints cleared: PR #29245 (#29062, engine.ts) merged as 344a16d; seat 5 #29253 and PR #29283 (#28861) still touch nearby code: merge main before opening
Resume-from: none — fresh from origin/main
State: pm:queue → pm:dispatched, assigned to os-steve, recorded by this claim. Order: project file server-seat3/standard-order.md + server-seat3/29291-delta.md.
This card's claim wrote the narrowing declaration as yes (narrowing). That is wrong. In scripts/pm/clause2-line.mjs, yes (narrowing) means a diff that widens one surface and narrows another. A narrowing that widens nothing is declared Clause-②: no (narrowing). If this card ships a narrowing, the PR body and the changeset carry Clause-②: no (narrowing), naming the key once on that line. Nothing else in the claim changes.
{
"issue": 29291,
"status": "done",
"branch": "claude/project-thread-qf9muq",
"pr": "https://github.com/objectstack-ai/objectstack/pull/29381",
"session": "session_01ADHkmQQoWsHzMSsiaM89mr",
"premise_still_valid": true,
"summary": "Measured on main a19759ac: atomic cascade rolled back (account + lines stored) yet the caller got ERR_SUMMARY_RECOMPUTE written:true, and REST DELETE answered 200 success; on a runtime that cannot roll back the first dependent was deleted, parent and later dependents stayed, same answer; a caller transaction the refusal rolled back answered the same. Fix at the engine only: one conversion in summary-errors.ts (unlandedWriteRefusal, code-less Error, original as cause, no new code/member/export) applied at the two cascade-walk call sites (by-id and predicate delete) and after the rollback in ObjectQL.transaction and ScopedContext.transaction. The refusal stays wherever it is still true (dependent's own delete, write outside a unit, caught-and-committed transaction, joined transaction).",
"tests": "New engine-cascade-summary-rollback.test.ts (7: 5 refusal pins + 2 controls; red on base 3 of first 5, the 2 later pins shown red by their own ablations) and rest data-delete-cascade-summary-rollback.test.ts (2; red on base 1 of 2 'expected 200 to be 500', objectql dist proven main's). Final head: 7/7 and 2/2. objectql full suite f1eec6fd 529/529 files 10266 tests; rest 368/368 files 6976 passed 332 skipped. Ablations via ablation-replace, each restored blob==HEAD: AB0 helper passes everything (dist marker present in 4 files; engine 5/7 red, REST 1/2 red; restored, rebuilt, --absent + tree clean); AB1 by-id walk 1/7; AB2 predicate walk 1/7; AB3 ObjectQL.transaction 1/7; AB4 ScopedContext.transaction 1/7.",
"gates": "dispatch-gates --commands on 3ff9597a: 63 derived; --ran reconciliation 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN (exit 0), all 63 exit 0; 4 changeset gates re-run after last changeset edit, exit 0. First pass on f1eec6fd: check:query-options-erasure exit 1 and check:rest-log-spy-declared exit 1, both real reds in my new tests, fixed in 4dd59050 (typed options, console spies removed); teardown self-test and dual-build-cjs-loads exit 3 prerequisites, green on re-run. pnpm lint exit 0 on 3ff9597a. objectql + rest typecheck exit 0 on 4dd59050 (same test files as head). PR body offline: check-changeset-no-major --event exit 0 (LEVEL AXIS narrowing + none at patch); closing-keyword-parity --body only Fixes #29291. CI check-runs: in_progress at report time.",
"dogfood_battery": "f1eec6fd · os-verify-lock VERDICT command-exit 0 (36m13s) · Test Files 391 passed | 3 skipped (394) · Tests 3223 passed | 28 skipped (3251) · no failures. Head 3ff9597a differs only in the two new test files and the changeset Clause line (outside the dogfood closure's build output); not re-run.",
"line_budget": "5 files, +463 / -8 (generated: none): 29291-cascade-summary-refusal-rolled-back.md +19/-0; engine-cascade-summary-rollback.test.ts +218/-0; engine.ts +18/-8; summary-errors.ts +59/-0; data-delete-cascade-summary-rollback.test.ts +149/-0",
"files_changed": [
".changeset/29291-cascade-summary-refusal-rolled-back.md",
"packages/objectql/src/engine.ts",
"packages/objectql/src/summary-errors.ts",
"packages/objectql/src/engine-cascade-summary-rollback.test.ts",
"packages/rest/src/data-delete-cascade-summary-rollback.test.ts"
],
"deviations": [
"PR body line 1-2 is the harness attribution block; Fixes #29291 follows on line 4 (os-dev exception for the harness attribution line).",
"No assignees/reviewer on the PR: the relay's dev pr_create carries none (maintainer ruling in ops.mjs); the harness ask to assign os-steve was skipped.",
"Dogfood ran on f1eec6fd, not the final head 3ff9597a: the two later commits touch only the two new test files and the changeset's Clause line.",
"First AB0 attempt used a comment marker tsup strips, so the dist preflight could not see it; rerun with a string-literal marker (same red counts).",
"Changeset's Clause-② line was first written in my own words, then replaced by the claim's line verbatim (3ff9597a)."
],
"mcp_calls": "5 — all reads: issue_read get #29291, issue_read get_comments #29291 x2, pull_request_read get #29283, pull_request_read get #29381; no write tool",
"api_writes": "2 — pr_create via fleet-write relay (run 41453441324, PR #29381, 14145 bytes sent = stored); this os-dev-report comment via post-stamped.mjs on #29291",
"open_questions": [
{
"question": "On a runtime that cannot roll back (and the 'split' plan), a dependent's stale-roll-up refusal stops the cascade after that dependent's delete landed. This PR refuses the parent's caller (its own delete did not land); should the walk instead tolerate a dependent's stale summary, finish the cascade and answer ERR_SUMMARY_RECOMPUTE for the parent?",
"options": [
"A Keep this PR's reading: refuse the parent's caller; what lands is unchanged",
"B Walk tolerates a dependent's stale summary (commit-then-rethrow like inAfterAbortUnit), parent delete lands, caller hears ERR_SUMMARY_RECOMPUTE — a widening, its own card"
],
"recommendation": "A, because the ruling covers it unambiguously (the parent's own write did not land) and B turns refused deletes into landed ones, which needs its own ruling."
}
],
"security_findings": "none",
"out_of_scope_findings": [
"class: none (read-only inference, not measured) · an after* hook whose ctx.api write raises ERR_SUMMARY_RECOMPUTE: inAfterAbortUnit commits and rethrows the hook write's refusal, so the outer caller reads written = the hook write's result, not its own · carrier: none · noted in PR Acceptance notes",
"class: none · ScopedContext begin/commit/rollback trio (QuickJS sandbox ctx.api.transaction) is caller-driven, so the engine cannot convert a refusal the caller rolls back; PR #29283 makes sandbox body writes answer success inside it · carrier: none · noted"
]
}
{
"issue": 29291,
"status": "done",
"branch": "claude/project-thread-qf9muq",
"pr": "https://github.com/objectstack-ai/objectstack/pull/29381",
"session": "session_01ADHkmQQoWsHzMSsiaM89mr",
"premise_still_valid": true,
"summary": "Measured on main a19759ac: atomic cascade rolled back (account + lines stored) yet the caller got ERR_SUMMARY_RECOMPUTE written:true, and REST DELETE answered 200 success; on a runtime that cannot roll back the first dependent was deleted, parent and later dependents stayed, same answer; a caller transaction the refusal rolled back answered the same. Fix at the engine only: one conversion in summary-errors.ts (unlandedWriteRefusal, code-less Error, original as cause, no new code/member/export) applied at the two cascade-walk call sites (by-id and predicate delete) and after the rollback in ObjectQL.transaction and ScopedContext.transaction. The refusal stays wherever it is still true (dependent's own delete, write outside a unit, caught-and-committed transaction, joined transaction). Round 2 (seat FAIL relay): main merged (8fc7f886, carries #29283 as 153869d7); one action-surface pin added; Clause-② now 'no (narrowing)' in PR body and changeset per claim amendment 6577647622; open question ruled A.",
"tests": "New engine-cascade-summary-rollback.test.ts (7: 5 refusal pins + 2 controls; red on base 3 of first 5, the 2 later pins shown red by their own ablations) and rest data-delete-cascade-summary-rollback.test.ts (2; red on base 1 of 2 'expected 200 to be 500', objectql dist proven main's). Final head: 7/7 and 2/2. objectql full suite f1eec6fd 529/529 files 10266 tests; rest 368/368 files 6976 passed 332 skipped. Ablations via ablation-replace, each restored blob==HEAD: AB0 helper passes everything (dist marker present in 4 files; engine 5/7 red, REST 1/2 red; restored, rebuilt, --absent + tree clean); AB1 by-id walk 1/7; AB2 predicate walk 1/7; AB3 ObjectQL.transaction 1/7; AB4 ScopedContext.transaction 1/7. Round 2: new packages/runtime/src/action-delete-cascade-summary-rollback.test.ts (2) on the real stack (HttpDispatcher -> POST /actions -> ctx.engine -> ObjectQL -> SqlDriver better-sqlite3): an action body's ctx.engine.delete of the account answers the action door's 400 refusal (success:false, engine message), the body's next line never runs, account + 3 lines stored; CONTROL deleting a line answers 200 with the body's result. AB0-action (same helper mutation, built, dist marker in 4 files): 1/2 red 'expected 200 to be 400', body {success:true,data:{deleted:'a1'}}; restored, rebuilt, --absent + tree clean exit 0, green again. Final head 2af3815d: objectql 7/7, rest 2/2, runtime 2/2; runtime typecheck exit 0.",
"gates": "dispatch-gates --commands on 2af3815d: 63 derived (same 63 as f1eec6fd/3ff9597a); --ran reconciliation 63 derived, 63 run, 0 NOT-MEASURED (exit 0); all 63 exit 0 — check:dual-build-cjs-loads first exit 3 (prerequisite: dists missing), recorded from its re-run after turbo build ./packages/** (78/78 cache hits): exit 0. pnpm lint exit 0 on 2af3815d. runtime typecheck exit 0. PR body offline: check-changeset-no-major --event exit 0 (LEVEL AXIS no (narrowing) + none at patch). Round 1 gate history: query-options-erasure and rest-log-spy-declared reds in my new tests fixed in 4dd59050.",
"dogfood_battery": "2af3815d (final head; rerun because the main merge changed code) · closure build 68/68 · os-verify-lock VERDICT command-exit 0 (34m19s, shared box) · Test Files 393 passed | 3 skipped (396) · Tests 3243 passed | 28 skipped (3271) · no failures. Round 1 on f1eec6fd also green (391 files, 3223 tests).",
"line_budget": "6 files, +618 / -8 (generated: none): 29291-cascade-summary-refusal-rolled-back.md +21/-0; engine-cascade-summary-rollback.test.ts +218/-0; engine.ts +18/-8; summary-errors.ts +59/-0; data-delete-cascade-summary-rollback.test.ts +149/-0; action-delete-cascade-summary-rollback.test.ts +153/-0",
"files_changed": [
".changeset/29291-cascade-summary-refusal-rolled-back.md",
"packages/objectql/src/engine.ts",
"packages/objectql/src/summary-errors.ts",
"packages/objectql/src/engine-cascade-summary-rollback.test.ts",
"packages/rest/src/data-delete-cascade-summary-rollback.test.ts",
"packages/runtime/src/action-delete-cascade-summary-rollback.test.ts"
],
"deviations": [
"PR body line 1-2 is the harness attribution block; Fixes #29291 follows on line 4 (os-dev exception for the harness attribution line).",
"No assignees/reviewer on the PR: the relay's dev pr_create carries none (maintainer ruling in ops.mjs); the harness ask to assign os-steve was skipped.",
"First AB0 attempt used a comment marker tsup strips, so the dist preflight could not see it; rerun with a string-literal marker (same red counts).",
"Changeset's Clause-② line was first written in my own words, then replaced by the claim's line verbatim (3ff9597a).",
"Round 1 PR body/changeset declared 'Clause-② yes (narrowing)' (copied from the first claim); amendment 6577647622 was posted after my read. Now 'no (narrowing)' on the key line, note on its own line.",
"Round 1 said PR #29283 was open; it had merged as 153869d7. Main merged and the action pin added this round.",
"The action door answers the refusal 400 (its rule for any throw out of an action body), not 500 like the data door; pinned as measured, door unchanged.",
"PR body now ends with the relay's '<!-- signed: … -->' trailer after the Generated-by footer (issue_patch requires it).",
"A queued runtime typecheck exceeded the lock's 540s queue budget during the dogfood and was rerun after it (exit 0)."
],
"mcp_calls": "5 — all reads (unchanged this round): issue_read get #29291, issue_read get_comments #29291 x2, pull_request_read get #29283, pull_request_read get #29381; no write tool",
"api_writes": "4 — round 1: pr_create (relay run 41453441324) + os-dev-report comment 6578447879; round 2: issue_patch PR #29381 body (relay run 41461192476, 16517 bytes sent = stored) + this round-2 report comment via post-stamped.mjs",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"class: none (read-only inference, not measured) · an after* hook whose ctx.api write raises ERR_SUMMARY_RECOMPUTE: inAfterAbortUnit commits and rethrows the hook write's refusal, so the outer caller reads written = the hook write's result, not its own · carrier: none · noted in PR Acceptance notes",
"class: none · ScopedContext begin/commit/rollback trio (QuickJS sandbox ctx.api.transaction) is caller-driven, so the engine cannot convert a refusal the caller rolls back; PR #29283 makes sandbox body writes answer success inside it · carrier: none · noted"
],
"round": 2,
"head": "2af3815d",
"ruled": "Open question (non-atomic cascade): ruled A by the seat — keep refusing the parent's caller."
}
git grep -n "export function rolledBackRefusal" origin/main -- packages/objectql/src/summary-errors.ts
line 131
Landed: merged as 32912139. A child's ERR_SUMMARY_RECOMPUTE can stop a cascade delete, or roll back an owned transaction(). In both cases the caller now gets an ordinary error saying the write did not land, with the refusal kept as cause: 500 at the data door, 400 at the action door, and every row still stored. Writes that really landed keep the refusal unchanged.
Seat ACCEPT 6579832522 and the CONTRACT_REVIEW_TIER record 6579773378 (narrowing face, PASS on 2af3815d) are on the PR. Clause-②: no (narrowing), full dogfood VERDICT 0. Fixes #29291 closed this card on merge.
Activity
objectstack-fleet commentedon Nov 2, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: p2 ·
domain:server·pm:queue· 2026-11-02T11:13Z⛔ Not a claim.
'atomic'plan, a cascade child'sERR_SUMMARY_RECOMPUTEescapes a delete that rolled back. Every surface that reads that code as success then reports a delete that never happened. p2. Measure onmainfirst.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67
Claim: PM loop round 1 · card thread · 2026-11-02T11:45Z
Session:
session_01ADHkmQJdgarUjPLNgFBY67(the seat); the card thread issession_01ADHkmQQoWsHzMSsiaM89mrAccount:
os-steve(the seat's linked user; the card's assignee)Branch:
claude/project-thread-qf9muqWorktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#3Ruling-ref: the maintainer, verbatim, in the project thread: 「开 server 2、3、5 号席位, 前任已经下班,接手所有开发,包括跟进之前未完成的开发落地。」
File surface:
packages/objectql/src/engine.ts(cascade walk inrunByIdDelete/ thetransactionwrapper) and the summary-recompute error path (+ tests, one REST delete pin)Container & model: card thread
cmsg_01FdL1XeUyvVNxLreh3F4Qi92cLDdfrzbfFte3TG7C44xC,mode:thread,model: opus(--tiernames no path mandate: PM's call, default),effort: highClause-②: yes (narrowing) — a delete that answered success after rolling back now refuses (
!in the title, full dogfood, seat contract review)Metadata-protocol: none
Responsibility: the card thread codes, tests and opens the PR; the seat reviews, runs the contract review, and lands
Thread-read: 6575733609
Premise: holds (site re-read on
origin/maina19759acby the seat before dispatch)Serial constraints cleared: PR #29245 (#29062, engine.ts) merged as 344a16d; seat 5 #29253 and PR #29283 (#28861) still touch nearby code: merge main before opening
Resume-from: none — fresh from
origin/mainState:
pm:queue→pm:dispatched, assigned toos-steve, recorded by this claim. Order: project file server-seat3/standard-order.md + server-seat3/29291-delta.md.objectstack-fleet commentedon Nov 2, 2026
domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67
Claim amendment · Clause-② spelling · 2026-11-02T12:53Z
This card's claim wrote the narrowing declaration as
yes (narrowing). That is wrong. Inscripts/pm/clause2-line.mjs,yes (narrowing)means a diff that widens one surface and narrows another. A narrowing that widens nothing is declaredClause-②: no (narrowing). If this card ships a narrowing, the PR body and the changeset carryClause-②: no (narrowing), naming the key once on that line. Nothing else in the claim changes.objectstack-fleet commentedon Nov 2, 2026
domain:server#3 · os-steve · session_01ADHkmQQoWsHzMSsiaM89mr
os-dev-report
{ "issue": 29291, "status": "done", "branch": "claude/project-thread-qf9muq", "pr": "https://github.com/objectstack-ai/objectstack/pull/29381", "session": "session_01ADHkmQQoWsHzMSsiaM89mr", "premise_still_valid": true, "summary": "Measured on main a19759ac: atomic cascade rolled back (account + lines stored) yet the caller got ERR_SUMMARY_RECOMPUTE written:true, and REST DELETE answered 200 success; on a runtime that cannot roll back the first dependent was deleted, parent and later dependents stayed, same answer; a caller transaction the refusal rolled back answered the same. Fix at the engine only: one conversion in summary-errors.ts (unlandedWriteRefusal, code-less Error, original as cause, no new code/member/export) applied at the two cascade-walk call sites (by-id and predicate delete) and after the rollback in ObjectQL.transaction and ScopedContext.transaction. The refusal stays wherever it is still true (dependent's own delete, write outside a unit, caught-and-committed transaction, joined transaction).", "tests": "New engine-cascade-summary-rollback.test.ts (7: 5 refusal pins + 2 controls; red on base 3 of first 5, the 2 later pins shown red by their own ablations) and rest data-delete-cascade-summary-rollback.test.ts (2; red on base 1 of 2 'expected 200 to be 500', objectql dist proven main's). Final head: 7/7 and 2/2. objectql full suite f1eec6fd 529/529 files 10266 tests; rest 368/368 files 6976 passed 332 skipped. Ablations via ablation-replace, each restored blob==HEAD: AB0 helper passes everything (dist marker present in 4 files; engine 5/7 red, REST 1/2 red; restored, rebuilt, --absent + tree clean); AB1 by-id walk 1/7; AB2 predicate walk 1/7; AB3 ObjectQL.transaction 1/7; AB4 ScopedContext.transaction 1/7.", "gates": "dispatch-gates --commands on 3ff9597a: 63 derived; --ran reconciliation 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN (exit 0), all 63 exit 0; 4 changeset gates re-run after last changeset edit, exit 0. First pass on f1eec6fd: check:query-options-erasure exit 1 and check:rest-log-spy-declared exit 1, both real reds in my new tests, fixed in 4dd59050 (typed options, console spies removed); teardown self-test and dual-build-cjs-loads exit 3 prerequisites, green on re-run. pnpm lint exit 0 on 3ff9597a. objectql + rest typecheck exit 0 on 4dd59050 (same test files as head). PR body offline: check-changeset-no-major --event exit 0 (LEVEL AXIS narrowing + none at patch); closing-keyword-parity --body only Fixes #29291. CI check-runs: in_progress at report time.", "dogfood_battery": "f1eec6fd · os-verify-lock VERDICT command-exit 0 (36m13s) · Test Files 391 passed | 3 skipped (394) · Tests 3223 passed | 28 skipped (3251) · no failures. Head 3ff9597a differs only in the two new test files and the changeset Clause line (outside the dogfood closure's build output); not re-run.", "line_budget": "5 files, +463 / -8 (generated: none): 29291-cascade-summary-refusal-rolled-back.md +19/-0; engine-cascade-summary-rollback.test.ts +218/-0; engine.ts +18/-8; summary-errors.ts +59/-0; data-delete-cascade-summary-rollback.test.ts +149/-0", "files_changed": [ ".changeset/29291-cascade-summary-refusal-rolled-back.md", "packages/objectql/src/engine.ts", "packages/objectql/src/summary-errors.ts", "packages/objectql/src/engine-cascade-summary-rollback.test.ts", "packages/rest/src/data-delete-cascade-summary-rollback.test.ts" ], "deviations": [ "PR body line 1-2 is the harness attribution block; Fixes #29291 follows on line 4 (os-dev exception for the harness attribution line).", "No assignees/reviewer on the PR: the relay's dev pr_create carries none (maintainer ruling in ops.mjs); the harness ask to assign os-steve was skipped.", "Dogfood ran on f1eec6fd, not the final head 3ff9597a: the two later commits touch only the two new test files and the changeset's Clause line.", "First AB0 attempt used a comment marker tsup strips, so the dist preflight could not see it; rerun with a string-literal marker (same red counts).", "Changeset's Clause-② line was first written in my own words, then replaced by the claim's line verbatim (3ff9597a)." ], "mcp_calls": "5 — all reads: issue_read get #29291, issue_read get_comments #29291 x2, pull_request_read get #29283, pull_request_read get #29381; no write tool", "api_writes": "2 — pr_create via fleet-write relay (run 41453441324, PR #29381, 14145 bytes sent = stored); this os-dev-report comment via post-stamped.mjs on #29291", "open_questions": [ { "question": "On a runtime that cannot roll back (and the 'split' plan), a dependent's stale-roll-up refusal stops the cascade after that dependent's delete landed. This PR refuses the parent's caller (its own delete did not land); should the walk instead tolerate a dependent's stale summary, finish the cascade and answer ERR_SUMMARY_RECOMPUTE for the parent?", "options": [ "A Keep this PR's reading: refuse the parent's caller; what lands is unchanged", "B Walk tolerates a dependent's stale summary (commit-then-rethrow like inAfterAbortUnit), parent delete lands, caller hears ERR_SUMMARY_RECOMPUTE — a widening, its own card" ], "recommendation": "A, because the ruling covers it unambiguously (the parent's own write did not land) and B turns refused deletes into landed ones, which needs its own ruling." } ], "security_findings": "none", "out_of_scope_findings": [ "class: none (read-only inference, not measured) · an after* hook whose ctx.api write raises ERR_SUMMARY_RECOMPUTE: inAfterAbortUnit commits and rethrows the hook write's refusal, so the outer caller reads written = the hook write's result, not its own · carrier: none · noted in PR Acceptance notes", "class: none · ScopedContext begin/commit/rollback trio (QuickJS sandbox ctx.api.transaction) is caller-driven, so the engine cannot convert a refusal the caller rolls back; PR #29283 makes sandbox body writes answer success inside it · carrier: none · noted" ] }Stamped 2026-11-02T13:30Z.
Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#3 · os-steve · session_01ADHkmQQoWsHzMSsiaM89mr
os-dev-report
{ "issue": 29291, "status": "done", "branch": "claude/project-thread-qf9muq", "pr": "https://github.com/objectstack-ai/objectstack/pull/29381", "session": "session_01ADHkmQQoWsHzMSsiaM89mr", "premise_still_valid": true, "summary": "Measured on main a19759ac: atomic cascade rolled back (account + lines stored) yet the caller got ERR_SUMMARY_RECOMPUTE written:true, and REST DELETE answered 200 success; on a runtime that cannot roll back the first dependent was deleted, parent and later dependents stayed, same answer; a caller transaction the refusal rolled back answered the same. Fix at the engine only: one conversion in summary-errors.ts (unlandedWriteRefusal, code-less Error, original as cause, no new code/member/export) applied at the two cascade-walk call sites (by-id and predicate delete) and after the rollback in ObjectQL.transaction and ScopedContext.transaction. The refusal stays wherever it is still true (dependent's own delete, write outside a unit, caught-and-committed transaction, joined transaction). Round 2 (seat FAIL relay): main merged (8fc7f886, carries #29283 as 153869d7); one action-surface pin added; Clause-② now 'no (narrowing)' in PR body and changeset per claim amendment 6577647622; open question ruled A.", "tests": "New engine-cascade-summary-rollback.test.ts (7: 5 refusal pins + 2 controls; red on base 3 of first 5, the 2 later pins shown red by their own ablations) and rest data-delete-cascade-summary-rollback.test.ts (2; red on base 1 of 2 'expected 200 to be 500', objectql dist proven main's). Final head: 7/7 and 2/2. objectql full suite f1eec6fd 529/529 files 10266 tests; rest 368/368 files 6976 passed 332 skipped. Ablations via ablation-replace, each restored blob==HEAD: AB0 helper passes everything (dist marker present in 4 files; engine 5/7 red, REST 1/2 red; restored, rebuilt, --absent + tree clean); AB1 by-id walk 1/7; AB2 predicate walk 1/7; AB3 ObjectQL.transaction 1/7; AB4 ScopedContext.transaction 1/7. Round 2: new packages/runtime/src/action-delete-cascade-summary-rollback.test.ts (2) on the real stack (HttpDispatcher -> POST /actions -> ctx.engine -> ObjectQL -> SqlDriver better-sqlite3): an action body's ctx.engine.delete of the account answers the action door's 400 refusal (success:false, engine message), the body's next line never runs, account + 3 lines stored; CONTROL deleting a line answers 200 with the body's result. AB0-action (same helper mutation, built, dist marker in 4 files): 1/2 red 'expected 200 to be 400', body {success:true,data:{deleted:'a1'}}; restored, rebuilt, --absent + tree clean exit 0, green again. Final head 2af3815d: objectql 7/7, rest 2/2, runtime 2/2; runtime typecheck exit 0.", "gates": "dispatch-gates --commands on 2af3815d: 63 derived (same 63 as f1eec6fd/3ff9597a); --ran reconciliation 63 derived, 63 run, 0 NOT-MEASURED (exit 0); all 63 exit 0 — check:dual-build-cjs-loads first exit 3 (prerequisite: dists missing), recorded from its re-run after turbo build ./packages/** (78/78 cache hits): exit 0. pnpm lint exit 0 on 2af3815d. runtime typecheck exit 0. PR body offline: check-changeset-no-major --event exit 0 (LEVEL AXIS no (narrowing) + none at patch). Round 1 gate history: query-options-erasure and rest-log-spy-declared reds in my new tests fixed in 4dd59050.", "dogfood_battery": "2af3815d (final head; rerun because the main merge changed code) · closure build 68/68 · os-verify-lock VERDICT command-exit 0 (34m19s, shared box) · Test Files 393 passed | 3 skipped (396) · Tests 3243 passed | 28 skipped (3271) · no failures. Round 1 on f1eec6fd also green (391 files, 3223 tests).", "line_budget": "6 files, +618 / -8 (generated: none): 29291-cascade-summary-refusal-rolled-back.md +21/-0; engine-cascade-summary-rollback.test.ts +218/-0; engine.ts +18/-8; summary-errors.ts +59/-0; data-delete-cascade-summary-rollback.test.ts +149/-0; action-delete-cascade-summary-rollback.test.ts +153/-0", "files_changed": [ ".changeset/29291-cascade-summary-refusal-rolled-back.md", "packages/objectql/src/engine.ts", "packages/objectql/src/summary-errors.ts", "packages/objectql/src/engine-cascade-summary-rollback.test.ts", "packages/rest/src/data-delete-cascade-summary-rollback.test.ts", "packages/runtime/src/action-delete-cascade-summary-rollback.test.ts" ], "deviations": [ "PR body line 1-2 is the harness attribution block; Fixes #29291 follows on line 4 (os-dev exception for the harness attribution line).", "No assignees/reviewer on the PR: the relay's dev pr_create carries none (maintainer ruling in ops.mjs); the harness ask to assign os-steve was skipped.", "First AB0 attempt used a comment marker tsup strips, so the dist preflight could not see it; rerun with a string-literal marker (same red counts).", "Changeset's Clause-② line was first written in my own words, then replaced by the claim's line verbatim (3ff9597a).", "Round 1 PR body/changeset declared 'Clause-② yes (narrowing)' (copied from the first claim); amendment 6577647622 was posted after my read. Now 'no (narrowing)' on the key line, note on its own line.", "Round 1 said PR #29283 was open; it had merged as 153869d7. Main merged and the action pin added this round.", "The action door answers the refusal 400 (its rule for any throw out of an action body), not 500 like the data door; pinned as measured, door unchanged.", "PR body now ends with the relay's '<!-- signed: … -->' trailer after the Generated-by footer (issue_patch requires it).", "A queued runtime typecheck exceeded the lock's 540s queue budget during the dogfood and was rerun after it (exit 0)." ], "mcp_calls": "5 — all reads (unchanged this round): issue_read get #29291, issue_read get_comments #29291 x2, pull_request_read get #29283, pull_request_read get #29381; no write tool", "api_writes": "4 — round 1: pr_create (relay run 41453441324) + os-dev-report comment 6578447879; round 2: issue_patch PR #29381 body (relay run 41461192476, 16517 bytes sent = stored) + this round-2 report comment via post-stamped.mjs", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "class: none (read-only inference, not measured) · an after* hook whose ctx.api write raises ERR_SUMMARY_RECOMPUTE: inAfterAbortUnit commits and rethrows the hook write's refusal, so the outer caller reads written = the hook write's result, not its own · carrier: none · noted in PR Acceptance notes", "class: none · ScopedContext begin/commit/rollback trio (QuickJS sandbox ctx.api.transaction) is caller-driven, so the engine cannot convert a refusal the caller rolls back; PR #29283 makes sandbox body writes answer success inside it · carrier: none · noted" ], "round": 2, "head": "2af3815d", "ruled": "Open question (non-atomic cascade): ruled A by the seat — keep refusing the parent's caller." }objectstack-fleet commentedon Nov 2, 2026
domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67
Landing record · #29291 · PR #29381
domain:serverseat 3 ·session_01ADHkmQJdgarUjPLNgFBY67· 2026-11-02T14:59Zgit ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-29381-*'git merge-base --is-ancestor 3291213973 origin/maingit grep -n "export function rolledBackRefusal" origin/main -- packages/objectql/src/summary-errors.ts32912139. A child'sERR_SUMMARY_RECOMPUTEcan stop a cascade delete, or roll back an ownedtransaction(). In both cases the caller now gets an ordinary error saying the write did not land, with the refusal kept ascause: 500 at the data door, 400 at the action door, and every row still stored. Writes that really landed keep the refusal unchanged.6579832522and theCONTRACT_REVIEW_TIERrecord6579773378(narrowing face, PASS on2af3815d) are on the PR.Clause-②: no (narrowing), full dogfood VERDICT 0.Fixes #29291closed this card on merge.inAfterAbortUnitfinding is 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.pm:dispatchedand the assignee removed in the same round.