Skip to content

fix(metadata-protocol): the /meta list serves one row per name after a runtime PUT - #8332

Merged
os-zhuang merged 4 commits into
mainfrom
claude/issue-7654-skill-read-path-dedup
Aug 13, 2026
Merged

fix(metadata-protocol): the /meta list serves one row per name after a runtime PUT#8332
os-zhuang merged 4 commits into
mainfrom
claude/issue-7654-skill-read-path-dedup

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Part of #7654

Closes the metadata half of that card — symptom 1, the duplicate row. Symptom 2 (the MCP prompt bridge) is domain:cli's lane, is deliberately not touched here, and is filed as #8328 with a Blocked-by line pointing at this work. packages/mcp is untouched by this PR.

The card's premise, verified

Symptom 1 reproduces on current main. Reverting just this PR's hunk and re-running the new suite serves the card's exact shape — one skill, two rows, disagreeing about active:

formula-helper|(none)|true              ← the store override row
formula-helper|com.acme.showcase|false  ← the package row

Root cause — neither "un-deduped" nor "registered wrongly"

The card was dispatched with two candidate shapes: add a dedup, or register skill the way types that already merge correctly are registered. It is neither, and the difference decides the fix.

getMetaItems merges three layers, and two of them answered the identity question differently:

  • mergePackageAwareOverlay — the sys_metadata overlay merge — resolves per (slot, package) and treats a package-less row as standing in for each package's row of that name. That is exactly how getMetaItem(name, packageId=P) resolves.
  • The MetadataService merge one layer below hand-rolled a Map keyed on (package, name) with strict equality, so a package-less row occupied a slot of its own rather than standing in for anything.

Which merge decides a type is a function of where its baseline comes from. email_template is resolved by the first, because its rows come from the SchemaRegistry. skill, agent and tool reach the list through the MetadataService's own loaders, so the registry listing is empty and they are resolved by the second.

A runtime PUT /api/v1/meta/{type}/{name} carries no ?package=, so sys_metadata takes a package_id IS NULL row. With an empty registry listing the overlay merge has no base row to take provenance from and leaves the override body with no _packageId. Its key then missed the package-bearing baseline row in the merge below, the "already present, do not overwrite" guard never fired, and both rows were served.

The fix

Delete the second implementation and call the first. The MetadataService merge now runs that same package-aware resolution, with the runtime listing as the base layer and the registry-plus-overlay result as the higher one — so the documented precedence (a sys_metadata customization wins over the artifact baseline) is preserved, while the two steps can no longer disagree about what a package-less row means.

One hunk, in the getMetaItems read merge only.

Not a skill special case

Two pins in the new suite make this a class fix rather than a patch:

  • agent duplicates identically and is closed by the same resolution. Any fix that special-cased skill, or that merely "registered skill like every other type", would leave it broken.
  • The mirrored attribution duplicates too — a package-less MetadataService baseline under a package-bearing higher row — and is closed by the same call.

ADR-0048 resolution is unchanged: two packages shipping one name still stay two rows, and a package-less override now reaches both of their slots.

Verification

Reverse verification, direction predicted before running: restoring the hand-rolled itemMap must turn the duplication cases red naming two rows where one is expected, and must leave protocol.i18n-bundle-list-merge.test.ts green. Both observed — 5 failed / 17 passed in the new suite, i18n suite green.

Green for the right reason, proved rather than asserted. A control suite that passes in both states proves nothing on its own, so its reachability was measured directly: with items = [] injected immediately after the new merge call, 3 of the i18n suite's tests go red. It genuinely executes the branch this PR replaces, and its green result under the real fix is therefore meaningful.

Suite Result
@objectstack/metadata-protocol 78 files, 1126 tests passed
@objectstack/rest 109 files, 1809 tests passed
@objectstack/objectql 196 files, 3451 tests passed
@objectstack/runtime 150 files, 2306 tests passed

Gates, re-derived against the actual changed paths rather than taken from the dispatch list: check:nul-bytes, check:cross-package-test-inputs, check:durability-log-level, check:filter-alias-parity, check:changeset-gate-self-tests, check:objectui-changeset, check:type-check-coverage, check:type-check-debt, check:query-options-erasure, check-changeset-no-major — all pass. The re-derivation surfaced the four changeset-triggered families the dispatch list did not name.

Concurrency

protocol.ts is under the region exemption. This PR's region is the getMetaItems read merge; #8136's is error and catch message construction. The regions are disjoint, main is merged as of this push, and the merge queue arbitrates — no manual ordering was applied to either side.


Generated by Claude Code

claude added 4 commits August 13, 2026 03:39
…override against the MetadataService baseline

The MetadataService merge in `getMetaItems` keyed a hand-rolled Map on
`(package, name)` with strict equality, while `mergePackageAwareOverlay`
one layer above resolves per `(slot, package)` and lets a package-less
row stand in for each package's row of that name. A runtime
`PUT /api/v1/meta/<type>/<name>` sends no `?package=`, so its sys_metadata
row is package-less; for a type whose baseline lives in the MetadataService
(skill, agent, tool) the registry listing is empty, the override left the
overlay merge unstamped, its key missed the package-bearing baseline row,
and the list served both — the duplicate skill row the card reports.

The MetadataService merge now runs the same package-aware resolution
instead of a second implementation of it, with the runtime listing as the
base and the registry+overlay result as the higher layer, so the
'overlay wins over the artifact baseline' guard it replaces is preserved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WMpuAfA2KSdDjGF6tm1bH
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 13, 2026 5:52am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol.

3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata-protocol)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/metadata-protocol)

1 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/v9.mdx (via @objectstack/metadata-protocol)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 13, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 13, 2026 06:18
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 13, 2026
Merged via the queue into main with commit d5031f6 Aug 13, 2026
26 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-7654-skill-read-path-dedup branch August 13, 2026 06:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants