Skip to content

feat(agents): slug uniqueness partial-index + 409-envelope existing_uuid (parity port of private cueapi PR #921)#98

Merged
mikemolinet merged 1 commit into
mainfrom
primary/cmpdl4-oss-port-slug-partial-index
May 21, 2026
Merged

feat(agents): slug uniqueness partial-index + 409-envelope existing_uuid (parity port of private cueapi PR #921)#98
mikemolinet merged 1 commit into
mainfrom
primary/cmpdl4-oss-port-slug-partial-index

Conversation

@mikemolinet
Copy link
Copy Markdown
Collaborator

Summary

OSS port of private cueapi PR #921 (Jingim Q1 fix). Replaces the full unique_user_agent_slug UNIQUE constraint with a partial UNIQUE INDEX scoped to deleted_at IS NULL, enabling re-create of previously-soft-deleted slugs. Adds existing_uuid to the 409 slug_taken envelope for caller ergonomics.

Why

Jingim (Dock cross-org agent) hit this empirically 2026-05-20: cue.dock.svc vendors public cueapi-core; soft-deleting an agent left the slug locked. POST /v1/agents returned 409 slug_taken; GET /v1/agents/{slug} returned 404 (default include_deleted=false). Net: no clean way to recreate a previously-soft-deleted slug.

Mike Q1 strategic call (private cueapi msg_bmu35t9aeql7): ship option (a) partial-index + fold option (b) existing_uuid envelope. Private PR #921 merged + G14 5/5 PASS on api.cueapi.ai @ commit 454aac3. This PR ports to public cueapi-core for OSS / Dock / self-hosted consumption.

Changes

File Change
alembic/versions/036_partial_index_user_agent_slug_active.py NEW — DROP CONSTRAINT unique_user_agent_slug + CREATE UNIQUE INDEX ... WHERE deleted_at IS NULL; fail-fast downgrade safety rail
app/models/agent.py Replace UniqueConstraint(...) with Index(..., postgresql_where=text("deleted_at IS NULL")) for test-DB vs prod schema parity
app/services/agent_service.py Add _lookup_existing_live_agent_uuid pure helper + _http_error_slug_taken envelope helper; IntegrityError handler enriches 409 with existing_uuid
tests/test_agents_slug_partial_index.py NEW — 9 boundary tests (HTTP integration + pure-helper unit tests + ASGI-bypass direct-call + defensive non-slug IntegrityError)
parity-manifest.json Add migration 036 entry: private 080 → OSS 036; OSS keeps VARCHAR(64) (column-width raise NOT ported)

New 409 envelope shape (additive; non-breaking)

{
  "error": {
    "code": "slug_taken",
    "message": "slug 'jingim' already in use for this user",
    "status": 409,
    "existing_uuid": "agt_abc123def456"
  }
}

Existing clients ignoring existing_uuid see code/message/status unchanged. New clients (Dock / @trydock/mcp / presence-runtime) read it to skip a GET round-trip.

Tests (9 total)

  1. test_create_agent_with_soft_deleted_slug_succeedsheadline ask
  2. test_create_agent_with_live_duplicate_slug_returns_409_with_existing_uuid
  3. test_409_envelope_includes_required_fields — shape pin
  4. test_create_agent_with_no_existing_slug_succeeds — happy path regression
  5. test_partial_index_allows_multiple_soft_deleted_rows_same_slug — DB-layer
    6-7. test_lookup_existing_live_agent_uuid_returns_* — pure-helper unit tests
  6. test_create_agent_direct_call_triggers_409_with_existing_uuid — ASGI bypass
  7. test_non_slug_integrity_error_falls_through_to_raise — defensive

9/9 pass locally + 22/22 existing agent tests pass (no regression).

OSS track-lag note

Private cueapi additionally raised agents.slug from VARCHAR(64)VARCHAR(128) in migration 079 (private PR #42 Substrate). That column-width raise is NOT ported here; OSS keeps VARCHAR(64). Self-hosters whose slugs fit in 64 chars (the common case) are unaffected. If OSS users need the 128-char ceiling for labeled-Live composite slugs, that's a separate port.

Test plan

  • CI green on Feature PR to Main workflow (Backend tests + CodeQL + alembic-collision-check + parity-check)
  • G11-β 2-way at pushed HEAD (cue-pm + cueapi-secondary if available)
  • Lighter G12 (CI greens + 1-2 directed route pings)
  • Admin-merge to main
  • Tag cut messaging-v1.1.4 post-merge → unblocks Jingim's follow-up cue.dock.svc bump round

No G14-on-api-cueapi-ai needed (private PR #921 already verified this code surface 5/5 PASS).

Cross-refs

  • Private cueapi PR #921 source: merge_commit b770983e6132727c2e7a17b202c375061537f52a
  • Private G14 evidence: cue Message msg_9ja5upulf6fo (5/5 PASS empirical verification)
  • Jingim Q1 source: msg_0xh469viafpc; parity-gap heads-up msg_3xy1kly05va2
  • cue-pm Q1 strategic call: msg_bmu35t9aeql7
  • This port's G11-α CONCUR: msg_bgoh17xb0ns5

🤖 Generated with Claude Code

…uid (parity port of private cueapi PR #921)

OSS port of private cueapi PR #921 (Jingim Q1 fix). Replaces the full
`unique_user_agent_slug` UNIQUE constraint with a partial UNIQUE INDEX
scoped to `deleted_at IS NULL`, enabling re-create of previously-soft-
deleted slugs. Adds `existing_uuid` to the 409 `slug_taken` envelope
for caller ergonomics.

## Why

Jingim (Dock cross-org agent) hit this empirically 2026-05-20:
cue.dock.svc vendors public cueapi-core; soft-deleting an agent on
cueapi side left the slug locked. POST /v1/agents returned 409
slug_taken; GET /v1/agents/{slug} returned 404 (default
include_deleted=false). Net: no clean way to recreate a previously-
soft-deleted slug.

Mike Q1 strategic call (private cueapi msg_bmu35t9aeql7 2026-05-20):
ship option (a) partial-index + fold option (b) existing_uuid
envelope. Private PR #921 merged + G14 5/5 PASS on api.cueapi.ai @
commit 454aac3. This PR ports to public cueapi-core for OSS / Dock /
self-hosted consumption.

## What this PR ships (verbatim port from private PR #921)

### Migration 036 — partial UNIQUE INDEX

Drops `unique_user_agent_slug` UNIQUE constraint. Recreates as a
partial UNIQUE INDEX of the same name with `WHERE deleted_at IS NULL`.
Uniqueness scoped to active rows only; soft-deleted rows can keep
their original slug without blocking recreate.

Index name preserved so the existing IntegrityError-error-text match
at `app/services/agent_service.py` (`if "unique_user_agent_slug" in
str(e.orig)`) continues to work without code change.

Fail-fast downgrade safety rail: refuses downgrade if any (user_id,
slug) pair has duplicates across active + soft-deleted rows.

### app/models/agent.py — Index instead of UniqueConstraint

Replaces `UniqueConstraint("user_id", "slug", ...)` with
`Index("unique_user_agent_slug", "user_id", "slug", unique=True,
postgresql_where=text("deleted_at IS NULL"))` so test-DB schema
(built via Base.metadata.create_all) matches prod schema (built via
alembic). Single declaration source.

### app/services/agent_service.py — 409 envelope + existing_uuid

- New pure helper `_lookup_existing_live_agent_uuid(db, user_id, slug)`
  → queries by `Agent.deleted_at.is_(None)`; returns Optional[str]
- New helper `_http_error_slug_taken(final_slug, existing_uuid)` →
  builds 409 envelope with the new field
- IntegrityError handler in `create_agent` calls both helpers when
  `"unique_user_agent_slug" in str(e.orig)` fires

New envelope shape (additive; existing clients ignoring existing_uuid
see code/message/status unchanged):

```json
{
  "error": {
    "code": "slug_taken",
    "message": "slug 'jingim' already in use for this user",
    "status": 409,
    "existing_uuid": "agt_abc123def456"
  }
}
```

### parity-manifest.json — migration 036 entry

Documents the port relationship: private 080 → OSS 036; private
chain was 079→080 (post PR #919 slug-VARCHAR-raise-to-128); OSS
keeps VARCHAR(64) for now (column-width raise NOT ported).

### tests/test_agents_slug_partial_index.py — 9 boundary tests

1. test_create_agent_with_soft_deleted_slug_succeeds — headline ask
2. test_create_agent_with_live_duplicate_slug_returns_409_with_existing_uuid
3. test_409_envelope_includes_required_fields — shape pin
4. test_create_agent_with_no_existing_slug_succeeds — happy path regression
5. test_partial_index_allows_multiple_soft_deleted_rows_same_slug — DB-layer
6-7. test_lookup_existing_live_agent_uuid_returns_{agt_id_when_live_match_exists,none_when_only_soft_deleted}
   — pure-helper unit tests (defeats ASGI trace gap)
8. test_create_agent_direct_call_triggers_409_with_existing_uuid
   — direct-call bypass for ASGI-dispatched IntegrityError handler body
9. test_non_slug_integrity_error_falls_through_to_raise
   — defensive (per private cue-pm G11-α add)

9/9 pass locally. 22/22 existing agents tests pass (no regression).

## OSS track-lag note

Private cueapi additionally raised `agents.slug` from `VARCHAR(64)` to
`VARCHAR(128)` in migration 079 (private PR #42 Substrate). That
column-width raise is NOT ported here; OSS keeps `VARCHAR(64)`. Self-
hosters whose slugs fit in 64 chars (the common case) are unaffected.
If OSS users need the 128-char ceiling for labeled-Live composite
slugs, that's a separate port.

## Gates

- ✅ G11-α 2-way CONCUR (private cueapi msg_259g99wa5n5o for source PR;
  port G11-α CONCUR via msg_bgoh17xb0ns5)
- ⏳ CI green on Feature PR to Main workflow
- ⏳ G11-β 2-way (cue-pm + cueapi-secondary if available)
- ⏳ Lighter G12 (CI greens + 1-2 directed route pings)
- ⏳ Admin-merge to main
- ⏳ Tag cut **messaging-v1.1.4** post-merge → Jingim's follow-up
  cue.dock.svc bump

## Cross-refs

- Private cueapi PR #921: merge_commit b770983e (source cherry-pick)
- Private G14 evidence: msg_9ja5upulf6fo (5/5 PASS verified)
- Jingim Q1 source: msg_0xh469viafpc (parity-gap heads-up at
  msg_3xy1kly05va2)
- cue-pm Q1 strategic call: msg_bmu35t9aeql7
@github-actions
Copy link
Copy Markdown

Parity check

This PR modifies files tracked in parity-manifest.json:

  • alembic/versions/036_partial_index_user_agent_slug_active.py
  • app/models/agent.py
  • app/services/agent_service.py

Please confirm one of the following in a reply or PR description update:

  1. The equivalent change has been applied to the private cueapi monorepo. Link the PR.
  2. This change is OSS-only and does not need porting. Briefly explain why (e.g. "fixes a bug that only exists in the OSS build").
  3. A follow-up issue has been filed to port the reverse direction. Link the issue.

This is a soft check — it does not block merge. The goal is visibility, not friction. See HOSTED_ONLY.md for the open-core policy.

@govindkavaturi-art govindkavaturi-art enabled auto-merge (squash) May 21, 2026 00:56
@mikemolinet mikemolinet merged commit 9d8b146 into main May 21, 2026
7 checks passed
@mikemolinet mikemolinet deleted the primary/cmpdl4-oss-port-slug-partial-index branch May 21, 2026 01:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant