# What does this PR do?
`conversation_items` keyed rows on a global `id`, so an item id could
belong to only one conversation. That allowed the silent re-parenting in
#6703, which #6704 papered over with a 400. This matches the OpenAI
Conversations API as probed in #6708: an id already in the target
conversation is a 400 (`Item already in conversation`), an id from
another conversation copies that item with its original content under
the same id, and an unknown client id gets a server-minted one. An id
the caller cannot read behaves like an unknown id, so the response is
not an existence oracle. `create_conversation` follows the same rules,
and deleting an item in one conversation leaves its copy in another.
The table keeps its name, since access policies match `sql_record`
resources on the physical table name (#6729 tracks that coupling). On
startup the service reads the primary key of `conversation_items`; a
table keyed on `id` alone is renamed to `conversation_items_v1`,
`conversation_items` is recreated keyed on `(conversation_id, id)` and
the rows are copied once, behind a flag in a `conversation_migrations`
table so later boots never scan the v1 table. The Responses sync path
uses an internal `ConversationItemSync` protocol, leaving the OpenAPI
spec unchanged. `fetch_all`'s cursor lookup now applies the caller's
`where` filters.
The 400 lacks the `item_already_in_conversation` code and `param` since
`OGXError` only carries a message.
## Release notes
Behavior changes:
1. Re-adding an item's id to its own conversation: 200 in-place update
becomes 400.
2. Client-minted item ids: honored becomes ignored (200 with a
server-minted id).
3. Cross-conversation id reuse: 400 becomes 200 copy.
4. `create_conversation` with a foreign item id: 500 becomes 200 copy.
Operator step: the first start after upgrading renames the old
`conversation_items` to `conversation_items_v1`, recreates
`conversation_items` with the new key and copies the rows. The server
warns on every start while `conversation_items_v1` exists; verify the
migrated items, then drop it manually. Policies that name
`conversation_items` keep working.
Closes #6708
## Test Plan
Unit tests cover the six probe cases, batch atomicity, `sync_items`,
pagination with a shared id, cross-tenant ids, and the upgrade on SQLite
and PostgreSQL 15: a fresh database gets the composite key directly with
no migrations table; an id-keyed table is renamed, recreated and copied
with owner, tenant and sort order intact; a second initialize issues no
rename and no read of `conversation_items_v1`, asserted on the executed
SQL. Not run locally: the auth integration tests, which need the CI
token setup.
```
$ ENABLE_POSTGRES_TESTS=true POSTGRES_PORT=5433 uv run pytest tests/unit/conversations tests/unit/utils/sqlstore tests/unit/providers/responses/builtin -q
454 passed
$ uv run pytest tests/unit --ignore=tests/unit/providers/vector_io -q
3954 passed, 17 skipped, 3 xfailed
$ uv run pre-commit run --files <changed files>
all hooks passed
```
---------
Signed-off-by: Rishabh <rishabhsaiv@gmail.com>
Signed-off-by: Matthew Farrellee <matt@cs.wisc.edu>
Co-authored-by: Matthew Farrellee <matt@cs.wisc.edu>