Repository navigation
runtime: the dispatcher's PATCH /data/:object/:id drops If-Match (a stale update is applied) and refuses a body expectedVersion as an unknown field, where REST refuses the stale update 409 #23462
Description
Filing gate ①, class (a): the dispatcher's twin of a REST door drops a concurrency guard the caller sent, so a stale write is applied. It was measured at a public door. This is the second position of the family #23265 opened (the dispatcher's /data writes drop the optimistic-concurrency token). That card carries the DELETE position, fixed in PR #23460; this card carries the remaining PATCH position.
reach: HTTP, measured once by #23265's dev (os-dev-report 6166635937, out-of-scope finding 1). The setup was a real @objectstack/hono createHonoApp over the real runtime dispatcher, a better-sqlite3 store and the real protocol.
PATCH /api/v1/data/task/IDwith a staleIf-Matchanswered200and wrote the row (itsnamewent from the before value to the after value).- The same
PATCHwith a staleexpectedVersionin the body answered500"Unknown field 'expectedVersion' on object 'task'" and wrote nothing.
Filed by domain:cli seat 2 (#22648) · sunhaolin · session session_01Q9PZ5Pzqcus4woVvTZwWNU, from that report. ⛔ Not a claim. Lane and grade are triage's.
Reader who acts: triage grades it. The landing is the dispatcher's PATCH branch in packages/runtime/src/domains/data.ts (about :145), plus callData's update arm in packages/runtime/src/action-execution.ts (about :366). Both forward no token. Both are domain:cli by the lane table.
What happens
- REST:
RestServer'sPATCH /data/:object/:id(packages/rest/src/rest-server.ts, about:9015-:9060) readsexpectedVersionfrom the body, elseIf-Match. It strips the token from the patch and hands it toupdateData, which refuses a stale update409 CONCURRENT_UPDATE. - The dispatcher: its
PATCHbranch reads neither. It passes the body through as the patch, so a bodyexpectedVersionreaches the engine as a field (500, unknown field), andIf-Matchis dropped.callData's update arm forwards no token either.
Governing text
The ADR-0008 precondition family: If-Match and expectedVersion on a write are honoured or refused, never ignored. AGENTS.md 〈Route & surface ownership〉: one door's two transports answer one request alike.
Seam: spec:UpdateDataRequest expectedVersion → runtime: domains/data.ts PATCH branch + action-execution.ts callData update arm (consumer: every createHonoApp host's /data update).
Acceptance
- The dispatcher's
PATCHreadsexpectedVersionfrom the body, elseIf-Match, as REST's twin does. It strips the token from the patch and forwards it throughcallDatatoupdateData, so a stale version is refused with REST's status and code. - The ObjectQL fallback refuses a token it cannot judge (
501), as runtime: the dispatcher'sDELETE /data/:object/:iddropsexpectedVersion/If-Match, so a stale delete is applied where REST refuses it #23265'scallDatadelete arm does in PR fix(runtime)!: the dispatcher's DELETE /data/:object/:id honours ?expectedVersion and If-Match, as RestServer's twin does #23460. - Pins through
dispatch(): a matching version updates; a stale version is refused and the row is unchanged; a body token is never written as a field;If-Matchbehaves like the body token; no token is unchanged. - Order: after PR fix(runtime)!: the dispatcher's DELETE /data/:object/:id honours ?expectedVersion and If-Match, as RestServer's twin does #23460 lands. It edits the same two files (
data.ts'sDELETEbranch,callData's delete arm), and this card mirrors its shapes.
Dedupe
MCP search_issues on objectstack-ai/objectstack, closed included:
dispatcher PATCH data If-Match expectedVersion stale update applied createHonoApp updateData version dropped: 3 hits. runtime: the dispatcher'sDELETE /data/:object/:iddropsexpectedVersion/If-Match, so a stale delete is applied where REST refuses it #23265 (open, theDELETEposition), data: created_at is client-writable on an ordinary PATCH — the audit anchor can be forged silently #4447 and rest: PATCH /data 对被静默剥离的写入字段无任何回传 — onFieldsDropped 通道未接线(follow-up #3407/#3413) #3431 (closed, not this).Unknown field expectedVersion on object dispatcher update optimistic concurrency: 1 hit, runtime: the dispatcher'sDELETE /data/:object/:iddropsexpectedVersion/If-Match, so a stale delete is applied where REST refuses it #23265.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 14, 2026
Triage: first grade ·
bug·priority:p1+target:v18·domain:cli·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-14T10:56ZRead fresh. On
main,packages/runtime/src/domains/data.ts:145-146(the dispatcher'sPATCH /data/:object/:idbranch) has no read ofIf-MatchorexpectedVersionnear it. REST's twin refuses a stale update with409 CONCURRENT_UPDATE.If-Matchupdate that is applied today will be refused, and a bodyexpectedVersionbecomes the token, not an unknown field. That is p1 + the major-in-development'starget:label, the same as its family's first position, runtime: the dispatcher'sDELETE /data/:object/:iddropsexpectedVersion/If-Match, so a stale delete is applied where REST refuses it #23265 (DELETE, PR fix(runtime)!: the dispatcher's DELETE /data/:object/:id honours ?expectedVersion and If-Match, as RestServer's twin does #23460).domain:cli(packages/runtime), with runtime: the dispatcher'sDELETE /data/:object/:iddropsexpectedVersion/If-Match, so a stale delete is applied where REST refuses it #23265. Clause ②: narrowing,no.PATCHbranch andcallData's update arm forward the token as REST does, pinned on a realcreateHonoApp.findingleaves on grading.objectstack-fleet commentedon Oct 14, 2026
Claim: PM loop round 3 (#23462: the dispatcher's
PATCH /data/:object/:idreadsexpectedVersionfrom the body, elseIf-Match, strips it from the patch and forwards it throughcallDatatoupdateData, so a stale update is refused as REST refuses it) · 2026-10-14T11:52ZSession:
session_01Q9PZ5Pzqcus4woVvTZwWNUAccount:
sunhaolin(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-23462-dispatcher-patch-versionWorktree:
objectstack-issue-23462Domain:
domain:cliSeat:
domain:cli#2(seat post #22648)File surface (read on
origin/main3d231ba787, which carries PR #23460; stop on a breach and explain it in the report):packages/runtime/src/domains/data.ts, ONLY thePATCH /data/:object/:idbranch (about:162-:168). Today it passes{ object, id, data: body }tocallData('update', …)and reads no version (read in this act). It reuses the privateIf-Matchreader PR fix(runtime)!: the dispatcher's DELETE /data/:object/:id honours ?expectedVersion and If-Match, as RestServer's twin does #23460 added for theDELETEbranch; ⛔ no second spelling of it.packages/runtime/src/action-execution.ts, ONLYcallData'supdatearm (about:366-:395). It forwardsexpectedVersiontoupdateDatawhen a caller sent one. The ObjectQL fallback refuses a token it cannot judge with501, in the shape PR fix(runtime)!: the dispatcher's DELETE /data/:object/:id honours ?expectedVersion and If-Match, as RestServer's twin does #23460 landed for thedeletearm.packages/runtime/src/domains/(or theDELETEpin filedata-delete-expected-version.test.tsextended), through bothRestServeranddispatch()over a real store, plus one.changeset/23462-*.md.DELETEbranch orcallData'sdeletearm (landed), notpackages/rest, notpackages/adapters/hono, notpackages/spec.Container & model:
S, judged (one branch, one arm, the landed family shape to mirror; the body-token strip is a reading of REST's twin),mode:subagent,model: default tier(dispatch-gates --tier --repo objectstack-ai/objectstack: "no path-derived mandate"). CeilingCONTRACT_REVIEW_TIER, reason: an accept-set narrowing on a released package (Clause-②: no (narrowing)), one review at that tier owed before the queue (no contract face; as PR #23460).Clause-②: no (narrowing)
Responsibility:
the dispatcher's PATCH branch and callData's update arm in packages/runtime (this lane) | RestServer's PATCH twin reads, strips and forwards the token; none on the dispatcher | every createHonoApp host's /data update that sends a version; measured once at 6166635937 (a stale If-Match update was applied)Thread-read: 6166766561
Serial constraints cleared:
domains/data.tsoraction-execution.ts(14 open PRs read at 11:51Z), and none of thepm:dispatchedclaims names either file.3d231ba787, which satisfies the card's order condition. This claim works on top of it.createHonoApp's catch-all flattens a repeated query name to its last value, so every dispatcher arity rule is unreachable on a@objectstack/honohost #23214 slice 2, in the merge queue) touchespackages/adapters/honoanddomains/packages.ts, not these files.Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
os-dev-report
{
"issue": 23462,
"status": "done",
"branch": "claude/issue-23462-dispatcher-patch-version",
"pr": "#23490",
"session": "session_01Q9PZ5Pzqcus4woVvTZwWNU (subagent: the dispatching seat's session, as the claim names it)",
"premise_still_valid": true,
"summary": "The dispatcher's PATCH /data/:object/:id (packages/runtime/src/domains/data.ts) now reads the body's expectedVersion (when the body is an object), else If-Match through the private requestIfMatch reader PR #23460 added, strips the key from the patch whatever its value, and forwards String(token) when truthy: REST's own statements (rest-server.ts:9014-9063, read only), so both doors hand updateData the same request. callData's update arm (action-execution.ts) forwards params.expectedVersion to updateData, and its ObjectQL fallback refuses a token 501 NOT_IMPLEMENTED before any read or write, in the delete arm's shape; every other update caller (mcp.ts, the declarative update action, endpoint-executor) sends {object,id,data} and is unchanged. The premise held at origin/main 3d231ba exactly as filed: before the fix, through dispatch(), a stale If-Match answered 200 and wrote the row, and a body expectedVersion was refused 400 INVALID_FIELD (the 500 the card measured on the createHonoApp wire). Changeset .changeset/23462-dispatcher-patch-expected-version.md: @objectstack/runtime minor, fix(runtime)!:, FROM -> TO, one ADR-0087 marker not-required (no-migration-prescription), the launch-window BREAKING line, and PR #23460's createHonoApp-renders-500 wording (no wire 409 promised).",
"tests": "Pin file packages/runtime/src/domains/data-update-expected-version.test.ts (PR #23460's rig: real better-sqlite3 store, real ObjectQL + ObjectStackProtocolImplementation, RestServer's registered PATCH handler and HttpDispatcher.dispatch() side by side, the store read after every request): 5 rows per door (no token; current body token updates and the patch carries no expectedVersion key; stale body token 409 CONCURRENT_UPDATE, row unchanged; If-Match both ways; body token judged before If-Match), 6 side-by-side token shapes (stale, number, null beside stale and current If-Match, '' beside stale If-Match, body only the current token) asserting equal status+code+row and no key reaching the engine, the Node header-record row, and 3 fallback rows (body token and If-Match 501 NOT_IMPLEMENTED with find/update never called; unguarded update still reaches the fallback). BEFORE (unfixed source, 94f0cf0): 'Tests 13 failed | 7 passed (20)' (every RestServer row and both controls green). AFTER (19d1b8d, with the landed DELETE file): 'Test Files 2 passed (2) / Tests 40 passed (40)'. ABLATIONS, each via node scripts/ablation-replace.mjs WRAP mode + a driver restore trap, anchor x1 -> x0, 'mutation landed', restore proven by blob == HEAD and empty git diff HEAD; subject imported relatively (../http-dispatcher.js -> src), so no dist rebuild in the path: A data.ts forward dropped '10 failed | 10 passed'; B callData update-arm forward dropped '8 failed | 12 passed' (501 rows stay green, the refusal is its own statement); C fallback refusal if(false) '2 failed | 18 passed'; D (beyond the dispatch's three, for 'never written as a field') the strip dropped '9 failed | 11 passed'. Expected direction red each time; each went red. A's first attempt was a no-op the tool refused before running anything (replacement 'data,' was a prefix of the anchor, so its count could not rise); redone with a marker comment. FINAL HEAD 75d46f1 (merges origin/main 1baf976; incoming #23458 #23466 #23464 touch nothing on this path; then pnpm install --frozen-lockfile, rm -rf packages/runtime/.objectstack, closure rebuilt pnpm --filter '@objectstack/runtime...' build VERDICT 0): pnpm --filter @objectstack/runtime run typecheck exit 0 (check:test-typecheck: 27 files / 190 errors / 68 pinned signatures held; new test in tsconfig.test.json --listFilesOnly 1 hit, tsconfig.json 0); vitest run --project local --maxWorkers=2: 'Test Files 377 passed (377) / Tests 5507 passed | 19 skipped (5526)'.",
"gates": "Local, all at final head 75d46f1, exit captured before any pipe: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack = 65 commands; 64 exit 0; 'pnpm check:dual-build-cjs-loads :: exit 3' PREREQUISITE NOT MET (needs a whole-repo pnpm build) = NOT MEASURED, CI runs it. --ran reconciliation (record with exits): '65 derived, 64 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3), 0 UNRUN'. The five roster gates whose roster sits under a touched directory, also run: check-changeset-fixed, check:authz-resolver, check:error-code-casing, check:filter-alias-parity, check:route-ledger-census, each exit 0. pnpm lint as the declared narrowing: (1) the diff touches no eslint config/ignore file (0 paths); (2) pnpm exec eslint --no-inline-config over data.ts, action-execution.ts and the new test exit 0, empty output; (3) the config is untyped and per-file (eslint.config.mjs:326-328 'never enables type-aware linting'; 6 active rules on data.ts, none cross-file). CI (PR #23490 check-runs on 75d46f1, read once at report time, not waited on): 36 runs: 4 completed success, 6 completed skipped, 18 in_progress, 8 queued; Test Core (1/6..6/6) queued, Build Core queued, Governed Surface Queue Guard in_progress; Lint & Repo Gates and TypeScript Type Check not yet listed. in_progress is the honest value.",
"deviations": [
"Ablation D (strip dropped) added beyond the dispatch's three, to prove the card's 'a body token is never written as a field' pin can fail; additive.",
"Ablation A's first attempt measured nothing (refused by ablation-replace's own evidence check before the command ran); redone, both legs reported.",
"Lint: the dispatch's 'this lane always adds pnpm lint' meets the role file's 'repo-wide scans are CI's, never owed'; took the dispatch's own alternative, the proven narrowing with three pieces of evidence, which satisfies both.",
"Merged origin/main (1baf976) once before opening the PR, per AGENTS.md section 10; the gate list was derived pre-merge at ae61e9d and re-derived at 75d46f1 by --ran (65 both times).",
"Harness attribution reminder asked a model-named Co-Authored-By trailer and a different PR footer; followed AGENTS.md's model-free trailer pair and the session-URL footer (role file precedence). Not a deviation from the binding text; reported.",
"Report comment follows the landed os-dev-report shape (marker line + raw JSON, no footer) so the PM's mechanical parse is unchanged."
],
"files_changed": [
".changeset/23462-dispatcher-patch-expected-version.md",
"packages/runtime/src/action-execution.ts",
"packages/runtime/src/domains/data-update-expected-version.test.ts",
"packages/runtime/src/domains/data.ts"
],
"mechanism_readings": {
"a": "Confirmed: rest-server.ts:9014-9063 reads body expectedVersion else If-Match, strips the key when present, forwards String(token) when truthy; mirrored; no @objectstack/rest export added.",
"b": "Confirmed: requestIfMatch and the delete arm's forward + 501 shape reused; no second spelling.",
"c": "Measured both doors (scratch, not committed). Token shapes: doors agree (number/object stringified and judged; null falls to If-Match; false or '' stripped and forwarded as no token, so If-Match unread; quoted-empty "" refused 400 VALIDATION_FAILED by the protocol on both). Non-record body: doors differ, not about the token: RestServer's UpdateDataRequestSchema gate answers 400 VALIDATION_FAILED for array/string/number and 200 for null; the dispatcher answers 500 with nothing written. Pre-existing, different defect class, reported as finding 1, not fixed.",
"d": "Confirmed: dispatch() throws ConcurrentUpdateError read 409/CONCURRENT_UPDATE. Real createHonoApp (scratch: adapter source over runtime dist built at 19d1b8d): stale body token and stale If-Match each 500 INTERNAL_ERROR with the conflict sentence, row unchanged; current token and no token 200, updated. #23365 remains open for the 500; changeset carries PR #23460's wording, no wire 409 promised.",
"e": "No hono-* pin added, so no surface supplement needed. Both PATCH hand-offs are already pinned at the adapter (hono.test.ts 'PATCH /api/data/account parses JSON body'; hono-catch-all-raw-request.test.ts raw Request for PUT/PATCH, where If-Match rides), and the domain's reading of both is pinned here through dispatch(). The one added fact would be the wire status, today #23365's 500; pinning it pins that defect. End-to-end reading taken as a scratch run instead, as PR #23460 did."
},
"mcp_calls": "0",
"api_writes": "2 relay strokes (each one POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft, #23490) + POST /repos//issues/23490/assignees [sunhaolin], run 38568895132 success, read-back 12154 bytes sent = stored; (2) comment -> POST /repos//issues/23462/comments (this report). Zero label writes (none expected). git push x5 (not REST).",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: HTTP, a real createHonoApp (scratch over runtime dist at 19d1b8d): PATCH /api/v1/data/task/ID with an array, a string or a null JSON body answered 500 INTERNAL_ERROR (the engine's own error: 'no such column: 0', 'Cannot use 'in' operator to search for 'organization_id' in text', 'Cannot read properties of null (reading 'id')'), nothing written; RestServer's twin answers 400 VALIDATION_FAILED (its UpdateDataRequestSchema gate) for array/string/number and 200 for null (patched as {}). A caller's mistake is answered as an internal fault, and one route's two transports answer differently. Pre-existing: the dispatcher's PATCH branch never had REST's body-schema gate (before this PR the same bodies passed through unchanged); POST create likely has the same gap (REST's create gate), unmeasured. Seam: spec:UpdateDataRequestSchema.data (record) -> runtime: domains/data.ts PATCH branch (no schema gate). Dedupe words: 'dispatcher PATCH non-record body 500 VALIDATION_FAILED', 'UpdateDataRequestSchema data record dispatcher createHonoApp', 'dispatcher data write body schema gate RestServer parity'.",
"class: a · reach: HTTP, a real createHonoApp (scratch): body {name, expectedVersion: ''} with a stale If-Match answered 200 and wrote the row; the same on RestServer's registered PATCH handler (scratch rig: 200, row written); a false body token likewise. REST's 'bodyVersion ?? If-Match' then a truthy test: an empty or false body key is not nullish, so If-Match is never read, and falsy, so no token is forwarded, and the update runs unguarded beside the stale If-Match the caller sent. It is the #13576 class (an empty token silently opting out of the check) on the body spelling; the protocol refuses the quoted-empty "" but never sees this one. Mirrored on purpose by PR #23490 to keep the doors alike, so the fix lands in RestServer and the dispatcher together. Seam: spec:UpdateDataRequest.expectedVersion -> runtime: rest-server.ts PATCH :9021-9024 + domains/data.ts PATCH branch. Dedupe words: 'expectedVersion empty string If-Match ignored', 'falsy body token skips If-Match stale update applied', 'PATCH data expectedVersion false'."
],
"noted_not_filed": [
"?expectedVersion as a query on PATCH is read by neither door (both apply a stale update sent that way); the spec declares the PATCH token as the body field or If-Match, so an undeclared spelling. In the PR's Acceptance notes; carrier: 承接者:无.",
"The declared-endpoint door (endpoint-executor.ts, object_operation update) hands ctx.body as data and reads no token; read-only inference, unmeasured. In the PR's Acceptance notes; carrier: 承接者:无."
]
}
objectstack-fleet commentedon Oct 14, 2026
✅ ACCEPT: PR #23490 at
75d46f1034(#23462). It lands after oneCONTRACT_REVIEW_TIERPASS and an all-green head · 2026-10-14T13:13Zdomain:cliseat 2 (#22648) ·sunhaolin· sessionsession_01Q9PZ5Pzqcus4woVvTZwWNU· holder of claim6167715147. Report6169083704. Thread-read:6169083704.Checklist, read against GitHub and
origin/main, not against the report:main, titlefix(runtime)!:, assigneesunhaolin. Line 1 isFixes #23462, the body's only closing keyword; line 2 isClause-②: no (narrowing).check-governed-merges --pr 23490: NOT governed, 447 changed lines. Nocontent/docs/releases/change.rest-server.ts:9020-:9058onmain0d3ea295d1):PATCHbranch ofdomains/data.tsreads the body'sexpectedVersionwhen the body is an object, elseIf-Matchthrough the privaterequestIfMatchPR fix(runtime)!: the dispatcher's DELETE /data/:object/:id honours ?expectedVersion and If-Match, as RestServer's twin does #23460 added. It strips the key from the patch whatever its value, and forwardsString(token)when truthy.bodyVersion ?? ifMatchHeader, its strip, and itsexpectedVersion ? { expectedVersion: String(expectedVersion) } : {}. There is no secondIf-Matchspelling.callData'supdatearm forwardsexpectedVersiononly when a caller sent one. Its ObjectQL fallback refuses a token withthrow { statusCode: 501 }before any read, in the shape of PR fix(runtime)!: the dispatcher's DELETE /data/:object/:id honours ?expectedVersion and If-Match, as RestServer's twin does #23460'sdeletearm.callData('update', …)callers hand{ object, id, data }and are unchanged.data-update-expected-version.test.tsruns over PR fix(runtime)!: the dispatcher's DELETE /data/:object/:id honours ?expectedVersion and If-Match, as RestServer's twin does #23460's rig: a real better-sqlite3 store, the real ObjectQL and the real protocol, withRestServerandHttpDispatcher.dispatch()side by side, and the row read back after each request.RestServerrow green. After: 40 green across both files.data.tsforward, thecallDataforward, the fallback refusal, and the strip.@objectstack/runtimeminor,fix(runtime)!:,Clause-②: no (narrowing), one ADR-0087not-required (no-migration-prescription)marker, and the launch-window BREAKING line.If-Match: FROM200and written, TO409 CONCURRENT_UPDATEthrown out ofdispatch(). It keeps PR fix(runtime)!: the dispatcher's DELETE /data/:object/:id honours ?expectedVersion and If-Match, as RestServer's twin does #23460's sentence that acreateHonoApphost still renders it500with the row unchanged; the dev measured exactly that on a realcreateHonoApp. No wire409is promised.expectedVersion: FROM refused as an unknown field, TO the version token, stripped from the patch and read beforeIf-Match.501.@objectstack/client'sdata.updatesendsdataas thePATCHbody (packages/client/src/index.ts:7962-:7966). Each FROM → TO holds on every host it names.75d46f1034, as follows:check:dual-build-cjs-loadsexited 3 on a missing prerequisite (a whole-repo build): NOT MEASURED, and CI runs it.pnpm lintwas taken as the proven narrowing, with three pieces of evidence: no eslint config or ignore file touched, eslint over the three files exits 0, and the config is untyped and per-file.75d46f1034at this stamp: 37 runs, of which 20success, 6 skipped and 11 still running. Every skip is on the expected-skip roster: Dogfood Verify CLI, Temporal Conformance, Console Pin Gate, Build Docs, Dogfood Regression Gate, and Packed-tarball smoke. Landing reads the check-runs again.Accepted deviations:
hono-*pin (hypothesis (e)). The dev reasoned that the one fact such a pin would add is today's wire500, and pinning it would pin hono adapter:createHonoApp's catch-all renders an error thrown out ofdispatch()fromstatusCodealone, so a declaredstatus/code(a missing record's404 RECORD_NOT_FOUND, an outage's503) answers500 INTERNAL_ERROR#23365's defect. BothPATCHhand-offs are already pinned at the adapter, and the end-to-end reading was taken as a scratch run, as PR fix(runtime)!: the dispatcher's DELETE /data/:object/:id honours ?expectedVersion and If-Match, as RestServer's twin does #23460 did. This seat agrees.pnpm lintas the proven narrowing, with the three pieces of evidence. This lane's standing sentence allows exactly that alternative.Out-of-scope findings, one line each:
PATCHbody answers500through the dispatcher, where REST'sUpdateDataRequestSchemagate answers400(请求体从不与声明它的 schema 对照(#3877 的请求侧对偶):7 个 schema 定义了从未启用,而 API 目录已宣称生效 #3899's class). ThePOSTcreate sibling was read in source. Filed runtime: the dispatcher'sPATCH /data/:object/:idhas no request-schema gate, so a non-record body answers500with the engine's own error, where REST answers400 VALIDATION_FAILED#23491.expectedVersionof''orfalsehidesIf-Matchon both doors, so an update sent with a staleIf-Matchis applied ([finding]If-Match: ""silently DISABLES optimistic concurrency — a quoted-empty entity-tag is read as "no token" and the guarded write proceeds unguarded #13576's class). PR fix(runtime)!: the dispatcher's PATCH /data/:object/:id honours a body expectedVersion and If-Match, as RestServer's twin does #23490 mirrors REST on purpose so the fix lands in both together. Filed rest, runtime: a bodyexpectedVersionof''orfalsehidesIf-MatchonPATCH /data/:object/:id, so an update sent with a staleIf-Matchis applied unguarded, on both doors #23492, ordered after this PR.?expectedVersionas aPATCHquery is read by neither door, and it is an undeclared spelling. Acceptance notes, carrier none.endpoint-executor.ts'sobject_operationupdate reads no token. This is unmeasured inference. Acceptance notes, carrier none.Landing: one
CONTRACT_REVIEW_TIERrecord is owed, from an isolated subagent reviewing75d46f1034. Thenpr_ready+automerge_enablego through the relay once every check on the current head issuccessor a roster skip.Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
ACCEPT → PR #23490 comment 6169313057 · 2026-10-14T13:19Z
The seat record of PR #23490 now lives on the PR, as
review-checklist.mdhas required since2659901c5e. This seat's earlier ACCEPT on this card,6169192789, carries the same verdict and is now history only.Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
Landed: PR #23490 →
650933ec76· 2026-10-14T13:48Zdomain:cliseat 2 (#22648) ·sunhaolin· sessionsession_01Q9PZ5Pzqcus4woVvTZwWNU· holder of claim6167715147, released by this landing. Review: seat record6169313057on the PR (pointerACCEPT → PR #23490 comment 6169313057on this card), andCONTRACT_REVIEW_TIERrecord6169384075(PASS on75d46f1034, served by the default model per the maintainer's 「契约复审 opus就够了」).merged: true,merged_at2026-10-14T13:47Z, through the merge queue (added_to_merge_queue2026-10-14T13:29Z)gh-readonly-queue/main/pr-23490-*git ls-remote, 0 refs)origin/main650933ec76is an ancestor; one parent (8693110c4c); 4 files, +445 / −2, as reviewedFixes #23462onlydomain:clicards. Against the 12:10Z set: minus #23462 (this landing), plus #23479 (filed 12:07Z, p2pm:queue). #23491 and #23492 (filed at this PR's ACCEPT) are stillfinding, ungradedState change: #23462 closed
completedby the merge;pm:dispatchedremoved in this act. #23492's order condition ("after PR #23490 lands") now holds. The contract reviewer's escalation (the declared-endpoint door'sobject_operationupdate reads no token) stays in Acceptance notes, 「承接者:无」: it is unmeasured inference with no named producer, so it has noreach:(filing-gate.md:15,:21).Generated by Claude Code