[Bug]: Archiving the thread you are in permanently wedges that project's new-thread draft (archived_at read-model asymmetry)
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:
- Open a project and start a thread normally. Send a prompt. The thread is created and works.
- Archive that thread while you are still on it (thread menu → Archive).
- Ask for a new thread in the same project (sidebar
+, keybinding, command palette — any entry point). - 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.
Server — requireThreadAbsent 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
- 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, aburnedThreadIdsset, or having the reuse gate consult archived shells too) covers both variants. - This one is recoverable server-side, unlike the deleted case where no
thread.restoreexists. 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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