Skip to content

[Bug]: /compact as the first message in a forked thread fails with "Cannot compact an empty thread" #24160

Closed

Description

@ardaaltinors

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

  1. Have a Claude thread with several user/assistant turns.
  2. Fork it from one of its runs (thread payload ends up with forkedFrom: { type: "run", threadId: <parent>, runId: <parent run> }).
  3. In the new fork, send /compact as 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:

const isEmptyCompaction =
message.text.trim().toLowerCase() === "/compact" && !projection.hasConversation;

hasConversation only counts user messages whose thread_id is the current thread:

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).

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions