import * as Effect from "effect/Effect"; import * as Schema from "effect/Schema"; import * as SchemaIssue from "effect/SchemaIssue"; import * as SchemaTransformation from "effect/SchemaTransformation"; import * as Struct from "effect/Struct"; import { ProviderOptionSelections } from "./model.ts"; import { RepositoryIdentity, ThreadEnvMode } from "./environment.ts"; import { ApprovalRequestId, CheckpointRef, ClientSurface, CommandId, EventId, IsoDateTime, MessageId, NonNegativeInt, PositiveInt, ProjectId, ProviderItemId, ThreadId, TrimmedNonEmptyString, TrimmedString, TurnId, } from "./baseSchemas.ts"; import { ProviderInstanceId } from "./providerInstance.ts"; import { PullRequestActor, PullRequestChecksState, PullRequestMergeability, PullRequestReviewDecision, PullRequestState, } from "./pullRequest.ts"; export const ORCHESTRATION_WS_METHODS = { dispatchCommand: "orchestration.dispatchCommand", getWorkflowScript: "orchestration.getWorkflowScript", getTurnDiff: "orchestration.getTurnDiff", getFullThreadDiff: "orchestration.getFullThreadDiff", searchThreads: "orchestration.searchThreads", getArchivedShellSnapshot: "orchestration.getArchivedShellSnapshot", subscribeShell: "orchestration.subscribeShell", subscribeThread: "orchestration.subscribeThread", } as const; export const ProviderApprovalPolicy = Schema.Literals([ "untrusted", "on-failure", "on-request", "never", ]); export type ProviderApprovalPolicy = typeof ProviderApprovalPolicy.Type; export const ProviderSandboxMode = Schema.Literals([ "read-only", "workspace-write", "danger-full-access", ]); export type ProviderSandboxMode = typeof ProviderSandboxMode.Type; /** * `ModelSelection` — selection of a model on a configured provider instance. * * The routing key is `instanceId` (a user-defined slug identifying one * configured provider instance). Drivers, credentials, working-directory * bindings, and any other per-instance state are recovered from the * runtime registry via the instance id. * * Wire legacy: persisted selections produced before the driver/instance * split carried a `provider: ` field instead. The schema absorbs * that shape via a pre-decoding transform — `{provider, model}` is promoted * to `{instanceId: defaultInstanceIdForDriver(provider), model}`. No * post-decode compatibility code lives in the runtime; the transform is the * only compat surface. */ const ModelSelectionWire = Schema.Struct({ instanceId: ProviderInstanceId, model: TrimmedNonEmptyString, options: Schema.optionalKey(ProviderOptionSelections), }); // Source shape for persisted legacy payloads. Fields are typed as // `Schema.Unknown` so malformed drafts still make it into the transform and // fail validation through the target schema (with proper error messages) // rather than at the source-struct layer where the error is less actionable. const ModelSelectionSource = Schema.Struct({ provider: Schema.optional(Schema.Unknown), instanceId: Schema.optional(Schema.Unknown), model: Schema.Unknown, options: Schema.optional(Schema.Unknown), }); export const ModelSelection = ModelSelectionSource.pipe( Schema.decodeTo( ModelSelectionWire, SchemaTransformation.transformOrFail({ decode: (raw) => { // Resolve the routing key: prefer an explicit `instanceId`; fall // back to promoting the legacy `provider` slug (the canonical // `defaultInstanceIdForDriver` mapping) so persisted rollout-era // payloads decode without data loss. The target schema brands the // string as `ProviderInstanceId`. const instanceIdSource = raw.instanceId !== undefined ? raw.instanceId : typeof raw.provider === "string" ? raw.provider : undefined; const base: Record = { instanceId: instanceIdSource, model: raw.model, }; if (raw.options !== undefined) base.options = raw.options; return Effect.succeed(base as typeof ModelSelectionWire.Encoded); }, encode: (value) => { const base: Record = { model: value.model, instanceId: value.instanceId, }; if (value.options !== undefined) base.options = value.options; return Effect.succeed(base as typeof ModelSelectionSource.Encoded); }, }), ), ); export type ModelSelection = typeof ModelSelection.Type; export const RuntimeMode = Schema.Literals([ "approval-required", "auto-accept-edits", "auto", "full-access", ]); export type RuntimeMode = typeof RuntimeMode.Type; export const DEFAULT_RUNTIME_MODE: RuntimeMode = "full-access"; export const ProviderInteractionMode = Schema.Literals(["default", "plan"]); export type ProviderInteractionMode = typeof ProviderInteractionMode.Type; export const DEFAULT_PROVIDER_INTERACTION_MODE: ProviderInteractionMode = "default"; export const ProviderRequestKind = Schema.Literals([ "command", "file-read", "file-change", "mcp-elicitation", ]); export type ProviderRequestKind = typeof ProviderRequestKind.Type; export const AssistantDeliveryMode = Schema.Literals(["buffered", "streaming"]); export type AssistantDeliveryMode = typeof AssistantDeliveryMode.Type; export const ProviderApprovalDecision = Schema.Literals([ "accept", "acceptForSession", "acceptAlways", "decline", "cancel", ]); export type ProviderApprovalDecision = typeof ProviderApprovalDecision.Type; export const ProviderApprovalOption = Schema.Struct({ decision: ProviderApprovalDecision, label: TrimmedNonEmptyString, /** Provider-supplied caution shown next to the option, such as a prompt injection warning. */ warning: Schema.optional(TrimmedNonEmptyString), }); export type ProviderApprovalOption = typeof ProviderApprovalOption.Type; export const ProviderUserInputAnswers = Schema.Record(Schema.String, Schema.Unknown); export type ProviderUserInputAnswers = typeof ProviderUserInputAnswers.Type; export const PROVIDER_SEND_TURN_MAX_INPUT_CHARS = 120_000; export const PROVIDER_SEND_TURN_MAX_ATTACHMENTS = 8; export const PROVIDER_SEND_TURN_MAX_IMAGE_BYTES = 10 * 1024 * 1024; export const PROVIDER_SEND_TURN_MAX_FILE_BYTES = 50 * 1024 * 1024; export const PROVIDER_SEND_TURN_SUPPORTED_IMAGE_MIME_TYPES = [ "image/gif", "image/jpeg", "image/png", "image/webp", ] as const; const PROVIDER_SEND_TURN_SUPPORTED_IMAGE_MIME_TYPE_SET = new Set( PROVIDER_SEND_TURN_SUPPORTED_IMAGE_MIME_TYPES, ); /** Whether a pasted or picked image mime type can be sent on a provider turn. */ export function isProviderSendTurnSupportedImageMimeType(mimeType: string): boolean { return PROVIDER_SEND_TURN_SUPPORTED_IMAGE_MIME_TYPE_SET.has(mimeType.toLowerCase()); } const PROVIDER_SEND_TURN_MAX_IMAGE_DATA_URL_CHARS = 14_000_000; const CHAT_ATTACHMENT_ID_MAX_CHARS = 128; // Correlation id is command id by design in this model. export const CorrelationId = CommandId; export type CorrelationId = typeof CorrelationId.Type; const ChatAttachmentId = TrimmedNonEmptyString.check( Schema.isMaxLength(CHAT_ATTACHMENT_ID_MAX_CHARS), Schema.isPattern(/^[a-z0-9_-]+$/i), ); export type ChatAttachmentId = typeof ChatAttachmentId.Type; export const SNAP_SHOT_ACCESSIBLE_TEXT_MAX_CHARS = 32_000; export const SNAP_SHOT_ACCESSIBILITY_MAX_NODES = 10_000; export const SNAP_SHOT_ACCESSIBILITY_MAX_SERIALIZED_CHARS = 32_000; const SnapShotAccessibilityBounds = Schema.Struct({ x: NonNegativeInt, y: NonNegativeInt, width: PositiveInt, height: PositiveInt, }); const SnapShotAccessibilityState = Schema.Struct({ active: Schema.optional(Schema.Boolean), busy: Schema.optional(Schema.Boolean), checked: Schema.optional(Schema.Literals(["on", "off", "mixed"])), editable: Schema.optional(Schema.Boolean), enabled: Schema.optional(Schema.Boolean), expanded: Schema.optional(Schema.Boolean), focused: Schema.optional(Schema.Boolean), selected: Schema.optional(Schema.Boolean), visible: Schema.optional(Schema.Boolean), }); export interface SnapShotAccessibilityNode { readonly role: string; readonly name?: string; readonly value?: string; readonly description?: string; readonly bounds: typeof SnapShotAccessibilityBounds.Type | null; readonly state?: typeof SnapShotAccessibilityState.Type; readonly actions?: Array; readonly children: Array; } export const SnapShotAccessibilityNode: Schema.Codec = Schema.Struct({ role: TrimmedNonEmptyString.check(Schema.isMaxLength(100)), name: Schema.optionalKey(TrimmedNonEmptyString.check(Schema.isMaxLength(1_000))), value: Schema.optionalKey(TrimmedNonEmptyString.check(Schema.isMaxLength(8_000))), description: Schema.optionalKey(TrimmedNonEmptyString.check(Schema.isMaxLength(2_000))), bounds: Schema.NullOr(SnapShotAccessibilityBounds), state: Schema.optionalKey(SnapShotAccessibilityState), actions: Schema.optionalKey( Schema.mutable(Schema.Array(TrimmedNonEmptyString.check(Schema.isMaxLength(100)))).check( Schema.isMaxLength(32), ), ), children: Schema.mutable( Schema.Array( Schema.suspend((): Schema.Codec => SnapShotAccessibilityNode), ), ).check(Schema.isMaxLength(SNAP_SHOT_ACCESSIBILITY_MAX_NODES)), }); const SnapShotAccessibilityWire = Schema.Union([ Schema.Struct({ format: Schema.Literal("flat-text"), text: TrimmedNonEmptyString.check(Schema.isMaxLength(SNAP_SHOT_ACCESSIBLE_TEXT_MAX_CHARS)), truncated: Schema.Boolean, }), Schema.Struct({ format: Schema.Literal("element-tree"), coordinateSpace: Schema.Literal("captured-image"), imageSize: Schema.Struct({ width: PositiveInt, height: PositiveInt }), truncated: Schema.Boolean, root: SnapShotAccessibilityNode, }), ]); export const SnapShotAccessibility = SnapShotAccessibilityWire.check( Schema.makeFilter((accessibility: typeof SnapShotAccessibilityWire.Type) => { if (accessibility.format === "flat-text") return undefined; let nodes = 0; const stack = [accessibility.root]; while (stack.length > 0) { const node = stack.pop()!; nodes += 1; if (nodes > SNAP_SHOT_ACCESSIBILITY_MAX_NODES) { return `Accessibility trees must not exceed ${SNAP_SHOT_ACCESSIBILITY_MAX_NODES} nodes.`; } stack.push(...node.children); } return ( JSON.stringify(accessibility).length <= SNAP_SHOT_ACCESSIBILITY_MAX_SERIALIZED_CHARS || `Accessibility trees must not exceed ${SNAP_SHOT_ACCESSIBILITY_MAX_SERIALIZED_CHARS} serialized characters.` ); }), ); export type SnapShotAccessibility = typeof SnapShotAccessibility.Type; export const SnapShotSource = Schema.Struct({ kind: Schema.Literal("snap-shot"), capturedAt: IsoDateTime, appName: TrimmedNonEmptyString.check(Schema.isMaxLength(255)), windowTitle: TrimmedString.check(Schema.isMaxLength(1_000)), accessibleText: Schema.optional( TrimmedNonEmptyString.check(Schema.isMaxLength(SNAP_SHOT_ACCESSIBLE_TEXT_MAX_CHARS)), ), accessibility: Schema.optional(SnapShotAccessibility), appIdentifier: Schema.optional(TrimmedNonEmptyString.check(Schema.isMaxLength(255))), appIconDataUrl: Schema.optional( TrimmedNonEmptyString.check( Schema.isMaxLength(100_000), Schema.isPattern(/^data:image\/png;base64,/i), ), ), }); export type SnapShotSource = typeof SnapShotSource.Type; export const ChatImageAttachment = Schema.Struct({ type: Schema.Literal("image"), id: ChatAttachmentId, name: TrimmedNonEmptyString.check(Schema.isMaxLength(255)), mimeType: TrimmedNonEmptyString.check(Schema.isMaxLength(100), Schema.isPattern(/^image\//i)), sizeBytes: NonNegativeInt.check(Schema.isLessThanOrEqualTo(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES)), source: Schema.optional(SnapShotSource), }); export type ChatImageAttachment = typeof ChatImageAttachment.Type; export const ChatFileAttachment = Schema.Struct({ type: Schema.Literal("file"), id: ChatAttachmentId, name: TrimmedNonEmptyString.check(Schema.isMaxLength(255)), mimeType: TrimmedNonEmptyString.check(Schema.isMaxLength(100)), sizeBytes: NonNegativeInt.check( Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(PROVIDER_SEND_TURN_MAX_FILE_BYTES), ), }); export type ChatFileAttachment = typeof ChatFileAttachment.Type; /** * Catch-all for attachment types this build does not know. Attachments ride on * persisted events and thread streams, so a newer server or client must be able * to introduce a type without making older readers fail to decode the whole * message. Decoders keep the shared base fields; consumers skip these or render * them as unsupported. Mirrors how `OrchestrationThreadActivity` keeps `kind` * open. The known discriminators are excluded so a malformed image or file * attachment fails its own schema instead of sliding through here with its * size and mime constraints unchecked. */ export const ChatUnknownAttachment = Schema.Struct({ type: TrimmedNonEmptyString.check( Schema.isMaxLength(50), Schema.isPattern(/^(?!(?:image|file)$)/), ), id: ChatAttachmentId, name: TrimmedNonEmptyString.check(Schema.isMaxLength(255)), mimeType: TrimmedNonEmptyString.check(Schema.isMaxLength(100)), sizeBytes: NonNegativeInt, }); export type ChatUnknownAttachment = typeof ChatUnknownAttachment.Type; const UploadChatImageAttachment = Schema.Struct({ type: Schema.Literal("image"), name: TrimmedNonEmptyString.check(Schema.isMaxLength(255)), mimeType: TrimmedNonEmptyString.check(Schema.isMaxLength(100), Schema.isPattern(/^image\//i)), sizeBytes: NonNegativeInt.check(Schema.isLessThanOrEqualTo(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES)), dataUrl: TrimmedNonEmptyString.check( Schema.isMaxLength(PROVIDER_SEND_TURN_MAX_IMAGE_DATA_URL_CHARS), ), source: Schema.optional(SnapShotSource), }); export type UploadChatImageAttachment = typeof UploadChatImageAttachment.Type; export const ChatAttachment = Schema.Union([ ChatImageAttachment, ChatFileAttachment, ChatUnknownAttachment, ]); export type ChatAttachment = typeof ChatAttachment.Type; export const UserInputAttachments = Schema.Record( Schema.String, Schema.Array(Schema.Union([ChatImageAttachment, ChatFileAttachment])).pipe( Schema.check(Schema.isMaxLength(PROVIDER_SEND_TURN_MAX_ATTACHMENTS)), ), ); export type UserInputAttachments = typeof UserInputAttachments.Type; export const UserInputAttachmentAnswerPayload = Schema.Struct({ requestId: ApprovalRequestId, questionTextById: Schema.optional(Schema.Record(Schema.String, Schema.String)), answers: ProviderUserInputAnswers, attachmentsByQuestionId: UserInputAttachments, }); export type UserInputAttachmentAnswerPayload = typeof UserInputAttachmentAnswerPayload.Type; const UploadChatAttachment = Schema.Union([UploadChatImageAttachment]); export type UploadChatAttachment = typeof UploadChatAttachment.Type; export const ProjectScriptIcon = Schema.Literals([ "play", "test", "lint", "configure", "build", "debug", ]); export type ProjectScriptIcon = typeof ProjectScriptIcon.Type; export const ProjectScript = Schema.Struct({ id: TrimmedNonEmptyString, name: TrimmedNonEmptyString, command: TrimmedNonEmptyString, icon: ProjectScriptIcon, runOnWorktreeCreate: Schema.Boolean, /** * URL to open in the in-app browser preview when this script runs (or * when the user explicitly requests a preview). Optional; only honored on * the desktop build. */ previewUrl: Schema.optional(TrimmedNonEmptyString), /** * When true, automatically open the preview panel pointed at `previewUrl` * the moment this script starts. Ignored without `previewUrl` or on web. */ autoOpenPreview: Schema.optional(Schema.Boolean), }); export type ProjectScript = typeof ProjectScript.Type; export const ProjectFaviconPath = TrimmedNonEmptyString.check( Schema.isMaxLength(1024), Schema.isPattern(/\.(?:avif|gif|ico|jpe?g|png|svg|webp)$/i), ); export type ProjectFaviconPath = typeof ProjectFaviconPath.Type; export const ProjectIconColor = Schema.Literals([ "gray", "red", "orange", "amber", "yellow", "lime", "green", "emerald", "teal", "cyan", "sky", "blue", "indigo", "violet", "purple", "fuchsia", "pink", "rose", ]); export type ProjectIconColor = typeof ProjectIconColor.Type; const ProjectLucideIconName = TrimmedNonEmptyString.check( Schema.isMaxLength(64), Schema.isPattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/), ); const ProjectEmoji = TrimmedNonEmptyString.check(Schema.isMaxLength(32)); export const ProjectIconOverride = Schema.Union([ Schema.Struct({ kind: Schema.Literal("lucide"), name: ProjectLucideIconName, color: ProjectIconColor, }), Schema.Struct({ kind: Schema.Literal("emoji"), emoji: ProjectEmoji, }), ]); export type ProjectIconOverride = typeof ProjectIconOverride.Type; export const OrchestrationProject = Schema.Struct({ id: ProjectId, title: TrimmedNonEmptyString, workspaceRoot: TrimmedNonEmptyString, repositoryIdentity: Schema.optional(Schema.NullOr(RepositoryIdentity)), defaultModelSelection: Schema.NullOr(ModelSelection), // Per-project override for where new threads start. Null/absent means // "no override": clients fall back to t3.json, then the global setting. defaultThreadEnvMode: Schema.optional(Schema.NullOr(ThreadEnvMode)), // Opt-in because background sync performs network I/O and may move the checkout. // Optional on the wire so cached snapshots from older servers still decode. autoPull: Schema.optional(Schema.Boolean), // Optional on the wire so cached snapshots from older servers still decode. faviconPath: Schema.optional(Schema.NullOr(ProjectFaviconPath)), projectIcon: Schema.optional(Schema.NullOr(ProjectIconOverride)), scripts: Schema.Array(ProjectScript), createdAt: IsoDateTime, updatedAt: IsoDateTime, deletedAt: Schema.NullOr(IsoDateTime), }); export type OrchestrationProject = typeof OrchestrationProject.Type; export const OrchestrationMessageRole = Schema.Literals(["user", "assistant", "system"]); export type OrchestrationMessageRole = typeof OrchestrationMessageRole.Type; export const OrchestrationMessage = Schema.Struct({ id: MessageId, role: OrchestrationMessageRole, text: Schema.String, attachments: Schema.optional(Schema.Array(ChatAttachment)), turnId: Schema.NullOr(TurnId), streaming: Schema.Boolean, createdAt: IsoDateTime, updatedAt: IsoDateTime, }); export type OrchestrationMessage = typeof OrchestrationMessage.Type; export const OrchestrationProposedPlanId = TrimmedNonEmptyString; export type OrchestrationProposedPlanId = typeof OrchestrationProposedPlanId.Type; export const OrchestrationProposedPlan = Schema.Struct({ id: OrchestrationProposedPlanId, turnId: Schema.NullOr(TurnId), planMarkdown: TrimmedNonEmptyString, implementedAt: Schema.NullOr(IsoDateTime).pipe(Schema.withDecodingDefault(Effect.succeed(null))), implementationThreadId: Schema.NullOr(ThreadId).pipe( Schema.withDecodingDefault(Effect.succeed(null)), ), createdAt: IsoDateTime, updatedAt: IsoDateTime, }); export type OrchestrationProposedPlan = typeof OrchestrationProposedPlan.Type; const SourceProposedPlanReference = Schema.Struct({ threadId: ThreadId, planId: OrchestrationProposedPlanId, }); export const OrchestrationSessionStatus = Schema.Literals([ "idle", "starting", "running", "ready", "interrupted", "stopped", "error", ]); export type OrchestrationSessionStatus = typeof OrchestrationSessionStatus.Type; export const OrchestrationSession = Schema.Struct({ threadId: ThreadId, status: OrchestrationSessionStatus, providerName: Schema.NullOr(TrimmedNonEmptyString), providerInstanceId: Schema.optional(ProviderInstanceId), runtimeMode: RuntimeMode.pipe(Schema.withDecodingDefault(Effect.succeed(DEFAULT_RUNTIME_MODE))), activeTurnId: Schema.NullOr(TurnId), lastError: Schema.NullOr(TrimmedNonEmptyString), updatedAt: IsoDateTime, }); export type OrchestrationSession = typeof OrchestrationSession.Type; export const OrchestrationCheckpointFile = Schema.Struct({ path: TrimmedNonEmptyString, kind: TrimmedNonEmptyString, additions: NonNegativeInt, deletions: NonNegativeInt, }); export type OrchestrationCheckpointFile = typeof OrchestrationCheckpointFile.Type; export const OrchestrationCheckpointStatus = Schema.Literals(["ready", "missing", "error"]); export type OrchestrationCheckpointStatus = typeof OrchestrationCheckpointStatus.Type; export const OrchestrationCheckpointSummary = Schema.Struct({ turnId: TurnId, checkpointTurnCount: NonNegativeInt, checkpointRef: CheckpointRef, status: OrchestrationCheckpointStatus, files: Schema.Array(OrchestrationCheckpointFile), assistantMessageId: Schema.NullOr(MessageId), completedAt: IsoDateTime, }); export type OrchestrationCheckpointSummary = typeof OrchestrationCheckpointSummary.Type; export const OrchestrationThreadActivityTone = Schema.Literals([ "info", "tool", "approval", "error", ]); export type OrchestrationThreadActivityTone = typeof OrchestrationThreadActivityTone.Type; export const OrchestrationThreadActivity = Schema.Struct({ id: EventId, tone: OrchestrationThreadActivityTone, kind: TrimmedNonEmptyString, summary: TrimmedNonEmptyString, payload: Schema.Unknown, turnId: Schema.NullOr(TurnId), sequence: Schema.optional(NonNegativeInt), createdAt: IsoDateTime, }); export type OrchestrationThreadActivity = typeof OrchestrationThreadActivity.Type; const OrchestrationLatestTurnState = Schema.Literals([ "running", "interrupted", "completed", "error", ]); export type OrchestrationLatestTurnState = typeof OrchestrationLatestTurnState.Type; export const OrchestrationLatestTurn = Schema.Struct({ turnId: TurnId, state: OrchestrationLatestTurnState, requestedAt: IsoDateTime, startedAt: Schema.NullOr(IsoDateTime), completedAt: Schema.NullOr(IsoDateTime), assistantMessageId: Schema.NullOr(MessageId), sourceProposedPlan: Schema.optional(SourceProposedPlanReference), }); export type OrchestrationLatestTurn = typeof OrchestrationLatestTurn.Type; export const ThreadTitleRegeneration = Schema.Struct({ requestId: CommandId, startedAt: IsoDateTime, }); export type ThreadTitleRegeneration = typeof ThreadTitleRegeneration.Type; /** * Legacy single-PR link. Still emitted as the thread's derived current pull * request (see `@t3tools/shared/threadPullRequests`) so clients from before * `pullRequests` keep working independently of their release schedule. */ export const ThreadLinkedPullRequest = Schema.Struct({ projectId: ProjectId, repository: TrimmedNonEmptyString, number: PositiveInt, url: TrimmedNonEmptyString, }); export type ThreadLinkedPullRequest = typeof ThreadLinkedPullRequest.Type; /** Who created a thread ↔ pull request link. `stack-dismissed` is a tombstone * for a native-stack member the user unlinked, so the sync reactor does not * re-add it; clients hide it. */ export const ThreadPullRequestLinkSource = Schema.Literals([ "manual", "created", "agent", "stack", "stack-dismissed", ]); export type ThreadPullRequestLinkSource = typeof ThreadPullRequestLinkSource.Type; /** * Host state persisted on a link by the sync reactor; null until first sync. The overview * fields are optional: a host whose cheap read lacks them leaves them out, and snapshots * written before they existed still decode. */ export const ThreadPullRequestSnapshot = Schema.Struct({ state: PullRequestState, title: TrimmedNonEmptyString, headBranch: TrimmedNonEmptyString, baseBranch: TrimmedNonEmptyString, isDraft: Schema.Boolean, updatedAt: Schema.NullOr(IsoDateTime), syncedAt: IsoDateTime, closedAt: Schema.optional(Schema.NullOr(Schema.String)), mergedAt: Schema.optional(Schema.NullOr(Schema.String)), author: Schema.optional(Schema.NullOr(PullRequestActor)), additions: Schema.optional(NonNegativeInt), deletions: Schema.optional(NonNegativeInt), changedFiles: Schema.optional(NonNegativeInt), reviewDecision: Schema.optional(Schema.NullOr(PullRequestReviewDecision)), checksState: Schema.optional(Schema.NullOr(PullRequestChecksState)), mergeability: Schema.optional(PullRequestMergeability), }); export type ThreadPullRequestSnapshot = typeof ThreadPullRequestSnapshot.Type; export const ThreadPullRequestStackLayer = Schema.Struct({ number: PositiveInt, headBranch: TrimmedNonEmptyString, state: PullRequestState, }); export type ThreadPullRequestStackLayer = typeof ThreadPullRequestStackLayer.Type; /** A host-native stack the pull request belongs to. Layers run bottom to top. */ export const ThreadPullRequestStack = Schema.Struct({ kind: Schema.Literal("native"), id: TrimmedNonEmptyString, number: PositiveInt, url: TrimmedNonEmptyString, base: TrimmedNonEmptyString, layers: Schema.Array(ThreadPullRequestStackLayer), }); export type ThreadPullRequestStack = typeof ThreadPullRequestStack.Type; /** Identity of a pull request as a thread link sees it: host-level, so the * same PR linked from two projects (or two environments) compares equal. */ export const ThreadPullRequestKey = Schema.Struct({ host: TrimmedNonEmptyString, repository: TrimmedNonEmptyString, number: PositiveInt, }); export type ThreadPullRequestKey = typeof ThreadPullRequestKey.Type; export const ThreadPullRequestLink = Schema.Struct({ ...ThreadPullRequestKey.fields, url: TrimmedNonEmptyString, source: ThreadPullRequestLinkSource, linkedAt: IsoDateTime, snapshot: Schema.NullOr(ThreadPullRequestSnapshot), stack: Schema.NullOr(ThreadPullRequestStack), }); export type ThreadPullRequestLink = typeof ThreadPullRequestLink.Type; export const OrchestrationThread = Schema.Struct({ id: ThreadId, projectId: ProjectId, title: TrimmedNonEmptyString, modelSelection: ModelSelection, runtimeMode: RuntimeMode, interactionMode: ProviderInteractionMode.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_PROVIDER_INTERACTION_MODE)), ), branch: Schema.NullOr(TrimmedNonEmptyString), worktreePath: Schema.NullOr(TrimmedNonEmptyString), linkedPullRequest: Schema.optional(Schema.NullOr(ThreadLinkedPullRequest)), // Optional so payloads from pre-link servers still decode. pullRequests: Schema.Array(ThreadPullRequestLink).pipe( Schema.withDecodingDefault(Effect.succeed([])), ), branchPullRequest: Schema.optional(Schema.NullOr(ThreadLinkedPullRequest)), latestTurn: Schema.NullOr(OrchestrationLatestTurn), createdAt: IsoDateTime, updatedAt: IsoDateTime, archivedAt: Schema.NullOr(IsoDateTime).pipe(Schema.withDecodingDefault(Effect.succeed(null))), settledOverride: Schema.NullOr(Schema.Literals(["settled", "active"])).pipe( Schema.withDecodingDefault(Effect.succeed(null)), ), settledAt: Schema.NullOr(IsoDateTime).pipe(Schema.withDecodingDefault(Effect.succeed(null))), // When the thread last re-entered the active list (any thread.unsettled). // Anchors the active-list sort so an unsettled thread surfaces at the top // instead of sinking back to its creation-order slot. Cleared on settle. // Optional so payloads from pre-stamp servers still decode. unsettledAt: Schema.optional(Schema.NullOr(IsoDateTime)), // Snooze is an overlay on the active lifecycle, not a fourth destination: // a snoozed thread stays "active" in the model and is only suppressed from // the inbox until snoozedUntil passes (or the thread raises its hand). // Optional so payloads from pre-snooze servers still decode. snoozedUntil: Schema.optional(Schema.NullOr(IsoDateTime)), snoozedAt: Schema.optional(Schema.NullOr(IsoDateTime)), // Active pinned threads render in the pinned block. Settled and snoozed // threads remain in their respective shelves even when pinned. // Optional so payloads from pre-pinning servers still decode. pinnedAt: Schema.optional(Schema.NullOr(IsoDateTime)), // Fractional index for user-arranged pinned order. Keyed threads sort by // string comparison ahead of keyless ones (which keep creation order), so // servers never need each other's threads to agree on the merged list. // Optional so payloads from pre-reorder servers still decode. pinOrderKey: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), // Manual Active placement. Keyless threads retain their creation/re-entry // order above the arranged run. Settling clears this slot. activeOrderKey: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), // Pending-only state. Optional so older servers remain compatible. titleRegeneration: Schema.optional(Schema.NullOr(ThreadTitleRegeneration)), deletedAt: Schema.NullOr(IsoDateTime), messages: Schema.Array(OrchestrationMessage), proposedPlans: Schema.Array(OrchestrationProposedPlan).pipe( Schema.withDecodingDefault(Effect.succeed([])), ), activities: Schema.Array(OrchestrationThreadActivity), checkpoints: Schema.Array(OrchestrationCheckpointSummary), session: Schema.NullOr(OrchestrationSession), }); export type OrchestrationThread = typeof OrchestrationThread.Type; export const OrchestrationReadModel = Schema.Struct({ snapshotSequence: NonNegativeInt, projects: Schema.Array(OrchestrationProject), threads: Schema.Array(OrchestrationThread), updatedAt: IsoDateTime, }); export type OrchestrationReadModel = typeof OrchestrationReadModel.Type; export const OrchestrationProjectShell = Schema.Struct({ id: ProjectId, title: TrimmedNonEmptyString, workspaceRoot: TrimmedNonEmptyString, repositoryIdentity: Schema.optional(Schema.NullOr(RepositoryIdentity)), defaultModelSelection: Schema.NullOr(ModelSelection), defaultThreadEnvMode: Schema.optional(Schema.NullOr(ThreadEnvMode)), autoPull: Schema.optional(Schema.Boolean), // Optional on the wire so cached snapshots from older servers still decode. faviconPath: Schema.optional(Schema.NullOr(ProjectFaviconPath)), projectIcon: Schema.optional(Schema.NullOr(ProjectIconOverride)), scripts: Schema.Array(ProjectScript), createdAt: IsoDateTime, updatedAt: IsoDateTime, }); export type OrchestrationProjectShell = typeof OrchestrationProjectShell.Type; export const OrchestrationThreadShell = Schema.Struct({ id: ThreadId, projectId: ProjectId, title: TrimmedNonEmptyString, modelSelection: ModelSelection, runtimeMode: RuntimeMode, interactionMode: ProviderInteractionMode.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_PROVIDER_INTERACTION_MODE)), ), branch: Schema.NullOr(TrimmedNonEmptyString), worktreePath: Schema.NullOr(TrimmedNonEmptyString), linkedPullRequest: Schema.optional(Schema.NullOr(ThreadLinkedPullRequest)), pullRequests: Schema.Array(ThreadPullRequestLink).pipe( Schema.withDecodingDefault(Effect.succeed([])), ), branchPullRequest: Schema.optional(Schema.NullOr(ThreadLinkedPullRequest)), latestTurn: Schema.NullOr(OrchestrationLatestTurn), createdAt: IsoDateTime, updatedAt: IsoDateTime, archivedAt: Schema.NullOr(IsoDateTime).pipe(Schema.withDecodingDefault(Effect.succeed(null))), settledOverride: Schema.NullOr(Schema.Literals(["settled", "active"])).pipe( Schema.withDecodingDefault(Effect.succeed(null)), ), settledAt: Schema.NullOr(IsoDateTime).pipe(Schema.withDecodingDefault(Effect.succeed(null))), // See OrchestrationThread.unsettledAt: last re-entry into the active list. unsettledAt: Schema.optional(Schema.NullOr(IsoDateTime)), snoozedUntil: Schema.optional(Schema.NullOr(IsoDateTime)), snoozedAt: Schema.optional(Schema.NullOr(IsoDateTime)), pinnedAt: Schema.optional(Schema.NullOr(IsoDateTime)), pinOrderKey: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), activeOrderKey: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), titleRegeneration: Schema.optional(Schema.NullOr(ThreadTitleRegeneration)), session: Schema.NullOr(OrchestrationSession), latestUserMessageAt: Schema.NullOr(IsoDateTime), hasPendingApprovals: Schema.Boolean, hasPendingUserInput: Schema.Boolean, hasActionableProposedPlan: Schema.Boolean, /** * Native background work alive after the turn settles: "working" while * subagents/workflows run, "monitoring" when watch loops are the only * live work. Optional so old servers/clients interop; absent = none. */ backgroundLiveness: Schema.optional(Schema.NullOr(Schema.Literals(["working", "monitoring"]))), /** * Current plan step while a turn runs, for the Working indicators * (sidebar row, in-chat working line). Cleared when the turn settles — * never persists as stale UI. Optional so old servers/clients interop. */ planProgress: Schema.optional( Schema.NullOr( Schema.Struct({ step: TrimmedNonEmptyString, completedSteps: NonNegativeInt, totalSteps: NonNegativeInt, }), ), ), }); export type OrchestrationThreadShell = typeof OrchestrationThreadShell.Type; export const OrchestrationShellSnapshot = Schema.Struct({ snapshotSequence: NonNegativeInt, projects: Schema.Array(OrchestrationProjectShell), threads: Schema.Array(OrchestrationThreadShell), updatedAt: IsoDateTime, }); export type OrchestrationShellSnapshot = typeof OrchestrationShellSnapshot.Type; export const OrchestrationShellStreamEvent = Schema.Union([ Schema.Struct({ kind: Schema.Literal("project-upserted"), sequence: NonNegativeInt, project: OrchestrationProjectShell, }), Schema.Struct({ kind: Schema.Literal("project-removed"), sequence: NonNegativeInt, projectId: ProjectId, }), Schema.Struct({ kind: Schema.Literal("thread-upserted"), sequence: NonNegativeInt, thread: OrchestrationThreadShell, }), Schema.Struct({ kind: Schema.Literal("thread-removed"), sequence: NonNegativeInt, threadId: ThreadId, }), ]); export type OrchestrationShellStreamEvent = typeof OrchestrationShellStreamEvent.Type; export const OrchestrationShellStreamItem = Schema.Union([ Schema.Struct({ kind: Schema.Literal("synchronized"), }), Schema.Struct({ kind: Schema.Literal("snapshot"), snapshot: OrchestrationShellSnapshot, }), OrchestrationShellStreamEvent, ]); export type OrchestrationShellStreamItem = typeof OrchestrationShellStreamItem.Type; export const OrchestrationSubscribeShellInput = Schema.Struct({ /** * When provided, the server skips the initial full shell snapshot and instead * replays shell events after this sequence before streaming live events. * Clients that already hold a cached (or HTTP-loaded) shell snapshot pass its * sequence here so the subscription resumes without re-sending the entire * projects/threads list (overlapping events are deduped by sequence on the * client). */ afterSequence: Schema.optionalKey(NonNegativeInt), /** * Requests an explicit marker after the subscription has emitted its initial * snapshot or catch-up replay and before it begins emitting live events. */ requestCompletionMarker: Schema.optionalKey(Schema.Boolean), }); export type OrchestrationSubscribeShellInput = typeof OrchestrationSubscribeShellInput.Type; export const OrchestrationSubscribeThreadInput = Schema.Struct({ threadId: ThreadId, /** * When provided, the server skips the initial snapshot frame and instead * replays events after this sequence before streaming live events. Clients * that load the snapshot over HTTP pass the snapshot's sequence here so the * live subscription resumes without a gap (overlapping events are deduped by * sequence on the client). */ afterSequence: Schema.optionalKey(NonNegativeInt), /** * Requests an explicit marker after the subscription has emitted its initial * snapshot or catch-up replay and before it begins emitting live events. */ requestCompletionMarker: Schema.optionalKey(Schema.Boolean), /** * When provided, the fallback snapshot frame (sent when `afterSequence` is * missing or the catch-up gap is too large) is windowed to the last * `turnLimit` user-anchored turns and carries `page` metadata. Absent means * the fallback snapshot is the full thread, preserving pre-pagination client * behavior. Live events are unaffected either way. */ turnLimit: Schema.optionalKey(PositiveInt), }); export type OrchestrationSubscribeThreadInput = typeof OrchestrationSubscribeThreadInput.Type; /** * Bounds a thread detail read to a window of recent turns. `turnLimit` counts * turns with a user pending message (subagent/fan-out turns between them ride * along), so the window always contains the last N user prompts. `beforeCursor` * requests the disjoint page of older turns strictly before a previously * returned cursor. Requests without a window get the full thread; pagination is * strictly opt-in so older clients keep today's behavior on both HTTP and the * WebSocket fallback snapshot. */ export const OrchestrationThreadDetailWindow = Schema.Struct({ turnLimit: Schema.optionalKey(PositiveInt), beforeCursor: Schema.optionalKey(TrimmedNonEmptyString), }); export type OrchestrationThreadDetailWindow = typeof OrchestrationThreadDetailWindow.Type; /** * Page metadata for a windowed thread detail read. `beforeCursor` is opaque and * exclusive: passing it back returns the adjacent disjoint slice of older * turns. `null` means the thread is fully loaded below this page. The * `snapshotSequence` mirrors the top-level snapshot sequence so history pages * can be sequence-checked against live state before merging. */ export const OrchestrationThreadDetailPage = Schema.Struct({ beforeCursor: Schema.NullOr(TrimmedNonEmptyString), hasMore: Schema.Boolean, snapshotSequence: NonNegativeInt, /** * Highest event sequence applied to THIS thread at page read time. The * global `snapshotSequence` advances with every thread's events, so a * client cannot wait for it via its per-thread subscription; this * thread-scoped watermark is reachable. A client merging an older page * must first have applied live events up to it — otherwise a streaming * turn outside the loaded window could have deltas replayed on top of * page content that already includes them, duplicating text. */ threadSequence: Schema.optionalKey(NonNegativeInt), }); export type OrchestrationThreadDetailPage = typeof OrchestrationThreadDetailPage.Type; export const OrchestrationThreadDetailSnapshot = Schema.Struct({ snapshotSequence: NonNegativeInt, thread: OrchestrationThread, // Present only on windowed responses. Absent on full snapshots (and from // pre-pagination servers), which clients treat as fully loaded. page: Schema.optional(OrchestrationThreadDetailPage), }); export type OrchestrationThreadDetailSnapshot = typeof OrchestrationThreadDetailSnapshot.Type; export const ProjectCreateCommand = Schema.Struct({ type: Schema.Literal("project.create"), commandId: CommandId, projectId: ProjectId, title: TrimmedNonEmptyString, workspaceRoot: TrimmedNonEmptyString, createWorkspaceRootIfMissing: Schema.optional(Schema.Boolean), // Retained for older clients that sent an automatic create-time seed. The // server ignores it; explicit project defaults use project.meta.update. defaultModelSelection: Schema.optional(Schema.NullOr(ModelSelection)), createdAt: IsoDateTime, }); const ProjectMetaUpdateCommand = Schema.Struct({ type: Schema.Literal("project.meta.update"), commandId: CommandId, projectId: ProjectId, title: Schema.optional(TrimmedNonEmptyString), workspaceRoot: Schema.optional(TrimmedNonEmptyString), defaultModelSelection: Schema.optional(Schema.NullOr(ModelSelection)), // Absent = leave unchanged; null = clear the override. defaultThreadEnvMode: Schema.optional(Schema.NullOr(ThreadEnvMode)), autoPull: Schema.optional(Schema.Boolean), faviconPath: Schema.optional(Schema.NullOr(ProjectFaviconPath)), projectIcon: Schema.optional(Schema.NullOr(ProjectIconOverride)), scripts: Schema.optional(Schema.Array(ProjectScript)), }); const ProjectDeleteCommand = Schema.Struct({ type: Schema.Literal("project.delete"), commandId: CommandId, projectId: ProjectId, force: Schema.optional(Schema.Boolean), }); const ThreadCreateCommand = Schema.Struct({ type: Schema.Literal("thread.create"), commandId: CommandId, threadId: ThreadId, projectId: ProjectId, title: TrimmedNonEmptyString, modelSelection: ModelSelection, runtimeMode: RuntimeMode, interactionMode: ProviderInteractionMode.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_PROVIDER_INTERACTION_MODE)), ), branch: Schema.NullOr(TrimmedNonEmptyString), worktreePath: Schema.NullOr(TrimmedNonEmptyString), createdAt: IsoDateTime, historyImport: Schema.optional(Schema.Literal(true)), }); const ThreadDeleteCommand = Schema.Struct({ type: Schema.Literal("thread.delete"), commandId: CommandId, threadId: ThreadId, }); const ThreadArchiveCommand = Schema.Struct({ type: Schema.Literal("thread.archive"), commandId: CommandId, threadId: ThreadId, }); const ThreadUnarchiveCommand = Schema.Struct({ type: Schema.Literal("thread.unarchive"), commandId: CommandId, threadId: ThreadId, }); const ThreadSettleCommand = Schema.Struct({ type: Schema.Literal("thread.settle"), commandId: CommandId, threadId: ThreadId, }); const ThreadAutoSettleCommand = Schema.Struct({ type: Schema.Literal("thread.auto-settle"), commandId: CommandId, threadId: ThreadId, snapshotSequence: NonNegativeInt, settledAt: IsoDateTime, }); const ThreadUnsettleCommand = Schema.Struct({ type: Schema.Literal("thread.unsettle"), commandId: CommandId, threadId: ThreadId, // Commands only carry "user": activity un-settles are decided server-side // (the decider emits thread.unsettled(reason: "activity") events directly, // never through this command), so a client cannot forge the neutral reset. reason: Schema.Literal("user"), }); const ThreadSnoozeCommand = Schema.Struct({ type: Schema.Literal("thread.snooze"), commandId: CommandId, threadId: ThreadId, // The wake time. Event-based wake conditions (PR merged, review posted) // will arrive as an optional condition field alongside this; time-based // snooze is just the first kind of condition. snoozedUntil: IsoDateTime, }); const ThreadUnsnoozeCommand = Schema.Struct({ type: Schema.Literal("thread.unsnooze"), commandId: CommandId, threadId: ThreadId, // Commands only carry "user": activity wakes are decided server-side (the // decider emits thread.unsnoozed(reason: "activity") directly), and timer // wakes need no event at all — clients derive visibility from snoozedUntil, // so a passed wake time simply stops classifying as snoozed. reason: Schema.Literal("user"), }); const ThreadPinCommand = Schema.Struct({ type: Schema.Literal("thread.pin"), commandId: CommandId, threadId: ThreadId, // Initial slot in the user-arranged pinned order (see ThreadPinReorderCommand). // Optional: clients on pre-reorder servers omit it, and the pinned block // falls back to creation order for keyless threads. orderKey: Schema.optional(TrimmedNonEmptyString), }); const ThreadUnpinCommand = Schema.Struct({ type: Schema.Literal("thread.unpin"), commandId: CommandId, threadId: ThreadId, }); const ThreadPinReorderCommand = Schema.Struct({ type: Schema.Literal("thread.pin.reorder"), commandId: CommandId, threadId: ThreadId, // Fractional index key: pinned threads sort by plain string comparison of // these keys, so a drag writes one key to one thread — neighbors (possibly // on other servers) are never touched. Clients compute a key that sorts // between the dropped position's neighbors. orderKey: TrimmedNonEmptyString, }); const ThreadActiveReorderCommand = Schema.Struct({ type: Schema.Literal("thread.active.reorder"), commandId: CommandId, threadId: ThreadId, orderKey: TrimmedNonEmptyString, }); const ThreadMetaUpdateCommand = Schema.Struct({ type: Schema.Literal("thread.meta.update"), commandId: CommandId, threadId: ThreadId, title: Schema.optional(TrimmedNonEmptyString), regenerateTitle: Schema.optional(Schema.Literal(true)), modelSelection: Schema.optional(ModelSelection), branch: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), expectedBranch: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), worktreePath: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), linkedPullRequest: Schema.optional(Schema.NullOr(ThreadLinkedPullRequest)), }).check( Schema.makeFilter( (input) => !(input.title !== undefined && input.regenerateTitle === true) || "title and regenerateTitle cannot be specified together", ), ); const ThreadPullRequestLinkCommand = Schema.Struct({ type: Schema.Literal("thread.pull-request.link"), commandId: CommandId, threadId: ThreadId, ...ThreadPullRequestKey.fields, url: TrimmedNonEmptyString, source: ThreadPullRequestLinkSource, }); const ThreadPullRequestUnlinkCommand = Schema.Struct({ type: Schema.Literal("thread.pull-request.unlink"), commandId: CommandId, threadId: ThreadId, ...ThreadPullRequestKey.fields, }); const ThreadRuntimeModeSetCommand = Schema.Struct({ type: Schema.Literal("thread.runtime-mode.set"), commandId: CommandId, threadId: ThreadId, runtimeMode: RuntimeMode, createdAt: IsoDateTime, }); const ThreadInteractionModeSetCommand = Schema.Struct({ type: Schema.Literal("thread.interaction-mode.set"), commandId: CommandId, threadId: ThreadId, interactionMode: ProviderInteractionMode, createdAt: IsoDateTime, }); const ThreadTurnStartBootstrapCreateThread = Schema.Struct({ projectId: ProjectId, title: TrimmedNonEmptyString, modelSelection: ModelSelection, runtimeMode: RuntimeMode, interactionMode: ProviderInteractionMode, branch: Schema.NullOr(TrimmedNonEmptyString), worktreePath: Schema.NullOr(TrimmedNonEmptyString), createdAt: IsoDateTime, }); const ThreadTurnStartBootstrapPrepareWorktree = Schema.Struct({ projectCwd: TrimmedNonEmptyString, baseBranch: TrimmedNonEmptyString, branch: Schema.optional(TrimmedNonEmptyString), startFromOrigin: Schema.optional(Schema.Boolean), }); const ThreadTurnStartBootstrap = Schema.Struct({ createThread: Schema.optional(ThreadTurnStartBootstrapCreateThread), prepareWorktree: Schema.optional(ThreadTurnStartBootstrapPrepareWorktree), runSetupScript: Schema.optional(Schema.Boolean), }); export type ThreadTurnStartBootstrap = typeof ThreadTurnStartBootstrap.Type; export const ThreadTurnStartCommand = Schema.Struct({ type: Schema.Literal("thread.turn.start"), commandId: CommandId, threadId: ThreadId, message: Schema.Struct({ messageId: MessageId, role: Schema.Literal("user"), text: Schema.String, attachments: Schema.Array(ChatAttachment), }), modelSelection: Schema.optional(ModelSelection), titleSeed: Schema.optional(TrimmedNonEmptyString), runtimeMode: RuntimeMode.pipe(Schema.withDecodingDefault(Effect.succeed(DEFAULT_RUNTIME_MODE))), interactionMode: ProviderInteractionMode.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_PROVIDER_INTERACTION_MODE)), ), bootstrap: Schema.optional(ThreadTurnStartBootstrap), sourceProposedPlan: Schema.optional(SourceProposedPlanReference), createdAt: IsoDateTime, }); const ClientThreadTurnStartCommand = Schema.Struct({ type: Schema.Literal("thread.turn.start"), commandId: CommandId, threadId: ThreadId, message: Schema.Struct({ messageId: MessageId, role: Schema.Literal("user"), text: Schema.String, attachments: Schema.Array(Schema.Union([UploadChatAttachment, ChatAttachment])), }), modelSelection: Schema.optional(ModelSelection), titleSeed: Schema.optional(TrimmedNonEmptyString), runtimeMode: RuntimeMode, interactionMode: ProviderInteractionMode, bootstrap: Schema.optional(ThreadTurnStartBootstrap), sourceProposedPlan: Schema.optional(SourceProposedPlanReference), createdAt: IsoDateTime, }); const ThreadTurnInterruptCommand = Schema.Struct({ type: Schema.Literal("thread.turn.interrupt"), commandId: CommandId, threadId: ThreadId, turnId: Schema.optional(TurnId), createdAt: IsoDateTime, }); const ThreadApprovalRespondCommand = Schema.Struct({ type: Schema.Literal("thread.approval.respond"), commandId: CommandId, threadId: ThreadId, requestId: ApprovalRequestId, decision: ProviderApprovalDecision, createdAt: IsoDateTime, }); const ThreadUserInputRespondCommand = Schema.Struct({ type: Schema.Literal("thread.user-input.respond"), commandId: CommandId, threadId: ThreadId, requestId: ApprovalRequestId, answers: ProviderUserInputAnswers, attachmentsByQuestionId: Schema.optional(UserInputAttachments), createdAt: IsoDateTime, }); // Closes an async question without answering it. The agent is not messaged; // the composer is simply released. Native callback questions cannot be dismissed // this way because the provider is blocked waiting on a reply. const ThreadUserInputDismissCommand = Schema.Struct({ type: Schema.Literal("thread.user-input.dismiss"), commandId: CommandId, threadId: ThreadId, requestId: ApprovalRequestId, createdAt: IsoDateTime, }); const ThreadCheckpointRevertCommand = Schema.Struct({ type: Schema.Literal("thread.checkpoint.revert"), commandId: CommandId, threadId: ThreadId, turnCount: NonNegativeInt, createdAt: IsoDateTime, }); const ThreadSessionStopCommand = Schema.Struct({ type: Schema.Literal("thread.session.stop"), commandId: CommandId, threadId: ThreadId, createdAt: IsoDateTime, // Settle-cleanup stops are conditional: the decider drops the stop if the // thread was re-engaged (unsettled, session starting/running, or a queued // turn start) between the settle and this command. Guarding in the decider // closes the race a post-settle snapshot read cannot: commands are decided // serially against the authoritative read model. onlyIfSettled: Schema.optional(Schema.Boolean), }); const DispatchableClientOrchestrationCommand = Schema.Union([ ProjectCreateCommand, ProjectMetaUpdateCommand, ProjectDeleteCommand, ThreadCreateCommand, ThreadDeleteCommand, ThreadArchiveCommand, ThreadUnarchiveCommand, ThreadSettleCommand, ThreadUnsettleCommand, ThreadSnoozeCommand, ThreadUnsnoozeCommand, ThreadPinCommand, ThreadUnpinCommand, ThreadPinReorderCommand, ThreadActiveReorderCommand, ThreadMetaUpdateCommand, ThreadPullRequestLinkCommand, ThreadPullRequestUnlinkCommand, ThreadRuntimeModeSetCommand, ThreadInteractionModeSetCommand, ThreadTurnStartCommand, ThreadTurnInterruptCommand, ThreadApprovalRespondCommand, ThreadUserInputRespondCommand, ThreadUserInputDismissCommand, ThreadCheckpointRevertCommand, ThreadSessionStopCommand, ]); export type DispatchableClientOrchestrationCommand = typeof DispatchableClientOrchestrationCommand.Type; export const ClientOrchestrationCommand = Schema.Union([ ProjectCreateCommand, ProjectMetaUpdateCommand, ProjectDeleteCommand, ThreadCreateCommand, ThreadDeleteCommand, ThreadArchiveCommand, ThreadUnarchiveCommand, ThreadSettleCommand, ThreadUnsettleCommand, ThreadSnoozeCommand, ThreadUnsnoozeCommand, ThreadPinCommand, ThreadUnpinCommand, ThreadPinReorderCommand, ThreadActiveReorderCommand, ThreadMetaUpdateCommand, ThreadPullRequestLinkCommand, ThreadPullRequestUnlinkCommand, ThreadRuntimeModeSetCommand, ThreadInteractionModeSetCommand, ClientThreadTurnStartCommand, ThreadTurnInterruptCommand, ThreadApprovalRespondCommand, ThreadUserInputRespondCommand, ThreadUserInputDismissCommand, ThreadCheckpointRevertCommand, ThreadSessionStopCommand, ]); export type ClientOrchestrationCommand = typeof ClientOrchestrationCommand.Type; const ThreadSessionSetCommand = Schema.Struct({ type: Schema.Literal("thread.session.set"), commandId: CommandId, threadId: ThreadId, session: OrchestrationSession, createdAt: IsoDateTime, }); const ThreadMessageAssistantDeltaCommand = Schema.Struct({ type: Schema.Literal("thread.message.assistant.delta"), commandId: CommandId, threadId: ThreadId, messageId: MessageId, delta: Schema.String, turnId: Schema.optional(TurnId), createdAt: IsoDateTime, }); const ThreadMessageAssistantCompleteCommand = Schema.Struct({ type: Schema.Literal("thread.message.assistant.complete"), commandId: CommandId, threadId: ThreadId, messageId: MessageId, turnId: Schema.optional(TurnId), createdAt: IsoDateTime, }); const ThreadHistoryImportCommand = Schema.Struct({ type: Schema.Literal("thread.history.import"), commandId: CommandId, threadId: ThreadId, messages: Schema.Array( Schema.Struct({ messageId: MessageId, role: Schema.Literals(["user", "assistant"]), text: Schema.String, createdAt: IsoDateTime, }), ).check(Schema.isNonEmpty()), }); const ThreadProposedPlanUpsertCommand = Schema.Struct({ type: Schema.Literal("thread.proposed-plan.upsert"), commandId: CommandId, threadId: ThreadId, proposedPlan: OrchestrationProposedPlan, createdAt: IsoDateTime, }); const ThreadTurnDiffCompleteCommand = Schema.Struct({ type: Schema.Literal("thread.turn.diff.complete"), commandId: CommandId, threadId: ThreadId, turnId: TurnId, completedAt: IsoDateTime, checkpointRef: CheckpointRef, status: OrchestrationCheckpointStatus, files: Schema.Array(OrchestrationCheckpointFile), assistantMessageId: Schema.optional(MessageId), checkpointTurnCount: NonNegativeInt, createdAt: IsoDateTime, }); const ThreadActivityAppendCommand = Schema.Struct({ type: Schema.Literal("thread.activity.append"), commandId: CommandId, threadId: ThreadId, activity: OrchestrationThreadActivity, createdAt: IsoDateTime, }); const ThreadRevertCompleteCommand = Schema.Struct({ type: Schema.Literal("thread.revert.complete"), commandId: CommandId, threadId: ThreadId, turnCount: NonNegativeInt, createdAt: IsoDateTime, }); const ThreadTitleRegenerationCompleteCommand = Schema.Struct({ type: Schema.Literal("thread.title.regeneration.complete"), commandId: CommandId, threadId: ThreadId, requestId: CommandId, title: Schema.optional(TrimmedNonEmptyString), }); const ThreadPullRequestSyncCommand = Schema.Struct({ type: Schema.Literal("thread.pull-request.sync"), commandId: CommandId, threadId: ThreadId, projectId: ProjectId, snapshotSequence: NonNegativeInt, expected: Schema.Struct({ workspaceRoot: TrimmedNonEmptyString, branch: Schema.NullOr(TrimmedNonEmptyString), worktreePath: Schema.NullOr(TrimmedNonEmptyString), linkedPullRequest: Schema.NullOr(ThreadLinkedPullRequest), branchPullRequest: Schema.NullOr(ThreadLinkedPullRequest), }), branchPullRequest: Schema.NullOr(ThreadLinkedPullRequest), linkedPullRequest: Schema.optional(ThreadLinkedPullRequest), }); const ThreadPullRequestLinkSyncCommand = Schema.Struct({ type: Schema.Literal("thread.pull-request-link.sync"), commandId: CommandId, threadId: ThreadId, ...ThreadPullRequestKey.fields, snapshot: ThreadPullRequestSnapshot, stack: Schema.NullOr(ThreadPullRequestStack), }); const InternalOrchestrationCommand = Schema.Union([ ThreadAutoSettleCommand, ThreadPullRequestSyncCommand, ThreadPullRequestLinkSyncCommand, ThreadSessionSetCommand, ThreadMessageAssistantDeltaCommand, ThreadMessageAssistantCompleteCommand, ThreadHistoryImportCommand, ThreadProposedPlanUpsertCommand, ThreadTurnDiffCompleteCommand, ThreadActivityAppendCommand, ThreadRevertCompleteCommand, ThreadTitleRegenerationCompleteCommand, ThreadPullRequestSyncCommand, ThreadPullRequestLinkSyncCommand, ]); export type InternalOrchestrationCommand = typeof InternalOrchestrationCommand.Type; export const OrchestrationCommand = Schema.Union([ DispatchableClientOrchestrationCommand, InternalOrchestrationCommand, ]); export type OrchestrationCommand = typeof OrchestrationCommand.Type; export const OrchestrationEventType = Schema.Literals([ "project.created", "project.meta-updated", "project.deleted", "thread.created", "thread.deleted", "thread.archived", "thread.unarchived", "thread.settled", "thread.unsettled", "thread.snoozed", "thread.unsnoozed", "thread.pinned", "thread.unpinned", "thread.pin-reordered", "thread.meta-updated", "thread.pull-request-linked", "thread.pull-request-unlinked", "thread.pull-request-synced", "thread.runtime-mode-set", "thread.interaction-mode-set", "thread.message-sent", "thread.turn-start-requested", "thread.turn-interrupt-requested", "thread.approval-response-requested", "thread.user-input-response-requested", "thread.checkpoint-revert-requested", "thread.reverted", "thread.session-stop-requested", "thread.session-set", "thread.proposed-plan-upserted", "thread.turn-diff-completed", "thread.activity-appended", ]); export type OrchestrationEventType = typeof OrchestrationEventType.Type; export const OrchestrationAggregateKind = Schema.Literals(["project", "thread"]); export type OrchestrationAggregateKind = typeof OrchestrationAggregateKind.Type; export const OrchestrationActorKind = Schema.Literals(["client", "server", "provider"]); export const ProjectCreatedPayload = Schema.Struct({ projectId: ProjectId, title: TrimmedNonEmptyString, workspaceRoot: TrimmedNonEmptyString, repositoryIdentity: Schema.optional(Schema.NullOr(RepositoryIdentity)), defaultModelSelection: Schema.NullOr(ModelSelection), // Optional so persisted events from older servers still decode. faviconPath: Schema.optional(Schema.NullOr(ProjectFaviconPath)), projectIcon: Schema.optional(Schema.NullOr(ProjectIconOverride)), scripts: Schema.Array(ProjectScript), createdAt: IsoDateTime, updatedAt: IsoDateTime, }); export const ProjectMetaUpdatedPayload = Schema.Struct({ projectId: ProjectId, title: Schema.optional(TrimmedNonEmptyString), workspaceRoot: Schema.optional(TrimmedNonEmptyString), repositoryIdentity: Schema.optional(Schema.NullOr(RepositoryIdentity)), defaultModelSelection: Schema.optional(Schema.NullOr(ModelSelection)), defaultThreadEnvMode: Schema.optional(Schema.NullOr(ThreadEnvMode)), autoPull: Schema.optional(Schema.Boolean), faviconPath: Schema.optional(Schema.NullOr(ProjectFaviconPath)), projectIcon: Schema.optional(Schema.NullOr(ProjectIconOverride)), scripts: Schema.optional(Schema.Array(ProjectScript)), updatedAt: IsoDateTime, }); export const ProjectDeletedPayload = Schema.Struct({ projectId: ProjectId, deletedAt: IsoDateTime, }); export const ThreadCreatedPayload = Schema.Struct({ threadId: ThreadId, projectId: ProjectId, title: TrimmedNonEmptyString, modelSelection: ModelSelection, runtimeMode: RuntimeMode.pipe(Schema.withDecodingDefault(Effect.succeed(DEFAULT_RUNTIME_MODE))), interactionMode: ProviderInteractionMode.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_PROVIDER_INTERACTION_MODE)), ), branch: Schema.NullOr(TrimmedNonEmptyString), worktreePath: Schema.NullOr(TrimmedNonEmptyString), createdAt: IsoDateTime, updatedAt: IsoDateTime, }); export const ThreadDeletedPayload = Schema.Struct({ threadId: ThreadId, deletedAt: IsoDateTime, }); export const ThreadArchivedPayload = Schema.Struct({ threadId: ThreadId, archivedAt: IsoDateTime, updatedAt: IsoDateTime, }); export const ThreadUnarchivedPayload = Schema.Struct({ threadId: ThreadId, updatedAt: IsoDateTime, }); export const ThreadSettledPayload = Schema.Struct({ threadId: ThreadId, settledAt: IsoDateTime, updatedAt: IsoDateTime, }); export const ThreadUnsettledPayload = Schema.Struct({ threadId: ThreadId, reason: Schema.Literals(["user", "activity"]), updatedAt: IsoDateTime, }); export const ThreadSnoozedPayload = Schema.Struct({ threadId: ThreadId, snoozedUntil: IsoDateTime, snoozedAt: IsoDateTime, updatedAt: IsoDateTime, }); export const ThreadUnsnoozedPayload = Schema.Struct({ threadId: ThreadId, // user: explicit "wake now". activity: real work arrived (user message / // session coming alive) and the decider cleared the snooze — mirrors // thread.unsettled's activity resets. Timer wakes emit no event: clients // derive them from snoozedUntil passing. reason: Schema.Literals(["user", "activity"]), updatedAt: IsoDateTime, }); export const ThreadPinnedPayload = Schema.Struct({ threadId: ThreadId, pinnedAt: IsoDateTime, // Absent on re-pins of an already-pinned thread (the existing key wins) // and on pins from clients that predate reordering. pinOrderKey: Schema.optional(TrimmedNonEmptyString), updatedAt: IsoDateTime, }); export const ThreadUnpinnedPayload = Schema.Struct({ threadId: ThreadId, updatedAt: IsoDateTime, }); export const ThreadPinReorderedPayload = Schema.Struct({ threadId: ThreadId, orderKey: TrimmedNonEmptyString, updatedAt: IsoDateTime, }); export const ThreadMetaUpdatedPayload = Schema.Struct({ threadId: ThreadId, // Order updates use this existing event so older clients can ignore the // new field while continuing to decode the event stream. activeOrderKey: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), title: Schema.optional(TrimmedNonEmptyString), /** Intent marker consumed by the title-generation reactor. Keeping this on the existing event lets older clients safely ignore the new field. */ regenerateTitle: Schema.optional(Schema.Literal(true)), /** Title at request time, used to avoid overwriting a later manual rename. */ previousTitle: Schema.optional(TrimmedNonEmptyString), /** Pending state shared with clients. Null clears a matching request. */ titleRegeneration: Schema.optional(Schema.NullOr(ThreadTitleRegeneration)), modelSelection: Schema.optional(ModelSelection), branch: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), worktreePath: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), // No longer produced; kept so persisted events from before // thread.pull-request-linked still decode and replay into the link table. linkedPullRequest: Schema.optional(Schema.NullOr(ThreadLinkedPullRequest)), branchPullRequest: Schema.optional(Schema.NullOr(ThreadLinkedPullRequest)), updatedAt: IsoDateTime, }); export const ThreadPullRequestLinkedPayload = Schema.Struct({ threadId: ThreadId, link: ThreadPullRequestLink, updatedAt: IsoDateTime, }); export type ThreadPullRequestLinkedPayload = typeof ThreadPullRequestLinkedPayload.Type; export const ThreadPullRequestUnlinkedPayload = Schema.Struct({ threadId: ThreadId, ...ThreadPullRequestKey.fields, updatedAt: IsoDateTime, }); export type ThreadPullRequestUnlinkedPayload = typeof ThreadPullRequestUnlinkedPayload.Type; export const ThreadPullRequestSyncedPayload = Schema.Struct({ threadId: ThreadId, ...ThreadPullRequestKey.fields, snapshot: ThreadPullRequestSnapshot, stack: Schema.NullOr(ThreadPullRequestStack), updatedAt: IsoDateTime, }); export type ThreadPullRequestSyncedPayload = typeof ThreadPullRequestSyncedPayload.Type; export const ThreadRuntimeModeSetPayload = Schema.Struct({ threadId: ThreadId, runtimeMode: RuntimeMode, updatedAt: IsoDateTime, }); export const ThreadInteractionModeSetPayload = Schema.Struct({ threadId: ThreadId, interactionMode: ProviderInteractionMode.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_PROVIDER_INTERACTION_MODE)), ), updatedAt: IsoDateTime, }); export const ThreadMessageSentPayload = Schema.Struct({ threadId: ThreadId, messageId: MessageId, role: OrchestrationMessageRole, text: Schema.String, attachments: Schema.optional(Schema.Array(ChatAttachment)), turnId: Schema.NullOr(TurnId), streaming: Schema.Boolean, createdAt: IsoDateTime, updatedAt: IsoDateTime, }); export const ThreadTurnStartRequestedPayload = Schema.Struct({ threadId: ThreadId, messageId: MessageId, modelSelection: Schema.optional(ModelSelection), titleSeed: Schema.optional(TrimmedNonEmptyString), runtimeMode: RuntimeMode.pipe(Schema.withDecodingDefault(Effect.succeed(DEFAULT_RUNTIME_MODE))), interactionMode: ProviderInteractionMode.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_PROVIDER_INTERACTION_MODE)), ), sourceProposedPlan: Schema.optional(SourceProposedPlanReference), createdAt: IsoDateTime, }); export const ThreadTurnInterruptRequestedPayload = Schema.Struct({ threadId: ThreadId, turnId: Schema.optional(TurnId), createdAt: IsoDateTime, }); export const ThreadApprovalResponseRequestedPayload = Schema.Struct({ threadId: ThreadId, requestId: ApprovalRequestId, decision: ProviderApprovalDecision, createdAt: IsoDateTime, }); const ThreadUserInputResponseRequestedPayload = Schema.Struct({ threadId: ThreadId, requestId: ApprovalRequestId, answers: ProviderUserInputAnswers, attachmentsByQuestionId: Schema.optional(UserInputAttachments), createdAt: IsoDateTime, }); export const ThreadCheckpointRevertRequestedPayload = Schema.Struct({ threadId: ThreadId, turnCount: NonNegativeInt, createdAt: IsoDateTime, }); export const ThreadRevertedPayload = Schema.Struct({ threadId: ThreadId, turnCount: NonNegativeInt, }); export const ThreadSessionStopRequestedPayload = Schema.Struct({ threadId: ThreadId, createdAt: IsoDateTime, }); export const ThreadSessionSetPayload = Schema.Struct({ threadId: ThreadId, session: OrchestrationSession, }); export const ThreadProposedPlanUpsertedPayload = Schema.Struct({ threadId: ThreadId, proposedPlan: OrchestrationProposedPlan, }); export const ThreadTurnDiffCompletedPayload = Schema.Struct({ threadId: ThreadId, turnId: TurnId, checkpointTurnCount: NonNegativeInt, checkpointRef: CheckpointRef, status: OrchestrationCheckpointStatus, files: Schema.Array(OrchestrationCheckpointFile), assistantMessageId: Schema.NullOr(MessageId), completedAt: IsoDateTime, }); export const ThreadActivityAppendedPayload = Schema.Struct({ threadId: ThreadId, activity: OrchestrationThreadActivity, }); /** * Which client connection dispatched the command that produced an event. * Stamped by the orchestration engine on client-dispatched commands; absent on * provider/server-originated events and on commands from clients too old to * report it. */ export const OrchestrationClientOrigin = Schema.Struct({ surface: Schema.optional(ClientSurface), appVersion: Schema.optional(TrimmedNonEmptyString), }); export type OrchestrationClientOrigin = typeof OrchestrationClientOrigin.Type; export const OrchestrationEventMetadata = Schema.Struct({ providerTurnId: Schema.optional(TrimmedNonEmptyString), providerItemId: Schema.optional(ProviderItemId), adapterKey: Schema.optional(TrimmedNonEmptyString), requestId: Schema.optional(ApprovalRequestId), ingestedAt: Schema.optional(IsoDateTime), historyImport: Schema.optional(Schema.Boolean), origin: Schema.optional(OrchestrationClientOrigin), }); export type OrchestrationEventMetadata = typeof OrchestrationEventMetadata.Type; const EventBaseFields = { sequence: NonNegativeInt, eventId: EventId, aggregateKind: OrchestrationAggregateKind, aggregateId: Schema.Union([ProjectId, ThreadId]), occurredAt: IsoDateTime, commandId: Schema.NullOr(CommandId), causationEventId: Schema.NullOr(EventId), correlationId: Schema.NullOr(CommandId), metadata: OrchestrationEventMetadata, } as const; export const OrchestrationEvent = Schema.Union([ Schema.Struct({ ...EventBaseFields, type: Schema.Literal("project.created"), payload: ProjectCreatedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("project.meta-updated"), payload: ProjectMetaUpdatedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("project.deleted"), payload: ProjectDeletedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.created"), payload: ThreadCreatedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.deleted"), payload: ThreadDeletedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.archived"), payload: ThreadArchivedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.unarchived"), payload: ThreadUnarchivedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.settled"), payload: ThreadSettledPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.unsettled"), payload: ThreadUnsettledPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.snoozed"), payload: ThreadSnoozedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.unsnoozed"), payload: ThreadUnsnoozedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.pinned"), payload: ThreadPinnedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.unpinned"), payload: ThreadUnpinnedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.pin-reordered"), payload: ThreadPinReorderedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.meta-updated"), payload: ThreadMetaUpdatedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.pull-request-linked"), payload: ThreadPullRequestLinkedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.pull-request-unlinked"), payload: ThreadPullRequestUnlinkedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.pull-request-synced"), payload: ThreadPullRequestSyncedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.runtime-mode-set"), payload: ThreadRuntimeModeSetPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.interaction-mode-set"), payload: ThreadInteractionModeSetPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.message-sent"), payload: ThreadMessageSentPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.turn-start-requested"), payload: ThreadTurnStartRequestedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.turn-interrupt-requested"), payload: ThreadTurnInterruptRequestedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.approval-response-requested"), payload: ThreadApprovalResponseRequestedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.user-input-response-requested"), payload: ThreadUserInputResponseRequestedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.checkpoint-revert-requested"), payload: ThreadCheckpointRevertRequestedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.reverted"), payload: ThreadRevertedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.session-stop-requested"), payload: ThreadSessionStopRequestedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.session-set"), payload: ThreadSessionSetPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.proposed-plan-upserted"), payload: ThreadProposedPlanUpsertedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.turn-diff-completed"), payload: ThreadTurnDiffCompletedPayload, }), Schema.Struct({ ...EventBaseFields, type: Schema.Literal("thread.activity-appended"), payload: ThreadActivityAppendedPayload, }), ]); export type OrchestrationEvent = typeof OrchestrationEvent.Type; export const OrchestrationThreadStreamItem = Schema.Union([ Schema.Struct({ kind: Schema.Literal("synchronized"), }), Schema.Struct({ kind: Schema.Literal("snapshot"), snapshot: OrchestrationThreadDetailSnapshot, }), Schema.Struct({ kind: Schema.Literal("event"), event: OrchestrationEvent, }), ]); export type OrchestrationThreadStreamItem = typeof OrchestrationThreadStreamItem.Type; export const OrchestrationCommandReceiptStatus = Schema.Literals(["accepted", "rejected"]); export type OrchestrationCommandReceiptStatus = typeof OrchestrationCommandReceiptStatus.Type; export const TurnCountRange = Schema.Struct({ fromTurnCount: NonNegativeInt, toTurnCount: NonNegativeInt, }).check( Schema.makeFilter( (input) => input.fromTurnCount <= input.toTurnCount || new SchemaIssue.InvalidValue({ message: "fromTurnCount must be less than or equal to toTurnCount", }), { identifier: "OrchestrationTurnDiffRange" }, ), ); export const ThreadTurnDiff = TurnCountRange.mapFields( Struct.assign({ threadId: ThreadId, diff: Schema.String, }), { unsafePreserveChecks: true }, ); export const ProviderSessionRuntimeStatus = Schema.Literals([ "starting", "running", "stopped", "error", ]); export type ProviderSessionRuntimeStatus = typeof ProviderSessionRuntimeStatus.Type; const ProjectionThreadTurnStatus = Schema.Literals([ "running", "completed", "interrupted", "error", ]); export type ProjectionThreadTurnStatus = typeof ProjectionThreadTurnStatus.Type; const ProjectionCheckpointRow = Schema.Struct({ threadId: ThreadId, turnId: TurnId, checkpointTurnCount: NonNegativeInt, checkpointRef: CheckpointRef, status: OrchestrationCheckpointStatus, files: Schema.Array(OrchestrationCheckpointFile), assistantMessageId: Schema.NullOr(MessageId), completedAt: IsoDateTime, }); export type ProjectionCheckpointRow = typeof ProjectionCheckpointRow.Type; export const ProjectionPendingApprovalStatus = Schema.Literals(["pending", "resolved"]); export type ProjectionPendingApprovalStatus = typeof ProjectionPendingApprovalStatus.Type; export const ProjectionPendingApprovalDecision = Schema.NullOr(ProviderApprovalDecision); export type ProjectionPendingApprovalDecision = typeof ProjectionPendingApprovalDecision.Type; export const DispatchResult = Schema.Struct({ sequence: NonNegativeInt, }); export type DispatchResult = typeof DispatchResult.Type; export const OrchestrationGetTurnDiffInput = TurnCountRange.mapFields( Struct.assign({ threadId: ThreadId, ignoreWhitespace: Schema.optionalKey(Schema.Boolean), }), { unsafePreserveChecks: true }, ); export type OrchestrationGetTurnDiffInput = typeof OrchestrationGetTurnDiffInput.Type; export const OrchestrationGetTurnDiffResult = ThreadTurnDiff; export type OrchestrationGetTurnDiffResult = typeof OrchestrationGetTurnDiffResult.Type; export const OrchestrationGetFullThreadDiffInput = Schema.Struct({ threadId: ThreadId, toTurnCount: NonNegativeInt, ignoreWhitespace: Schema.optionalKey(Schema.Boolean), }); export type OrchestrationGetFullThreadDiffInput = typeof OrchestrationGetFullThreadDiffInput.Type; export const OrchestrationGetFullThreadDiffResult = ThreadTurnDiff; export type OrchestrationGetFullThreadDiffResult = typeof OrchestrationGetFullThreadDiffResult.Type; export const OrchestrationThreadSearchSource = Schema.Literals(["user", "assistant"]); export type OrchestrationThreadSearchSource = typeof OrchestrationThreadSearchSource.Type; // The server's SQLite client is synchronous and single-connection. Bound both // scan input and response size so a search cannot monopolize that connection. export const OrchestrationSearchThreadsInput = Schema.Struct({ query: TrimmedString.check(Schema.isMinLength(2), Schema.isMaxLength(200)), limit: Schema.optionalKey(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 50 }))), }); export type OrchestrationSearchThreadsInput = typeof OrchestrationSearchThreadsInput.Type; export const OrchestrationThreadSearchMatch = Schema.Struct({ threadId: ThreadId, projectId: ProjectId, source: OrchestrationThreadSearchSource, snippet: Schema.String.check(Schema.isMaxLength(240)), messageCreatedAt: Schema.NullOr(IsoDateTime), }); export type OrchestrationThreadSearchMatch = typeof OrchestrationThreadSearchMatch.Type; export const OrchestrationSearchThreadsResult = Schema.Struct({ matches: Schema.Array(OrchestrationThreadSearchMatch), }); export type OrchestrationSearchThreadsResult = typeof OrchestrationSearchThreadsResult.Type; export const OrchestrationGetWorkflowScriptInput = Schema.Struct({ threadId: ThreadId, /** Absolute path from the workflow's runHandles.scriptPath. The server * re-derives containment; the client value is a hint, never trusted. */ scriptPath: TrimmedNonEmptyString, }); export type OrchestrationGetWorkflowScriptInput = typeof OrchestrationGetWorkflowScriptInput.Type; export const OrchestrationGetWorkflowScriptResult = Schema.Struct({ scriptPath: TrimmedNonEmptyString, contents: Schema.String, truncated: Schema.Boolean, }); export type OrchestrationGetWorkflowScriptResult = typeof OrchestrationGetWorkflowScriptResult.Type; const WORKFLOW_SCRIPT_ERROR_MESSAGES = { "invalid-path": "Workflow scripts must be absolute .js paths.", "root-unavailable": "Script root unavailable.", "not-found": "Script not found.", "outside-root": "Script path is outside the workflow scripts root.", "not-js": "Resolved script is not a .js file.", "not-regular-file": "Script is not a regular file.", "changed-during-read": "Script changed between resolution and open.", "read-failed": "Script read failed.", } as const; export class OrchestrationGetWorkflowScriptError extends Schema.TaggedError()( "OrchestrationGetWorkflowScriptError", { reason: Schema.Literals([ "invalid-path", "root-unavailable", "not-found", "outside-root", "not-js", "not-regular-file", "changed-during-read", "read-failed", ]), scriptPath: Schema.String, cause: Schema.optional(Schema.Defect()), }, ) { override get message(): string { return WORKFLOW_SCRIPT_ERROR_MESSAGES[this.reason]; } } export const OrchestrationRpcSchemas = { dispatchCommand: { input: ClientOrchestrationCommand, output: DispatchResult, }, getWorkflowScript: { input: OrchestrationGetWorkflowScriptInput, output: OrchestrationGetWorkflowScriptResult, }, getTurnDiff: { input: OrchestrationGetTurnDiffInput, output: OrchestrationGetTurnDiffResult, }, getFullThreadDiff: { input: OrchestrationGetFullThreadDiffInput, output: OrchestrationGetFullThreadDiffResult, }, searchThreads: { input: OrchestrationSearchThreadsInput, output: OrchestrationSearchThreadsResult, }, getArchivedShellSnapshot: { input: Schema.Struct({}), output: OrchestrationShellSnapshot, }, subscribeThread: { input: OrchestrationSubscribeThreadInput, output: OrchestrationThreadStreamItem, }, subscribeShell: { input: OrchestrationSubscribeShellInput, output: OrchestrationShellStreamItem, }, } as const; export class OrchestrationGetSnapshotError extends Schema.TaggedError()( "OrchestrationGetSnapshotError", { message: TrimmedNonEmptyString, cause: Schema.optional(Schema.Defect()), }, ) {} export class OrchestrationDispatchCommandError extends Schema.TaggedError()( "OrchestrationDispatchCommandError", { message: TrimmedNonEmptyString, cause: Schema.optional(Schema.Defect()), bootstrapThreadDisposition: Schema.optional(Schema.Literal("deleted")), }, ) {} export class OrchestrationGetTurnDiffError extends Schema.TaggedError()( "OrchestrationGetTurnDiffError", { message: TrimmedNonEmptyString, cause: Schema.optional(Schema.Defect()), }, ) {} export class OrchestrationGetFullThreadDiffError extends Schema.TaggedError()( "OrchestrationGetFullThreadDiffError", { message: TrimmedNonEmptyString, cause: Schema.optional(Schema.Defect()), }, ) {} export class OrchestrationSearchThreadsError extends Schema.TaggedError()( "OrchestrationSearchThreadsError", { message: TrimmedNonEmptyString, cause: Schema.optional(Schema.Defect()), }, ) {}