Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
114 changes: 114 additions & 0 deletions apps/server/src/mcp/OrchestratorMcpService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,120 @@ describe("OrchestratorMcpService", () => {
}),
);

it.effect("omits latestTerminalSummary when it repeats the task summary", () =>
Effect.gen(function* () {
const parentThreadId = ThreadId.make("thread:mcp-dedupe-parent");
const childThreadId = ThreadId.make("thread:mcp-dedupe-child");
const taskId = NodeId.make("node:mcp-dedupe-task");
const taskRunId = RunId.make("run:mcp-dedupe-task");
const followupRunId = RunId.make("run:mcp-dedupe-followup");
const taskResult = "Delegated result. ".repeat(1_000);
const parentProjection = {
thread: { id: parentThreadId },
runs: [],
contextTransfers: [],
subagents: [
{
id: taskId,
threadId: parentThreadId,
origin: "app_owned",
childThreadId,
driver: "codex",
model: "gpt-5.6-terra",
status: "completed",
result: taskResult,
completionDelivery: { state: "acknowledged" },
},
],
} as unknown as OrchestrationV2ThreadProjection;
const taskRun = {
id: taskRunId,
ordinal: 1,
status: "completed",
delegatedTaskId: taskId,
startedAt: DateTime.makeUnsafe("2026-10-03T10:00:00Z"),
completedAt: DateTime.makeUnsafe("2026-10-03T10:10:00Z"),
};
const followupRun = {
id: followupRunId,
ordinal: 2,
status: "completed",
startedAt: DateTime.makeUnsafe("2026-10-03T10:20:00Z"),
completedAt: DateTime.makeUnsafe("2026-10-03T10:30:00Z"),
};
let followupText: string | null = null;
const childProjection = () =>
({
thread: { id: childThreadId },
runs: followupText === null ? [taskRun] : [taskRun, followupRun],
contextTransfers: [],
messages:
followupText === null
? []
: [
{
id: "message:mcp-dedupe-followup",
runId: followupRunId,
role: "assistant",
text: followupText,
updatedAt: followupRun.completedAt,
},
],
turnItems: [],
subagents: [],
providerThreads: [],
}) as unknown as OrchestrationV2ThreadProjection;
const layerDependencies = Layer.mergeAll(
NodeServices.layer,
Layer.mock(ThreadManagementService.ThreadManagementService)({
getThreadRecords: (threadId) =>
Effect.succeed(threadId === parentThreadId ? parentProjection : childProjection()),
}),
ServerSettings.layerTest(),
Layer.mock(ProviderRegistry.ProviderRegistry)({ getProviders: Effect.succeed([]) }),
Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({
list: () => Effect.succeed([]),
}),
Layer.mock(ProjectService.ProjectService)({}),
Layer.mock(SecretRequests.SecretRequests)({}),
Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
);
const scope: McpInvocationScope = {
environmentId: EnvironmentId.make("environment:mcp-dedupe"),
requestNamespace: "provider-session:mcp-dedupe",
thread: {
threadId: parentThreadId,
providerSessionId: "provider-session:mcp-dedupe",
providerInstanceId: ProviderInstanceId.make("codex"),
},
client: undefined,
capabilities: new Set(["orchestration"]),
issuedAt: 1,
};

yield* Effect.gen(function* () {
const service = yield* OrchestratorMcpService.OrchestratorMcpService;
const own = yield* service.taskStatus(scope, taskId);
assert.equal(own.summary, taskResult);
assert.equal(own.latestTerminalRunId, taskRunId);
assert.equal(own.latestTerminalStatus, "completed");
assert.isNull(own.latestTerminalSummary);

followupText = taskResult;
const identical = yield* service.taskStatus(scope, taskId);
assert.equal(identical.summary, taskResult);
assert.equal(identical.latestTerminalRunId, followupRunId);
assert.isNull(identical.latestTerminalSummary);

followupText = "Follow-up result.";
const distinct = yield* service.taskStatus(scope, taskId);
assert.equal(distinct.summary, taskResult);
assert.equal(distinct.latestTerminalRunId, followupRunId);
assert.equal(distinct.latestTerminalSummary, "Follow-up result.");
}).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(layerDependencies))));
}),
);

it.effect("reports a restart-cut child as working until its continuation settles", () =>
Effect.gen(function* () {
const parentThreadId = ThreadId.make("thread:mcp-restart-parent");
Expand Down
21 changes: 14 additions & 7 deletions apps/server/src/mcp/OrchestratorMcpService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1443,6 +1443,13 @@ const make = Effect.gen(function* () {
null);
const resultTransfer = taskResultTransfer(parentProjection, childProjection, task) ?? null;
const terminalStatus = terminalRun === undefined ? null : taskStatusForRun(terminalRun);
const latestTerminalSummary = canExposeTaskRunResult(terminalRun)
? terminalRun.id === childRun?.id
? derivedResult
: subagentResultForRun(childProjection, terminalRun).text
: null;
const latestTerminalTransferId = resultTransferForRun(terminalRun)?.id ?? null;
const resultContextTransferId = resultTransfer?.id ?? null;
const response = {
taskId: task.id,
childThreadId: task.childThreadId,
Expand All @@ -1454,16 +1461,16 @@ const make = Effect.gen(function* () {
providerInstanceId: task.providerInstanceId,
model: task.model,
summary: derivedResult,
resultContextTransferId: resultTransfer?.id ?? null,
resultContextTransferId,
latestTerminalRunId: terminalRun?.id ?? null,
latestTerminalStatus:
terminalStatus !== null && isTerminalTaskStatus(terminalStatus) ? terminalStatus : null,
latestTerminalSummary: canExposeTaskRunResult(terminalRun)
? terminalRun.id === childRun?.id
? derivedResult
: subagentResultForRun(childProjection, terminalRun).text
: null,
latestTerminalResultContextTransferId: resultTransferForRun(terminalRun)?.id ?? null,
// Null when it would repeat summary/resultContextTransferId, so the
// task's own result is not shipped twice.
latestTerminalSummary:
latestTerminalSummary === derivedResult ? null : latestTerminalSummary,
latestTerminalResultContextTransferId:
latestTerminalTransferId === resultContextTransferId ? null : latestTerminalTransferId,
// A finished task has no question of its own; later child runs belong to other tasks.
hasPendingQuestion:
!isTerminalTaskStatus(status) &&
Expand Down
14 changes: 6 additions & 8 deletions apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1801,10 +1801,10 @@ describe("orchestrator MCP toolkit", () => {
hasPendingChildRuns: false,
latestTerminalRunId: delegated.childRunId,
latestTerminalStatus: "completed",
latestTerminalSummary: delegatedResult,
latestTerminalSummary: null,
latestTerminalResultContextTransferId: null,
});
expect(delegatedStatus.resultContextTransferId).not.toBeNull();
expect(delegatedStatus.latestTerminalResultContextTransferId).not.toBeNull();

// Wait-mode completion already returned its result while the parent was live.
yield* expectOffersToStay(0);
Expand Down Expand Up @@ -4045,12 +4045,10 @@ describe("orchestrator MCP toolkit", () => {
hasPendingChildRuns: false,
latestTerminalRunId: delegated.childRunId,
latestTerminalStatus: "completed",
latestTerminalSummary: delegatedResult,
latestTerminalSummary: null,
latestTerminalResultContextTransferId: null,
});
expect(delegated.resultContextTransferId).not.toBeNull();
expect(delegated.latestTerminalResultContextTransferId).toBe(
delegated.resultContextTransferId,
);

// Delegated children are subagent threads too, but T3 owns them, so
// they keep taking follow-ups (provider-native children do not).
Expand Down Expand Up @@ -4146,8 +4144,8 @@ describe("orchestrator MCP toolkit", () => {
hasPendingChildRuns: true,
latestTerminalRunId: delegated.childRunId,
latestTerminalStatus: "completed",
latestTerminalSummary: delegatedResult,
latestTerminalResultContextTransferId: delegated.resultContextTransferId,
latestTerminalSummary: null,
latestTerminalResultContextTransferId: null,
});

const finalSequence = yield* orchestrator.getThreadEventSequence(delegated.childThreadId);
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/mcp/toolkits/orchestrator/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const DelegateTaskTool = Tool.make("delegate_task", {

const TaskStatusTool = Tool.make("task_status", {
description:
"Needs an agent running inside a T3 thread. Read a T3-owned delegated task created by this parent thread. childRunId identifies the original delegated run. workState distinguishes working, waiting_for_children, and result_available; a completed turn with live nested work is not a completed task. summary is the final task result, including provider errors on failure, and remains stable after publication. hasPendingChildRuns reports later queued or executing turns in the backing child thread, even after the task is terminal; it does not reopen the task, and task_cancel stops those turns too. Turns held in a stopped queue wait for the user and do not count. latestTerminal* provides later non-monitor turn results. Reading a terminal result acknowledges its automatic parent delivery.",
"Needs an agent running inside a T3 thread. Read a T3-owned delegated task created by this parent thread. childRunId identifies the original delegated run. workState distinguishes working, waiting_for_children, and result_available; a completed turn with live nested work is not a completed task. summary is the final task result, including provider errors on failure, and remains stable after publication. hasPendingChildRuns reports later queued or executing turns in the backing child thread, even after the task is terminal; it does not reopen the task, and task_cancel stops those turns too. Turns held in a stopped queue wait for the user and do not count. latestTerminal* provides later non-monitor turn results; latestTerminalSummary and latestTerminalResultContextTransferId are null when they would repeat summary and resultContextTransferId. Reading a terminal result acknowledges its automatic parent delivery.",
parameters: OrchestratorMcpTaskStatusInput,
success: OrchestratorMcpDelegateTaskResult,
failure: OrchestratorMcpFailure,
Expand Down
Loading