# Zedra Protocol Specs

Canonical specification for Zedra's wire protocol, RPC contracts, and protocol-layer change process.

- Primary implementation: `crates/zedra-rpc/src/proto.rs`
- Host dispatcher: `crates/zedra-host/src/rpc_daemon.rs`
- Client integration: `crates/zedra-session/src/handle.rs`

When this document conflicts with code, treat code as current behavior and immediately update this file.

---

## 1) Scope and Source of Truth

This document defines protocol and RPC conventions for:

- Message enum and transport channels (`ZedraProto`)
- All request/response structures
- Host-initiated event contracts (`HostEvent`)
- Stream semantics (terminal bidirectional stream, subscribe stream)
- Compatibility/versioning rules for evolving protocol safely

The protocol layer includes:

- `crates/zedra-rpc/src/proto.rs`
- `crates/zedra-host/src/rpc_daemon.rs`
- `crates/zedra-session/src/handle.rs`
- Any code that serializes/deserializes protocol messages

---

## 2) Transport and Encoding

### 2.1 Transport

- Network transport: iroh QUIC.
- RPC framing: `irpc` over iroh streams.
- ALPN: `zedra/rpc/2` (see `ZEDRA_ALPN` in `crates/zedra-rpc/src/proto.rs`).

### 2.2 Serialization

- Binary serialization: `postcard`.
- No JSON/base64 at protocol message layer.
- Terminal data uses raw bytes (`serde_bytes`) to avoid text encoding overhead.

### 2.3 Message Envelope

- All RPC operations are declared in `ZedraProto` and expanded by `#[rpc_requests]`.
- Each operation explicitly declares channel shape:
  - Unary request/response: oneshot sender
  - Server stream: mpsc sender from host to client
  - Bidirectional stream: client rx + host tx
- `ZedraProto` variant order is append-only. Never insert/reorder existing variants.

---

## 3) Protocol Design Conventions

### 3.1 Naming

- Requests end with `Req`.
- Responses end with `Result`.
- Event types use explicit enum variants (`HostEvent::...`).
- Enum variant names are verb-first for RPCs (`FsList`, `GitStatus`, `TermCreate`).

### 3.2 Path Rules

- Client paths are always workspace-relative strings.
- Host resolves and jails paths using canonicalization.
- Absolute paths and traversal escapes are rejected host-side.

### 3.3 Streaming Rules

- `Subscribe` is long-lived server stream for host-initiated events.
- Only one active subscribe stream is expected per session; new subscribe replaces old sender.
- `TermAttach` is long-lived bidirectional stream for PTY I/O.

### 3.4 Event Delivery Semantics

- Host events are best-effort, at-most-once.
- Clients must treat host events as invalidation signals and refresh via canonical RPC reads.
- Observers should coalesce duplicate invalidations when possible.

---

## 4) Authentication and Session Flow

### 4.1 First Pairing

1. `Register(RegisterReq)` with HMAC proof from QR ticket handshake secret.
2. `Connect(ConnectReq)` with `session_token: None` — server always issues a `Challenge` since no token exists yet.
3. Verify `Challenge.host_signature` against stored host `EndpointId`.
4. `AuthProve(AuthProveReq)` with client signature and session attachment.
5. `AuthProveResult::Ok(SyncSessionResult)` — bootstrap data is piggybacked, no separate `SyncSession` needed.
6. Normal RPCs begin after success.

### 4.2 Token Resume (fast path — 1 RTT)

1. `Connect(ConnectReq)` with `session_token: Some(token)` — in-memory token from last successful connect.
2. `ConnectResult::Ok(SyncSessionResult)` — session attached immediately, fresh token issued.
3. Normal RPCs begin after success.

### 4.3 PKI Reconnect (2 RTTs)

If the client has no valid session token (first connection after restart, or token expired):

1. `Connect(ConnectReq)` with `session_token: None`.
2. `ConnectResult::Challenge { nonce, host_signature }` — server embeds the PKI challenge, saving a separate challenge request RTT.
3. Verify `host_signature` against stored host `EndpointId`.
4. `AuthProve(AuthProveReq)` with client signature.
5. `AuthProveResult::Ok(SyncSessionResult)` — bootstrap data piggybacked.
6. Normal RPCs begin after success.

