Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions apps/server/src/mcp/OrchestratorMcpService.activity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,3 +531,113 @@ it("readThread and sendToThread reach threads in other projects", async () => {
expect(staleDelete.code).toBe("parent_not_active");
}).pipe(Effect.provide(layer), Effect.runPromise);
});

it("readThread names the thread that sent a message with t3_thread_send", async () => {
const userMessageRow = (position: number, messageId: string, text: string) => ({
position,
visibility: "local",
sourceThreadId: parentThreadId,
sourceItemId: `item-${messageId}`,
item: {
id: `item-${messageId}`,
type: "user_message",
messageId,
runId: activeRunId,
status: "completed",
title: null,
text,
createdAt: now,
updatedAt: now,
},
});
const visibleTurnItems = [
userMessageRow(0, "message-from-child", "Child reporting in"),
userMessageRow(1, "message-from-user", "Thanks"),
];
const projection = {
thread: baseThread({
threadId: parentThreadId,
title: "Parent",
instanceId: parentInstanceId,
model: "gpt-5.4",
}),
runs: [makeRun({ id: activeRunId, ordinal: 1, status: "running" })],
visibleTurnItems,
runtimeRequests: [],
messages: [
{
id: "message-from-child",
threadId: parentThreadId,
role: "user",
text: "Child reporting in",
createdBy: "agent",
creationSource: "mcp",
senderThreadId: childThreadId,
},
{
id: "message-from-user",
threadId: parentThreadId,
role: "user",
text: "Thanks",
createdBy: "user",
creationSource: "web",
},
],
contextTransfers: [],
subagents: [],
updatedAt: now,
} as unknown as OrchestrationV2ThreadProjection;

const layer = layerOrchestratorService.pipe(
Layer.provide(
Layer.mergeAll(
Layer.mock(ThreadManagementService.ThreadManagementService)({
getTimelinePage: () =>
Effect.succeed({
items: visibleTurnItems,
totalItems: visibleTurnItems.length,
hasMore: false,
} as never),
getThreadRecords: (threadId) =>
threadId === parentThreadId
? Effect.succeed(projection)
: Effect.die(`unexpected thread ${threadId}`),
getThreadShell: (threadId) =>
Effect.succeed(
threadId === parentThreadId
? (projection.thread as unknown as OrchestrationV2ThreadShell)
: null,
),
getProjectThreadRecords: (input) =>
input.threadId === parentThreadId
? Effect.succeed(projection)
: Effect.die(`unexpected thread ${input.threadId}`),
} satisfies Partial<ThreadManagementService.ThreadManagementService["Service"]>),
ServerSettings.layerTest(),
Layer.mock(ProviderRegistry.ProviderRegistry)({
getProviders: Effect.succeed([]),
} satisfies Partial<ProviderRegistry.ProviderRegistry["Service"]>),
Layer.mock(ProjectService.ProjectService)({}),
Layer.mock(SecretRequests.SecretRequests)({}),
Layer.mock(ScheduledTaskService.ScheduledTaskService)({
list: () => Effect.succeed({ tasks: [] }),
} satisfies Partial<ScheduledTaskService.ScheduledTaskService["Service"]>),
Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({
list: () => Effect.succeed([]),
} satisfies Partial<ProviderAdapterRegistry.ProviderAdapterRegistryV2["Service"]>),
NodeCrypto.layer,
),
),
);

await Effect.gen(function* () {
const service = yield* OrchestratorMcpService.OrchestratorMcpService;
const result = yield* service.readThread(makeScope(), { threadId: parentThreadId });
const [fromChild, fromUser] = result.items;
expect(fromChild?.messageId).toBe("message-from-child");
expect(fromChild?.senderThreadId).toBe(childThreadId);
expect(fromUser?.messageId).toBe("message-from-user");
// Messages without a sending thread omit the field rather than returning null.
expect(fromUser).not.toHaveProperty("senderThreadId");
}).pipe(Effect.provide(layer), Effect.runPromise);
});
1 change: 1 addition & 0 deletions apps/server/src/mcp/OrchestratorMcpService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,7 @@ function timelineItem(input: {
messageId,
createdBy: message?.createdBy ?? null,
creationSource: message?.creationSource ?? null,
...(message?.senderThreadId === undefined ? {} : { senderThreadId: message.senderThreadId }),
type: input.row.item.type,
status: input.row.item.status,
title: input.row.item.title,
Expand Down
2 changes: 2 additions & 0 deletions packages/contracts/src/orchestratorMcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,8 @@ export const OrchestratorMcpThreadTimelineItem = Schema.Struct({
messageId: Schema.NullOr(MessageId),
createdBy: Schema.NullOr(OrchestrationV2Actor),
creationSource: Schema.NullOr(OrchestrationV2CreationSource),
/** The agent thread that sent this message to another thread; omitted for other messages. */
senderThreadId: Schema.optional(ThreadId),
type: Schema.String,
status: OrchestrationV2TurnItemStatus,
title: Schema.NullOr(Schema.String),
Expand Down
Loading