[Bug]: Thread context references show thread_<uuid>, but t3_thread_read only accepts the bare UUID#15849

opened18d agobyYYL404

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

Summary

When a user attaches a thread as context, the provider prompt renders an inline marker [Thread: <title>; ref=thread_<uuid>], and the stored message keeps the link t3-context://v1/thread/thread_<uuid>. The thread-scoped MCP tools resolve threadId by exact lookup, so an agent that copies the ID from the marker or link fails. Only the bare UUID carried in the injected <t3_context> envelope (threadId: <uuid>) works.

On the current build the failure is masked by #15566 as an output-schema error, so the agent cannot tell it is an ID-format problem. In a live session I watched the agent fail with the prefixed ID and recover only after stripping the prefix on retry.

Steps to reproduce

  1. In a running thread, attach another thread as context (composer context chip) and send the message.

  2. As the agent, call t3_thread_read with the ID exactly as displayed in the reference — the ref= value in the marker, or the last path segment of the t3-context://v1/thread/... link:

    json
    1{"threadId": "thread_<uuid>"}
  3. The call fails. With #15566 unfixed this surfaces as:

    1Structured content does not match the tool's output schema: data must have required property 'thread', data must have required property 'recentRuns', ...

    The server trace for the call shows the failed lookup:

    1McpServer.@effect/mcp/2025-06-18/tools/call tool=t3_thread_read parameters={"threadId":"thread_61431f2f-5a89-4dc9-8d6d-a2a357d20238"} exit=Success
    2sql.transaction exit=Failure cause=ProjectionStoreThreadNotFoundError: No orchestration projection exists ...
  4. Call again with the bare UUID from the envelope's threadId: line and it succeeds.

Expected behavior

The reference ID form the product displays should be accepted by the tool the injected context tells the agent to use, or the reference should display the canonical bare thread ID. Accepting both forms server-side (normalizing a leading thread_) seems least disruptive, because the marker ref is also the pairing key for the <t3_context> envelope entry while the payload already carries the bare threadId.

Actual behavior

t3_thread_read({threadId: "thread_<uuid>"}) fails with thread_not_found (masked as the #15566 schema error). t3_thread_read({threadId: "<uuid>"}) succeeds.

Relevant code:

  • packages/shared/src/composerContextReferences.ts:121-131 — the provider marker emits ref=<contextId>.
  • packages/shared/src/composerContextReferences.ts:236-242 — the thread payload carries the bare threadId.
  • apps/web/src/lib/composerContextReferences.ts:55-70, 160-180 — thread context ids are built as thread_<uuid>.
  • apps/server/src/mcp/OrchestratorMcpService.ts:849-875 — loadReadableThread does an exact lookup, and the attached-thread fallback set stores bare ids, so the prefixed form never resolves.

Impact

Minor bug or occasional failure, but it affects every attached-thread reference for agents that copy the visible ID. The intended path works, so this is a discoverability/robustness trap rather than a hard block.

Version or commit

0.0.46-nightly.20261004.2657 (live reproduction); source inspection on current main.

Environment

macOS, T3 Code desktop nightly, provider opencode (opencode-go).

Logs or stack traces

shell
1McpServer.@effect/mcp/2025-06-18/tools/call tool=t3_thread_read parameters={"threadId":"thread_61431f2f-5a89-4dc9-8d6d-a2a357d20238"} exit=Success
2sql.transaction exit=Failure cause=ProjectionStoreThreadNotFoundError: No orchestration projection exists ...

Workaround

Pass the bare UUID from the <t3_context> envelope instead of the displayed ref.

Note

Grok responding on behalf of Julius.

Triage

Thanks @YL404 for the precise write-up and the server trace. Pinning down exactly which ID form works made this easy to follow. It reproduces on current main (801ca76ed6) and on v0.0.46-nightly.20261004.2657, and it's separate from #15566.

What I found

Two different IDs are in play:

  • The inline marker ref= and the t3-context://v1/thread/... link carry the context id, which is the key that pairs the marker with its <t3_context> entry. For a desktop thread that's thread_<uuid>: desktop mints thread ids with newThreadId() (apps/web/src/lib/utils.ts), and threadContextId prefixes it with thread_ (apps/web/src/lib/composerContextRecords.ts) via toKindScopedComposerContextId in apps/web/src/lib/composerContextReferences.ts.
  • The tool argument is the threadId: line inside the envelope. t3_thread_read looks that value up exactly, and userAttachedThreadIds stores record.threadId, not contextId (apps/server/src/mcp/OrchestratorMcpService.ts). So thread_<uuid> misses both the project lookup and the attached-thread fallback and becomes ProjectionStoreThreadNotFoundError, which #15566 then masks as the schema error (failureMode: "return").

The envelope does tell the agent to call t3_thread_read(threadId), and the provider projection test locks in ref=thread_abc next to threadId: abc. The trap is that ref= is the first ID in the marker line and looks like a thread id.

One caveat on prefix stripping: it only recovers client-minted UUID ids. Server-minted ids contain : (for example thread:project:<projectId>:<uuid>, thread:delegated-task:<commandId>, and imported import:... ids). Those don't fit the context-id pattern and are stored as a slug plus a hash, so stripping thread_ wouldn't map them back. Replacing ref= with the raw thread id would break marker/envelope pairing.

Likely fix area

  • One option is that after an exact miss, t3_thread_read resolves the supplied id against a user-authored attached-thread record's contextId and reads that record's threadId, keeping the existing rule that agent-written records don't grant access.
  • Another is normalizing a leading thread_ server-side, which covers the reported case but not server-minted ids.
  • Changing the marker text so the bare thread id is more prominent is a third, prompt-side option.

A maintainer will decide on the fix direction.

Synced from GitHub

I can implement this after you choose a direction. The triage lists three options:

  1. After an exact miss, resolve the supplied id against the contextId of a user-authored attached-thread record, and read that record's threadId.
  2. Strip a leading thread_ server-side.
  3. Change the marker text so the bare thread id is more prominent.

Option 1 looks like the most complete one to me. It covers server-minted ids with : that option 2 cannot map back. It also keeps the rule that agent-written records do not grant access. The change would be in OrchestratorMcpService.ts, with tests for a prefixed client id and a server-minted id.

Is option 1 acceptable, or do you prefer another option?

Synced from GitHub

Closing for housekeeping — still reproducible. The triage laid out three fix directions and @phant0um offered to implement once one is chosen; feel free to reopen if a maintainer picks a direction.