diff --git a/apps/server/src/orchestration-v2/ThreadManagementService.ts b/apps/server/src/orchestration-v2/ThreadManagementService.ts index de12768ac7b17..4590fb10f2d88 100644 --- a/apps/server/src/orchestration-v2/ThreadManagementService.ts +++ b/apps/server/src/orchestration-v2/ThreadManagementService.ts @@ -865,6 +865,7 @@ const make = Effect.gen(function* () { ...(input.modelSelection === undefined ? {} : { modelSelection: input.modelSelection }), ...(input.freshContext ? { freshContext: true } : {}), dispatchMode, + ...(mode === "auto" ? { deliveryIntent: "auto" } : {}), createdBy: input.createdBy, creationSource: input.creationSource, }); diff --git a/apps/server/src/orchestration-v2/ThreadStop.test.ts b/apps/server/src/orchestration-v2/ThreadStop.test.ts index 6b3ea08a86449..a0e6d4bae4c86 100644 --- a/apps/server/src/orchestration-v2/ThreadStop.test.ts +++ b/apps/server/src/orchestration-v2/ThreadStop.test.ts @@ -796,6 +796,98 @@ it.effect("a message the user sends after Stop releases the held queue; an agent }).pipe(Effect.provide(layerTest)), ); +it.effect( + "an agent's auto send queues behind a run that is still stopping instead of steering it", + () => + Effect.gen(function* () { + const orchestrator = yield* Orchestrator.OrchestratorV2; + const projections = yield* ProjectionStore.ProjectionStoreV2; + const threads = yield* ThreadManagementService.ThreadManagementService; + const threadId = ThreadId.make("thread:stop-mcp-send"); + yield* createWatchingThread(threadId, 6); + yield* send(threadId, "active", "start_immediately"); + const initial = yield* orchestrator.getThreadProjection(threadId); + const run = initial.runs[0]!; + const providerSessionId = ProviderSessionId.make("session:stop-mcp-send"); + const now = yield* DateTime.now; + yield* projections.apply({ + id: EventId.make("event:stop-mcp-send:session"), + type: "provider-session.updated", + threadId, + occurredAt: now, + payload: { + id: providerSessionId, + driver: adapter.driver, + providerInstanceId: instanceId, + status: "ready", + cwd: "/repo", + model: modelSelection.model, + capabilities: CodexProviderCapabilitiesV2, + createdAt: now, + updatedAt: now, + lastError: null, + }, + }); + yield* projections.apply({ + id: EventId.make("event:stop-mcp-send:provider-thread"), + type: "provider-thread.updated", + threadId, + occurredAt: now, + payload: { ...initial.providerThreads[0]!, providerSessionId }, + }); + yield* projections.apply({ + id: EventId.make("event:stop-mcp-send:running"), + type: "run.updated", + threadId, + runId: run.id, + occurredAt: now, + payload: { ...run, status: "running", startedAt: now }, + }); + yield* projections.apply({ + id: EventId.make("event:stop-mcp-send:turn"), + type: "provider-turn.updated", + threadId, + occurredAt: now, + payload: { + id: ProviderTurnId.make("provider-turn:stop-mcp-send"), + providerThreadId: run.providerThreadId!, + nodeId: run.rootNodeId!, + runAttemptId: run.activeAttemptId!, + nativeTurnRef: null, + ordinal: 1, + status: "running", + startedAt: now, + completedAt: null, + }, + }); + yield* send(threadId, "queued before the stop", "queue_after_active"); + yield* orchestrator.dispatch({ + type: "thread.stop", + commandId: CommandId.make("stop-mcp-send"), + threadId, + }); + assert.deepEqual((yield* threadState(threadId)).runs, ["running", "queued:held"]); + + const result = yield* threads.sendToThread({ + projectId: ProjectId.make("project:thread-stop"), + commandId: CommandId.make("command:stop-mcp-send"), + threadId, + messageId: MessageId.make("message:stop-mcp-send"), + text: "Follow up after Stop", + attachments: [], + mode: "auto", + createdBy: "agent", + creationSource: "mcp", + }); + assert.equal(result.delivery, "queued"); + assert.deepEqual((yield* threadState(threadId)).runs, [ + "running", + "queued:held", + "queued:held", + ]); + }).pipe(Effect.provide(layerTest)), +); + it.effect( "thread.stop ends an idle thread's watches and accepts a thread with nothing to stop", () =>