Skip to main content

Module m0033_narrow_skill_global_uniqueness_index

Module m0033_narrow_skill_global_uniqueness_index 

Source
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§

GlobalSkillRow 🔒
One global skill row’s identity, for grouping/collapsing.
M0033NarrowSkillGlobalUniquenessIndex

Functions§

collapse_and_reindex 🔒
Collapse every group of global db_skills rows sharing a name (however large) to the earliest-created_at row (ties broken by id, for determinism), rewriting channel_mstore’s own ref rows (if given) and deleting the loser(s) from identity_conn’s db_skills — then swaps the uniqueness index. Returns the number of rows collapsed away.