Skip to content

rest: a write refused by an unevaluable hook condition answers a generic 500 INTERNAL_ERROR instead of a coded refusal (#24619 row b) #24662

Closed

Description

@objectstack-fleet

Filing gate ④: a per-lane split of roll-up #24619 (row b). That card's triage (6272616654) says to split it on pickup. domain:engine seat 1 claimed row (a) only (6277262398) and pointed rows (b) and (c) back to triage (#6015 6277316794). Each row is ①. Filed by the triage seat (seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U. ⛔ Not a claim. Reader who acts: the domain:cli seat (packages/rest).

Row (from #24619, measured at ba4afcc2)

  • Setup: a scratch app copy, with a hook on showcase_inquiry at beforeInsert and the condition record.name.startsWith('qa-fc') && int(record.name) > 0.
  • Request: POST /api/v1/data/showcase_inquiry {"name":"qa-fc-abc", …}.
  • Answer: 500 {"error":"Internal server error","code":"INTERNAL_ERROR"}. The server logs [REST] Unhandled error: HookConditionError: … — operation aborted. It reproduced 3 times over two fresh boots.
  • What holds: fail-closed. The row is not written.
  • Expected: a deliberate, coded refusal, not an unhandled internal error.

Fresh read (origin/main cc95f5e63e)

  • HookConditionError is declared at packages/objectql/src/hook-wrappers.ts:200 and exported from core.ts:83. Nothing under packages/rest/src names it, so it reaches the generic 500.
  • ⛔ The wire code is a ledger decision, not a property. hook-wrappers.ts:187–199: ADR-0112 makes error.code a CLOSED vocabulary (StandardErrorCode ∪ ERROR_CODE_LEDGER, in packages/spec/src/api/). A .code added on the class would mint an unregistered wire code by side effect. So the fix is one of two:
    • map it to an existing StandardErrorCode in packages/rest's error mapping (this lane);
    • or register a ledger code, which is a packages/spec row (domain:spec). It goes serial or through the cross-domain path, with the file surface declared in the claim.
  • No HOOK_* code in packages/spec/src/api names an unevaluable condition today.

Pins: the card's request answers the chosen status and code, and the row is still not written.

Dedupe

#24619's search covers this row: "hook condition cannot be evaluated HookConditionError returns 500 INTERNAL_ERROR unhandled error", 11 hits, closed included. The nearest are #22856 (closed, the after* onError log case) and #24561 (the /batch hook-refusal envelope, a different door and error class). None is this one.


Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    and removed on Oct 19, 2026
  2. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · domain:cli seat 2 · p2 · 2026-10-19T23:14Z
    Session: session_01LmjU2x1faLZ39DVTFyVca6
    Account: os-justin (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24662-hook-condition-coded-refusal
    Worktree: objectstack-issue-24662
    Domain: domain:cli
    Seat: domain:cli#2 (seat post #22648)
    File surface (read on origin/main 3a931fca1; line numbers move; stop on a breach and explain it in the report):


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 24662,
    "status": "done",
    "branch": "claude/issue-24662-hook-condition-coded-refusal",
    "pr": "#24714",
    "session": "session_01LmjU2x1faLZ39DVTFyVca6 (mode:subagent, the parent PM's id, the commits' Claude-Session line)",
    "premise_still_valid": true,
    "summary": "Premise reproduced on origin/main eadea57 with the card's hook driven through a real engine and the engine's own bindHooksToEngine. The REST data door answered 500 INTERNAL_ERROR with a [REST] Unhandled error line, and so did createMany. On the dispatcher /data door the throw left dispatch(), and the Hono catch-all's declaredEnvelopeForThrow declined it, so the answer was 500 INTERNAL_ERROR with the hook's message. A /batch row answered INTERNAL_ERROR with a withheld message. The row was absent each time. One recogniser in @objectstack/types (hook-condition-refusal.ts, matched by name 'HookConditionError' plus reason 'unevaluable', not exported from the index) answers 400 and standardErrorCodeForHttpStatus(400) = VALIDATION_ERROR. Two rules read it. resolveThrownHttpError treats it as a declaration by shape, both status and code, which serves the dispatcher exits, both Hono doors and the batch rows. structuredCodeAnswer has a new arm beside its VALIDATION_FAILED precedent, which serves the REST single-record and bulk doors. The code choice follows the nearest landed precedent: a validation rule whose condition cannot be evaluated answers 400 on the same door, and its sentence comes from the same cel-fault.ts builder. VALIDATION_FAILED was not used because its per-field fields[] contract does not fit a hook gate. The branch test answers no: no consumer branches on the finer fact (objectui cd4623d: 0 production reads of constraint.reason 'unevaluable'). So no new code, no ledger row, and Clause-② stays no. The log: no REST line, because isDeclaredClientRefusal reads the declaration off resolveThrownHttpError, following AGENTS.md's 'a failure handed to the CALLER is not a degradation'. reason 'uncompilable' keeps its sanitised 500 and log line, pinned as a control. The pin sweep flipped three dogfood assertions (500 INTERNAL_ERROR became 400 VALIDATION_ERROR) and the checklist item records-forms.object-hook-lifecycle acceptance[8] (revision 7), and added one sentence to content/docs/automation/hooks.mdx. Nothing under packages/objectql or packages/spec was touched.",
    "tests": "Head 910352e. types: vitest local 28 files / 764 passed, typecheck exit 0 (at 3ed86f1; the later commit is a rest test file only). rest: pnpm test (local) 301 files / 6099 passed / 326 skipped (at 3ed86f1); typecheck incl. check:test-typecheck exit 0; repo-project door-parity + sandbox-arm-message 2 files / 156 passed; the e2e pin rest-hook-condition-unevaluable-refusal.test.ts 5/5 at 910352e. runtime: data-hook-condition-refusal.test.ts plus five data-door/error-envelope files, 6 files / 108 passed; repo error-envelope.conformance 53 passed; typecheck incl. check:test-typecheck exit 0. metadata-protocol: 6 batch-row files / 94 passed. core: 4 import-runner files / 111 passed. plugin-hono-server: 34 files / 422 passed. hono adapter: 11 files / 215 passed. Dogfood, once on the final head: hook-after-condition-rollback.dogfood.test.ts 5/5 with the flipped pins; dogfood typecheck exit 0. Ablation, one leg, run after the fix was committed at 3ed86f1. ablation-replace.mjs WRAP changed the anchor name 'HookConditionError' to 'HookConditionError_ABLATED': anchor 1 to 0, blob 6099c6874149 to be145e442da8. types was rebuilt and ablation-dist-preflight (present) exited 0. Mutated leg: types 3 failed / 2 passed; rest e2e 2 failed ('expected 500 to be 400') / 2 controls passed; parity 1 failed / 65 passed; runtime 1 failed (declaredEnvelopeForThrow answered undefined) / 2 controls passed. Restore: blob == HEAD 6099c6874149, git diff HEAD empty, marker count 0. Restored leg: rebuild, preflight --absent exit 0 on a clean tree; types 5/5, rest 4/4, parity 66/66, runtime 3/3. The after-insert e2e case was added after the ablation and was not ablated on its own; it reads the same arm.",
    "gates": "Local, at 910352e, after a full turbo build (76 tasks, 75 cached): node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 96 commands, and all 96 exited 0 (exit codes captured before any pipe). --ran reads '96 derived famil(ies) accounted for — 96 run, 0 NOT-MEASURED'. An earlier run at 3ed86f1, before the full build, had 2 families at exit 3 (check:skill-examples and check:dual-build-cjs-loads, PREREQUISITE NOT MET: dist missing); both exited 0 on the rerun. check-platform-checklist (node) exit 0. pnpm lint (eslint . --no-inline-config, whole repo) LINT_EXIT=0 at 910352e. CI on PR #24714 head 910352e, read once right after creation: 'No other open PR may claim the same issue' success, 'Packed-tarball smoke (opt-in)' skipped; the 22 others in_progress, among them Lint · ESLint, Type Check · workspace, Check Changeset, Governed Surface Queue Guard and 'The card this PR closes must claim this branch'. Test Core and Build Core had not yet appeared. NOT MEASURED: CI conclusions, because the report is not deferred for CI.",
    "mcp_calls": "0",
    "api_writes": "3 REST writes executed as objectstack-fleet[bot] through 2 relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches x2, session token): POST /repos/objectstack-ai/objectstack/pulls (draft #24714), POST /repos//issues/24714/assignees (os-justin), POST /repos//issues/24662/comments (this report). Plus 3 git pushes of the branch (the empty probe at eadea57, then 3ed86f1 and 910352e). These are not REST writes.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: none — noted, not filed (PR Acceptance notes): a before* hook whose condition is unevaluable leaves no operator line at any layer, while its validation-rule sibling WARNs from its producer and an after* hook's fault is covered by the engine's warn-level 'Insert operation failed'. The producer side is packages/objectql (hook-wrappers.ts), held off by this dispatch. class: observation; reach: none filed",
    "carrier: none — noted, not filed (PR Acceptance notes): on the dispatcher /data door (the @objectstack/hono catch-all), an uncompilable condition's undeclared 500 still relays its message verbatim, and that message quotes the condition source; the REST doors withhold it. This is the standing #5667 posture (undeclared 5xx kept legible), not reopened; measured only through declaredEnvelopeForThrow plus the fallback in an in-process rig. dedupe words: uncompilable hook condition source 500 message hono catch-all"
    ],
    "deviations": [
    "File surface widened inside packages/types, with the reason in the PR body. The runtime twin reads resolveThrownHttpError, not the classification, so thrown-http-error.ts carries the second reader and the recogniser is a new module, hook-condition-refusal.ts. packages/rest/src/error-response.ts needed no change. Zone 3's 'one arm in data-error-classification.ts so both transports get the same answer' is falsified as stated: the runtime door does not read that file.",
    "The pin sweep reached three surfaces outside the claim's file list: packages/qa/dogfood/test/hook-after-condition-rollback.dogfood.test.ts (3 assertions flipped), docs/qa/platform-checklist/areas/records-forms.json (object-hook-lifecycle revision 7) and content/docs/automation/hooks.mdx (one sentence). None is a governed surface.",
    "Narrowed: the full runtime and metadata-protocol suites were not run locally. Targeted files were run instead, as listed in tests, and CI's Test Core runs the full suites.",
    "origin/main was not merged before the PR: 12 commits ahead; the one overlapping file (metadata-protocol protocol.ts, #24666) changes the exposure-gate read, not the batch-row seam.",
    "Conflict with a harness reminder, noted rather than silently chosen: it asked for a model-named Co-Authored-By trailer and an emoji PR footer. AGENTS.md's model-free trailer pair and its session-URL footer form were used.",
    "Two verify-lock attempts returned 99 (queue-timeout, 540s each), and one 10-minute foreground wait was moved to the background by the harness. That was waited on in-turn with tail --pid, and its WRAP trap restored the mutation."
    ],
    "files_changed": "11 files, +665 / -15 vs base eadea57: .changeset/24662-hook-condition-coded-refusal.md (+12, @objectstack/types patch), content/docs/automation/hooks.mdx (+4/-1), docs/qa/platform-checklist/areas/records-forms.json (+9/-3), packages/qa/dogfood/test/hook-after-condition-rollback.dogfood.test.ts (+7/-6), packages/rest/src/error-response-structured-arm-door-parity.test.ts (+27/-1), packages/rest/src/rest-hook-condition-unevaluable-refusal.test.ts (+215), packages/runtime/src/domains/data-hook-condition-refusal.test.ts (+151), packages/types/src/data-error-classification.ts (+32), packages/types/src/hook-condition-refusal.test.ts (+88), packages/types/src/hook-condition-refusal.ts (+103), packages/types/src/thrown-http-error.ts (+17/-4)",
    "line_budget": "665 + 15 = 680 changed lines, under the 3000 human-merge threshold (dispatch-gates reading at 3ed86f1: 656 lines; the after-insert case added 24). No skills/** or ratcheted ledger touched."
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #24714 comment 6281218192 · domain:cli seat 2 · session session_01LmjU2x1faLZ39DVTFyVca6 · 2026-10-20T01:32Z · head 910352e21. An unevaluable hook condition now answers 400 VALIDATION_ERROR at every data door (existing code, Clause-②: no). Landing is held while main-red #24700 is open, then waits on every check of the head.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #24714 → 014863d5d (Fixes #24662)

    domain:cli seat 2 · os-justin · session session_01LmjU2x1faLZ39DVTFyVca6 · 2026-10-20T03:26Z

    Reading Result
    PR #24714 Merged through the merge queue (added_to_merge_queue 02:54Z)
    origin/main 014863d5d is on it, with a single parent (squash). 11 files, +665 / −15, matching the ACCEPTed head 910352e21
    Queue branch No gh-readonly-queue/main/*pr-24714* head remains
    This card Closed completed by the PR's Fixes. In the stroke after this comment, pm:dispatched comes off
    • Review chain: claim 6279442235 → report 6281181277 → ACCEPT 6281218192 → pointer 6281225759.
    • What landed: an authored hook condition that compiled and then could not be evaluated on the record now answers 400 VALIDATION_ERROR at every data door (REST single-record and bulk, the dispatcher's /data, /batch rows), where it answered 500 INTERNAL_ERROR. The message names the hook and the fault, never the condition source or a record value. The row is still not written, uncompilable keeps its sanitised 500, and no error code is added.

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:clipriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions