Repository navigation
fix(rest): the batch door reaches the engine slot through wiredEngineOrLoud, so a wired-and-failing engine answers 503 on every wiring - #18805
Merged
os-support-ai merged 2 commits intoSep 17, 2026
Conversation
…eOrLoud`, so a wired-and-failing engine answers 503 on every wiring (#18559) `objectQLProvider`'s third consumer, `POST /api/v1/batch`, read the field directly, so a rejection escaped the read, missed the adjacent 501 arm and landed in the handler's generic outer catch as 500 INTERNAL_ERROR. Its two sibling consumers answer 503 SERVICE_UNAVAILABLE for the identical fact. Measured at the door on a real RestServer over a real ObjectKernel: the 500 was reachable only on the MULTI-KERNEL wiring. On the single-kernel composition the open core boots, `computeExecCtx` resolves the engine through its own `wiredEngineOrLoud` branch and raises first, so this door already answered 503. The repair removes a wiring-dependent divergence rather than choosing a new wire answer. Both ABSENCE shapes still answer 501 NOT_IMPLEMENTED on both wirings, and the fault message is still withheld; only the status and the machine code move. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3
…tch-door-wired-failing-engine
Contributor
📓 Docs Drift Check
What this run could not see
Coarse fallback — 15 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
os-support-ai
marked this pull request as ready for review
September 17, 2026 22:49
os-support-ai
enabled auto-merge
September 17, 2026 22:50
os-support-ai
deleted the
claude/issue-18559-batch-door-wired-failing-engine
branch
September 17, 2026 23:17
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #18559
Clause-②: no
objectQLProvider's THIRD consumer —POST /api/v1/batch— now reaches the data-engine seam throughwiredEngineOrLoud, the same helper its two sibling consumers already use. A wired-and-failing engine answers503 SERVICE_UNAVAILABLEinstead of the500 INTERNAL_ERRORthe handler's generic outercatchused to produce.The card's reading REPRODUCES — and narrows
Driven at the real door on a real
RestServerover a realObjectKernel(⛔ not inferred from the card's prose, ⛔ not from a unit double), the 500 is there. But it is reachable on ONE of the two wirings only, and the card's 503/503/500 table silently mixes wirings:SERVICE_UNAVAILABLEkernelManageris wiredINTERNAL_ERRORSERVICE_UNAVAILABLEOn the single-kernel wiring
computeExecCtxresolves the engine through its OWNwiredEngineOrLoudbranch and raises before the batch handler's engine line runs, so this door already answered 503. The 500 was reachable only where that gate's KERNEL branch absorbs by design (wiredEngineOrLoud's RESIDUE note) and hands the engine question down. This is the same narrowing PR #17077 had to make for the sibling/meta/object/:name/state/:fielddoor, and for the same structural reason.⇒ the repair does not choose a new wire answer for this door. It removes a wiring-dependent divergence, leaving the answer this door already gave on the composition the open core boots. §2b of the census test measures both wirings side by side so that sentence is a reading rather than an argument.
Why this is NOT a re-collapse, and NOT a regression
The two facts always differed on the wire (500 against the 501 an absent engine gets), so the decidable test #14251 tightened — "NO consumer re-collapses a rejection into the
undefinedpath" — was already satisfied at this consumer. What was wrong is that they differed through a catch-all that knows nothing about this seam. The adjacent501 NOT_IMPLEMENTEDarm tests!ql || typeof ql.transaction !== 'function', which a rejection never reaches.The pin: fails before, passes after — both readings
packages/rest/src/objectql-slot-consumer-census.test.tsrecorded the 500 explicitly as "RECORDED here, not ruled". That is the pin that moved, and the ablation is shown in the PR thread: with the test file at this branch andpackages/rest/src/rest-server.tsreverted to the merge base, the new assertions FAIL; restored, they pass. ⛔ No test was skipped, disabled or quarantined.Three things are pinned UNCHANGED, because they are what says no accept set moved:
501 NOT_IMPLEMENTED, on BOTH wirings — no provider wired at all, and a provider that RESOLVESundefined(the seam contract declaring absence rather than failing);Internal server error,INTERNAL_ERROR_MESSAGE, asserted plus a negative assertion that the driver's own sentence never reaches the wire;asyncprovider that throws SYNCHRONOUSLY reaches the same answer as one that rejects ([finding] at a RestServer provider seam a SYNCHRONOUS throw loses the whole execution context while an async rejection is absorbed — same fault, two different wire answers #13280).Controls, including the one that came back dark
emailServiceProvider+wiredEngineOrLoudand still FIRE (1) foremailServiceProvider+seamOrUndefined, the helper that slot really uses. Without the second half "3 of 3" is the only sentence the instrument can produce.resolveProtocol/loadObjectItems, which the harness's auth-only kernel cannot serve — measured, a HEALTHY engine answers 500 INTERNAL_ERROR there too. A 500 read on that wiring with a real op is therefore ambiguous between "the seam answered" and "the harness ran out of kernel". The healthy control uses{ operations: [] }, which returns inside the door after the engine line and before the protocol is touched, so the engine fact is the only variable. §2b adds the control the multi-kernel harness cannot give: on the single-kernel wiring a healthy engine SERVES the same real operation with 200.The
/actionsprecedent does NOT transfer — measured, not assumedPR #18760 (card #18540) was handed over as the precedent for this family. It is a different defect on a different axis, in a different package:
/api/v1/actionsships its native error message verbatim, where the/datadoor sanitises the identical crash #18540 is a MESSAGE leak: "the status is already right here; what leaks is the sentence." Its remedy withheld prose. Here the message is ALREADY withheld —INTERNAL_ERROR_MESSAGEis on the wire today — and what moves is the STATUS.classifyDataError,looksLikeInternalErrorLeak,errorFromThrown,UNCLASSIFIED_FAULT,deps.error) lives inpackages/runtime/src/domains/actions.ts. None of it is on this door's path:POST /api/v1/batchispackages/rest/src/rest-server.tsreaching the wire throughhandleRouteError→resolveErrorResponse.errorFromThrownbranch that must survive (an error declaring its own HTTP status is served with it) is untouched here: nothing in this diff is in that file, and the brandedAuthzStoreUnavailableErrorthis repair raises IS an error declaring its own status — it is served with it, which is exactly how the 503 reaches the wire.The precedent that DOES transfer is #15405 / PR #17077 — same slot, same file, same helper, the sibling consumer — which the card itself names as the pattern to copy.
Clause ② — declared
no, from this diffClause-②: noabove is derived from the delivered diff, not inherited:SERVICE_UNAVAILABLEis an existingStandardErrorCode, already inpackages/spec/src/api/error-code-ledger.zod.tsmapped to 503, and already emitted by the sibling door for this same fact.AGENTS.mdand the PM protocol are explicit that a NEW code is alwaysyes; this is not one.{ error, code }shape the 500 wore.wiredEngineOrLoud's own table andAUTHZ_STORE_UNAVAILABLE_STATUS = 503. The PM protocol states that pulling back to an already-declared contract does not touch clause ②: 「条款②只指已发布契约面,拉回已声明契约不触它」.(widening)nor(narrowing)is truthful here: no accept set widens, and nothing that was accepted is now rejected.no (widening)is malformed by construction, and a fabricated(narrowing)would carry a false BREAKING declaration against apatchchangeset.This is the same declaration PR #17077 landed and its acceptance confirmed for the identical move at the sibling consumer (404 → 503 on a published route), on the same three grounds.
minorwith a BREAKING banner and an ADR-0087 disposition. Measured againstAGENTS.md's own arms, that spelling has no truthful form here:minor+ BREAKING is what a declared(narrowing)owes, and this diff narrows nothing. The changeset ispatchperAGENTS.md: "A bug fix in a released package takes apatchchangeset — never none, and ⛔ neverskip-changeset".@objectstack/restpublishes (files: ["dist", "README.md", "CHANGELOG.md"], not private), and the changed door is in the published bundle — soskip-changesetis refused on a measurement, not on the file's look.Stated plainly because it is the half a reviewer should decide, and this PR is a draft for exactly that reason.
The card reserves a per-consumer wire ruling: "A public door changing its wire answer is the per-consumer wire ruling #14251 reserves … and the ruling half is nobody's to assume." For the sibling card #15405 that ruling was obtained explicitly — it sat at
pm:awaiting-maintaineruntil the maintainer's batch move, quoted verbatim on that thread with the criterion 「恢复一条既有修复的不变量,属具名不升级类,⛔ 无产品语义分叉」. No equivalent maintainer act exists on #18559: triage moved it straight topm:queue.What this delivery offers in its place is a reading the card's filer did not have, and it is why the work was written rather than handed back: this door already answers 503 for this fact on the composition the open core boots. That reframes the remedy from "pick a wire answer for a public door" to "remove a divergence between two compositions of one server". If a reviewer weighs it the other way, the revert is one commit and the PR is a draft.
⛔ Not widened into the undeclared-5xx band at
/metaor/mcp— that is #5667's recorded decision with #12281 as the card for the declared limb, and reversing a recorded decision is a decision, not an execution.Acceptance notes
500 INTERNAL_ERRORwhen the resolved kernel carries no metadata protocol, becauseresolveProtocol/loadObjectItemsfault under the generic outer catch. Observed only in this file's harness (anObjectKernelwithauthand nothing else), which is not a supported deployment shape — a real multi-kernel host registers the protocol. ⛔ Not a reproducible defect against a supported wiring, so it is not one of the three filing classes; recorded because it is what makes a naive 500-based control on that wiring ambiguous, and the next reader of this harness will meet it. Successor who would touch this file: whoever extendsobjectql-slot-consumer-census.test.tsfor a FOURTH consumer.this.resolveExecCtx(environmentId, req)without the.catch(rethrowAuthzStoreUnavailable)that many sibling call sites inrest-server.tscarry. Measured, it needs none:computeExecCtxre-raises the branded outage from its owncatchand this handler's outercatchserves it with its declared status — which is how the single-kernel 503 above reaches the wire. Observation, not a defect, and ⛔ not touched here.Generated by Claude Code