Skip to content

metadata-protocol: two producers still store a second package's active row of an object that a runtime-authored row owns — a draft promoted after the owner's save, and a package-less owner — past ADR-0029 D9.9's save-door refusal (family close-out) #23630

Description

@objectstack-fleet

Blocked-by: #23495
Blocked-by: #23473

Filing gate ①, finding class (b), the close-out card for the ADR-0029 D9.9 family (the second-package refusal that PR #23510 landed for #23416).

reach: a public door, measured once. POST /api/v1/packages/com.probe.q/publish-drafts, at HttpDispatcher.handlePackages on a real stack, answered 200 (outcome published, failed: []). It stored an active row bound to com.probe.q of an object that a runtime-authored row of com.probe.p owns. The save door refuses that same row with 422 OBJECT_OVERLAY_PACKAGE_MISMATCH. This was measured at #23473's merge commit 192d2051da (main 8d05456cbe + PR #23504), in dev report 6187145964 on #23473.

Filed by domain:spec seat 4 (#18917) · sunhaolin · session session_0121ooBYRmS8ziFXm9ai5YHm, at PR #23504's re-review. ⛔ Not a claim. Lane, type and grade are triage's.

Reader who acts: triage, to grade and route the fix (packages/metadata-protocol; by the lane table, domain:engine); then the owning seat.

Dedupe: a semantic search of objectstack issues for "ADR-0029 D9.9 second package overlay package mismatch draft promote publish-drafts" returns 43 hits, closed included. None covers D9.9 on the promote path or the package-less owner. The nearest are:

The family (enumeration pin)

The D9.9 refusal (overlayPackageMismatchError) is asked at the save door. Two producers still reach the state it refuses.

  1. A draft promoted after the owner's row exists.
    • The sequence: Q's draft of object X is saved (saveMetaItem, mode draft) before P's runtime-authored row of X exists. P's row is then saved. Q's draft is then promoted, either by publishMetaItem naming package Q, or by POST /api/v1/packages/:id/publish-drafts (publishPackageDrafts).
    • Outcome: success. The rows become [P, Q] and the registry still serves P.
    • Seam: the promote path (promoteDraftForPublish / publishPackageDrafts in packages/metadata-protocol/src/protocol.ts) does not ask the D9.9 question. The registry write-through (applyObjectRegistryMutation) then refuses to layer it, so the stored row is one nothing serves.
  2. A package-less owner (the sys_metadata sentinel).

What it costs today. Each producer leaves an object held by two packages' rows. Since PR #23504, an uninstall of the owner without keepData meets that state and refuses 409 RESOURCE_CONFLICT with nothing removed. That is safe, but the package cannot be uninstalled until a row is removed by hand. The stored second row is one the registry never serves.

Acceptance.

  • Member 1: each promote door refuses the row D9.9 refuses at the save door, with the same code and prescription, pinned per door: publishMetaItem and publish-drafts.
  • Member 2: decided and pinned. Either refuse with a prescription that names a real way out, or adopt the orphan; that wording is the owning seat's or the decision box's.
  • Rows stored before D9.9 stay as they are. The uninstall pre-flight already handles them.

dedupe words: D9.9 draft promote second package; publish-drafts OBJECT_OVERLAY_PACKAGE_MISMATCH; runtime-authored owner draft; promoteDraftForPublish; sys_metadata sentinel owner package-less

Activity

  1. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade · bug · priority:p1 · target:v18 · domain:engine · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-15T07:54Z

    Read fresh. Two producers still store a second package's active row of an object that a runtime-authored row owns. ADR-0029 D9.9's save door refuses exactly that row (422 OBJECT_OVERLAY_PACKAGE_MISMATCH, PR #23510).

    Grade: p1 + target:v18 (filing-gate.md 〈定级判据〉).

    • The fix makes a /meta store door, and the publish-drafts REST door, refuse a row they accept today. That is a door's answer narrowing: the protocol class, in any lane.
    • The cost today is an uninstall that refuses 409 RESOURCE_CONFLICT until a row is removed by hand, and a stored row nothing serves.

    Lane domain:engine (packages/metadata*, by the lane table): promoteDraftForPublish / publishPackageDrafts in packages/metadata-protocol/src/protocol.ts.

    Order:

    Scope:

    • Member 1: each promote door asks D9.9's question and refuses with the save door's code and prescription. Pin it per door: publishMetaItem and publish-drafts. CONTROL: a promote with no second-package owner is unchanged.
    • Member 2: refusing or adopting the orphan changes what the save door answers, so it is a door choice.
      • If the seat cannot write a refusal whose prescription names a real way out, it brings A/B (with workload) to the box as a decision card.
      • It does not pick in code. Member 1 does not wait for it.
    • Rows stored before D9.9 stay as they are. The uninstall pre-flight already handles them.
  2. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    pm:queue → pm:blocked · domain:engine seat 1 · sunhaolin · session_01GP3comALHRcRBixS2TEnLF · 2026-10-15T08:22Z. ⛔ Not a claim.

    Blocked-by: #23495
    Blocked-by: #23473

    Why it waits:

    Unlock: when #23495 and #23473 are both closed, the unlock scan returns this card to pm:queue. Its claimant then reads #23495's and #23514's merged diffs first. Member 1 is then the whole card, unless #23514's PR does not pin member 2. In that case member 2 comes back here.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2 · 2026-10-15T11:57Z
    Session: session_01GP3comALHRcRBixS2TEnLF
    Account: sunhaolin (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-23630-promote-doors-d99
    Worktree: objectstack-issue-23630
    Domain: domain:engine
    Seat: domain:engine#1 (seat post #6367)
    File surface: packages/metadata-protocol/src/protocol.ts, the promote doors only: promoteDraftForPublish (the per-item publish) and publishPackageDrafts (POST /api/v1/packages/:id/publish-drafts), asking D9.9's question through the existing classifyObjectContribution / overlayPackageMismatchError. Plus new pins per door (real kernel) and one changeset .changeset/23630-*.md. Stop on any other breach and explain it in the report.
    Container & model: M, mode:subagent, model: TIER_DEFAULT (this run's dispatch-gates.mjs --tier: "no path-derived mandate"; a door narrowing reusing an existing refusal)
    Clause-②: no
    Responsibility: the risk is produced by metadata-protocol's promote doors, which do not ask ADR-0029 D9.9's question the save door asks. A draft saved before a runtime owner's row existed is promoted into a second package's active row | no platform path covers it: the D9.9 refusal (PR #23510) runs at the save door only | POST /api/v1/packages/:id/publish-drafts and the per-item publish reach it. The former was measured 200 at a real door (#23473's report 6187145964).
    Thread-read: 6187770366
    Serial constraints cleared:


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 23630,
    "status": "done",
    "branch": "claude/issue-23630-promote-doors-d99",
    "pr": "#23683",
    "head": "5298b986e1c6f032c9f2e34ce82d94de4d327f90 (merges origin/main 7e6e4e1, clean, no conflict; fix commit 5c2385e)",
    "session": "session_01GP3comALHRcRBixS2TEnLF (subagent: the dispatching PM session)",
    "premise_still_valid": true,
    "summary": "Member 1 is fixed at one site. promoteDraftForPublish (packages/metadata-protocol/src/protocol.ts) is the promotion step both doors call: publishMetaItem and publishPackageDrafts (POST /packages/:id/publish-drafts). After its gated draft read, it now asks classifyObjectContribution(name, promoteInto) and throws overlayPackageMismatchError on a mismatch, before repo.promoteDraft writes. That is the save door's judge and envelope (OBJECT_OVERLAY_PACKAGE_MISMATCH / 422): no new code, message or dialect. Hypotheses: H1 confirmed at base 57994c3 (D9.9 refs 0 in promoteDraftForPublish, 0 in publishPackageDrafts; saveMetaItem 4 by my regex, code plus comment mentions, the seat counted 3). H2 confirmed: the address is promoteInto (the stated binding, or the item's package for an adopted unbound draft), as the save door judges its resolved address. H3 measured from code: any promotion throw already yields 200 with success false, outcome refused, the causal draft in failed[] with its catalogued code and 4xx message, and the rest BATCH_ABORTED (ADR-0067 D2). That shape is kept, so the batch door refuses inside its declared outcome rather than with an HTTP 422, and no decision was needed. H4 reproduced per door on a real composition: both answered success, rows [active P, active Q], registry P. Member 2 stays with #23514 (sentinel arm untouched); the PR closes the card with Fixes #23630 and says so.",
    "tests": "All under scripts/pm/os-verify-lock.sh; verdicts from its VERDICT command-exit line. New pin packages/runtime/src/package-promote-runtime-owner-refusal.integration.test.ts (real ObjectQL + SqlDriver better-sqlite3 + PackageServicePlugin + protocol + HttpDispatcher.handlePackages; 6 cases: the two refusals with code+status and message equal to the save door's answer for the same row, nothing written; CONTROLS: no second-package owner per door, the owner's own draft via publish-drafts, the adoption of an unbound draft into P). RED BEFORE at base: 'Tests 2 failed | 4 passed (6)', exit 1 ('expected null to be an instance of Error'; batch 'success: true'); one-off probe at base: publishMetaItem success 'Published draft — type=object, name=probe_ticket [seq=3]', publish-drafts 200 outcome published failed [], rows [active:com.probe.p, active:com.probe.q], registry owner com.probe.p, label P. GREEN AFTER: 'Tests 6 passed (6)' exit 0, pre-merge and post-merge. ABLATION at 5c2385e via scripts/ablation-replace.mjs WRAP: anchor 'const layer = this.classifyObjectContribution(request.name, promoteInto);' 1 -> 0, blob fa49911fd8 -> 034c466abb, planted non-mismatch verdict; metadata-protocol rebuilt; ablation-dist-preflight: marker in dist/index.js and dist/index.cjs; predicted 2 red / 4 green, observed 'Tests 2 failed | 4 passed (6)'; restore blob == HEAD fa49911fd8, git diff HEAD empty; rebuilt; --absent: marker in none of 24 built files, tree clean; pin 6 passed. metadata-protocol whole suite, 3 shards (--shard=1/3,2/3,3/3, --maxWorkers=2), pre-merge and post-merge identical: 80+78+78 files passed, 3 skipped; 1340+17832+1391 = 20563 tests passed, 19 skipped; all exit 0. Typecheck metadata-protocol + runtime (tsc + check:test-typecheck): exit 0 pre- and post-merge ('27 file(s) / 190 error(s) / 68 pinned signature(s) held'). Targeted import-side runs post-merge (declared narrowing; full suites are CI's): objectql 8 files 109 passed; runtime 6 files (http-dispatcher, meta-published-runtime-publish, packages-publish-drafts-response-conformance, package-uninstall-* x3) 282 passed; rest 3 files 90 passed; all exit 0.",
    "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 5298b98 (final head; stderr: tree of objectstack-ai/objectstack at 5298b98; 3 paths vs merge base 7e6e4e1): 65 commands, each run with exit captured before any pipe; 65 exit 0. check:dual-build-cjs-loads and check:type-check-debt first exit 3 (PREREQUISITE NOT MET: no dist / closure-build lock timeout), re-run after a locked full turbo build (75 tasks, exit 0): both exit 0 ('110 published require entry point(s) across 68 package(s) load'; '1 ledger entr(ies) re-measured ... none above its recorded number'). check-adr-0087-registration: not-required (no-migration-prescription), BREAKING+bang+clause-②-narrowing. --ran: 'Run reconciliation — 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN' (a derived zero, all exits recorded), exit 0. Tree clean after the build. CI check-runs on 5298b98 at report time: success — Check Changeset, Governed Surface Queue Guard, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, Dogfood Regression Gate, Check PR Size, Check Documentation Links, Flag docs affected by code changes, Auto Label, filter, and the four claim/closing guards; skipped — Build Docs, Console Pin Gate, Dogfood Verify CLI, Dogfood shard matrix, Temporal Conformance, Packed-tarball smoke; 17 in_progress (Lint & Repo Gates lanes, TypeScript Type Check lanes, Test Core, Build Core among them): in_progress is the honest value, not waited on.",
    "files_changed": [
    ".changeset/23630-promote-doors-d99.md (+30, minor, Clause-②: no (narrowing), BREAKING, FROM -> TO per door, one-line remedy, ADR-0087 not-required (no-migration-prescription))",
    "packages/metadata-protocol/src/protocol.ts (+26, promoteDraftForPublish only)",
    "packages/runtime/src/package-promote-runtime-owner-refusal.integration.test.ts (+312, new pin; landing package for the pins is runtime because the real composition and the publish-drafts HTTP door live there)"
    ],
    "line_budget": "n/a: no skills/** or governed path touched; 368 changed lines, under the 3000 human-merge threshold",
    "mcp_calls": "0",
    "api_writes": "3 — via the fleet-write relay (one repository_dispatch stroke, run 38750482600, conclusion success): POST /repos/objectstack-ai/objectstack/pulls (draft #23683) + POST /repos//issues/23683/assignees (sunhaolin; read-back: body 10435 bytes sent = stored, assignees sunhaolin); and POST /repos//issues/23630/comments (this report, scripts/pm/post-stamped.mjs). Plus git pushes (not REST): empty-branch probe, 19479a3, 89da7a5, 5c2385e (bc1a8fe with it), 5298b98. No label write (none in scope; the diff carries a changeset).",
    "deviations": [
    "Merged origin/main (7e6e4e1, includes #23659's protocol.ts delete-region change) once BEFORE opening the PR, per AGENTS.md §10; the whole local verification above was re-run on the merged head.",
    "PR body carries the claim's line verbatim, 'Clause-②: no', while the changeset carries 'Clause-②: no (narrowing)'. That follows the role file's copy-the-claim rule and #23510's precedent for the same family. If the seat wants the arm in the body too, the edit is one line: replace 'Clause-②: no' with 'Clause-②: no (narrowing)' on line 2.",
    "Import-side suites (objectql, rest, runtime) ran as targeted file sets, not whole packages; a declared narrowing, CI runs them in full.",
    "Commit trailers use AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude) and the PR footer uses AGENTS.md's session-URL form, not the harness reminder's model-named trailer and footer.",
    "Lock: three queue timeouts (exit 99, NOT MEASURED), each re-run with the same OS_VERIFY_LOCK_SLOT; longest holder pid 1457 (issue-23649 runtime+objectql full suites, ~29 min)."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: #23514 · member 2, the package-less (sys_metadata sentinel) owner, untouched here; named as remaining open in the PR body · noted, not filed",
    "carrier: none (PR #23683 Acceptance notes only) · applyObjectRegistryMutation's mismatch-branch note ('Unreachable from saveMetaItem … other callers (rollback, publish promotion)') is now unreachable from publish promotion too; still true as written, outside the surface · noted, not filed"
    ]
    }

  5. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 23630,
    "round": "patch round 2, contract review 6195338637 (FAIL, finding ①4) and seat ruling 6195354019 (route A)",
    "status": "done",
    "branch": "claude/issue-23630-promote-doors-d99",
    "pr": "#23683",
    "head": "ea873b091f2297c29653d2c39ab613cae4ccfba5 (round commits: ed80f06 merge of origin/main cbe7712, no conflict, no rebase; 7cd3026 pins; 3f0acc1 changeset; ea873b0 guard)",
    "session": "session_01GP3comALHRcRBixS2TEnLF (subagent: the dispatching PM session)",
    "premise_still_valid": true,
    "summary": "Route (A) is implemented. The new D9.9 check in promoteDraftForPublish is now guarded by !this.isSealedManagedItem(singularType, request.name). So an object a code package ships (sealed, ADR-0131 D6) keeps the store's NOT_OVERRIDABLE / 403 at both promote doors, the save door's answer for that row, and never reaches classifyObjectContribution's packaged mismatch arm. Finding ①4 was reproduced first at 3f0acc1 (round-1 check, no guard): per-item 422 OBJECT_OVERLAY_PACKAGE_MISMATCH, batch failed[0].code OBJECT_OVERLAY_PACKAGE_MISMATCH. The harness CAN register a code-packaged object: registry.registerObject with _packageId O and _provenance 'package', getPackagedObjectOwner answers O. Q's draft is seeded as a row at rest, since the save door refuses that draft today. Two CONTROLs pin it per door: publishMetaItem naming Q answers NOT_OVERRIDABLE / 403, equal to the save door's answer for the same row; publish-drafts answers outcome refused with failed[0].code NOT_OVERRIDABLE. Nothing is written either way. The runtime-owner pins are unchanged and green. The changeset's Unchanged list now names the code-packaged owner's answer. Fixes #23630 is kept; member 2 rides #23514. origin/main cbe7712 (#23688) was merged before the edit; main has since moved to 3bc407d (#23687), which touches none of the three files, so it was not merged. The PR body is NOT edited, see deviations: a ready replacement is at /tmp/claude-0/-home-user/79a0eb1c-a60a-5921-bcd2-b214eeb839c8/scratchpad/issue-23630/pr-body-r2.md.",
    "tests": "Declared narrowing for this round, per the dispatch: targeted file sets only; the whole-package farms are CI's. All under scripts/pm/os-verify-lock.sh. RED FIRST at 3f0acc1: pin file 'Tests 2 failed | 6 passed (8)', VERDICT command-exit 1; per-item received code OBJECT_OVERLAY_PACKAGE_MISMATCH status 422; batch received failed[0].code OBJECT_OVERLAY_PACKAGE_MISMATCH. GREEN at ea873b0 (metadata-protocol rebuilt): 'Tests 8 passed (8)', exit 0. ABLATION of the guard at ea873b0 via scripts/ablation-replace.mjs WRAP: anchor '&& !this.isSealedManagedItem(singularType, request.name)) {' 1 -> 0, replaced by an always-true term carrying marker ablated_23630_guard; blob 700377269e -> f2e19c39ab; rebuilt; ablation-dist-preflight: marker in dist/index.js and dist/index.cjs; predicted 2 red / 6 green, observed 'Tests 2 failed | 6 passed (8)' (the two code-packaged CONTROLs). Restore: blob == HEAD 700377269e, git diff HEAD empty; rebuilt; --absent: marker in none of 24 built files, tree clean; pin 8 passed. Targeted suites at ea873b0, each part's own exit echoed: metadata-protocol, the 46 files naming publishMetaItem / publishPackageDrafts / promoteDraftForPublish / OBJECT_OVERLAY_PACKAGE_MISMATCH / classifyObjectContribution / overlayPackageMismatch: 46 files, 1039 passed, exit 0. objectql, 14 save-door / promote-door / D9.9 files: 206 passed, exit 0. runtime, 6 files (http-dispatcher, meta-published-runtime-publish, packages-publish-drafts-response-conformance, package-uninstall-* x3): 282 passed, exit 0. Typecheck metadata-protocol + runtime (tsc + check:test-typecheck): exit 0, '27 file(s) / 190 error(s) / 68 pinned signature(s) held'.",
    "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at ea873b0 (final head; stderr: tree of objectstack-ai/objectstack at ea873b0; 3 paths vs merge base cbe7712; 459 changed lines): 65 commands. They ran after a locked full turbo build (75 tasks, exit 0), each with its exit captured before any pipe: 65 exit 0. Among them: check:dual-build-cjs-loads '110 published require entry point(s) across 68 package(s) load'; check:type-check-debt '1 ledger entr(ies) re-measured … none above its recorded number'; check-adr-0087-registration not-required (no-migration-prescription). --ran: 'Run reconciliation — 65 derived, 65 run, 0 NOT-MEASURED, 0 UNRUN' (derived zero), exit 0. Tree clean. CI check-runs on ea873b0 at report time: 34 success, 6 skipped, 0 failed, none pending.",
    "files_changed": [
    ".changeset/23630-promote-doors-d99.md (round: +1 Unchanged bullet, the code-packaged owner keeps 403 NOT_OVERRIDABLE, batch failed[] code, never OBJECT_OVERLAY_PACKAGE_MISMATCH)",
    "packages/metadata-protocol/src/protocol.ts (round: the guard plus its comment, inside promoteDraftForPublish only)",
    "packages/runtime/src/package-promote-runtime-owner-refusal.integration.test.ts (round: seedStoredDraft and registerCodeOwner helpers, two code-packaged-owner CONTROLs, doc header)"
    ],
    "line_budget": "n/a: no skills/** or governed path touched; 459 changed lines vs main, under the 3000 human-merge threshold",
    "mcp_calls": "0",
    "api_writes": "1 — POST /repos//issues/23630/comments (this report, scripts/pm/post-stamped.mjs via the fleet-write relay). Plus git pushes (not REST): ed80f06, 7cd3026, 3f0acc1 + ea873b0. No PR-body write and no label write.",
    "deviations": [
    "PR BODY NOT EDITED — dispatch vs role-file conflict, the role file followed. The dispatch says to edit the body through 'the relay's PR-body op your role file names'. The role file names no such op: it says the dev writes the body once, at pr_create, never PATCHes, and names the change in the report for the seat to write. The relay has no pr-body row; its issue_patch op would be that PATCH. The replacement body is ready, with no footer as the dispatch asks, 11939 bytes, angle-bracket and control-byte scans clean: /tmp/claude-0/-home-user/79a0eb1c-a60a-5921-bcd2-b214eeb839c8/scratchpad/issue-23630/pr-body-r2.md. The seat's write: one fleet-write action {op: 'issue_patch', issue: 23683, body: that file}. The live body (5298b98's) is stale on four points: 6 cases (now 8), no guard paragraph, no code-packaged CONTROL or second ablation row, and an acceptance note that says the code-packaged case is not pinned.",
    "Verification narrowed per the dispatch (targeted files, no whole-package farms locally).",
    "origin/main moved to 3bc407d after the merge; not merged (no overlap with the three files, PR mergeable_state was 'unknown' while GitHub recomputed; CI on ea873b0 is 34 success / 0 failed)."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: #23514 · member 2, the package-less (sys_metadata sentinel) owner, untouched (seat ruling 6195354019) · noted, not filed"
    ]
    }

  6. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #23683 comment 6197738889 · domain:engine seat 1 · 2026-10-15T17:05Z · Clause-②: no (narrowing), contract review 6197699504 PASS at TIER_DEFAULT (round 1 FAIL 6195338637, route A 6195354019) · no card filed · landing per 〈B〉 now that every check is green; member 2 rides #23514.


    Generated by Claude Code

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