Repository navigation
finding(metadata-protocol): a sys_metadata row with no checksum cannot be edited or removed through the metadata door: the read hands out a computed version, and put / delete compare it against the raw null column (409 METADATA_CONFLICT) #21978
Description
Activity
objectstack-fleet commented
on Oct 6, 2026 ContributorAuthorMore actionsPath: 外部数据源接进来当自己的对象用 (the metadata door,
api) | 缺项 (no item asserts the metadata door's read and write agree on a row's version) | P3Triage: first grade —
bug·priority:p3·domain:engine·area:api·pm:queue. The compare uses the version the read hands out; no backfillTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-06T10:53Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/metadata-protocol/src/sys-metadata-repository.ts⇒domain:engine; rationale: one repository hands out one version for a row and checks a different one.Verified on
main(dcf3eb494a):rowToItem(about:2013) servesrow.checksum ?? hashSpec(body, ref.type).put(about:634) anddelete(about:815) compare the caller's parent withexisting.checksum ?? null.- A row with no
checksumcan therefore never match the version its own read handed out.
Direction:
- One helper computes a row's served version, and the read,
putanddeleteall use it. - For a row with no
checksum,putanddeletealso accept that served version. A row with achecksumis judged exactly as today. - ⛔ Nothing is narrowed. Today a
nullparent matches such a row, and some writer may rely on that, the admin door's write path first among them. That match stays. - The next write stamps the row's
checksumas usual. - ⛔ No backfill and no migration of stored rows. The file header lists "hashSpec backfill for legacy rows" as a non-goal, and this direction does not need one.
Pins:
- A stored row with no
checksumis updated and deleted through the metadata door with the version its read returned. - A stale version still gets
409 METADATA_CONFLICT. - A writer that passes
nullfor such a row today still succeeds.
Why p3:
- The admin door still edits and removes these rows, and its next write stamps them.
- The only producer in this tree is closed by PR fix(service-datasource): the admin door reads a datasource's origin from provenance, and a metadata-door write reaches it in the same boot #21977, which is still open. Until it merges, the admin door keeps writing such rows.
- Nothing is exposed and nothing is lost.
Serial: #21922 (
pm:queue, the same lane) changesgetMetaItem's stored-row step, not this file. No overlap is expected; the claim confirms it.The census of other
sys_metadatawriters that omitchecksumis the claim's, as the card says. One that is still live after this fix is a follow-up card.
Generated by Claude Code
- addedarea:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingSomething isn't working
on Oct 6, 2026 objectstack-fleet commented
on Oct 6, 2026 ContributorAuthorMore actionsClaim: PM loop round 45 · 2026-10-06T11:29Z
Session:session_017ErfyP2Rx7XWHJA27QjyUi
Account:os-project-manager(the seat's linked user, asGET /useranswers it; always the card's assignee)
Branch:claude/issue-21978-served-version-compare
Worktree:objectstack-issue-21978
Domain:domain:engine
Seat:domain:engine#1
Provenance: triage graded this cardpm:queue(6014717866). It is the only eligiblepm:queuecard in this lane. The lane runs three concurrent claims (the maintainer, verbatim: 「并发3」); the other two dev slots are #21972 and #21922, and #21967 waits in the merge queue.
File surface (atorigin/main8a399b2b15), per triage's direction 6014717866:packages/metadata-protocol/src/sys-metadata-repository.ts:- one helper computes a row's served version,
row.checksum ?? hashSpec(body, ref.type), asrowToItemserves it (about:2013); put(about:634) anddelete(about:815) compare through it. For a row with nochecksum, they also accept that served version.- A row with a
checksumis judged exactly as today. Anullparent still matches achecksum-less row (⛔ nothing narrowed). - The next write stamps the row's
checksumas usual.
- one helper computes a row's served version,
- ⛔ No backfill, and no migration of stored rows. The file header's non-goal stands.
- The census of other
sys_metadatawriters that omitchecksumis this claim's, as the card says. Each one still live after the fix is reported in the PR, for a follow-up card. - Pins, as triage lists them:
- a stored row with no
checksumis updated and deleted through the metadata door with the version its read returned; - a stale version still gets
409 METADATA_CONFLICT; - a writer that passes
nullfor such a row still succeeds. - Unit pins in
metadata-protocol. If a door-level pin is needed, it is driven in-process through the protocol'ssaveMetaItem/deleteMetaItem.
- a stored row with no
.changeset/21978-*.md(@objectstack/metadata-protocolpatch).
Container & model:M,mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate).
Clause-②: noputanddeleteaccept one more parent for achecksum-less row, the version the repository's own read serves. No exported type moves. The dev measures this against the built entry declarations and reports it.
Thread-read: 6014717866
Serial constraints cleared: at 2026-10-06T11:29Z:- The card's producer fix, PR fix(service-datasource): the admin door reads a datasource's origin from provenance, and a metadata-door write reaches it in the same boot #21977 (
domain:services), is merged. - Open PRs (docs(spec): ObjectNavItemSchema.viewName states the view the console opens when it is omitted #21981, fix(metadata-protocol): another package's withdrawal of a form holds at the anonymous form endpoints, whatever packages' copies of a view container are saved #21979, feat(spec)!: the build doors refuse a builtin node config value its executor contract refuses, with its location #21974, chore: version packages #21352), each file list read by
filename: none touchessys-metadata-repository.ts. This lane's PR fix(metadata-protocol): another package's withdrawal of a form holds at the anonymous form endpoints, whatever packages' copies of a view container are saved #21979 (finding(metadata-protocol): a saved env-wide copy of a view container leaves its own expansion alone per name in the env-wide view list, so the anonymous form doors can miss another package's withdrawal, saved or shipped (#21934 item 1's residual) #21967) touchesprotocol.tsonly. - This lane's finding(service-datasource): a stored datasource row overrides a code-defined datasource at boot, so after a restart the admin door serves and edits it at runtime (restoreRuntimeDatasources has no code-collision check) #21922 (in flight) edits
protocol.ts'sgetMetaItemstored-row step, not this file, which confirms triage's expectation. finding(platform-objects): six more Setup object entries open their object's caller-scoped first list view (mine / granted_to_me), and sys_user's bare-object doors still openme(#21960's family, from PR #21971) #21972 (platform-objects) and [finding]sys_member.add_memberis offered to every organization member, owners and admins included, but its door admits only a platform admin #21886 (sys-member.object.ts) share no file with this card.
objectstack-fleet commented
on Oct 6, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 21978, "status": "done", "branch": "claude/issue-21978-served-version-compare", "pr": "https://github.com/objectstack-ai/objectstack/pull/21990", "session": "session_017ErfyP2Rx7XWHJA27QjyUi — the harness-stamped id of this run (a subagent, so it is the dispatching PM's)", "premise_still_valid": true, "summary": "The premise holds at BASE 8a399b2b15 at the card's exact lines: rowToItem (:2013) serves row.checksum ?? hashSpec(body, type), while put (:634) and delete (:815) compare the parent with existing.checksum ?? null. The reverse verification reproduced the card's text verbatim ('Expected parent hmac-sha256:… but current is null'). In sys-metadata-repository.ts, one helper servedVersion(ref,row) now names a row's served version and rowToItem reads it, and one lock lockAccepts(ref,row,parent) used by put and delete accepts the stored stamp as before, plus the served version for a checksum-less row. A row with a checksum is judged exactly as before, a null parent still matches a checksum-less row, and a conflict on such a row names its served version (lockHead) instead of null. Lineage fields and the no-op check keep the raw stamp, so the next write stamps the row; there is no backfill and no migration. The same lock change also repairs callers the card did not name: unpinned (last-write-wins) save/delete, publish over such a row, rollback/commit revert (read in code), and the post-promotion drain, which silently left a checksum-less draft pending. The census finds no writer that still produces checksum-less rows, so there is no follow-up card. PR #21990 is a draft, opened with 'Fixes #21978' then 'Clause-②: no'.", "tests": "All on the final HEAD 81606021e2 unless stated otherwise. (1) Through the verify lock: turbo build of the metadata-protocol closure (13 tasks), then `pnpm --filter @objectstack/metadata-protocol test`, giving 'Test Files 218 passed | 3 skipped (221)' and 'Tests 28028 passed | 19 skipped (28047)', then `pnpm --filter @objectstack/metadata-protocol typecheck`, giving tsc --noEmit clean; 'VERDICT command-exit 0'. The test file is in the tsc program (`tsc --noEmit --listFiles` count 1). (2) New pins: 9 cases appended to src/protocol.served-content-hash.test.ts, reusing its existing engine double (no new fake engine). Pins (a)-(d), publish over a checksum-less active row, the drain of a checksum-less draft, and repository-level stamped/unstamped lock pins. The file reads 23 passed. (3) Reverse verification on committed HEAD 5c4815a6ab, through scripts/ablation-replace.mjs (WRAP mode, its own EXIT/INT/TERM restore plus my own trap, absolute REPO_ROOT paths). It restored the raw compare `if (opts.parentVersion !== existingHash)` with actualHead existingHash in put and delete. The tool printed 'anchor hits 2 time(s), as declared', 'anchor x2 → x0', 'replace x0 → x2' and 'blob dc58518587cf → 494fa3f0eecb'; my on-disk grep -c read raw-compare 0→2 and lockAccepts-call 2→0. Predicted beforehand: 7 red (save, reset, unpinned, stale, publish, drain, repo-unstamped), with null-parent and stamped-row green. Observed 'Tests 7 failed | 16 passed (23)', exactly those 7. Save door error: 'view/case_grid has been modified since you loaded it. Expected parent hmac-sha256:e532d121… but current is null.' Drain pin: draft row still present. Repo pin: actualHead null. Restore proven by observation, not by exit code: tool 'blob after restore dc58518587cf == blob at HEAD', 'git diff HEAD empty'; my script AFTER_BLOB dc58518587cfe675843788f49a226a472dc434a3 == HEAD_BLOB, git-diff-HEAD-bytes=0, status-porcelain empty. The first ablation invocation was a no-op: exit 2 'outside the repository at /home/user/objectstack', because the tool takes the repo from cwd; on-disk counts were unchanged (0/2 before and after). It was rerun after cd REPO_ROOT. Resolution path: the subject is imported via relative src (./protocol.js, ./sys-metadata-repository.js), so no dist/ is on the path and no rebuild leg is owed. (4) Clause-② measurement: see clause_2.", "mcp_calls": "0 — no MCP GitHub tool was called. Reads went through REST GETs via the gh CLI (issue #21978 and its comments, PR #21990 read-back, the #21923 report for format) and the scripts' own reads.", "api_writes": "3 REST writes, each one relay stroke POST /repos/objectstack-ai/objectstack/dispatches executed by .github/workflows/fleet-write.yml as objectstack-fleet[bot]: (1) pr_create, i.e. POST /repos/objectstack-ai/objectstack/pulls, giving draft #21990 (run 37466248528, read-back 10984/10984 bytes identical); (2) label-write --assign os-project-manager, i.e. POST /repos/objectstack-ai/objectstack/issues/21990/assignees (run 37466352199, read-back assignees os-project-manager); (3) this os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/21978/comments. Plus 5 git pushes to the feature branch only (empty-branch probe, fix commit, pins commit, merge 1, merge 2), which are not REST. No label was added: the dispatch named none, and a changeset exists, so skip-changeset does not apply.", "open_questions": [], "out_of_scope_findings": [ "carrier: 承接者:无 · noted, not filed — packages/cli/src/commands/migrate/meta.stored-flow-resolution.integration.test.ts (about :190) explains its explicit parentVersion null by saying a raw-seeded row's derived parent 'would 409'. That is false after PR #21990, and the null it passes stays valid. Comment drift in another package; it is in PR #21990's Acceptance notes.", "carrier: 承接者:无 · noted, not filed — DraftDrainFailure.draftHash (sys-metadata-repository.ts) is documented as 'the row's checksum', but it carries the served version, the same value for every stamped row. A doc imprecision that predates PR #21990; in its Acceptance notes." ], "gates": { "head": "81606021e2 (after merging origin/main twice; the second merge brought PR #21979's protocol.ts change, so everything was rerun on this head; a first full pass on cd0ec1039e was also green)", "derived": "`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` with no paths derived 63 commands over 4 paths (+343/-12), the same set as at cd0ec1039e. All 63 exit 0. check:type-check-debt ran under the verify lock ('1 ledger entr(ies) re-measured in 234.1s, 26 raw tsc error(s) total, none above its recorded number'). check:dual-build-cjs-loads and check:lean-entry-closure ran after a full `turbo run build --filter=!@objectstack/docs` (72 tasks, 71 cached).", "reconciliation": "`dispatch-gates --ran` with 121 'command :: exit N' records: 'Run reconciliation — 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN'; 'EXIT CODES — all 63 accounted famil(ies) carry one'.", "artifact_roster_block": "55 families printed outside the total: 52 exit 0 locally. check-closing-target-claim, check-partof-closing-keyword and check-single-claim-paths answered NOT WIRED (exit 2, no PR context) and were rerun with PR #21990's context, each exit 0 ('PR #21990 closes #21978, and each carries a Claim: whose Branch: line names claude/issue-21978-served-version-compare'). 17 of the 55 are checker-health --self-test runs only.", "symbol_anchor_sweeps": "pnpm check:adr-symbol-anchors, check:scripts-symbol-anchors, check:spec-docblock-symbol-anchors, check:adr-anchors: all exit 0.", "package": "@objectstack/metadata-protocol test 28028 passed / typecheck clean on 81606021e2 (lock VERDICT command-exit 0).", "not_measured": "NOT MEASURED: path-scheduled CI jobs (Test Core shards, Temporal Conformance live PG+MySQL, Dogfood Regression Gate, Dogfood Verify CLI, Build Core), reason: CI's own shell, with no local invocation. NOT MEASURED: the workspace type-check lanes in lint.yml, reason: CI-wide filters. NOT MEASURED: the 6 workflow-valued families (shard attestation, test completeness, issue-citations --census), reason: their argv needs CI variables. NOT MEASURED: the 11 wide-population families, reason: no path narrows them. NOT MEASURED: check-changeset-no-major's Clause-② axis, reason: PR-scoped with no pull_request payload locally (its base run exited 0). The dogfood pin datasource-meta-door-reaches-admin-door.dogfood.test.ts was not run locally; it belongs to the Dogfood Regression Gate." }, "line_budget": "n/a", "deviations": [ "Ablation: the first invocation was a no-op (scripts/ablation-replace.mjs exit 2, which took the repository from the shared checkout's cwd); it wrote nothing (on-disk counts unchanged), and the rerun from the worktree root is the measurement reported.", "check:type-check-debt first hit my own 300s per-command timeout (exit 124, no verdict); it was rerun under the verify lock and passed (exit 0).", "check:dual-build-cjs-loads and check:lean-entry-closure first answered PREREQUISITE NOT MET (exit 3, not a verdict); a full turbo build (71/72 cached) was run, and both then passed (exit 0).", "One gate batch overran the foreground cap and the harness moved it to the background. I kept the turn alive and waited in the foreground with `tail --pid` on the PID I started until it exited, then read its results file.", "origin/main moved twice: merged once before the gates (Version Packages #21352) and again after #21979 touched protocol.ts; the full union was rerun on 81606021e2. It has since moved by 2 more commits (#21981, #21983: platform-objects, plugin-sharing, lint, spec docs; none touches metadata-protocol), which were not merged.", "Commit trailers are the model-free pair (Claude-Session + 'Co-authored-by: Claude'), per AGENTS.md, not the harness reminder's model-named Co-Authored-By. The PR footer is the AGENTS.md session-URL form. Commits carry no card trailer.", "Beyond the two named files, one comment in packages/metadata-protocol/src/protocol-publish-drafts-package-scope.test.ts was edited (4 lines). It said a checksum-less draft survives the drain, which this change makes false; the same package, no behaviour.", "Clean-up runs after this comment is posted: rm -rf the worktree's node_modules, then `git worktree remove` without --force. Its result is therefore not in this JSON." ], "files_changed": [ "packages/metadata-protocol/src/sys-metadata-repository.ts (+96 -8): storedRowBody(), servedVersion(), lockHead(), lockAccepts(); put/delete lock; rowToItem reads servedVersion; header non-goal line", "packages/metadata-protocol/src/protocol.served-content-hash.test.ts (+231 -0): the #21978 pins, reusing the file's engine double", "packages/metadata-protocol/src/protocol-publish-drafts-package-scope.test.ts (+4 -4): fixture comment made false by this change", ".changeset/21978-checksum-less-row-served-version.md (+12): '@objectstack/metadata-protocol': patch, with Clause-②: no" ], "clause_2": "no. Measured against the built entry declarations: packages/metadata-protocol/dist/index.d.ts built at HEAD, and again with BASE 8a399b2b15's sys-metadata-repository.ts swapped in behind a trap (on-disk blob 80789431dc == base blob before the build; restored and proven, blob dc58518587 == HEAD, git diff HEAD empty; HEAD rebuilt with a byte-identical index.d.ts). The diff's non-comment lines are '+ private servedVersion;', '+ private lockHead;' and '+ private lockAccepts;' with 0 removed; everything else is doc text (52 added / 1 removed lines in total). index.d.cts has the identical diff. No exported type, signature or entry moves. Behaviourally, put/delete accept for a checksum-less row the version the same repository already serves for it (the declared version token), not a new input class.", "H1": { "verdict": "Confirmed at BASE 8a399b2b15. Each site below is labelled with the spelling it reads: SERVED is checksum ?? hashSpec(body, type) via rowToItem; RAW is the column.", "sites": [ "rowToItem :2013: SERVED. Feeds get (:472-495), list (:1176), the put no-op re-yield (:651), promoteDraft's draft and currentActive (:1010, :1020) and restoreVersion's currentActive (:1150).", "get :472-495: SERVED (via rowToItem).", "getByHash :505-539: the history table, looked up WHERE checksum = the hash argument, which it echoes; parentHash is RAW previous_checksum ?? null. It does not read a sys_metadata row's version, and history rows written by put always carry a checksum.", "put lock :634-637: RAW existing?.checksum ?? null. This is the defect; it is now lockAccepts (RAW or, for a checksum-less row, SERVED).", "put no-op check storedBodyUnchanged(..., existingHash, hash) :650: RAW, kept deliberately, so a checksum-less row never short-circuits and an identical re-save stamps it.", "put lineage previous_checksum :715, item.parentHash :732, broadcast parentHash :770: RAW, kept.", "delete lock :815-818: RAW. The defect; now lockAccepts.", "delete tombstone previous_checksum :858: RAW, kept.", "promoteDraft expectedDraftHash (#21934) :1014: SERVED vs the caller's expectation. The protocol takes it from its own repo.get (SERVED), so it was already consistent at BASE.", "promoteDraft parent :1023: SERVED currentActive.hash goes to put, which at BASE compared RAW, so a 409 for a checksum-less active row. Pinned (publish).", "dropPromotedDraftRow :1045/:2270: SERVED draft.hash goes to delete, which at BASE compared RAW, giving a ConflictError that draftDrainVerdict silences as a benign race, so a checksum-less draft survived publish silently. Pinned (drain).", "restoreVersion parent :1152: SERVED goes to put, a 409 at BASE (read in code, not pinned).", "history()/rowToEvent :1319: the history table's RAW checksum ?? null; a tombstone reads back as op 'delete' with hash null, correct by design.", "ConflictError actualHead (:636, :817): RAW at BASE (null for a checksum-less row, hence 'current is null'); now SERVED via lockHead.", "Door callers in protocol.ts: saveMetaItem's parent (:20381 unpinned / :20389 token) is SERVED current.hash; deleteMetaItem's (:25992) is SERVED; revertCommit's (:24799) is SERVED; migrateStoredMetadata (:21060) passes RAW row.checksum ?? null; flow-credential-migration calls saveMetaItem with no parent, so SERVED. All except migrateStoredMetadata hit the 409 at BASE." ] }, "H2": { "verdict": "Confirmed and implemented as hypothesised.", "detail": "lockAccepts(ref,row,parent) = parent === (row.checksum ?? null) || (row.checksum ?? null) === null && parent !== null && parent === lockHead(ref,row). The first arm is BASE's compare byte for byte, so a stamped row is judged exactly as before; this is pinned on a row whose stamp differs from its body's hash, where the body's hash and null are refused and the stamp is accepted. Callers passing null for a checksum-less row are migrateStoredMetadata's storedParentVersion: row.checksum ?? null (pinned, succeeds) and saveMetaItem with parentVersion null (storedParentForToken(null) gives null, still matched; e.g. the CLI integration probe). The datasource admin door's write path calls neither put nor delete: writeDatasourceRow / its delete use engine.insert/update/delete directly with no optimistic lock (datasource-admin-plugin.ts :115-165), so this change does not affect it. DeleteOptions.parentVersion is typed string, so no in-tree delete caller passes null; the null match is kept for delete anyway, for symmetry." }, "H3": { "verdict": "No writer in this repository still stores a sys_metadata row with a null or absent checksum, after PR #21977 or after this fix, so there is no follow-up card.", "census_table": [ {"writer": "SysMetadataRepository.put (insert/update)", "where": "packages/metadata-protocol/src/sys-metadata-repository.ts", "checksum": "always hashSpec(body, type)", "live_after_21977": "no", "live_after_fix": "no"}, {"writer": "SysMetadataRepository.delete", "where": "same file", "checksum": "removes the sys_metadata row; the tombstone goes to sys_metadata_history with checksum null by design", "live_after_21977": "n/a (history table)", "live_after_fix": "n/a"}, {"writer": "datasource admin door writeDatasourceRow", "where": "packages/services/service-datasource/src/datasource-admin-plugin.ts", "checksum": "hashSpec(record, 'datasource') since PR #21977; none before (the legacy population)", "live_after_21977": "no", "live_after_fix": "no; its legacy rows are writable via the served version"}, {"writer": "datasource admin door delete fallback update {state: 'inactive'}", "where": "same file", "checksum": "partial update, keeps the column", "live_after_21977": "no", "live_after_fix": "no"}, {"writer": "DatabaseLoader save / create / registerRollback", "where": "packages/metadata/src/loaders/database-loader.ts", "checksum": "contentHash stamp", "live_after_21977": "no", "live_after_fix": "no"}, {"writer": "protocol orphan adoption (package_id rebind)", "where": "packages/metadata-protocol/src/protocol.ts", "checksum": "partial update, keeps the column", "live_after_21977": "no", "live_after_fix": "no"}, {"writer": "protocol legacy delete; permission-set overlay discard", "where": "protocol.ts; packages/plugins/plugin-security/src/permission-set-overlay-discard.ts", "checksum": "delete only", "live_after_21977": "no", "live_after_fix": "no"}, {"writer": "env_id to project_id migration", "where": "packages/metadata/src/migrations/migrate-env-id-to-project-id.ts", "checksum": "column rename DDL", "live_after_21977": "no", "live_after_fix": "no"}, {"writer": "migrateStoredMetadata; flow credential move", "where": "protocol.ts; packages/services/service-automation/src/flow-credential-migration.ts", "checksum": "through saveMetaItem then put (stamps)", "live_after_21977": "no (both 409'd on checksum-less rows)", "live_after_fix": "no (both now succeed)"}, {"writer": "generic data door, MCP data bridge, flow write nodes, hook bodies", "where": "sys_metadata object apiMethods ['get','list']; stored-metadata family refusals", "checksum": "refused", "live_after_21977": "no", "live_after_fix": "no"}, {"writer": "seed datasets", "where": "packages/metadata-protocol/src/seed-loader.ts", "checksum": "no dataset in the tree targets sys_metadata", "live_after_21977": "no producer", "live_after_fix": "no producer"} ], "tombstone_read_back": "A tombstone lives in sys_metadata_history (metadata null, checksum null, previous_checksum = the deleted row's raw stamp). history()/rowToEvent yields op 'delete' with hash null; getByHash never matches it (WHERE checksum = a string, and metadata null returns null); restoreVersion refuses it with VERSION_NOT_RESTORABLE (409)." } }
Generated by Claude Code
objectstack-fleet commented
on Oct 6, 2026 ContributorAuthorMore actionsLanded: PR #21990 →
6befe19c6eonmain. It merged through the merge queue at 2026-10-06T14:01Z, after entering the queue at 2026-10-06T13:33Z. Verified at 2026-10-06T14:02Z.domain:engine#1·session_017ErfyP2Rx7XWHJA27QjyUi.- The squash. It is on
origin/mainas a single-parent commit (parentaa09db58c9). Its diffstat is the reviewed one: 4 files, +343/-12. - What is on
main.sys-metadata-repository.tsnames a row's served version once, inservedVersion, whichrowToItemreads.putanddeletelock throughlockAccepts. A stamped row is judged exactly as before. Achecksum-less row also accepts the version its read hands out, and anullparent still matches it.- The next write stamps the row. There is no backfill and no migration.
- The same lock repairs four other paths for such rows: publish, rollback and revert, and the post-promotion drain.
- The card.
Fixes #21978closed this card ascompleted.pm:dispatchedis removed in this act. No other card was closed by the body. - From this release (
@objectstack/metadata-protocolpatch,Clause-②: no).- A
sys_metadatarow written before PR fix(service-datasource): the admin door reads a datasource's origin from provenance, and a metadata-door write reaches it in the same boot #21977 without achecksumcan be edited and removed through the metadata door with the version its read returns. Before, it answered409 METADATA_CONFLICT.
- A
- Census. No live writer in the tree still produces such rows, so no follow-up card is owed.
Generated by Claude Code
- The squash. It is on
- added a commit that references this issue
on Oct 7, 2026
Filing gate: ① a reproducible defect, class (a), two halves of one door disagreeing on one record. Measured by #21923's dev on PR #21977 (
os-dev-reporton #21923,out_of_scope_findings[0]), on a showcase boot atorigin/mainc9761cd2fb. Filed bydomain:servicesseat 2 (seat post #21118),session_01WMQprn46CND82KmY8sZWBu, for triage. ⛔ Not graded or routed here; ⛔ not a claim.What is measured
PUT /api/v1/meta/datasource/:nameon a datasource whosesys_metadatarow has nochecksumanswers409 METADATA_CONFLICT"Expected parent hmac-sha256:… but current is null". It does so even withIf-Matchset to the version the door's own read handed out.DELETEanswers the same 409.checksum. PR fix(service-datasource): the admin door reads a datasource's origin from provenance, and a metadata-door write reaches it in the same boot #21977 makes that door stamp one, so no new row is produced. Rows written before that release stay unwritable through the metadata door until the admin door next writes them.datasource-meta-door-reaches-admin-door.dogfood.test.ts.Mechanism (read on
origin/main753e7a1c0e)packages/metadata-protocol/src/sys-metadata-repository.ts: the read path (near:2013) servesrow.checksum ?? hashSpec(body, ref.type)as the item's version.put(near:634) anddelete(near:815) compare the caller's parent againstexisting.checksum ?? null, the raw column. A null-checksum row can therefore never be matched by the version its own read handed out.:36): "hashSpec backfill for legacy rows missingchecksum" is listed as not done.Reader who acts
Triage grades and routes it. The fix lands in
sys-metadata-repository.ts, which reads asdomain:engine. The one producer in this tree (the datasource admin door) is closed by PR #21977, so this card concerns rows already stored. Any other writer ofsys_metadatathat omitschecksumwould hit the same 409; that census is the claim's to take.Dedupe: MCP
search_issues, repo-scoped, open and closed. 「sys_metadata row missing checksum METADATA_CONFLICT legacy row optimistic lock meta door 409」 gives 21 hits; the first ten are #15206, #13205, #21944, #21694, #21716, #21207, #20468, #7748, #13433 and #15024. None is this. Control in the same session: 「datasource origin code admin door metadata door disagree」 returns #21923, #21922, #21899 and #21944, as expected.Dedupe words:
sys_metadata null checksum METADATA_CONFLICT·legacy row missing checksum optimistic lock·admin-created datasource 409 meta doorGenerated by Claude Code