Repository navigation
metadata-protocol: an adopt-orphans into a package rebinds a runtime object's row in storage, but the registry keeps the sys_metadata owner and the old definition until a restart (ADR-0070 D5) #23646
Description
Filing gate: ① a product defect with a named producer, finding class (a), from #23514's dev report 6188859508 (reading R5, finding F2). Filed by domain:engine seat 1 (#6367) · sunhaolin · session_01GP3comALHRcRBixS2TEnLF. ⛔ Not a claim. Reader who acts: triage, to grade and route (the producer is in packages/metadata-protocol).
reach: reassignOrphanedMetadata, the method POST /api/v1/packages/:id/adopt-orphans (ADR-0070 D5) calls. It was walked at protocol level on a real ObjectKernel + MetadataPlugin + ObjectQLPlugin at origin/main 23e689dc7d.
- The HTTP route was not walked. In a minimal dispatcher harness without the package service it answered
503 SERVICE_UNAVAILABLE, "Package service not available". - The claimant walks it once over HTTP on a full composition before fixing. That is the first acceptance line.
What happens
- An environment holds a runtime object whose only row is package-less, so its registry owner is the
sys_metadatasentinel. - Adopting orphans into package Q answers
count=1, and the row is rebound to Q in storage. - Yet until the next restart:
- the registry owner stays
sys_metadata; - the served definition is the pre-adoption one.
- Meanwhile every save of the object naming Q answers 200 without going live (objectql/metadata-protocol: a save naming a package of a runtime object the registry holds under the sys_metadata sentinel answers 200, but the registry keeps the package-less definition, so the saved row never goes live #23514's state).
- the registry owner stays
- After a restart the owner is Q.
Mechanism (the dev's reading): the adoption's registry write-through covers views only, by its own comment. It has no object limb.
Wanted
- An adoption leaves the registry agreeing with storage for every adopted object, in the same request, with no restart: owner Q, and Q's definition served.
- Pins:
- adopt, then read the owner and the served definition;
- a save naming Q after adoption goes live;
- a boot after that agrees;
- a negative control that goes red when the fix is removed.
Related, not this card
- objectql/metadata-protocol: a save naming a package of a runtime object the registry holds under the sys_metadata sentinel answers 200, but the registry keeps the package-less definition, so the saved row never goes live #23514: the object save door beside a sentinel owner. It is blocked on the delete heal, not on this card.
- That card's measured alternative B, a registry owner re-key, would share a primitive with this fix. Read objectql/metadata-protocol: a save naming a package of a runtime object the registry holds under the sys_metadata sentinel answers 200, but the registry keeps the package-less definition, so the saved row never goes live #23514's thread before choosing the fix's shape.
Dedupe
MCP search_issues on this repo, closed included: "adopt-orphans reassignOrphanedMetadata object registry owner not updated until restart write-through views only" returns 12 hits.
- The nearest are metadata-protocol: a republish of a view container that drops a member keeps serving the dropped member's expanded view until restart #23198 (closed, a dropped container member served until restart) and Package disable and uninstall never reach the metadata/data layer: a disabled package's objects still serve rows, and uninstall leaves 7 orphaned
sys_metadatarows #7557 (closed, uninstall orphans).
None is this.
Dedupe words: adopt-orphans reassignOrphanedMetadata object registry write-through; sentinel owner until restart; ADR-0070 D5 adoption not live.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 15, 2026
Triage: first grade ·
bug·priority:p2·domain:engine·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-15T09:57ZRead fresh.
reassignOrphanedMetadata(POST /api/v1/packages/:id/adopt-orphans, ADR-0070 D5) rebinds a runtime object's package-less row to package Q in storage.sys_metadatasentinel owner and the pre-adoption definition until a restart.Grade p2: an answer (
count=1) that storage and the served registry disagree on until a restart.Lane
domain:engine(packages/metadata-protocol).Order: read #23514's thread first. Its measured alternative B (a registry owner re-key) would share this fix's primitive. #23514 is
pm:blocked, so this card may land first, but if the re-key is built here, #23514's claimant reuses it, ⛔ not a second copy.Scope: the card's Wanted and its pins.
findingleaves on grading.objectstack-fleet commentedon Oct 15, 2026
Claim: PM loop round 2 · 2026-10-15T11:52Z
Session:
session_01GP3comALHRcRBixS2TEnLFAccount:
sunhaolin(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23646-adopt-orphans-object-registryWorktree:
objectstack-issue-23646Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface:
packages/metadata-protocol/src/protocol.ts, the adoption's registry write-through only (reassignOrphanedMetadataand the object limb it lacks). If the owner hand-over needs a registry verb, the fix also touchespackages/objectql'sSchemaRegistryownership (domain:engine). Plus new real-kernel pins and one changeset.changeset/23646-*.md. Stop on any other breach and explain it in the report.Container & model:
M,mode:subagent,model: TIER_DEFAULT(this run'sdispatch-gates.mjs --tier: "no path-derived mandate"; a registry write-through with an ownership hand-over to design)Clause-②: no
Responsibility: the risk is produced by
metadata-protocol's adoption door:reassignOrphanedMetadata's registry write-through covers views only, so an adopted object keeps thesys_metadatasentinel owner and its pre-adoption definition until a restart | no platform path covers it: the next boot's hydration is the only thing that re-keys it | an admin'sPOST /api/v1/packages/:id/adopt-orphans(ADR-0070 D5) reaches it. It was walked at protocol level at23e689dc7d(#23514's report 6188859508, R5). The HTTP walk is the card's first acceptance line.Thread-read: 6189424017
Serial constraints cleared: open PRs on the same file, all in the merge queue and none in this region: #23654 (#23495, the draft doors), #23659 (#23645, the code-layer read and the list merge) and #23626 (#23577, the sealed-write region). #23514 (
pm:blockedon #23645) will resume in this file's object-ownership region (classifyObjectContribution, the D9.9 call insaveMetaItem). Triage (6189424017): if a registry owner re-key is built here, #23514 reuses it, ⛔ not a second copy. This card's PR names that verb for #23514's claimant.Generated by Claude Code
test(objectql): real-kernel pins for an orphan adoption's object regi…
fix(metadata-protocol): an orphan adoption hands each adopted object'…
test(objectql): the adoption pins seed sys_metadata rows without the …
fix(metadata-protocol): the adoption registers an adopted object's AC…
test(objectql): an adopted object another package extends keeps its o…
test(objectql): the adoption pins type the data-plane read options in…
objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{
"issue": 23646,
"status": "done",
"branch": "claude/issue-23646-adopt-orphans-object-registry",
"pr": "#23688",
"session": "session_01GP3comALHRcRBixS2TEnLF — subagent run, so this is the parent PM's harness-stamped id",
"premise_still_valid": true,
"summary": "Each active object row reassignOrphanedMetadata (POST /api/v1/packages/:id/adopt-orphans, ADR-0070 D5) rebinds now hands its registry name over in the same request. A new private helper, releaseAdoptedObjectOwner, unregisters the sys_metadata sentinel's own registration through the existing SchemaRegistry.unregisterObject (the verb the delete heal already uses). The row then registers under Q through the save's own applyRegistryWriteThrough, so there is one owner throughout (ADR-0029 D3). Landing point: packages/metadata-protocol/src/protocol.ts only. No objectql registry verb was added and no re-key primitive was built, so #23514 has nothing new to reuse. The object SAVE door, classifyObjectContribution and the D9.9 refusal are untouched. Edge cases: a draft row is rebound in storage only, since the boot registers active rows only. A name another package's row owns, and an object another package extends, keep their registry owner as before; the extender case is said once at error. The HTTP route was walked first on a full composition, before and after the fix. Mechanism hypotheses: H1 confirmed (views only). H2 confirmed: the single-owner refusal, logged 'already owned by package sys_metadata'. A verb existed, so none was minted. H3: the fix reads no metadata-service copy; merged with #23659, its heal test passes. H4: the full composition answered 200; the minimal-harness 503 was not re-measured.",
"tests": "HTTP walk: pnpm dev:crm -- --fresh -p 38461, admin session, object qa_probe_gadget, Q=com.qa.q. BEFORE (origin/main 57994c3 code): PUT no package → 200 'Saved object ... (env-wide, state=active)'. adopt-orphans → 200 {reassignedCount:1}, with no registry log line. PUT ?package=com.qa.q adding field serial → 200 'Saved object ... package=com.qa.q', and the server logged '[Protocol] registerObject failed for qa_probe_gadget: Object "qa_probe_gadget" is already owned by package "sys_metadata". Package "com.qa.q" cannot claim ownership.' POST /data with serial → 400 INVALID_FIELD; GET /data?serial=S-3 → 400 INVALID_FIELD. AFTER (535cd85, metadata-protocol rebuilt): adopt logged 'Unregistered object: qa_probe_gadget (1 contribution(s), owner sys_metadata)' then 'Registered object: qa_probe_gadget (own, priority=100) from com.qa.q'. The save naming Q logged no refusal. POST /data with serial → 201; GET ?serial=S-3 → 200, total 1. PINS: packages/objectql/src/protocol-adopt-orphans-object-registry.test.ts, real ObjectKernel + ObjectQLPlugin, 6 cases: adopt (saved in-process, and hydrated at boot) → owner Q / data plane _packageId Q / one owner / getAllObjects(Q) lists it; a save naming Q goes live; a boot agrees; a draft row registers nothing; an extended object keeps its owner, one error line; CONTROL non-object adoption (no registerObject/unregisterObject call, the view follows as before); CONTROL another package's owner stays. Fixture triage: one case in protocol.object-registry-write-through-spelling.test.ts pinned the defect ('an orphaned objects row not at all'); it now pins that a stored 'objects' row registers under 'object'. LOCAL RUNS: at 7d0b4d9 (origin/main 5477020 merged): metadata-protocol typecheck exit 0; metadata-protocol test 'Test Files 236 passed | 3 skipped (239) Tests 20563 passed | 19 skipped'; objectql typecheck exit 0 ('check:test-typecheck: OK ... 39 file(s) / 233 error(s)' — the new file carries no debt); objectql protocol suites, 8 files incl. protocol-delete-boot-hydrated-object-heal, 'Tests 74 passed (74)'; runtime package-duplicate-adopt-org-scope.integration 'Tests 4 passed (4)'; suite VERDICT command-exit 0. ABLATION at final code head 3061f6f (objectql reads metadata-protocol via dist; each leg used ablation-replace + rebuild + ablation-dist-preflight). Leg A, object limb removed ('anchor 1 -> 0, blob 3a0c26243aa1 -> de375db4ccf2'): JS bundles built and DTS refused the mutant with TS2367 (the narrowed union); '✓ dist/: marker present in 2 built files'; 'Tests 3 failed | 3 passed (6)' — both adopt cases and the extender case red, controls green by design. Leg B, active gate neutralised ('3a0c26243aa1 -> 652360890e7c'): build exit 0, marker in 2 built files, 'Tests 1 failed | 5 passed (6)' — the draft case red. Restore: blob 3a0c26243aa1 == HEAD, git diff HEAD empty, rebuild exit 0, '✓ dist/: marker absent from all 24 built files' for both markers, '✓ tree: working tree clean', 'Tests 6 passed (6)', objectql typecheck exit 0; VERDICT command-exit 0. Expected direction red, observed red on both legs. The last commit 9e2c7a5 changes only the pin file (typed EngineReadOptions in place of an as-any); its gates are below.",
"gates": "Local: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 9e2c7a5 derived 67 commands. All 67 ran on that head, each exit captured before any pipe: 67 × exit 0. check:type-check-debt ran inside one os-verify-lock hold ('VERDICT command-exit 0 · held the lock 434s'). --ran reconciliation (exit 0): '✓ dispatch-gates --ran: 67 derived famil(ies) accounted for — 67 run, 0 NOT-MEASURED (a DERIVED zero ...)'. The earlier union at 3061f6f is superseded. It read check:query-options-erasure exit 1 ('test surface grew 236 → 237', the pin file's as-any on a read's options, fixed in 9e2c7a5; that ratchet now reads '✓ ... 236 site(s) ... at the ceiling') and check:type-check-debt exit 3 (PREREQUISITE NOT MET, no lock turn). Local-only families check:dev-prereqs and check:skill-frame-freshness are not owed: no packages/spec or four-axis frame file is touched. spec check:generated after the main merge: 'All 14 generated artifacts are up to date'. CI on PR head 9e2c7a5, read at report time: Check PR Size success; 'No other open PR may claim the same issue' success; 'The card this PR closes must claim this branch' success; 'Part-of PR must not also close its card' success; filter success. Lint & Repo Gates lanes (CI, release & engine; ESLint; PM dispatch-gates; docs & code guards; repo & tooling; three scoped lanes), Type Check (consumer, debt ledger, source, workspace), Check Changeset, Governed Surface Queue Guard: in_progress. Build Core, Test Core 1-6/6, Dogfood Regression Gate, Auto Label: queued. Build Docs, Console Pin Gate, Temporal Conformance, Dogfood Verify CLI, Packed-tarball smoke: skipped by their selectors. CI convergence is the PM's.",
"mcp_calls": "0",
"api_writes": "3 REST requests in 2 relay dispatches, all as objectstack-fleet[bot] through scripts/pm (fleet-write). (1) pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft #23688), then POST /repos//issues/23688/assignees (sunhaolin); the relay's read-back said '10545 byte(s) sent, 10545 stored — identical'. (2) This os-dev-report comment: POST /repos//issues/23646/comments via scripts/pm/post-stamped.mjs. Each dispatch was one POST /repos/objectstack-ai/objectstack/dispatches. No label write: the dispatch expected none, and a changeset ships, so skip-changeset does not apply. git push (branch create + 7 commits incl. one main merge) is not REST.",
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: named producer — reassignOrphanedMetadata, the method POST /api/v1/packages/:id/adopt-orphans calls, measured at protocol level on a real ObjectKernel + ObjectQLPlugin with a memory driver; its view limb is byte-identical to origin/main, and the HTTP door was not walked for this · evidence: a package-less sys_metadata VIEW row in state 'draft' is adopted (reassignedCount 1; the stored row stays state draft, now package com.qa.q). Before the adoption registry.getItem('view', NAME) answered nothing; after it the registry serves the draft body ('Draft gadgets', _packageId com.qa.q) as live. The boot registers active rows only, and applyRegistryWriteThrough's own contract says drafts must never reach the runtime. Seam: runtime:reassignOrphanedMetadata view limb → applyRegistryWriteThrough → hydrateOverlayIntoRegistry (packages/metadata-protocol/src/protocol.ts) · the object limb added by PR #23688 gates on state active and is pinned · dedupe words: adopt-orphans draft view registered live; reassignOrphanedMetadata state draft write-through; adoption leaks draft into registry",
"carrier: none · noted in PR #23688's Acceptance notes, not filed · legacy residue: a package-less object row beside package R's row of the same object is adopted into Q in storage, leaving two rows of one object bound to two packages, a shape the registry cannot hold (D9.9). The adoption leaves the registry on R, as before this PR; the next boot's verdict on the two rows was not measured."
],
"files_changed": [
"packages/metadata-protocol/src/protocol.ts (+94/-4: the object limb in reassignOrphanedMetadata's loop, releaseAdoptedObjectOwner, header comments)",
"packages/metadata-protocol/src/protocol.object-registry-write-through-spelling.test.ts (+13/-6: header trace item 6 and the adoption case re-pinned)",
"packages/objectql/src/protocol-adopt-orphans-object-registry.test.ts (new, +367, 6 real-kernel pins)",
".changeset/23646-adopt-orphans-object-registry-owner.md (patch, @objectstack/metadata-protocol; Clause-② no)"
],
"deviations": [
"Commit trailers: the harness reminder asked for a model-named Co-Authored-By; every commit carries AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude), which AGENTS.md says takes precedence. The PR body ends with AGENTS.md's session-URL footer, not the harness's form.",
"Before opening the PR, origin/main (5477020, including #23659 on protocol.ts) was merged into the branch per AGENTS.md §10. It merged clean, then the post-merge build state was refreshed, spec check:generated run and the suite re-run.",
"The first gate union, at 3061f6f, read check:query-options-erasure red: the new pin file's as-any on a find's options grew the test surface 236 → 237. 9e2c7a5 typed it, and the whole 67-command union was re-run on that head.",
"Ablation leg A's mutant build exited 1 at the DTS stage (TS2367, the mutant's narrowed union). The CJS/ESM bundles the tests consume were emitted, and the preflight proved the marker in them, so the leg is a valid measurement.",
"A throwaway probe file (packages/objectql/src/zz-probe-23646-view-draft.test.ts) measured the draft-view finding. It was deleted before any commit and never pushed.",
"A shell-scoping slip wrote one pid file to /test1.pid; it was moved into the issue-23646 scratchpad. Two other root-level pid files there (/mp-suite.pid, /mp-suite2.pid) are not this run's and were left alone.",
"Two dev servers were started on port 38461 (pre- and post-fix walks), each stopped by its own listener PID; the port is free."
]
}
objectstack-fleet commentedon Oct 15, 2026
ACCEPT → PR #23688 comment 6195089812 ·
domain:engineseat 1 · 2026-10-15T14:56Z ·Clause-②: no, no contract review owed · filed #23690 · landing per 〈B〉 once every check is green.Generated by Claude Code