### 4.4 Session Token Properties

- **In-memory only**: never persisted to disk. Host restart requires PKI reconnect.
- **Single-slot per session**: one token at a time, bound to the currently active client pubkey.
- **Consumed on validation**: token is atomically removed when validated, preventing replay.
- **Rotated on every successful connect**: both `ConnectResult::Ok` and `AuthProveResult::Ok` return a fresh token.

### 4.5 Health

- `Ping(PingReq)` / `PongResult` used for RTT and liveness.

### 4.6 Deprecated Append-Only Auth Variant

`Authenticate(AuthReq) -> AuthChallengeResult` remains in `ZedraProto` only
because protocol enum order is append-only. It is reserved for wire compatibility
and is not part of the active connection lifecycle. Current clients start auth
with `Register` for first pairing or `Connect` for all other paths; the PKI
challenge is carried by `ConnectResult::Challenge`.

---

## 5) RPC Surface (Current)

## 5.1 Auth

- `Register(RegisterReq) -> RegisterResult`
- `Connect(ConnectReq) -> ConnectResult`
- `AuthProve(AuthProveReq) -> AuthProveResult`
- `Authenticate(AuthReq) -> AuthChallengeResult` (deprecated/reserved append-only variant; do not use in current clients)

## 5.2 Health

- `Ping(PingReq) -> PongResult`

## 5.3 Session

- `SyncSession(SyncSessionReq) -> SyncSessionResult` (mid-session only; bootstrap payload is piggybacked on `ConnectResult::Ok` and `AuthProveResult::Ok`)
- `GetSessionInfo(SessionInfoReq) -> SessionInfoResult`
- `ListSessions(SessionListReq) -> SessionListResult`
- `SwitchSession(SessionSwitchReq) -> SessionSwitchResult` (reserved/unsupported for active workspace switching; current host dispatch stays bound to the originally authenticated session)
- `SubscribeHostInfo(SubscribeHostInfoReq) -> stream HostInfoSnapshot`

## 5.4 Filesystem

- `FsList(FsListReq) -> FsListResult`
- `FsRead(FsReadReq) -> FsReadResult`
- `FsWrite(FsWriteReq) -> FsWriteResult`
- `FsStat(FsStatReq) -> FsStatResult`
- `FsDocsTree(FsDocsTreeReq) -> FsDocsTreeResult`
- `FsWatch(FsWatchReq) -> FsWatchResult`
- `FsUnwatch(FsUnwatchReq) -> FsUnwatchResult`

### Error convention

Most result structs carry `error: Option<String>`. When set, the operation failed and the host has already logged the cause. Client rules:

- Treat `error: Some(msg)` as a terminal failure for that request.
- All other fields are zero-valued when `error` is set (empty string, empty vec, `false`, `0`, `None`).
- Never silently ignore a set `error` — propagate as `Err` or show in UI.

Result types that carry `error: Option<String>`:
`FsListResult`, `FsReadResult`, `FsStatResult`, `SessionSwitchResult`, `TermCreateResult`,
`GitStatusResult`, `GitDiffResult`, `GitLogResult`, `GitCommitResult`, `GitStageResult`,
`GitUnstageResult`, `GitBranchesResult`, `AgentListResult`, `AgentSessionsResult`,
`AgentResumeResult`, `LspDiagnosticsResult`.

Types that use non-string status fields or enum variants instead:
`FsWriteResult` (`ok: bool`), `GitCheckoutResult` (`ok: bool`), `FsWatchResult`/`FsUnwatchResult` (enum),
`FsDocsTreeResult` (`error: Option<FsDocsTreeError>`).

### FsRead additional fields

- `content`: file contents (empty on error or when `too_large`)
- `too_large`: true when file exceeds the 500 KB limit

### FsList paging conventions

- `offset` is zero-based index into stable listing order returned by host.
- `limit` is clamped by host to `FS_LIST_DEFAULT_LIMIT` when necessary.
- `has_more` indicates additional entries exist after this page.

### FsDocsTree conventions

