| 1 | import * as Effect from "effect/Effect"; |
| 2 | import * as Schema from "effect/Schema"; |
| 3 | |
| 4 | import { |
| 5 | EnvironmentId, |
| 6 | ForwardCompatibleOptional, |
| 7 | ProjectId, |
| 8 | ThreadId, |
| 9 | TrimmedNonEmptyString, |
| 10 | } from "./baseSchemas.ts"; |
| 11 | |
| 12 | /** Wire version for orchestration snapshots, streams, commands, and RPC payloads. */ |
| 13 | export const ORCHESTRATION_PROTOCOL_VERSION = 2; |
| 14 | export const ORCHESTRATION_PROTOCOL_VERSION_TEXT = "2"; |
| 15 | export const ORCHESTRATION_PROTOCOL_QUERY_PARAM = "orchestrationProtocol"; |
| 16 | export const ORCHESTRATION_PROTOCOL_HEADER = "x-t3-orchestration-protocol"; |
| 17 | |
| 18 | export const ExecutionEnvironmentPlatformOs = Schema.Literals([ |
| 19 | "darwin", |
| 20 | "linux", |
| 21 | "windows", |
| 22 | "unknown", |
| 23 | ]); |
| 24 | export type ExecutionEnvironmentPlatformOs = typeof ExecutionEnvironmentPlatformOs.Type; |
| 25 | |
| 26 | export const ExecutionEnvironmentPlatformArch = Schema.Literals(["arm64", "x64", "other"]); |
| 27 | export type ExecutionEnvironmentPlatformArch = typeof ExecutionEnvironmentPlatformArch.Type; |
| 28 | |
| 29 | /** |
| 30 | * The curated set of machine shapes and OS identities an environment can wear as its icon. |
| 31 | * Servers detect one from the hardware they run on (`platform.machine`), and |
| 32 | * the `environmentIcon` server setting lets a user pick one instead. |
| 33 | */ |
| 34 | export const ENVIRONMENT_MACHINE_KINDS = [ |
| 35 | "server", |
| 36 | "cloud", |
| 37 | "linux", |
| 38 | "desktop", |
| 39 | "laptop", |
| 40 | "mac-mini", |
| 41 | "mac-studio", |
| 42 | ] as const; |
| 43 | export const EnvironmentMachineKind = Schema.Literals(ENVIRONMENT_MACHINE_KINDS); |
| 44 | export type EnvironmentMachineKind = typeof EnvironmentMachineKind.Type; |
| 45 | export const isEnvironmentMachineKind = Schema.is(EnvironmentMachineKind); |
| 46 | |
| 47 | export const ExecutionEnvironmentPlatform = Schema.Struct({ |
| 48 | os: ExecutionEnvironmentPlatformOs, |
| 49 | arch: ExecutionEnvironmentPlatformArch, |
| 50 | /** Hardware shape detected at startup. Absent when the host gives no usable |
| 51 | signal (containers, Windows, unknown DMI), on servers that predate it, or |
| 52 | when a newer server names a kind this build cannot draw. */ |
| 53 | machine: ForwardCompatibleOptional(EnvironmentMachineKind), |
| 54 | }); |
| 55 | |
| 56 | /** |
| 57 | * Where a new thread runs: the project's current checkout ("local") or a |
| 58 | * fresh git worktree ("worktree"). Lives here (not settings.ts) so |
| 59 | * orchestration contracts can reference it without an import cycle. |
| 60 | */ |
| 61 | export const ThreadEnvMode = Schema.Literals(["local", "worktree"]); |
| 62 | export type ThreadEnvMode = typeof ThreadEnvMode.Type; |
| 63 | |
| 64 | /** |
| 65 | * How a new worktree populates git submodules: every nested level, only the |
| 66 | * ones this repository declares, or not at all. |
| 67 | */ |
| 68 | export const WorktreeSubmodules = Schema.Literals(["recursive", "top-level", "none"]); |
| 69 | export type WorktreeSubmodules = typeof WorktreeSubmodules.Type; |
| 70 | export type ExecutionEnvironmentPlatform = typeof ExecutionEnvironmentPlatform.Type; |
| 71 | |
| 72 | /** How a server can replace itself with another version when asked over RPC. |
| 73 | New servers only advertise the stable launcher-backed "boot-service" path; |
| 74 | "respawn" remains decodable for compatibility with older servers. |
| 75 | "desktop-app" means the supervising desktop app updated and relaunched |
| 76 | itself, bringing the server back with it. */ |
| 77 | export const ServerSelfUpdateMethod = Schema.Literals(["boot-service", "respawn", "desktop-app"]); |
| 78 | export type ServerSelfUpdateMethod = typeof ServerSelfUpdateMethod.Type; |
| 79 | |
| 80 | /** Proven ownership for a manual update; unknown installs omit this descriptor. */ |
| 81 | export const ServerInstallation = Schema.Union([ |
| 82 | Schema.Struct({ kind: Schema.Literals(["npx", "pnpm-dlx", "bunx"]) }), |
| 83 | Schema.Struct({ kind: Schema.Literal("npm-global"), prefix: TrimmedNonEmptyString }), |
| 84 | ]); |
| 85 | export type ServerInstallation = typeof ServerInstallation.Type; |
| 86 | |
| 87 | /** What update path a client should offer for a server: one of the RPC |
| 88 | self-update methods above, or "desktop-managed" when the backend's |
| 89 | version belongs to the T3 Code desktop app supervising it — updating the |
| 90 | app on that machine is the only way to update the server. */ |
| 91 | export const ServerSelfUpdateCapability = Schema.Literals([ |
| 92 | "boot-service", |
| 93 | "respawn", |
| 94 | "desktop-managed", |
| 95 | ]); |
| 96 | export type ServerSelfUpdateCapability = typeof ServerSelfUpdateCapability.Type; |
| 97 | |
| 98 | export const ExecutionEnvironmentCapabilities = Schema.Struct({ |
| 99 | uiSettingsSync: Schema.optionalKey(Schema.Boolean), |
| 100 | repositoryIdentity: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), |
| 101 | connectionProbe: Schema.optionalKey(Schema.Boolean), |
| 102 | /** Missing on older servers, which still accept inline image attachments. */ |
| 103 | attachmentUploads: Schema.optionalKey(Schema.Boolean), |
| 104 | /** Uploaded files may accompany question answers. */ |
| 105 | questionAttachments: Schema.optionalKey(Schema.Boolean), |
| 106 | /** Missing on servers that only accept image attachments. */ |
| 107 | fileAttachments: Schema.optionalKey( |
| 108 | Schema.Struct({ |
| 109 | maxUploadBytes: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)), |
| 110 | }), |
| 111 | ), |
| 112 | /** Server exposes the pull-request list, detail, activity, diff, and mutation APIs. Absent on |
| 113 | servers from before the pull-request workspace shipped, so clients must not probe them. */ |
| 114 | pullRequests: Schema.optionalKey(Schema.Boolean), |
| 115 | pullRequestChecks: Schema.optionalKey(Schema.Boolean), |
| 116 | /** Server understands canonical inline context links plus their message context records. |
| 117 | Absent on servers from before inline context shipped, which drop the records and forward |
| 118 | the links as literal text -- so a client must serialize context the legacy way for them. */ |
| 119 | inlineMessageContext: Schema.optionalKey(Schema.Boolean), |
| 120 | /** Server rejects required worktrees instead of falling back to the project checkout. */ |
| 121 | requiredWorktreeBootstrap: Schema.optionalKey(Schema.Boolean), |
| 122 | /** Server understands thread.settle / thread.unsettle commands. Absent on |
| 123 | pre-settlement servers, so clients treat missing as unsupported and |
| 124 | never send the commands under version skew. */ |
| 125 | threadSettlement: Schema.optionalKey(Schema.Boolean), |
| 126 | /** Server evaluates merge and inactivity settlement without a client. */ |
| 127 | threadAutoSettlement: Schema.optionalKey(Schema.Boolean), |
| 128 | storageCleanup: Schema.optionalKey(Schema.Boolean), |
| 129 | storageCleanupRun: Schema.optionalKey(Schema.Boolean), |
| 130 | /** Server checks OTLP endpoints for the telemetry export settings. */ |
| 131 | otlpEndpointCheck: Schema.optionalKey(Schema.Boolean), |
| 132 | projectWorktreeCleanup: Schema.optionalKey(Schema.Boolean), |
| 133 | settledWorktreeCleanup: Schema.optionalKey(Schema.Boolean), |
| 134 | /** Server honors the `worktreesDirectory` setting. */ |
| 135 | worktreesDirectory: Schema.optionalKey(Schema.Boolean), |
| 136 | /** Server can exclude PR-linked threads from inactivity settlement. */ |
| 137 | threadAutoSettlementScope: Schema.optionalKey(Schema.Boolean), |
| 138 | /** Server persists the opt-in for continuing interrupted threads after restarts. */ |
| 139 | threadRestartContinuation: Schema.optionalKey(Schema.Boolean), |
| 140 | /** Server resolves `projectSettingsOverrides`; older servers ignore the key. */ |
| 141 | projectSettingsOverrides: Schema.optionalKey(Schema.Boolean), |
| 142 | /** Server resolves a project's `providerInstances` override entry. Older |
| 143 | servers decode the key away (unknown fields are stripped, not |
| 144 | rejected), so the per-project provider switch would silently do |
| 145 | nothing; clients hide it instead of sending a patch that goes nowhere. */ |
| 146 | projectProviderInstanceOverrides: Schema.optionalKey(Schema.Boolean), |
| 147 | /** Server understands thread.snooze / thread.unsnooze commands. Same |
| 148 | version-skew contract as threadSettlement. */ |
| 149 | threadSnooze: Schema.optionalKey(Schema.Boolean), |
| 150 | /** Server streams themes an environment publishes. Absent on servers from |
| 151 | before environment themes shipped, which never emit the events -- so a |
| 152 | client reconnecting to one must drop published themes rather than keep |
| 153 | showing a set nothing will ever update. */ |
| 154 | environmentThemes: Schema.optionalKey(Schema.Boolean), |
| 155 | /** Server streams quota from configured usage-limit sources. Same |
| 156 | version-skew contract as environmentThemes. */ |
| 157 | usageLimitSources: Schema.optionalKey(Schema.Boolean), |
| 158 | /** Server persists custom model rates and applies them to usage summaries. */ |
| 159 | usagePriceOverrides: Schema.optionalKey(Schema.Boolean), |
| 160 | /** Server persists model mappings and folds mapped usage into the target model. */ |
| 161 | usageModelAliases: Schema.optionalKey(Schema.Boolean), |
| 162 | /** Server understands thread.pin / thread.unpin commands. Same |
| 163 | version-skew contract as threadSettlement. */ |
| 164 | threadPinning: Schema.optionalKey(Schema.Boolean), |
| 165 | /** Server understands thread.pin.reorder (and orderKey on thread.pin). |
| 166 | Same version-skew contract as threadSettlement. */ |
| 167 | threadPinReorder: Schema.optionalKey(Schema.Boolean), |
| 168 | /** Server persists manual Active order through thread.active.reorder. */ |
| 169 | threadActiveReorder: Schema.optionalKey(Schema.Boolean), |
| 170 | /** Server understands thread.auto-settle.set (per-thread auto-settle off). |
| 171 | Same version-skew contract as threadSettlement. */ |
| 172 | threadAutoSettleOptOut: Schema.optionalKey(Schema.Boolean), |
| 173 | /** Server understands regenerateTitle on thread.meta.update. Absent on |
| 174 | older servers, so clients hide the action instead of sending it. */ |
| 175 | threadTitleRegeneration: Schema.optionalKey(Schema.Boolean), |
| 176 | /** Server understands thread.visit / thread.mark-unread commands and |
| 177 | projects lastVisitedAt on thread shells. Same version-skew contract as |
| 178 | threadSettlement: clients keep their local visited state against |
| 179 | servers that lack this. */ |
| 180 | threadVisitedTracking: Schema.optionalKey(Schema.Boolean), |
| 181 | /** Server persists a pull request reference on thread.meta.update. */ |
| 182 | threadPullRequestLinking: Schema.optionalKey(Schema.Boolean), |
| 183 | /** Server resolves message delivery and model-selection context and validates |
| 184 | identified rollback readiness. Clients retain projection-based command |
| 185 | shaping and validation when this is absent. */ |
| 186 | serverResolvedCommandContext: Schema.optionalKey(Schema.Boolean), |
| 187 | threadPullRequests: Schema.optionalKey(Schema.Boolean), |
| 188 | /** Server understands thread.pull-request.watch and wakes agents on pull request changes. */ |
| 189 | threadPullRequestWatch: Schema.optionalKey(Schema.Boolean), |
| 190 | pullRequestStackActions: Schema.optionalKey(Schema.Boolean), |
| 191 | /** The update path clients should offer for this server. Absent on |
| 192 | servers that must be relaunched manually (dev checkouts, Windows |
| 193 | foreground runs, pre-update servers). */ |
| 194 | serverSelfUpdate: Schema.optionalKey(ServerSelfUpdateCapability), |
| 195 | /** Manual commands must update this install, not the host's default global prefix. */ |
| 196 | serverInstallation: ForwardCompatibleOptional(ServerInstallation), |
| 197 | /** Server can stream self-update progress before acknowledging the |
| 198 | restart. Clients fall back to server.updateServer when absent. */ |
| 199 | serverSelfUpdateProgress: Schema.optionalKey(Schema.Boolean), |
| 200 | /** Server can durably mark running provider turns before a self-update and |
| 201 | continue them after the replacement process starts. */ |
| 202 | serverUpdateThreadContinuation: Schema.optionalKey(Schema.Boolean), |
| 203 | /** Agent-activity publishes (push notifications and Live Activities) |
| 204 | currently leave this environment: the publish opt-in is enabled and the |
| 205 | relay link credentials exist. Clients skip seeding a Live Activity when |
| 206 | this is false — no update would ever repaint it. Absent on older |
| 207 | servers, which may still publish, so only an explicit false skips. */ |
| 208 | agentActivityPublishing: Schema.optionalKey(Schema.Boolean), |
| 209 | /** Server runs repository clones for new projects in the background and |
| 210 | streams their progress (`projectClone.*`, `subscribeProjectClones`). |
| 211 | Absent on older servers, where clients must clone with the blocking |
| 212 | `sourceControl.cloneRepository` call instead. */ |
| 213 | projectCloneTracking: Schema.optionalKey(Schema.Boolean), |
| 214 | /** Server detects `platform.machine` and persists the `environmentIcon` |
| 215 | setting. Older servers drop the key on write, so clients show the |
| 216 | picker inert rather than offering a choice that would never stick. */ |
| 217 | environmentIcon: Schema.optionalKey(Schema.Boolean), |
| 218 | /** The desktop app supervising this server can be driven over RPC: |
| 219 | server.updateServer runs its check -> download -> relaunch. Absent on |
| 220 | desktop servers whose app predates the remote trigger, where clients |
| 221 | must keep telling the user to update the app on that machine. */ |
| 222 | desktopAppUpdate: Schema.optionalKey(Schema.Boolean), |
| 223 | /** Server hosts preview tabs in its own headless Chromium (`runtime: |
| 224 | "server"`) and streams them over `/api/preview-stream`. Clients |
| 225 | without a local browser runtime open server tabs here. */ |
| 226 | serverBrowser: Schema.optionalKey(Schema.Boolean), |
| 227 | }); |
| 228 | export type ExecutionEnvironmentCapabilities = typeof ExecutionEnvironmentCapabilities.Type; |
| 229 | |
| 230 | export const ExecutionEnvironmentDescriptor = Schema.Struct({ |
| 231 | environmentId: EnvironmentId, |
| 232 | label: TrimmedNonEmptyString, |
| 233 | platform: ExecutionEnvironmentPlatform, |
| 234 | serverVersion: TrimmedNonEmptyString, |
| 235 | /** Absent on hosts from before explicit orchestration protocol negotiation. */ |
| 236 | orchestrationProtocolVersion: Schema.optionalKey(Schema.Int), |
| 237 | capabilities: ExecutionEnvironmentCapabilities, |
| 238 | }); |
| 239 | export type ExecutionEnvironmentDescriptor = typeof ExecutionEnvironmentDescriptor.Type; |
| 240 | |
| 241 | export const RepositoryIdentityLocator = Schema.Struct({ |
| 242 | source: Schema.Literal("git-remote"), |
| 243 | remoteName: TrimmedNonEmptyString, |
| 244 | remoteUrl: TrimmedNonEmptyString, |
| 245 | }); |
| 246 | export type RepositoryIdentityLocator = typeof RepositoryIdentityLocator.Type; |
| 247 | |
| 248 | /** |
| 249 | * The checkout's own remote when it names a different repository than the canonical one, such as |
| 250 | * a fork that tracks its upstream. Clients group and label by it so a fork stays distinct from the |
| 251 | * repository it forked, while pull request features keep the canonical identity. |
| 252 | */ |
| 253 | export const RepositoryOrigin = Schema.Struct({ |
| 254 | canonicalKey: TrimmedNonEmptyString, |
| 255 | displayName: Schema.optionalKey(TrimmedNonEmptyString), |
| 256 | }); |
| 257 | export type RepositoryOrigin = typeof RepositoryOrigin.Type; |
| 258 | |
| 259 | export const RepositoryIdentity = Schema.Struct({ |
| 260 | canonicalKey: TrimmedNonEmptyString, |
| 261 | locator: RepositoryIdentityLocator, |
| 262 | /** Repository browser URL resolved from the server's configured hosting account. */ |
| 263 | webUrl: Schema.optionalKey(TrimmedNonEmptyString), |
| 264 | rootPath: Schema.optionalKey(TrimmedNonEmptyString), |
| 265 | displayName: Schema.optionalKey(TrimmedNonEmptyString), |
| 266 | provider: Schema.optionalKey(TrimmedNonEmptyString), |
| 267 | owner: Schema.optionalKey(TrimmedNonEmptyString), |
| 268 | name: Schema.optionalKey(TrimmedNonEmptyString), |
| 269 | origin: Schema.optionalKey(RepositoryOrigin), |
| 270 | }); |
| 271 | export type RepositoryIdentity = typeof RepositoryIdentity.Type; |
| 272 | |
| 273 | /** Key clients group checkouts by: a fork's own remote, otherwise the canonical repository. */ |
| 274 | export function repositoryGroupingKeyOf(identity: RepositoryIdentity): string { |
| 275 | return identity.origin?.canonicalKey ?? identity.canonicalKey; |
| 276 | } |
| 277 | |
| 278 | /** Label clients show for a checkout's repository, matching `repositoryGroupingKeyOf`. */ |
| 279 | export function repositoryGroupingDisplayNameOf(identity: RepositoryIdentity): string | undefined { |
| 280 | return identity.origin |
| 281 | ? (identity.origin.displayName ?? identity.origin.canonicalKey) |
| 282 | : identity.displayName; |
| 283 | } |
| 284 | |
| 285 | export const ScopedProjectRef = Schema.Struct({ |
| 286 | environmentId: EnvironmentId, |
| 287 | projectId: ProjectId, |
| 288 | }); |
| 289 | export type ScopedProjectRef = typeof ScopedProjectRef.Type; |
| 290 | |
| 291 | export const ScopedThreadRef = Schema.Struct({ |
| 292 | environmentId: EnvironmentId, |
| 293 | threadId: ThreadId, |
| 294 | }); |
| 295 | export type ScopedThreadRef = typeof ScopedThreadRef.Type; |