There is no Session object anywhere in the system. 'Who is streaming what' is reconstructed at render time by joining two polling sources. This causes silent contradictions, stale state after crashes, and zero session history.
Sunshine is configured with a prep-cmd hook pointing at hydrabody's local HTTP server. When a Moonlight client successfully connects and starts receiving video, Sunshine executes the prep-cmd: POST /api/v1/stream/started fires on hydrabody's httpserver.go handleStreamStarted. When the client disconnects (cleanly or not), Sunshine executes the prep-cmd Undo: POST /api/v1/stream/ended fires on handleStreamEnded.
hydrabody maintains an in-memory sunshine.SessionRegistry (go-sunshine library) tracking per-session state: app name, state (active/disconnected/expired), PID, and duration computed from start time. This is the earliest and most reliable signal of a real stream — it comes directly from Sunshine before any network reporting happens.
Additionally, hydrabody runs a stale-session watchdog (stale_session_watchdog_windows.go) that polls TCP port 47999 every 30s. If no Moonlight client has been connected for 2 minutes, it synthetically fires OnStreamEnded. This is the recovery path when Sunshine's prep-cmd Undo was never called (e.g. Sunshine crash, ungraceful disconnect).
hydrabody reports StreamStatus to hydracluster via POST /api/v1/body/status on every tick (~30s) and on state transitions. It also reports a richer payload including StreamSessions[] to hydrabodystatus for durable storage.
hydraheadipad embeds the Moonlight iOS client library (moonlight-ios fork, HydraStreamSession.m). LiStartConnection initiates the GameStream handshake with the body's Sunshine instance (port 47989 for pairing, 47984/47998 for the stream). The Moonlight library fires connection state callbacks into AppState.swift when the stream is established, when it drops, and on error.
While streaming, AppState heartbeats hydracluster every 30s via PUT /api/v1/heads/{id} with status: 'streaming' and liveBodyID set to the body node ID. When the user exits or the stream ends, AppState.stopStream() calls DELETE /api/v1/heads/{id}/stream with the body_id, then notifyStreamStopped clears liveBodyID in the next heartbeat.
hydraheadflatscreen follows the same pattern: heartbeat with HeadStatus, and a stream-stop exec command routed through handleHeadStreamStop in hydracluster.
hydracluster receives both signals independently but never joins them into a Session:
There is no session ID. No start timestamp. No end reason. When a body crashes mid-stream, the body eventually reports idle but the head keeps heartbeating streaming — hydracluster holds a contradiction with no way to resolve it automatically. The monitor shows them joined or not based on a client-side LiveBodyID lookup that may be stale.
Head crashes without calling stopStream: body stays 'streaming' for up to 2 minutes (stale-session watchdog), then body reports idle. hydracluster's LiveBodyID for the head is never cleared — monitor shows head as streaming against no body.
Body crashes or Sunshine restarts: body reports idle on next tick. Head is still heartbeating streaming. hydracluster sees head: streaming, body: idle — contradiction. Requires operator to manually call stop-stream.
hydracluster restart: bodyStatus map is lost. All bodies appear idle until their next heartbeat (~30s gap). During this window discoverBody() returns actively-streaming bodies as eligible, risking double-assignment.
No session history: no way to answer 'how long did that stream run?' or 'why did it end?' without grepping logs.
Add SessionRecord to hydracluster server state:
SessionRecord {
ID string
BodyID string
HeadID string // resolved from LiveBodyID map at session-open time
Experience string
StartedAt time.Time
EndedAt *time.Time
EndReason string // 'head_stop', 'body_idle', 'body_offline', 'head_offline'
}
Open a session in handleBodyStatus when StreamStatus transitions from non-streaming to streaming. Close it when the body next reports idle, when the body goes offline (MarkOffline), or when handleHeadStreamStop fires. Store the last 200 closed sessions in an in-memory ring buffer — same pattern as the existing eventLog in event_log.go.
Expose:
GET /api/v1/sessions — active sessions
GET /api/v1/sessions/history — last 200 closed sessions
The streaming monitor replaces its two-call polling join with a single query to /api/v1/sessions. Operator visibility: one place to see all active streams with duration, head, body, and experience.
No hydrabody changes. No hydraheadipad changes. Deployable independently.
Make sessions explicit on both sides:
This eliminates the inference gap and makes the Sunshine prep-cmd → Moonlight connect sequence the authoritative session lifecycle trigger.