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