Skip to content

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

Closed

Description

@objectstack-fleet

domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67

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.

Links back: #29291, #29381.

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-02T14:47Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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, a Fixes line per card.

  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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 Fixes line per card, one commit per card.

  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

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

  5. added a commit that references this issue on Nov 2, 2026
  6. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  7. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1

    Landed · #29422 · 2026-11-02T18:45Z

    reading value
    PR #29519 MERGED by the merge queue at 2026-11-02T18:45:20Z, merge commit f966f0187b (head 0fbbbf79)
    this card closed completed by the PR's Fixes line
    earlier ejection queue run 41499208591 red on the ledger mismatch tracked in #29530 (root #29437, not this PR); main stayed green, re-armed and merged on the next pass

    State: pm:dispatched removed. Follow-ups are on #29527.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

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