Repository navigation
[Bug]: thread.create accepts existing ThreadIds, resets the provider link, and revives deleted threads #19698
Description
Area
apps/server
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Steps to reproduce
Every dispatch below uses a fresh commandId. A repeated commandId is deduplicated by the receipt store, which is different behaviour.
A — existing thread (Codex provider)
- Create thread X (
orchestration.dispatchCommandwiththread.create), send a message, and get a reply. Note the thread's active native Codex thread id. - Dispatch the same
thread.createpayload again with the samethreadIdand a freshcommandId. It returns success, and the thread now showsactiveProviderThreadId: null. - Ask the model to recall a marker from step 1. A new native Codex thread id is used, and the model replies that it has no such marker (
NONEin our run).
B — deleted thread (separate, empty thread)
- Create thread D, then delete it.
- Dispatch
thread.createwith id D and a freshcommandId. It returns success, and the shell stream showsthread.removedfollowed bythread.updatedwithdeletedAt: null.
We didn't test whether a deleted thread's earlier messages come back.
Expected behavior
thread.create refuses an id that already has a projection, live or tombstoned, with a clear error. Restoring a deleted thread, if that's wanted, would be a separate explicit command.
Actual behavior
Both creates succeed.
- In A, the create reset the title,
createdAtandactiveProviderThreadId, while keeping the thread's earlier runs and messages. Later Codex turns ran on a new native conversation that couldn't see the earlier one. - Recreating the thread didn't release the earlier native session. In our run, its MCP child stayed alive until the shared Codex app-server exited, about 32 minutes later.
- Reading the code (not measured): the upsert replaces the whole thread record, so project, runtime and interaction mode, model, branch and worktree, and archive or snooze state are reset too.
Where this happens (at 348f217a):
apps/server/src/orchestration-v2/Orchestrator.ts:dispatchThreadCreate, around lines 2236–2285, builds a fresh record (deletedAt: null,activeProviderThreadId: null) and emitsthread.createdwithout looking up an existing thread.apps/server/src/orchestration-v2/ThreadManagementService.ts: around lines 82–83,existingThreadIdsForCommandreturns[]forthread.create.apps/server/src/orchestration-v2/ProjectionStore.ts: around lines 1859–1926, thethread.createdupsert usesON CONFLICT(thread_id) DO UPDATE SET … created_at = excluded.created_at, … deleted_at = excluded.deleted_at.
Impact
Minor bug or occasional failure
The web client always mints fresh UUIDs, so this mainly affects API and integration clients, and imports, that choose their own thread ids. For those clients, one duplicate create disconnects later turns from the earlier native conversation while the displayed history stays, and a deleted thread can come back. Related precedent: #18360 adds a check-before-create for retried creation in the MCP fan-out path, but not the dispatchCommand path.
Version or commit
Runtime tested: nightly 0.0.46-nightly.20261014.3118. Source inspected: 348f217a; the same code is still present on main at ab3a3cd.
Environment
Linux; Codex CLI 0.162.1 as the provider; a throwaway T3 server and state; commands dispatched over /ws with an operate-scoped session from t3 auth session issue.
Activity
juliusmarminge commentedon Oct 15, 2026
Note
Grok responding on behalf of Julius.
Confirmed on main as a low-priority bug:
thread.createdoesn't check whether theThreadIdalready exists.Orchestrator.tsdispatchThreadCreatebuilds a fresh record (activeProviderThreadId: null,deletedAt: null, newcreatedAt) and emitsthread.createdwithout looking anything up.ThreadManagementService.tsexistingThreadIdsForCommandreturns[]forthread.create.thread.createdprojection upsert inProjectionStore.tsoverwrites every column, includingcreated_atanddeleted_at.The upsert itself is reasonable, because it keeps projection replay idempotent. Command retries are already deduplicated by
commandIdreceipts, though, so a create with a freshcommandIdand an existing id isn't a retry and should be rejected. #18360 only touches MCP retry handling, before dispatch, so it doesn't overlap.Only clients that pick their own thread ids are affected; the web client always mints new UUIDs. Restoring a deleted thread would be a separate, explicit command.