From f0d7dfdd89cc2acd5628b2f90b42766c420c868e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 26 Oct 2026 22:07:00 +0000 Subject: [PATCH 1/9] test(server): reproduce issue #25644 queued message cannot move ahead of delegated completions Co-Authored-By: Claude Opus 5.5 --- .../src/orchestration-v2/runtimeLayer.test.ts | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/apps/server/src/orchestration-v2/runtimeLayer.test.ts b/apps/server/src/orchestration-v2/runtimeLayer.test.ts index 025191f098823..29d137cd22bf0 100644 --- a/apps/server/src/orchestration-v2/runtimeLayer.test.ts +++ b/apps/server/src/orchestration-v2/runtimeLayer.test.ts @@ -18,6 +18,7 @@ import { TurnItemId, type ModelSelection, type OrchestrationV2Run, + type OrchestrationV2ThreadProjection, ProjectId, type PullRequestDetail, type PullRequestComment, @@ -58,6 +59,7 @@ import * as VcsProcess from "../vcs/VcsProcess.ts"; import * as WorkspacePaths from "../workspace/WorkspacePaths.ts"; import * as LegacyV1ThreadImporter from "./legacy/LegacyV1ThreadImporter.ts"; import * as Orchestrator from "./Orchestrator.ts"; +import { queuedRunsInDeliveryOrder } from "./QueuedRunOrder.ts"; import { ROLLBACK_FAILED_MESSAGE } from "./CheckpointRollbackService.ts"; import * as EffectWorker from "./EffectWorker.ts"; import * as EffectOutbox from "./EffectOutbox.ts"; @@ -5216,6 +5218,116 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => { assert.equal(cancelAgainError._tag, "OrchestratorDispatchError"); }), ); + + it.effect("moves a queued message ahead of queued delegated completions", () => + Effect.gen(function* () { + const orchestrator = yield* Orchestrator.OrchestratorV2; + const eventSink = yield* EventSink.EventSinkV2; + const threadId = ThreadId.make("runtime-layer-reorder-before-completion-thread"); + const messageId = (key: string) => + MessageId.make(`runtime-layer-reorder-before-completion-${key}`); + yield* orchestrator.dispatch({ + type: "thread.create", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("runtime-layer-reorder-before-completion-create"), + threadId, + projectId: ProjectId.make("runtime-layer-reorder-before-completion-project"), + title: "Reorder before completions", + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: "/tmp/runtime-layer-reorder-before-completion", + }); + yield* orchestrator.dispatch({ + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("runtime-layer-reorder-before-completion-active"), + threadId, + messageId: messageId("active"), + text: "Keep the provider occupied.", + attachments: [], + modelSelection, + dispatchMode: { type: "start_immediately" }, + }); + const queueCompletion = (generation: number) => + Effect.gen(function* () { + const activeRun = (yield* orchestrator.getThreadProjection(threadId)).runs[0]!; + const taskIds = [ + NodeId.make(`runtime-layer-reorder-before-completion-task-${generation}`), + ]; + yield* eventSink.write({ + events: [ + { + id: EventId.make(`runtime-layer-reorder-before-completion-open-${generation}`), + type: "run.updated", + threadId, + runId: activeRun.id, + occurredAt: yield* DateTime.now, + payload: { + ...activeRun, + delegatedCompletion: { + disposition: "open", + nextGeneration: generation + 1, + delivery: { + messageId: messageId(`completion-${generation}`), + generation, + taskIds, + }, + }, + }, + }, + ], + }); + yield* orchestrator.dispatch({ + type: "message.dispatch", + createdBy: "agent", + creationSource: "server", + commandId: CommandId.make(`runtime-layer-reorder-before-completion-${generation}`), + threadId, + messageId: messageId(`completion-${generation}`), + text: `Completion ${generation}`, + attachments: [], + modelSelection, + dispatchMode: { type: "queue_after_active" }, + delegatedCompletion: { parentRunId: activeRun.id, generation, taskIds }, + }); + }); + yield* queueCompletion(1); + yield* queueCompletion(2); + yield* orchestrator.dispatch({ + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make("runtime-layer-reorder-before-completion-correction"), + threadId, + messageId: messageId("correction"), + text: "Rule correction.", + attachments: [], + modelSelection, + dispatchMode: { type: "queue_after_active" }, + }); + const runIdFor = (projection: OrchestrationV2ThreadProjection, key: string) => + projection.runs.find((run) => run.userMessageId === messageId(key))!.id; + const before = yield* orchestrator.getThreadProjection(threadId); + + yield* orchestrator.dispatch({ + type: "queued-run.reorder", + commandId: CommandId.make("runtime-layer-reorder-before-completion-reorder"), + threadId, + runId: runIdFor(before, "correction"), + beforeRunId: runIdFor(before, "completion-1"), + }); + + const after = yield* orchestrator.getThreadProjection(threadId); + assert.deepEqual( + queuedRunsInDeliveryOrder(after).map((run) => run.id), + ["correction", "completion-1", "completion-2"].map((key) => runIdFor(after, key)), + ); + }), + ); }); it.layer(layerSharedApplicationDataPlaneTest)("pending provider interruption", (it) => { From 8dc13aed14c0c7df06abb1b9290d5cd8780b830c Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 26 Oct 2026 22:18:03 +0000 Subject: [PATCH 2/9] fix(server): queued messages can move ahead of delegated completions Reordering a queued message before an automatic completion delivery was rejected, and delivery order forced every completion first regardless of queue position. Delivery order now follows queuePosition, with completions winning ties. A new completion takes the position of the last queued completion (or the queue head), so by default it still runs before visible messages and after earlier completions. The reorder handler treats completions like any other queued run as a target while still refusing to move them. Fixes pingdotgg/t3code#25644 Co-Authored-By: Claude Opus 5.5 --- .../src/orchestration-v2/Orchestrator.ts | 43 ++++++++----------- .../orchestration-v2/QueuedRunOrder.test.ts | 41 +++++++++++++++++- .../src/orchestration-v2/QueuedRunOrder.ts | 26 +++++++---- 3 files changed, 75 insertions(+), 35 deletions(-) diff --git a/apps/server/src/orchestration-v2/Orchestrator.ts b/apps/server/src/orchestration-v2/Orchestrator.ts index 0c6ca268f1e04..31077ef880651 100644 --- a/apps/server/src/orchestration-v2/Orchestrator.ts +++ b/apps/server/src/orchestration-v2/Orchestrator.ts @@ -146,7 +146,11 @@ import { makeProviderFailure } from "@t3tools/provider-core/server/failure"; import * as RunExecutionService from "./RunExecutionService.ts"; import { ProviderSessionManagerV2 } from "./ProviderSessionManager.ts"; import { ProviderSwitchServiceV2 } from "./ProviderSwitchService.ts"; -import { isAutomaticCompletionRun, queuedRunsInDeliveryOrder } from "./QueuedRunOrder.ts"; +import { + isAutomaticCompletionRun, + nextAutomaticCompletionQueuePosition, + queuedRunsInDeliveryOrder, +} from "./QueuedRunOrder.ts"; import { RuntimePolicyV2 } from "./RuntimePolicy.ts"; import { endOrphanedNativeSubagent, @@ -5614,12 +5618,14 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio queuePosition: goalStopRunId !== undefined ? 1 - : Math.max( - 0, - ...projection.runs - .filter((candidate) => candidate.status === "queued") - .map((candidate) => candidate.queuePosition ?? candidate.ordinal), - ) + 1, + : delegatedCompletion !== undefined + ? nextAutomaticCompletionQueuePosition(projection) + : Math.max( + 0, + ...projection.runs + .filter((candidate) => candidate.status === "queued") + .map((candidate) => candidate.queuePosition ?? candidate.ordinal), + ) + 1, requestedAt: now, startedAt: null, completedAt: null, @@ -8484,19 +8490,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio cause: "Automatic completion deliveries cannot be reordered.", }); } - const automaticRuns = queuedRuns.filter((run) => isAutomaticCompletionRun(projection, run)); - if ( - command.beforeRunId !== null && - automaticRuns.some((run) => run.id === command.beforeRunId) - ) { - return yield* new OrchestratorDispatchError({ - commandId: command.commandId, - commandType: command.type, - cause: "Queued messages cannot be reordered ahead of automatic completion delivery.", - }); - } - const reorderableRuns = queuedRuns.filter((run) => !isAutomaticCompletionRun(projection, run)); - const withoutMoving = reorderableRuns.filter((run) => run.id !== command.runId); + const withoutMoving = queuedRuns.filter((run) => run.id !== command.runId); const beforeIndex = command.beforeRunId === null ? withoutMoving.length @@ -8509,7 +8503,6 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio }); } const reordered = [ - ...automaticRuns, ...withoutMoving.slice(0, beforeIndex), moving, ...withoutMoving.slice(beforeIndex), @@ -10509,10 +10502,12 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio }) { const { command, events, effects, projection, now, targetRunId } = input; const others = queuedRunsInDeliveryOrder(projection).filter( - (run) => run.id !== input.queuedRunId && !isAutomaticCompletionRun(projection, run), + (run) => run.id !== input.queuedRunId, ); - for (const [index, run] of others.entries()) { - const queuePosition = index + 2; + let visiblePosition = 1; + for (const run of others) { + // Completions share the goal command's position, which keeps them first. + const queuePosition = isAutomaticCompletionRun(projection, run) ? 1 : ++visiblePosition; if (run.queuePosition === queuePosition) continue; yield* emit( events, diff --git a/apps/server/src/orchestration-v2/QueuedRunOrder.test.ts b/apps/server/src/orchestration-v2/QueuedRunOrder.test.ts index 838ec141a4b50..1a1f74ebd56c8 100644 --- a/apps/server/src/orchestration-v2/QueuedRunOrder.test.ts +++ b/apps/server/src/orchestration-v2/QueuedRunOrder.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from "vite-plus/test"; import { queuedRunsInDeliveryOrder } from "./QueuedRunOrder.ts"; describe("queued run delivery order", () => { - it("keeps automatic completion delivery ahead of visible queued messages", () => { + it("delivers automatic completions ahead of visible messages at the same position", () => { const projection = { messages: [ { id: "message:visible-first" }, @@ -28,7 +28,7 @@ describe("queued run delivery order", () => { { id: "run:automatic", ordinal: 4, - queuePosition: 3, + queuePosition: 1, status: "queued", userMessageId: "message:automatic", }, @@ -48,4 +48,41 @@ describe("queued run delivery order", () => { "run:visible-second", ]); }); + + it("delivers a visible message moved ahead of automatic completions first", () => { + const projection = { + messages: [ + { + id: "message:automatic", + delegatedCompletion: { + generation: 1, + parentRunId: "run:parent", + taskIds: ["task:child"], + }, + }, + { id: "message:visible" }, + ], + runs: [ + { + id: "run:automatic", + ordinal: 2, + queuePosition: 2, + status: "queued", + userMessageId: "message:automatic", + }, + { + id: "run:visible", + ordinal: 3, + queuePosition: 1, + status: "queued", + userMessageId: "message:visible", + }, + ], + } as never; + + expect(queuedRunsInDeliveryOrder(projection).map((run) => run.id)).toEqual([ + "run:visible", + "run:automatic", + ]); + }); }); diff --git a/apps/server/src/orchestration-v2/QueuedRunOrder.ts b/apps/server/src/orchestration-v2/QueuedRunOrder.ts index 72c5e319d69ed..96194396cf8c4 100644 --- a/apps/server/src/orchestration-v2/QueuedRunOrder.ts +++ b/apps/server/src/orchestration-v2/QueuedRunOrder.ts @@ -9,6 +9,7 @@ export function isAutomaticCompletionRun( ); } +/** At an equal queue position, automatic completion delivery runs before visible messages. */ export function queuedRunsInDeliveryOrder( projection: Pick, ): ReadonlyArray { @@ -19,14 +20,21 @@ export function queuedRunsInDeliveryOrder( ); return projection.runs .filter((run) => run.status === "queued") - .toSorted((left, right) => { - const deliveryPriority = - Number(automaticCompletionMessageIds.has(right.userMessageId)) - - Number(automaticCompletionMessageIds.has(left.userMessageId)); - return ( - deliveryPriority || + .toSorted( + (left, right) => (left.queuePosition ?? left.ordinal) - (right.queuePosition ?? right.ordinal) || - left.ordinal - right.ordinal - ); - }); + Number(automaticCompletionMessageIds.has(right.userMessageId)) - + Number(automaticCompletionMessageIds.has(left.userMessageId)) || + left.ordinal - right.ordinal, + ); +} + +/** A new completion shares the last queued completion's position, else the queue head's. */ +export function nextAutomaticCompletionQueuePosition( + projection: Pick, +): number { + const queuedRuns = queuedRunsInDeliveryOrder(projection); + const anchor = + queuedRuns.findLast((run) => isAutomaticCompletionRun(projection, run)) ?? queuedRuns[0]; + return anchor === undefined ? 1 : (anchor.queuePosition ?? anchor.ordinal); } From a08cba76808870d4b4ef31a194af14c0814577e7 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 26 Oct 2026 22:33:34 +0000 Subject: [PATCH 3/9] test(server): legacy queues keep completions ahead of visible messages Co-Authored-By: Claude Opus 5.5 --- .../orchestration-v2/QueuedRunOrder.test.ts | 44 ++++++------------- 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/apps/server/src/orchestration-v2/QueuedRunOrder.test.ts b/apps/server/src/orchestration-v2/QueuedRunOrder.test.ts index 1a1f74ebd56c8..e4e7cabd618ab 100644 --- a/apps/server/src/orchestration-v2/QueuedRunOrder.test.ts +++ b/apps/server/src/orchestration-v2/QueuedRunOrder.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from "vite-plus/test"; import { queuedRunsInDeliveryOrder } from "./QueuedRunOrder.ts"; describe("queued run delivery order", () => { - it("delivers automatic completions ahead of visible messages at the same position", () => { + it("keeps automatic completion delivery ahead of visible queued messages", () => { const projection = { messages: [ { id: "message:visible-first" }, @@ -28,7 +28,7 @@ describe("queued run delivery order", () => { { id: "run:automatic", ordinal: 4, - queuePosition: 1, + queuePosition: 3, status: "queued", userMessageId: "message:automatic", }, @@ -49,40 +49,24 @@ describe("queued run delivery order", () => { ]); }); - it("delivers a visible message moved ahead of automatic completions first", () => { + it("keeps a stored completion queued behind visible messages ahead of them", () => { + const completion = (generation: number) => ({ + id: `message:completion-${generation}`, + delegatedCompletion: { generation, parentRunId: "run:parent", taskIds: ["task:child"] }, + }); const projection = { - messages: [ - { - id: "message:automatic", - delegatedCompletion: { - generation: 1, - parentRunId: "run:parent", - taskIds: ["task:child"], - }, - }, - { id: "message:visible" }, - ], + messages: [{ id: "message:visible" }, completion(1), completion(2)], runs: [ - { - id: "run:automatic", - ordinal: 2, - queuePosition: 2, - status: "queued", - userMessageId: "message:automatic", - }, - { - id: "run:visible", - ordinal: 3, - queuePosition: 1, - status: "queued", - userMessageId: "message:visible", - }, - ], + { id: "run:visible", ordinal: 2, queuePosition: 1, status: "queued" }, + { id: "run:completion-1", ordinal: 3, queuePosition: 2, status: "queued" }, + { id: "run:completion-2", ordinal: 4, queuePosition: 3, status: "queued" }, + ].map((run) => ({ ...run, userMessageId: run.id.replace("run:", "message:") })), } as never; expect(queuedRunsInDeliveryOrder(projection).map((run) => run.id)).toEqual([ + "run:completion-1", + "run:completion-2", "run:visible", - "run:automatic", ]); }); }); From f0c4840a83a496a5b7aff4425156e7e42a723087 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 26 Oct 2026 23:26:10 +0000 Subject: [PATCH 4/9] fix(server): keep legacy queue order while allowing reorders ahead of completions Delivering by queuePosition alone silently reordered queues stored before the upgrade: a completion queued behind visible messages (U@1 C@2) used to run first and would now run last. Positions cannot tell that legacy shape apart from a user reorder, and widening them past PositiveInt would break older clients' decoding. Delivery keeps the completions-first rule. A reorder that places a message before a queued completion marks it queuedAheadOfCompletions, which puts it in the completions' priority band by position. Goal normalization clears the mark, and new completions keep their max+1 position, so they land after earlier completions and after messages the user moved ahead. Fixes pingdotgg/t3code#25644 Co-Authored-By: Claude Opus 5.5 --- .../src/orchestration-v2/Orchestrator.ts | 47 ++++++++++--------- .../orchestration-v2/QueuedRunOrder.test.ts | 34 ++++++++++++++ .../src/orchestration-v2/QueuedRunOrder.ts | 28 +++++------ .../src/orchestration-v2/runtimeLayer.test.ts | 6 ++- packages/contracts/src/orchestrationV2.ts | 2 + 5 files changed, 77 insertions(+), 40 deletions(-) diff --git a/apps/server/src/orchestration-v2/Orchestrator.ts b/apps/server/src/orchestration-v2/Orchestrator.ts index 31077ef880651..eb74aeec8350d 100644 --- a/apps/server/src/orchestration-v2/Orchestrator.ts +++ b/apps/server/src/orchestration-v2/Orchestrator.ts @@ -146,11 +146,7 @@ import { makeProviderFailure } from "@t3tools/provider-core/server/failure"; import * as RunExecutionService from "./RunExecutionService.ts"; import { ProviderSessionManagerV2 } from "./ProviderSessionManager.ts"; import { ProviderSwitchServiceV2 } from "./ProviderSwitchService.ts"; -import { - isAutomaticCompletionRun, - nextAutomaticCompletionQueuePosition, - queuedRunsInDeliveryOrder, -} from "./QueuedRunOrder.ts"; +import { isAutomaticCompletionRun, queuedRunsInDeliveryOrder } from "./QueuedRunOrder.ts"; import { RuntimePolicyV2 } from "./RuntimePolicy.ts"; import { endOrphanedNativeSubagent, @@ -5618,14 +5614,12 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio queuePosition: goalStopRunId !== undefined ? 1 - : delegatedCompletion !== undefined - ? nextAutomaticCompletionQueuePosition(projection) - : Math.max( - 0, - ...projection.runs - .filter((candidate) => candidate.status === "queued") - .map((candidate) => candidate.queuePosition ?? candidate.ordinal), - ) + 1, + : Math.max( + 0, + ...projection.runs + .filter((candidate) => candidate.status === "queued") + .map((candidate) => candidate.queuePosition ?? candidate.ordinal), + ) + 1, requestedAt: now, startedAt: null, completedAt: null, @@ -8376,7 +8370,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio nodeId: queuedRootNode.id, providerInstanceId: queuedRun.providerInstanceId, occurredAt: now, - payload: { ...queuedRun, queuePosition: 1 }, + payload: { ...queuedRun, queuePosition: 1, queuedAheadOfCompletions: false }, }); const stops = target !== undefined && @@ -8507,6 +8501,9 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio moving, ...withoutMoving.slice(beforeIndex), ]; + const lastCompletionIndex = reordered.findLastIndex((run) => + isAutomaticCompletionRun(projection, run), + ); const now = yield* DateTime.now; const emitEvent = emit(events, command); yield* Effect.forEach( @@ -8514,7 +8511,12 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio (run, index) => Effect.gen(function* () { const queuePosition = index + 1; - if (run.queuePosition === queuePosition) { + const aheadOfCompletions = + index < lastCompletionIndex && !isAutomaticCompletionRun(projection, run); + if ( + run.queuePosition === queuePosition && + (run.queuedAheadOfCompletions === true) === aheadOfCompletions + ) { return; } yield* emitEvent({ @@ -8527,6 +8529,9 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio payload: { ...run, queuePosition, + ...(aheadOfCompletions || run.queuedAheadOfCompletions === true + ? { queuedAheadOfCompletions: aheadOfCompletions } + : {}), }, }); }), @@ -10502,13 +10507,11 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio }) { const { command, events, effects, projection, now, targetRunId } = input; const others = queuedRunsInDeliveryOrder(projection).filter( - (run) => run.id !== input.queuedRunId, + (run) => run.id !== input.queuedRunId && !isAutomaticCompletionRun(projection, run), ); - let visiblePosition = 1; - for (const run of others) { - // Completions share the goal command's position, which keeps them first. - const queuePosition = isAutomaticCompletionRun(projection, run) ? 1 : ++visiblePosition; - if (run.queuePosition === queuePosition) continue; + for (const [index, run] of others.entries()) { + const queuePosition = index + 2; + if (run.queuePosition === queuePosition && run.queuedAheadOfCompletions !== true) continue; yield* emit( events, command, @@ -10519,7 +10522,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio ...(run.rootNodeId === null ? {} : { nodeId: run.rootNodeId }), providerInstanceId: run.providerInstanceId, occurredAt: now, - payload: { ...run, queuePosition }, + payload: { ...run, queuePosition, queuedAheadOfCompletions: false }, }); } if (targetRunId === undefined) return undefined; diff --git a/apps/server/src/orchestration-v2/QueuedRunOrder.test.ts b/apps/server/src/orchestration-v2/QueuedRunOrder.test.ts index e4e7cabd618ab..f75c1d53820ea 100644 --- a/apps/server/src/orchestration-v2/QueuedRunOrder.test.ts +++ b/apps/server/src/orchestration-v2/QueuedRunOrder.test.ts @@ -69,4 +69,38 @@ describe("queued run delivery order", () => { "run:visible", ]); }); + + it("delivers a message reordered ahead of completions before them", () => { + const projection = { + messages: [ + { id: "message:visible" }, + { + id: "message:automatic", + delegatedCompletion: { + generation: 1, + parentRunId: "run:parent", + taskIds: ["task:child"], + }, + }, + { id: "message:later" }, + ], + runs: [ + { id: "run:automatic", ordinal: 2, queuePosition: 2, status: "queued" }, + { + id: "run:visible", + ordinal: 3, + queuePosition: 1, + queuedAheadOfCompletions: true, + status: "queued", + }, + { id: "run:later", ordinal: 4, queuePosition: 3, status: "queued" }, + ].map((run) => ({ ...run, userMessageId: run.id.replace("run:", "message:") })), + } as never; + + expect(queuedRunsInDeliveryOrder(projection).map((run) => run.id)).toEqual([ + "run:visible", + "run:automatic", + "run:later", + ]); + }); }); diff --git a/apps/server/src/orchestration-v2/QueuedRunOrder.ts b/apps/server/src/orchestration-v2/QueuedRunOrder.ts index 96194396cf8c4..3d60a9fab4705 100644 --- a/apps/server/src/orchestration-v2/QueuedRunOrder.ts +++ b/apps/server/src/orchestration-v2/QueuedRunOrder.ts @@ -9,7 +9,7 @@ export function isAutomaticCompletionRun( ); } -/** At an equal queue position, automatic completion delivery runs before visible messages. */ +/** Automatic completions, and messages reordered ahead of them, run before other queued messages. */ export function queuedRunsInDeliveryOrder( projection: Pick, ): ReadonlyArray { @@ -20,21 +20,15 @@ export function queuedRunsInDeliveryOrder( ); return projection.runs .filter((run) => run.status === "queued") - .toSorted( - (left, right) => + .toSorted((left, right) => { + const hasPriority = (run: OrchestrationV2Run) => + automaticCompletionMessageIds.has(run.userMessageId) || + run.queuedAheadOfCompletions === true; + const deliveryPriority = Number(hasPriority(right)) - Number(hasPriority(left)); + return ( + deliveryPriority || (left.queuePosition ?? left.ordinal) - (right.queuePosition ?? right.ordinal) || - Number(automaticCompletionMessageIds.has(right.userMessageId)) - - Number(automaticCompletionMessageIds.has(left.userMessageId)) || - left.ordinal - right.ordinal, - ); -} - -/** A new completion shares the last queued completion's position, else the queue head's. */ -export function nextAutomaticCompletionQueuePosition( - projection: Pick, -): number { - const queuedRuns = queuedRunsInDeliveryOrder(projection); - const anchor = - queuedRuns.findLast((run) => isAutomaticCompletionRun(projection, run)) ?? queuedRuns[0]; - return anchor === undefined ? 1 : (anchor.queuePosition ?? anchor.ordinal); + left.ordinal - right.ordinal + ); + }); } diff --git a/apps/server/src/orchestration-v2/runtimeLayer.test.ts b/apps/server/src/orchestration-v2/runtimeLayer.test.ts index 29d137cd22bf0..133ae25dbd2ba 100644 --- a/apps/server/src/orchestration-v2/runtimeLayer.test.ts +++ b/apps/server/src/orchestration-v2/runtimeLayer.test.ts @@ -5321,10 +5321,14 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => { beforeRunId: runIdFor(before, "completion-1"), }); + yield* queueCompletion(3); + const after = yield* orchestrator.getThreadProjection(threadId); assert.deepEqual( queuedRunsInDeliveryOrder(after).map((run) => run.id), - ["correction", "completion-1", "completion-2"].map((key) => runIdFor(after, key)), + ["correction", "completion-1", "completion-2", "completion-3"].map((key) => + runIdFor(after, key), + ), ); }), ); diff --git a/packages/contracts/src/orchestrationV2.ts b/packages/contracts/src/orchestrationV2.ts index f4e8303972267..33633fb9063f8 100644 --- a/packages/contracts/src/orchestrationV2.ts +++ b/packages/contracts/src/orchestrationV2.ts @@ -639,6 +639,8 @@ export const OrchestrationV2Run = Schema.Struct({ status: OrchestrationV2RunStatus, queuePosition: Schema.optional(Schema.NullOr(PositiveInt)), queueHeld: Schema.optional(Schema.Boolean), + /** Set by a reorder that placed this message before queued automatic completions. */ + queuedAheadOfCompletions: Schema.optional(Schema.Boolean), requestedAt: Schema.DateTimeUtc, startedAt: Schema.NullOr(Schema.DateTimeUtc), completedAt: Schema.NullOr(Schema.DateTimeUtc), From 39da8b24584ab2f453dd4835713d4b221e0df955 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 27 Oct 2026 07:14:37 +0000 Subject: [PATCH 5/9] test(server): takeover tests for reordering a message ahead of completions Replace the PR's tests with the takeover's minimum: one runtime test that drives the real orchestrator. A queued message reordered ahead of two queued delegated completions runs first, the completions keep their order and a later completion queues behind it; completion runs still cannot be reordered; moving the message to the end clears its priority so a plain follow-up runs before it. Legacy completion-first order stays covered by the existing QueuedRunOrder unit test, so the PR's two unit tests are dropped. Co-Authored-By: Claude Opus 5.5 --- .../orchestration-v2/QueuedRunOrder.test.ts | 55 -------------- .../src/orchestration-v2/runtimeLayer.test.ts | 74 ++++++++++++++----- 2 files changed, 54 insertions(+), 75 deletions(-) diff --git a/apps/server/src/orchestration-v2/QueuedRunOrder.test.ts b/apps/server/src/orchestration-v2/QueuedRunOrder.test.ts index f75c1d53820ea..838ec141a4b50 100644 --- a/apps/server/src/orchestration-v2/QueuedRunOrder.test.ts +++ b/apps/server/src/orchestration-v2/QueuedRunOrder.test.ts @@ -48,59 +48,4 @@ describe("queued run delivery order", () => { "run:visible-second", ]); }); - - it("keeps a stored completion queued behind visible messages ahead of them", () => { - const completion = (generation: number) => ({ - id: `message:completion-${generation}`, - delegatedCompletion: { generation, parentRunId: "run:parent", taskIds: ["task:child"] }, - }); - const projection = { - messages: [{ id: "message:visible" }, completion(1), completion(2)], - runs: [ - { id: "run:visible", ordinal: 2, queuePosition: 1, status: "queued" }, - { id: "run:completion-1", ordinal: 3, queuePosition: 2, status: "queued" }, - { id: "run:completion-2", ordinal: 4, queuePosition: 3, status: "queued" }, - ].map((run) => ({ ...run, userMessageId: run.id.replace("run:", "message:") })), - } as never; - - expect(queuedRunsInDeliveryOrder(projection).map((run) => run.id)).toEqual([ - "run:completion-1", - "run:completion-2", - "run:visible", - ]); - }); - - it("delivers a message reordered ahead of completions before them", () => { - const projection = { - messages: [ - { id: "message:visible" }, - { - id: "message:automatic", - delegatedCompletion: { - generation: 1, - parentRunId: "run:parent", - taskIds: ["task:child"], - }, - }, - { id: "message:later" }, - ], - runs: [ - { id: "run:automatic", ordinal: 2, queuePosition: 2, status: "queued" }, - { - id: "run:visible", - ordinal: 3, - queuePosition: 1, - queuedAheadOfCompletions: true, - status: "queued", - }, - { id: "run:later", ordinal: 4, queuePosition: 3, status: "queued" }, - ].map((run) => ({ ...run, userMessageId: run.id.replace("run:", "message:") })), - } as never; - - expect(queuedRunsInDeliveryOrder(projection).map((run) => run.id)).toEqual([ - "run:visible", - "run:automatic", - "run:later", - ]); - }); }); diff --git a/apps/server/src/orchestration-v2/runtimeLayer.test.ts b/apps/server/src/orchestration-v2/runtimeLayer.test.ts index 133ae25dbd2ba..9072ff3e2e7d2 100644 --- a/apps/server/src/orchestration-v2/runtimeLayer.test.ts +++ b/apps/server/src/orchestration-v2/runtimeLayer.test.ts @@ -5219,7 +5219,7 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => { }), ); - it.effect("moves a queued message ahead of queued delegated completions", () => + it.effect("moves a queued message ahead of queued delegated completions and back", () => Effect.gen(function* () { const orchestrator = yield* Orchestrator.OrchestratorV2; const eventSink = yield* EventSink.EventSinkV2; @@ -5297,18 +5297,20 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => { }); yield* queueCompletion(1); yield* queueCompletion(2); - yield* orchestrator.dispatch({ - type: "message.dispatch", - createdBy: "user", - creationSource: "web", - commandId: CommandId.make("runtime-layer-reorder-before-completion-correction"), - threadId, - messageId: messageId("correction"), - text: "Rule correction.", - attachments: [], - modelSelection, - dispatchMode: { type: "queue_after_active" }, - }); + for (const key of ["correction", "follow-up"]) { + yield* orchestrator.dispatch({ + type: "message.dispatch", + createdBy: "user", + creationSource: "web", + commandId: CommandId.make(`runtime-layer-reorder-before-completion-${key}`), + threadId, + messageId: messageId(key), + text: key, + attachments: [], + modelSelection, + dispatchMode: { type: "queue_after_active" }, + }); + } const runIdFor = (projection: OrchestrationV2ThreadProjection, key: string) => projection.runs.find((run) => run.userMessageId === messageId(key))!.id; const before = yield* orchestrator.getThreadProjection(threadId); @@ -5323,13 +5325,45 @@ it.layer(layerTest)("OrchestrationV2LayerLive lifecycle", (it) => { yield* queueCompletion(3); - const after = yield* orchestrator.getThreadProjection(threadId); - assert.deepEqual( - queuedRunsInDeliveryOrder(after).map((run) => run.id), - ["correction", "completion-1", "completion-2", "completion-3"].map((key) => - runIdFor(after, key), - ), - ); + const deliveryOrder = Effect.gen(function* () { + const projection = yield* orchestrator.getThreadProjection(threadId); + return queuedRunsInDeliveryOrder(projection).map((run) => + run.userMessageId.replace(messageId(""), ""), + ); + }); + assert.deepEqual(yield* deliveryOrder, [ + "correction", + "completion-1", + "completion-2", + "completion-3", + "follow-up", + ]); + + const completionReorder = yield* orchestrator + .dispatch({ + type: "queued-run.reorder", + commandId: CommandId.make("runtime-layer-reorder-before-completion-completion"), + threadId, + runId: runIdFor(before, "completion-2"), + beforeRunId: runIdFor(before, "completion-1"), + }) + .pipe(Effect.flip); + assert.equal(completionReorder._tag, "OrchestratorDispatchError"); + + yield* orchestrator.dispatch({ + type: "queued-run.reorder", + commandId: CommandId.make("runtime-layer-reorder-before-completion-back"), + threadId, + runId: runIdFor(before, "correction"), + beforeRunId: null, + }); + assert.deepEqual(yield* deliveryOrder, [ + "completion-1", + "completion-2", + "completion-3", + "follow-up", + "correction", + ]); }), ); }); From a77de508c2d34983d2bf07dd64e075df582554dc Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 3 Nov 2026 18:23:44 +0000 Subject: [PATCH 6/9] fix(server): a goal promoted into a compaction keeps completions ahead of it The compaction-skip branch of promote-to-steer kept a goal's reorder priority, so it ran ahead of queued completions while the same goal promoted into an ordinary turn ran after them. Co-Authored-By: Claude Opus 5.5 --- .../src/orchestration-v2/Orchestrator.ts | 6 +- .../SteeringCompletion.integration.test.ts | 68 ++++++++++++++++--- 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/apps/server/src/orchestration-v2/Orchestrator.ts b/apps/server/src/orchestration-v2/Orchestrator.ts index 68f753722639e..6692a589844e2 100644 --- a/apps/server/src/orchestration-v2/Orchestrator.ts +++ b/apps/server/src/orchestration-v2/Orchestrator.ts @@ -9536,7 +9536,11 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio nodeId: queuedRootNode.id, providerInstanceId: queuedRun.providerInstanceId, occurredAt: now, - payload: { ...queuedRun, queuePosition: 1 }, + payload: { + ...queuedRun, + queuePosition: 1, + ...(goalCommand === null ? {} : { queuedAheadOfCompletions: false }), + }, }); const stops = isLiveRun(target) && !(yield* stopReachedRun(command, command.threadId, target.id)); diff --git a/apps/server/src/orchestration-v2/SteeringCompletion.integration.test.ts b/apps/server/src/orchestration-v2/SteeringCompletion.integration.test.ts index bd3f4e5b2e7d8..cdcf3fbf7c2aa 100644 --- a/apps/server/src/orchestration-v2/SteeringCompletion.integration.test.ts +++ b/apps/server/src/orchestration-v2/SteeringCompletion.integration.test.ts @@ -1628,11 +1628,11 @@ it.effect("promotes a queued goal by stopping the running turn", () => ); it.effect.each(["next", "/goal ship it"])( - "promoting %s into a compaction stops it and sends it next", + "promoting %s, moved ahead of a completion, into a compaction stops it", (promoted) => Effect.scoped( Effect.gen(function* () { - const { events, layer, send, read, started, steered, startFirstTurn } = yield* goalHarness( + const { events, layer, send, started, steered, startFirstTurn } = yield* goalHarness( `compaction-promote-${promoted.length}`, ); yield* Effect.gen(function* () { @@ -1688,14 +1688,62 @@ it.effect.each(["next", "/goal ship it"])( const compactRun = (yield* orchestrator.getThreadProjection(threadId)).runs.find( (run) => run.userMessageId === MessageId.make("message:compact"), )!; - const nextRun = (yield* orchestrator.getThreadProjection(threadId)).runs.find( - (run) => run.userMessageId === MessageId.make("message:next"), + const firstRun = (yield* orchestrator.getThreadProjection(threadId)).runs.find( + (run) => run.id === started[0]!.runId, )!; + const taskIds = [NodeId.make("task:compaction-promote")]; + yield* (yield* EventSink.EventSinkV2).write({ + events: [ + { + id: EventId.make("event:compaction-promote-completion"), + type: "run.updated", + threadId, + runId: firstRun.id, + occurredAt: yield* DateTime.now, + payload: { + ...firstRun, + delegatedCompletion: { + disposition: "open", + nextGeneration: 2, + delivery: { + messageId: MessageId.make("message:completion"), + generation: 1, + taskIds, + }, + }, + }, + }, + ], + }); + yield* orchestrator.dispatch({ + type: "message.dispatch", + commandId: CommandId.make("completion"), + threadId, + messageId: MessageId.make("message:completion"), + text: "Completion", + attachments: [], + modelSelection: runSelection, + dispatchMode: { type: "queue_after_active" }, + delegatedCompletion: { parentRunId: firstRun.id, generation: 1, taskIds }, + createdBy: "agent", + creationSource: "server", + }); + const runFor = (key: string) => + Effect.map(orchestrator.getThreadProjection(threadId), ({ runs }) => + runs.find((run) => run.userMessageId === MessageId.make(`message:${key}`))!, + ); + yield* orchestrator.dispatch({ + type: "queued-run.reorder", + commandId: CommandId.make("reorder"), + threadId, + runId: (yield* runFor("next")).id, + beforeRunId: (yield* runFor("completion")).id, + }); yield* orchestrator.dispatch({ type: "queued-message.promote-to-steer", commandId: CommandId.make("promote"), threadId, - queuedRunId: nextRun.id, + queuedRunId: (yield* runFor("next")).id, targetRunId: compactRun.id, }); yield* worker.drain(); @@ -1707,10 +1755,14 @@ it.effect.each(["next", "/goal ship it"])( (item) => item.type === "run_interrupt_request" && item.runId === compactRun.id, ), ); - assert.deepEqual((yield* read(threadId)).queue, ["queued"]); + // Like any queued goal, a goal keeps automatic completions ahead of it. assert.deepEqual( - started.map((turn) => turn.message.text), - ["first", "/compact", promoted], + started.map((turn) => turn.runId), + [ + started[0]!.runId, + compactRun.id, + (yield* runFor(promoted === "next" ? "next" : "completion")).id, + ], ); }).pipe(Effect.provide(layer)); }), From ef3571a1e4cc0cc4f9d95b0e5418cb9d56f0b7bb Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 3 Nov 2026 18:40:49 +0000 Subject: [PATCH 7/9] fix(server): skipping a compaction keeps other messages ahead of completions Only a goal resets queued messages' priority over completions; an ordinary message sent into a compaction left the others demoted behind them. Co-Authored-By: Claude Opus 5.5 --- .../src/orchestration-v2/Orchestrator.ts | 17 ++- .../SteeringCompletion.integration.test.ts | 118 ++++++++++-------- 2 files changed, 83 insertions(+), 52 deletions(-) diff --git a/apps/server/src/orchestration-v2/Orchestrator.ts b/apps/server/src/orchestration-v2/Orchestrator.ts index 6692a589844e2..1f425d0743f0e 100644 --- a/apps/server/src/orchestration-v2/Orchestrator.ts +++ b/apps/server/src/orchestration-v2/Orchestrator.ts @@ -6193,6 +6193,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio queuedRunId: runId, targetRunId: goalStopRunId, reason: "Stopped to apply /goal", + goal: true, now, }); } @@ -9552,6 +9553,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio queuedRunId: queuedRun.id, targetRunId: stops ? target.id : undefined, reason: "Skipped compaction to send a queued message", + goal: goalCommand !== null, now, }); } @@ -9583,6 +9585,7 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio queuedRunId: queuedRun.id, targetRunId: stops ? target.id : undefined, reason: "Stopped to apply /goal", + goal: true, now, }); } @@ -11800,6 +11803,8 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio readonly queuedRunId: RunId; readonly targetRunId: RunId | undefined; readonly reason: string; + /** A goal goes first among queued messages, so it drops their priority over completions. */ + readonly goal: boolean; readonly now: DateTime.Utc; }) { const { command, events, effects, projection, now, targetRunId } = input; @@ -11808,7 +11813,11 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio ); for (const [index, run] of others.entries()) { const queuePosition = index + 2; - if (run.queuePosition === queuePosition && run.queuedAheadOfCompletions !== true) continue; + if ( + run.queuePosition === queuePosition && + (!input.goal || run.queuedAheadOfCompletions !== true) + ) + continue; yield* emit( events, command, @@ -11819,7 +11828,11 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio ...(run.rootNodeId === null ? {} : { nodeId: run.rootNodeId }), providerInstanceId: run.providerInstanceId, occurredAt: now, - payload: { ...run, queuePosition, queuedAheadOfCompletions: false }, + payload: { + ...run, + queuePosition, + ...(input.goal ? { queuedAheadOfCompletions: false } : {}), + }, }); } if (targetRunId === undefined) return undefined; diff --git a/apps/server/src/orchestration-v2/SteeringCompletion.integration.test.ts b/apps/server/src/orchestration-v2/SteeringCompletion.integration.test.ts index cdcf3fbf7c2aa..e2a1793ba5f93 100644 --- a/apps/server/src/orchestration-v2/SteeringCompletion.integration.test.ts +++ b/apps/server/src/orchestration-v2/SteeringCompletion.integration.test.ts @@ -1642,49 +1642,59 @@ it.effect.each(["next", "/goal ship it"])( yield* send("compact", "/compact", "queue"); yield* send("queued", "queued", "queue"); yield* send("next", promoted, "queue"); - const settled = yield* orchestrator.streamDomainEvents.pipe( - Stream.filter( - (event) => - event.type === "run.updated" && - event.payload.id === started[0]!.runId && - event.payload.status === "waiting", - ), - Stream.take(1), - Stream.runDrain, - Effect.forkScoped, - ); - const compacting = yield* orchestrator.streamDomainEvents.pipe( - Stream.filter( - (event) => - event.type === "provider-turn.updated" && - event.payload.status === "running" && - event.payload.runAttemptId !== started[0]!.attemptId, - ), - Stream.take(1), - Stream.runDrain, - Effect.forkScoped, - ); - const turn = (yield* orchestrator.getThreadProjection(threadId)).providerTurns[0]!; - yield* Queue.offer(events, { - type: "provider_turn.updated", - driver, - providerTurn: { ...turn, status: "completed", completedAt: yield* DateTime.now }, + const nextStart = Effect.suspend(() => { + const known = started.map((turn) => turn.attemptId); + return orchestrator.streamDomainEvents.pipe( + Stream.filter( + (event) => + event.type === "provider-turn.updated" && + event.payload.status === "running" && + !known.includes(event.payload.runAttemptId), + ), + Stream.take(1), + Stream.runDrain, + Effect.forkScoped, + ); }); - yield* Queue.offer(events, { - type: "turn.terminal", - driver, - providerThreadId: turn.providerThreadId, - providerTurnId: turn.id, - runOrdinal: started[0]!.runOrdinal, - status: "completed", - failure: null, - threadDisposition: "reusable", + const finish = Effect.fn("finish")(function* (index: number) { + const { runId, attemptId, runOrdinal } = started[index]!; + const settled = yield* orchestrator.streamDomainEvents.pipe( + Stream.filter( + (event) => + event.type === "run.updated" && + event.payload.id === runId && + (event.payload.status === "waiting" || event.payload.status === "completed"), + ), + Stream.take(1), + Stream.runDrain, + Effect.forkScoped, + ); + const startedNext = yield* nextStart; + const turn = (yield* orchestrator.getThreadProjection(threadId)).providerTurns.find( + (candidate) => candidate.id === ProviderTurnId.make(`provider-turn:${attemptId}`), + )!; + yield* Queue.offer(events, { + type: "provider_turn.updated", + driver, + providerTurn: { ...turn, status: "completed", completedAt: yield* DateTime.now }, + }); + yield* Queue.offer(events, { + type: "turn.terminal", + driver, + providerThreadId: turn.providerThreadId, + providerTurnId: turn.id, + runOrdinal, + status: "completed", + failure: null, + threadDisposition: "reusable", + }); + yield* Fiber.join(settled); + yield* worker.drain(); + yield* orchestrator.resumeQueuedRuns; + yield* worker.drain(); + yield* Fiber.join(startedNext); }); - yield* Fiber.join(settled); - yield* worker.drain(); - yield* orchestrator.resumeQueuedRuns; - yield* worker.drain(); - yield* Fiber.join(compacting); + yield* finish(0); const compactRun = (yield* orchestrator.getThreadProjection(threadId)).runs.find( (run) => run.userMessageId === MessageId.make("message:compact"), )!; @@ -1732,13 +1742,16 @@ it.effect.each(["next", "/goal ship it"])( Effect.map(orchestrator.getThreadProjection(threadId), ({ runs }) => runs.find((run) => run.userMessageId === MessageId.make(`message:${key}`))!, ); - yield* orchestrator.dispatch({ - type: "queued-run.reorder", - commandId: CommandId.make("reorder"), - threadId, - runId: (yield* runFor("next")).id, - beforeRunId: (yield* runFor("completion")).id, - }); + for (const key of ["next", "queued"]) { + yield* orchestrator.dispatch({ + type: "queued-run.reorder", + commandId: CommandId.make(`reorder-${key}`), + threadId, + runId: (yield* runFor(key)).id, + beforeRunId: (yield* runFor("completion")).id, + }); + } + const promotedStarted = yield* nextStart; yield* orchestrator.dispatch({ type: "queued-message.promote-to-steer", commandId: CommandId.make("promote"), @@ -1747,6 +1760,7 @@ it.effect.each(["next", "/goal ship it"])( targetRunId: compactRun.id, }); yield* worker.drain(); + yield* Fiber.join(promotedStarted); const projection = yield* orchestrator.getThreadProjection(threadId); assert.deepEqual(steered, []); @@ -1755,13 +1769,17 @@ it.effect.each(["next", "/goal ship it"])( (item) => item.type === "run_interrupt_request" && item.runId === compactRun.id, ), ); - // Like any queued goal, a goal keeps automatic completions ahead of it. + yield* finish(2); + // A plain message keeps the other prioritized message ahead of the completion; a goal, + // like any queued goal, sends after automatic completions. assert.deepEqual( started.map((turn) => turn.runId), [ started[0]!.runId, compactRun.id, - (yield* runFor(promoted === "next" ? "next" : "completion")).id, + ...(promoted === "next" + ? [(yield* runFor("next")).id, (yield* runFor("queued")).id] + : [(yield* runFor("completion")).id, (yield* runFor("next")).id]), ], ); }).pipe(Effect.provide(layer)); From f6290eb6d3ee3a71261788c3a1cd0bf528679efd Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 3 Nov 2026 18:49:32 +0000 Subject: [PATCH 8/9] fix(server): renumber completions when a queued command goes first Completions kept their old positions while other messages were renumbered, so a message moved ahead of them could tie with a completion and lose. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/orchestration-v2/Orchestrator.ts | 3 ++- .../SteeringCompletion.integration.test.ts | 12 +++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/server/src/orchestration-v2/Orchestrator.ts b/apps/server/src/orchestration-v2/Orchestrator.ts index 1f425d0743f0e..8aa8fab8ac589 100644 --- a/apps/server/src/orchestration-v2/Orchestrator.ts +++ b/apps/server/src/orchestration-v2/Orchestrator.ts @@ -11808,8 +11808,9 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio readonly now: DateTime.Utc; }) { const { command, events, effects, projection, now, targetRunId } = input; + // Completions are renumbered too: a message moved ahead of them shares their position range. const others = queuedRunsInDeliveryOrder(projection).filter( - (run) => run.id !== input.queuedRunId && !isAutomaticCompletionRun(projection, run), + (run) => run.id !== input.queuedRunId, ); for (const [index, run] of others.entries()) { const queuePosition = index + 2; diff --git a/apps/server/src/orchestration-v2/SteeringCompletion.integration.test.ts b/apps/server/src/orchestration-v2/SteeringCompletion.integration.test.ts index e2a1793ba5f93..b7d0569d1e644 100644 --- a/apps/server/src/orchestration-v2/SteeringCompletion.integration.test.ts +++ b/apps/server/src/orchestration-v2/SteeringCompletion.integration.test.ts @@ -1628,7 +1628,7 @@ it.effect("promotes a queued goal by stopping the running turn", () => ); it.effect.each(["next", "/goal ship it"])( - "promoting %s, moved ahead of a completion, into a compaction stops it", + "promoting %s into a compaction while a message is moved ahead of a completion", (promoted) => Effect.scoped( Effect.gen(function* () { @@ -1742,7 +1742,9 @@ it.effect.each(["next", "/goal ship it"])( Effect.map(orchestrator.getThreadProjection(threadId), ({ runs }) => runs.find((run) => run.userMessageId === MessageId.make(`message:${key}`))!, ); - for (const key of ["next", "queued"]) { + yield* send("correction", "correction", "queue"); + // A goal is moved ahead of the completion too, so promoting it must drop that priority. + for (const key of promoted === "next" ? ["correction"] : ["next", "correction"]) { yield* orchestrator.dispatch({ type: "queued-run.reorder", commandId: CommandId.make(`reorder-${key}`), @@ -1770,15 +1772,15 @@ it.effect.each(["next", "/goal ship it"])( ), ); yield* finish(2); - // A plain message keeps the other prioritized message ahead of the completion; a goal, - // like any queued goal, sends after automatic completions. + // A plain message leaves the reordered correction ahead of the completion; a goal, like + // any queued goal, goes after automatic completions and ahead of other messages. assert.deepEqual( started.map((turn) => turn.runId), [ started[0]!.runId, compactRun.id, ...(promoted === "next" - ? [(yield* runFor("next")).id, (yield* runFor("queued")).id] + ? [(yield* runFor("correction")).id, (yield* runFor("completion")).id] : [(yield* runFor("completion")).id, (yield* runFor("next")).id]), ], ); From e3db5a2d61d2c38aff26ef24f62b110a4bc6b29c Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 3 Nov 2026 18:54:48 +0000 Subject: [PATCH 9/9] test(server): a provider turn without an attempt is not the next start Co-Authored-By: Claude Opus 5.5 --- .../src/orchestration-v2/SteeringCompletion.integration.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/server/src/orchestration-v2/SteeringCompletion.integration.test.ts b/apps/server/src/orchestration-v2/SteeringCompletion.integration.test.ts index b7d0569d1e644..b0308bb7167c5 100644 --- a/apps/server/src/orchestration-v2/SteeringCompletion.integration.test.ts +++ b/apps/server/src/orchestration-v2/SteeringCompletion.integration.test.ts @@ -1649,6 +1649,7 @@ it.effect.each(["next", "/goal ship it"])( (event) => event.type === "provider-turn.updated" && event.payload.status === "running" && + event.payload.runAttemptId !== null && !known.includes(event.payload.runAttemptId), ), Stream.take(1),