Service Lifecycle Deep Dive
The V2 managed service keeps a background server process that owns sessions, configuration, plugins, permissions, and tool execution. This design replaced the earlier heartbeat-lease approach after incident #36688.
Problem
The original V2 service used a heartbeat lease with a 3-second staleness timeout. This caused four failures:
- Multiple TUIs spawned heavyweight server contenders simultaneously
- A slow winner was unobservable during cold boot, so another wave displaced it
- A fresh TUI exhausted its reconnect budget and crashed
- A losing contender stayed alive consuming ~1GB RSS
Solution: Process-Held OS Lock
The new design uses flock on POSIX and an exclusively bound named pipe on Windows. The lock:
- Is acquired before any expensive boot work
- Is held for the entire process lifetime
- Is released by the OS on process death (no cleanup callback needed)
- Cannot be broken by a stale heartbeat
Lifecycle Shell
The elected process binds a minimal HTTP surface before initializing the application. This lets clients distinguish a slow winner from an absent server.
The health response includes a status discriminant:
type ServiceHealth = {
healthy: true
version: string
pid: number
instanceID: string
status: ServiceStatus
}
During starting or stopping, application requests receive:
HTTP/1.1 503 Service Unavailable
Retry-After: 1
Content-Type: application/json
{"code":"service_starting"}
Update Activation
Background updates don’t restart the running service. A fresh TUI launch activates updates:
- Background updater installs the new package
- Running process continues with old image
- Fresh TUI detects version mismatch
- Fresh TUI requests graceful stop of the old instance
- Old service suspends sessions, reports
stopping, exits - Lock releases, a new contender wins, boots the new version
- TUIs reconnect and resume
Client Reconnect
Fresh and existing TUIs use the same status loop:
| Status | User-facing state |
|---|---|
| No registration | Starting background service... |
| Registration unreachable | Waiting for background service... |
starting | Starting OpenCode vX... |
stopping | Updating to vX... |
failed | Actionable failure message |
ready | Normal TUI |
Gotcha: Transport loss is never a terminal error. TUIs rediscover and reconnect indefinitely. Only explicit
service restartcan perform destructive recovery of an unresponsive owner.
Launch vs Reconnect
| Connection type | Version policy |
|---|---|
launch (fresh TUI) | Requires installed package version, may activate replacement |
reconnect (existing TUI) | Accepts current owner, never activates replacement |
This preserves permissive reconnect behavior - existing TUIs don’t disrupt the service when they reconnect after a network blip.