import { assert, it, vi } from "@effect/vitest";import { CommandId, DEFAULT_SERVER_SETTINGS, GitCommandError, MessageId, ProjectId, ProviderDriverKind, ProviderInstanceId, type ServerProvider, ThreadId,} from "@t3tools/contracts";import * as Deferred from "effect/Deferred";import * as DateTime from "effect/DateTime";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Ref from "effect/Ref";import * as Stream from "effect/Stream";import * as SqlClient from "effect/unstable/sql/SqlClient";import * as TestClock from "effect/testing/TestClock"; import * as GitWorkflow from "../git/GitWorkflowService.ts";import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";import { ProjectionProjectRepository } from "../persistence/Services/ProjectionProjects.ts";import * as ProjectService from "../project/ProjectService.ts";import * as ProjectSetupScriptRunner from "../project/ProjectSetupScriptRunner.ts";import { makeProviderRegistryLayer } from "../provider/testUtils/providerRegistryMock.ts";import * as ServerSettings from "../serverSettings.ts";import * as TextGeneration from "../textGeneration/TextGeneration.ts";import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts";import * as CommandReceiptStore from "./CommandReceiptStore.ts";import * as EffectOutbox from "./EffectOutbox.ts";import * as IdAllocator from "./IdAllocator.ts";import type { ProviderAdapterV2Shape } from "./ProviderAdapter.ts";import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts";import * as ThreadLaunch from "./ThreadLaunchService.ts";import * as ThreadManagement from "./ThreadManagementService.ts";import * as ThreadTitleRegeneration from "./ThreadTitleRegenerationService.ts";import { makeOrchestratorV2ReplayLayerWithRegistry } from "./testkit/ProviderReplayHarness.ts"; const projectId = ProjectId.make("project:launch-test");const modelSelection = { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.1-codex",} as const;const project = { id: projectId, title: "Project", workspaceRoot: "/repo", repositoryIdentity: null, faviconPath: null, defaultModelSelection: modelSelection, defaultThreadEnvMode: null, scripts: [], createdAt: "2026-06-20T00:00:00.000Z", updatedAt: "2026-06-20T00:00:00.000Z", deletedAt: null,} as const; const adapter = { instanceId: modelSelection.instanceId, driver: ProviderDriverKind.make("codex"), getCapabilities: () => Effect.succeed(CodexProviderCapabilitiesV2), planSelectionTransition: () => Effect.succeed({ type: "apply_on_next_turn" as const }), openSession: () => Effect.die("provider execution is disabled in launch tests"),} as ProviderAdapterV2Shape; interface HarnessOptions { readonly createWorktree?: GitWorkflow.GitWorkflowService["Service"]["createWorktree"]; readonly fetchRemote?: GitWorkflow.GitWorkflowService["Service"]["fetchRemote"]; readonly renameBranch?: GitWorkflow.GitWorkflowService["Service"]["renameBranch"]; readonly runSetup?: ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]["runForThread"]; readonly generateTitle?: TextGeneration.TextGeneration["Service"]["generateThreadTitle"]; readonly generateBranchName?: TextGeneration.TextGeneration["Service"]["generateBranchName"]; readonly serverSettings?: Parameters<typeof ServerSettings.layerTest>[0]; readonly providers?: ReadonlyArray<ServerProvider>;} function makeHarness(options: HarnessOptions = {}) { const database = SqlitePersistenceMemory; const registry = ProviderAdapterRegistry.makeLayer([adapter]); const orchestrator = makeOrchestratorV2ReplayLayerWithRegistry( { name: "thread-launch" }, registry, { databaseLayer: database, runEffectWorker: false }, ); const threadManagement = ThreadManagement.layer.pipe(Layer.provide(orchestrator)); const receipts = CommandReceiptStore.layer.pipe(Layer.provide(database)); const outbox = EffectOutbox.layer.pipe(Layer.provide(database)); const createWorktree = vi.fn( options.createWorktree ?? ((input) => Effect.succeed({ worktree: { path: "/repo-worktrees/feature", refName: input.newRefName, headSha: "abc" }, } as never)), ); const renameBranch = vi.fn( options.renameBranch ?? ((input) => Effect.succeed({ branch: input.newBranch })), ); const runSetup = vi.fn( options.runSetup ?? (() => Effect.succeed({ status: "no-script" as const })), ); const generateBranchName = vi.fn( options.generateBranchName ?? (() => Effect.succeed({ branch: "generated-branch" })), ); const generateThreadTitle = vi.fn( options.generateTitle ?? (() => Effect.succeed({ title: "Generated title" })), ); const externalServices = Layer.mergeAll( Layer.succeed(ProjectService.ProjectService, { create: () => Effect.die("unused"), bootstrap: () => Effect.die("unused"), update: () => Effect.die("unused"), delete: () => Effect.die("unused"), getById: (id) => Effect.succeed(id === projectId ? Option.some(project) : Option.none()), getByWorkspaceRoot: () => Effect.succeed(Option.some(project)), snapshot: Effect.die("unused"), }), Layer.mock(GitWorkflow.GitWorkflowService)({ createWorktree, renameBranch, fetchRemote: options.fetchRemote ?? (() => Effect.void), remoteExists: () => Effect.succeed(true), remoteBranchExists: () => Effect.succeed(true), removeWorktree: () => Effect.void, resolveRemoteTrackingCommit: () => Effect.succeed({ commitSha: "remote-main-sha", remoteRefName: "origin/main" }), }), Layer.succeed(ProjectSetupScriptRunner.ProjectSetupScriptRunner, { runForThread: runSetup, }), Layer.mock(TextGeneration.TextGeneration)({ generateThreadTitle, generateBranchName, }), ServerSettings.layerTest(options.serverSettings), makeProviderRegistryLayer(options.providers), ); const launch = ThreadLaunch.layer.pipe( Layer.provide(Layer.mergeAll(externalServices, threadManagement, receipts, IdAllocator.layer)), ); const projectedProjects = Layer.mock(ProjectionProjectRepository)({ getById: ({ projectId: requestedProjectId }) => Effect.succeed( requestedProjectId === projectId ? Option.some({ projectId, title: project.title, workspaceRoot: project.workspaceRoot, defaultModelSelection: project.defaultModelSelection, defaultThreadEnvMode: null, autoPull: false, scripts: project.scripts, createdAt: project.createdAt, updatedAt: project.updatedAt, deletedAt: project.deletedAt, }) : Option.none(), ), }); const titleRegeneration = ThreadTitleRegeneration.layer.pipe( Layer.provide(Layer.mergeAll(threadManagement, projectedProjects, externalServices)), ); return { layer: Layer.mergeAll(launch, threadManagement, titleRegeneration, outbox, database), createWorktree, renameBranch, generateBranchName, generateThreadTitle, runSetup, };} function launchInput(input: { readonly command: string; readonly thread: string; readonly message?: string; readonly workspace?: ThreadLaunch.ThreadLaunchWorkspaceStrategy;}) { return { commandId: CommandId.make(input.command), threadId: ThreadId.make(input.thread), projectId, title: "New thread", modelSelection, runtimeMode: "full-access" as const, interactionMode: "default" as const, workspaceStrategy: input.workspace ?? { type: "root" as const }, ...(input.message === undefined ? {} : { initialMessage: { messageId: MessageId.make(`${input.message}:id`), text: input.message, attachments: [], }, }), createdBy: "user" as const, creationSource: "web" as const, };} function waitUntil<E, R>(predicate: () => Effect.Effect<boolean, E, R>): Effect.Effect<void, E, R> { return Effect.gen(function* () { for (let attempt = 0; attempt < 200; attempt += 1) { if (yield* predicate()) return; yield* Effect.promise( () => new Promise<void>((resolve) => { setImmediate(resolve); }), ); } assert.fail("Condition was not reached before timeout."); });} it.effect("returns a visible preparing message while provisioning is still blocked", () => Effect.gen(function* () { const worktreeEntered = yield* Deferred.make<void>(); const allowWorktree = yield* Deferred.make<void>(); const setupEntered = yield* Deferred.make<void>(); const allowSetup = yield* Deferred.make<void>(); const harness = makeHarness({ createWorktree: () => Deferred.succeed(worktreeEntered, undefined).pipe( Effect.andThen(Deferred.await(allowWorktree)), Effect.as({ worktree: { path: "/repo-worktrees/feature", refName: "feature", headSha: "abc" }, } as never), ), runSetup: () => Deferred.succeed(setupEntered, undefined).pipe( Effect.andThen(Deferred.await(allowSetup)), Effect.as({ status: "no-script" as const }), ), }); yield* Effect.gen(function* () { const launches = yield* ThreadLaunch.ThreadLaunchService; const outbox = yield* EffectOutbox.EffectOutboxV2; const threads = yield* ThreadManagement.ThreadManagementService; const input = launchInput({ command: "command:launch:blocked", thread: "thread:launch:blocked", message: "Build the feature", workspace: { type: "worktree", baseRef: "main" }, }); const launched = yield* launches.launch(input); assert.equal(launched.projection.messages[0]?.text, "Build the feature"); assert.equal(launched.projection.runs[0]?.status, "preparing"); assert.equal( launched.projection.turnItems.find((item) => item.type === "command_execution")?.status, "running", ); yield* Deferred.await(worktreeEntered); let current = yield* threads.getThreadProjection(launched.threadId); assert.equal( current.turnItems.find((item) => item.type === "command_execution")?.title, "Preparing worktree", ); yield* Deferred.succeed(allowWorktree, undefined); const entered = yield* Deferred.await(setupEntered).pipe( Effect.timeoutOption(Duration.seconds(2)), ); if (Option.isNone(entered)) { current = yield* threads.getThreadProjection(launched.threadId); assert.fail( `Setup was not reached; run=${current.runs[0]?.status ?? "missing"}, worklog=${current.turnItems.find((item) => item.type === "command_execution")?.title ?? "missing"}.`, ); } current = yield* threads.getThreadProjection(launched.threadId); assert.equal( current.turnItems.find((item) => item.type === "command_execution")?.title, "Starting setup script", ); const prematureEffects = yield* outbox.listByCommandId( CommandId.make("command:launch:blocked:initial-message"), ); assert.isEmpty(prematureEffects); yield* Deferred.succeed(allowSetup, undefined); }).pipe(Effect.provide(harness.layer)); }),); it.effect("provisions independent launches concurrently instead of behind a global semaphore", () => Effect.gen(function* () { const setupCount = yield* Ref.make(0); const bothEntered = yield* Deferred.make<void>(); const allowSetup = yield* Deferred.make<void>(); const harness = makeHarness({ runSetup: () => Ref.updateAndGet(setupCount, (count) => count + 1).pipe( Effect.tap((count) => count === 2 ? Deferred.succeed(bothEntered, undefined) : Effect.void, ), Effect.andThen(Deferred.await(allowSetup)), Effect.as({ status: "no-script" as const }), ), }); yield* Effect.gen(function* () { const launches = yield* ThreadLaunch.ThreadLaunchService; const results = yield* Effect.all( [ launches.launch( launchInput({ command: "command:launch:concurrent-a", thread: "thread:launch:concurrent-a", message: "First", }), ), launches.launch( launchInput({ command: "command:launch:concurrent-b", thread: "thread:launch:concurrent-b", message: "Second", }), ), ], { concurrency: "unbounded" }, ); assert.deepEqual( results.map((result) => result.projection.runs[0]?.status), ["preparing", "preparing"], ); yield* Deferred.await(bothEntered); assert.equal(yield* Ref.get(setupCount), 2); yield* Deferred.succeed(allowSetup, undefined); }).pipe(Effect.provide(harness.layer)); }),); it.effect("enqueues provider work only after setup has been initiated", () => Effect.gen(function* () { const setupEntered = yield* Deferred.make<void>(); const allowSetup = yield* Deferred.make<void>(); const harness = makeHarness({ runSetup: () => Deferred.succeed(setupEntered, undefined).pipe( Effect.andThen(Deferred.await(allowSetup)), Effect.as({ status: "started" as const, scriptId: "setup", scriptName: "Setup", terminalId: "setup", cwd: "/repo", }), ), }); yield* Effect.gen(function* () { const launches = yield* ThreadLaunch.ThreadLaunchService; const outbox = yield* EffectOutbox.EffectOutboxV2; const threads = yield* ThreadManagement.ThreadManagementService; const input = launchInput({ command: "command:launch:release", thread: "thread:launch:release", message: "Start after setup", workspace: { type: "worktree", baseRef: "main" }, }); const launched = yield* launches.launch(input); yield* Deferred.await(setupEntered); assert.isEmpty( yield* outbox.listByCommandId(CommandId.make("command:launch:release:release")), ); yield* Deferred.succeed(allowSetup, undefined); yield* waitUntil(() => outbox .listByCommandId(CommandId.make("command:launch:release:release")) .pipe(Effect.map((effects) => effects.length === 1)), ); const projection = yield* threads.getThreadProjection(launched.threadId); assert.equal(projection.runs[0]?.status, "starting"); assert.equal(projection.checkpointScopes[0]?.cwd, "/repo-worktrees/feature"); assert.equal( projection.turnItems.find((item) => item.type === "command_execution")?.status, "completed", ); }).pipe(Effect.provide(harness.layer)); }),); it.effect( "queues follow-up messages behind preparation and checkpoints them in the final workspace", () => Effect.gen(function* () { const setupEntered = yield* Deferred.make<void>(); const failSetup = yield* Deferred.make<void>(); const harness = makeHarness({ runSetup: () => Deferred.succeed(setupEntered, undefined).pipe( Effect.andThen(Deferred.await(failSetup)), Effect.andThen(Effect.fail(new Error("setup failed") as never)), ), }); yield* Effect.gen(function* () { const launches = yield* ThreadLaunch.ThreadLaunchService; const threads = yield* ThreadManagement.ThreadManagementService; const launched = yield* launches.launch( launchInput({ command: "command:launch:queued-during-preparation", thread: "thread:launch:queued-during-preparation", message: "Prepare the workspace", workspace: { type: "worktree", baseRef: "main" }, }), ); yield* Deferred.await(setupEntered); const followUp = yield* threads.sendToThread({ projectId, commandId: CommandId.make("command:launch:queued-follow-up"), threadId: launched.threadId, messageId: MessageId.make("message:launch:queued-follow-up"), text: "Run after preparation", attachments: [], mode: "auto", createdBy: "user", creationSource: "web", }); assert.equal(followUp.delivery, "queued"); assert.equal(followUp.run.status, "queued"); assert.equal( followUp.projection.nodes.find( (node) => node.runId === followUp.run.id && node.kind === "root_turn", )?.checkpointScopeId, null, ); yield* Deferred.succeed(failSetup, undefined); yield* waitUntil(() => threads .getThreadProjection(launched.threadId) .pipe( Effect.map( (projection) => projection.runs.find((run) => run.id === followUp.run.id)?.status === "starting", ), ), ); const projection = yield* threads.getThreadProjection(launched.threadId); const rootNode = projection.nodes.find( (node) => node.runId === followUp.run.id && node.kind === "root_turn", ); assert.isNotNull(rootNode?.checkpointScopeId); assert.equal( projection.checkpointScopes.find((scope) => scope.id === rootNode?.checkpointScopeId) ?.cwd, "/repo-worktrees/feature", ); }).pipe(Effect.provide(harness.layer)); }),); for (const nativeCommand of [" /COMPACT ", "/logout"]) { it.effect(`uses the first conversation message for a title after ${nativeCommand}`, () => Effect.gen(function* () { const harness = makeHarness(); yield* Effect.gen(function* () { const launches = yield* ThreadLaunch.ThreadLaunchService; const threads = yield* ThreadManagement.ThreadManagementService; const outbox = yield* EffectOutbox.EffectOutboxV2; const launched = yield* launches.launch({ ...launchInput({ command: "compact-title", thread: "compact-title-thread", message: nativeCommand, }), generateTitle: true, }); assert.isUndefined( (yield* threads.getThreadProjection(launched.threadId)).thread.titleRegeneration, ); assert.isFalse( (yield* outbox.listByCommandId(CommandId.make("compact-title:initial-message"))).some( (effect) => effect.request.type === "thread-title.generate", ), ); const commandId = CommandId.make("compact-title-conversation"); const messageId = MessageId.make("compact-title-conversation-message"); yield* threads.dispatch({ type: "message.dispatch", commandId, threadId: launched.threadId, messageId, createdBy: "user", creationSource: "web", text: "Fix the failing parser", attachments: [], dispatchMode: { type: "defer_start" }, }); assert.equal( (yield* threads.getThreadProjection(launched.threadId)).thread.titleRegeneration ?.requestId, commandId, ); assert.deepEqual( (yield* outbox.listByCommandId(commandId)) .filter((effect) => effect.request.type === "thread-title.generate")Lines 1–500 of 1288. Load more
import { assert, it, vi } from "@effect/vitest"; import { CommandId, DEFAULT_SERVER_SETTINGS, GitCommandError, MessageId, ProjectId, ProviderDriverKind, ProviderInstanceId, type ServerProvider, ThreadId, } from "@t3tools/contracts"; import * as Deferred from "effect/Deferred"; import * as DateTime from "effect/DateTime"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Exit from "effect/Exit"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Ref from "effect/Ref"; import * as Stream from "effect/Stream"; import * as SqlClient from "effect/unstable/sql/SqlClient"; import * as TestClock from "effect/testing/TestClock"; import * as GitWorkflow from "../git/GitWorkflowService.ts"; import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts"; import { ProjectionProjectRepository } from "../persistence/Services/ProjectionProjects.ts"; import * as ProjectService from "../project/ProjectService.ts"; import * as ProjectSetupScriptRunner from "../project/ProjectSetupScriptRunner.ts"; import { makeProviderRegistryLayer } from "../provider/testUtils/providerRegistryMock.ts"; import * as ServerSettings from "../serverSettings.ts"; import * as TextGeneration from "../textGeneration/TextGeneration.ts"; import { CodexProviderCapabilitiesV2 } from "./Adapters/CodexAdapterV2.ts"; import * as CommandReceiptStore from "./CommandReceiptStore.ts"; import * as EffectOutbox from "./EffectOutbox.ts"; import * as IdAllocator from "./IdAllocator.ts"; import type { ProviderAdapterV2Shape } from "./ProviderAdapter.ts"; import * as ProviderAdapterRegistry from "./ProviderAdapterRegistry.ts"; import * as ThreadLaunch from "./ThreadLaunchService.ts"; import * as ThreadManagement from "./ThreadManagementService.ts"; import * as ThreadTitleRegeneration from "./ThreadTitleRegenerationService.ts"; import { makeOrchestratorV2ReplayLayerWithRegistry } from "./testkit/ProviderReplayHarness.ts"; const projectId = ProjectId.make("project:launch-test"); const modelSelection = { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.1-codex", } as const; const project = { id: projectId, title: "Project", workspaceRoot: "/repo", repositoryIdentity: null, faviconPath: null, defaultModelSelection: modelSelection, defaultThreadEnvMode: null, scripts: [], createdAt: "2026-06-20T00:00:00.000Z", updatedAt: "2026-06-20T00:00:00.000Z", deletedAt: null, } as const; const adapter = { instanceId: modelSelection.instanceId, driver: ProviderDriverKind.make("codex"), getCapabilities: () => Effect.succeed(CodexProviderCapabilitiesV2), planSelectionTransition: () => Effect.succeed({ type: "apply_on_next_turn" as const }), openSession: () => Effect.die("provider execution is disabled in launch tests"), } as ProviderAdapterV2Shape; interface HarnessOptions { readonly createWorktree?: GitWorkflow.GitWorkflowService["Service"]["createWorktree"]; readonly fetchRemote?: GitWorkflow.GitWorkflowService["Service"]["fetchRemote"]; readonly renameBranch?: GitWorkflow.GitWorkflowService["Service"]["renameBranch"]; readonly runSetup?: ProjectSetupScriptRunner.ProjectSetupScriptRunner["Service"]["runForThread"]; readonly generateTitle?: TextGeneration.TextGeneration["Service"]["generateThreadTitle"]; readonly generateBranchName?: TextGeneration.TextGeneration["Service"]["generateBranchName"]; readonly serverSettings?: Parameters<typeof ServerSettings.layerTest>[0]; readonly providers?: ReadonlyArray<ServerProvider>; } function makeHarness(options: HarnessOptions = {}) { const database = SqlitePersistenceMemory; const registry = ProviderAdapterRegistry.makeLayer([adapter]); const orchestrator = makeOrchestratorV2ReplayLayerWithRegistry( { name: "thread-launch" }, registry, { databaseLayer: database, runEffectWorker: false }, ); const threadManagement = ThreadManagement.layer.pipe(Layer.provide(orchestrator)); const receipts = CommandReceiptStore.layer.pipe(Layer.provide(database)); const outbox = EffectOutbox.layer.pipe(Layer.provide(database)); const createWorktree = vi.fn( options.createWorktree ?? ((input) => Effect.succeed({ worktree: { path: "/repo-worktrees/feature", refName: input.newRefName, headSha: "abc" }, } as never)), ); const renameBranch = vi.fn( options.renameBranch ?? ((input) => Effect.succeed({ branch: input.newBranch })), ); const runSetup = vi.fn( options.runSetup ?? (() => Effect.succeed({ status: "no-script" as const })), ); const generateBranchName = vi.fn( options.generateBranchName ?? (() => Effect.succeed({ branch: "generated-branch" })), ); const generateThreadTitle = vi.fn( options.generateTitle ?? (() => Effect.succeed({ title: "Generated title" })), ); const externalServices = Layer.mergeAll( Layer.succeed(ProjectService.ProjectService, { create: () => Effect.die("unused"), bootstrap: () => Effect.die("unused"), update: () => Effect.die("unused"), delete: () => Effect.die("unused"), getById: (id) => Effect.succeed(id === projectId ? Option.some(project) : Option.none()), getByWorkspaceRoot: () => Effect.succeed(Option.some(project)), snapshot: Effect.die("unused"), }), Layer.mock(GitWorkflow.GitWorkflowService)({ createWorktree, renameBranch, fetchRemote: options.fetchRemote ?? (() => Effect.void), remoteExists: () => Effect.succeed(true), remoteBranchExists: () => Effect.succeed(true), removeWorktree: () => Effect.void, resolveRemoteTrackingCommit: () => Effect.succeed({ commitSha: "remote-main-sha", remoteRefName: "origin/main" }), }), Layer.succeed(ProjectSetupScriptRunner.ProjectSetupScriptRunner, { runForThread: runSetup, }), Layer.mock(TextGeneration.TextGeneration)({ generateThreadTitle, generateBranchName, }), ServerSettings.layerTest(options.serverSettings), makeProviderRegistryLayer(options.providers), ); const launch = ThreadLaunch.layer.pipe( Layer.provide(Layer.mergeAll(externalServices, threadManagement, receipts, IdAllocator.layer)), ); const projectedProjects = Layer.mock(ProjectionProjectRepository)({ getById: ({ projectId: requestedProjectId }) => Effect.succeed( requestedProjectId === projectId ? Option.some({ projectId, title: project.title, workspaceRoot: project.workspaceRoot, defaultModelSelection: project.defaultModelSelection, defaultThreadEnvMode: null, autoPull: false, scripts: project.scripts, createdAt: project.createdAt, updatedAt: project.updatedAt, deletedAt: project.deletedAt, }) : Option.none(), ), }); const titleRegeneration = ThreadTitleRegeneration.layer.pipe( Layer.provide(Layer.mergeAll(threadManagement, projectedProjects, externalServices)), ); return { layer: Layer.mergeAll(launch, threadManagement, titleRegeneration, outbox, database), createWorktree, renameBranch, generateBranchName, generateThreadTitle, runSetup, }; } function launchInput(input: { readonly command: string; readonly thread: string; readonly message?: string; readonly workspace?: ThreadLaunch.ThreadLaunchWorkspaceStrategy; }) { return { commandId: CommandId.make(input.command), threadId: ThreadId.make(input.thread), projectId, title: "New thread", modelSelection, runtimeMode: "full-access" as const, interactionMode: "default" as const, workspaceStrategy: input.workspace ?? { type: "root" as const }, ...(input.message === undefined ? {} : { initialMessage: { messageId: MessageId.make(`${input.message}:id`), text: input.message, attachments: [], }, }), createdBy: "user" as const, creationSource: "web" as const, }; } function waitUntil<E, R>(predicate: () => Effect.Effect<boolean, E, R>): Effect.Effect<void, E, R> { return Effect.gen(function* () { for (let attempt = 0; attempt < 200; attempt += 1) { if (yield* predicate()) return; yield* Effect.promise( () => new Promise<void>((resolve) => { setImmediate(resolve); }), ); } assert.fail("Condition was not reached before timeout."); }); } it.effect("returns a visible preparing message while provisioning is still blocked", () => Effect.gen(function* () { const worktreeEntered = yield* Deferred.make<void>(); const allowWorktree = yield* Deferred.make<void>(); const setupEntered = yield* Deferred.make<void>(); const allowSetup = yield* Deferred.make<void>(); const harness = makeHarness({ createWorktree: () => Deferred.succeed(worktreeEntered, undefined).pipe( Effect.andThen(Deferred.await(allowWorktree)), Effect.as({ worktree: { path: "/repo-worktrees/feature", refName: "feature", headSha: "abc" }, } as never), ), runSetup: () => Deferred.succeed(setupEntered, undefined).pipe( Effect.andThen(Deferred.await(allowSetup)), Effect.as({ status: "no-script" as const }), ), }); yield* Effect.gen(function* () { const launches = yield* ThreadLaunch.ThreadLaunchService; const outbox = yield* EffectOutbox.EffectOutboxV2; const threads = yield* ThreadManagement.ThreadManagementService; const input = launchInput({ command: "command:launch:blocked", thread: "thread:launch:blocked", message: "Build the feature", workspace: { type: "worktree", baseRef: "main" }, }); const launched = yield* launches.launch(input); assert.equal(launched.projection.messages[0]?.text, "Build the feature"); assert.equal(launched.projection.runs[0]?.status, "preparing"); assert.equal( launched.projection.turnItems.find((item) => item.type === "command_execution")?.status, "running", ); yield* Deferred.await(worktreeEntered); let current = yield* threads.getThreadProjection(launched.threadId); assert.equal( current.turnItems.find((item) => item.type === "command_execution")?.title, "Preparing worktree", ); yield* Deferred.succeed(allowWorktree, undefined); const entered = yield* Deferred.await(setupEntered).pipe( Effect.timeoutOption(Duration.seconds(2)), ); if (Option.isNone(entered)) { current = yield* threads.getThreadProjection(launched.threadId); assert.fail( `Setup was not reached; run=${current.runs[0]?.status ?? "missing"}, worklog=${current.turnItems.find((item) => item.type === "command_execution")?.title ?? "missing"}.`, ); } current = yield* threads.getThreadProjection(launched.threadId); assert.equal( current.turnItems.find((item) => item.type === "command_execution")?.title, "Starting setup script", ); const prematureEffects = yield* outbox.listByCommandId( CommandId.make("command:launch:blocked:initial-message"), ); assert.isEmpty(prematureEffects); yield* Deferred.succeed(allowSetup, undefined); }).pipe(Effect.provide(harness.layer)); }), ); it.effect("provisions independent launches concurrently instead of behind a global semaphore", () => Effect.gen(function* () { const setupCount = yield* Ref.make(0); const bothEntered = yield* Deferred.make<void>(); const allowSetup = yield* Deferred.make<void>(); const harness = makeHarness({ runSetup: () => Ref.updateAndGet(setupCount, (count) => count + 1).pipe( Effect.tap((count) => count === 2 ? Deferred.succeed(bothEntered, undefined) : Effect.void, ), Effect.andThen(Deferred.await(allowSetup)), Effect.as({ status: "no-script" as const }), ), }); yield* Effect.gen(function* () { const launches = yield* ThreadLaunch.ThreadLaunchService; const results = yield* Effect.all( [ launches.launch( launchInput({ command: "command:launch:concurrent-a", thread: "thread:launch:concurrent-a", message: "First", }), ), launches.launch( launchInput({ command: "command:launch:concurrent-b", thread: "thread:launch:concurrent-b", message: "Second", }), ), ], { concurrency: "unbounded" }, ); assert.deepEqual( results.map((result) => result.projection.runs[0]?.status), ["preparing", "preparing"], ); yield* Deferred.await(bothEntered); assert.equal(yield* Ref.get(setupCount), 2); yield* Deferred.succeed(allowSetup, undefined); }).pipe(Effect.provide(harness.layer)); }), ); it.effect("enqueues provider work only after setup has been initiated", () => Effect.gen(function* () { const setupEntered = yield* Deferred.make<void>(); const allowSetup = yield* Deferred.make<void>(); const harness = makeHarness({ runSetup: () => Deferred.succeed(setupEntered, undefined).pipe( Effect.andThen(Deferred.await(allowSetup)), Effect.as({ status: "started" as const, scriptId: "setup", scriptName: "Setup", terminalId: "setup", cwd: "/repo", }), ), }); yield* Effect.gen(function* () { const launches = yield* ThreadLaunch.ThreadLaunchService; const outbox = yield* EffectOutbox.EffectOutboxV2; const threads = yield* ThreadManagement.ThreadManagementService; const input = launchInput({ command: "command:launch:release", thread: "thread:launch:release", message: "Start after setup", workspace: { type: "worktree", baseRef: "main" }, }); const launched = yield* launches.launch(input); yield* Deferred.await(setupEntered); assert.isEmpty( yield* outbox.listByCommandId(CommandId.make("command:launch:release:release")), ); yield* Deferred.succeed(allowSetup, undefined); yield* waitUntil(() => outbox .listByCommandId(CommandId.make("command:launch:release:release")) .pipe(Effect.map((effects) => effects.length === 1)), ); const projection = yield* threads.getThreadProjection(launched.threadId); assert.equal(projection.runs[0]?.status, "starting"); assert.equal(projection.checkpointScopes[0]?.cwd, "/repo-worktrees/feature"); assert.equal( projection.turnItems.find((item) => item.type === "command_execution")?.status, "completed", ); }).pipe(Effect.provide(harness.layer)); }), ); it.effect( "queues follow-up messages behind preparation and checkpoints them in the final workspace", () => Effect.gen(function* () { const setupEntered = yield* Deferred.make<void>(); const failSetup = yield* Deferred.make<void>(); const harness = makeHarness({ runSetup: () => Deferred.succeed(setupEntered, undefined).pipe( Effect.andThen(Deferred.await(failSetup)), Effect.andThen(Effect.fail(new Error("setup failed") as never)), ), }); yield* Effect.gen(function* () { const launches = yield* ThreadLaunch.ThreadLaunchService; const threads = yield* ThreadManagement.ThreadManagementService; const launched = yield* launches.launch( launchInput({ command: "command:launch:queued-during-preparation", thread: "thread:launch:queued-during-preparation", message: "Prepare the workspace", workspace: { type: "worktree", baseRef: "main" }, }), ); yield* Deferred.await(setupEntered); const followUp = yield* threads.sendToThread({ projectId, commandId: CommandId.make("command:launch:queued-follow-up"), threadId: launched.threadId, messageId: MessageId.make("message:launch:queued-follow-up"), text: "Run after preparation", attachments: [], mode: "auto", createdBy: "user", creationSource: "web", }); assert.equal(followUp.delivery, "queued"); assert.equal(followUp.run.status, "queued"); assert.equal( followUp.projection.nodes.find( (node) => node.runId === followUp.run.id && node.kind === "root_turn", )?.checkpointScopeId, null, ); yield* Deferred.succeed(failSetup, undefined); yield* waitUntil(() => threads .getThreadProjection(launched.threadId) .pipe( Effect.map( (projection) => projection.runs.find((run) => run.id === followUp.run.id)?.status === "starting", ), ), ); const projection = yield* threads.getThreadProjection(launched.threadId); const rootNode = projection.nodes.find( (node) => node.runId === followUp.run.id && node.kind === "root_turn", ); assert.isNotNull(rootNode?.checkpointScopeId); assert.equal( projection.checkpointScopes.find((scope) => scope.id === rootNode?.checkpointScopeId) ?.cwd, "/repo-worktrees/feature", ); }).pipe(Effect.provide(harness.layer)); }), ); for (const nativeCommand of [" /COMPACT ", "/logout"]) { it.effect(`uses the first conversation message for a title after ${nativeCommand}`, () => Effect.gen(function* () { const harness = makeHarness(); yield* Effect.gen(function* () { const launches = yield* ThreadLaunch.ThreadLaunchService; const threads = yield* ThreadManagement.ThreadManagementService; const outbox = yield* EffectOutbox.EffectOutboxV2; const launched = yield* launches.launch({ ...launchInput({ command: "compact-title", thread: "compact-title-thread", message: nativeCommand, }), generateTitle: true, }); assert.isUndefined( (yield* threads.getThreadProjection(launched.threadId)).thread.titleRegeneration, ); assert.isFalse( (yield* outbox.listByCommandId(CommandId.make("compact-title:initial-message"))).some( (effect) => effect.request.type === "thread-title.generate", ), ); const commandId = CommandId.make("compact-title-conversation"); const messageId = MessageId.make("compact-title-conversation-message"); yield* threads.dispatch({ type: "message.dispatch", commandId, threadId: launched.threadId, messageId, createdBy: "user", creationSource: "web", text: "Fix the failing parser", attachments: [], dispatchMode: { type: "defer_start" }, }); assert.equal( (yield* threads.getThreadProjection(launched.threadId)).thread.titleRegeneration ?.requestId, commandId, ); assert.deepEqual( (yield* outbox.listByCommandId(commandId)) .filter((effect) => effect.request.type === "thread-title.generate")