595 lines · 20.8 KB · TypeScript
RawDownload
import { pipe } from "effect/Function";import * as Arr from "effect/Array";import * as O from "effect/Order";import type {  MessageId,  OrchestrationCheckpointSummary,  OrchestrationEvent,  OrchestrationLatestTurn,  OrchestrationMessage,  OrchestrationSession,  OrchestrationThread,  OrchestrationThreadActivity,  TurnId,} from "@t3tools/contracts"; export type ThreadDetailReducerResult =  | { readonly kind: "updated"; readonly thread: OrchestrationThread }  | { readonly kind: "deleted" }  | { readonly kind: "unchanged" }; const proposedPlanOrder = O.combine<OrchestrationThread["proposedPlans"][number]>(  O.mapInput(O.String, (p) => p.createdAt),  O.mapInput(O.String, (p) => p.id),); const checkpointOrder = O.mapInput(  O.Number,  (cp: OrchestrationThread["checkpoints"][number]) =>    cp.checkpointTurnCount ?? Number.MAX_SAFE_INTEGER,); const activityOrder = O.combineAll<OrchestrationThreadActivity>([  O.mapInput(O.Number, (a) => a.sequence ?? Number.MAX_SAFE_INTEGER),  O.mapInput(O.String, (a) => a.createdAt),  O.mapInput(O.String, (a) => a.id),]); /** * Apply a single orchestration event to an `OrchestrationThread`, returning * the updated thread, a deletion signal, or an "unchanged" marker when the * event doesn't affect this thread. * * This is a pure reducer operating on contract types. UI-specific mapping * (e.g. resolving attachment preview URLs, normalising model slugs, adding * scoped fields like `environmentId`) is the caller's responsibility. */export function applyThreadDetailEvent(  thread: OrchestrationThread,  event: OrchestrationEvent,): ThreadDetailReducerResult {  switch (event.type) {    // ── Project events (irrelevant to thread detail) ────────────────    case "project.created":    case "project.meta-updated":    case "project.deleted":      return { kind: "unchanged" };     // ── Thread lifecycle ────────────────────────────────────────────    case "thread.created":      return {        kind: "updated",        thread: {          id: event.payload.threadId,          projectId: event.payload.projectId,          title: event.payload.title,          modelSelection: event.payload.modelSelection,          runtimeMode: event.payload.runtimeMode,          interactionMode: event.payload.interactionMode,          branch: event.payload.branch,          worktreePath: event.payload.worktreePath,          latestTurn: null,          createdAt: event.payload.createdAt,          updatedAt: event.payload.updatedAt,          archivedAt: null,          settledOverride: null,          settledAt: null,          snoozedUntil: null,          snoozedAt: null,          deletedAt: null,          messages: [],          proposedPlans: [],          activities: [],          checkpoints: [],          session: null,        },      };     case "thread.deleted":      return { kind: "deleted" };     case "thread.archived":      return {        kind: "updated",        thread: {          ...thread,          archivedAt: event.payload.archivedAt,          updatedAt: event.payload.updatedAt,        },      };     case "thread.unarchived":      return {        kind: "updated",        thread: { ...thread, archivedAt: null, updatedAt: event.payload.updatedAt },      };     case "thread.settled":      return {        kind: "updated",        thread: {          ...thread,          settledOverride: "settled",          settledAt: event.payload.settledAt,          updatedAt: event.payload.updatedAt,        },      };     case "thread.unsettled":      return {        kind: "updated",        thread: {          ...thread,          settledOverride: event.payload.reason === "user" ? "active" : null,          settledAt: null,          updatedAt: event.payload.updatedAt,        },      };     case "thread.snoozed":      return {        kind: "updated",        thread: {          ...thread,          snoozedUntil: event.payload.snoozedUntil,          snoozedAt: event.payload.snoozedAt,          updatedAt: event.payload.updatedAt,        },      };     case "thread.unsnoozed":      return {        kind: "updated",        thread: {          ...thread,          snoozedUntil: null,          snoozedAt: null,          updatedAt: event.payload.updatedAt,        },      };     // ── Thread metadata ─────────────────────────────────────────────    case "thread.meta-updated":      return {        kind: "updated",        thread: {          ...thread,          ...(event.payload.title !== undefined ? { title: event.payload.title } : {}),          ...(event.payload.modelSelection !== undefined            ? { modelSelection: event.payload.modelSelection }            : {}),          ...(event.payload.branch !== undefined ? { branch: event.payload.branch } : {}),          ...(event.payload.worktreePath !== undefined            ? { worktreePath: event.payload.worktreePath }            : {}),          updatedAt: event.payload.updatedAt,        },      };     case "thread.runtime-mode-set":      return {        kind: "updated",        thread: {          ...thread,          runtimeMode: event.payload.runtimeMode,          updatedAt: event.payload.updatedAt,        },      };     case "thread.interaction-mode-set":      return {        kind: "updated",        thread: {          ...thread,          interactionMode: event.payload.interactionMode,          updatedAt: event.payload.updatedAt,        },      };     // ── Turn lifecycle ──────────────────────────────────────────────    case "thread.turn-start-requested":      return {        kind: "updated",        thread: {          ...thread,          ...(event.payload.modelSelection !== undefined            ? { modelSelection: event.payload.modelSelection }            : {}),          runtimeMode: event.payload.runtimeMode,          interactionMode: event.payload.interactionMode,          updatedAt: event.occurredAt,        },      };     case "thread.turn-interrupt-requested": {      if (event.payload.turnId === undefined) {        return { kind: "unchanged" };      }      const latestTurn = thread.latestTurn;      if (latestTurn === null || latestTurn.turnId !== event.payload.turnId) {        return { kind: "unchanged" };      }      return {        kind: "updated",        thread: {          ...thread,          latestTurn: {            ...latestTurn,            state: "interrupted",            startedAt: latestTurn.startedAt ?? event.payload.createdAt,            completedAt: latestTurn.completedAt ?? event.payload.createdAt,          },          updatedAt: event.occurredAt,        },      };    }     // ── Messages ────────────────────────────────────────────────────    case "thread.message-sent": {      const message: OrchestrationMessage = {        id: event.payload.messageId,        role: event.payload.role,        text: event.payload.text,        ...(event.payload.attachments !== undefined          ? { attachments: event.payload.attachments }          : {}),        turnId: event.payload.turnId,        streaming: event.payload.streaming,        createdAt: event.payload.createdAt,        updatedAt: event.payload.updatedAt,      };       const existingMessage = thread.messages.find((entry) => entry.id === message.id);      const messages = existingMessage        ? Arr.map(thread.messages, (entry) =>            entry.id !== message.id              ? entry              : {                  ...entry,                  text: message.streaming                    ? `${entry.text}${message.text}`                    : message.text.length > 0                      ? message.text                      : entry.text,                  streaming: message.streaming,                  ...(message.turnId !== undefined ? { turnId: message.turnId } : {}),                  ...(message.streaming ? {} : { updatedAt: message.updatedAt }),                  ...(message.attachments !== undefined                    ? { attachments: message.attachments }                    : {}),                },          )        : Arr.append(thread.messages, message);      // Update latestTurn for assistant messages bound to a turn. A completed      // assistant message only settles the turn once the session is no longer      // running it — providers may emit several assistant messages per turn      // (commentary between tool calls), and the turn must stay unsettled      // until the provider reports turn end.      const turnStillRunning =        event.payload.turnId !== null &&        thread.session?.status === "running" &&        thread.session.activeTurnId === event.payload.turnId;      const settlesTurn = !event.payload.streaming && !turnStillRunning;      const latestTurn: OrchestrationThread["latestTurn"] =        event.payload.role === "assistant" &&        event.payload.turnId !== null &&        (thread.latestTurn === null || thread.latestTurn.turnId === event.payload.turnId)          ? {              turnId: event.payload.turnId,              state: settlesTurn                ? thread.latestTurn?.state === "interrupted"                  ? "interrupted"                  : thread.latestTurn?.state === "error"                    ? "error"                    : "completed"                : "running",              requestedAt:                thread.latestTurn?.turnId === event.payload.turnId                  ? thread.latestTurn.requestedAt                  : event.payload.createdAt,              startedAt:                thread.latestTurn?.turnId === event.payload.turnId                  ? (thread.latestTurn.startedAt ?? event.payload.createdAt)                  : event.payload.createdAt,              completedAt: settlesTurn                ? event.payload.updatedAt                : thread.latestTurn?.turnId === event.payload.turnId                  ? (thread.latestTurn.completedAt ?? null)                  : null,              assistantMessageId: event.payload.messageId,            }          : thread.latestTurn;       // Rebind checkpoint assistant message IDs for assistant messages.      const checkpoints =        event.payload.role === "assistant" && event.payload.turnId !== null          ? rebindCheckpointAssistantMessage(              thread.checkpoints,              event.payload.turnId,              event.payload.messageId,            )          : thread.checkpoints;       return {        kind: "updated",        thread: {          ...thread,          messages,          checkpoints,          latestTurn,          updatedAt: event.occurredAt,        },      };    }     // ── Session ─────────────────────────────────────────────────────    case "thread.session-set": {      // Leaving the "running" session status is the turn-end signal: settle a      // still-running latest turn so its duration reflects the whole turn.      const settledTurnState = settledTurnStateForSessionStatus(event.payload.session.status);      const latestTurn: OrchestrationLatestTurn | null =        event.payload.session.status === "running" && event.payload.session.activeTurnId !== null          ? {              turnId: event.payload.session.activeTurnId,              state: "running",              requestedAt:                thread.latestTurn?.turnId === event.payload.session.activeTurnId                  ? thread.latestTurn.requestedAt                  : event.payload.session.updatedAt,              startedAt:                thread.latestTurn?.turnId === event.payload.session.activeTurnId                  ? (thread.latestTurn.startedAt ?? event.payload.session.updatedAt)                  : event.payload.session.updatedAt,              completedAt: null,              assistantMessageId:                thread.latestTurn?.turnId === event.payload.session.activeTurnId                  ? thread.latestTurn.assistantMessageId                  : null,            }          : thread.latestTurn !== null &&              thread.latestTurn.state === "running" &&              settledTurnState !== null            ? {                ...thread.latestTurn,                state: settledTurnState,                // A running turn's completedAt can only hold a mid-turn                // placeholder checkpoint timestamp — the session leaving                // "running" is the authoritative turn end.                completedAt: event.payload.session.updatedAt,              }            : thread.latestTurn;       return {        kind: "updated",        thread: {          ...thread,          session: event.payload.session,          latestTurn,          updatedAt: event.occurredAt,        },      };    }     case "thread.session-stop-requested":      return thread.session === null        ? { kind: "unchanged" }        : {            kind: "updated",            thread: {              ...thread,              session: {                ...thread.session,                status: "stopped",                activeTurnId: null,                updatedAt: event.payload.createdAt,              },              updatedAt: event.occurredAt,            },          };     // ── Proposed plans ──────────────────────────────────────────────    case "thread.proposed-plan-upserted": {      const proposedPlan = event.payload.proposedPlan;       const proposedPlans = pipe(        thread.proposedPlans,        Arr.filter((entry) => entry.id !== proposedPlan.id),        Arr.append(proposedPlan),        Arr.sort(proposedPlanOrder),      );       return {        kind: "updated",        thread: { ...thread, proposedPlans, updatedAt: event.occurredAt },      };    }     // ── Checkpoints / turn diffs ────────────────────────────────────    case "thread.turn-diff-completed": {      const checkpoint: OrchestrationCheckpointSummary = {        turnId: event.payload.turnId,        checkpointTurnCount: event.payload.checkpointTurnCount,        checkpointRef: event.payload.checkpointRef,        status: event.payload.status,        files: event.payload.files,        assistantMessageId: event.payload.assistantMessageId,        completedAt: event.payload.completedAt,      };       const existing = thread.checkpoints.find((entry) => entry.turnId === checkpoint.turnId);      // Don't overwrite a non-missing checkpoint with a missing one.      if (existing && existing.status !== "missing" && checkpoint.status === "missing") {        return { kind: "unchanged" };      }       const checkpoints = pipe(        thread.checkpoints,        Arr.filter((entry) => entry.turnId !== checkpoint.turnId),        Arr.append(checkpoint),        Arr.sort(checkpointOrder),      );       // Mid-turn diff updates produce placeholder checkpoints; record the      // checkpoint, but don't settle a turn its session is still running.      const diffTurnStillRunning =        thread.session?.status === "running" &&        thread.session.activeTurnId === event.payload.turnId;      const latestTurn =        !diffTurnStillRunning &&        (thread.latestTurn === null || thread.latestTurn.turnId === event.payload.turnId)          ? {              turnId: event.payload.turnId,              state: checkpointStatusToTurnState(event.payload.status),              requestedAt: thread.latestTurn?.requestedAt ?? event.payload.completedAt,              startedAt: thread.latestTurn?.startedAt ?? event.payload.completedAt,              completedAt: event.payload.completedAt,              assistantMessageId: event.payload.assistantMessageId,            }          : thread.latestTurn;       return {        kind: "updated",        thread: { ...thread, checkpoints, latestTurn, updatedAt: event.occurredAt },      };    }     // ── Revert ──────────────────────────────────────────────────────    case "thread.reverted": {      const checkpoints = pipe(        thread.checkpoints,        Arr.filter(          (entry) =>            entry.checkpointTurnCount !== undefined &&            entry.checkpointTurnCount <= event.payload.turnCount,        ),        Arr.sort(checkpointOrder),      );       const retainedTurnIds = new Set(Arr.map(checkpoints, (entry) => entry.turnId));      const messages = retainMessagesAfterRevert(thread.messages, retainedTurnIds);      const proposedPlans = pipe(        thread.proposedPlans,        Arr.filter((plan) => plan.turnId === null || retainedTurnIds.has(plan.turnId)),      );      const activities = pipe(        thread.activities,        Arr.filter((activity) => activity.turnId === null || retainedTurnIds.has(activity.turnId)),      );      const latestCheckpoint = checkpoints.at(-1) ?? null;       return {        kind: "updated",        thread: {          ...thread,          checkpoints,          messages,          proposedPlans,          activities,          latestTurn:            latestCheckpoint === null              ? null              : {                  turnId: latestCheckpoint.turnId,                  state: checkpointStatusToTurnState(                    latestCheckpoint.status as "ready" | "missing" | "error",                  ),                  requestedAt: latestCheckpoint.completedAt,                  startedAt: latestCheckpoint.completedAt,                  completedAt: latestCheckpoint.completedAt,                  assistantMessageId: latestCheckpoint.assistantMessageId ?? null,                },

Lines 1–500 of 595.