Sign in to edit tickets from this page.

← all tickets · home

Refactor: retire World UUID, use slug as the only world identifier

resolved 5e00c1ee-9523-4333-8174-43aeb77eaf6f

created_at
2026-04-23
updated_at
2026-04-23
code_context
src/kernel.rs, src/worlds.rs, src/scenarios.rs, src/mcp.rs, src/persistence.rs, src/turn_job.rs, src/server.rs, src/html.rs, src/slug.rs (new), tests/phase0.rs, tests/ant_scenario.rs, docs/terms.md
priority
P2
ticket_type
chore
resolved_at
2026-04-23
resolution
accepted

Body

MOTIVATION

Worlds currently have two identifiers: an internal UUID (World.world_id, used as the routing key and directory name) and a display-only name. With slugs coming in as a first-class, required, URL-safe identifier, that three-identifier situation is complexity for no gain. Simpler: the slug is the identifier. The UUID goes away entirely.

This is a refactor, not a feature. Scope is deliberately narrow: replace every reference to world_id: Uuid with the slug. No UI changes. No new MCP tools. No behavioral changes besides the identifier swap.

A follow-up ticket for a bolt-on read-only web UI at /w/:slug/... depends on this one landing first.

================================================================ THE RULE

A world has exactly one identifier: its slug.

================================================================ GRAMMAR

Valid slug, precisely:

Examples: ant-smoke OK my_world OK ant-1 OK prod OK a OK a1-b2_c3 OK Ant-smoke REJECT (uppercase) ant smoke REJECT (whitespace) -ant REJECT (leading hyphen) ant REJECT (leading underscore) ant- REJECT (trailing hyphen) ant REJECT (trailing underscore) ant.smoke REJECT (dot is reserved for entity id hierarchy) "" REJECT (empty) 65+ chars REJECT (too long)

Validation is strict. No normalization, no trimming, no prettification. The string as typed is either valid or rejected.

================================================================ WHAT CHANGES AT EACH LAYER

src/slug.rs (NEW, ~60 lines + tests)

src/kernel.rs

src/scenarios.rs

src/worlds.rs (the core of the refactor)

src/mcp.rs

src/persistence.rs

src/turn_job.rs

src/server.rs

src/html.rs

tests/phase0.rs and tests/ant_scenario.rs

docs/terms.md

================================================================ WHAT DOES NOT CHANGE

================================================================ MIGRATION

Production registry is empty (list_worlds returned 0 earlier this session). No live worlds on disk. No migration path is built.

If any world happens to exist on disk at deploy time, it will fail to load: WorldMeta::read rejects meta.json without a slug field, and load_all skips directories whose names don't parse as slugs (UUIDs happen to be valid slug-shaped strings by grammar, so the directory name check passes — the meta.json check is what fails). The fix is manual: add a slug field to each meta.json, rename the directory to match. No fallback code path is added.

================================================================ IMPLEMENTATION ORDER

Bottom-up, each step compiles before the next:

  1. src/slug.rs + unit tests
  2. src/kernel.rs, src/scenarios.rs (World, Scenario::seed signatures)
  3. src/worlds.rs (WorldMeta, registry keys, create/delete/load)
  4. src/persistence.rs + src/turn_job.rs (audit fields, attempt records)
  5. src/mcp.rs (McpEnv, require_*, every world-touching handler, tool manifest descriptions, new error codes)
  6. src/server.rs + src/html.rs (AppState, dashboard)
  7. tests (phase0, ant_scenario) — these will not compile until their scenario/create_world calls gain slugs; update alongside each preceding step as needed
  8. docs/terms.md

================================================================ ACCEPTANCE

================================================================ OPEN KNOB

Should the MCP wire-level arg field AND the on-disk audit-event JSON field be renamed from world_id to world_slug, or stay as world_id with only the described value format changing?

RECOMMENDATION: rename to world_slug at both layers. It is a breaking MCP contract change, but the registry is empty and the renamed field is unambiguously correct — a client seeing world_slug will supply a slug-shaped value; a client seeing world_id might still reach for a UUID and hit BAD_SLUG errors.

Alternative: keep world_id as the wire/on-disk name with updated descriptions. Less churn, more legacy baggage.

The ticket body assumes RENAME unless the caller overrides.

Proposed resolution

Implemented end-to-end, committed, deployed, and smoke-tested in production.

New module src/slug.rs:

Identifier retirement at every layer:

What didn't change (per spec): attempt_id stays Uuid, ticket_id stays Uuid, entity_id stays semantic string, turn_ref stays turn_NNNNNN, OAuth and code_nav untouched.

Docs: new "World slug — grammar, uniqueness, lifetime" section in docs/terms.md including the grammar table, uniqueness semantics, lifetime-and-immutability discussion, and an explicit rationale for why slugs don't normalize while entity_ids do (different callers: operators vs. LLMs; different policies).

Receipts — source verification:

Receipts — deploy + production smoke:

Acceptance checklist vs spec:

Known non-issue worth flagging: Claude Code's ToolSearch may still show cached MCP tool schemas (old world_id arg) until it re-negotiates with the server. Actual tool invocations pass through — the old tool_id worked with the new required slug arg, and required world_slug arg, proving the server is the source of truth.

Per standing guidance I am not confirming this ticket — only proposing. Over to you. The follow-up read-only /w/:slug/... web UI ticket is now unblocked whenever you want to file it.

History (4 events)

Sign in as a human to drive this ticket from the page, or use the MCP tools.