Skip to content

[Bug]: Reusing an accepted commandId for another thread returns false success #5231

Description

@JorgeMenaDev

Before submitting

  • I searched existing open and closed issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

Use only two disposable synthetic threads, thread-A and thread-B:

  1. Start T3 Code and create thread-A and thread-B in one project.
  2. Dispatch a thread.turn.start command to thread-A with a deterministic command ID such as cmd-synthetic-replay, a fresh message ID, and a short synthetic user message.
  3. Wait for the dispatch to be accepted and record the returned sequence N.
  4. Dispatch a second thread.turn.start using the same commandId: cmd-synthetic-replay, but target thread-B and use a different fresh message ID.
  5. Inspect the second HTTP response and the snapshots/events for both threads.

The same issue also applies more generally when a previously accepted command ID is reused with a different command payload on the same aggregate: receipt identity is currently the command ID alone, not the normalized command.

Expected behavior

T3 should never replay an accepted receipt as success for a command addressed to a different aggregate or carrying a different payload.

At minimum, reusing a command ID with a different aggregateKind/aggregateId should return a typed conflict. Ideally, the stored receipt should include a normalized-command fingerprint and reject any same-ID/different-command request.

For a genuine retry of the identical command, the response should make the replay explicit and return enough receipt metadata to verify it, for example:

{
  "sequence": 123,
  "replayed": true,
  "aggregateKind": "thread",
  "aggregateId": "thread-A"
}

Actual behavior

The second dispatch returns HTTP 200 with the first command's accepted sequence N. No message or turn is created on thread-B.

The persisted receipt remains associated with thread-A, but the caller receives no replay marker or aggregate identity and can therefore report that delivery to thread-B succeeded.

Current source explains the result:

  • OrchestrationEngine.ts looks up an existing receipt only by commandId and immediately returns its resultSequence when accepted. It does not compare the stored aggregate with the incoming command's aggregate.
  • 002_OrchestrationCommandReceipts.ts makes command_id the sole primary key. Aggregate kind and ID are stored but not part of receipt identity.
  • The dispatch HTTP response exposes only the sequence, so a client cannot distinguish a real same-command replay from a destination/payload collision.

This creates a false-success boundary for any external dispatcher that uses stable command IDs for exactly-once retries. A recovery or completion notification can be silently dropped when its destination changes, while the caller sees HTTP 200 and the old sequence.

Impact

Major degradation or frequent failure

This can indefinitely stall orchestration, completion notifications, or recovery flows and can make a cross-thread delivery attempt appear successful when nothing reached the intended thread. The durable receipt prevents an ordinary retry from repairing the delivery.

Version or commit

v0.0.32-nightly.20260802.980 / e60821f0e0d82a5d671ca3b94719c49d333921c8

The behavior is also present on current main at the same commit.

Environment

macOS, T3 Code Desktop nightly, local orchestration HTTP API. Provider/model independent; the failure occurs before provider execution.

Logs or stack traces

# Sanitized synthetic outcome
POST commandId=cmd-synthetic-replay aggregate=thread-A -> 200 {"sequence": N}
POST commandId=cmd-synthetic-replay aggregate=thread-B -> 200 {"sequence": N}

receipt command_id=cmd-synthetic-replay aggregate_id=thread-A status=accepted sequence=N
events on thread-B for cmd-synthetic-replay: 0

No private thread IDs, user messages, credentials, or provider data are involved in this reproduction.

Screenshots, recordings, or supporting files

No response.

Workaround

Clients can include destination aggregate and a durable semantic notification ID in their generated command IDs, then verify a command-correlated message/turn on the intended destination before reporting delivery.

That mitigates accidental cross-destination collisions but does not replace server-side validation: T3 should reject a reused command ID whose aggregate or normalized command differs from the stored receipt.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions