Repository navigation
fix(plugin-hono-server): a Hono door's 409 CONCURRENT_UPDATE carries currentRecord, the caller's own read of the row (#23531) - #23667
Conversation
…currentRecord, the caller's own read of the row The protocol now re-reads a conflicting row under the caller's context before it throws ConcurrentUpdateError, so the record holds only what the caller may read. declaredArmContext stops holding currentRecord back, and both Hono doors forward it in error.details as RestServer forwards it. The absence pins flip to presence; the composed-host pin shows a field the caller's read masks staying off the record. The docs pages that said a Hono door never sends currentRecord are corrected. Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
…stead of casting them check:query-options-erasure counts an `as any` on engine query options in test code; the two reads the #23531 masked-field row takes need no cast. Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
…ft-current-record-hold Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 42c6507374ae802063d32b5d0de6e36d9736e049 && git checkout 42c6507374ae802063d32b5d0de6e36d9736e049
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1ced0f82bd93260b660e6f448c7dc5952bc82371 4bc9b0c08be1b3398bd5b3d73136b17f5e8b5a49 && git checkout -B drift-repro 1ced0f82bd93260b660e6f448c7dc5952bc82371 && git merge --no-ff 4bc9b0c08be1b3398bd5b3d73136b17f5e8b5a49
node scripts/docs-audit/affected-docs.mjs --json 1ced0f82bd93260b660e6f448c7dc5952bc82371
|
席位核对✅ ACCEPT: PR #23667 at
Checklist, read against GitHub and
Accepted deviations:
Out-of-scope, Acceptance notes, 「承接者:无」:
Landing: Generated by Claude Code |
Fixes #23531
Clause-②: no
This lands the remainder of #23531: the
currentRecordhalf that PR #23582 held back. The hold waited on #23586, whose answer landed as PR #23628 (7dc7d2213): the protocol now reads that row as the caller.What this does
A stale
If-Match(orexpectedVersion) onPATCHorDELETE /data/:object/:idis refused by the protocol withConcurrentUpdateError(409 CONCURRENT_UPDATE). Both Hono doors render it throughdeclaredEnvelopeForThrow. They are@objectstack/hono'screateHonoAppand a route mounted onHonoHttpServer. That function reads the structured fields offRestServer's classification (structuredCodeAnswer) indeclaredArmContext. That helper droppedcurrentRecordby name, because the record used to be the row the existence probe (probeRecord) reads with a system context.Since PR #23628, on a version mismatch
metadata-protocolre-reads the row under the caller's own context. A row the caller's row-level rules hide answers404 RECORD_NOT_FOUNDwith neither field. A readable row'scurrentRecordis the caller's field-masked read. So the hold is lifted:declaredArmContext(packages/plugins/plugin-hono-server/src/adapter.ts) now drops only the arm's ownerrorandcode.currentRecordrideserror.detailsbesidecurrentVersion, asRestServerforwards it flat.The docblock lines that said the field is held are rewritten. The field is forwarded because the producer reads it as the caller. An arm that grows a field read with a system context must still be held there by name until its producer reads it as the caller.
FROM
{"success":false,"error":{"code":"CONCURRENT_UPDATE","message":"…","details":{"currentVersion":"…"}}}TO
{"success":false,"error":{"code":"CONCURRENT_UPDATE","message":"…","details":{"currentVersion":"…","currentRecord":{…}}}}No other arm's behaviour changes.
metadata-protocol,packages/typesandpackages/adapters/hono/src/index.tsare not edited. Changeset:@objectstack/plugin-hono-serverand@objectstack/hono,patch, as PR #23582 did (one fixed version group, andcreateHonoApp's answer is the measured door).Pins
packages/plugins/plugin-hono-server/src/handler-throw-declared-envelope.test.ts(direct-mount route): the[#23531]rows flip from absence to presence.error.detailsmust equal{ currentVersion, currentRecord }exactly, as the producer stated them, so a field the door drops or invents turns them red. The controls are kept: the stored version is accepted, and a conflict stating no current state keeps the two-key envelope.packages/qa/http-conformance/src/hono-declared-envelope.conformance.test.ts(a realcreateHonoAppover the real dispatcher and protocol, on SQLite WASM):PATCHandDELETEwith a staleIf-Match:error.detailsequals thecurrentVersionandcurrentRecordthe real protocol threw, read off the protocol call, and the record names the stored row. ThecurrentVersionecho pins are kept: echoed as the nextIf-Matchit is accepted (PATCHwrites,DELETEdeletes).PATCHandDELETE: a row whosesecretfield the caller's read masks.currentRecordcarries the row'sidandnameand nosecret, and no secret text is on the wire. The record equals both the caller's own read and the record the protocol threw, while the store still holds the field.next(). That is the seam@objectstack/plugin-security's field-level rules rewrite a read at. The security plugin itself is not booted there, becausehttp-conformancedoes not depend on it, and adding it meanspackage.jsonand the lockfile, outside this claim. The real field- and row-level rules are pinned bypackages/runtime/src/domains/data-conflict-caller-visibility.test.ts(PR fix(metadata-protocol): an optimistic-concurrency conflict reports only what the caller may read #23628), and are measured on this head below.Verification (at
4bc9b0c08b)The branch merged
origin/mainat286eb9ef3(4 commits, none touching these files) before the PR opened. Every reading below is on4bc9b0c08bunless it names another head.turbo run buildof the workspace (75/75):pnpm --filter @objectstack/plugin-hono-server typecheck(tsc,tsconfig.typecheck.json,check:test-typecheckOK with 0 debt) andtest: Test Files 29 passed, Tests 354 passed.pnpm --filter @objectstack/http-conformance typecheck(check:test-typecheckOK, ledger unchanged) andvitest run: Test Files 13 passed, Tests 144 passed.packages/runtime, which resolves@objectstack/plugin-hono-serverthroughdist/:data-conflict-caller-visibility,data-update-expected-versionanddata-delete-expected-versiongave 3 files, 115 passed.7a8e9bf386):node scripts/ablation-replace.mjsin wrap mode, plus an outertraprestore on EXIT INT TERM over the absolute path. The exclusion was put back: anchor x1 to x0, replacement carrying the markerABLATION_23531_RESTORE_HOLDx0 to x1, blob4b7ca0f62478to3e3313e4060c.plugin-hono-serverwas rebuilt, andablation-dist-preflightfound the marker indist/index.jsanddist/index.mjs. The direction was as predicted:expected { Object (currentVersion) } to deeply equal { …(2) };expected undefined to match object).git diff HEADis empty. The rebuild was 6/6 turbo cache hits. Preflight--absentfound the marker gone from all 6 built files, with a clean tree. Both files were green again (37/37, 20/20).7a8e9bf386, a scratch test inpackages/runtime, deleted after the run and never committed): the same stalePATCHandDELETEran on the FLS-masked row and on the plain row. The Hono exit'serror.detailsequalsRestServer's{ currentVersion, currentRecord }, and neither wire carries the masked value: 4/4. The committeddata-conflict-caller-visibility.test.tswas green beside it (16/16).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(7 paths vs merge base286eb9ef3) derived 95 commands. All 95 ran, each exit captured before any pipe: 95 x exit 0.--ranreports 95 derived, 95 run, 0 NOT-MEASURED and 0 UNRUN.7a8e9bf386caughtcheck:query-options-erasure: twoas anyonfindOneoptions in the new pin took the test surface from 236 to 238. Fixed inbc83077a4f, back to 236.eslint --no-inline-configon the 3 changed TS sources exited 0, with 0 errors and 0 warnings each.--print-config.eslint.config.mjsis unchanged from base toorigin/main..mdxpages fall outside ESLint's config (File ignored), as they do underpnpm lint.pnpm lintis CI's (Lint · ESLint).Acceptance notes
currentRecord. PR fix(plugin-hono-server): a Hono door's 409 DELETE_RESTRICTED, and every structured refusal, carries RestServer's arm fields (#23587) #23616 wrote them, and this change makes them false:content/docs/api/index.mdx,content/docs/api/wire-format.mdx("Hono hosts nest the extras", whose example now showscurrentRecord), andcontent/docs/protocol/kernel/error-handling.mdx(the Hono-door table and the "never sends" line). They are corrected here, with one line each saying the record is the row as the caller reads it.currentRecordstays held:.changeset/23531-hono-conflict-body.mdand.changeset/23587-hono-structured-arm-fields.md. They are not edited here (outside this claim). This PR's changeset says it supersedes that note. If one CHANGELOG story is preferred, those two sentences can be cut before the next release. Carrier: the seat.currentRecordoff the thrown error, not the wire, because the wire did not carry it before this change. On this head the wire carries it, and it equalsRestServer's, measured with a scratch test that was not committed (below). Tightening those rows to the wire is a possible later edit inpackages/runtime. Noted, not filed. Carrier: none.not.toHaveProperty('secret')and equality with the caller's read are what would catch it.Generated by Claude Code