Expand description
Narrow the global skill uniqueness index from (name, skill_type) to
name alone, matching the invariant skill_upsert_unique_global’s own
dup-check has always enforced — with a defensive dedup pass for any
existing same-name/different-type pair the OLD, wider index let through.
Part B of Phase 2’s application-layer redirect,
SPEC_DURABLE_BINDINGS_2026_09_10.md §5.4 (Codex P2, PR #3182).
idx_ids_skills_global_name_type (IDENTITY_STORE_SCHEMA_VERSION v7) is
narrower than what the application actually promises (“a global skill’s
name is unique, full stop, regardless of type” — see
managed_upsert_unique_global’s error message). MCP servers don’t have
this mismatch (idx_ids_mcp_servers_global_name is already name-only) —
this migration is skills-specific.
Because m0031_carry_skills_and_mcp_servers_to_identity_store’s own dedup
key is (name, skill_type), a store that already ran that migration (in
an earlier release) may already hold two global db_skills rows sharing
a name with different types — exactly the pair the OLD index allowed and
the NEW one would reject outright. This migration detects any such
group (not assuming exactly two), collapses to the earliest-created_at
row (ties broken by id), rewrites refs, deletes the loser(s), and only
THEN creates the new index — in that order, since creating a UNIQUE
index over still-colliding data would fail.
§Cross-channel ref cleanup is intentionally partial
db_agent_skills_ref/db_bundle_skills_ref live in EACH CHANNEL’s own
mstore, not in the identity store this migration (Global-scoped) opens.
A single global migration run cannot reach every channel’s ref rows the
way m0022_identity_store_links_backfill reads multiple sources —
reaching in and WRITING to a sibling channel’s live objects.db from a
process that isn’t running that channel is the same unnecessary risk
m0031’s own doc comment already declines to take. So this migration
rewrites only the CURRENT channel’s refs (the one whose objects.db is
in MigrationContext); any other channel’s ref rows still pointing at a
collapsed-away loser id are left dangling until that channel’s own next
boot runs this same migration and would have to reconcile them itself —
except this migration is Global-scoped and runs exactly ONCE across every
channel, so a sibling channel never gets a second pass. This mirrors the
already-accepted “cross-channel dangling refs on catalog delete” gap
§5.4 documents elsewhere (a dangling ref degrades gracefully: skill_get
against a superseded id returns None, which every read path already
handles as “this bound skill no longer exists”), not a new regression —
but it is a real, acknowledged incompleteness worth a second look.
Structs§
- Global
Skill 🔒Row - One global skill row’s identity, for grouping/collapsing.
- M0033
Narrow Skill Global Uniqueness Index
Functions§
- collapse_
and_ 🔒reindex - Collapse every group of global
db_skillsrows sharing aname(however large) to the earliest-created_atrow (ties broken byid, for determinism), rewritingchannel_mstore’s own ref rows (if given) and deleting the loser(s) fromidentity_conn’sdb_skills— then swaps the uniqueness index. Returns the number of rows collapsed away.