189 lines · 7.1 KB · TypeScript
RawDownload
import type {  OrchestrationCheckpointSummary,  OrchestrationLatestTurn,  OrchestrationMessage,  OrchestrationProposedPlan,  OrchestrationSession,  OrchestrationThread,  OrchestrationThreadActivity,  ScopedThreadRef,} from "@t3tools/contracts";import * as Option from "effect/Option";import { AsyncResult, Atom } from "effect/unstable/reactivity"; import type { EnvironmentThread, EnvironmentThreadShell } from "./models.ts";import { scopeThread } from "./models.ts";import { EMPTY_ENVIRONMENT_THREAD_STATE, type EnvironmentThreadState } from "./threadState.ts";import { parseThreadKey, threadKey } from "./entities.ts";import { THREAD_STATE_IDLE_TTL_MS } from "./threadRetention.ts"; const EMPTY_MESSAGES: ReadonlyArray<OrchestrationMessage> = Object.freeze([]);const EMPTY_ACTIVITIES: ReadonlyArray<OrchestrationThreadActivity> = Object.freeze([]);const EMPTY_PROPOSED_PLANS: ReadonlyArray<OrchestrationProposedPlan> = Object.freeze([]);const EMPTY_CHECKPOINTS: ReadonlyArray<OrchestrationCheckpointSummary> = Object.freeze([]); /** * Combine detail-only collections with the shell's authoritative thread metadata. * * Shell and detail subscriptions are intentionally independent. A cached detail can * therefore briefly outlive a newer shell snapshot after reconnecting. Workspace * consumers must use the shell branch/worktree/project fields so they do not target * a stale checkout while retaining messages, activities, plans, and checkpoints * from the detail subscription. */export function mergeEnvironmentThread(  detail: EnvironmentThread | null,  shell: EnvironmentThreadShell | null,): EnvironmentThread | null {  if (detail === null || shell === null) {    return detail;  }  if (detail.environmentId !== shell.environmentId || detail.id !== shell.id) {    return detail;  }   return {    ...detail,    environmentId: shell.environmentId,    id: shell.id,    projectId: shell.projectId,    title: shell.title,    modelSelection: shell.modelSelection,    runtimeMode: shell.runtimeMode,    interactionMode: shell.interactionMode,    branch: shell.branch,    worktreePath: shell.worktreePath,    latestTurn: shell.latestTurn,    createdAt: shell.createdAt,    updatedAt: shell.updatedAt,    archivedAt: shell.archivedAt,    settledOverride: shell.settledOverride,    settledAt: shell.settledAt,    snoozedUntil: shell.snoozedUntil,    snoozedAt: shell.snoozedAt,    session: shell.session,  };} export function createEnvironmentThreadDetailAtoms<E>(  threadStateAtom: (    environmentId: ScopedThreadRef["environmentId"],    threadId: ScopedThreadRef["threadId"],  ) => Atom.Atom<AsyncResult.AsyncResult<EnvironmentThreadState, E>>,) {  const threadStateValueAtomFamily = Atom.family((key: string) => {    const ref = parseThreadKey(key);    return Atom.make((get) =>      Option.getOrElse(        AsyncResult.value(get(threadStateAtom(ref.environmentId, ref.threadId))),        () => EMPTY_ENVIRONMENT_THREAD_STATE,      ),    ).pipe(      Atom.setIdleTTL(THREAD_STATE_IDLE_TTL_MS),      Atom.withLabel(`environment-thread-state-value:${key}`),    );  });   const threadDetailAtomFamily = Atom.family((key: string) => {    const ref = parseThreadKey(key);    let previousSource: OrchestrationThread | null = null;    let previousValue: EnvironmentThread | null = null;    return Atom.make((get) => {      const source = Option.getOrNull(get(threadStateValueAtomFamily(key)).data);      if (source === previousSource) {        return previousValue;      }      previousSource = source;      previousValue = source === null ? null : scopeThread(ref.environmentId, source);      return previousValue;    }).pipe(      Atom.setIdleTTL(THREAD_STATE_IDLE_TTL_MS),      Atom.withLabel(`environment-thread-detail:${key}`),    );  });   const threadStatusAtomFamily = Atom.family((key: string) =>    Atom.make((get) => get(threadStateValueAtomFamily(key)).status).pipe(      Atom.setIdleTTL(THREAD_STATE_IDLE_TTL_MS),      Atom.withLabel(`environment-thread-status:${key}`),    ),  );   const threadErrorAtomFamily = Atom.family((key: string) =>    Atom.make((get) => Option.getOrNull(get(threadStateValueAtomFamily(key)).error)).pipe(      Atom.setIdleTTL(THREAD_STATE_IDLE_TTL_MS),      Atom.withLabel(`environment-thread-error:${key}`),    ),  );   const threadMessagesAtomFamily = Atom.family((key: string) =>    Atom.make(      (get): ReadonlyArray<OrchestrationMessage> =>        get(threadDetailAtomFamily(key))?.messages ?? EMPTY_MESSAGES,    ).pipe(      Atom.setIdleTTL(THREAD_STATE_IDLE_TTL_MS),      Atom.withLabel(`environment-thread-messages:${key}`),    ),  );   const threadActivitiesAtomFamily = Atom.family((key: string) =>    Atom.make(      (get): ReadonlyArray<OrchestrationThreadActivity> =>        get(threadDetailAtomFamily(key))?.activities ?? EMPTY_ACTIVITIES,    ).pipe(      Atom.setIdleTTL(THREAD_STATE_IDLE_TTL_MS),      Atom.withLabel(`environment-thread-activities:${key}`),    ),  );   const threadProposedPlansAtomFamily = Atom.family((key: string) =>    Atom.make(      (get): ReadonlyArray<OrchestrationProposedPlan> =>        get(threadDetailAtomFamily(key))?.proposedPlans ?? EMPTY_PROPOSED_PLANS,    ).pipe(      Atom.setIdleTTL(THREAD_STATE_IDLE_TTL_MS),      Atom.withLabel(`environment-thread-proposed-plans:${key}`),    ),  );   const threadCheckpointsAtomFamily = Atom.family((key: string) =>    Atom.make(      (get): ReadonlyArray<OrchestrationCheckpointSummary> =>        get(threadDetailAtomFamily(key))?.checkpoints ?? EMPTY_CHECKPOINTS,    ).pipe(      Atom.setIdleTTL(THREAD_STATE_IDLE_TTL_MS),      Atom.withLabel(`environment-thread-checkpoints:${key}`),    ),  );   const threadSessionAtomFamily = Atom.family((key: string) =>    Atom.make(      (get): OrchestrationSession | null => get(threadDetailAtomFamily(key))?.session ?? null,    ).pipe(      Atom.setIdleTTL(THREAD_STATE_IDLE_TTL_MS),      Atom.withLabel(`environment-thread-session:${key}`),    ),  );   const threadLatestTurnAtomFamily = Atom.family((key: string) =>    Atom.make(      (get): OrchestrationLatestTurn | null => get(threadDetailAtomFamily(key))?.latestTurn ?? null,    ).pipe(      Atom.setIdleTTL(THREAD_STATE_IDLE_TTL_MS),      Atom.withLabel(`environment-thread-latest-turn:${key}`),    ),  );   return {    stateAtom: (ref: ScopedThreadRef) => threadStateValueAtomFamily(threadKey(ref)),    detailAtom: (ref: ScopedThreadRef) => threadDetailAtomFamily(threadKey(ref)),    statusAtom: (ref: ScopedThreadRef) => threadStatusAtomFamily(threadKey(ref)),    errorAtom: (ref: ScopedThreadRef) => threadErrorAtomFamily(threadKey(ref)),    messagesAtom: (ref: ScopedThreadRef) => threadMessagesAtomFamily(threadKey(ref)),    activitiesAtom: (ref: ScopedThreadRef) => threadActivitiesAtomFamily(threadKey(ref)),    proposedPlansAtom: (ref: ScopedThreadRef) => threadProposedPlansAtomFamily(threadKey(ref)),    checkpointsAtom: (ref: ScopedThreadRef) => threadCheckpointsAtomFamily(threadKey(ref)),    sessionAtom: (ref: ScopedThreadRef) => threadSessionAtomFamily(threadKey(ref)),    latestTurnAtom: (ref: ScopedThreadRef) => threadLatestTurnAtomFamily(threadKey(ref)),  };}