- `rebuild = true` scans the requested directory, replaces the per-session in-memory docs-tree snapshot, and returns page 0.
- `rebuild = false` serves a page from the matching `snapshot_id`; `CacheMiss` or `StaleSnapshot` means the client should ask the user to rebuild.
- The host detects markdown files with case-insensitive `.md` and `.markdown` extensions only.
- The host combines gitignore matching with built-in fallback ignores for dot-prefixed, generated, dependency, and build directories, and never follows symlink directories.
- Limits: default page size `200`, max page size `1_000`, max offset `5_000`, max visited entries per rebuild `10_000`.
- `truncated = true` means host caps prevented proving the full docs tree was scanned.
- The client treats `Unsupported` as a compatibility result for older hosts and should not keep showing an active build state.

### FsWatch/FsUnwatch result enums

- `FsWatchResult`:
  - `Ok`
  - `InvalidPath`
  - `RateLimited`
  - `QuotaExceeded`
  - `Unsupported` (client-local fallback when host does not support observer RPCs)
- `FsUnwatchResult`:
  - `Ok`
  - `InvalidPath`
  - `RateLimited`
  - `NotWatched`
  - `Unsupported` (client-local fallback when host does not support observer RPCs)

## 5.5 Terminals

- `TermCreate(TermCreateReq) -> TermCreateResult`
- `TermAttach(TermAttachReq) <-> TermInput/TermOutput` (bidirectional)
- `TermResize(TermResizeReq) -> TermResizeResult`
- `TermClose(TermCloseReq) -> TermCloseResult`
- `TermList(TermListReq) -> TermListResult`
- `TermReorder(TermReorderReq) -> TermReorderResult`
- `SyncSessionResult.terminals -> Vec<TerminalSyncEntry>`
- Terminal ids are opaque host-generated UUID strings.
- `TermCreateReq.color_scheme` is optional. New clients send `Dark` or `Light`
  so the host can answer startup OSC 10/11/12 color queries immediately for
  launch-command TUIs before a client terminal view attaches.

### TermAttach conventions

- Client passes `last_seq` to request backlog replay.
- Host may replay missed output before live stream.
- Host may send a synthetic metadata preamble as `TermOutput { seq: 0, ... }` before backlog replay. Clients must process the bytes as normal PTY output but must not use `seq=0` for backlog sequence tracking.
- The synthetic preamble replays cached OSC metadata that may have fallen out of the backlog, including title, icon name, cwd, shell command line, command start/idle state, and last exit code.
- Output `seq` is monotonic per session backlog stream and used for gap detection.
- `SyncSessionResult.terminals` and `TermListResult.terminals` are ordered by host-owned terminal order. Creation order is the default until a client submits an explicit order.
- `TerminalSyncEntry.position` and `TermListEntry.position` are zero-based positions in that host-owned order.
- `TerminalSyncEntry.last_seq` is the host's latest backlog sequence observed for that terminal at sync time.
- `TerminalSyncEntry.title`, `TerminalSyncEntry.cwd`, and `TerminalSyncEntry.icon_name` are the host's latest cached terminal metadata at sync time. `TermAttach` still replays the same metadata as PTY bytes so normal terminal-event consumers are seeded through one path.
- Clients should keep local terminal tabs keyed by terminal id and use `last_seq` to seed reconnect `TermAttach` calls.

### SyncSession conventions

- `SyncSession` is a mid-session state refresh; connect-time bootstrap is piggybacked on `ConnectResult::Ok` and `AuthProveResult::Ok`.
- Host rotates and returns a fresh `session_token` on every successful `SyncSession`, token-accepted `Connect`, and `AuthProve` attach.
- `session_id` in `SyncSessionResult` is authoritative and must replace any stale client-side session id.
- `SyncSessionResult.terminals` is the authoritative server-side terminal set at bootstrap time. During reconnect, clients preserve the existing local order for terminals still present in that set and append any newly discovered host terminals unless the client has submitted an explicit host order.
- `TermReorderReq.ordered_ids` must be an exact permutation of the current active terminal ids. The host rejects partial, duplicate, or unknown-id orders.
- `ConnectReq.session_token` and `SyncSessionResult.session_token` are opaque, host-issued, session-bound, and client-bound.
- Session tokens are currently ephemeral host memory only; host restart may invalidate them and force PKI fallback.

### SwitchSession status

`SwitchSession` is reserved for a future active-session transfer protocol. The
current host handler returns an explicit unsupported error because it does not
replace the session bound to the authenticated dispatch loop. Clients must not
use it to switch the active workspace; connect to the target session through the
normal `Connect`/`AuthProve` flow instead.

## 5.6 Git

- `GitStatus(GitStatusReq) -> GitStatusResult`
- `GitDiff(GitDiffReq) -> GitDiffResult`
- `GitLog(GitLogReq) -> GitLogResult`
- `GitCommit(GitCommitReq) -> GitCommitResult`
- `GitStage(GitStageReq) -> GitStageResult`
- `GitUnstage(GitUnstageReq) -> GitUnstageResult`
- `GitBranches(GitBranchesReq) -> GitBranchesResult`
- `GitCheckout(GitCheckoutReq) -> GitCheckoutResult`

### Git error handling

All Git result types carry `error: Option<String>`. Host sends error when git repo cannot be opened or the operation fails. Client `git_*` handle methods propagate these as `Err`.

### Git status conventions

- `GitStatusEntry` reports index and working-tree state independently via `staged_status` and `unstaged_status`.
- A file may appear in both UI sections when both fields are present (for example, partially staged edits).
- Status strings use lowercase semantic names such as `modified`, `added`, `deleted`, `renamed`, `untracked`, and `conflicted`.
- `GitStage` stages the provided paths with `git add -- <paths>`.
- `GitUnstage` removes the provided paths from the index while preserving working tree contents.

## 5.7 AI, Managed Agents, and LSP

- `AiPrompt(AiPromptReq) -> AiPromptResult`
- `AgentList(AgentListReq) -> AgentListResult`
- `AgentSessions(AgentSessionsReq) -> AgentSessionsResult`
- `AgentResume(AgentResumeReq) -> AgentResumeResult`
- `AgentInstalledList(AgentInstalledListReq) -> AgentInstalledListResult`
- `LspDiagnostics(LspDiagnosticsReq) -> LspDiagnosticsResult`
- `LspHover(LspHoverReq) -> LspHoverResult`

### Managed agent conventions

**Terminology:** A *managed agent* is one of `ManagedAgentKind`: Claude Code (`Claude`), Codex (`Codex`), and OpenCode (`OpenCode`). This is narrower than `AgentInstalledList`, which lists every terminal agent CLI the host can launch (Amp, Cline, Cursor, etc.).

- `AgentListResult.agents` returns one `AgentSummary` for every managed agent, even when the CLI is missing or no sessions exist.
- `AgentListReq.refresh` and `AgentInstalledListReq.refresh` default to `false`. When `false`, the host serves its startup cache; when `true`, the host rescans synchronous fields before responding.
- `AgentInstalledList` is for `TermCreate` launch commands only, not manage-agent views.
- `AgentSessionsResult.sessions` returns the latest workspace-matching sessions for one managed agent. `AgentSessionsResult.total` is the full match count before applying `limit`.
- `AgentSessionsReq.limit` defaults to `0`, which uses the host default (`50`, overridable with `ZEDRA_AGENT_SESSION_LIMIT`, max `200`).
- `AgentSessionsReq.refresh` follows the same cache rule as `AgentListReq.refresh`.
- `AgentResume` creates a new terminal and starts the provider-specific resume command on the host. Clients must not build provider shell commands from summary fields.
- `AgentInstalledList` returns all supported terminal agent slugs with host-owned `launch_cmd` values for available CLIs. Clients create agents through `TermCreate` with that launch command.
- `AgentSummary.usage` carries a live `AgentUsageSnapshot` fetched asynchronously from the provider's API at daemon start and on `AgentListReq{refresh:true}`. Fields populated per agent:
  - **Claude** (preferred): `rate_limit_*_used_percent`, `rate_limit_*_resets_at`, `total_cost_usd` / `context_used_percent` (extra credit spend) — from `api.anthropic.com/api/oauth/usage` when `~/.claude/.credentials.json` has a valid OAuth token (structured JSON; same reliability model as Codex).
  - **Claude** (PTY fallback, unix host only): when there is no readable credentials file (typical Keychain-only CLI login). The host spawns `claude`, captures `/usage` text, and scrapes percentages and optional `resets_at`. This path is **best-effort**: Claude’s TUI layout is not a protocol contract; reset timestamps may be absent or approximate (host local time). Zedra does not read the macOS Keychain for Claude tokens. One PTY probe is cached ~60s and shared by usage + plan scans.
  - **Codex**: `rate_limit_five_hour_used_percent` (primary window), `rate_limit_seven_day_used_percent` (secondary window) — from `chatgpt.com/backend-api/wham/usage` using `~/.codex/auth.json`.
  - `None` when credentials are missing and the CLI PTY probe fails. When a credentials file token is present but the OAuth usage/profile call fails, the host falls back to the CLI PTY probe (still no Keychain read).
- `AgentSummary.account.fields` exposes locally discovered account/setup metadata for manage-agent detail views. Values must remain privacy-safe. Current fields per agent kind:
  - **Claude**: Logged in, Plan (from `~/.claude/.credentials.json` `subscriptionType` / `rateLimitTier`, refreshed from `api.anthropic.com/api/oauth/profile` when OAuth is available, or CLI PTY when the token lives in Keychain); Organization (OAuth profile only); Model, Effort, Permission mode (from `~/.claude/settings.json`); Total cost (USD), Today msgs (from `~/.claude/stats-cache.json` `dailyActivity`). Live rate limits and extra spend come from `AgentUsageSnapshot` on the summary (OAuth API or CLI PTY), not account fields.
  - **Codex**: Logged in, Account (name from JWT), Plan, Plan until (from `~/.codex/auth.json` `id_token` payload, re-read on async refresh); Model, Personality, Reasoning effort (from `~/.codex/config.toml`); Week threads, Total threads (from `~/.codex/state_5.sqlite`).
  - **OpenCode**: Config dir presence (from `~/.config/opencode`).
- `AgentSessionSummary.title` uses provider-stored titles when available, otherwise `"Unknown"`.
- `AgentSessionSummary.transcript_size_bytes` reports transcript file size when the host scan has a local file path.
- `AgentGitSummary.worktree` is populated for Claude when the encoded project path includes `--claude-worktrees-<name>`.
- Summary timestamps use `DateTime<Utc>` serialization through serde/postcard.
- Data sources are explicit through `AgentDataSource` so clients can distinguish CLI/setup checks, historical scans, terminal metadata, hook state, status lines, and provider CLI output.
- Summaries must not expose prompt text, command arguments, tool input/output, transcript bodies, or last assistant messages. Allowed fields are safe labels, ids, timestamps, counts, paths already scoped to the workspace, and provider metadata such as model, source, permission mode, CLI version, git branch, and PR link metadata.
- `AgentLifecycleStatus`, `AgentEventKind`, and `AgentActionKind` provide the cross-agent vocabulary for future hook-driven prompts and notifications.

### Async managed-agent fetching

CLI `--version` probes are slow and cached separately from the synchronous agent scan.

1. **Daemon startup:** preload scans agents into cache, then probes versions in the background.
2. **`AgentList` read:** returns cache with any known versions merged; live terminal bindings merged on the host for the response.
3. **`AgentList` with `refresh: true`:** rescans synchronous fields immediately, then starts a background version refresh.
4. **Background completion:** host pushes `HostEvent::AgentInfoChanged { info }` once per managed agent to every session with an active `Subscribe` stream. `info` is the full cached `AgentSummary` for that agent (including versions and live bindings for that session).
5. **Client update:** replace the cached row for `info.kind` (do not treat the event as a partial patch).

---

## 6) Host Events

Current host event variants:

- `TerminalCreated { id, launch_cmd }`
- `GitChanged`
- `FsChanged { path }`
- `AgentInfoChanged { info }`

Client rules:

