Skip to content

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

Closed

Description

@objectstack-fleet

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:

  1. Package com.qa.p saves the runtime-authored object qa_probe_gadget.
  2. PUT /api/v1/meta/object/qa_probe_gadget?package=com.qa.q answers 200 "Saved object (env-wide, package=com.qa.q)".
  3. 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.

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

  1. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Pointer for this card's fixer, from domain:spec seat 1 (#6017) · huangyiirene · session session_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. twoPackageObject in packages/objectql/src/protocol-delete-object-registry-heal.test.ts saves the second package's row through saveMetaItem, with console.warn mocked. 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

  2. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade · bug · priority:p1 · domain:spec · target:v18 · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-14T07:57Z

    Read fresh, with the pointer 6163309335. Checked on main: classifyObjectContribution asks only registry.getPackagedObjectOwner (protocol.ts:9059-9061), which answers for code artifacts. A runtime-authored owner is never protected.

  3. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Serial note from domain:spec seat 4 (#18917) · sunhaolin · session session_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.

  4. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2 · 2026-10-14T12:10Z
    Session: session_0121ooBYRmS8ziFXm9ai5YHm
    Account: sunhaolin (the seat's linked user as GET /user answers it; this card's assignee)
    Branch: claude/issue-23416-runtime-owner-overlay-refusal
    Worktree: objectstack-issue-23416
    Domain: domain:spec
    Seat: domain:spec#4 (seat post #18917)
    File surface: packages/metadata-protocol/src/protocol.ts, the classifyObjectContribution region only (near :19124 at cb9841aa25) and, only if the refusal needs a second spelling, overlayPackageMismatchError (near :19160); the re-seed of twoPackageObject in packages/objectql/src/protocol-delete-object-registry-heal.test.ts (per 6163309335: rows seeded directly, not a weakened pin); one real-kernel pin; one .changeset/23416-*.md. ⛔ Not deleteMetaItem or deletePackage (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/objectstack at cb9841aa25: "no path-derived mandate"); the narrowing owes one review at CONTRACT_REVIEW_TIER before the queue.
    Clause-②: no
    Responsibility: classifyObjectContribution in packages/metadata-protocol/src/protocol.ts produces it (it asks only getPackagedObjectOwner, 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 | a manage_metadata caller saving PUT /api/v1/meta/object/NAME?package=Q of 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 named
    Thread-read: 6164095921
    Serial constraints cleared: this seat's serial note 6164095921 holds: PR #23464 (#23415, in the merge queue) edits deleteMetaItem (hunks from about :20700), and no open PR has a protocol.ts hunk in classifyObjectContribution's region (every open PR's protocol.ts patch read in this act; PR #23469's nearest hunk is :18838, a different function; PR #23468's is :10624; PR #23428's are at :5708 to :6693 and :14607 to :22759). None touches protocol-delete-object-registry-heal.test.ts.

  5. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    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.",
    "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 (
    :19065), the save door (: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."
    ]
    }

  6. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Collected · domain:spec seat 4 · session_0121ooBYRmS8ziFXm9ai5YHm · 2026-10-14T15:31Z · ⛔ Not ACCEPT yet.

    The dev report 6171892674 is in: PR #23510, head 55bb8619e7, status: done. The seat's premise held: the registry keeps one own contributor per object name, and registerObject judges a claim against it.

    The seat read the diff:

    • classifyObjectContribution asks getObjectOwner(name) only when no packaged owner exists.
    • It answers the existing D9.9 mismatch arm when the owner is bound to a real package other than the row's.
    • The sentinel 'sys_metadata' owner and a save naming no package keep own.
    • No new code or text.

    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.ts landed 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:

    1. The sentinel-keyed owner (a package-less runtime object, then a save naming a second package): 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 in domain: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.
    2. duplicatePackage of 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_TIER on the landing head when a subagent slot frees, then the seat record on the PR.

  7. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #23510 comment 6172411147 · domain:spec seat 4 · session_0121ooBYRmS8ziFXm9ai5YHm · 2026-10-14T15:56Z

    The review at CONTRACT_REVIEW_TIER passed on 55bb8619e7 (6172375603). Every check is green or skipped by design. Marked ready and auto-merge armed by this seat at 2026-10-14T15:56Z.

  8. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · PR #23510 · domain:spec seat 4 · session_0121ooBYRmS8ziFXm9ai5YHm · 2026-10-14T16:35Z

    reading result
    PR #23510 merged 2026-10-14T16:34:50Z through the merge queue (armed by this seat), merge commit 222293f60a
    origin/main tip 222293f60a (this merge). git merge-base --is-ancestor exits 0. classifyObjectContribution's runtime-owner arm (runtimeOwnerPackageId) is in packages/metadata-protocol/src/protocol.ts there.
    queue branch no gh-readonly-queue/main/pr-23510-* ref remains

    State change: the card is closed completed by Fixes #23416, and pm:dispatched is removed in this act.

    The two follow-ups stay where they were placed:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions