rk server managed lifecycle (phase 0 contract)

The managed lifecycle mode adds VS Code window-aware lifecycle management to rk server without breaking existing behavior. This document defines the Phase 0 API contract for that mode.

You start the mode with rk server start --lifecycle managed. The server then tracks active VS Code windows as client leases and stops when no active clients remain.

Goals

The managed lifecycle has the following goals:

  • Preserve current behavior for manually started servers.
  • Add optional managed lifecycle mode for VS Code/Pilot-started background servers.
  • Track active VS Code windows (one extension host = one client lease).
  • Stop managed servers when active clients reach zero.
  • Keep existing inactivity timeout behavior (--timeout) as a global fallback.

Non-goals

The managed lifecycle does not aim to do the following:

  • Replace or remove inactivity timeout logic.
  • Require client leases for manual CLI usage.
  • Persist client lease state across server restarts.

Lifecycle modes

Server startup supports two modes:

  • unmanaged (default):

    • The mode is intended for manual server startup (for example, a user runs npx rk server in a terminal).
    • The server does not require client registration.
    • The existing inactivity timeout and an explicit stop control the server lifetime.
  • managed:

    • The mode is intended for Pilot background mode.
    • The server tracks window clients through the register, heartbeat, and release lease API.
    • The server can stop early when active clients become zero.
    • The existing inactivity timeout still applies.

CLI contract changes

The rk server command has an optional lifecycle flag:

npx rk server start --lifecycle unmanaged
npx rk server start --lifecycle managed

The flag follows these rules:

  • The default is unmanaged for backward compatibility.
  • An invalid lifecycle value should return a non-zero exit code with a validation message.
  • The rk server stop command remains the explicit stop command.
  • The deprecated --stop command continues to work during the migration window.

Lease model

A lease works as follows:

  • A lease represents one active VS Code window client.
  • The lease key is clientId, an opaque string that the client generates.
  • The server stores the following fields in memory:
    • clientId
    • workspaceRoot (optional but recommended for diagnostics)
    • createdAt
    • lastHeartbeatAt
    • metadata (optional: client type/version)
  • The server defines the lease time to live (TTL), for example 90 seconds.
  • Clients send a heartbeat periodically (less than half the TTL is recommended).

HTTP API contract

All lease endpoints are under /api/clients.

POST /api/clients/register

This endpoint registers or refreshes a client lease.

The following example shows the request body:

{
  "clientId": "extension-window-uuid",
  "workspaceRoot": "/abs/path/to/workspace",
  "clientType": "extension",
  "clientVersion": "0.3.0"
}

The following example shows the response (HTTP 200):

{
  "success": true,
  "mode": "managed",
  "clientId": "extension-window-uuid",
  "leaseTtlMs": 90000,
  "heartbeatIntervalMs": 30000,
  "activeClients": 2,
  "serverStartTime": 1710870000000
}

The endpoint returns the following errors:

  • If the mode is unmanaged, the endpoint returns 409 with the code LIFECYCLE_UNMANAGED.
  • If a required field is missing or not valid, the endpoint returns 400.

POST /api/clients/heartbeat

This endpoint extends an existing lease.

The following example shows the request body:

{
  "clientId": "extension-window-uuid"
}

The following example shows the response (HTTP 200):

{
  "success": true,
  "clientId": "extension-window-uuid",
  "activeClients": 2,
  "leaseExpiresInMs": 90000
}

The endpoint returns the following errors:

  • An unknown or expired client should receive 404 with the code CLIENT_NOT_FOUND.
  • If the mode is unmanaged, the endpoint returns 409 with the code LIFECYCLE_UNMANAGED.

POST /api/clients/release

This endpoint releases a client lease explicitly.

The following example shows the request body:

{
  "clientId": "extension-window-uuid"
}

The following example shows the response (HTTP 200):

{
  "success": true,
  "clientId": "extension-window-uuid",
  "activeClients": 1
}

The endpoint returns the following errors:

  • An unknown client may receive an idempotent 200 success or 404.
  • Recommended: return an idempotent 200 success to simplify deactivate flows.

GET /api/clients/status (optional but recommended)

This endpoint is a debug and diagnostic endpoint.

The following example shows the response (HTTP 200):

{
  "mode": "managed",
  "activeClients": 2,
  "leaseTtlMs": 90000,
  "clients": [
    {
      "clientId": "extension-window-uuid",
      "workspaceRoot": "/abs/path",
      "lastHeartbeatAt": 1710871234567
    }
  ]
}

Shutdown rules

In managed mode, the server follows these rules:

  • If active clients become 0, the server starts a short grace timer (for example, 5 to 10 seconds).
  • When the timer fires, the server re-checks the active clients.
    • If the count is still 0, the server stops.
    • If a new client appeared, the server cancels the shutdown.

In both modes, the following rules apply:

  • The existing inactivity timeout (--timeout) remains active.
  • The timeout can stop the server even if active clients exist (explicit requirement).
  • Every stop (zero clients, timeout, /api/stop, or a signal) first stops the Roku Cloud Emulator devices that the server started through MCP device_start, then closes its sessions, and then closes the listener (forced after 10 seconds). See Server and MCP for more information.

Health/diagnostics metadata

Extend the health response with lifecycle fields:

{
  "status": "ok",
  "mode": "managed",
  "activeClients": 2,
  "leaseTtlMs": 90000
}

Optionally, you can include the same metadata in .roku/rk-server.json for richer client diagnostics.

Concurrency and race handling

The server must follow these concurrency rules:

  • Serialize register, heartbeat, release, lease sweep, and zero-client shutdown checks, or otherwise make them race-safe.
  • Release and heartbeat for the same clientId should be idempotent and safe under retries.
  • Before the server stops, it must do a final active-client check.

Backward compatibility

The lifecycle flag preserves existing behavior:

  • Existing clients that do not use the lease API continue working in unmanaged mode.
  • The existing --timeout behavior is unchanged.
  • The existing /api/stop behavior is unchanged.

Pilot integration contract (for phase 1)

In managed background mode, Pilot should do the following:

  • Call register once per VS Code window on activation or start.
  • Start periodic heartbeats.
  • Call release on deactivation.
  • When the server is unavailable, keep the heartbeat loop active but do not start the server implicitly.
  • Re-register after an explicit server start path succeeds.

Manual terminal usage remains unmanaged and does not require leases.

Clarification: no implicit start from lease API

Lease endpoints are liveness and coordination endpoints only. They should not serve as an implicit server startup path:

  • The register, heartbeat, and release endpoints are expected to return an unavailable or not-applicable response when the server is down.
  • Server start remains an explicit operation (rk server, Pilot Start rk server, or explicit ensureRunning entry points).

Acceptance mapping

The acceptance criteria map to two modes:

  • Mode A (manual terminal start): The server behaves as it does without managed mode, and the timeout drives its lifetime.
  • Mode B (VS Code managed start): The server counts clients across windows and stops when no clients remain.
  • B extends A: The timeout remains active in both modes.

Understanding current managed runtime behavior

In current Pilot-managed flows, the following behaviors apply:

  • Lease APIs (register, heartbeat, release) do not start the server automatically when it is down.
  • The server treats a heartbeat as a liveness update only. If the server is unavailable, clients log a skipped heartbeat and keep waiting.
  • Server restart should happen only from explicit start flows (for example, user-triggered Start rk server or other explicit ensureRunning paths).
  • In multi-window use, one window stopping the server can cause all windows to report skipped heartbeat until a new explicit start occurs.

Related documentation


Did this page help you?