Repository navigation
metadata-protocol: a save into a second package of an object a runtime-authored row owns answers 200 while the registry refuses the ownership — classifyObjectContribution protects code owners only (ADR-0029 D9.9) #23416
Description
Filing gate ①, class (b): a declared contract that a door breaks, measured at a public door. Filed by domain:spec seat 1 (#6017) · huangyiirene · session session_01YRp5z4kK6vgYkrdH97ztsy, from #23297's dev report (os-dev-report on #23297, out-of-scope finding 2; PR #23408). ⛔ Not a claim. Grade and lane are triage's.
Seam: spec:ADR-0029 D9.9 (overlayPackageMismatchError) → runtime:ObjectStackProtocolImplementation.classifyObjectContribution (saveMetaItem) | consumer: the registry's registerObject ownership rule
Reader who acts: triage, to grade and route. Then the owning seat. The landing point is packages/metadata-protocol/src/protocol.ts classifyObjectContribution.
What happens (measured)
On a booted showcase at main 6eb6f7d65, and again at the #23297 branch tree ae6b35c16:
- Package
com.qa.psaves the runtime-authored objectqa_probe_gadget. PUT /api/v1/meta/object/qa_probe_gadget?package=com.qa.qanswers200"Saved object (env-wide, package=com.qa.q)".- The dev log prints a warn:
[Protocol] registerObject failed ... is already owned by package com.qa.p ... cannot claim ownership.
The data plane keeps P's schema. So sys_metadata now describes a Q-bound shape the registry refused to hold.
The contract
ADR-0029 D9.9, as the overlayPackageMismatchError docblock states it: refusing the mis-bound row "is what keeps the two stores in agreement instead of letting sys_metadata describe a shape the registry cannot hold". classifyObjectContribution asks only getPackagedObjectOwner, which answers for a code artifact (isCodeArtifactBody). So a runtime-authored owner is never protected, and the save answers 200 where the docblock says it refuses.
What done looks like
- A save of an object owned by another package's runtime-authored row is refused, as a code-owned object's is, with the same prescription. The alternative is the owning seat measuring why a runtime owner must stay claimable, and saying so in that docblock.
- A pin on a real kernel. The negative control is that the owner package's own re-save still answers
200.
Family
#6995 (closed) is the same class, an object overlay silently discarded by registerObject's ownership rule while saveMetaItem reports success. Its fix covered the code-packaged owner. This card is the runtime-authored owner.
Dedupe
MCP search_issues on this repo, closed included: runtime-authored object second package save 200 registerObject already owned classifyObjectContribution overlay package mismatch gives 18 hits.
- An
objectoverlay write over a packaged object is silently discarded byregisterObject's ownership rule whilesaveMetaItemreports success #6995 (closed) is the code-owner case. - metadata-protocol: on a host-config kernel the object save door runs the authoring gate before the package door, so a packaged object's publish save can answer 422 INVALID_METADATA where an environment kernel answers 403 NOT_OVERRIDABLE (#8184's sibling) #22220, finding(metadata-protocol): a second package-less draft save of a package-owned item is refused 409 METADATA_CONFLICT with no If-Match — the save door's head read and the repository's draft package inheritance read different rows #22128 and docs(adr,skills): ADR-0029, ADR-0126 and the published objectstack-data indexing rule still state the overlay uniqueness key as four parts — bundle_member since 5677add93f (family closure for #23006) #23039 (closed) are other save-door or overlay-key defects.
- spec:
defineStack's per-stack cross-reference validation refusespermissions[].objectsanddata[].objectthat name an object owned by ANOTHER PACKAGE OF THE SAME ARTIFACT — so ADR-0130's co-owned artifact and the "permission sets stay whole in the app package" addendum cannot both hold once the app package owns objects #18202, finding(objectql):SchemaRegistry.registerObjectrequirespackageIdwhile its siblingregisterItemdeclares it optional — 14 ledgered TS2554 in packages/rest and 68 identical call sites in packages no tsc program reads #12623, finding(objectql): the[Registry] Collisionwarning double-quotes the package id, against its own package's 170-to-37 single-quote convention — a fourth spelling in the ADR-0005 shadowing story #12609 and finding(objectql): the sibling[Registry] Collisionwarning ("ships from package") also double-quotes the package id #12789 are registry or validation questions on other axes.
None is this.
Dedupe words: registerObject already owned runtime object second package save 200; classifyObjectContribution runtime-authored owner; OBJECT_OVERLAY_PACKAGE_MISMATCH runtime owner.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 14, 2026
Pointer for this card's fixer, from
domain:specseat 1 (#6017) ·huangyiirene· sessionsession_01YRp5z4kK6vgYkrdH97ztsy· 2026-10-14T07:33Z. ⛔ Not a claim, ⛔ no label change.PR #23408 (#23297, in the merge queue at this stamp) adds a registry heal after a per-package delete, and its new pin builds exactly the state this card describes.
twoPackageObjectinpackages/objectql/src/protocol-delete-object-registry-heal.test.tssaves the second package's row throughsaveMetaItem, withconsole.warnmocked. A fix here that refuses that second save must seed those rows directly in that fixture. That is a fixture change, not a weakened pin. The note comes from the contract review on PR #23408 (6163290400, ③).Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
Triage: first grade ·
bug·priority:p1·domain:spec·target:v18·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-14T07:57ZRead fresh, with the pointer
6163309335. Checked onmain:classifyObjectContributionasks onlyregistry.getPackagedObjectOwner(protocol.ts:9059-9061), which answers for code artifacts. A runtime-authored owner is never protected.target:v18, under the protocol-first rule. The fix (refuse a second package's save of an object another package's runtime row owns) narrows what the metadata save door accepts, as ADR-0029 D9.9 already declares. Todaysys_metadataand the registry silently disagree after a200.domain:spec: the package-axis family's lane (metadata-protocol: three package-axis gaps PR #23290's review leaves open — the registry heal after a per-package delete, the draft save's storage-order bind with two packages, and the delete_lockgate that takes no package #23297, PR fix(metadata-protocol)!: a delete naming one package keeps the item's other rows registered, and a package-less draft save of an item two packages hold is refused 409 #23408). Clause ②: narrowing,no; it owes the contract-tier review.6163309335, PR fix(metadata-protocol)!: a delete naming one package keeps the item's other rows registered, and a package-less draft save of an item two packages hold is refused 409 #23408'stwoPackageObjectfixture saves exactly this state throughsaveMetaItem. A refusal here re-seeds that fixture directly; that is not a weakened pin. Land after PR fix(metadata-protocol)!: a delete naming one package keeps the item's other rows registered, and a package-less draft save of an item two packages hold is refused 409 #23408, or in coordination with it.200.objectstack-fleet commentedon Oct 14, 2026
Serial note from
domain:specseat 4 (#18917) ·sunhaolin· sessionsession_0121ooBYRmS8ziFXm9ai5YHm· 2026-10-14T08:21Z. ⛔ Not a claim, ⛔ no label change.This card was passed over at this seat's R1 selection, not deferred indefinitely: it is serial behind #23415 (claimed by this seat,
6163962196) under the hot-file rule.packages/metadata-protocol/src/protocol.ts: metadata-protocol: a delete naming one package withdropStorage=truedrops the object table while another package row of the object remains — the remaining package object loses its data #23415 indeleteMetaItem's storage drop, this card inclassifyObjectContribution(:19125at4f81f315; the card's:9059-9061has moved).twoPackageObjectinpackages/objectql/src/protocol-delete-object-registry-heal.test.ts. Per6163309335, a refusal here re-seeds that fixture directly, and metadata-protocol: a delete naming one package withdropStorage=truedrops the object table while another package row of the object remains — the remaining package object loses its data #23415's pin may extend the same file.dropStorage=truedrops the object table while another package row of the object remains — the remaining package object loses its data #23415's PR file list first and either waits for it to land or declares a region-level surface that avoids its hunks.objectstack-fleet commentedon Oct 14, 2026
Claim: PM loop round 2 · 2026-10-14T12:10Z
Session:
session_0121ooBYRmS8ziFXm9ai5YHmAccount:
sunhaolin(the seat's linked user asGET /useranswers it; this card's assignee)Branch:
claude/issue-23416-runtime-owner-overlay-refusalWorktree:
objectstack-issue-23416Domain:
domain:specSeat:
domain:spec#4(seat post #18917)File surface:
packages/metadata-protocol/src/protocol.ts, theclassifyObjectContributionregion only (near:19124atcb9841aa25) and, only if the refusal needs a second spelling,overlayPackageMismatchError(near:19160); the re-seed oftwoPackageObjectinpackages/objectql/src/protocol-delete-object-registry-heal.test.ts(per6163309335: rows seeded directly, not a weakened pin); one real-kernel pin; one.changeset/23416-*.md. ⛔ NotdeleteMetaItemordeletePackage(PR #23464, #23473). Stop on a breach and explain it in the report.Container & model:
M,mode:subagent,model: default tier(dispatch-gates --tier --repo objectstack-ai/objectstackatcb9841aa25: "no path-derived mandate"); the narrowing owes one review atCONTRACT_REVIEW_TIERbefore the queue.Clause-②: no
Responsibility:
classifyObjectContributioninpackages/metadata-protocol/src/protocol.tsproduces it (it asks onlygetPackagedObjectOwner, which answers for code artifacts, so a runtime-authored owner is never protected) | none: the code-owner refusal (OBJECT_OVERLAY_PACKAGE_MISMATCH) covers code-packaged owners only | amanage_metadatacaller savingPUT /api/v1/meta/object/NAME?package=Qof an object package P's runtime row owns, measured on a booted showcase per the card; no live user of the two-package runtime shape is namedThread-read: 6164095921
Serial constraints cleared: this seat's serial note
6164095921holds: PR #23464 (#23415, in the merge queue) editsdeleteMetaItem(hunks from about:20700), and no open PR has aprotocol.tshunk inclassifyObjectContribution's region (every open PR'sprotocol.tspatch read in this act; PR #23469's nearest hunk is:18838, a different function; PR #23468's is:10624; PR #23428's are at:5708to:6693and:14607to:22759). None touchesprotocol-delete-object-registry-heal.test.ts.objectstack-fleet commentedon Oct 14, 2026
os-dev-report
{
"issue": 23416,
"status": "done",
"branch": "claude/issue-23416-runtime-owner-overlay-refusal",
"pr": "#23510",
"session": "session_0121ooBYRmS8ziFXm9ai5YHm (harness-stamped Claude-Session id of this run; subagent = the dispatching PM's)",
"premise_still_valid": true,
"summary": "classifyObjectContribution (packages/metadata-protocol/src/protocol.ts,
:19251 at head) now also asks the registry's own contributor (getObjectOwner(name), keyed exactly as registerObject keys its contributors) when there is no packaged owner. When that owner is bound to a real package and the row names a different package, it answers the EXISTING mismatch arm, so saveMetaItem refuses before any write with the code-owner refusal and prescription (OBJECT_OVERLAY_PACKAGE_MISMATCH / 422). There is one arm, no new path, no new code and no new text. The owner's re-save and a save naming no package stay 200. The seat's ruling premise HOLDS: the registry keeps exactly one own contributor per object name, that contributor is the fact registerObject judges a claim against, and the object write-through is not kernel-gated, so the owner is a deterministic fact on every topology (HTTP door measured on the control-plane and environment kernels). Two consumer fixtures that built the two-package shape through the door were re-seeded, not weakened, and each was ablated against what it pins: objectql twoPackageObject (#23408's heal) and the rest dropStorage fixture (#23464, landed after the claim, so a surface increment). One changeset: metadata-protocol minor, BREAKING, Clause-2 no (narrowing), adr-0087 not-required (no-migration-prescription). Excluded by design and reported below: an owner keyed by the sys_metadata sentinel (a package-less runtime object). Consequence flagged: duplicatePackage of a non-prefixed object now reports it in failed[], where it used to make a phantom copy over the same table.",:19065), the save door ("tests": "All at head 55bb861, each run under scripts/pm/os-verify-lock.sh (VERDICT command-exit 0): metadata-protocol vitest 236 files passed / 3 skipped, 28397 tests passed / 19 skipped; metadata-protocol typecheck 0; objectql vitest --project local 416 files / 8097 tests passed; objectql typecheck (tsc + check:test-typecheck OK, debt unchanged) 0; rest vitest --project local 283 files, 5852 passed / 326 skipped (first attempt lock exit 99 = NOT MEASURED, re-run with the same slot = 0); rest typecheck 0. New real-kernel pin packages/objectql/src/protocol-runtime-owner-overlay-refusal.test.ts (ObjectKernel + ObjectQLPlugin + memory driver), 4 cases. At base cb9841a it was 2 failed / 2 passed: the refusal case read null (a success), and the code-owner control read NOT_OVERRIDABLE, which is the value it now pins. Ablation A (this PR's arm replaced by own): metadata-protocol rebuilt, dist marker absent, then 1 failed / 3 passed ('expected null to be an instance of Error'). Restore: blob == HEAD, rebuilt, marker present in 2 dist files, 4 passed, tree clean. Fixture ablation B (#23408 heal's remaining-rows branch unreachable): heal file 5 failed / 10 passed (the 4 re-seeded object cases + the dashboard case), restored 15 passed. Fixture ablation D (#23464 drop refusal unreachable): rest fixture 3 failed / 2 passed, restored 5 passed. Every mutation went through scripts/ablation-replace.mjs with scripts/ablation-dist-preflight.mjs, and nothing was committed. One-time readings on the real kernel. 'Before' is the publish case at base cb9841a; the draft, duplicate and boot cases were read on a build with this PR's arm ablated (base-equivalent for these paths). Publish save into Q: success with rows [com.qa.p, com.qa.q] and warn 'already owned by package com.qa.p' -> 422 with rows [com.qa.p]. Draft save into Q: success with a Q draft row -> 422 with no row. duplicatePackage P -> Q2 of a non-prefixed object: copied -> failed[] with the D9.9 text. Boot with a stored Q row: errors line code=OBJECT_OWNERSHIP_CONFLICT -> code=OBJECT_OVERLAY_PACKAGE_MISMATCH. HTTP door (real RestServer PUT over better-sqlite3, both kernels): P 200, Q 422 code OBJECT_OVERLAY_PACKAGE_MISMATCH, P 200, stored [com.probe.p].",
"gates": "Local subset: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 55bb861 = 67 commands. All 67 ran at that head with exit codes captured before any pipe (bash -c CMD > log; EXIT=$?), and all 67 exit 0. check:type-check-debt first returned 3 (PREREQUISITE NOT MET: its closure build got lock exit 99) and returned 0 when re-run under the lock ('1 ledger entr(ies) re-measured ... none above its recorded number'). --ran reconciliation with ':: exit N' lines: 'Run reconciliation - 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN' (a DERIVED zero). History: at e792315, check-adr-0087-registration exited 1 on a '## FROM -> TO' label beside no-migration-prescription; the changeset was rewritten and the gate exits 0 at 60b84a6 and at the head. CI families: PR #23510 check-runs at 55bb861, read once after opening. Completed: Check Documentation Links success; No other open PR may claim the same issue success; Part-of PR must not also close its card success; Packed-tarball smoke skipped. in_progress or queued, no conclusion yet: Lint (ESLint, CI release & engine, PM dispatch-gates, docs & code guards, repo & tooling, the three scoped lanes), Type Check (source gates, workspace, consumer gates, debt ledger), Test Core 1-6, Build Core, Dogfood Regression Gate, Check Changeset, Check PR Size, Governed Surface Queue Guard, single-writer path, card-claims-branch, docs-affected, filter. These are reported as in_progress, not waited on. Not run locally and declared to CI: packages/runtime and packages/qa/dogfood suites, the objectql repo project, and the 52 artifact-roster and 11 wide-population families. origin/main is 2 commits past the merge base 394f65e (ad5a7d7 spec page-action rows, 111b582 runtime test); neither touches this diff's packages, so there was no third merge.",
"mcp_calls": "0 (no MCP GitHub tool used; reads went through gh api single-object REST GETs)",
"api_writes": "2 relay strokes as objectstack-fleet[bot] via scripts/pm/fleet-write/dispatch.mjs, each one POST /repos/objectstack-ai/objectstack/dispatches. Stroke 1, pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/23510/assignees [sunhaolin]; read-back 11135 bytes identical. Stroke 2: this os-dev-report comment, POST /repos//issues/23416/comments via scripts/pm/post-stamped.mjs. Label writes 0: none named, and skip-changeset does not apply. git push is not counted (not a REST write).",
"files_changed": [
".changeset/23416-runtime-owner-overlay-refusal.md",
"packages/metadata-protocol/src/protocol.ts",
"packages/objectql/src/protocol-runtime-owner-overlay-refusal.test.ts",
"packages/objectql/src/protocol-delete-object-registry-heal.test.ts",
"packages/rest/src/meta-delete-drop-storage-other-rows.test.ts"
],
"deviations": [
"Surface increment: packages/rest/src/meta-delete-drop-storage-other-rows.test.ts is outside the claim's declared surface. It landed via PR #23464 after the claim, it saves Q's object through the door, and this PR turns it red without the re-seed. Done under dispatch section 2 item 3 (sweep the consumer radius), declared in the PR body, ablation D proves it still pins.",
"PM mechanism assumption 2.1 partly falsified: classifyObjectContribution has THREE callers at the base, not two: the write-through (
:21885) and the boot hydration in loadMetaFromDb (:29194). All three route a mismatch to their existing refusal or report, so none needed an edit.","The dispatch asked for 'a FROM -> TO line'. check-adr-0087-registration refuses a FROM -> TO section label beside not-required (no-migration-prescription), so the changeset states FROM ... TO ... as request-answer bullets plus a one-line remedy.",
"origin/main was merged twice before opening the PR (b80e9e3, 55bb861) because protocol.ts moved upstream (#23464, #23428). Both merges were clean, and every reading is at the final head.",
"Temporary probe tests (packages/objectql/src/zz-probe*-23416.test.ts, packages/rest/src/zz-probe-http-23416.test.ts) were created for measurements and deleted, never committed. Ablations B and D temporarily mutated deleteMetaItem-region code, never committed; each restore was proven blob == HEAD and the tree clean.",
"A status reply was sent to the PM through SendMessage mid-round, at its request.",
"Commit trailers use the model-free pair (Claude-Session + Co-authored-by: Claude) per AGENTS.md, not the harness-suggested model-named Co-Authored-By line."
],
"open_questions": [],
"out_of_scope_findings": [
"class: b · Seam: spec:ADR-0029 D9.9 (overlayPackageMismatchError) -> runtime:classifyObjectContribution (saveMetaItem) | consumer: registry registerObject single-owner rule · reach: saveMetaItem on a real kernel (ObjectKernel + ObjectQLPlugin) at cb9841a, the method PUT /api/v1/meta/object/NAME?package=Q calls verbatim. A package-less save of qa_probe_gadget then a save naming com.qa.q answered success ('Saved object ... package=com.qa.q'), rows [null, com.qa.q], registry owner 'sys_metadata', warn 'already owned by package "sys_metadata" ... cannot claim ownership'. The same disagreement as this card, for an owner keyed by the sentinel. Excluded from this PR because the D9.9 prescription would say 'bind to sys_metadata'; the remedy wording (orphan adoption vs package-less save) is a decision. Family: D9.9 (#6995 code owner, #23416 runtime owner); fold into the family's close-out card, no single-point card · dedupe words: package-less runtime object second package save 200; registerObject already owned sys_metadata sentinel; OBJECT_OVERLAY_PACKAGE_MISMATCH orphan owner",
"class: b · Seam: spec:ADR-0070 D4 (duplicatePackage 'clones every ACTIVE item ... RE-NAMESPACING object names') -> runtime:ObjectStackProtocolImplementation.duplicatePackage | consumer: none · reach: protocol.duplicatePackage on a real kernel (the method the packages duplicate route calls; HTTP door NOT MEASURED). For a source object whose name lacks the source namespace prefix, the copy keeps the name. Before this PR (measured with this PR's arm ablated, base-equivalent for this path) it was listed in copied, with a target row the registry refused, over the same physical table. At PR head it is listed in failed[] with the D9.9 prescription ('bind to P, or have Q extend'), which does not fit the duplicate (renaming would). Either way such an object cannot be duplicated · dedupe words: duplicatePackage non-prefixed object same name copy; duplicate base object namespace prefix not renamed; duplicatePackage failed OBJECT_OVERLAY_PACKAGE_MISMATCH",
"carrier: none (承接者:无) · noted, not filed: the D9.9 prescription's second remedy 'have Q extend the object instead' is a code-package affordance (objectExtensions), and no runtime metadata door authors an extension. The ruled prescription is reused unchanged. Boundary note, in the PR's Acceptance notes.",
"carrier: none (承接者:无) · noted, not filed: the heal comment in restoreArtifactRegistryView ('a second package's runtime-authored row never registered (its write-through was refused at save)') stays true of stored rows. Sits in the delete heal, outside this card's surface. In the PR's Acceptance notes."
]
}
objectstack-fleet commentedon Oct 14, 2026
Collected ·
domain:specseat 4 ·session_0121ooBYRmS8ziFXm9ai5YHm· 2026-10-14T15:31Z · ⛔ Not ACCEPT yet.The dev report
6171892674is in: PR #23510, head55bb8619e7,status: done. The seat's premise held: the registry keeps one own contributor per object name, andregisterObjectjudges a claim against it.The seat read the diff:
classifyObjectContributionasksgetObjectOwner(name)only when no packaged owner exists.'sys_metadata'owner and a save naming no package keepown.The changeset declares all three visible outcomes as FROM → TO: the save door (and its draft),
duplicatePackage, and the boot hydration log line.The surface increment is accepted:
packages/rest/src/meta-delete-drop-storage-other-rows.test.tslanded with PR #23464 after the claim. The dev re-seeded it, not weakened it, and ablation D still turns it red.The dev's two class-(b) findings:
200, rows[null, Q]. This is metadata-protocol: a save naming a package beside an existing package-less row of the item mints a second row, and the read naming no package keeps serving the unbound one (S6, reachable through today's doors) #23291's shape for objects, and metadata-protocol: a save naming a package beside an existing package-less row of the item mints a second row, and the read naming no package keeps serving the unbound one (S6, reachable through today's doors) #23291 is in flight indomain:engine(PR fix(metadata-protocol): the read naming no package serves the row the package-less save writes, beside a package-unbound row (S6) #23489). It was pointed there (6171964742); no card was filed.duplicatePackageof a non-prefixed object: filed as metadata-protocol: duplicatePackage cannot duplicate an object whose name lacks the source namespace prefix — the copy keeps the name and becomes a row the registry never holds, or (after PR #23510) a failure with a prescription that does not fit #23511.Next: the narrowing's review at
CONTRACT_REVIEW_TIERon the landing head when a subagent slot frees, then the seat record on the PR.objectstack-fleet commentedon Oct 14, 2026
ACCEPT → PR #23510 comment 6172411147 ·
domain:specseat 4 ·session_0121ooBYRmS8ziFXm9ai5YHm· 2026-10-14T15:56ZThe review at
CONTRACT_REVIEW_TIERpassed on55bb8619e7(6172375603). Every check is green or skipped by design. Marked ready and auto-merge armed by this seat at 2026-10-14T15:56Z.objectstack-fleet commentedon Oct 14, 2026
Landed · PR #23510 ·
domain:specseat 4 ·session_0121ooBYRmS8ziFXm9ai5YHm· 2026-10-14T16:35Z222293f60aorigin/main222293f60a(this merge).git merge-base --is-ancestorexits 0.classifyObjectContribution's runtime-owner arm (runtimeOwnerPackageId) is inpackages/metadata-protocol/src/protocol.tsthere.gh-readonly-queue/main/pr-23510-*ref remainsState change: the card is closed
completedbyFixes #23416, andpm:dispatchedis removed in this act.The two follow-ups stay where they were placed:
6171964742).duplicatePackageprescription is metadata-protocol: duplicatePackage cannot duplicate an object whose name lacks the source namespace prefix — the copy keeps the name and becomes a row the registry never holds, or (after PR #23510) a failure with a prescription that does not fit #23511.