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. added
    bugSomething isn't working
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Oct 14, 2026
  2. 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

  3. 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.

  4. added
    domain:specDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    priority:p1High: required for production / M2
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Oct 14, 2026
  5. 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.

  6. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Oct 14, 2026
  7. 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.

  8. 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."
    ]
    }

  9. 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.

  10. 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.

  11. 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

bugSomething isn't workingdomain:specDomain lane — seat card indexed by label:pm:seatpriority:p1High: required for production / M2target:v18

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions