pingdotgg / pingdotgg/t3code

[Bug]: Archiving the thread you are in permanently wedges that project's new-thread draft (archived_at read-model asymmetry)

Open
#6,128 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
23k
Forks
5.9k
Avg merge
11h 14m
Merged PRs (30d)
357

Description

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

apps/web, apps/server

Steps to reproduce

No failure of any kind is required. On current main:

  1. Open a project and start a thread normally. Send a prompt. The thread is created and works.
  2. Archive that thread while you are still on it (thread menu → Archive).
  3. Ask for a new thread in the same project (sidebar +, keybinding, command palette — any entry point).
  4. Type a prompt and send.

Every send from then on fails, permanently, for that project in that browser profile.

Expected behavior

Archiving a thread should have no effect on the project's next draft. The new-thread flow should mint a fresh thread ID.

Actual behavior

The draft reuses the archived thread's ID, and the server rejects it:

Orchestration command invariant failed (thread.create):
Thread 'aa174e38-399d-442d-8c2e-0cd4f29aba58' already exists and cannot be created twice.

The draft is pinned to that ID in t3code:composer-drafts:v1, so it survives reloads and server restarts.

Mechanism: a second read-model asymmetry, on archived_at

This is the same symptom as #4647, but a different trigger and a different filter, so I'm filing it separately rather than commenting there — a fix scoped to bootstrap rollback would leave this variant alive.

#4647 covers the case where a bootstrap failure deletes the provisional thread, and @carlosricojr's analysis there traces it to the deleted_at IS NULL filter on client-facing projections. The archived case is a distinct path to the same wedge:

Client — the reuse gate only retires a stored draft when its thread ID resolves to a live shell (apps/web/src/hooks/useHandleNewThread.ts:161):

const reusableStoredDraftThread =
  storedDraftThreadRef && readThreadShell(storedDraftThreadRef) !== null
    ? null               // exists → mint a fresh draft
    : storedDraftThread; // → reuse this draft, including its threadId

readThreadShell reads environmentThreadShells (apps/web/src/state/entities.ts:222), which is fed by the live shell snapshot — WHERE deleted_at IS NULL AND archived_at IS NULL in ProjectionSnapshotQuery. Archived shells arrive through a separate atom family (archivedShellSnapshot / orchestration.getArchivedShellSnapshot, packages/client-runtime/src/state/orchestration.ts:32) that this gate never consults.

So readThreadShell returns null for an archived thread — not transiently, but structurally, regardless of what the user has loaded on screen. Opening the Archived section does not help.

ServerrequireThreadAbsent matches on ID alone, filtering neither deleted_at nor archived_at. The ID is taken forever.

Same closed loop as #4647: the client is structurally certain the ID is free, the server is structurally certain it is taken. The difference is that here nothing went wrong — no failed bootstrap, no tombstone, no rollback. The thread is alive and well, just archived. Any user who archives the thread they are working in walks into it.

Two consequences worth separating from #4647
  1. The fix surface differs. The server-side suggestion in #4647 — don't burn IDs when compensating a thread.create — does nothing here, because nothing was compensated. Only a client-side fix (mint-at-send, a burnedThreadIds set, or having the reuse gate consult archived shells too) covers both variants.
  2. This one is recoverable server-side, unlike the deleted case where no thread.restore exists. See Workaround.
Impact

Blocks work completely

Per-project and per-browser-profile. Other projects keep working, and the same account on another client is unaffected — which makes it look like a client glitch rather than persisted state.

Version or commit

Server 0.0.33-nightly.20260807.1026. Guard verified still present on main at 2026-08-11 (useHandleNewThread.ts:161-163).

Environment

Server on macOS 12.7.6 (Node 24.15.0), reached over the web client from iOS Safari. Two occurrences, both within three minutes of archiving:

Thread Archived First rejection
bce129d3 2026-07-26 12:49Z 2026-07-26 12:50Z
aa174e38 2026-08-09 15:32Z 2026-08-09 15:35Z
Logs or stack traces

From orchestration_command_receipts, the rejections carry a server-issued command ID, i.e. the UI's atomic thread.turn.start + bootstrap.createThread path:

2026-08-11T07:38:40.985Z  server:bootstrap-thread-create:097d9237…  rejected
  Orchestration command invariant failed (thread.create):
  Thread 'aa174e38-399d-442d-8c2e-0cd4f29aba58' already exists and cannot be created twice.

The thread row itself is intact — deleted_at null, archived_at set.

Workaround

Unarchive the thread. It returns to the live shell snapshot, the reuse gate sees it, and the next new-thread request mints a fresh ID. No DevTools needed, which matters on mobile and on the web client where the localStorage surgery in #4647 isn't available:

POST /api/orchestration/dispatch
{"type":"thread.unarchive","commandId":"<uuid>","threadId":"<uuid from the error>"}

Or just unarchive it from the UI on any client that can reach the archived list. Reload, start the new thread, then re-archive the old one if you want. Copy your prompt out first — it stays with the old draft.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by tracing the reuse gate in apps/web/src/hooks/useHandleNewThread.ts and the live-shell state in apps/web/src/state/entities.ts, then compare it with archivedShellSnapshot in packages/client-runtime/src/state/orchestration.ts. Done means archiving the current thread no longer causes a later new-thread send to reuse its ID, while the existing workaround remains unnecessary.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
backend, frontend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.