Skip to content

feat: expose background service lifecycle#36895

Merged
kitlangton merged 16 commits into
v2from
service-lifecycle-v2
Jul 14, 2026
Merged

feat: expose background service lifecycle#36895
kitlangton merged 16 commits into
v2from
service-lifecycle-v2

Conversation

@kitlangton

@kitlangton kitlangton commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

What

Give the V2 shared background service an observable, process-held lifecycle instead of treating registration as proof that the full application is ready.

  • Elect exactly one channel-scoped owner with a process-held POSIX flock or Windows named pipe.
  • Bind and register an authenticated lifecycle shell before application initialization.
  • Expose starting, ready, stopping, and latched failed health states, plus client-only discovery states.
  • Reconnect existing TUIs indefinitely without enforcing the installed version or activating replacement.
  • Replace fresh-launch version mismatches through an exact-instance graceful stop before signal fallback.
  • Show semantic starting, updating, failed, and unresponsive reconnect copy instead of transport diagnostics.

Before / After

Before: registration could point at a stale, booting, or failed process while clients treated it as a ready endpoint. Concurrent launches used a renewable file lease, replacement could race with a different instance, and a reconnecting TUI could exhaust retries or participate in replacement. A deterministic startup failure could become a client-driven respawn loop.

After: ownership is held by the process for its full lifetime. The winner binds an authenticated shell, publishes the same instance identity used by health, and only transitions to ready after the application layer is built. Startup failure remains bound as one actionable failed state. Reconnects wait for the current owner; only fresh version-enforcing launches request exact-instance replacement.

How

  • packages/core/src/util/process-lock.ts owns cross-platform process lifetime locking, including process-death and suspension coverage.
  • packages/protocol/src/groups/health.ts defines the lifecycle health and exact-instance stop contracts; generated Effect and Promise clients are updated from that API.
  • packages/server/src/process.ts binds one stable HTTP dispatcher before boot, publishes through onListen, activates the application atomically, fences shutdown, and latches managed startup failure.
  • packages/server/src/service-status.ts owns the small atomic status transition model and idempotent addressed stop handling.
  • packages/client/src/effect/service.ts discovers status, waits for live owners, throttles contenders, reports failures, and performs graceful replacement with legacy signal fallback.
  • packages/cli/src/server-process.ts holds ownership for the complete process lifetime and repairs registration without displacing a live owner.
  • packages/tui/src/context/client.tsx re-resolves cancellably while disconnected and exposes semantic service status to packages/tui/src/component/reconnecting.tsx.
  • packages/cli/script/service-smoke.ts verifies the compiled artifact, including authentication, two-process election, readiness, exact-instance stop, and cleanup.

Scope

  • Local service drains remain process-local; this does not add clustered execution ownership.
  • An unresponsive owner is reported but never killed automatically by an arbitrary reconnecting client.
  • Crash-time continuation recovery remains separate from service discovery and restart continuity.
  • Legacy health responses without status continue to decode as ready.

Testing

  • packages/core: bun run test - 1,279 passed, 6 skipped.
  • packages/server: bun run test - 9 passed.
  • packages/cli: bun run test - 30 passed.
  • packages/tui: bun run test - 226 passed, 1 skipped.
  • packages/client: bun run test - 38 passed.
  • Core, Protocol, Server, Client, CLI, and TUI: bun typecheck.
  • Pre-push workspace typecheck: 32 Turbo tasks passed.
  • Generated clients: bun run check:generated.
  • Focused lifecycle Oxlint: zero warnings or errors.
  • Release compilation: all 12 Linux, Linux musl, macOS, and Windows architecture/baseline targets built successfully.
  • Native compiled smoke: one owner elected from two contenders, authenticated health reached ready, unauthenticated health/stop returned 401, the loser exited, exact-instance stop was accepted, and registration was removed.
  • Linux and Windows compiled-service lifecycle smoke passed in CI.

Flow

sequenceDiagram
    participant Client
    participant Registration
    participant Contender
    participant Shell as Owning lifecycle shell
    participant App as Application layer

    Client->>Registration: Read channel registration
    alt no responding compatible owner
        Client->>Contender: Spawn lightweight contender
        Contender->>Contender: Acquire process-held lock
        Note over Contender: Losing contenders exit
        Contender->>Shell: Bind authenticated HTTP shell
        Shell->>Registration: Publish URL, PID, version, instance ID, password
        Shell->>App: Build in a child scope
        App-->>Shell: Activate full router
        Shell-->>Client: status = ready
    else owner starting, stopping, or failed
        Shell-->>Client: Return semantic status
        Client->>Client: Wait or present recovery guidance
    end
    Client->>Shell: POST exact instance stop
    Shell-->>Client: accepted = true
    Shell->>Registration: Remove registration on exit
Loading

@kitlangton
kitlangton marked this pull request as ready for review July 14, 2026 17:46
@kitlangton
kitlangton force-pushed the service-lifecycle-v2 branch from d817ebd to 453f458 Compare July 14, 2026 19:38
@kitlangton
kitlangton merged commit ece2b16 into v2 Jul 14, 2026
7 checks passed
@kitlangton
kitlangton deleted the service-lifecycle-v2 branch July 14, 2026 20:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant