feat(agents): slug uniqueness partial-index + 409-envelope existing_uuid (parity port of private cueapi PR #921)#98
Merged
Conversation
…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
Parity checkThis PR modifies files tracked in
Please confirm one of the following in a reply or PR description update:
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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
OSS port of private cueapi PR #921 (Jingim Q1 fix). Replaces the full
unique_user_agent_slugUNIQUE constraint with a partial UNIQUE INDEX scoped todeleted_at IS NULL, enabling re-create of previously-soft-deleted slugs. Addsexisting_uuidto the 409slug_takenenvelope 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/agentsreturned 409slug_taken; GET/v1/agents/{slug}returned 404 (defaultinclude_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
alembic/versions/036_partial_index_user_agent_slug_active.pyDROP CONSTRAINT unique_user_agent_slug+CREATE UNIQUE INDEX ... WHERE deleted_at IS NULL; fail-fast downgrade safety railapp/models/agent.pyUniqueConstraint(...)withIndex(..., postgresql_where=text("deleted_at IS NULL"))for test-DB vs prod schema parityapp/services/agent_service.py_lookup_existing_live_agent_uuidpure helper +_http_error_slug_takenenvelope helper; IntegrityError handler enriches 409 withexisting_uuidtests/test_agents_slug_partial_index.pyparity-manifest.jsonNew 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_uuidsee code/message/status unchanged. New clients (Dock / @trydock/mcp / presence-runtime) read it to skip a GET round-trip.Tests (9 total)
test_create_agent_with_soft_deleted_slug_succeeds— headline asktest_create_agent_with_live_duplicate_slug_returns_409_with_existing_uuidtest_409_envelope_includes_required_fields— shape pintest_create_agent_with_no_existing_slug_succeeds— happy path regressiontest_partial_index_allows_multiple_soft_deleted_rows_same_slug— DB-layer6-7.
test_lookup_existing_live_agent_uuid_returns_*— pure-helper unit teststest_create_agent_direct_call_triggers_409_with_existing_uuid— ASGI bypasstest_non_slug_integrity_error_falls_through_to_raise— defensive9/9 pass locally + 22/22 existing agent tests pass (no regression).
OSS track-lag note
Private cueapi additionally raised
agents.slugfromVARCHAR(64)→VARCHAR(128)in migration 079 (private PR #42 Substrate). That column-width raise is NOT ported here; OSS keepsVARCHAR(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
messaging-v1.1.4post-merge → unblocks Jingim's follow-up cue.dock.svc bump roundNo G14-on-api-cueapi-ai needed (private PR #921 already verified this code surface 5/5 PASS).
Cross-refs
b770983e6132727c2e7a17b202c375061537f52a🤖 Generated with Claude Code