diff --git a/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts b/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts index 376f27321ca4c..edf5c4462887f 100644 --- a/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts +++ b/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts @@ -53,6 +53,7 @@ import * as ServerSettings from "../serverSettings.ts"; import * as ScheduledTasks from "../scheduledTasks/ScheduledTaskService.ts"; import * as SecretRequests from "../secrets/SecretRequests.ts"; import * as TextGeneration from "../textGeneration/TextGeneration.ts"; +import { ClaudeProviderCapabilitiesV2 } from "./Adapters/ClaudeAdapterV2.ts"; import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts"; import * as CommandReceiptStore from "./CommandReceiptStore.ts"; import * as EffectOutbox from "./EffectOutbox.ts"; @@ -101,6 +102,18 @@ const adapter = { openSession: () => Effect.die("provider execution is disabled in launch tests"), } as ProviderAdapter.ProviderAdapterV2["Service"]; +const claudeModelSelection = { + instanceId: ProviderInstanceId.make("claudeAgent"), + model: "claude-opus-5-5", +} as const; +const claudeAdapter = { + instanceId: claudeModelSelection.instanceId, + driver: ProviderDriverKind.make("claudeAgent"), + getCapabilities: () => Effect.succeed(ClaudeProviderCapabilitiesV2), + planSelectionTransition: () => Effect.succeed({ type: "apply_on_next_turn" as const }), + openSession: () => Effect.die("provider execution is disabled in launch tests"), +} as ProviderAdapter.ProviderAdapterV2["Service"]; + interface HarnessOptions { readonly closeTerminal?: TerminalManager.TerminalManager["Service"]["close"]; readonly adapters?: ReadonlyArray; @@ -374,6 +387,57 @@ it.effect.each( }, ); +it.effect("runs a thread-bound automation on the provider the thread uses now", () => { + const harness = makeHarness({ adapters: [adapter, claudeAdapter] }); + const layerScheduledTasks = ScheduledTasks.layer.pipe( + Layer.provide( + Layer.mergeAll( + harness.layer, + NodeCrypto.layer, + Scheduler.layer, + Layer.mock(SecretRequests.SecretRequests)({}), + ), + ), + ); + return Effect.gen(function* () { + const tasks = yield* ScheduledTasks.ScheduledTaskService; + const launches = yield* ThreadLaunch.ThreadLaunchService; + const threads = yield* ThreadManagement.ThreadManagementService; + const launched = yield* launches.launch( + launchInput({ command: "command:switched", thread: "thread:switched" }), + ); + const { task } = yield* tasks.upsert({ + id: ScheduledTaskId.make("scheduled-task:switched"), + title: "Hourly check", + prompt: "Check on the delegated workers.", + enabled: false, + schedule: { type: "interval", everyMs: 3_600_000 }, + projectId, + threadId: launched.threadId, + workspaceStrategy: { type: "root" }, + modelSelection, + runtimeMode: "full-access", + interactionMode: "default", + createdBy: "agent", + creationSource: "mcp", + }); + yield* threads.dispatch({ + type: "thread.model-selection.set", + commandId: CommandId.make("command:switch-selection"), + threadId: launched.threadId, + modelSelection: claudeModelSelection, + }); + + const result = yield* tasks.runNow({ id: task.id }); + assert.equal(result.task.lastRunStatus, "succeeded"); + const projection = yield* threads.getThreadProjection(launched.threadId); + const message = projection.messages.find((item) => item.scheduledTaskId === task.id); + const run = projection.runs.find((item) => item.id === message?.runId); + assert.deepStrictEqual(run?.modelSelection, claudeModelSelection); + assert.deepStrictEqual(projection.thread.modelSelection, claudeModelSelection); + }).pipe(Effect.provide(Layer.mergeAll(harness.layer, layerScheduledTasks))); +}); + it.effect("retains automation and sender attribution while a message waits in the queue", () => { const harness = makeHarness({ runSetup: () => Effect.never }); return Effect.gen(function* () { diff --git a/apps/server/src/scheduledTasks/ScheduledTaskService.ts b/apps/server/src/scheduledTasks/ScheduledTaskService.ts index 482ed1669d1af..82899bba752d0 100644 --- a/apps/server/src/scheduledTasks/ScheduledTaskService.ts +++ b/apps/server/src/scheduledTasks/ScheduledTaskService.ts @@ -761,6 +761,8 @@ export const layer = Layer.effect( }), ) : yield* Effect.exit( + // The stored selection only seeds fresh threads. A bound run + // follows the thread's current provider, so it passes none. threadManagement.sendToThread({ projectId: active.projectId, commandId, @@ -769,7 +771,6 @@ export const layer = Layer.effect( scheduledTaskId: active.id, text: prompt, attachments: [], - modelSelection: active.modelSelection, mode: "queue", createdBy: active.createdBy, creationSource: active.creationSource,