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
43 changes: 33 additions & 10 deletions apps/server/src/orchestration-v2/Orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -909,16 +909,23 @@ function nextQueuedRun(
return queuedRunsInDeliveryOrder(projection).find((run) => run.queueHeld !== true);
}

// Delegated completions, monitor notifications and unpinned scheduled deliveries never choose a
// model, so a queued one runs on the thread's current pick rather than the one frozen at queue time.
function followsCurrentConversation(
message:
| Pick<
OrchestrationV2ConversationMessage,
"createdBy" | "creationSource" | "notification" | "delegatedCompletion"
| "createdBy"
| "creationSource"
| "notification"
| "delegatedCompletion"
| "followsThreadSelection"
>
| undefined,
): boolean {
return (
message?.delegatedCompletion !== undefined ||
message?.followsThreadSelection === true ||
(message?.notification?.source.kind === "monitor" && !isBufferedProviderNotification(message))
);
}
Expand Down Expand Up @@ -1490,7 +1497,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio

/** The driver when this provider runs `/goal` natively, else undefined. */
const goalDriverFor = Effect.fn("Orchestrator.goalDriverFor")(function* (
command: OrchestrationV2ServerCommand,
command: Pick<OrchestrationV2ServerCommand, "commandId" | "type">,
providerInstanceId: ProviderInstanceId,
) {
const adapter = yield* providerAdapters.get(providerInstanceId).pipe(mapDispatchError(command));
Expand All @@ -1505,7 +1512,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
/** Unsupported goal controls can become new objectives in native providers. */
const ensureGoalCommandSupported = Effect.fn("Orchestrator.ensureGoalCommandSupported")(
function* (
command: OrchestrationV2ServerCommand,
command: Pick<OrchestrationV2ServerCommand, "commandId" | "type">,
message: { readonly text: string; readonly attachments: ReadonlyArray<ChatAttachment> },
providerInstanceId: ProviderInstanceId,
) {
Expand All @@ -1531,7 +1538,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
);

const mapDispatchError =
(command: OrchestrationV2ServerCommand) =>
(command: Pick<OrchestrationV2ServerCommand, "commandId" | "type">) =>
<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, OrchestratorDispatchError, R> =>
effect.pipe(
Effect.mapError(
Expand Down Expand Up @@ -2452,6 +2459,12 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
cause: `Queued run ${queuedRun.id} is missing projection state.`,
});
}
// A scheduled delivery starts on the thread's current pick, so /goal must suit that provider.
yield* ensureGoalCommandSupported(
{ commandId, type: "message.dispatch" },
queuedMessage,
queuedRun.providerInstanceId,
);

const now = yield* DateTime.now;
const targetAdapter = yield* providerAdapters.get(queuedRun.providerInstanceId).pipe(
Expand All @@ -2478,7 +2491,9 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
? activeProviderThread
: rootProviderThreadsForProvider(projection, queuedRun.providerInstanceId)[0]
: freshContext &&
(storedProviderThread.firstRunOrdinal !== null ||
// The reserved row belongs to the instance the run queued on, which may have changed.
(storedProviderThread.providerInstanceId !== queuedRun.providerInstanceId ||
storedProviderThread.firstRunOrdinal !== null ||
storedProviderThread.nativeThreadRef !== null)
? undefined
: storedProviderThread;
Expand Down Expand Up @@ -7439,6 +7454,9 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
...(command.senderThreadId === undefined ? {} : { senderThreadId: command.senderThreadId }),
...(command.linkedSender === undefined ? {} : { linkedSender: command.linkedSender }),
...(command.idleCompaction === undefined ? {} : { idleCompaction: command.idleCompaction }),
...(command.scheduledTaskId !== undefined && command.modelSelection === undefined
? { followsThreadSelection: true as const }
: {}),
id: command.messageId,
threadId: command.threadId,
runId,
Expand Down Expand Up @@ -7851,6 +7869,9 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
...(command.senderThreadId === undefined ? {} : { senderThreadId: command.senderThreadId }),
...(command.linkedSender === undefined ? {} : { linkedSender: command.linkedSender }),
...(command.idleCompaction === undefined ? {} : { idleCompaction: command.idleCompaction }),
...(command.scheduledTaskId !== undefined && command.modelSelection === undefined
? { followsThreadSelection: true as const }
: {}),
id: command.messageId,
threadId: command.threadId,
runId,
Expand Down Expand Up @@ -10965,10 +10986,12 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio

const now = yield* DateTime.now;
const emitEvent = emit(events, command);
// A scheduled delivery steers on the thread's current pick, so /goal must suit that provider.
const modelSelection = queuedRunModelSelection(projection, queuedRun);
const parsedGoalCommand = parseGoalCommand(queuedMessage);
const goalCommand =
parsedGoalCommand === null ||
(yield* goalDriverFor(command, queuedRun.providerInstanceId)) === undefined
(yield* goalDriverFor(command, modelSelection.instanceId)) === undefined
? null
: parsedGoalCommand;
const target = projection.runs.find((run) => run.id === command.targetRunId);
Expand All @@ -10982,7 +11005,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
isCompactionCommandMessage(targetMessage)
) {
if (goalCommand !== null) {
yield* ensureGoalCommandSupported(command, queuedMessage, queuedRun.providerInstanceId);
yield* ensureGoalCommandSupported(command, queuedMessage, modelSelection.instanceId);
}
yield* emitEvent({
type: "run.updated",
Expand Down Expand Up @@ -11015,7 +11038,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
});
}
if (goalCommand !== null) {
yield* ensureGoalCommandSupported(command, queuedMessage, queuedRun.providerInstanceId);
yield* ensureGoalCommandSupported(command, queuedMessage, modelSelection.instanceId);
const goal = projection.providerThreads.find(
(candidate) => candidate.id === target?.providerThreadId,
)?.goal;
Expand Down Expand Up @@ -11124,7 +11147,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
events,
effects,
projection,
modelSelection: queuedRun.modelSelection,
modelSelection,
targetRunId: command.targetRunId,
messageId: queuedMessage.id,
text: queuedMessage.text,
Expand Down Expand Up @@ -11348,7 +11371,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
yield* ensureGoalCommandSupported(
command,
{ text: command.text, attachments: command.attachments ?? queuedMessage.attachments },
queuedRun.providerInstanceId,
queuedRunModelSelection(projection, queuedRun).instanceId,
);
const queuedTurnItem = projection.turnItems.find(
(candidate) => candidate.type === "user_message" && candidate.messageId === queuedMessage.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ProviderInstanceId,
ProviderThreadId,
ProviderTurnId,
ScheduledTaskId,
ThreadId,
TurnItemId,
type ModelSelection,
Expand Down Expand Up @@ -511,6 +512,7 @@ const nextTurnSelectionHarness = Effect.fn("nextTurnSelectionHarness")(function*
onSteer: (
turn: ProviderAdapter.ProviderAdapterV2SteerInput,
) => Effect.Effect<void, ProviderAdapter.ProviderAdapterV2Error> = () => Effect.void,
extraAdapters: ReadonlyArray<ProviderAdapter.ProviderAdapterV2["Service"]> = [],
) {
const driver = ProviderDriverKind.make(nativeGoals === undefined ? "codex" : "opencode");
const cwd = yield* checkpointWorkspace(name);
Expand Down Expand Up @@ -647,7 +649,7 @@ const nextTurnSelectionHarness = Effect.fn("nextTurnSelectionHarness")(function*
};
const layer = ProviderReplayHarness.layerWithRegistry(
{ name },
ProviderAdapterRegistry.layerSingle(adapter),
ProviderAdapterRegistry.layerFromAdapters([adapter, ...extraAdapters]),
{ runEffectWorker: false },
);
const startFirstTurn = Effect.gen(function* () {
Expand Down Expand Up @@ -2158,6 +2160,193 @@ it.effect("steers a promoted queued message with the selection it was queued wit
}),
);

it.effect("steers a promoted scheduled delivery with the thread's current selection", () =>
Effect.scoped(
Effect.gen(function* () {
const { started, steered, layer, startFirstTurn } = yield* nextTurnSelectionHarness(
"promote-scheduled-selection",
);
yield* Effect.gen(function* () {
const orchestrator = yield* Orchestrator.OrchestratorV2;
const worker = yield* EffectWorker.OrchestrationEffectWorkerV2;
const threadId = yield* startFirstTurn;
// Bound scheduled tasks send like ScheduledTaskService: no model of their own.
yield* orchestrator.dispatch({
type: "message.dispatch",
commandId: CommandId.make("scheduled"),
threadId,
messageId: MessageId.make("message:scheduled"),
scheduledTaskId: ScheduledTaskId.make("task:scheduled"),
text: "scheduled",
attachments: [],
dispatchMode: { type: "queue_after_active" },
createdBy: "user",
creationSource: "web",
});
const queuedRun = (yield* orchestrator.getThreadProjection(threadId)).runs.find(
(run) => run.status === "queued",
)!;
assert.deepEqual(queuedRun.modelSelection, runSelection);
yield* orchestrator.dispatch({
type: "thread.model-selection.set",
commandId: CommandId.make("select-composer"),
threadId,
modelSelection: composerSelection,
});
yield* orchestrator.dispatch({
type: "queued-message.promote-to-steer",
commandId: CommandId.make("promote"),
threadId,
queuedRunId: queuedRun.id,
targetRunId: started[0]!.runId,
});
yield* worker.drain();

const projection = yield* orchestrator.getThreadProjection(threadId);
assert.deepEqual(steered, ["scheduled"]);
assert.deepEqual(projection.thread.modelSelection, composerSelection);
}).pipe(Effect.provide(layer));
}),
),
);

const claudeSelection = {
instanceId: ProviderInstanceId.make("claudeAgent"),
model: "claude-test",
};
const claudeAdapter: ProviderAdapter.ProviderAdapterV2["Service"] = {
instanceId: claudeSelection.instanceId,
driver: ProviderDriverKind.make("claudeAgent"),
getCapabilities: () => Effect.succeed(CodexProviderCapabilitiesV2),
planSelectionTransition: () => Effect.succeed({ type: "apply_on_next_turn" }),
openSession: () => Effect.die("the Claude instance never starts a turn here"),
};

// Queues a scheduled delivery behind the Codex turn, stops that turn (Stop holds the queue), then
// switches the thread to Claude: a thread changes instance only while no run is active.
const scheduledDeliveryAfterClaudeSwitch = (name: string, text: string) =>
Effect.gen(function* () {
const harness = yield* nextTurnSelectionHarness(
name,
false,
true,
Effect.void,
true,
undefined,
undefined,
[claudeAdapter],
);
const queueAndSwitch = Effect.gen(function* () {
const orchestrator = yield* Orchestrator.OrchestratorV2;
const threadId = yield* harness.startFirstTurn;
yield* orchestrator.dispatch({
type: "message.dispatch",
commandId: CommandId.make("scheduled"),
threadId,
messageId: MessageId.make("message:scheduled"),
scheduledTaskId: ScheduledTaskId.make("task:scheduled"),
text,
attachments: [],
dispatchMode: { type: "queue_after_active" },
createdBy: "user",
creationSource: "web",
});
const queuedRun = (yield* orchestrator.getThreadProjection(threadId)).runs.find(
(run) => run.status === "queued",
)!;
assert.equal(queuedRun.providerInstanceId, instanceId);
const worker = yield* EffectWorker.OrchestrationEffectWorkerV2;
const first = harness.started[0]!;
yield* orchestrator.dispatch({
type: "run.interrupt",
commandId: CommandId.make("stop"),
threadId,
runId: first.runId,
holdQueue: true,
});
yield* worker.drain();
const stopped = (yield* orchestrator.getThreadProjection(threadId)).runs;
assert.equal(stopped.find((run) => run.id === first.runId)?.status, "interrupted");
const held = stopped.find((run) => run.id === queuedRun.id);
assert.equal(held?.status, "queued");
assert.isTrue(held?.queueHeld);
yield* orchestrator.dispatch({
type: "thread.model-selection.set",
commandId: CommandId.make("select-claude"),
threadId,
modelSelection: claudeSelection,
});
return { threadId, queuedRun };
});
return { ...harness, queueAndSwitch };
});

it.effect("checks an edited scheduled /goal against the thread's current provider", () =>
Effect.scoped(
Effect.gen(function* () {
const { layer, queueAndSwitch } = yield* scheduledDeliveryAfterClaudeSwitch(
"edit-scheduled-goal",
"scheduled",
);
yield* Effect.gen(function* () {
const orchestrator = yield* Orchestrator.OrchestratorV2;
const { threadId, queuedRun } = yield* queueAndSwitch;
const editError = yield* orchestrator
.dispatch({
type: "queued-run.edit",
commandId: CommandId.make("edit"),
threadId,
runId: queuedRun.id,
text: "/goal pause",
})
.pipe(Effect.flip);

assert.match(String(editError.cause), /Claude goals cannot be paused or resumed/);
assert.equal(
(yield* orchestrator.getThreadProjection(threadId)).messages.find(
(message) => message.id === queuedRun.userMessageId,
)?.text,
"scheduled",
);
}).pipe(Effect.provide(layer));
}),
),
);

it.effect("checks a queued scheduled /goal against the thread's current provider at start", () =>
Effect.scoped(
Effect.gen(function* () {
const { started, layer, queueAndSwitch } = yield* scheduledDeliveryAfterClaudeSwitch(
"start-scheduled-goal",
"/goal pause",
);
yield* Effect.gen(function* () {
const orchestrator = yield* Orchestrator.OrchestratorV2;
const worker = yield* EffectWorker.OrchestrationEffectWorkerV2;
const { threadId, queuedRun } = yield* queueAndSwitch;
yield* orchestrator.dispatch({
type: "queue.resume",
commandId: CommandId.make("resume"),
threadId,
});
yield* orchestrator.resumeQueuedRuns;
yield* worker.drain();

const projection = yield* orchestrator.getThreadProjection(threadId);
const failure = projection.turnItems.find(
(item) => item.type === "error" && item.runId === queuedRun.id,
);
assert.equal(started.length, 1);
assert.equal(projection.runs.find((run) => run.id === queuedRun.id)?.status, "failed");
assert.equal(
failure?.type === "error" ? failure.failure.code : null,
"queued_start_failed",
);
}).pipe(Effect.provide(layer));
}),
),
);

it.effect("marks the model switch on a promoted message that restarts the turn", () =>
Effect.gen(function* () {
const { started, layer, startFirstTurn } = yield* nextTurnSelectionHarness(
Expand Down
Loading
Loading