> ## Documentation Index
> Fetch the complete documentation index at: https://docs.synheart.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Watch Protocol

> Phone↔watch wire format (session.proto), transport mapping, and the raw streaming opt-in

The phone↔watch protocol is defined in `session.proto`, package `synheart.session.v1`.

## Envelope

Every message on the wire is a `SessionMessage` with a `oneof payload`:

```protobuf theme={null}
message SessionMessage {
  oneof payload {
    StartSession      start_session       = 1;   // phone → watch
    StopSession       stop_session        = 2;   // phone → watch
    GetStatus         get_status          = 3;   // phone → watch
    SessionStarted    session_started     = 10;  // watch → phone
    SessionFrame      session_frame       = 11;  // watch → phone
    SessionSummary    session_summary     = 12;  // watch → phone
    SessionError      session_error       = 13;  // watch → phone
    SessionStatus     session_status      = 14;  // watch → phone (response to GetStatus)
    BiosignalBatch    biosignal_batch     = 15;  // watch → phone (raw streaming opt-in)
  }
}
```

Field numbers `1–9` are reserved for phone→watch; `10+` for watch→phone.

## Phone → watch

### `StartSession`

```protobuf theme={null}
message StartSession {
  string         session_id        = 1;
  Mode           mode              = 2;
  uint32         duration_sec      = 3;
  ComputeProfile profile           = 4;
  string         window_label      = 5;  // optional
  bool           enable_raw_stream = 6;  // default false
}

message ComputeProfile {
  uint32 window_sec        = 1;
  uint32 emit_interval_sec = 2;
}
```

<Note>
  The wire `ComputeProfile` carries only `window_sec` and `emit_interval_sec`. The Dart-side `ComputeProfile.rawEmitIntervalSec` is **phone-side only** — it doesn't cross the wire, because the watch derives raw cadence from `enable_raw_stream` + its own buffer policy.
</Note>

`enable_raw_stream = true` activates `BiosignalBatch` emission from watch to phone. Default is `false` — raw biosignals must not transmit unless explicitly enabled.

### `StopSession`

```protobuf theme={null}
message StopSession { string session_id = 1; }
```

Halts the watch engine. The watch responds with the final `SessionSummary` and transitions to `TERMINATED`.

### `GetStatus`

Empty message. Watch responds with `SessionStatus`.

## Watch → phone

### `SessionStarted`

```protobuf theme={null}
message SessionStarted {
  string session_id    = 1;
  uint64 started_at_ms = 2;
}
```

Fired once after the watch engine reaches `ACTIVE` (sensors hot, first sample admitted).

### `SessionFrame`

```protobuf theme={null}
message SessionFrame {
  string             session_id     = 1;
  uint64             seq            = 2;  // monotonic per session
  uint64             emitted_at_ms  = 3;
  SessionMetrics     metrics        = 4;
  BehaviorSnapshot   behavior       = 5;  // optional
}
```

### `SessionMetrics`

```protobuf theme={null}
message SessionMetrics {
  double hr_mean_bpm     = 1;
  double hr_sdnn_ms      = 2;
  double rmssd_ms        = 3;
  uint32 sample_count    = 4;
  uint64 start_ms        = 5;
  uint64 end_ms          = 6;

  optional double motion_rms_g          = 7;   // accel-equipped wearables
  optional uint32 motion_sample_count   = 8;
  optional double active_energy_kcal    = 9;   // iOS HKLiveWorkoutBuilder
}
```

The Dart SDK projects this into a flat `Map<String, dynamic>` on `SessionFrame.metrics`.

### `BehaviorSnapshot`

```protobuf theme={null}
message BehaviorSnapshot {
  double typing_cadence            = 1;
  double inter_key_latency         = 2;
  uint32 burst_length              = 3;
  double scroll_velocity           = 4;
  double scroll_acceleration       = 5;
  double scroll_jitter             = 6;
  double tap_rate                  = 7;
  uint32 app_switches_per_minute   = 8;
  double foreground_duration       = 9;
  double idle_gap_seconds          = 10;
  double stability_index           = 11;
  double fragmentation_index       = 12;
  uint64 timestamp                 = 13;
}
```

