| 1 | import { |
| 2 | CommandId, |
| 3 | DEFAULT_MODEL, |
| 4 | DEFAULT_PROVIDER_INTERACTION_MODE, |
| 5 | DEFAULT_SERVER_SETTINGS, |
| 6 | type ModelSelection, |
| 7 | type Project, |
| 8 | ProjectId, |
| 9 | ProviderInstanceId, |
| 10 | ThreadId, |
| 11 | } from "@t3tools/contracts"; |
| 12 | import { resolveProjectSettings } from "@t3tools/shared/projectSettings"; |
| 13 | import * as Cause from "effect/Cause"; |
| 14 | import * as Console from "effect/Console"; |
| 15 | import * as Context from "effect/Context"; |
| 16 | import * as Crypto from "effect/Crypto"; |
| 17 | import * as DateTime from "effect/DateTime"; |
| 18 | import * as Deferred from "effect/Deferred"; |
| 19 | import * as Effect from "effect/Effect"; |
| 20 | import * as Exit from "effect/Exit"; |
| 21 | import * as Fiber from "effect/Fiber"; |
| 22 | import * as Layer from "effect/Layer"; |
| 23 | import * as Path from "effect/Path"; |
| 24 | import * as Queue from "effect/Queue"; |
| 25 | import * as Ref from "effect/Ref"; |
| 26 | import * as Schema from "effect/Schema"; |
| 27 | |
| 28 | import * as ServerConfig from "./config.ts"; |
| 29 | import * as ServiceLauncherClient from "./cloud/serviceLauncherClient.ts"; |
| 30 | import { flushCompileCache } from "./compileCache.ts"; |
| 31 | import * as Keybindings from "./keybindings.ts"; |
| 32 | import * as ExternalLauncher from "./process/externalLauncher.ts"; |
| 33 | import * as EffectWorker from "./orchestration-v2/EffectWorker.ts"; |
| 34 | import * as LegacyV1ThreadImporter from "./orchestration-v2/legacy/LegacyV1ThreadImporter.ts"; |
| 35 | import * as Orchestrator from "./orchestration-v2/Orchestrator.ts"; |
| 36 | import * as ProviderRuntimeRecovery from "./orchestration-v2/ProviderRuntimeRecoveryService.ts"; |
| 37 | import * as ProviderSessionManager from "./orchestration-v2/ProviderSessionManager.ts"; |
| 38 | import * as ThreadLaunch from "./orchestration-v2/ThreadLaunchService.ts"; |
| 39 | import * as ThreadManagement from "./orchestration-v2/ThreadManagementService.ts"; |
| 40 | import * as ProjectService from "./project/ProjectService.ts"; |
| 41 | import * as GitVcsDriver from "./vcs/GitVcsDriver.ts"; |
| 42 | import * as ProjectStore from "./orchestration-v2/ProjectStore.ts"; |
| 43 | import * as AgentAwarenessRelay from "./relay/AgentAwarenessRelay.ts"; |
| 44 | import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts"; |
| 45 | import * as ServerSettings from "./serverSettings.ts"; |
| 46 | import { forkParked, forkParkedFiber } from "./serverActivation.ts"; |
| 47 | import * as ServerEnvironment from "./environment/ServerEnvironment.ts"; |
| 48 | import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts"; |
| 49 | import { |
| 50 | formatHeadlessServeOutput, |
| 51 | formatHostForUrl, |
| 52 | isWildcardHost, |
| 53 | issueHeadlessServeAccessInfo, |
| 54 | } from "./startupAccess.ts"; |
| 55 | |
| 56 | export class ServerRuntimeStartupError extends Schema.TaggedError<ServerRuntimeStartupError>()( |
| 57 | "ServerRuntimeStartupError", |
| 58 | { |
| 59 | mode: ServerConfig.RuntimeMode, |
| 60 | host: Schema.NullOr(Schema.String), |
| 61 | port: Schema.Number, |
| 62 | cause: Schema.Defect(), |
| 63 | }, |
| 64 | ) { |
| 65 | override get message(): string { |
| 66 | return "Server runtime startup failed before command readiness."; |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | export class ServerRuntimeStartup extends Context.Service< |
| 71 | ServerRuntimeStartup, |
| 72 | { |
| 73 | readonly awaitCommandReady: Effect.Effect<void, ServerRuntimeStartupError>; |
| 74 | readonly markHttpListening: Effect.Effect<void>; |
| 75 | readonly enqueueCommand: <A, E>( |
| 76 | effect: Effect.Effect<A, E>, |
| 77 | ) => Effect.Effect<A, E | ServerRuntimeStartupError>; |
| 78 | } |
| 79 | >()("t3/serverRuntimeStartup") {} |
| 80 | |
| 81 | interface QueuedCommand { |
| 82 | readonly run: Effect.Effect<void, never>; |
| 83 | } |
| 84 | |
| 85 | type CommandReadinessState = "pending" | "ready" | ServerRuntimeStartupError; |
| 86 | |
| 87 | interface CommandGate { |
| 88 | readonly awaitCommandReady: Effect.Effect<void, ServerRuntimeStartupError>; |
| 89 | readonly signalCommandReady: Effect.Effect<void>; |
| 90 | readonly failCommandReady: (error: ServerRuntimeStartupError) => Effect.Effect<void>; |
| 91 | readonly enqueueCommand: <A, E>( |
| 92 | effect: Effect.Effect<A, E>, |
| 93 | ) => Effect.Effect<A, E | ServerRuntimeStartupError>; |
| 94 | } |
| 95 | |
| 96 | const settleQueuedCommand = <A, E>(deferred: Deferred.Deferred<A, E>, exit: Exit.Exit<A, E>) => |
| 97 | Exit.isSuccess(exit) |
| 98 | ? Deferred.succeed(deferred, exit.value) |
| 99 | : Deferred.failCause(deferred, exit.cause); |
| 100 | |
| 101 | export const makeCommandGate = Effect.gen(function* () { |
| 102 | const commandReady = yield* Deferred.make<void, ServerRuntimeStartupError>(); |
| 103 | const commandQueue = yield* Queue.unbounded<QueuedCommand>(); |
| 104 | const commandReadinessState = yield* Ref.make<CommandReadinessState>("pending"); |
| 105 | |
| 106 | const commandWorker = Effect.forever( |
| 107 | Queue.take(commandQueue).pipe(Effect.flatMap((command) => command.run)), |
| 108 | ); |
| 109 | yield* Effect.forkScoped(commandWorker); |
| 110 | |
| 111 | return { |
| 112 | awaitCommandReady: Deferred.await(commandReady), |
| 113 | signalCommandReady: Effect.gen(function* () { |
| 114 | yield* Ref.set(commandReadinessState, "ready"); |
| 115 | yield* Deferred.succeed(commandReady, undefined).pipe(Effect.orDie); |
| 116 | }), |
| 117 | failCommandReady: (error) => |
| 118 | Effect.gen(function* () { |
| 119 | yield* Ref.set(commandReadinessState, error); |
| 120 | yield* Deferred.fail(commandReady, error).pipe(Effect.orDie); |
| 121 | }), |
| 122 | enqueueCommand: <A, E>(effect: Effect.Effect<A, E>) => |
| 123 | Effect.gen(function* () { |
| 124 | const readinessState = yield* Ref.get(commandReadinessState); |
| 125 | if (readinessState === "ready") { |
| 126 | return yield* effect; |
| 127 | } |
| 128 | if (readinessState !== "pending") { |
| 129 | return yield* readinessState; |
| 130 | } |
| 131 | |
| 132 | const result = yield* Deferred.make<A, E | ServerRuntimeStartupError>(); |
| 133 | yield* Queue.offer(commandQueue, { |
| 134 | run: Deferred.await(commandReady).pipe( |
| 135 | Effect.flatMap(() => effect), |
| 136 | Effect.exit, |
| 137 | Effect.flatMap((exit) => settleQueuedCommand(result, exit)), |
| 138 | ), |
| 139 | }); |
| 140 | return yield* Deferred.await(result); |
| 141 | }), |
| 142 | } satisfies CommandGate; |
| 143 | }); |
| 144 | |
| 145 | export const getAutoBootstrapThreadModelSelection = (): ModelSelection => ({ |
| 146 | instanceId: ProviderInstanceId.make("codex"), |
| 147 | model: DEFAULT_MODEL, |
| 148 | }); |
| 149 | |
| 150 | interface AutoBootstrapWelcomeTargets { |
| 151 | readonly bootstrapProjectId?: ProjectId; |
| 152 | readonly bootstrapThreadId?: ThreadId; |
| 153 | } |
| 154 | |
| 155 | export const autoPullProjects = Effect.fn("autoPullProjects")(function* ( |
| 156 | projects: ReadonlyArray<Pick<Project, "id" | "workspaceRoot" | "autoPull">>, |
| 157 | settings = DEFAULT_SERVER_SETTINGS, |
| 158 | ) { |
| 159 | const git = yield* GitVcsDriver.GitVcsDriver; |
| 160 | const workspaceRoots = [ |
| 161 | ...new Set( |
| 162 | projects |
| 163 | .filter((project) => resolveProjectSettings(settings, project.id).settings.defaultAutoPull) |
| 164 | .map((project) => project.workspaceRoot), |
| 165 | ), |
| 166 | ]; |
| 167 | |
| 168 | yield* Effect.forEach( |
| 169 | workspaceRoots, |
| 170 | (cwd) => |
| 171 | Effect.gen(function* () { |
| 172 | const status = yield* git.statusDetails(cwd); |
| 173 | if ( |
| 174 | !status.isRepo || |
| 175 | !status.isDefaultBranch || |
| 176 | !status.hasUpstream || |
| 177 | status.hasWorkingTreeChanges || |
| 178 | status.aheadCount > 0 |
| 179 | ) { |
| 180 | yield* Effect.logDebug("Skipped automatic project pull", { |
| 181 | cwd, |
| 182 | reason: !status.isRepo |
| 183 | ? "not-a-repository" |
| 184 | : !status.isDefaultBranch |
| 185 | ? "not-on-default-branch" |
| 186 | : !status.hasUpstream |
| 187 | ? "no-upstream" |
| 188 | : status.hasWorkingTreeChanges |
| 189 | ? "working-tree-changes" |
| 190 | : "local-commits", |
| 191 | }); |
| 192 | return; |
| 193 | } |
| 194 | |
| 195 | if (status.behindCount <= 0) return; |
| 196 | |
| 197 | const result = yield* git.pullCurrentBranch(cwd); |
| 198 | yield* Effect.logDebug("Automatic project pull completed", { |
| 199 | cwd, |
| 200 | status: result.status, |
| 201 | refName: result.refName, |
| 202 | }); |
| 203 | }).pipe( |
| 204 | Effect.catch((cause) => |
| 205 | Effect.logWarning("Automatic project pull failed", { |
| 206 | cwd, |
| 207 | cause, |
| 208 | }), |
| 209 | ), |
| 210 | ), |
| 211 | { concurrency: 4, discard: true }, |
| 212 | ); |
| 213 | }); |
| 214 | |
| 215 | export const resolveWelcomeBase = Effect.gen(function* () { |
| 216 | const serverConfig = yield* ServerConfig.ServerConfig; |
| 217 | const segments = serverConfig.cwd.split(/[/\\]/).filter(Boolean); |
| 218 | const projectName = segments[segments.length - 1] ?? "project"; |
| 219 | |
| 220 | return { |
| 221 | cwd: serverConfig.cwd, |
| 222 | projectName, |
| 223 | } as const; |
| 224 | }); |
| 225 | |
| 226 | const resolveAutoBootstrapWelcomeTargets = Effect.gen(function* () { |
| 227 | const crypto = yield* Crypto.Crypto; |
| 228 | const randomUUID = crypto.randomUUIDv4(); |
| 229 | const serverConfig = yield* ServerConfig.ServerConfig; |
| 230 | const projects = yield* ProjectService.ProjectService; |
| 231 | const threads = yield* ThreadManagement.ThreadManagementService; |
| 232 | const threadLaunch = yield* ThreadLaunch.ThreadLaunchService; |
| 233 | const path = yield* Path.Path; |
| 234 | |
| 235 | let bootstrapProjectId: ProjectId | undefined; |
| 236 | let bootstrapThreadId: ThreadId | undefined; |
| 237 | |
| 238 | if (serverConfig.autoBootstrapProjectFromCwd) { |
| 239 | // Project creation has no user model choice; only the bootstrap thread |
| 240 | // gets an automatic selection, and an explicit project default wins. |
| 241 | const threadModelSelection = getAutoBootstrapThreadModelSelection(); |
| 242 | const { project } = yield* projects.bootstrap({ |
| 243 | commandId: CommandId.make(yield* randomUUID), |
| 244 | projectId: ProjectId.make(yield* randomUUID), |
| 245 | title: path.basename(serverConfig.cwd) || "project", |
| 246 | workspaceRoot: serverConfig.cwd, |
| 247 | }); |
| 248 | const shell = yield* threads.getShellSnapshot(); |
| 249 | const existingThread = shell.threads.find( |
| 250 | (thread) => |
| 251 | thread.projectId === project.id && thread.lineage.relationshipToParent !== "subagent", |
| 252 | ); |
| 253 | if (existingThread === undefined) { |
| 254 | const serverSettings = yield* ServerSettings.ServerSettingsService; |
| 255 | const settings = yield* serverSettings.getSettings; |
| 256 | const launched = yield* threadLaunch.launch({ |
| 257 | commandId: CommandId.make(yield* randomUUID), |
| 258 | projectId: project.id, |
| 259 | title: "New thread", |
| 260 | modelSelection: |
| 261 | resolveProjectSettings(settings, project.id, project).settings.defaultModelSelection ?? |
| 262 | threadModelSelection, |
| 263 | interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE, |
| 264 | runtimeMode: resolveProjectSettings(settings, project.id, project).settings |
| 265 | .defaultRuntimeMode, |
| 266 | workspaceStrategy: { type: "root" }, |
| 267 | createdBy: "system", |
| 268 | creationSource: "server", |
| 269 | }); |
| 270 | bootstrapProjectId = project.id; |
| 271 | bootstrapThreadId = launched.threadId; |
| 272 | } else { |
| 273 | bootstrapProjectId = project.id; |
| 274 | bootstrapThreadId = existingThread.id; |
| 275 | } |
| 276 | } |
| 277 | |
| 278 | return { |
| 279 | ...(bootstrapProjectId ? { bootstrapProjectId } : {}), |
| 280 | ...(bootstrapThreadId ? { bootstrapThreadId } : {}), |
| 281 | } satisfies AutoBootstrapWelcomeTargets; |
| 282 | }); |
| 283 | |
| 284 | const resolveStartupBrowserTarget = Effect.gen(function* () { |
| 285 | const serverConfig = yield* ServerConfig.ServerConfig; |
| 286 | const serverAuth = yield* EnvironmentAuth.EnvironmentAuth; |
| 287 | const localUrl = `http://localhost:${serverConfig.port}`; |
| 288 | const bindUrl = |
| 289 | serverConfig.host && !isWildcardHost(serverConfig.host) |
| 290 | ? `http://${formatHostForUrl(serverConfig.host)}:${serverConfig.port}` |
| 291 | : localUrl; |
| 292 | const baseTarget = serverConfig.devUrl?.toString() ?? bindUrl; |
| 293 | return serverConfig.mode === "desktop" |
| 294 | ? baseTarget |
| 295 | : yield* serverAuth.issueStartupPairingUrl(baseTarget); |
| 296 | }); |
| 297 | |
| 298 | const maybeOpenBrowser = (target: string) => |
| 299 | Effect.gen(function* () { |
| 300 | const serverConfig = yield* ServerConfig.ServerConfig; |
| 301 | if (serverConfig.noBrowser) { |
| 302 | return; |
| 303 | } |
| 304 | const externalLauncher = yield* ExternalLauncher.ExternalLauncher; |
| 305 | |
| 306 | yield* externalLauncher.launchBrowser(target).pipe( |
| 307 | Effect.catch(() => |
| 308 | Effect.logInfo("browser auto-open unavailable", { |
| 309 | hint: `Open ${target} in your browser.`, |
| 310 | }), |
| 311 | ), |
| 312 | ); |
| 313 | }); |
| 314 | |
| 315 | const runStartupPhase = <A, E, R>(phase: string, effect: Effect.Effect<A, E, R>) => |
| 316 | effect.pipe( |
| 317 | // Attributes on the span itself, not annotateSpans: an annotation follows |
| 318 | // every fiber forked inside the phase, such as the effect worker, for the |
| 319 | // life of the server. |
| 320 | Effect.withSpan(`server.startup.${phase}`, { attributes: { "startup.phase": phase } }), |
| 321 | ); |
| 322 | |
| 323 | interface StartupOptions { |
| 324 | readonly activate?: Effect.Effect<void>; |
| 325 | readonly awaitAuxiliaryParked?: Effect.Effect<void>; |
| 326 | readonly abort?: (error: ServerRuntimeStartupError) => Effect.Effect<void>; |
| 327 | } |
| 328 | |
| 329 | export const startEffectWorkerWithRelay = Effect.fn( |
| 330 | "ServerRuntimeStartup.startEffectWorkerWithRelay", |
| 331 | )(function* <WorkerContext, RelayContext>(input: { |
| 332 | readonly runWorker: Effect.Effect<void, never, WorkerContext>; |
| 333 | readonly startRelay: Effect.Effect<void, never, RelayContext>; |
| 334 | readonly workerFiberRef: Ref.Ref<Fiber.Fiber<void, never> | null>; |
| 335 | }) { |
| 336 | const workerFiber = yield* forkParkedFiber(input.runWorker); |
| 337 | yield* Ref.set(input.workerFiberRef, workerFiber); |
| 338 | yield* input.startRelay.pipe( |
| 339 | Effect.onExit((exit) => { |
| 340 | if (Exit.isSuccess(exit)) { |
| 341 | return Effect.void; |
| 342 | } |
| 343 | return Ref.getAndSet(input.workerFiberRef, null).pipe( |
| 344 | Effect.flatMap((ownedWorkerFiber) => |
| 345 | ownedWorkerFiber === null |
| 346 | ? Effect.void |
| 347 | : Fiber.interrupt(ownedWorkerFiber).pipe(Effect.asVoid), |
| 348 | ), |
| 349 | ); |
| 350 | }), |
| 351 | ); |
| 352 | }); |
| 353 | |
| 354 | export function runOrderedV2StartupPhases< |
| 355 | Import, |
| 356 | Recovery, |
| 357 | Bootstrap, |
| 358 | ImportError, |
| 359 | RecoveryError, |
| 360 | DelegationError, |
| 361 | WorkerError, |
| 362 | BootstrapError, |
| 363 | ImportContext, |
| 364 | RecoveryContext, |
| 365 | DelegationContext, |
| 366 | WorkerContext, |
| 367 | BootstrapContext, |
| 368 | >(input: { |
| 369 | readonly importLegacyShells: Effect.Effect<Import, ImportError, ImportContext>; |
| 370 | readonly recover: Effect.Effect<Recovery, RecoveryError, RecoveryContext>; |
| 371 | /** Settles delegated tasks whose runs recovery just terminalized. */ |
| 372 | readonly recoverDelegatedTasks: Effect.Effect<void, DelegationError, DelegationContext>; |
| 373 | readonly startEffectWorker: Effect.Effect<void, WorkerError, WorkerContext>; |
| 374 | readonly autoBootstrap: Effect.Effect<Bootstrap, BootstrapError, BootstrapContext>; |
| 375 | }) { |
| 376 | return Effect.gen(function* () { |
| 377 | yield* input.importLegacyShells; |
| 378 | const recovery = yield* input.recover; |
| 379 | yield* input.recoverDelegatedTasks; |
| 380 | yield* input.startEffectWorker; |
| 381 | const bootstrap = yield* input.autoBootstrap; |
| 382 | return { recovery, bootstrap } as const; |
| 383 | }); |
| 384 | } |
| 385 | |
| 386 | const make = (options?: StartupOptions) => |
| 387 | Effect.gen(function* () { |
| 388 | const serverConfig = yield* ServerConfig.ServerConfig; |
| 389 | const keybindings = yield* Keybindings.Keybindings; |
| 390 | const legacyV1ThreadImporter = yield* LegacyV1ThreadImporter.LegacyV1ThreadImporter; |
| 391 | const providerRuntimeRecovery = yield* ProviderRuntimeRecovery.ProviderRuntimeRecoveryService; |
| 392 | const orchestrator = yield* Orchestrator.OrchestratorV2; |
| 393 | const providerSessions = yield* ProviderSessionManager.ProviderSessionManagerV2; |
| 394 | const agentAwarenessRelay = yield* AgentAwarenessRelay.AgentAwarenessRelay; |
| 395 | const lifecycleEvents = yield* ServerLifecycleEvents.ServerLifecycleEvents; |
| 396 | const serverSettings = yield* ServerSettings.ServerSettingsService; |
| 397 | const serverEnvironment = yield* ServerEnvironment.ServerEnvironment; |
| 398 | const crypto = yield* Crypto.Crypto; |
| 399 | const launcher = yield* ServiceLauncherClient.ServiceLauncherClient; |
| 400 | |
| 401 | const commandGate = yield* makeCommandGate; |
| 402 | const httpListening = yield* Deferred.make<void>(); |
| 403 | const effectWorkerFiber = yield* Ref.make<Fiber.Fiber<void, never> | null>(null); |
| 404 | |
| 405 | yield* Effect.addFinalizer(() => |
| 406 | Effect.gen(function* () { |
| 407 | yield* commandGate.failCommandReady( |
| 408 | new ServerRuntimeStartupError({ |
| 409 | mode: serverConfig.mode, |
| 410 | host: serverConfig.host ?? null, |
| 411 | port: serverConfig.port, |
| 412 | cause: "Server runtime is shutting down.", |
| 413 | }), |
| 414 | ); |
| 415 | const workerFiber = yield* Ref.getAndSet(effectWorkerFiber, null); |
| 416 | if (workerFiber !== null) { |
| 417 | yield* Fiber.interrupt(workerFiber).pipe(Effect.ignore); |
| 418 | } |
| 419 | yield* providerRuntimeRecovery.prepareForShutdown.pipe( |
| 420 | Effect.ensuring(providerSessions.shutdown), |
| 421 | ); |
| 422 | const reconciliation = yield* providerRuntimeRecovery.reconcile("shutdown"); |
| 423 | yield* Effect.logInfo("V2 orchestration shutdown reconciliation completed", reconciliation); |
| 424 | }).pipe( |
| 425 | Effect.catchCause((cause) => |
| 426 | Effect.logWarning("V2 orchestration shutdown reconciliation failed", { |
| 427 | cause: Cause.pretty(cause), |
| 428 | }), |
| 429 | ), |
| 430 | ), |
| 431 | ); |
| 432 | |
| 433 | const startup = Effect.gen(function* () { |
| 434 | yield* Effect.logDebug("startup phase: starting keybindings runtime"); |
| 435 | yield* runStartupPhase( |
| 436 | "keybindings.start", |
| 437 | keybindings.start.pipe( |
| 438 | Effect.catch((error) => |
| 439 | Effect.logWarning("failed to start keybindings runtime", { |
| 440 | path: error.configPath, |
| 441 | detail: error.detail, |
| 442 | cause: error.cause, |
| 443 | }), |
| 444 | ), |
| 445 | ), |
| 446 | ); |
| 447 | |
| 448 | yield* Effect.logDebug("startup phase: starting server settings runtime"); |
| 449 | yield* runStartupPhase( |
| 450 | "settings.start", |
| 451 | serverSettings.start.pipe( |
| 452 | Effect.catch((error) => |
| 453 | Effect.logWarning("failed to start server settings runtime", { |
| 454 | path: error.settingsPath, |
| 455 | operation: error.operation, |
| 456 | providerInstanceId: error.providerInstanceId, |
| 457 | environmentVariable: error.environmentVariable, |
| 458 | cause: error.cause, |
| 459 | }), |
| 460 | ), |
| 461 | ), |
| 462 | ); |
| 463 | |
| 464 | const welcomeBase = yield* resolveWelcomeBase; |
| 465 | const environment = yield* serverEnvironment.getDescriptor; |
| 466 | const legacyMigrationThreadCount = yield* legacyV1ThreadImporter.pendingThreadCount; |
| 467 | if (legacyMigrationThreadCount > 0) { |
| 468 | yield* lifecycleEvents.publish({ |
| 469 | version: 1, |
| 470 | type: "legacyThreadMigration", |
| 471 | payload: { |
| 472 | status: "running", |
| 473 | totalThreadCount: legacyMigrationThreadCount, |
| 474 | }, |
| 475 | }); |
| 476 | } |
| 477 | const { recovery, bootstrap: bootstrapTargets } = yield* runOrderedV2StartupPhases({ |
| 478 | importLegacyShells: runStartupPhase( |
| 479 | "orchestration-v2.legacy-v1.import-shells", |
| 480 | legacyV1ThreadImporter.reconcileShells.pipe( |
| 481 | Effect.tap((summary) => |
| 482 | summary.importedThreadCount === 0 |
| 483 | ? Effect.void |
| 484 | : Effect.logInfo("Imported legacy v1 thread shells", summary), |
| 485 | ), |
| 486 | ), |
| 487 | ), |
| 488 | recover: runStartupPhase("orchestration-v2.recovery", providerRuntimeRecovery.recover), |
| 489 | recoverDelegatedTasks: runStartupPhase( |
| 490 | "orchestration-v2.delegated-tasks.recover", |
| 491 | orchestrator.recoverDelegatedTasks, |
| 492 | ), |
| 493 | startEffectWorker: runStartupPhase( |
| 494 | "orchestration-v2.effect-worker.start", |
| 495 | startEffectWorkerWithRelay({ |
| 496 | runWorker: EffectWorker.runDaemon, |
| 497 | startRelay: agentAwarenessRelay.start(), |
| 498 | workerFiberRef: effectWorkerFiber, |
| 499 | }), |
| 500 | ), |
| 501 | autoBootstrap: (serverConfig.autoBootstrapProjectFromCwd |
| 502 | ? runStartupPhase( |
| 503 | "welcome.autobootstrap", |
| 504 | resolveAutoBootstrapWelcomeTargets.pipe(Effect.provideService(Crypto.Crypto, crypto)), |
| 505 | ) |
| 506 | : Effect.succeed({}) |
| 507 | ).pipe(Effect.map((targets): AutoBootstrapWelcomeTargets => targets)), |
| 508 | }); |
| 509 | yield* Effect.logInfo("V2 orchestration recovery completed", recovery); |
| 510 | // Runs after activation: the status check fetches every enabled project's |
| 511 | // remote, and awaiting it here held command readiness for that long. |
| 512 | yield* runStartupPhase( |
| 513 | "projects.auto-pull", |
| 514 | Effect.gen(function* () { |
| 515 | const projects = yield* (yield* ProjectStore.ProjectStoreV2).listShells(); |
| 516 | const settings = yield* serverSettings.getSettings; |
| 517 | yield* autoPullProjects(projects, settings); |
| 518 | }), |
| 519 | ).pipe( |
| 520 | Effect.catch((cause) => |
| 521 | Effect.logWarning("Failed to load projects for automatic pull", { cause }), |
| 522 | ), |
| 523 | forkParked, |
| 524 | ); |
| 525 | |
| 526 | const importPendingTranscripts = legacyV1ThreadImporter.importPendingTranscripts.pipe( |
| 527 | Effect.tap((summary) => |
| 528 | summary.importedThreadCount === 0 |
| 529 | ? Effect.void |
| 530 | : Effect.logInfo("Hydrated legacy v1 thread transcripts", summary), |
| 531 | ), |
| 532 | ); |
| 533 | yield* ( |
| 534 | legacyMigrationThreadCount > 0 |
| 535 | ? importPendingTranscripts.pipe( |
| 536 | Effect.tap(() => |
| 537 | lifecycleEvents.publish({ |
| 538 | version: 1, |
| 539 | type: "legacyThreadMigration", |
| 540 | payload: { |
| 541 | status: "complete", |
| 542 | totalThreadCount: legacyMigrationThreadCount, |
| 543 | }, |
| 544 | }), |
| 545 | ), |
| 546 | ) |
| 547 | : importPendingTranscripts |
| 548 | ).pipe(forkParked); |
| 549 | |
| 550 | yield* forkParked( |
| 551 | Effect.gen(function* () { |
| 552 | if (serverConfig.startupPresentation === "headless") { |
| 553 | yield* Effect.logDebug("startup phase: headless access info"); |
| 554 | const accessInfo = yield* issueHeadlessServeAccessInfo(); |
| 555 | yield* runStartupPhase( |
| 556 | "headless.output", |
| 557 | Console.log(formatHeadlessServeOutput(accessInfo)), |
| 558 | ); |
| 559 | } else { |
| 560 | yield* Effect.logDebug("startup phase: browser open check"); |
| 561 | const startupBrowserTarget = yield* resolveStartupBrowserTarget; |
| 562 | if (serverConfig.mode !== "desktop") { |
| 563 | yield* Effect.logInfo( |
| 564 | "Authentication required. Open T3 Code using the pairing URL.", |
| 565 | ).pipe(Effect.annotateLogs({ pairingUrl: startupBrowserTarget })); |
| 566 | } |
| 567 | yield* runStartupPhase("browser.open", maybeOpenBrowser(startupBrowserTarget)); |
| 568 | } |
| 569 | }), |
| 570 | ); |
| 571 | |
| 572 | yield* Effect.logDebug("startup phase: waiting for http listener"); |
| 573 | yield* runStartupPhase("http.wait", Deferred.await(httpListening)); |
| 574 | yield* runStartupPhase( |
| 575 | "auxiliary-roots.parked", |
| 576 | options?.awaitAuxiliaryParked ?? Effect.void, |
| 577 | ); |
| 578 | |
| 579 | const updateOutcome = yield* launcher.prepareTrial; |
| 580 | |
| 581 | yield* Effect.logDebug("startup phase: publishing welcome event", { |
| 582 | environmentId: environment.environmentId, |
| 583 | cwd: welcomeBase.cwd, |
| 584 | projectName: welcomeBase.projectName, |
| 585 | bootstrapProjectId: bootstrapTargets.bootstrapProjectId, |
| 586 | bootstrapThreadId: bootstrapTargets.bootstrapThreadId, |
| 587 | }); |
| 588 | yield* runStartupPhase( |
| 589 | "welcome.publish", |
| 590 | lifecycleEvents.publish({ |
| 591 | version: 1, |
| 592 | type: "welcome", |
| 593 | payload: { |
| 594 | environment, |
| 595 | ...welcomeBase, |
| 596 | ...bootstrapTargets, |
| 597 | }, |
| 598 | }), |
| 599 | ); |
| 600 | |
| 601 | yield* options?.activate ?? Effect.void; |
| 602 | yield* Effect.logDebug("Accepting commands"); |
| 603 | yield* commandGate.signalCommandReady; |
| 604 | yield* Effect.logDebug("startup phase: publishing ready event"); |
| 605 | yield* runStartupPhase( |
| 606 | "ready.publish", |
| 607 | lifecycleEvents.publish({ |
| 608 | version: 1, |
| 609 | type: "ready", |
| 610 | payload: { |
| 611 | at: DateTime.formatIso(yield* DateTime.now), |
| 612 | environment, |
| 613 | ...(updateOutcome === undefined ? {} : { updateOutcome }), |
| 614 | }, |
| 615 | }), |
| 616 | ); |
| 617 | yield* Effect.logDebug("startup phase: complete"); |
| 618 | yield* flushCompileCache; |
| 619 | }).pipe( |
| 620 | Effect.withSpan("server.startup", { |
| 621 | kind: "server", |
| 622 | root: true, |
| 623 | attributes: { |
| 624 | "server.mode": serverConfig.mode, |
| 625 | "server.port": serverConfig.port, |
| 626 | "server.host": serverConfig.host ?? "default", |
| 627 | }, |
| 628 | }), |
| 629 | ); |
| 630 | |
| 631 | yield* Effect.forkScoped( |
| 632 | Effect.exit(startup).pipe( |
| 633 | Effect.flatMap((startupExit) => { |
| 634 | if (Exit.isSuccess(startupExit)) return Effect.void; |
| 635 | const error = new ServerRuntimeStartupError({ |
| 636 | mode: serverConfig.mode, |
| 637 | host: serverConfig.host ?? null, |
| 638 | port: serverConfig.port, |
| 639 | cause: startupExit.cause, |
| 640 | }); |
| 641 | return Effect.logError("server runtime startup failed", { |
| 642 | cause: Cause.pretty(startupExit.cause), |
| 643 | }).pipe( |
| 644 | Effect.andThen(commandGate.failCommandReady(error)), |
| 645 | Effect.andThen(options?.abort?.(error) ?? Effect.void), |
| 646 | ); |
| 647 | }), |
| 648 | ), |
| 649 | ); |
| 650 | |
| 651 | return { |
| 652 | awaitCommandReady: commandGate.awaitCommandReady, |
| 653 | markHttpListening: Deferred.succeed(httpListening, undefined), |
| 654 | enqueueCommand: commandGate.enqueueCommand, |
| 655 | } satisfies ServerRuntimeStartup["Service"]; |
| 656 | }); |
| 657 | |
| 658 | export const layerWithOptions = (options?: StartupOptions) => |
| 659 | Layer.effect(ServerRuntimeStartup, make(options)); |