895 lines · 31.5 KB · TypeScript
RawDownload
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.