Skip to content
Merged
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
57 changes: 48 additions & 9 deletions apps/server/src/mcp/OrchestratorMcpService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,21 @@ function pendingTaskApproval(
return approval;
}

/** Every task `directAppOwnedChildTasks` can return, so callers load only those. */
function directChildTaskIds(
target: Pick<OrchestrationV2ThreadProjection, "thread" | "runs">,
returnedRunIds: ReadonlySet<RunId | null>,
): ReadonlyArray<NodeId> {
const ids = new Set<NodeId>();
if (target.thread.forkedFrom?.type === "node") ids.add(target.thread.forkedFrom.nodeId);
for (const run of target.runs) {
if (returnedRunIds.has(run.id) && run.delegatedTaskId !== undefined) {
ids.add(run.delegatedTaskId);
}
}
return [...ids];
}

function directAppOwnedChildTasks(
parent: Pick<OrchestrationV2ThreadProjection, "thread" | "subagents">,
target: Pick<OrchestrationV2ThreadProjection, "thread" | "runs">,
Expand Down Expand Up @@ -1016,7 +1031,6 @@ const make = Effect.gen(function* () {
[
"runs",
"messages",
"subagents",
"providerThreads",
"providerTurns",
"runtimeRequests",
Expand All @@ -1034,6 +1048,15 @@ const make = Effect.gen(function* () {
),
);

// A dispatcher thread holds thousands of tasks, each with its full prompt; read only the ones named.
const loadTasks = (threadId: ThreadId, taskIds: ReadonlyArray<NodeId>) =>
threadManagement.getThreadRecords(threadId, ["subagents"], { subagentIds: taskIds }).pipe(
Effect.map(({ subagents }) => subagents),
Effect.mapError((error) =>
failure("orchestration_error", `Unable to read thread ${threadId}: ${errorMessage(error)}`),
),
);

// Task waits read on every wake of a parent that may hold thousands of
// tasks, so they read only the task they watch and what its status needs.
const loadTaskParent = (threadId: ThreadId, taskId: NodeId) =>
Expand Down Expand Up @@ -2385,7 +2408,7 @@ const make = Effect.gen(function* () {
const childThreadId = current.childThreadId;
if (childThreadId === null) {
// The task runs in a linked environment: its thread there is stopped through the link.
const { subagents } = yield* loadProjection(scope.thread.threadId);
const subagents = yield* loadTasks(scope.thread.threadId, [input.taskId]);
const remoteTask = subagents.find(
(task) =>
task.id === input.taskId &&
Expand Down Expand Up @@ -2428,7 +2451,7 @@ const make = Effect.gen(function* () {
}
});
yield* assertStoppable(childThreadId);
const parentTask = parentProjection.subagents.find(
const parentTask = (yield* loadTasks(scope.thread.threadId, [input.taskId])).find(
(task) => task.id === input.taskId && task.origin === "app_owned",
);
const disposeCompletionDelivery =
Expand Down Expand Up @@ -2548,9 +2571,11 @@ const make = Effect.gen(function* () {
runId: parentTask.runId,
subagentId: parentTask.id,
});
const { subagents } = yield* threadManagement.getThreadRecords(scope.thread.threadId, [
"subagents",
]);
const { subagents } = yield* threadManagement.getThreadRecords(
scope.thread.threadId,
["subagents"],
{ subagentIds: [input.taskId] },
);
return subagents.some((task) => task.id === input.taskId && task.status === "cancelled");
}).pipe(Effect.mapError(threadManagementFailure));
if (isTerminalTaskStatus(current.status)) {
Expand Down Expand Up @@ -2847,7 +2872,19 @@ const make = Effect.gen(function* () {
page.filter((row) => row.sourceThreadId === target.thread.id).map((row) => row.item.runId),
);
const tasks =
parent === undefined ? [] : directAppOwnedChildTasks(parent, target, returnedRunIds);
parent === undefined
? []
: directAppOwnedChildTasks(
{
thread: parent.thread,
subagents: yield* loadTasks(
parent.thread.id,
directChildTaskIds(target, returnedRunIds),
),
},
target,
returnedRunIds,
);
if (
parent !== undefined &&
scope.thread !== undefined &&
Expand Down Expand Up @@ -2978,14 +3015,16 @@ const make = Effect.gen(function* () {
const ownedTask =
result.timedOut &&
status !== "waiting" &&
parent?.subagents.some(
parent !== undefined &&
taskId !== undefined &&
(yield* loadTasks(parent.thread.id, [taskId])).some(
(task) =>
task.id === taskId &&
task.origin === "app_owned" &&
task.threadId === parent.thread.id &&
task.childThreadId === input.threadId &&
task.completionDelivery?.state !== "disposed",
) === true;
);
// A child waiting on a question or approval needs an answer, not a wake.
const childRequests = ownedTask
? (yield* threadManagement
Expand Down
5 changes: 3 additions & 2 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4944,7 +4944,8 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
});

// Claude has no recovered frame; the next assistant message proves recovery.
const completeProviderRetry = Effect.fn("ClaudeAdapterV2.completeProviderRetry")(function* (
// Runs on every model output frame and is almost always a no-op; only a pending retry gets a span.
const completeProviderRetry = Effect.fnUntraced(function* (
context: ActiveClaudeTurnContext,
updatedAt: DateTime.Utc,
) {
Expand Down Expand Up @@ -4977,7 +4978,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
startedAt: providerRetry.startedAt,
updatedAt,
}),
});
}).pipe(Effect.withSpan("ClaudeAdapterV2.completeProviderRetry"));
});

const rememberProviderThread =
Expand Down
5 changes: 3 additions & 2 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1945,7 +1945,8 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* (
});

// Only new model output proves recovery; local item/completed can arrive during upstream retries.
const completeProviderRetry = Effect.fn("CodexAdapterV2.completeProviderRetry")(function* (
// Runs on every model output frame and is almost always a no-op; only a pending retry gets a span.
const completeProviderRetry = Effect.fnUntraced(function* (
context: ActiveCodexTurnContext,
updatedAt: DateTime.Utc,
) {
Expand All @@ -1965,7 +1966,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* (
type: "turn_item.updated",
driver: CODEX_PROVIDER,
turnItem: codexRetryTurnItem(context, providerRetry, "completed", updatedAt),
});
}).pipe(Effect.withSpan("CodexAdapterV2.completeProviderRetry"));
});

const registerRootTurn = Effect.fn("CodexAdapterV2.registerRootTurn")(function* (input: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ const seedParentWithTerminalTask = (input: {
readonly deliveryState: "delivered" | "claimed" | "acknowledged" | "disposed";
readonly completionWake?: "always" | "settled_only";
readonly deliveryTaskIds?: ReadonlyArray<NodeId>;
readonly parentRunStatus?: "running" | "completed";
readonly siblingTasks?: ReadonlyArray<{
readonly id: NodeId;
readonly deliveryState: "delivered" | "claimed";
}>;
readonly now: DateTime.Utc;
}) =>
Effect.gen(function* () {
Expand Down Expand Up @@ -241,10 +246,10 @@ const seedParentWithTerminalTask = (input: {
userMessageId: MessageId.make(`message:seed-user:${input.threadId}`),
rootNodeId: input.rootNodeId,
activeAttemptId: null,
status: "running",
status: input.parentRunStatus ?? "running",
requestedAt: input.now,
startedAt: input.now,
completedAt: null,
completedAt: input.parentRunStatus === "completed" ? input.now : null,
checkpointId: null,
contextHandoffId: null,
delegatedCompletion: {
Expand All @@ -261,22 +266,25 @@ const seedParentWithTerminalTask = (input: {
},
},
},
{
id: EventId.make(`event:seed-task:${input.threadId}`),
type: "subagent.updated",
...[
{ id: input.taskId, deliveryState: input.deliveryState },
...(input.siblingTasks ?? []),
].map((task) => ({
id: EventId.make(`event:seed-task:${input.threadId}:${task.id}`),
type: "subagent.updated" as const,
threadId: input.threadId,
runId: input.runId,
nodeId: input.taskId,
nodeId: task.id,
driver,
providerInstanceId: modelSelection.instanceId,
occurredAt: input.now,
payload: {
id: input.taskId,
id: task.id,
threadId: input.threadId,
runId: input.runId,
parentNodeId: input.rootNodeId,
origin: "app_owned",
createdBy: "agent",
origin: "app_owned" as const,
createdBy: "agent" as const,
driver,
providerInstanceId: modelSelection.instanceId,
providerThreadId: null,
Expand All @@ -287,16 +295,16 @@ const seedParentWithTerminalTask = (input: {
model: null,
completionWake: input.completionWake ?? "settled_only",
completionDelivery: {
state: input.deliveryState,
observedByRunId: input.deliveryState === "acknowledged" ? input.runId : null,
state: task.deliveryState,
observedByRunId: task.deliveryState === "acknowledged" ? input.runId : null,
},
status: "completed",
status: "completed" as const,
result: "child finished",
startedAt: input.now,
completedAt: input.now,
updatedAt: input.now,
},
},
})),
],
});
});
Expand Down Expand Up @@ -683,6 +691,57 @@ it.layer(layerTest)("delegated completion delivery repairs", (it) => {
}),
);

it.effect("counts already delivered siblings in the completion notice", () =>
Effect.gen(function* () {
const orchestrator = yield* Orchestrator.OrchestratorV2;
const now = yield* DateTime.now;
const threadId = ThreadId.make("thread:delegated-delivery-notice-count");
const runId = RunId.make("run:delegated-delivery-notice-count");
const firstTaskId = NodeId.make("node:delegated-delivery-notice-count-first");
const secondTaskId = NodeId.make("node:delegated-delivery-notice-count-second");

yield* seedParentWithTerminalTask({
threadId,
projectId: ProjectId.make("project:delegated-delivery-notice-count"),
runId,
rootNodeId: NodeId.make("node:delegated-delivery-notice-count-root"),
taskId: firstTaskId,
deliveryState: "claimed",
completionWake: "always",
deliveryTaskIds: [firstTaskId, secondTaskId],
parentRunStatus: "completed",
siblingTasks: [
{ id: secondTaskId, deliveryState: "claimed" },
{
id: NodeId.make("node:delegated-delivery-notice-count-earlier"),
deliveryState: "delivered",
},
],
now,
});

yield* orchestrator.dispatch({
type: "message.dispatch",
commandId: CommandId.make("command:delegated-delivery-notice-count"),
threadId,
messageId: MessageId.make(`message:delegated-delivery:${threadId}`),
text: "Delegated tasks reached a terminal state.",
attachments: [],
dispatchMode: { type: "queue_after_active" },
createdBy: "agent",
creationSource: "server",
delegatedCompletion: { parentRunId: runId, generation: 1, taskIds: [firstTaskId] },
});

const projection = yield* orchestrator.getThreadProjection(threadId);
const notice = projection.turnItems.find((item) => item.type === "notification");
assert.match(
notice?.type === "notification" ? notice.summary : "",
/^2 of 3 delegated tasks finished:/,
);
}),
);

it.effect.each([
{
name: "oversized UTF-8 result",
Expand Down
Loading
Loading