rest: a write refused by an unevaluable hook condition answers a generic 500 INTERNAL_ERROR instead of a coded refusal (#24619 row b) #24662
Description
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_inquiryatbeforeInsertand the conditionrecord.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)
HookConditionErroris declared atpackages/objectql/src/hook-wrappers.ts:200and exported fromcore.ts:83. Nothing underpackages/rest/srcnames it, so it reaches the generic 500.- ⛔ The wire code is a ledger decision, not a property.
hook-wrappers.ts:187–199: ADR-0112 makeserror.codea CLOSED vocabulary (StandardErrorCode∪ERROR_CODE_LEDGER, inpackages/spec/src/api/). A.codeadded on the class would mint an unregistered wire code by side effect. So the fix is one of two:- map it to an existing
StandardErrorCodeinpackages/rest's error mapping (this lane); - or register a ledger code, which is a
packages/specrow (domain:spec). It goes serial or through the cross-domain path, with the file surface declared in the claim.
- map it to an existing
- No
HOOK_*code inpackages/spec/src/apinames 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
objectstack-fleet commentedon Oct 19, 2026
Claim: PM loop round 1 ·
domain:cliseat 2 · p2 · 2026-10-19T23:14ZSession:
session_01LmjU2x1faLZ39DVTFyVca6Account:
os-justin(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24662-hook-condition-coded-refusalWorktree:
objectstack-issue-24662Domain:
domain:cliSeat:
domain:cli#2(seat post #22648)File surface (read on
origin/main3a931fca1; line numbers move; stop on a breach and explain it in the report):HookConditionError(packages/objectql/src/hook-wrappers.ts:200, exported fromcore.ts:83) into a response. It now lives inpackages/types/src/data-error-classification.ts(mapDataError), andpackages/rest/src/error-response.tsre-exports it and resolves and emits from it. Today nothing names the class, so it reaches the generic500 INTERNAL_ERRORand the[REST] Unhandled errorline.error.codein the ADR-0112 envelope, the row is still not written, and no[REST] Unhandled errorline is logged. A sibling control covers a hook that refuses on purpose, whose answer is unchanged. Each fixed door gets an ablation. Changesets:patchon each package whose shipped code moves.packages/spec/**, which includes registering a new ledger code. If no existingStandardErrorCodefits, stop and report it with options.packages/objectql/**: the class itself, its fields and its message. PR fix(objectql): a runAs:'user' hook carries its user's email, so a current_user.email row policy admits it as it admits the user's own request #24674 has an open hunk inhook-wrappers.ts.Container & model:
M(a judgment on the code choice, two transports),mode:subagent,model: default tier(dispatch-gates.mjs --tier: "no path-derived mandate")Clause-②: no
Basis for the Clause-② line: the write is refused before and after. Only the refusal's status and code move, onto a member of the published
StandardErrorCodecatalog. No accept set moves, and no code is added. A new ledger code would make thisyes, and it is out of this dispatch.Responsibility: this lane's error classification (
packages/types,packages/rest) answers an authored hook's unevaluable condition as an unhandled server fault. The platform path that covers it is fail-closed: the row is not written. It reaches every API caller whose write meets such a condition, and an AI client cannot tell that answer from a crash.Thread-read: none
Serial constraints cleared:
data-error-classification.tsorerror-response.ts, and nopm:dispatchedclaim names them. Read in this act. PR perf(rest): GET /data/OBJECT no longer computes metadata diagnostics; the exposure gate reads getMetaItemForExecution #24666 (rest: every GET /api/v1/data/OBJECT still runs a full zod safeParse of the object's definition (the API-exposure gate's by-name read is decorated with diagnostics) — 11.8% of a HotCRM read's CPU #24535) editsrest-server.ts, which is another file.hook-wrappers.ts, which is held off here.dependenciescan be set only at create —PATCH /api/v1/packages/:idpersists only name / description / version, so a save refused with "declare a dependency" has no remedy short of recreating the package #24670 and Flaky on Test Core (6/6):packages/runtimeshare-links-refusal-order.test.ts› dispatcher twin › "[#23372] email audience: the refusal is the one an unknown token and a password-less email link answer" times out at vitest's 5000 ms default #24633, are disjoint.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
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
objectstack-fleet commentedon Oct 20, 2026
ACCEPT → PR #24714 comment
6281218192·domain:cliseat 2 · sessionsession_01LmjU2x1faLZ39DVTFyVca6· 2026-10-20T01:32Z · head910352e21. An unevaluable hook condition now answers400 VALIDATION_ERRORat 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
objectstack-fleet commentedon Oct 20, 2026
Landed: PR #24714 →
014863d5d(Fixes #24662)domain:cliseat 2 ·os-justin· sessionsession_01LmjU2x1faLZ39DVTFyVca6· 2026-10-20T03:26Zadded_to_merge_queue02:54Z)origin/main014863d5dis on it, with a single parent (squash). 11 files, +665 / −15, matching the ACCEPTed head910352e21gh-readonly-queue/main/*pr-24714*head remainscompletedby the PR'sFixes. In the stroke after this comment,pm:dispatchedcomes off6279442235→ report6281181277→ ACCEPT6281218192→ pointer6281225759.400 VALIDATION_ERRORat every data door (REST single-record and bulk, the dispatcher's/data,/batchrows), where it answered500 INTERNAL_ERROR. The message names the hook and the fault, never the condition source or a record value. The row is still not written,uncompilablekeeps its sanitised500, and no error code is added.Generated by Claude Code