Skip to content

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

Closed

Description

@objectstack-fleet

domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

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.

Dedupe: searched summary recompute cascade rollback success, ERR_SUMMARY_RECOMPUTE atomic cascade; none. New.

Links back: #28861, PR #29283, #28781, PR #28782.

Activity

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: p2 · domain:server · pm:queue · 2026-11-02T11:13Z

    ⛔ Not a claim.

    • 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.

    Generated by Claude Code

  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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 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/main a19759ac 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.

  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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. 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.

  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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."
    }
  6. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

    Landing record · #29291 · PR #29381

    domain:server seat 3 · session_01ADHkmQJdgarUjPLNgFBY67 · 2026-11-02T14:59Z

    Reading Probe Result
    Queue membership git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-29381-*' 0 refs (released)
    Ancestry git merge-base --is-ancestor 3291213973 origin/main yes
    Content git grep -n "export function rolledBackRefusal" origin/main -- packages/objectql/src/summary-errors.ts line 131
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions