1import {
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";
12import { resolveProjectSettings } from "@t3tools/shared/projectSettings";
13import * as Cause from "effect/Cause";
14import * as Console from "effect/Console";
15import * as Context from "effect/Context";
16import * as Crypto from "effect/Crypto";
17import * as DateTime from "effect/DateTime";
18import * as Deferred from "effect/Deferred";
19import * as Effect from "effect/Effect";
20import * as Exit from "effect/Exit";
21import * as Fiber from "effect/Fiber";
22import * as Layer from "effect/Layer";
23import * as Path from "effect/Path";
24import * as Queue from "effect/Queue";
25import * as Ref from "effect/Ref";
26import * as Schema from "effect/Schema";
27
28import * as ServerConfig from "./config.ts";
29import * as ServiceLauncherClient from "./cloud/serviceLauncherClient.ts";
30import { flushCompileCache } from "./compileCache.ts";
31import * as Keybindings from "./keybindings.ts";
32import * as ExternalLauncher from "./process/externalLauncher.ts";
33import * as EffectWorker from "./orchestration-v2/EffectWorker.ts";
34import * as LegacyV1ThreadImporter from "./orchestration-v2/legacy/LegacyV1ThreadImporter.ts";
35import * as Orchestrator from "./orchestration-v2/Orchestrator.ts";
36import * as ProviderRuntimeRecovery from "./orchestration-v2/ProviderRuntimeRecoveryService.ts";
37import * as ProviderSessionManager from "./orchestration-v2/ProviderSessionManager.ts";
38import * as ThreadLaunch from "./orchestration-v2/ThreadLaunchService.ts";
39import * as ThreadManagement from "./orchestration-v2/ThreadManagementService.ts";
40import * as ProjectService from "./project/ProjectService.ts";
41import * as GitVcsDriver from "./vcs/GitVcsDriver.ts";
42import * as ProjectStore from "./orchestration-v2/ProjectStore.ts";
43import * as AgentAwarenessRelay from "./relay/AgentAwarenessRelay.ts";
44import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts";
45import * as ServerSettings from "./serverSettings.ts";
46import { forkParked, forkParkedFiber } from "./serverActivation.ts";
47import * as ServerEnvironment from "./environment/ServerEnvironment.ts";
48import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";
49import {
50 formatHeadlessServeOutput,
51 formatHostForUrl,
52 isWildcardHost,
53 issueHeadlessServeAccessInfo,
54} from "./startupAccess.ts";
55
56export 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
70export 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
81interface QueuedCommand {
82 readonly run: Effect.Effect<void, never>;
83}
84
85type CommandReadinessState = "pending" | "ready" | ServerRuntimeStartupError;
86
87interface 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
96const 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
101export 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
145export const getAutoBootstrapThreadModelSelection = (): ModelSelection => ({
146 instanceId: ProviderInstanceId.make("codex"),
147 model: DEFAULT_MODEL,
148});
149
150interface AutoBootstrapWelcomeTargets {
151 readonly bootstrapProjectId?: ProjectId;
152 readonly bootstrapThreadId?: ThreadId;
153}
154
155export 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
215export 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
226const 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
284const 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
298const 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
315const 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
323interface StartupOptions {
324 readonly activate?: Effect.Effect<void>;
325 readonly awaitAuxiliaryParked?: Effect.Effect<void>;
326 readonly abort?: (error: ServerRuntimeStartupError) => Effect.Effect<void>;
327}
328
329export 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
354export 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
386const 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
658export const layerWithOptions = (options?: StartupOptions) =>
659 Layer.effect(ServerRuntimeStartup, make(options));