Part III · Boot and connectServer activation
Chapter 21source checked

Server composition, activation, and readiness

Effect layers acquire the server graph, while an explicit activation barrier separates listening, parked roots, command admission, and lifecycle readiness.

What this chapter resolves
  • Read the server as a scoped layer graph rather than one startup function.
  • Distinguish an open listener from an activated, command-ready runtime.
  • Trace startup failure and shutdown ownership without inventing transactional guarantees.

Chapter 20 ended with one resolved ServerConfig. It does not get passed to a constructor that imperatively starts everything. runServer launches an Effect layer whose acquisition builds a graph of platform services, application services, routes, long-running roots, and release finalizers. A second mechanism—the activation barrier—controls when already-acquired roots may begin external work.

The server is a scoped graph

Figure 21.1 · Layer acquisition and runtime ownershipsolid arrows provide dependencies; dotted arrows open parked roots
T3 server layer graphDiagram loading

Zoom with the controls, +/−, or Ctrl/⌘ + trackpad scroll. Enable Pan to drag, use two-finger scrolling, or use the arrow keys. 0 fits the diagram; Esc leaves Pan or expanded view.

T3 server layer graph
Text equivalent

ServerConfig selects Bun or Node platform services. Platform and SQLite services support persistence, authentication, settings, orchestration, providers, VCS, checkpoints, terminals, preview, relay, and telemetry. Those application services support HTTP, WebSocket RPC, MCP, and static routes. The HTTP server owns the assembled graph for one Effect scope. An activation deferred is a separate gate shared by long-running roots.

Figure 21.1. Platform services support durable and runtime services; those services support HTTP, WebSocket RPC, MCP, and static routes. Activation crosses the graph separately, after every required root has reported that it is parked.

The graph is broad because the environment server is the execution authority from Chapter 16. Composition still preserves boundaries: each service exposes a typed interface, layers declare requirements, and the launched scope owns acquisition and release. makeServerLayer is the composition root; Layer.launch keeps it alive.

SQLite is acquired before command traffic

The SQLite layer configures a busy timeout, enables foreign keys, selects WAL, and runs a statically ordered migration manifest during acquisition. At this revision the manifest contains 50 migrations. That number describes the implementation, not a compatibility promise for later revisions.

Four signals that sound similar but are not

Distinct synchronization signals in server startup
SignalWhat it provesWhat it does not prove
HTTP listeningthe platform server has bound an addresscommands are accepted or auxiliary roots are active
parked / routes readyeach participating root reached its pre-activation wait pointthe activation gate has opened
command readythe global route barrier and startup command queue may release waiting effectsevery later lifecycle subscriber has observed ready
ready eventactivation and command admission have occurred and startup publishes its final lifecycle payloadall future optional integrations will remain healthy

The listener can therefore exist while every route effect waits behind global middleware; a separate FIFO startup queue also holds WebSocket-dispatched commands. That is intentional: routes need a real bound address before presentation and remote-access setup can be prepared, but external work must not race ahead of a candidate runtime that has not crossed its activation boundary.

apps/server/src/serverRuntimeStartup.ts:514–548 ↗verbatim · typescript · 1911fc48
      ),
    ))
      .filter(
        (binding) =>
          readServerUpdateContinuationTurnId(binding.runtimePayload) !== null &&
          readRuntimePayload(binding.runtimePayload).activeTurnId === null &&
          readRuntimePayload(binding.runtimePayload).continueAfterServerUpdatePrepared === true,
      )
      .map((binding) => binding.threadId),
  );
  const orphanedThreads = threads.filter(
    (thread) =>
      thread.session !== null &&
      (thread.session.status === "starting" ||
        thread.session.status === "running" ||
        thread.session.activeTurnId !== null ||
        (thread.session.status === "ready" && preparedThreadIds.has(thread.id))) &&
      !liveThreadIds.has(thread.id),
  );
 
  for (const thread of orphanedThreads) {
    const session = thread.session;
    if (session === null) {
      continue;
    }
    const binding = yield* directory.getBinding(thread.id).pipe(
      Effect.catchCause((cause) =>
        Cause.hasInterrupts(cause)
          ? Effect.failCause(cause)
          : Effect.logWarning("failed to read orphaned provider session directory binding", {
              threadId: thread.id,
              cause,
            }).pipe(Effect.as(Option.none())),
      ),
    );
Read this as: The prepared boundary occurs only after the listener and auxiliary roots are parked. The launcher trial, base welcome, activation, command gate, and ready event then happen in that order.
Interactive flow

Open the server in the order the code does

Play the one-shot sequence or select any boundary. Notice that listening arrives before activation, while command readiness arrives after it.

1 / 7

Step 1 of 7: Acquire

Effect layersDurable stateAcquire

Layers acquire dependencies and SQLite

The selected platform, database pragmas and migrations, application services, routes, and scoped resources are constructed. Acquisition failure prevents a usable runtime.

  1. 1. Acquire · Layers acquire dependencies and SQLite (Durable state)

    The selected platform, database pragmas and migrations, application services, routes, and scoped resources are constructed. Acquisition failure prevents a usable runtime.

    apps/server/src/server.ts:123–240 ↗apps/server/src/server.ts:242–429 ↗apps/server/src/persistence/Layers/Sqlite.ts:19–41 ↗apps/server/src/persistence/Migrations.ts:15–152 ↗
  2. 2. Park roots · Long-running roots start but await activation (Runtime work)

    The orchestration reactors and provider-session reaper are started inside a dedicated scope; activation-aware children first signal that they are parked.

    apps/server/src/orchestration/Layers/OrchestrationReactor.ts:14–31 ↗apps/server/src/serverActivation.ts:11–25 ↗apps/server/src/serverRuntimeStartup.ts:385–583 ↗
  3. 3. Reconcile · Orphaned provider state is repaired best-effort (Durable state)

    Startup reconciles persisted sessions that cannot still be live after process loss. This precedes welcome publication and command admission.

    apps/server/src/serverRuntimeStartup.ts:385–583 ↗
  4. 4. Listen · HTTP and every auxiliary participant report prepared (Transport boundary)

    The HTTP layer marks its bound listener; runtime state, routes, cloud link, and optional Tailscale roots reach their parked points. Route effects still wait behind the readiness barrier.

    apps/server/src/server.ts:474–689 ↗apps/server/src/server.ts:431–437 ↗apps/server/src/serverRuntimeStartup.ts:385–583 ↗
  5. 5. Prepare trial · The managed launcher evaluates the candidate (Runtime work)

    Only at the prepared boundary does startup ask the service launcher to prepare a managed-update trial and capture its outcome.

    apps/server/src/serverRuntimeStartup.ts:385–583 ↗
  6. 6. Activate · Welcome publishes and the shared gate opens (External side effect)

    Startup publishes the base welcome payload, then resolves the activation deferred. Parked roots may now perform their work and activation-owned side effects may begin.

    apps/server/src/server.ts:474–689 ↗apps/server/src/serverRuntimeStartup.ts:385–583 ↗
  7. 7. Admit · Commands are released, then ready is published (Client state)

    The command gate is signaled before the ready lifecycle event. Startup failures instead fail the command gate and invoke the layer's abort path.

    apps/server/src/serverRuntimeStartup.ts:94–136 ↗apps/server/src/serverRuntimeStartup.ts:385–583 ↗

After activate, the parked fibers are released concurrently. Heartbeat, browser/headless presentation, optional auto-bootstrap welcome, runtime-state persistence, and remote-access work may interleave with command admission and the ready publication. The flow orders the gate operations; it does not serialize all post-activation side effects.

Why park before activating?

forkParked is small but architectural. Its child fiber first fulfills a “parked” deferred and then waits on ServerActivation. The parent can prove all participating roots exist and are waiting before it releases a single shared gate. That prevents one early root from acting against a partially prepared graph.

This is synchronization, not a global transaction. Once activation opens, roots can perform filesystem, provider, Tailscale, cloud, browser, or other side effects with their own failure handling. Effect scopes provide release ownership; they do not magically roll back every effect that crossed an external boundary.

Interactive lab

Inject a startup or shutdown condition

Choose a condition to see what the barrier guarantees and what remains best-effort.

Scenario 1 of 4: Settings warning

Settings warningCaveat

Startup deliberately degrades and continues

Keybinding and settings start errors are caught, logged as warnings, and do not fail the activation sequence.

  • The runtime may become ready with a reported configuration issue.
  • This exception is local to those startup phases, not a blanket ignore policy.
  • Later settings operations still use their typed service errors.
  1. Settings warning · Startup deliberately degrades and continues (Caveat)

    Keybinding and settings start errors are caught, logged as warnings, and do not fail the activation sequence.

    • The runtime may become ready with a reported configuration issue.
    • This exception is local to those startup phases, not a blanket ignore policy.
    • Later settings operations still use their typed service errors.
    apps/server/src/serverRuntimeStartup.ts:385–583 ↗
  2. Before prepared · A required failure prevents activation (Failure path)

    If a non-degraded startup phase fails before the prepared boundary, the startup fiber records a ServerRuntimeStartupError.

    • Command readiness is failed, so waiting command effects fail instead of hanging indefinitely.
    • The abort hook fails the activation deferred.
    • The surrounding server scope owns acquired-resource cleanup.
    apps/server/src/serverRuntimeStartup.ts:94–136 ↗apps/server/src/serverRuntimeStartup.ts:385–583 ↗apps/server/src/server.ts:474–689 ↗
  3. After activation · Individual roots own later resilience (Caveat)

    Activation establishes a common start boundary, not permanent health of every auxiliary integration.

    • Tailscale and cloud-link paths catch and log selected failures.
    • Commands are already admitted after the shared gate opens.
    • Service-specific retry and cleanup policies determine later behavior.
    apps/server/src/server.ts:474–689 ↗apps/server/src/serverRuntimeStartup.ts:385–583 ↗
  4. Scope closes · Release finalizers run under Effect ownership (Expected path)

    Closing the launched scope closes the reactor scope and releases acquired resources such as persisted runtime state and configured Tailscale Serve.

    • Registered finalizers run according to their owning scopes.
    • The composition root does not assemble one production-wide atomic drain across every external side effect.
    • External systems may require their own compensating cleanup.
    apps/server/src/server.ts:474–689 ↗apps/server/src/serverRuntimeStartup.ts:385–583 ↗

Provider, import, review, and device services

The server graph also assembles four control areas that deliberately remain outside the provider adapter itself. Model manifests and provider-auth services manage catalog and sign-in state. Agent-session scanners and importers turn selected native history into durable project/thread records. Pull-request synchronization observes linked reviews without making the Git host part of the orchestration transaction. DeviceService owns local and SSH hosts, targets, sessions, and frame/control traffic.

Keeping these as services preserves the narrow adapter rule: a provider integration translates its runtime, while the environment server owns product policy and access. The authoritative composition is in server.ts and serverRuntimeStartup.ts.

T3
Source-locked editionRead against 859304b78 · 10 Sept 2026
Book search

Find a concept, module, or source path

Type two or more characters.