From 3325bfc4aec3d87e1403283583bde24874f3fd52 Mon Sep 17 00:00:00 2001 From: akash9676 Date: Tue, 27 Oct 2026 14:20:16 +0100 Subject: [PATCH 1/2] fix(server): honor worktree prefixes before automatic naming --- .../ThreadLaunchService.test.ts | 77 +++++++++++++++++++ .../orchestration-v2/ThreadLaunchService.ts | 47 +++++++---- 2 files changed, 110 insertions(+), 14 deletions(-) diff --git a/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts b/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts index a091d96feda6..a0c0d88818a2 100644 --- a/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts +++ b/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts @@ -25,6 +25,7 @@ import { ScheduledTaskId, type ServerProvider, ThreadId, + TextGenerationError, } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; import * as Deferred from "effect/Deferred"; @@ -914,6 +915,82 @@ const preparationSettled = (threadId: ThreadId) => return yield* threads.getThreadProjection(threadId); }); +it.effect.each([ + { prefix: "my-prefix", collision: null, expected: /^my-prefix\/[0-9a-f]{8}$/ }, + { prefix: "my-prefix", collision: "my-prefix", expected: /^my-prefix-[0-9a-f]{8}$/ }, + { prefix: "team/topic", collision: "team", expected: /^team-topic-[0-9a-f]{8}$/ }, + { prefix: "team/topic", collision: "team/topic", expected: /^team-topic-[0-9a-f]{8}$/ }, + { prefix: "", collision: null, expected: /^[0-9a-f]{8}$/ }, +])("keeps prefix '$prefix' with collision '$collision' when naming fails", (scenario) => + Effect.gen(function* () { + const namingAttempted = yield* Deferred.make(); + const harness = makeHarness({ + serverSettings: { + branchNamePrefix: "global-prefix", + projectSettingsOverrides: { + [projectId]: { branchNamingMode: "static", branchNamePrefix: scenario.prefix }, + }, + }, + hasCommit: ({ refName }) => Effect.succeed(refName === `refs/heads/${scenario.collision}`), + generateBranchName: () => + Deferred.succeed(namingAttempted, undefined).pipe( + Effect.andThen( + Effect.fail( + new TextGenerationError({ operation: "generateBranchName", detail: "Unavailable" }), + ), + ), + ), + }); + yield* Effect.gen(function* () { + const launches = yield* ThreadLaunch.ThreadLaunchService; + const launched = yield* launches.launch( + launchInput({ + command: "prefix-launch", + thread: "prefix-thread", + message: "Fix the parser", + workspace: { type: "worktree", baseRef: "main" }, + }), + ); + const projection = yield* preparationSettled(launched.threadId); + assert.match(projection.thread.branch!, scenario.expected); + yield* Deferred.await(namingAttempted); + assert.equal(projection.runs[0]?.status, "starting"); + assert.equal(harness.createWorktree.mock.calls[0]?.[0]?.newRefName, projection.thread.branch); + assert.equal(harness.renameBranch.mock.calls.length, 0); + assert.isFalse(projection.turnItems.some((item) => item.type === "error")); + }).pipe(Effect.provide(harness.layer)); + }), +); + +it.effect("renames a temporary branch with a configured prefix when generation succeeds", () => { + const harness = makeHarness({ + serverSettings: { branchNamingMode: "static", branchNamePrefix: "my-prefix" }, + generateBranchName: () => Effect.succeed({ branch: "my-prefix/fix-parser" }), + }); + return Effect.gen(function* () { + const launches = yield* ThreadLaunch.ThreadLaunchService; + const threads = yield* ThreadManagement.ThreadManagementService; + const launched = yield* launches.launch( + launchInput({ + command: "prefix-rename", + thread: "prefix-rename-thread", + message: "Fix the parser", + workspace: { type: "worktree", baseRef: "main" }, + }), + ); + yield* threads.streamStoredEventsFrom({ threadId: launched.threadId }).pipe( + Stream.filter((stored) => stored.commandId === "prefix-rename:branch-rename"), + Stream.runHead, + ); + const projection = yield* threads.getThreadProjection(launched.threadId); + assert.equal(projection.thread.branch, "my-prefix/fix-parser"); + assert.match( + harness.renameBranch.mock.calls[0]?.[0]?.oldBranch ?? "", + /^my-prefix\/[0-9a-f]{8}$/, + ); + }).pipe(Effect.provide(harness.layer)); +}); + function scheduledTaskInput(id: string, targetProjectId: ProjectId) { return { id: ScheduledTaskId.make(id), diff --git a/apps/server/src/orchestration-v2/ThreadLaunchService.ts b/apps/server/src/orchestration-v2/ThreadLaunchService.ts index ff032f757bbe..05328ef6e609 100644 --- a/apps/server/src/orchestration-v2/ThreadLaunchService.ts +++ b/apps/server/src/orchestration-v2/ThreadLaunchService.ts @@ -33,8 +33,8 @@ import * as Stream from "effect/Stream"; import { buildTemporaryWorktreeBranchName, flattenTemporaryWorktreeBranchName, + formatGeneratedBranchName, isTemporaryWorktreeBranch, - WORKTREE_BRANCH_PREFIX, } from "@t3tools/shared/git"; import * as GitWorkflow from "../git/GitWorkflowService.ts"; @@ -363,10 +363,23 @@ const make = Effect.gen(function* () { ); const requestedBranch = input.workspaceStrategy.branch; + const generatedTemporaryBranch = + input.workspaceStrategy.type === "worktree" && requestedBranch === undefined; let branch: string | null; - if (input.workspaceStrategy.type === "worktree" && requestedBranch === undefined) { + if (generatedTemporaryBranch) { const uuid = yield* randomUuidV4; branch = buildTemporaryWorktreeBranchName(() => uuid.replaceAll("-", "")); + const settings = resolveProjectSettings( + yield* serverSettings.getSettings, + input.projectId, + ).settings; + if (settings.branchNamingMode === "static") { + branch = formatGeneratedBranchName(branch.slice(-8), { + mode: "static", + prefix: settings.branchNamePrefix, + instructions: "", + }); + } } else { branch = requestedBranch ?? null; } @@ -410,17 +423,23 @@ const make = Effect.gen(function* () { } } if (shouldFetch) yield* setupTracker.stageStatus(threadId, "fetch", "done"); - if ( - branch !== null && - isTemporaryWorktreeBranch(branch) && - (yield* git - .hasCommit({ - cwd: project.workspaceRoot, - refName: `refs/heads/${WORKTREE_BRANCH_PREFIX}`, - }) - .pipe(Effect.mapError(mapError(input, "provision-worktree", threadId)))) - ) { - branch = flattenTemporaryWorktreeBranchName(branch); + if (branch !== null && (generatedTemporaryBranch || isTemporaryWorktreeBranch(branch))) { + const parts = branch.split("/"); + for (let depth = 1; depth < parts.length; depth++) { + if ( + yield* git + .hasCommit({ + cwd: project.workspaceRoot, + refName: `refs/heads/${parts.slice(0, depth).join("/")}`, + }) + .pipe(Effect.mapError(mapError(input, "provision-worktree", threadId))) + ) { + branch = generatedTemporaryBranch + ? branch.replaceAll("/", "-") + : flattenTemporaryWorktreeBranchName(branch); + break; + } + } } yield* setupTracker.stageStatus(threadId, "checkout", "running"); const worktree = yield* git @@ -470,7 +489,7 @@ const make = Effect.gen(function* () { worktreePath !== null && branch !== null && initialMessage !== undefined && - isTemporaryWorktreeBranch(branch) + (generatedTemporaryBranch || isTemporaryWorktreeBranch(branch)) ) { const oldBranch = branch; const worktreeCwd = worktreePath; From c4b9a6035b0b4ee77af4df300d585bd4799f3944 Mon Sep 17 00:00:00 2001 From: akash9676 Date: Tue, 27 Oct 2026 14:30:06 +0100 Subject: [PATCH 2/2] fix(server): keep worktree creation resilient to settings failures --- .../ThreadLaunchService.test.ts | 36 +++++++++++++++++++ .../orchestration-v2/ThreadLaunchService.ts | 10 +++--- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts b/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts index a0c0d88818a2..6475d1da9676 100644 --- a/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts +++ b/apps/server/src/orchestration-v2/ThreadLaunchService.test.ts @@ -26,6 +26,7 @@ import { type ServerProvider, ThreadId, TextGenerationError, + ServerSettingsError, } from "@t3tools/contracts"; import * as Cause from "effect/Cause"; import * as Deferred from "effect/Deferred"; @@ -127,6 +128,7 @@ interface HarnessOptions { readonly generateTitle?: TextGeneration.TextGeneration["Service"]["generateThreadTitle"]; readonly generateBranchName?: TextGeneration.TextGeneration["Service"]["generateBranchName"]; readonly serverSettings?: Parameters[0]; + readonly getLaunchSettings?: ServerSettings.ServerSettingsService["Service"]["getSettings"]; readonly providers?: ReadonlyArray; readonly git?: Partial; } @@ -229,6 +231,11 @@ function makeHarness(options: HarnessOptions = {}) { layerThreadManagement, layerReceipts, IdAllocator.layer, + options.getLaunchSettings === undefined + ? Layer.empty + : Layer.mock(ServerSettings.ServerSettingsService)({ + getSettings: options.getLaunchSettings, + }), ), ), ); @@ -962,6 +969,35 @@ it.effect.each([ }), ); +it.effect( + "starts a worktree with the default temporary name when settings secrets cannot be read", + () => { + const harness = makeHarness({ + getLaunchSettings: Effect.fail( + new ServerSettingsError({ settingsPath: "/settings.json", operation: "read-secret" }), + ), + }); + return Effect.gen(function* () { + const launches = yield* ThreadLaunch.ThreadLaunchService; + const launched = yield* launches.launch( + launchInput({ + command: "settings-fallback", + thread: "settings-fallback-thread", + message: "Fix the parser", + workspace: { type: "worktree", baseRef: "main" }, + }), + ); + const projection = yield* preparationSettled(launched.threadId); + assert.equal(projection.runs[0]?.status, "starting"); + assert.match(projection.thread.branch!, /^t3\/[0-9a-f]{8}$/); + assert.equal(harness.createWorktree.mock.calls[0]?.[0]?.newRefName, projection.thread.branch); + assert.equal(harness.runSetup.mock.calls.length, 1); + assert.equal(harness.renameBranch.mock.calls.length, 0); + assert.isFalse(projection.turnItems.some((item) => item.type === "error")); + }).pipe(Effect.provide(harness.layer)); + }, +); + it.effect("renames a temporary branch with a configured prefix when generation succeeds", () => { const harness = makeHarness({ serverSettings: { branchNamingMode: "static", branchNamePrefix: "my-prefix" }, diff --git a/apps/server/src/orchestration-v2/ThreadLaunchService.ts b/apps/server/src/orchestration-v2/ThreadLaunchService.ts index 05328ef6e609..4f6eba4190e7 100644 --- a/apps/server/src/orchestration-v2/ThreadLaunchService.ts +++ b/apps/server/src/orchestration-v2/ThreadLaunchService.ts @@ -369,11 +369,11 @@ const make = Effect.gen(function* () { if (generatedTemporaryBranch) { const uuid = yield* randomUuidV4; branch = buildTemporaryWorktreeBranchName(() => uuid.replaceAll("-", "")); - const settings = resolveProjectSettings( - yield* serverSettings.getSettings, - input.projectId, - ).settings; - if (settings.branchNamingMode === "static") { + const settings = yield* serverSettings.getSettings.pipe( + Effect.map((settings) => resolveProjectSettings(settings, input.projectId).settings), + Effect.orElseSucceed(() => null), + ); + if (settings?.branchNamingMode === "static") { branch = formatGeneratedBranchName(branch.slice(-8), { mode: "static", prefix: settings.branchNamePrefix,