Mirrors the Dart `BehaviorSnapshot` field-for-field.

### `SessionSummary`

```protobuf theme={null}
message SessionSummary {
  string           session_id          = 1;
  uint32           duration_actual_sec = 2;
  SessionMetrics   metrics             = 3;
  BehaviorSnapshot behavior            = 4;  // optional
}
```

Fires once at session end.

### `SessionError`

```protobuf theme={null}
message SessionError {
  string    session_id = 1;
  ErrorCode code       = 2;
  string    message    = 3;
}
```

`ErrorCode` enum: `ERROR_PERMISSION_DENIED`, `ERROR_SENSOR_UNAVAILABLE`, `ERROR_LOW_BATTERY`, `ERROR_OS_TERMINATED`, `ERROR_INVALID_STATE`. See [Errors](/synheart-session/errors).

### `SessionStatus`

```protobuf theme={null}
message SessionStatus {
  string       session_id = 1;
  SessionState state      = 2;
  bool         active     = 3;
  uint64       last_seq   = 4;
}
```

Response to `GetStatus`. The Dart-side status returned by `getStatus()` carries only `(sessionId, active, lastSeq)`; `state` is currently dropped at the SDK boundary.

### `BiosignalBatch`

```protobuf theme={null}
message BiosignalSample {
  uint64     timestamp_ms = 1;
  SignalType type         = 2;   // SIGNAL_HR_BPM | SIGNAL_IBI_MS | SIGNAL_HRV_RMSSD
  float      value        = 3;
}

message BiosignalBatch {
  string                   session_id = 1;
  repeated BiosignalSample samples    = 2;
}
```

Sent as a batch to amortise transport cost. Per the streaming RFC: "Samples must be batched. No per-sample transport."

## Transport mapping

The proto is transport-agnostic; the SDK uses each platform's native companion channel:

| Platform          | Transport                               | Notes                                                            |
| ----------------- | --------------------------------------- | ---------------------------------------------------------------- |
| iOS + watchOS     | `WatchConnectivity.WCSession`           | `sendMessage` for live, `transferUserInfo` for queued.           |
| Android + Wear OS | `Wearable.MessageClient` / `DataClient` | Same pattern: `MessageClient` for live, `DataClient` for queued. |

The Dart side sees both transports through a single Flutter `MethodChannel("ai.synheart.session/methods")` + `EventChannel("ai.synheart.session/events")` pair.

## Channel surface

```dart theme={null}
class SessionChannel {
  Future<void> startSession(SessionConfig config);
  Future<void> stopSession(String sessionId);
  Future<SessionStatus?> getStatus();
  Future<WatchStatus?> getWatchStatus();
  Stream<SessionEvent> get events;  // EventChannel-backed
}
```

`getWatchStatus()` returns the connectivity snapshot:

```dart theme={null}
class WatchStatus {
  final bool supported;   // OS supports watch companion
  final bool reachable;   // watch is reachable now
  final bool paired;      // a watch is paired
  final bool installed;   // companion app is installed on the watch
}
```

## Ordering guarantees

* `SessionStarted` precedes any frames or summary for a given `session_id`.
* `seq` is monotonic non-decreasing within a single session id; gaps may exist if the transport drops frames.
* `SessionSummary` and `SessionError` are mutually exclusive — the watch emits one or the other to terminate.
* `BiosignalBatch` events are not ordered relative to `SessionFrame` events; consumers must use `timestamp_ms` for chronological alignment.

## Privacy posture

* Default `enable_raw_stream = false`. Opt-in only.
* When raw streaming is enabled, the producing HSI block must include `raw_biosignals_allowed = true`.
* No PII fields cross the wire — `session_id` is a host-supplied identifier (default UUID v4), `window_label` is free-form but documented as no-PII.

## Related

* [Lifecycle](/synheart-session/lifecycle) — `SessionState` semantics.
* [Providers](/synheart-session/providers) — `WatchBiosignalProvider` and how it consumes this protocol.
* [Errors](/synheart-session/errors) — `ErrorCode` taxonomy.
