Skip to content

[Bug]: An agent that receives a t3_thread_send message can't tell which thread sent it (senderThreadId is recorded but t3_thread_read omits it) #17517

Open

Description

@pawelwitek34

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. Open a Claude thread (parent) and have it call delegate_task twice, so it has two child threads. I used Codex gpt-6.1-sol (reasoning low) and Codex gpt-6-luna.
  2. Open one child thread and ask it to send a message to its parent with t3_thread_send.
  3. In the parent, ask the agent which child sent that message, using only what it received and what t3_thread_read returns for its own thread.

Expected behavior

The receiving agent can identify the sender. The server already records senderThreadId on the message and the web and mobile timelines use it, so the same value should be available to the agent: as a senderThreadId field on the item returned by t3_thread_read, and ideally as a server-authored line in the delivered turn so the agent does not need a second call.

Actual behavior

The message arrives in the parent as a plain user turn. Nothing in it names the sending thread or its model.

t3_thread_read on the parent returns this for the item (IDs shortened):

type: "user_message"
createdBy: "agent"
creationSource: "mcp"
sourceThreadId: "<the parent's own thread id>"
messageId: "message:mcp:<uuid>:thread-send:<clientRequestId>"

createdBy and creationSource show that an agent sent it, but not which one. sourceThreadId is the receiving thread. The messageId carries the sender's clientRequestId and no thread id. t3_thread_transfers lists only the delegate_task spawn and result transfers, so the message is not there either.

The parent could only identify the sender indirectly: it read the timeline of each child thread and found the t3_thread_send call whose returned messageId matched. That works only for threads the receiver already knows about and can read.

Once the sender is known, its model and effort are easy to get (t3_thread_list and t3_thread_configuration returned gpt-6.1-sol and low correctly). The missing piece is the sender's thread id.

In the code at main @ ec80933:

  • sendToThread passes senderThreadId to ThreadManagementService:
    ...(parent === undefined ? {} : { senderThreadId: parent.thread.id }),
  • timelineItem, which builds the t3_thread_read items, returns createdBy and creationSource but not senderThreadId:
    function timelineItem(input: {
    readonly row: OrchestrationV2ThreadProjection["visibleTurnItems"][number];
    readonly maxChars: number;
    readonly textOffset?: number;
    readonly messagesByThreadId: ReadonlyMap<ThreadId, OrchestrationV2ThreadProjection["messages"]>;
    }): OrchestratorMcpThreadTimelineItem {
    const text = turnItemText(input.row.item);
    const offset = input.textOffset ?? 0;
    const end = offset + input.maxChars;
    const textTruncated = text !== null && text.length > end;
    const messageId =
    input.row.item.type === "user_message" || input.row.item.type === "assistant_message"
    ? input.row.item.messageId
    : null;
    const message =
    messageId === null
    ? undefined
    : input.messagesByThreadId
    .get(input.row.sourceThreadId)
    ?.find((candidate) => candidate.id === messageId);
    return {
    position: input.row.position,
    visibility: input.row.visibility,
    sourceThreadId: input.row.sourceThreadId,
    itemId: input.row.sourceItemId,
    runId: input.row.item.runId,
    messageId,
    createdBy: message?.createdBy ?? null,
    creationSource: message?.creationSource ?? null,
    type: input.row.item.type,
    status: input.row.item.status,
    title: input.row.item.title,
    text: text === null ? null : text.slice(offset, end),
    textTruncated,
    nextTextOffset: textTruncated ? end : null,
    updatedAt: DateTime.formatIso(input.row.item.updatedAt),
    };

The line references are from main, not from the exact nightly build I ran; the behavior itself was observed live.

Related: #6573 proposed server-authored sender attribution for inter-thread messages and was closed unmerged during backlog cleanup. #15250 is a different problem (a Codex child's model reported as "Not reported").

Impact

Minor bug or occasional failure

Version or commit

T3 Code (Nightly) 0.0.46-nightly.20261009.2873

Environment

macOS 15.7.7, Apple Silicon, desktop app. Parent: Claude Code 2.1.295, claude-opus-5-5. Children: Codex CLI 0.162.0, gpt-6.1-sol (reasoning low) and gpt-6-luna.

Logs or stack traces

Sender side (child thread, gpt-6.1-sol), t3_thread_send result:
{
  "threadId": "<parent-thread-id>",
  "messageId": "message:mcp:<uuid>:thread-send:subagent-connectivity-proof-9283-20261009",
  "runId": "run:thread:<parent-thread-id>:ordinal:2",
  "status": "starting",
  "delivery": "started"
}

Receiver side (parent thread), the same message in t3_thread_read:
{
  "sourceThreadId": "<parent-thread-id>",
  "messageId": "message:mcp:<uuid>:thread-send:subagent-connectivity-proof-9283-20261009",
  "createdBy": "agent",
  "creationSource": "mcp",
  "type": "user_message"
}

Screenshots, recordings, or supporting files

No response

Workaround

Have the sender put its own threadId in the message text, or have the receiver read each candidate thread with t3_thread_read and match the messageId returned by the sender's t3_thread_send call. The first relies on the sender being truthful, the second only works for threads the receiver can already read.

Activity

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