1564 lines · 56.2 KB · TypeScript
RawDownload
import {  type ChatAttachment,  CommandId,  EventId,  type ModelSelection,  type OrchestrationEvent,  ProviderDriverKind,  type ProjectId,  type OrchestrationSession,  ThreadId,  type ProviderSession,  type RuntimeMode,  type TurnId,} from "@t3tools/contracts";import { isTemporaryWorktreeBranch, WORKTREE_BRANCH_PREFIX } from "@t3tools/shared/git";import * as Cache from "effect/Cache";import * as Cause from "effect/Cause";import * as Crypto from "effect/Crypto";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as Equal from "effect/Equal";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Schema from "effect/Schema";import * as Stream from "effect/Stream";import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker"; import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts";import { increment, orchestrationEventsProcessedTotal } from "../../observability/Metrics.ts";import { ProviderAdapterRequestError } from "../../provider/Errors.ts";import type { ProviderServiceError } from "../../provider/Errors.ts";import { TextGeneration } from "../../textGeneration/TextGeneration.ts";import { ProviderService } from "../../provider/Services/ProviderService.ts";import { ProviderRegistry } from "../../provider/Services/ProviderRegistry.ts";import { OrchestrationEngineService } from "../Services/OrchestrationEngine.ts";import { ProjectionSnapshotQuery } from "../Services/ProjectionSnapshotQuery.ts";import {  ProviderCommandReactor,  type ProviderCommandReactorShape,} from "../Services/ProviderCommandReactor.ts";import { forkParked, ServerActivation } from "../../serverActivation.ts";import { canReplaceThreadTitle, DEFAULT_THREAD_TITLE } from "../threadTitles.ts";import {  resolveSourceControlWriterModelSelection,  ServerSettingsService,} from "../../serverSettings.ts";import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts";import { GitWorkflowService } from "../../git/GitWorkflowService.ts";const isProviderAdapterRequestError = Schema.is(ProviderAdapterRequestError);const isProviderDriverKind = Schema.is(ProviderDriverKind); type ProviderIntentEvent = Extract<  OrchestrationEvent,  {    type:      | "thread.meta-updated"      | "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 HANDLED_TURN_START_KEY_MAX = 10_000;const HANDLED_TURN_START_KEY_TTL = Duration.minutes(30);const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access";const MAX_REGENERATION_ATTACHMENTS = 4;const MAX_THREAD_TITLE_CONTEXT_CHARS = 8_000;const MAX_FIRST_USER_TITLE_CONTEXT_CHARS = 2_000;const THREAD_TITLE_CONTEXT_TRUNCATION_MARKER = "[Earlier content truncated]\n\n";const FIRST_USER_CONTEXT_TRUNCATION_MARKER = "\n[First user message truncated]"; type ThreadTitleMessage = {  readonly role: "user" | "assistant" | "system";  readonly text: string;  readonly attachments?: ReadonlyArray<ChatAttachment> | undefined;}; function formatThreadTitleSection(message: ThreadTitleMessage): string | undefined {  if (message.role === "system") {    return undefined;  }  const text = message.text.trim();  const attachmentSummary = (message.attachments ?? [])    .map((attachment) => attachment.name)    .join(", ");  const contents = [    ...(text.length > 0 ? [text] : []),    ...(attachmentSummary.length > 0 ? [`[Attachments: ${attachmentSummary}]`] : []),  ].join("\n");  return contents.length > 0 ? `${message.role.toUpperCase()}:\n${contents}` : undefined;} function limitFirstUserSection(section: string): string {  if (section.length <= MAX_FIRST_USER_TITLE_CONTEXT_CHARS) {    return section;  }  return `${section.slice(    0,    MAX_FIRST_USER_TITLE_CONTEXT_CHARS - FIRST_USER_CONTEXT_TRUNCATION_MARKER.length,  )}${FIRST_USER_CONTEXT_TRUNCATION_MARKER}`;} function collectRecentThreadTitleContext(  messages: ReadonlyArray<ThreadTitleMessage>,  maxChars: number,): {  readonly context: string;  readonly attachments: ReadonlyArray<ChatAttachment>;  readonly truncated: boolean;} {  let context = "";  let truncated = false;  const retainedAttachments: Array<ChatAttachment> = [];   for (const message of messages.toReversed()) {    const section = formatThreadTitleSection(message);    if (section === undefined) {      continue;    }     const separator = context.length > 0 ? "\n\n" : "";    const available = maxChars - context.length - separator.length;    if (section.length > available) {      if (available > 0) {        context = `${section.slice(-available)}${separator}${context}`;        retainedAttachments.unshift(...(message.attachments ?? []));      }      truncated = true;      break;    }    context = `${section}${separator}${context}`;    retainedAttachments.unshift(...(message.attachments ?? []));  }   return { context, attachments: retainedAttachments, truncated };} function formatThreadTitleContext(messages: ReadonlyArray<ThreadTitleMessage>): {  readonly message: string;  readonly attachments: ReadonlyArray<ChatAttachment>;} {  const recent = collectRecentThreadTitleContext(messages, MAX_THREAD_TITLE_CONTEXT_CHARS);  if (!recent.truncated) {    return {      message: recent.context,      attachments: recent.attachments.slice(-MAX_REGENERATION_ATTACHMENTS),    };  }   const firstUserMessage = messages.find(    (message) => message.role === "user" && formatThreadTitleSection(message),  );  const firstUserSection = firstUserMessage    ? formatThreadTitleSection(firstUserMessage)    : undefined;  if (!firstUserMessage || !firstUserSection) {    return {      message: `${THREAD_TITLE_CONTEXT_TRUNCATION_MARKER}${recent.context}`,      attachments: recent.attachments.slice(-MAX_REGENERATION_ATTACHMENTS),    };  }   const pinnedSection = limitFirstUserSection(firstUserSection);  const recentContextBudget =    MAX_THREAD_TITLE_CONTEXT_CHARS -    pinnedSection.length -    "\n\n".length -    THREAD_TITLE_CONTEXT_TRUNCATION_MARKER.length;  const retainedRecent = collectRecentThreadTitleContext(messages, recentContextBudget);  const pinnedAttachment = firstUserMessage.attachments?.[0];  const recentAttachments = retainedRecent.attachments.filter(    (attachment) => attachment.id !== pinnedAttachment?.id,  );   return {    message: `${pinnedSection}\n\n${THREAD_TITLE_CONTEXT_TRUNCATION_MARKER}${retainedRecent.context}`,    attachments: [      ...(pinnedAttachment ? [pinnedAttachment] : []),      ...recentAttachments.slice(        -(MAX_REGENERATION_ATTACHMENTS - (pinnedAttachment === undefined ? 0 : 1)),      ),    ],  };} export function providerErrorLabel(value: string | undefined): string {  const normalized = value?.trim();  return normalized && normalized.length > 0 ? normalized : "unknown";} export function providerErrorLabelFromInstanceHint(input: {  readonly instanceId?: string | undefined;  readonly modelSelectionInstanceId?: string | undefined;  readonly sessionProvider?: string | undefined;}): string {  return providerErrorLabel(    input.instanceId ?? input.modelSelectionInstanceId ?? input.sessionProvider,  );} function findProviderAdapterRequestError(  cause: Cause.Cause<ProviderServiceError>,): ProviderAdapterRequestError | undefined {  const failReason = cause.reasons.find(Cause.isFailReason);  return isProviderAdapterRequestError(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) {    const detail = error.detail.toLowerCase();    return (      detail.includes("unknown pending user-input request") ||      detail.includes("unknown pending user input request") ||      detail.includes("unknown pending codex user input request")    );  }  const message = Cause.pretty(cause).toLowerCase();  return (    message.includes("unknown pending user-input request") ||    message.includes("unknown pending user input request") ||    message.includes("unknown pending codex 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 crypto = yield* Crypto.Crypto;  const orchestrationEngine = yield* OrchestrationEngineService;  const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;  const providerService = yield* ProviderService;  const providerRegistry = yield* ProviderRegistry;  const gitWorkflow = yield* GitWorkflowService;  const fileSystem = yield* FileSystem.FileSystem;  const vcsStatusBroadcaster = yield* VcsStatusBroadcaster;  const textGeneration = yield* TextGeneration;  const serverSettingsService = yield* ServerSettingsService;  const serverCommandId = (tag: string) =>    crypto.randomUUIDv4.pipe(Effect.map((uuid) => CommandId.make(`server:${tag}:${uuid}`)));  const serverEventId = () => crypto.randomUUIDv4.pipe(Effect.map(EventId.make));  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;  }) =>    Effect.all({      commandId: serverCommandId("provider-failure-activity"),      eventId: serverEventId(),    }).pipe(      Effect.flatMap(({ commandId, eventId }) =>        orchestrationEngine.dispatch({          type: "thread.activity.append",          commandId,          threadId: input.threadId,          activity: {            id: eventId,            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 = isProviderAdapterRequestError(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;  }) =>    serverCommandId("provider-session-set").pipe(      Effect.flatMap((commandId) =>        orchestrationEngine.dispatch({          type: "thread.session.set",          commandId,          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);    if (!thread) {      return;    }    const session = thread.session;    yield* setThreadSession({      threadId: input.threadId,      session: {        ...(session ?? {          threadId: input.threadId,          providerName: null,          providerInstanceId: thread.modelSelection.instanceId,          runtimeMode: thread.runtimeMode,        }),        status: session?.status === "stopped" ? "stopped" : "error",        activeTurnId: null,        lastError: input.detail,        updatedAt: input.createdAt,      },      createdAt: input.createdAt,    });  });   const resolveProject = Effect.fnUntraced(function* (projectId: ProjectId) {    return yield* projectionSnapshotQuery      .getProjectShellById(projectId)      .pipe(Effect.map(Option.getOrUndefined));  });   /**   * Recreates a thread's worktree from its branch when the directory has   * disappeared. Provider sessions resume into the persisted cwd, so a missing   * worktree makes every later turn fail as a bogus "session not found".   * Best-effort: on failure the turn proceeds and reports the real error.   */  const ensureThreadWorktree = Effect.fnUntraced(function* (thread: {    readonly id: ThreadId;    readonly projectId: ProjectId;    readonly branch: string | null;    readonly worktreePath: string | null;  }) {    const { worktreePath, branch } = thread;    if (!worktreePath || !branch) {      return;    }    const exists = yield* fileSystem.exists(worktreePath).pipe(Effect.orElseSucceed(() => true));    if (exists) {      return;    }    const project = yield* resolveProject(thread.projectId);    if (!project) {      return;    }    const cwd = project.workspaceRoot;    yield* Effect.logWarning("provider command reactor recreating missing worktree", {      threadId: thread.id,      worktreePath,      branch,    });    // A directory deleted without `git worktree remove` leaves an admin entry    // that makes `git worktree add` refuse the path; prune clears it.    yield* gitWorkflow.pruneWorktrees({ cwd }).pipe(      Effect.andThen(gitWorkflow.createWorktree({ cwd, refName: branch, path: worktreePath })),      Effect.catchCause((cause) =>        Cause.hasInterruptsOnly(cause)          ? Effect.failCause(cause)          : Effect.logWarning("provider command reactor failed to recreate worktree", {              threadId: thread.id,              worktreePath,              cause: Cause.pretty(cause),            }),      ),    );  });   const resolveThread = Effect.fnUntraced(function* (threadId: ThreadId) {    return yield* projectionSnapshotQuery      .getThreadDetailById(threadId)      .pipe(Effect.map(Option.getOrUndefined));  });   const rejectStartedThreadModelChangeIfRequired = Effect.fnUntraced(function* (input: {    readonly threadId: ThreadId;    readonly currentModelSelection: ModelSelection;    readonly requestedModelSelection: ModelSelection | undefined;  }) {    const requestedModelSelection = input.requestedModelSelection;    if (      requestedModelSelection === undefined ||      (input.currentModelSelection.instanceId === requestedModelSelection.instanceId &&        input.currentModelSelection.model === requestedModelSelection.model)    ) {      return;    }    const providers = yield* providerRegistry.getProviders;    const requiresNewThread =      providers.find((snapshot) => snapshot.instanceId === input.currentModelSelection.instanceId)

Lines 1–500 of 1564.