- `TerminalCreated`: attach/open terminal view if relevant.
- `GitChanged`: invalidate cached git state and refresh when appropriate.
- `FsChanged { path }`: invalidate cached file tree for the watched path and reload affected expanded nodes.
- `AgentInfoChanged`: replace cached `AgentSummary` for `info.kind`. One event per managed agent per version refresh. Requires an active `Subscribe` stream.

---

## 6.1 Host Info Subscription

`SubscribeHostInfo` is a separate server-streaming subscription for host resource display. It does not reuse `HostEvent`, because resource snapshots are periodic state rather than invalidation events.

Current sampling behavior:

- Host sends the first `HostInfoSnapshot` after CPU counters are primed.
- Host sends subsequent snapshots every 5 seconds.
- Stream ends when the client disconnects or stops reading.
- Battery list may be empty when the platform does not expose battery data.

Current snapshot fields:

- `captured_at_ms`
- `cpu_usage_percent`
- `cpu_count`
- `memory_used_bytes`
- `memory_total_bytes`
- `swap_used_bytes`
- `swap_total_bytes`
- `system_uptime_secs`
- `batteries: Vec<HostBatteryInfo>`

---

## 7) Compatibility Policy

### 7.1 General

- Avoid breaking wire compatibility unless absolutely required.
- Prefer additive evolution:
  - Add new enum variants
  - Add new RPC calls instead of repurposing old semantics
- With postcard-encoded structs, prefer new request/response types over widening existing structs when mixed-version decoding matters.

### 7.2 Breaking Changes

If a breaking change is unavoidable:

1. Document it in this file under "Protocol Changelog".
2. Update ALPN/version strategy (`zedra/rpc/N`) if cross-version interop is impacted.
3. Ship host/client changes together.

### 7.3 Unknown Variants

- New variants may fail on old binaries; do not assume forwards compatibility across arbitrary versions.
- Keep deployment pairs synchronized when introducing enum variants.

---

## 8) Error Handling Conventions

- For authorization/validation failures, return typed protocol results when possible.
- For transport-level failure paths, dropping sender/stream is acceptable where already established.
- Client should treat RPC failure as recoverable unless mapped to fatal auth/session errors.

Recommended rule:

- Prefer explicit `Result` payload structs/enums over implicit stream closure for new APIs.

---

## 9) Performance and Reliability Conventions

- Keep protocol payloads compact and typed.
- Use streaming for unbounded/high-volume channels (terminal output, host events).
- Coalesce high-frequency invalidation events at source when possible.
- Never block async executors with shell/fs heavy operations; use `spawn_blocking`.

### 9.1 Observer Hardening (Abuse Protection)

For filesystem observer control RPCs (`FsWatch`, `FsUnwatch`):

- Per-session watch quota: max `128` watched paths.
- Per-session rate limit: token bucket, `10 req/s` refill, `20` burst.
- Invalid/absolute/traversal paths are rejected by normalization.

For host event emission:

- Event emit is non-blocking (`try_send`) to avoid observer stalls.
- Bounded channel applies backpressure by dropping when full.
- Drops are counted via metrics counters:
  - `observer_events_dropped_full`
  - `observer_events_dropped_no_subscriber`

Operational metrics/logging:

- Periodic observer metrics log (current watched paths, sent/dropped counters, rate-limit/quota rejections).
- Explicit warn logs on watch RPC rate-limit and quota rejections.

---

## 10) Protocol Change Checklist (Required)

Any protocol-layer change must include all applicable steps:

1. Update `crates/zedra-rpc/src/proto.rs`.
2. Update host dispatch in `crates/zedra-host/src/rpc_daemon.rs`.
3. Update client handlers in `crates/zedra-session/src/handle.rs`.
4. Add/adjust UI consumers if behavior is user-visible.
5. Update this document (`docs/PROTOCOL_SPECS.md`).
6. Add or update tests:
   - Serialization roundtrip tests in `zedra-rpc`
   - Host/client behavior tests when feasible
7. Mention protocol changes explicitly in PR description.

---

## 11) Protocol Changelog

### 2026-04-29

- Added host-built docs tree RPC:
  - `FsDocsTree(FsDocsTreeReq) -> FsDocsTreeResult`
