import { type ChatAttachment, CommandId, EventId, type ModelSelection, type OrchestrationEvent, ProviderKind, type OrchestrationSession, ThreadId, type ProviderSession, type RuntimeMode, type TurnId,} from "@t3tools/contracts";import { isTemporaryWorktreeBranch, WORKTREE_BRANCH_PREFIX } from "@t3tools/shared/git";import { Cache, Cause, Duration, Effect, Equal, Layer, Option, Schema, Stream } from "effect";import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker"; import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts";import { GitCore } from "../../git/Services/GitCore.ts";import { GitStatusBroadcaster } from "../../git/Services/GitStatusBroadcaster.ts";import { increment, orchestrationEventsProcessedTotal } from "../../observability/Metrics.ts";import { ProviderAdapterRequestError } from "../../provider/Errors.ts";import type { ProviderServiceError } from "../../provider/Errors.ts";import { TextGeneration } from "../../git/Services/TextGeneration.ts";import { ProviderService } from "../../provider/Services/ProviderService.ts";import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";import { ProviderCommandReactor, type ProviderCommandReactorShape,} from "../Services/ProviderCommandReactor.ts";import { ServerSettingsService } from "../../serverSettings.ts"; type ProviderIntentEvent = Extract< OrchestrationEvent, { type: | "thread.runtime-mode-set" | "thread.turn-start-requested" | "thread.turn-interrupt-requested" | "thread.approval-response-requested" | "thread.user-input-response-requested" | "thread.session-stop-requested"; }>; function toNonEmptyProviderInput(value: string | undefined): string | undefined { const normalized = value?.trim(); return normalized && normalized.length > 0 ? normalized : undefined;} function mapProviderSessionStatusToOrchestrationStatus( status: "connecting" | "ready" | "running" | "error" | "closed",): OrchestrationSession["status"] { switch (status) { case "connecting": return "starting"; case "running": return "running"; case "error": return "error"; case "closed": return "stopped"; case "ready": default: return "ready"; }} const turnStartKeyForEvent = (event: ProviderIntentEvent): string => event.commandId !== null ? `command:${event.commandId}` : `event:${event.eventId}`; const serverCommandId = (tag: string): CommandId => CommandId.make(`server:${tag}:${crypto.randomUUID()}`); const HANDLED_TURN_START_KEY_MAX = 10_000;const HANDLED_TURN_START_KEY_TTL = Duration.minutes(30);const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access";const DEFAULT_THREAD_TITLE = "New thread"; function canReplaceThreadTitle(currentTitle: string, titleSeed?: string): boolean { const trimmedCurrentTitle = currentTitle.trim(); if (trimmedCurrentTitle === DEFAULT_THREAD_TITLE) { return true; } const trimmedTitleSeed = titleSeed?.trim(); return trimmedTitleSeed !== undefined && trimmedTitleSeed.length > 0 ? trimmedCurrentTitle === trimmedTitleSeed : false;} function findProviderAdapterRequestError( cause: Cause.Cause<ProviderServiceError>,): ProviderAdapterRequestError | undefined { const failReason = cause.reasons.find(Cause.isFailReason); return Schema.is(ProviderAdapterRequestError)(failReason?.error) ? failReason.error : undefined;} function isUnknownPendingApprovalRequestError(cause: Cause.Cause<ProviderServiceError>): boolean { const error = findProviderAdapterRequestError(cause); if (error) { const detail = error.detail.toLowerCase(); return ( detail.includes("unknown pending approval request") || detail.includes("unknown pending permission request") ); } const message = Cause.pretty(cause); return ( message.includes("unknown pending approval request") || message.includes("unknown pending permission request") );} function isUnknownPendingUserInputRequestError(cause: Cause.Cause<ProviderServiceError>): boolean { const error = findProviderAdapterRequestError(cause); if (error) { return error.detail.toLowerCase().includes("unknown pending user-input request"); } return Cause.pretty(cause).toLowerCase().includes("unknown pending user-input request");} function stalePendingRequestDetail( requestKind: "approval" | "user-input", requestId: string,): string { return `Stale pending ${requestKind} request: ${requestId}. Provider callback state does not survive app restarts or recovered sessions. Restart the turn to continue.`;} function buildGeneratedWorktreeBranchName(raw: string): string { const normalized = raw .trim() .toLowerCase() .replace(/^refs\/heads\//, "") .replace(/['"`]/g, ""); const withoutPrefix = normalized.startsWith(`${WORKTREE_BRANCH_PREFIX}/`) ? normalized.slice(`${WORKTREE_BRANCH_PREFIX}/`.length) : normalized; const branchFragment = withoutPrefix .replace(/[^a-z0-9/_-]+/g, "-") .replace(/\/+/g, "/") .replace(/-+/g, "-") .replace(/^[./_-]+|[./_-]+$/g, "") .slice(0, 64) .replace(/[./_-]+$/g, ""); const safeFragment = branchFragment.length > 0 ? branchFragment : "update"; return `${WORKTREE_BRANCH_PREFIX}/${safeFragment}`;} const make = Effect.gen(function* () { const orchestrationEngine = yield* OrchestrationEngineService; const providerService = yield* ProviderService; const git = yield* GitCore; const gitStatusBroadcaster = yield* GitStatusBroadcaster; const textGeneration = yield* TextGeneration; const serverSettingsService = yield* ServerSettingsService; const handledTurnStartKeys = yield* Cache.make<string, true>({ capacity: HANDLED_TURN_START_KEY_MAX, timeToLive: HANDLED_TURN_START_KEY_TTL, lookup: () => Effect.succeed(true), }); const hasHandledTurnStartRecently = (key: string) => Cache.getOption(handledTurnStartKeys, key).pipe( Effect.flatMap((cached) => Cache.set(handledTurnStartKeys, key, true).pipe(Effect.as(Option.isSome(cached))), ), ); const threadModelSelections = new Map<string, ModelSelection>(); const appendProviderFailureActivity = (input: { readonly threadId: ThreadId; readonly kind: | "provider.turn.start.failed" | "provider.turn.interrupt.failed" | "provider.approval.respond.failed" | "provider.user-input.respond.failed" | "provider.session.stop.failed"; readonly summary: string; readonly detail: string; readonly turnId: TurnId | null; readonly createdAt: string; readonly requestId?: string; }) => orchestrationEngine.dispatch({ type: "thread.activity.append", commandId: serverCommandId("provider-failure-activity"), threadId: input.threadId, activity: { id: EventId.make(crypto.randomUUID()), tone: "error", kind: input.kind, summary: input.summary, payload: { detail: input.detail, ...(input.requestId ? { requestId: input.requestId } : {}), }, turnId: input.turnId, createdAt: input.createdAt, }, createdAt: input.createdAt, }); const formatFailureDetail = (cause: Cause.Cause<unknown>): string => { const failReason = cause.reasons.find(Cause.isFailReason); const providerError = Schema.is(ProviderAdapterRequestError)(failReason?.error) ? failReason.error : undefined; if (providerError) { return providerError.detail; } return Cause.pretty(cause); }; const setThreadSession = (input: { readonly threadId: ThreadId; readonly session: OrchestrationSession; readonly createdAt: string; }) => orchestrationEngine.dispatch({ type: "thread.session.set", commandId: serverCommandId("provider-session-set"), threadId: input.threadId, session: input.session, createdAt: input.createdAt, }); const setThreadSessionErrorOnTurnStartFailure = Effect.fnUntraced(function* (input: { readonly threadId: ThreadId; readonly detail: string; readonly createdAt: string; }) { const thread = yield* resolveThread(input.threadId); const session = thread?.session; if (!session) { return; } yield* setThreadSession({ threadId: input.threadId, session: { ...session, status: session.status === "stopped" ? "stopped" : "ready", activeTurnId: null, lastError: input.detail, updatedAt: input.createdAt, }, createdAt: input.createdAt, }); }); const resolveThread = Effect.fnUntraced(function* (threadId: ThreadId) { const readModel = yield* orchestrationEngine.getReadModel(); return readModel.threads.find((entry) => entry.id === threadId); }); const ensureSessionForThread = Effect.fn("ensureSessionForThread")(function* ( threadId: ThreadId, createdAt: string, options?: { readonly modelSelection?: ModelSelection; }, ) { const readModel = yield* orchestrationEngine.getReadModel(); const thread = readModel.threads.find((entry) => entry.id === threadId); if (!thread) { return yield* Effect.die(new Error(`Thread '${threadId}' was not found in read model.`)); } const desiredRuntimeMode = thread.runtimeMode; const currentProvider: ProviderKind | undefined = Schema.is(ProviderKind)( thread.session?.providerName, ) ? thread.session.providerName : undefined; const requestedModelSelection = options?.modelSelection; const threadProvider: ProviderKind = currentProvider ?? thread.modelSelection.provider; if ( requestedModelSelection !== undefined && requestedModelSelection.provider !== threadProvider ) { return yield* new ProviderAdapterRequestError({ provider: threadProvider, method: "thread.turn.start", detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot switch to '${requestedModelSelection.provider}'.`, }); } const preferredProvider: ProviderKind = threadProvider; const desiredModelSelection = requestedModelSelection ?? thread.modelSelection; const effectiveCwd = resolveThreadWorkspaceCwd({ thread, projects: readModel.projects, }); const resolveActiveSession = (threadId: ThreadId) => providerService .listSessions() .pipe(Effect.map((sessions) => sessions.find((session) => session.threadId === threadId))); const startProviderSession = (input?: { readonly resumeCursor?: unknown; readonly provider?: ProviderKind; }) => providerService.startSession(threadId, { threadId, ...(preferredProvider ? { provider: preferredProvider } : {}), ...(effectiveCwd ? { cwd: effectiveCwd } : {}), modelSelection: desiredModelSelection, ...(input?.resumeCursor !== undefined ? { resumeCursor: input.resumeCursor } : {}), runtimeMode: desiredRuntimeMode, }); const bindSessionToThread = (session: ProviderSession) => setThreadSession({ threadId, session: { threadId, status: mapProviderSessionStatusToOrchestrationStatus(session.status), providerName: session.provider, runtimeMode: desiredRuntimeMode, // Provider turn ids are not orchestration turn ids. activeTurnId: null, lastError: session.lastError ?? null, updatedAt: session.updatedAt, }, createdAt, }); const activeSession = yield* resolveActiveSession(threadId); const existingSessionThreadId = thread.session && thread.session.status !== "stopped" && activeSession ? thread.id : null; if (existingSessionThreadId) { const runtimeModeChanged = thread.runtimeMode !== thread.session?.runtimeMode; const cwdChanged = effectiveCwd !== activeSession?.cwd; const sessionModelSwitch = currentProvider === undefined ? "in-session" : (yield* providerService.getCapabilities(currentProvider)).sessionModelSwitch; const modelChanged = requestedModelSelection !== undefined && requestedModelSelection.model !== activeSession?.model; const shouldRestartForModelChange = modelChanged && sessionModelSwitch === "unsupported"; const previousModelSelection = threadModelSelections.get(threadId); const shouldRestartForModelSelectionChange = currentProvider === "claudeAgent" && requestedModelSelection !== undefined && !Equal.equals(previousModelSelection, requestedModelSelection); if ( !runtimeModeChanged && !cwdChanged && !shouldRestartForModelChange && !shouldRestartForModelSelectionChange ) { return existingSessionThreadId; } const resumeCursor = shouldRestartForModelChange ? undefined : (activeSession?.resumeCursor ?? undefined); yield* Effect.logInfo("provider command reactor restarting provider session", { threadId, existingSessionThreadId, currentProvider, desiredProvider: desiredModelSelection.provider, currentRuntimeMode: thread.session?.runtimeMode, desiredRuntimeMode: thread.runtimeMode, runtimeModeChanged, previousCwd: activeSession?.cwd, desiredCwd: effectiveCwd, cwdChanged, modelChanged, shouldRestartForModelChange, shouldRestartForModelSelectionChange, hasResumeCursor: resumeCursor !== undefined, }); const restartedSession = yield* startProviderSession( resumeCursor !== undefined ? { resumeCursor } : undefined, ); yield* Effect.logInfo("provider command reactor restarted provider session", { threadId, previousSessionId: existingSessionThreadId, restartedSessionThreadId: restartedSession.threadId, provider: restartedSession.provider, runtimeMode: restartedSession.runtimeMode, cwd: restartedSession.cwd, }); yield* bindSessionToThread(restartedSession); return restartedSession.threadId; } const startedSession = yield* startProviderSession(undefined); yield* bindSessionToThread(startedSession); return startedSession.threadId; }); const buildSendTurnRequestForThread = Effect.fnUntraced(function* (input: { readonly threadId: ThreadId; readonly messageText: string; readonly attachments?: ReadonlyArray<ChatAttachment>; readonly modelSelection?: ModelSelection; readonly interactionMode?: "default" | "plan"; readonly createdAt: string; }) { const thread = yield* resolveThread(input.threadId); if (!thread) { return yield* Effect.die( new Error(`Thread '${input.threadId}' was not found in read model.`), ); } yield* ensureSessionForThread( input.threadId, input.createdAt, input.modelSelection !== undefined ? { modelSelection: input.modelSelection } : {}, ); if (input.modelSelection !== undefined) { threadModelSelections.set(input.threadId, input.modelSelection); } const normalizedInput = toNonEmptyProviderInput(input.messageText); const normalizedAttachments = input.attachments ?? []; const activeSession = yield* providerService .listSessions() .pipe( Effect.map((sessions) => sessions.find((session) => session.threadId === input.threadId)), ); const sessionModelSwitch = activeSession === undefined ? "in-session" : (yield* providerService.getCapabilities(activeSession.provider)).sessionModelSwitch; const requestedModelSelection = input.modelSelection ?? threadModelSelections.get(input.threadId) ?? thread.modelSelection; const modelForTurn = sessionModelSwitch === "unsupported" && input.modelSelection === undefined ? activeSession?.model !== undefined ? { ...requestedModelSelection, model: activeSession.model, } : requestedModelSelection : input.modelSelection; return { threadId: input.threadId, ...(normalizedInput ? { input: normalizedInput } : {}), ...(normalizedAttachments.length > 0 ? { attachments: normalizedAttachments } : {}), ...(modelForTurn !== undefined ? { modelSelection: modelForTurn } : {}), ...(input.interactionMode !== undefined ? { interactionMode: input.interactionMode } : {}), }; }); const maybeGenerateAndRenameWorktreeBranchForFirstTurn = Effect.fn( "maybeGenerateAndRenameWorktreeBranchForFirstTurn", )(function* (input: { readonly threadId: ThreadId; readonly branch: string | null; readonly worktreePath: string | null; readonly messageText: string; readonly attachments?: ReadonlyArray<ChatAttachment>; }) { if (!input.branch || !input.worktreePath) { return; } if (!isTemporaryWorktreeBranch(input.branch)) { return; } const oldBranch = input.branch; const cwd = input.worktreePath; const attachments = input.attachments ?? []; yield* Effect.gen(function* () { const { textGenerationModelSelection: modelSelection } = yield* serverSettingsService.getSettings; const generated = yield* textGeneration.generateBranchName({ cwd, message: input.messageText, ...(attachments.length > 0 ? { attachments } : {}), modelSelection, }); if (!generated) return; const targetBranch = buildGeneratedWorktreeBranchName(generated.branch); if (targetBranch === oldBranch) return; const renamed = yield* git.renameBranch({ cwd, oldBranch, newBranch: targetBranch }); yield* orchestrationEngine.dispatch({ type: "thread.meta.update", commandId: serverCommandId("worktree-branch-rename"), threadId: input.threadId, branch: renamed.branch, worktreePath: cwd, }); yield* gitStatusBroadcaster.refreshStatus(cwd).pipe(Effect.ignoreCause({ log: true })); }).pipe( Effect.catchCause((cause) => Effect.logWarning("provider command reactor failed to generate or rename worktree branch", { threadId: input.threadId,Lines 1–500 of 895. Load more
import { type ChatAttachment, CommandId, EventId, type ModelSelection, type OrchestrationEvent, ProviderKind, type OrchestrationSession, ThreadId, type ProviderSession, type RuntimeMode, type TurnId, } from "@t3tools/contracts"; import { isTemporaryWorktreeBranch, WORKTREE_BRANCH_PREFIX } from "@t3tools/shared/git"; import { Cache, Cause, Duration, Effect, Equal, Layer, Option, Schema, Stream } from "effect"; import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker"; import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts"; import { GitCore } from "../../git/Services/GitCore.ts"; import { GitStatusBroadcaster } from "../../git/Services/GitStatusBroadcaster.ts"; import { increment, orchestrationEventsProcessedTotal } from "../../observability/Metrics.ts"; import { ProviderAdapterRequestError } from "../../provider/Errors.ts"; import type { ProviderServiceError } from "../../provider/Errors.ts"; import { TextGeneration } from "../../git/Services/TextGeneration.ts"; import { ProviderService } from "../../provider/Services/ProviderService.ts"; import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts"; import { ProviderCommandReactor, type ProviderCommandReactorShape, } from "../Services/ProviderCommandReactor.ts"; import { ServerSettingsService } from "../../serverSettings.ts"; type ProviderIntentEvent = Extract< OrchestrationEvent, { type: | "thread.runtime-mode-set" | "thread.turn-start-requested" | "thread.turn-interrupt-requested" | "thread.approval-response-requested" | "thread.user-input-response-requested" | "thread.session-stop-requested"; } >; function toNonEmptyProviderInput(value: string | undefined): string | undefined { const normalized = value?.trim(); return normalized && normalized.length > 0 ? normalized : undefined; } function mapProviderSessionStatusToOrchestrationStatus( status: "connecting" | "ready" | "running" | "error" | "closed", ): OrchestrationSession["status"] { switch (status) { case "connecting": return "starting"; case "running": return "running"; case "error": return "error"; case "closed": return "stopped"; case "ready": default: return "ready"; } } const turnStartKeyForEvent = (event: ProviderIntentEvent): string => event.commandId !== null ? `command:${event.commandId}` : `event:${event.eventId}`; const serverCommandId = (tag: string): CommandId => CommandId.make(`server:${tag}:${crypto.randomUUID()}`); const HANDLED_TURN_START_KEY_MAX = 10_000; const HANDLED_TURN_START_KEY_TTL = Duration.minutes(30); const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; const DEFAULT_THREAD_TITLE = "New thread"; function canReplaceThreadTitle(currentTitle: string, titleSeed?: string): boolean { const trimmedCurrentTitle = currentTitle.trim(); if (trimmedCurrentTitle === DEFAULT_THREAD_TITLE) { return true; } const trimmedTitleSeed = titleSeed?.trim(); return trimmedTitleSeed !== undefined && trimmedTitleSeed.length > 0 ? trimmedCurrentTitle === trimmedTitleSeed : false; } function findProviderAdapterRequestError( cause: Cause.Cause<ProviderServiceError>, ): ProviderAdapterRequestError | undefined { const failReason = cause.reasons.find(Cause.isFailReason); return Schema.is(ProviderAdapterRequestError)(failReason?.error) ? failReason.error : undefined; } function isUnknownPendingApprovalRequestError(cause: Cause.Cause<ProviderServiceError>): boolean { const error = findProviderAdapterRequestError(cause); if (error) { const detail = error.detail.toLowerCase(); return ( detail.includes("unknown pending approval request") || detail.includes("unknown pending permission request") ); } const message = Cause.pretty(cause); return ( message.includes("unknown pending approval request") || message.includes("unknown pending permission request") ); } function isUnknownPendingUserInputRequestError(cause: Cause.Cause<ProviderServiceError>): boolean { const error = findProviderAdapterRequestError(cause); if (error) { return error.detail.toLowerCase().includes("unknown pending user-input request"); } return Cause.pretty(cause).toLowerCase().includes("unknown pending user-input request"); } function stalePendingRequestDetail( requestKind: "approval" | "user-input", requestId: string, ): string { return `Stale pending ${requestKind} request: ${requestId}. Provider callback state does not survive app restarts or recovered sessions. Restart the turn to continue.`; } function buildGeneratedWorktreeBranchName(raw: string): string { const normalized = raw .trim() .toLowerCase() .replace(/^refs\/heads\//, "") .replace(/['"`]/g, ""); const withoutPrefix = normalized.startsWith(`${WORKTREE_BRANCH_PREFIX}/`) ? normalized.slice(`${WORKTREE_BRANCH_PREFIX}/`.length) : normalized; const branchFragment = withoutPrefix .replace(/[^a-z0-9/_-]+/g, "-") .replace(/\/+/g, "/") .replace(/-+/g, "-") .replace(/^[./_-]+|[./_-]+$/g, "") .slice(0, 64) .replace(/[./_-]+$/g, ""); const safeFragment = branchFragment.length > 0 ? branchFragment : "update"; return `${WORKTREE_BRANCH_PREFIX}/${safeFragment}`; } const make = Effect.gen(function* () { const orchestrationEngine = yield* OrchestrationEngineService; const providerService = yield* ProviderService; const git = yield* GitCore; const gitStatusBroadcaster = yield* GitStatusBroadcaster; const textGeneration = yield* TextGeneration; const serverSettingsService = yield* ServerSettingsService; const handledTurnStartKeys = yield* Cache.make<string, true>({ capacity: HANDLED_TURN_START_KEY_MAX, timeToLive: HANDLED_TURN_START_KEY_TTL, lookup: () => Effect.succeed(true), }); const hasHandledTurnStartRecently = (key: string) => Cache.getOption(handledTurnStartKeys, key).pipe( Effect.flatMap((cached) => Cache.set(handledTurnStartKeys, key, true).pipe(Effect.as(Option.isSome(cached))), ), ); const threadModelSelections = new Map<string, ModelSelection>(); const appendProviderFailureActivity = (input: { readonly threadId: ThreadId; readonly kind: | "provider.turn.start.failed" | "provider.turn.interrupt.failed" | "provider.approval.respond.failed" | "provider.user-input.respond.failed" | "provider.session.stop.failed"; readonly summary: string; readonly detail: string; readonly turnId: TurnId | null; readonly createdAt: string; readonly requestId?: string; }) => orchestrationEngine.dispatch({ type: "thread.activity.append", commandId: serverCommandId("provider-failure-activity"), threadId: input.threadId, activity: { id: EventId.make(crypto.randomUUID()), tone: "error", kind: input.kind, summary: input.summary, payload: { detail: input.detail, ...(input.requestId ? { requestId: input.requestId } : {}), }, turnId: input.turnId, createdAt: input.createdAt, }, createdAt: input.createdAt, }); const formatFailureDetail = (cause: Cause.Cause<unknown>): string => { const failReason = cause.reasons.find(Cause.isFailReason); const providerError = Schema.is(ProviderAdapterRequestError)(failReason?.error) ? failReason.error : undefined; if (providerError) { return providerError.detail; } return Cause.pretty(cause); }; const setThreadSession = (input: { readonly threadId: ThreadId; readonly session: OrchestrationSession; readonly createdAt: string; }) => orchestrationEngine.dispatch({ type: "thread.session.set", commandId: serverCommandId("provider-session-set"), threadId: input.threadId, session: input.session, createdAt: input.createdAt, }); const setThreadSessionErrorOnTurnStartFailure = Effect.fnUntraced(function* (input: { readonly threadId: ThreadId; readonly detail: string; readonly createdAt: string; }) { const thread = yield* resolveThread(input.threadId); const session = thread?.session; if (!session) { return; } yield* setThreadSession({ threadId: input.threadId, session: { ...session, status: session.status === "stopped" ? "stopped" : "ready", activeTurnId: null, lastError: input.detail, updatedAt: input.createdAt, }, createdAt: input.createdAt, }); }); const resolveThread = Effect.fnUntraced(function* (threadId: ThreadId) { const readModel = yield* orchestrationEngine.getReadModel(); return readModel.threads.find((entry) => entry.id === threadId); }); const ensureSessionForThread = Effect.fn("ensureSessionForThread")(function* ( threadId: ThreadId, createdAt: string, options?: { readonly modelSelection?: ModelSelection; }, ) { const readModel = yield* orchestrationEngine.getReadModel(); const thread = readModel.threads.find((entry) => entry.id === threadId); if (!thread) { return yield* Effect.die(new Error(`Thread '${threadId}' was not found in read model.`)); } const desiredRuntimeMode = thread.runtimeMode; const currentProvider: ProviderKind | undefined = Schema.is(ProviderKind)( thread.session?.providerName, ) ? thread.session.providerName : undefined; const requestedModelSelection = options?.modelSelection; const threadProvider: ProviderKind = currentProvider ?? thread.modelSelection.provider; if ( requestedModelSelection !== undefined && requestedModelSelection.provider !== threadProvider ) { return yield* new ProviderAdapterRequestError({ provider: threadProvider, method: "thread.turn.start", detail: `Thread '${threadId}' is bound to provider '${threadProvider}' and cannot switch to '${requestedModelSelection.provider}'.`, }); } const preferredProvider: ProviderKind = threadProvider; const desiredModelSelection = requestedModelSelection ?? thread.modelSelection; const effectiveCwd = resolveThreadWorkspaceCwd({ thread, projects: readModel.projects, }); const resolveActiveSession = (threadId: ThreadId) => providerService .listSessions() .pipe(Effect.map((sessions) => sessions.find((session) => session.threadId === threadId))); const startProviderSession = (input?: { readonly resumeCursor?: unknown; readonly provider?: ProviderKind; }) => providerService.startSession(threadId, { threadId, ...(preferredProvider ? { provider: preferredProvider } : {}), ...(effectiveCwd ? { cwd: effectiveCwd } : {}), modelSelection: desiredModelSelection, ...(input?.resumeCursor !== undefined ? { resumeCursor: input.resumeCursor } : {}), runtimeMode: desiredRuntimeMode, }); const bindSessionToThread = (session: ProviderSession) => setThreadSession({ threadId, session: { threadId, status: mapProviderSessionStatusToOrchestrationStatus(session.status), providerName: session.provider, runtimeMode: desiredRuntimeMode, // Provider turn ids are not orchestration turn ids. activeTurnId: null, lastError: session.lastError ?? null, updatedAt: session.updatedAt, }, createdAt, }); const activeSession = yield* resolveActiveSession(threadId); const existingSessionThreadId = thread.session && thread.session.status !== "stopped" && activeSession ? thread.id : null; if (existingSessionThreadId) { const runtimeModeChanged = thread.runtimeMode !== thread.session?.runtimeMode; const cwdChanged = effectiveCwd !== activeSession?.cwd; const sessionModelSwitch = currentProvider === undefined ? "in-session" : (yield* providerService.getCapabilities(currentProvider)).sessionModelSwitch; const modelChanged = requestedModelSelection !== undefined && requestedModelSelection.model !== activeSession?.model; const shouldRestartForModelChange = modelChanged && sessionModelSwitch === "unsupported"; const previousModelSelection = threadModelSelections.get(threadId); const shouldRestartForModelSelectionChange = currentProvider === "claudeAgent" && requestedModelSelection !== undefined && !Equal.equals(previousModelSelection, requestedModelSelection); if ( !runtimeModeChanged && !cwdChanged && !shouldRestartForModelChange && !shouldRestartForModelSelectionChange ) { return existingSessionThreadId; } const resumeCursor = shouldRestartForModelChange ? undefined : (activeSession?.resumeCursor ?? undefined); yield* Effect.logInfo("provider command reactor restarting provider session", { threadId, existingSessionThreadId, currentProvider, desiredProvider: desiredModelSelection.provider, currentRuntimeMode: thread.session?.runtimeMode, desiredRuntimeMode: thread.runtimeMode, runtimeModeChanged, previousCwd: activeSession?.cwd, desiredCwd: effectiveCwd, cwdChanged, modelChanged, shouldRestartForModelChange, shouldRestartForModelSelectionChange, hasResumeCursor: resumeCursor !== undefined, }); const restartedSession = yield* startProviderSession( resumeCursor !== undefined ? { resumeCursor } : undefined, ); yield* Effect.logInfo("provider command reactor restarted provider session", { threadId, previousSessionId: existingSessionThreadId, restartedSessionThreadId: restartedSession.threadId, provider: restartedSession.provider, runtimeMode: restartedSession.runtimeMode, cwd: restartedSession.cwd, }); yield* bindSessionToThread(restartedSession); return restartedSession.threadId; } const startedSession = yield* startProviderSession(undefined); yield* bindSessionToThread(startedSession); return startedSession.threadId; }); const buildSendTurnRequestForThread = Effect.fnUntraced(function* (input: { readonly threadId: ThreadId; readonly messageText: string; readonly attachments?: ReadonlyArray<ChatAttachment>; readonly modelSelection?: ModelSelection; readonly interactionMode?: "default" | "plan"; readonly createdAt: string; }) { const thread = yield* resolveThread(input.threadId); if (!thread) { return yield* Effect.die( new Error(`Thread '${input.threadId}' was not found in read model.`), ); } yield* ensureSessionForThread( input.threadId, input.createdAt, input.modelSelection !== undefined ? { modelSelection: input.modelSelection } : {}, ); if (input.modelSelection !== undefined) { threadModelSelections.set(input.threadId, input.modelSelection); } const normalizedInput = toNonEmptyProviderInput(input.messageText); const normalizedAttachments = input.attachments ?? []; const activeSession = yield* providerService .listSessions() .pipe( Effect.map((sessions) => sessions.find((session) => session.threadId === input.threadId)), ); const sessionModelSwitch = activeSession === undefined ? "in-session" : (yield* providerService.getCapabilities(activeSession.provider)).sessionModelSwitch; const requestedModelSelection = input.modelSelection ?? threadModelSelections.get(input.threadId) ?? thread.modelSelection; const modelForTurn = sessionModelSwitch === "unsupported" && input.modelSelection === undefined ? activeSession?.model !== undefined ? { ...requestedModelSelection, model: activeSession.model, } : requestedModelSelection : input.modelSelection; return { threadId: input.threadId, ...(normalizedInput ? { input: normalizedInput } : {}), ...(normalizedAttachments.length > 0 ? { attachments: normalizedAttachments } : {}), ...(modelForTurn !== undefined ? { modelSelection: modelForTurn } : {}), ...(input.interactionMode !== undefined ? { interactionMode: input.interactionMode } : {}), }; }); const maybeGenerateAndRenameWorktreeBranchForFirstTurn = Effect.fn( "maybeGenerateAndRenameWorktreeBranchForFirstTurn", )(function* (input: { readonly threadId: ThreadId; readonly branch: string | null; readonly worktreePath: string | null; readonly messageText: string; readonly attachments?: ReadonlyArray<ChatAttachment>; }) { if (!input.branch || !input.worktreePath) { return; } if (!isTemporaryWorktreeBranch(input.branch)) { return; } const oldBranch = input.branch; const cwd = input.worktreePath; const attachments = input.attachments ?? []; yield* Effect.gen(function* () { const { textGenerationModelSelection: modelSelection } = yield* serverSettingsService.getSettings; const generated = yield* textGeneration.generateBranchName({ cwd, message: input.messageText, ...(attachments.length > 0 ? { attachments } : {}), modelSelection, }); if (!generated) return; const targetBranch = buildGeneratedWorktreeBranchName(generated.branch); if (targetBranch === oldBranch) return; const renamed = yield* git.renameBranch({ cwd, oldBranch, newBranch: targetBranch }); yield* orchestrationEngine.dispatch({ type: "thread.meta.update", commandId: serverCommandId("worktree-branch-rename"), threadId: input.threadId, branch: renamed.branch, worktreePath: cwd, }); yield* gitStatusBroadcaster.refreshStatus(cwd).pipe(Effect.ignoreCause({ log: true })); }).pipe( Effect.catchCause((cause) => Effect.logWarning("provider command reactor failed to generate or rename worktree branch", { threadId: input.threadId,