Repository navigation
[Bug]: /compact as the first message in a forked thread fails with "Cannot compact an empty thread" #24160
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/server
Steps to reproduce
- Have a Claude thread with several user/assistant turns.
- Fork it from one of its runs (thread payload ends up with
forkedFrom: { type: "run", threadId: <parent>, runId: <parent run> }). - In the new fork, send
/compactas the very first message.
Expected behavior
The fork inherits the parent's conversation, so /compact should compact that inherited context, the same as /compact on the parent.
Actual behavior
The run fails immediately without reaching the provider:
Cannot compact an empty thread
Start a conversation before compacting this thread.
If the fork already has at least one non-/compact user message of its own, /compact works. Only a fork whose first message is /compact hits this.
Cause
ProviderTurnStartService short-circuits /compact when !projection.hasConversation:
t3code/apps/server/src/orchestration-v2/ProviderTurnStartService.ts
Lines 390 to 391 in 66ee628
| const isEmptyCompaction = | |
| message.text.trim().toLowerCase() === "/compact" && !projection.hasConversation; |
hasConversation only counts user messages whose thread_id is the current thread:
t3code/apps/server/src/orchestration-v2/ProjectionStore.ts
Lines 4259 to 4266 in 66ee628
| const conversation = yield* sql<{ present: number }>` | |
| SELECT EXISTS( | |
| SELECT 1 FROM orchestration_v2_projection_messages | |
| WHERE thread_id = ${threadId} AND role = 'user' | |
| AND (lower(trim(json_extract(payload_json, '$.text'), ${javascriptTrimWhitespace})) <> '/compact' | |
| OR json_array_length(payload_json, '$.attachments') > 0) | |
| ) AS present | |
| `; |
A fork does not copy the parent's messages. It only references them through thread.forkedFrom, and its provider thread is still pending (nativeThreadRef: null, status: "not_loaded", forkedFrom.providerThreadId set) until the first real run forks the native session. So for a fresh fork hasConversation is false even though the fork has a full inherited history.
State from an affected install (statev2.sqlite):
| thread | own user messages | /compact result |
|---|---|---|
| parent | 7 | — |
| fork (run fork of the parent) | 1 (/compact only) |
"Cannot compact an empty thread" |
another fork with 4 own messages before /compact |
10 | compacted normally |
Possible fix: treat the thread as having a conversation when it has forkedFrom (or when its pending provider thread has forkedFrom.providerThreadId), or include the inherited parent messages up to the fork point in the hasConversation check. The in-memory variant at ProjectionStore.ts#L7252 has the same shape.
Impact
Minor bug or occasional failure
Version or commit
v0.1.2 (installed runtime). The same code is on main @ 66ee628.
Environment
T3 server v0.1.2 on macOS (Mac mini), desktop GUI client on another Mac connected remotely. Provider: Claude Agent (claude-opus-5-5).
Workaround
Send any normal message in the fork first so it gets its own user message and native session, then run /compact (untested beyond the observation in the table above).
Activity