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";
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: <driver-id>` 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<string, unknown> = {
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<string, unknown> = {
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<string>(
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 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)),
});
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),
),
});
export type UploadChatImageAttachment = typeof UploadChatImageAttachment.Type;
export const ChatAttachment = Schema.Union([
ChatImageAttachment,
ChatFileAttachment,
ChatUnknownAttachment,
]);
export type ChatAttachment = typeof ChatAttachment.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;
export const ThreadLinkedPullRequest = Schema.Struct({
projectId: ProjectId,
repository: TrimmedNonEmptyString,
number: PositiveInt,
url: TrimmedNonEmptyString,
});
export type ThreadLinkedPullRequest = typeof ThreadLinkedPullRequest.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)),
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)),
// 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)),
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)),
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,
});
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 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 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,
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,
ThreadMetaUpdateCommand,
ThreadRuntimeModeSetCommand,
ThreadInteractionModeSetCommand,
ThreadTurnStartCommand,
ThreadTurnInterruptCommand,
ThreadApprovalRespondCommand,
ThreadUserInputRespondCommand,
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,
ThreadMetaUpdateCommand,
ThreadRuntimeModeSetCommand,
ThreadInteractionModeSetCommand,
ClientThreadTurnStartCommand,
ThreadTurnInterruptCommand,
ThreadApprovalRespondCommand,
ThreadUserInputRespondCommand,
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 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 InternalOrchestrationCommand = Schema.Union([
ThreadAutoSettleCommand,
ThreadSessionSetCommand,
ThreadMessageAssistantDeltaCommand,
ThreadMessageAssistantCompleteCommand,
ThreadProposedPlanUpsertCommand,
ThreadTurnDiffCompleteCommand,
ThreadActivityAppendCommand,
ThreadRevertCompleteCommand,
ThreadTitleRegenerationCompleteCommand,
]);
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.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,
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)),
linkedPullRequest: Schema.optional(Schema.NullOr(ThreadLinkedPullRequest)),
updatedAt: IsoDateTime,
});
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,
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),
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.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.TaggedErrorClass<OrchestrationGetWorkflowScriptError>()(
"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.TaggedErrorClass<OrchestrationGetSnapshotError>()(
"OrchestrationGetSnapshotError",
{
message: TrimmedNonEmptyString,
cause: Schema.optional(Schema.Defect()),
},
) {}
export class OrchestrationDispatchCommandError extends Schema.TaggedErrorClass<OrchestrationDispatchCommandError>()(
"OrchestrationDispatchCommandError",
{
message: TrimmedNonEmptyString,
cause: Schema.optional(Schema.Defect()),
bootstrapThreadDisposition: Schema.optional(Schema.Literal("deleted")),
},
) {}
export class OrchestrationGetTurnDiffError extends Schema.TaggedErrorClass<OrchestrationGetTurnDiffError>()(
"OrchestrationGetTurnDiffError",
{
message: TrimmedNonEmptyString,
cause: Schema.optional(Schema.Defect()),
},
) {}
export class OrchestrationGetFullThreadDiffError extends Schema.TaggedErrorClass<OrchestrationGetFullThreadDiffError>()(
"OrchestrationGetFullThreadDiffError",
{
message: TrimmedNonEmptyString,
cause: Schema.optional(Schema.Defect()),
},
) {}
export class OrchestrationSearchThreadsError extends Schema.TaggedErrorClass<OrchestrationSearchThreadsError>()(
"OrchestrationSearchThreadsError",
{
message: TrimmedNonEmptyString,
cause: Schema.optional(Schema.Defect()),
},
) {}