Skip to content

[Bug]: thread.create accepts existing ThreadIds, resets the provider link, and revives deleted threads #19698

Closed

Description

@ViperJuice

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)

  1. Create thread X (orchestration.dispatchCommand with thread.create), send a message, and get a reply. Note the thread's active native Codex thread id.
  2. Dispatch the same thread.create payload again with the same threadId and a fresh commandId. It returns success, and the thread now shows activeProviderThreadId: null.
  3. 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 (NONE in our run).

B — deleted thread (separate, empty thread)

  1. Create thread D, then delete it.
  2. Dispatch thread.create with id D and a fresh commandId. It returns success, and the shell stream shows thread.removed followed by thread.updated with deletedAt: 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, createdAt and activeProviderThreadId, 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 emits thread.created without looking up an existing thread.
  • apps/server/src/orchestration-v2/ThreadManagementService.ts: around lines 82–83, existingThreadIdsForCommand returns [] for thread.create.
  • apps/server/src/orchestration-v2/ProjectionStore.ts: around lines 1859–1926, the thread.created upsert uses ON 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

  1. juliusmarminge commented on Oct 15, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Confirmed on main as a low-priority bug: thread.create doesn't check whether the ThreadId already exists.

    • Orchestrator.ts dispatchThreadCreate builds a fresh record (activeProviderThreadId: null, deletedAt: null, new createdAt) and emits thread.created without looking anything up.
    • ThreadManagementService.ts existingThreadIdsForCommand returns [] for thread.create.
    • The thread.created projection upsert in ProjectionStore.ts overwrites every column, including created_at and deleted_at.

    The upsert itself is reasonable, because it keeps projection replay idempotent. Command retries are already deduplicated by commandId receipts, though, so a create with a fresh commandId and 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.

  2. added
    acceptedfeature request accepted
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedfeature request acceptedbugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions