Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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
167 changes: 96 additions & 71 deletions apps/server/src/mcp/OrchestratorMcpService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
type OrchestratorMcpCapabilitiesResult,
OrchestratorMcpTarget,
type OrchestrationV2ThreadShell,
type OrchestrationV2Run,
type OrchestrationV2Subagent,
type ScheduledTask,
ScheduledTaskId,
ProjectId,
Expand Down Expand Up @@ -2413,78 +2415,101 @@ describe("OrchestratorMcpService provider resolution", () => {
}),
);
});
it.effect(
"reports the child run's selection after a restart and the thread selection before launch",
() =>
Effect.gen(function* () {
const runSelection = {
instanceId: ProviderInstanceId.make("claudeAgent-personal"),
model: "claude-opus-4-6",
options: [
{ id: "effort", value: "max" },
{ id: "fastMode", value: true },
],
} as const;
const threadSelection = {
instanceId: codexInstanceId,
model: "gpt-5.4",
options: [{ id: "reasoningEffort", value: "high" }],
} as const;
let hasRun = true;
const task = {
id: taskId,
threadId: parentThreadId,
origin: "app_owned",
childThreadId,
providerInstanceId: ProviderInstanceId.make("claudeAgent-work"),
model: "claude-sonnet-4-6",
status: "running",
result: null,
};
const dependencies = Layer.mergeAll(
NodeServices.layer,
Layer.mock(ThreadManagementService.ThreadManagementService)({
getThreadRecords: (threadId) =>
Effect.succeed(
threadId === parentThreadId
? parentProjection([task])
: ({
...childProjection,
thread: { ...childProjection.thread, modelSelection: threadSelection },
runs: hasRun
? [
{
id: RunId.make("run:mcp-restarted-child"),
ordinal: 1,
status: "running",
modelSelection: runSelection,
},
]
: [],
} as unknown as OrchestrationV2ThreadProjection),
),
}),
ServerSettings.layerTest(),
providerRegistryLayer([]),
adapterRegistryLayer([]),
Layer.mock(ProjectService.ProjectService)({}),
Layer.mock(SecretRequests.SecretRequests)({}),
Layer.mock(RemoteDelegation.RemoteDelegation)({}),
Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
);
yield* Effect.gen(function* () {
const service = yield* OrchestratorMcpService.OrchestratorMcpService;
for (const selection of [runSelection, threadSelection]) {
const result = yield* service.taskStatus(scope, taskId);
assert.equal(result.providerInstanceId, selection.instanceId);
assert.equal(result.model, selection.model);
assert.deepEqual(result.options, selection.options);
hasRun = false;
}
}).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(dependencies))));
}),
);
it.effect.each([
{ phase: "after restart", hasRun: true },
{ phase: "before launch", hasRun: false },
])("reports the child selection $phase with execution provenance when available", ({ hasRun }) =>
Effect.gen(function* () {
const runSelection = {
instanceId: ProviderInstanceId.make("claudeAgent-personal"),
model: "claude-opus-5-5",
options: [
{ id: "effort", value: "max" },
{ id: "fastMode", value: true },
],
} as const;
const threadSelection = {
instanceId: codexInstanceId,
model: "gpt-5.4",
options: [{ id: "reasoningEffort", value: "high" }],
} as const;
const child = idleThreadProjection(liveThreadShell(childThreadId));
const run = {
id: RunId.make("run:mcp-restarted-child"),
threadId: childThreadId,
ordinal: 1,
providerInstanceId: runSelection.instanceId,
modelSelection: runSelection,
observedModel: "claude-sonnet-5-5",
providerThreadId: null,
userMessageId: MessageId.make("message:mcp-restarted-child"),
rootNodeId: null,
activeAttemptId: null,
status: "running",
requestedAt: child.updatedAt,
startedAt: child.updatedAt,
completedAt: null,
checkpointId: null,
contextHandoffId: null,
} satisfies OrchestrationV2Run;
const task = {
id: taskId,
threadId: parentThreadId,
parentNodeId,
runId: null,
origin: "app_owned",
createdBy: "agent",
driver: ProviderDriverKind.make("claudeAgent"),
childThreadId,
providerThreadId: null,
nativeTaskRef: null,
prompt: "Review the implementation.",
title: null,
providerInstanceId: ProviderInstanceId.make("claudeAgent-work"),
model: "claude-sonnet-4-6",
status: "running",
result: null,
startedAt: child.updatedAt,
completedAt: null,
updatedAt: child.updatedAt,
} satisfies OrchestrationV2Subagent;
const parent = {
...idleThreadProjection(liveThreadShell(parentThreadId)),
subagents: [task],
};
const childWithRun = {
...child,
thread: { ...child.thread, modelSelection: threadSelection },
runs: hasRun ? [run] : [],
};
const dependencies = Layer.mergeAll(
NodeServices.layer,
Layer.mock(ThreadManagementService.ThreadManagementService)({
getThreadRecords: (id) => Effect.succeed(id === parentThreadId ? parent : childWithRun),
}),
ServerSettings.layerTest(),
providerRegistryLayer([]),
adapterRegistryLayer([]),
Layer.mock(ProjectService.ProjectService)({}),
Layer.mock(SecretRequests.SecretRequests)({}),
Layer.mock(RemoteDelegation.RemoteDelegation)({}),
Layer.mock(ScheduledTaskService.ScheduledTaskService)({}),
);
yield* Effect.gen(function* () {
const service = yield* OrchestratorMcpService.OrchestratorMcpService;
const result = yield* service.taskStatus(scope, taskId);

const selection = hasRun ? runSelection : threadSelection;
const observedModel = hasRun ? run.observedModel : undefined;

assert.equal(result.providerInstanceId, selection.instanceId);
assert.equal(result.model, observedModel ?? selection.model);
assert.equal(result.requestedModel, selection.model);
assert.equal(result.observedModel, observedModel);
assert.deepEqual(result.options, selection.options);
}).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(dependencies))));
}),
);
it.effect(
"inherits delegated options, merges overrides, and resets on explicit clearing or a new target",
() =>
Expand Down
17 changes: 13 additions & 4 deletions apps/server/src/mcp/OrchestratorMcpService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,9 @@ function threadDetail(
): OrchestratorMcpThreadDetail {
const latest = ThreadManagementService.latestRun(projection);
const active = ThreadManagementService.latestActiveRun(projection);
const modelSelection =
latest?.observedModel == null ? projection.thread.modelSelection : latest.modelSelection;

return {
threadId: projection.thread.id,
projectId: projection.thread.projectId,
Expand All @@ -800,8 +803,10 @@ function threadDetail(
status: active?.status ?? latest?.status ?? "idle",
latestRunId: latest?.id ?? null,
activeRunId: active?.id ?? null,
providerInstanceId: projection.thread.modelSelection.instanceId,
model: projection.thread.modelSelection.model,
providerInstanceId: modelSelection.instanceId,
model: latest?.observedModel ?? modelSelection.model,
requestedModel: modelSelection.model,
...(latest?.observedModel == null ? {} : { observedModel: latest.observedModel }),
runtimeMode: projection.thread.runtimeMode,
interactionMode: projection.thread.interactionMode,
linkedPullRequest: projection.thread.linkedPullRequest ?? null,
Expand Down Expand Up @@ -837,7 +842,9 @@ function threadRun(run: OrchestrationV2Run): OrchestratorMcpThreadRun {
ordinal: run.ordinal,
status: run.status,
providerInstanceId: run.modelSelection.instanceId,
model: run.modelSelection.model,
model: run.observedModel ?? run.modelSelection.model,
requestedModel: run.modelSelection.model,
...(run.observedModel == null ? {} : { observedModel: run.observedModel }),
requestedAt: DateTime.formatIso(run.requestedAt),
startedAt: run.startedAt === null ? null : DateTime.formatIso(run.startedAt),
completedAt: run.completedAt === null ? null : DateTime.formatIso(run.completedAt),
Expand Down Expand Up @@ -1535,7 +1542,9 @@ const make = Effect.gen(function* () {
workState,
hasPendingChildRuns: hasPendingChildRuns(childProjection, childRun),
providerInstanceId: modelSelection.instanceId,
model: modelSelection.model,
model: childRun?.observedModel ?? modelSelection.model,
requestedModel: modelSelection.model,
...(childRun?.observedModel == null ? {} : { observedModel: childRun.observedModel }),
options: modelSelection.options ?? [],
summary: derivedResult,
resultContextTransferId,
Expand Down
Loading
Loading