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
22 changes: 12 additions & 10 deletions apps/server/src/mcp/OrchestratorMcpService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1655,7 +1655,11 @@ const make = Effect.gen(function* () {
task.completionDelivery?.state !== "acknowledged" &&
task.completionDelivery?.state !== "disposed"
) {
const observingRun = ThreadManagementService.latestActiveRun(parentProjection);
const observingRun = ThreadManagementService.latestActiveRun({
runs: parentProjection.runs.filter(
(run) => run.providerInstanceId === scope.thread.providerInstanceId,
),
});
const acknowledgementRequestKey = yield* requestKey(undefined);
yield* threadManagement
.dispatch({
Expand All @@ -1667,10 +1671,7 @@ const make = Effect.gen(function* () {
}),
parentThreadId: scope.thread.threadId,
taskId,
observedByRunId:
observingRun?.providerInstanceId === scope.thread.providerInstanceId
? observingRun.id
: null,
observedByRunId: observingRun?.id ?? null,
})
.pipe(
Effect.mapError((error) =>
Expand Down Expand Up @@ -1750,7 +1751,11 @@ const make = Effect.gen(function* () {
),
),
);
const observingRun = ThreadManagementService.latestActiveRun(parentProjection);
const observingRun = ThreadManagementService.latestActiveRun({
runs: parentProjection.runs.filter(
(run) => run.providerInstanceId === scope.thread.providerInstanceId,
),
});
yield* threadManagement
.dispatch({
type: "delegated_task.completion-delivery.acknowledge",
Expand All @@ -1761,10 +1766,7 @@ const make = Effect.gen(function* () {
}),
parentThreadId: scope.thread.threadId,
taskId: task.id,
observedByRunId:
observingRun?.providerInstanceId === scope.thread.providerInstanceId
? observingRun.id
: null,
observedByRunId: observingRun?.id ?? null,
})
.pipe(
Effect.mapError((error) =>
Expand Down
205 changes: 143 additions & 62 deletions apps/server/src/orchestration-v2/DelegatedCompletionDelivery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1628,6 +1628,73 @@ it.layer(layerContinuationWorkerTest)("interrupted completion wakes", (it) => {
it.layer(Layer.merge(layerTest, ProviderContinuationRequests.layer))(
"subagent follow-ups",
(it) => {
it.effect("a late unobserved read wakes a pending result without restarting", () =>
Effect.gen(function* () {
const orchestrator = yield* Orchestrator.OrchestratorV2;
const sink = yield* EventSink.EventSinkV2;
const requests = yield* ProviderContinuationRequests.ProviderContinuationRequests;
const now = yield* DateTime.now;
const threadId = ThreadId.make("thread:late-read-live");
const runId = RunId.make("run:late-read-live");
const taskId = NodeId.make("task:late-read-live");
yield* seedParentWithTerminalTask({
threadId,
projectId: ProjectId.make("project:late-read-live"),
runId,
rootNodeId: NodeId.make("root:late-read-live"),
taskId,
deliveryState: "pending",
now,
});
const acknowledge = (key: string) =>
orchestrator.dispatch({
type: "delegated_task.completion-delivery.acknowledge",
commandId: CommandId.make(`command:late-read-live:${key}`),
parentThreadId: threadId,
taskId,
observedByRunId: null,
});
const before = yield* orchestrator.getThreadProjection(threadId);
const liveRead = yield* acknowledge("live");
assert.lengthOf(liveRead.storedEvents, 0);
const parent = before.runs.find((run) => run.id === runId)!;
yield* sink.write({
events: [
{
id: EventId.make("event:late-read-live:parent-ended"),
type: "run.updated",
threadId,
runId,
occurredAt: now,
payload: { ...parent, status: "completed", completedAt: now },
},
],
});
yield* acknowledge("ended");
const wake = yield* requests.take;
assert.equal(wake.threadId, threadId);
assert.equal(wake.delegatedCompletion?.parentRunId, runId);
const after = yield* orchestrator.getThreadProjection(threadId);
assert.equal(
after.subagents.find((task) => task.id === taskId)?.completionDelivery?.state,
"claimed",
);
assert.deepEqual(
after.runs.find((run) => run.id === runId)?.delegatedCompletion?.delivery?.taskIds,
[taskId],
);
const repeated = yield* acknowledge("repeated");
assert.lengthOf(repeated.storedEvents, 0);
assert.deepEqual(yield* orchestrator.getThreadProjection(threadId), after);
yield* orchestrator.dispatch({
type: "delegated_task.completion-delivery.dispose",
commandId: CommandId.make("command:late-read-live:dispose"),
parentThreadId: threadId,
taskId,
});
}),
);

it.effect("recovers a reserved delivery without loading conversation history", () =>
Effect.gen(function* () {
const orchestrator = yield* Orchestrator.OrchestratorV2;
Expand Down Expand Up @@ -3418,70 +3485,84 @@ it.layer(layerTest)("delegated tasks across a server restart", (it) => {
}),
);

it.effect("wakes the parent for a settled_only task once its spawning turn ended", () =>
Effect.gen(function* () {
const orchestrator = yield* Orchestrator.OrchestratorV2;
const eventSink = yield* EventSink.EventSinkV2;
const now = yield* DateTime.now;
const threadId = ThreadId.make("thread:settled-only-restart");
const projectId = ProjectId.make("project:settled-only-restart");
const runId = RunId.make("run:settled-only-restart");
const rootNodeId = NodeId.make("node:settled-only-restart-root");
yield* seedParentWithTerminalTask({
threadId,
projectId,
runId,
rootNodeId,
taskId: NodeId.make("node:settled-only-restart-settled"),
deliveryState: "delivered",
now,
});
yield* eventSink.write({
commandId: reconcileCommandId("settled-only-restart"),
events: [
runEvent({
threadId,
runId,
ordinal: 1,
status: "cancelled",
now,
providerThreadId: parentProviderThreadId(threadId),
userMessageId: MessageId.make(`message:seed-user:${threadId}`),
delegatedCompletion: { disposition: "open", nextGeneration: 2, delivery: null },
}),
runEvent({
threadId,
runId: RunId.make("run:settled-only-restart:continuation"),
ordinal: 2,
status: "running",
now,
providerThreadId: parentProviderThreadId(threadId),
}),
],
});
const child = yield* seedRestartCancelledChild({
parentThreadId: threadId,
projectId,
parentRunId: runId,
rootNodeId,
name: "settled-only-restart-child",
completionWake: "settled_only",
continuationPending: false,
now,
});
it.effect.each([false, true])(
"wakes the parent for a settled_only result with a running continuation: %s",
(runningContinuation) =>
Effect.gen(function* () {
const orchestrator = yield* Orchestrator.OrchestratorV2;
const eventSink = yield* EventSink.EventSinkV2;
const now = yield* DateTime.now;
const suffix = runningContinuation ? "running" : "idle";
const threadId = ThreadId.make(`thread:settled-only-restart-${suffix}`);
const projectId = ProjectId.make(`project:settled-only-restart-${suffix}`);
const runId = RunId.make(`run:settled-only-restart-${suffix}`);
const rootNodeId = NodeId.make(`node:settled-only-restart-root-${suffix}`);
yield* seedParentWithTerminalTask({
threadId,
projectId,
runId,
rootNodeId,
taskId: NodeId.make(`node:settled-only-restart-settled-${suffix}`),
deliveryState: "delivered",
now,
});
yield* eventSink.write({
commandId: reconcileCommandId(`settled-only-restart-${suffix}`),
events: [
runEvent({
threadId,
runId,
ordinal: 1,
status: "cancelled",
now,
providerThreadId: parentProviderThreadId(threadId),
userMessageId: MessageId.make(`message:seed-user:${threadId}`),
delegatedCompletion: { disposition: "open", nextGeneration: 2, delivery: null },
}),
...(runningContinuation
? [
runEvent({
threadId,
runId: RunId.make(`run:settled-only-restart:continuation-${suffix}`),
ordinal: 2,
status: "running",
now,
providerThreadId: parentProviderThreadId(threadId),
}),
]
: []),
],
});
const child = yield* seedRestartCancelledChild({
parentThreadId: threadId,
projectId,
parentRunId: runId,
rootNodeId,
name: `settled-only-restart-child-${suffix}`,
completionWake: "settled_only",
continuationPending: false,
now,
});

yield* orchestrator.recoverDelegatedTasks;
yield* orchestrator.dispatch({
type: "delegated_task.completion-delivery.acknowledge",
commandId: CommandId.make(`command:settled-only-late-read-${suffix}`),
parentThreadId: threadId,
taskId: child.taskId,
observedByRunId: null,
});
yield* orchestrator.recoverDelegatedTasks;

const projection = yield* orchestrator.getThreadProjection(threadId);
assert.equal(
projection.subagents.find((row) => row.id === child.taskId)?.completionDelivery?.state,
"claimed",
);
assert.deepEqual(
projection.runs.find((row) => row.id === runId)?.delegatedCompletion?.delivery?.taskIds,
[child.taskId],
);
}),
const projection = yield* orchestrator.getThreadProjection(threadId);
assert.equal(
projection.subagents.find((row) => row.id === child.taskId)?.completionDelivery?.state,
"claimed",
);
assert.deepEqual(
projection.runs.find((row) => row.id === runId)?.delegatedCompletion?.delivery?.taskIds,
[child.taskId],
);
}),
);

it.effect.each(["a background command", "a nested subagent"] as const)(
Expand Down
Loading
Loading