2029 lines · 70.5 KB · TypeScript
RawDownload
import {  EventId,  MAX_SCRIPT_ID_LENGTH,  SCRIPT_RUN_COMMAND_PATTERN,  MessageId,  ThreadLinkedPullRequest,  UserInputRequestedPayload,  isImportedAgentSessionMessageId,  type OrchestrationCommand,  type OrchestrationEvent,  type OrchestrationReadModel,  type OrchestrationThread,  type ThreadPullRequestKey,  type ThreadPullRequestLink,  type OrchestrationThreadActivity,} from "@t3tools/contracts";import {  legacyLinkedPullRequestOf,  legacyThreadPullRequestKey,  normalizeThreadPullRequestKey,  threadPullRequestKeysEqual,} from "@t3tools/shared/threadPullRequests";import { compareDateTimeStrings } from "@t3tools/shared/dateTime";import * as DateTime from "effect/DateTime";import * as Crypto from "effect/Crypto";import * as Effect from "effect/Effect";import * as Schema from "effect/Schema";import * as Option from "effect/Option";import * as Predicate from "effect/Predicate";import type * as PlatformError from "effect/PlatformError"; import {  OrchestrationCommandInvariantError,  OrchestrationThreadSettleBlockedError,  type OrchestrationCommandRejection,} from "./Errors.ts";import {  listThreadsByProjectId,  requireActiveProjectWorkspaceRootAbsent,  requireProject,  requireProjectAbsent,  requireThread,  requireThreadArchived,  requireThreadAbsent,  requireThreadNotArchived,} from "./commandInvariants.ts";import { projectEvent } from "./projector.ts";import { threadHasQueuedTurnStart } from "./ThreadSettlementPolicy.ts"; const isScriptRunCommand = Schema.is(SCRIPT_RUN_COMMAND_PATTERN); const nowIso = Effect.map(DateTime.now, DateTime.formatIso);const decodeUserInputRequestedPayload = Schema.decodeUnknownOption(UserInputRequestedPayload);const threadPullRequestLinksEqual = Schema.toEquivalence(Schema.NullOr(ThreadLinkedPullRequest)); /** * Blocked-on-you work derived from the thread's retained activities: an * approval or user-input request with no later resolution for the same * requestId. The server-side twin of the shell's hasPendingApprovals / * hasPendingUserInput flags, which the decider read model does not carry. * The clearing rules MUST match ProjectionPipeline's pending accounting — * resolved activities always clear, respond.failed clears only when the * failure detail marks the request stale/unknown — or settle would be * rejected on threads whose shell flags read as clear. */function isStaleRequestFailureDetail(payload: Record<string, unknown> | null): boolean {  const detail = typeof payload?.detail === "string" ? payload.detail.toLowerCase() : null;  if (detail === null) return false;  return (    detail.includes("stale pending approval request") ||    detail.includes("unknown pending approval request") ||    detail.includes("unknown pending permission request") ||    detail.includes("stale pending user-input request") ||    detail.includes("unknown pending user-input request") ||    detail.includes("unknown pending user input request") ||    detail.includes("unknown pending codex user input request")  );} // Scans the read model's activities, which the projector caps at the most// recent 500 plus pending async questions. Async questions remain actionable// while the agent works, so they must not expire with the activity window.function openRequests(thread: Pick<OrchestrationThread, "activities">) {  const requests = new Map<string, OrchestrationThreadActivity>();  for (const activity of thread.activities) {    const payload =      typeof activity.payload === "object" && activity.payload !== null        ? (activity.payload as Record<string, unknown>)        : null;    const requestId = typeof payload?.requestId === "string" ? payload.requestId : null;    if (requestId === null) continue;    if (activity.kind === "approval.requested" || activity.kind === "user-input.requested") {      requests.set(requestId, activity);    } else if (activity.kind === "approval.resolved" || activity.kind === "user-input.resolved") {      requests.delete(requestId);    } else if (      (activity.kind === "provider.approval.respond.failed" ||        activity.kind === "provider.user-input.respond.failed") &&      isStaleRequestFailureDetail(payload)    ) {      requests.delete(requestId);    }  }  return requests;} /** Apply the shared shell-level rule to the detailed command read model. */function hasQueuedTurnStartForThread(  thread: Pick<OrchestrationThread, "messages" | "latestTurn" | "session">,  now: string,): boolean {  let latestUserMessageAt: string | null = null;  let latestUserMessageAtMs = Number.NEGATIVE_INFINITY;  for (const message of thread.messages) {    if (message.role !== "user" || isImportedAgentSessionMessageId(message.id)) continue;    const messageAtMs = Date.parse(message.createdAt);    latestUserMessageAtMs = Math.max(latestUserMessageAtMs, messageAtMs);    if (messageAtMs === latestUserMessageAtMs) {      latestUserMessageAt = message.createdAt;    }  }  return threadHasQueuedTurnStart(    {      latestUserMessageAt: Number.isFinite(latestUserMessageAtMs) ? latestUserMessageAt : null,      latestTurn: thread.latestTurn,      session: thread.session,    },    now,  );} function findPullRequestLink(  thread: Pick<OrchestrationThread, "pullRequests">,  key: ThreadPullRequestKey,): ThreadPullRequestLink | undefined {  return thread.pullRequests.find((link) => threadPullRequestKeysEqual(link, key));} function withEventBase(  input: Pick<OrchestrationCommand, "commandId"> & {    readonly aggregateKind: OrchestrationEvent["aggregateKind"];    readonly aggregateId: OrchestrationEvent["aggregateId"];    readonly occurredAt: string;    readonly metadata?: OrchestrationEvent["metadata"];  },): Effect.Effect<  Omit<OrchestrationEvent, "sequence" | "type" | "payload">,  PlatformError.PlatformError,  Crypto.Crypto> {  return Crypto.Crypto.pipe(    Effect.flatMap((crypto) =>      crypto.randomUUIDv4.pipe(        Effect.map((eventId) => ({          eventId: EventId.make(eventId),          aggregateKind: input.aggregateKind,          aggregateId: input.aggregateId,          occurredAt: input.occurredAt,          commandId: input.commandId,          causationEventId: null,          correlationId: input.commandId,          metadata: input.metadata ?? {},        })),      ),    ),  );} type PlannedOrchestrationEvent = Omit<OrchestrationEvent, "sequence">; type DecideOrchestrationCommandResult =  | PlannedOrchestrationEvent  | ReadonlyArray<PlannedOrchestrationEvent>; const decideCommandSequence = Effect.fn("decideCommandSequence")(function* ({  commands,  readModel,}: {  readonly commands: ReadonlyArray<OrchestrationCommand>;  readonly readModel: OrchestrationReadModel;}): Effect.fn.Return<  ReadonlyArray<PlannedOrchestrationEvent>,  OrchestrationCommandRejection | PlatformError.PlatformError,  Crypto.Crypto> {  let nextReadModel = readModel;  let nextSequence = readModel.snapshotSequence;  const plannedEvents: PlannedOrchestrationEvent[] = [];   for (const nextCommand of commands) {    const decided = yield* decideOrchestrationCommand({      command: nextCommand,      readModel: nextReadModel,    });    const nextEvents = Array.isArray(decided) ? decided : [decided];    for (const nextEvent of nextEvents) {      plannedEvents.push(nextEvent);      nextSequence += 1;      nextReadModel = yield* projectEvent(nextReadModel, {        ...nextEvent,        sequence: nextSequence,      }).pipe(Effect.orDie);    }  }   return plannedEvents;}); export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand")(function* ({  command,  readModel,  userInputActivity,}: {  readonly command: OrchestrationCommand;  readonly readModel: OrchestrationReadModel;  readonly userInputActivity?: OrchestrationThreadActivity;}): Effect.fn.Return<  DecideOrchestrationCommandResult,  OrchestrationCommandRejection | PlatformError.PlatformError,  Crypto.Crypto> {  switch (command.type) {    case "project.create": {      yield* requireProjectAbsent({        readModel,        command,        projectId: command.projectId,      });      yield* requireActiveProjectWorkspaceRootAbsent({        readModel,        command,        workspaceRoot: command.workspaceRoot,        exceptProjectId: command.projectId,      });       return {        ...(yield* withEventBase({          aggregateKind: "project",          aggregateId: command.projectId,          occurredAt: command.createdAt,          commandId: command.commandId,        })),        type: "project.created",        payload: {          projectId: command.projectId,          title: command.title,          workspaceRoot: command.workspaceRoot,          // Project creation has no user model choice. Older clients sent an          // automatic seed here, but only a metadata update records an          // explicit project default.          defaultModelSelection: null,          faviconPath: null,          projectIcon: null,          scripts: [],          createdAt: command.createdAt,          updatedAt: command.createdAt,        },      };    }     case "project.meta.update": {      const project = yield* requireProject({        readModel,        command,        projectId: command.projectId,      });      if (command.scripts !== undefined) {        // Persisted IDs predate shortcut validation. Let users edit or remove them        // without allowing another invalid ID to enter the project.        const existingIds = new Set(project.scripts.map((script) => script.id));        for (const script of command.scripts) {          if (!existingIds.has(script.id) && !isScriptRunCommand(`script.${script.id}.run`)) {            return yield* new OrchestrationCommandInvariantError({              commandType: command.type,              detail: `Script ID '${script.id}' must be 1-${MAX_SCRIPT_ID_LENGTH} lowercase letters, digits or hyphens, starting with a letter or digit.`,            });          }        }      }      if (command.workspaceRoot !== undefined) {        yield* requireActiveProjectWorkspaceRootAbsent({          readModel,          command,          workspaceRoot: command.workspaceRoot,          exceptProjectId: command.projectId,        });      }      const occurredAt = yield* nowIso;      return {        ...(yield* withEventBase({          aggregateKind: "project",          aggregateId: command.projectId,          occurredAt,          commandId: command.commandId,        })),        type: "project.meta-updated",        payload: {          projectId: command.projectId,          ...(command.title !== undefined ? { title: command.title } : {}),          ...(command.workspaceRoot !== undefined ? { workspaceRoot: command.workspaceRoot } : {}),          ...(command.defaultModelSelection !== undefined            ? { defaultModelSelection: command.defaultModelSelection }            : {}),          ...(command.defaultThreadEnvMode !== undefined            ? { defaultThreadEnvMode: command.defaultThreadEnvMode }            : {}),          ...(command.autoPull !== undefined ? { autoPull: command.autoPull } : {}),          ...(command.faviconPath !== undefined ? { faviconPath: command.faviconPath } : {}),          ...(command.projectIcon !== undefined ? { projectIcon: command.projectIcon } : {}),          ...(command.scripts !== undefined ? { scripts: command.scripts } : {}),          updatedAt: occurredAt,        },      };    }     case "project.delete": {      yield* requireProject({        readModel,        command,        projectId: command.projectId,      });      const activeThreads = listThreadsByProjectId(readModel, command.projectId).filter(        (thread) => thread.deletedAt === null,      );      if (activeThreads.length > 0 && command.force !== true) {        return yield* new OrchestrationCommandInvariantError({          commandType: command.type,          detail: `Project '${command.projectId}' is not empty and cannot be deleted without force=true.`,        });      }      if (activeThreads.length > 0) {        return yield* decideCommandSequence({          readModel,          commands: [            ...activeThreads.map(              (thread): Extract<OrchestrationCommand, { type: "thread.delete" }> => ({                type: "thread.delete",                commandId: command.commandId,                threadId: thread.id,              }),            ),            {              type: "project.delete",              commandId: command.commandId,              projectId: command.projectId,            },          ],        });      }       const occurredAt = yield* nowIso;      return {        ...(yield* withEventBase({          aggregateKind: "project",          aggregateId: command.projectId,          occurredAt,          commandId: command.commandId,        })),        type: "project.deleted" as const,        payload: {          projectId: command.projectId,          deletedAt: occurredAt,        },      };    }     case "thread.create": {      yield* requireProject({        readModel,        command,        projectId: command.projectId,      });      yield* requireThreadAbsent({        readModel,        command,        threadId: command.threadId,      });      return {        ...(yield* withEventBase({          aggregateKind: "thread",          aggregateId: command.threadId,          occurredAt: command.createdAt,          commandId: command.commandId,          ...(command.historyImport === true ? { metadata: { historyImport: true } } : {}),        })),        type: "thread.created",        payload: {          threadId: command.threadId,          projectId: command.projectId,          title: command.title,          modelSelection: command.modelSelection,          runtimeMode: command.runtimeMode,          interactionMode: command.interactionMode,          branch: command.branch,          worktreePath: command.worktreePath,          createdAt: command.createdAt,          updatedAt: command.createdAt,        },      };    }     case "thread.delete": {      yield* requireThread({        readModel,        command,        threadId: command.threadId,      });      const occurredAt = yield* nowIso;      return {        ...(yield* withEventBase({          aggregateKind: "thread",          aggregateId: command.threadId,          occurredAt,          commandId: command.commandId,        })),        type: "thread.deleted",        payload: {          threadId: command.threadId,          deletedAt: occurredAt,        },      };    }     case "thread.archive": {      yield* requireThreadNotArchived({        readModel,        command,        threadId: command.threadId,      });      const occurredAt = yield* nowIso;      return {        ...(yield* withEventBase({          aggregateKind: "thread",          aggregateId: command.threadId,          occurredAt,          commandId: command.commandId,        })),        type: "thread.archived",        payload: {          threadId: command.threadId,          archivedAt: occurredAt,          updatedAt: occurredAt,        },      };    }     case "thread.unarchive": {      yield* requireThreadArchived({        readModel,        command,        threadId: command.threadId,      });      const occurredAt = yield* nowIso;      return {        ...(yield* withEventBase({          aggregateKind: "thread",          aggregateId: command.threadId,          occurredAt,          commandId: command.commandId,        })),        type: "thread.unarchived",        payload: {          threadId: command.threadId,          updatedAt: occurredAt,        },      };    }     case "thread.settle":    case "thread.auto-settle": {      const thread = yield* requireThreadNotArchived({        readModel,        command,        threadId: command.threadId,      });      if (command.type === "thread.auto-settle" && thread.settledOverride !== null) {        return yield* Effect.fail(          new OrchestrationCommandInvariantError({            commandType: command.type,            detail: `thread ${command.threadId} changed before automatic settlement`,          }),        );      }      // The server owns settle eligibility. A stale command must not settle      // a thread whose session is coming alive or working.      if (thread.session?.status === "starting" || thread.session?.status === "running") {        return yield* new OrchestrationThreadSettleBlockedError({ threadId: command.threadId });      }      const pendingRequests = openRequests(thread);      // Manual settlement dismisses async questions without answering them.      // Native callbacks and approvals still need a response or interruption.      if (        Array.from(pendingRequests.values()).some(          (activity) =>            command.type === "thread.auto-settle" ||            activity.kind !== "user-input.requested" ||            !Predicate.isObject(activity.payload) ||            activity.payload.responseMode !== "message",        )      ) {

Lines 1–500 of 2029.