- Added recursive docs tree payload and typed docs-tree errors:
  - `FsDocNode`
  - `FsDocsTreeError::{InvalidPath, InvalidRequest, CacheMiss, StaleSnapshot, Busy, ScanFailed, Unsupported}`
- Added bounded manual rebuild snapshot behavior:
  - per-session in-memory cache
  - 10 minute cache TTL
  - no ALPN change

### 2026-04-28

- Aligned docs with `ZEDRA_ALPN = b"zedra/rpc/2"`.
- Documented `Authenticate` as a deprecated/reserved append-only enum variant.
- Documented `SwitchSession` as reserved/unsupported for active workspace switching because the current host dispatch loop remains bound to the originally authenticated session.

### 2026-04-27

- Extended the `TermAttach` `seq=0` synthetic metadata preamble to replay cached OSC command lifecycle metadata, including command line, running/idle state, and last exit code, in addition to title, icon name, and cwd.
- Added cached `icon_name` to `TerminalSyncEntry` so `SyncSession` carries the host's latest OSC 1 value alongside title and cwd.
- Clients must continue processing `seq=0` preamble bytes as PTY output while excluding `seq=0` from terminal backlog sequence tracking.

### 2026-04-26

- Added host resource subscription:
  - `SubscribeHostInfo(SubscribeHostInfoReq) -> stream HostInfoSnapshot`
- Added host resource snapshot types:
  - `HostInfoSnapshot`
  - `HostBatteryInfo`
  - `HostBatteryState`
- Client displays the latest snapshot from `WorkspaceState` in the session panel.

### 2026-03-19

- Added filesystem observer RPCs:
  - `FsWatch(FsWatchReq) -> FsWatchResult`
  - `FsUnwatch(FsUnwatchReq) -> FsUnwatchResult`
- Added host invalidation events:
  - `HostEvent::GitChanged`
  - `HostEvent::FsChanged { path }`
- Added observer hardening:
  - per-session watch quota (`128`)
  - per-session watch RPC token-bucket rate limit (`10/s`, burst `20`)
  - non-blocking event emission (`try_send`) with drop metrics
- Switched watch control RPC responses from `ok: bool` to explicit enums:
  - `FsWatchResult::{Ok, InvalidPath, RateLimited, QuotaExceeded, Unsupported}`
  - `FsUnwatchResult::{Ok, InvalidPath, RateLimited, NotWatched, Unsupported}`
- Added compatibility guard:
  - observer RPCs auto-disable client-side on protocol decode/variant mismatch,
    returning `Unsupported` instead of repeatedly retrying incompatible calls.

### 2026-03-28

- Deprecated `Authenticate(AuthReq) -> AuthChallengeResult` as a reserved append-only enum variant.
- Removed the older standalone reconnect RPC from the active protocol.
- Added unified `Connect(ConnectReq) -> ConnectResult` replacing the previous challenge request and standalone reconnect paths:
  - `ConnectReq` carries optional `session_token` for 1-RTT fast path.
  - `ConnectResult::Ok(SyncSessionResult)` — token valid; session attached immediately.
  - `ConnectResult::Challenge { nonce, host_signature }` — no valid token; host embeds PKI challenge inline, saving a separate challenge request RTT.
- `AuthProveResult::Ok` now carries `SyncSessionResult` (piggybacked bootstrap); separate `SyncSession` call no longer needed at connect time.
- Renamed the bootstrap token field to `session_token`.
- Session token storage changed to single-slot (`Option<([u8; 32], SessionToken)>`) per session — only one token is valid at a time, consumed atomically on validation. No TTL.
- ALPN bumped to `zedra/rpc/2` (breaking change).

### 2026-03-26

- Added an earlier fast reconnect bootstrap RPC, later superseded by `Connect(session_token)` on 2026-03-28.
- Added canonical session bootstrap RPC:
  - `SyncSession(SyncSessionReq) -> SyncSessionResult`
- Added `TerminalSyncEntry` to return resumable terminal ids + latest backlog sequence + cached title/CWD.
- Updated connection lifecycle:
  - first pairing and PKI fallback now end with `SyncSession`
  - token resume may skip PKI challenge/AuthProve entirely when a valid host-issued session token is present
