# Daemon Transport

> Canonical path: `specs/daemon/daemon-transport.md`
> Status: living-spec
> Scope: Desktop daemon 和 AgentTeam Server 之间的 first-party transport、remote request、command 模块化、endpoint 边界、兼容 fallback、容量与测试契约。

## 1. Purpose

Daemon Transport 是 Desktop daemon 与 AgentTeam Server 之间的一方传输体系。它服务于三类目标：

1. 让 Server 知道某台 machine 当前是否在线、具备哪些 runtime / transport / endpoint capability、以及 daemon 当前的会话 generation。
2. 让 Server 与 Desktop 交换 daemon command、event、request/response 结果，并保持旧 daemon command 兼容。
3. 在请求变重、响应变大或 Session Link 负载升高时，自动扩展短生命周期的 Elastic Request Link，把高负载 Remote Request 从常驻会话主链路上隔离出去。

WebSocket 是当前主要承载技术之一，不是产品抽象名。代码、spec 和注释应优先使用本规范里的 transport 概念，而不是用具体承载技术命名长期模型。

### 1.1 User-active warm pool child spec

Elastic Request Link 的按需创建、fallback、token、Remote Request envelope、Endpoint Registry 和基础生命周期以本规范为入口。用户活跃窗口内主动保持 idle ERL carrier 的完整设计由子规范 [`Elastic Request Link User-Active Warm Pool`](./elastic-request-link-user-active-warm-pool.md) 定义。

该子规范是 Daemon Transport 的 ERL 子规范，覆盖：

- `RemoteRequestOrigin`、`WarmPoolPolicy`、`WarmLease`、`WarmEndpointProfile`、`LinkGrantSpec` 和 `WarmLinkState` 等一等模型；
- Web/API 用户请求 origin 与 endpoint 分类解耦：用户触发的 demand-only endpoint 可以刷新 warm lease，但不会进入 warm allowlist，也不会改变当前请求的 LinkPolicy/fallback/no-retry 语义；
- registry-driven warm profile 和 guardrail，防止 destructive/non-idempotent endpoint 被误加到默认 warm profile；
- warm controller 状态机、cooldown/backoff、same-node/sticky、权限/fallback/metrics/test gate；
- 先写 Spec、再做模块化重构和行为实现的迁移路线，以及后续代码注释必须解释行为存在原因和边界的要求。

修改 ERL warm pool 语义时，必须同步检查本入口摘要与子规范，避免 Daemon Transport 主规范和 warm pool 子规范漂移。

## 2. Vocabulary

### 2.1 Preferred terms

| Term | Meaning |
| --- | --- |
| `Daemon Transport` | Desktop↔Server 的整体传输体系。它包含 Session Link、Elastic Request Link Pool、Command Registry、Endpoint Registry、Link Policy、Remote Request tracking 和观测。 |
| `Session Link` | 每台 machine 的常驻主会话连接。它绑定 machine identity、workspace、session generation 和 connection-scoped capabilities。低流量时，只有 Session Link 存在。 |
| `Session Generation` | Session Link 每次成功建立后生成的单调 generation。所有 Elastic Request Link、link grant token、capability snapshot 和 pending Remote Request assignment 都绑定到当前 generation。 |
| `Elastic Request Link` | Desktop 按需拨入 Server 的弹性请求连接。它用于承载较重的 Remote Request；同一时刻只处理一个 request，可短暂复用，空闲后关闭。 |
| `Elastic Request Link Pool` | 同一 machine 当前可用的 Elastic Request Link 集合。Pool 从 0 开始，按负载增长到上限，并在空闲、generation 变化或异常时收缩。 |
| `Remote Request` | Server 对 Desktop 上某个 allowlisted Remote Endpoint 的一次请求/响应调用。它替代历史设计里容易显得过重的 `Invocation` 概念。 |
| `Remote Endpoint` | Desktop 公开给 Server 内部调用的 typed allowlist 能力，例如 `workspace.list`。Remote Endpoint 不是任意本地 HTTP forward。 |
| `Command Registry` | daemon command 的注册表。它声明 command 类型、方向、domain、schema、capability、timeout、fallback、owner 和测试要求。 |
| `Endpoint Registry` | Remote Endpoint 的注册表。它声明 endpoint id、schema、link policy、deadline、limits、fallback command 和 Desktop handler/adapter。 |
| `Link Policy` | Remote Request 的选路策略。它决定 request 可走 Session Link、优先走 Elastic Request Link、必须走 Elastic Request Link，或失败时是否 fallback。 |

### 2.2 Terms to avoid for this model

- Do not introduce a new transport object whose name collides with AgentTeam conversation/product surfaces.
- Do not describe the long-lived daemon connection as a pure control-plane link; use `Session Link`.
- Do not name the elastic request connection after the carrier technology; use `Elastic Request Link`.
- Prefer `Remote Request` over `Invocation` unless referencing a historical artifact.
- Prefer `Remote Endpoint` over “local HTTP forward”; forwarding is not the security model.

## 3. Transport model

Each connected machine has exactly one active Session Link and zero or more Elastic Request Links:

```text
machine
  ├─ Session Link                 always-on while daemon is online
  └─ Elastic Request Link Pool    0..N, created under load, closed when idle
```

### 3.1 Session Link responsibilities

The Session Link is the primary daemon session. It carries:

- machine online/offline and replacement semantics;
- ready payload, capability snapshot, endpoint registry summary, runtime defaults, running agents, and daemon version;
- heartbeat and session health;
- agent lifecycle commands, delivery commands, and lightweight daemon events;
- legacy command request/response flows that must remain compatible with older Desktop builds;
- lightweight Remote Request traffic when the selected Link Policy and current load allow it;
- open/drain/cancel/status messages for Elastic Request Link lifecycle.

A Session Link is not limited to “coordination-only” work. The important invariant is that heavy, large, slow, or latency-sensitive request/response work must be eligible for isolation on Elastic Request Links so it cannot starve the primary session duties.

### 3.2 Elastic Request Link responsibilities

An Elastic Request Link is a Desktop-initiated request carrier. It exists to isolate heavier request/response traffic while preserving the same NAT/firewall model as the Session Link.

An Elastic Request Link:

- is opened by Desktop after Server sends an open instruction on the Session Link;
- authenticates with a short-lived single-use Bearer token;
- binds to `workspaceId`, `machineId`, `sessionGeneration`, `linkId` or `linkScope`, purpose, expiry, and link limits;
- handles at most one in-flight Remote Request at a time;
- may remain idle briefly for reuse;
- closes after idle timeout, max age, drain, cancel, generation change, auth failure, or transport failure.

The first version does not multiplex concurrent assignments over one Elastic Request Link. A link receives at most one Remote Request envelope at a time; parallelism comes from multiple links in the pool.

### 3.3 Carrier paths

The current Session Link carrier uses the authenticated daemon WebSocket route:

```text
GET /api/v1/workspaces/:workspaceId/daemon/connect?key=...
```

Elastic Request Link carrier routes must be workspace-scoped and link-scoped. They use short Bearer tokens rather than the long-lived machine key. A route shape should keep `linkId` visible in the path for routing/debuggability while keeping authorization in the token, for example:

```text
GET /api/v1/workspaces/:workspaceId/daemon/elastic-request-links/:linkId/connect
Authorization: Bearer <short-lived single-use link token>
```

The exact carrier can evolve, but the product/API contract is the same: Desktop initiates the connection to Server, Server validates and consumes the link token, the link joins the Elastic Request Link Pool, and each concrete Remote Request is authorized by the typed envelope that Server sends over that link. No caller receives raw carrier-level failures.

## 4. Capability negotiation and automatic enablement

Daemon Transport is capability-driven.

There is no normal user-facing or product feature flag for “turning on” Elastic Request Link. If both sides support the protocol, endpoint registry, endpoint policy, and current link health, Server may choose Elastic Request Link automatically. If any required capability is missing or unhealthy, Server falls back to Session Link behavior according to the endpoint policy.

An operator-only emergency kill switch may exist for rollback safety. It is not part of normal enablement and must not become the default decision point.

### 4.1 Ready payload additions

A Desktop that supports this spec should advertise transport capability in its ready payload:

```json
{
  "type": "ready",
  "capabilities": [
    "daemon-transport:v1",
    "daemon-transport:elastic-request-link:v1",
    "daemon-endpoints:v1"
  ],
  "daemonTransport": {
    "schemaVersion": 1,
    "sessionGeneration": "...",
    "maxElasticRequestLinks": 3,
    "hardMaxElasticRequestLinks": 4,
    "supportedLinkModes": ["session", "elastic_request"],
    "endpointRegistryVersion": "...",
    "supportedEndpoints": [
      {"endpointId": "workspace.list", "version": 1}
    ]
  }
}
```

`daemonTransport` is connection-scoped operational state. It should be stored in the active Session Link state and may be persisted only as diagnostic metadata when values change. It must not sit on a hot telemetry write path.

### 4.2 Enablement predicate

For a Remote Request to use Elastic Request Link, all of these must be true:

1. Server build supports Daemon Transport v1 and the endpoint's Link Policy.
2. The current Desktop ready payload advertises `daemon-transport:elastic-request-link:v1`.
3. Session Generation is current and the Session Link is healthy enough to send open/cancel/drain instructions.
4. Server and Desktop agree on endpoint id, endpoint version, endpoint registry version or compatible range.
5. The endpoint is allowlisted on both Server and Desktop.
6. The endpoint policy permits Elastic Request Link for this request class.
7. Per-machine capacity and per-endpoint budgets allow another request.
8. Sticky/same-node routing can return the Elastic Request Link to the Server node that owns the pending request.
9. The emergency kill switch, if present, is not active.

If any predicate fails, Server must not expose that failure to Web/API callers. It must either use the configured Session Link fallback or return the endpoint's stable business error.

### 4.3 Turn-state snapshot and connection-epoch fencing

The Daemon Transport session generation doubles as the **connection epoch** for
Agent turn-state publication. It is rotated on every connect
(`rotateDaemonTransportSessionGeneration`), carried in the ready payload's
`daemonTransport.sessionGeneration`, and cleared on disconnect.

The **authoritative current-epoch turn-state snapshot** is the ready payload's
`runningAgentTurnStates` (`agentId -> "active"|"queued"|"idle"`). Server applies
it only for the current connection and marks the connection as having received a
snapshot. Incremental `agent:turn_state` events after ready carry the same
current epoch:

```json
{"type": "agent:turn_state", "agentId": "agt_1", "turnState": "idle", "sessionId": "sess_1", "epoch": "<sessionGeneration>", "revision": 7}
```

**Same-connection monotonic ordering.** The daemon serializes fresh turn-state
capture, revision allocation, and enqueue under a single publication boundary
(`turnStatePublicationMu`) for both ready snapshots and incremental events —
state is re-read inside the boundary, never stamped from a precomputed stale
value. **Every modern ready publication carries an explicit snapshot watermark**
(`runningAgentTurnStatesWatermark`), even when `runningAgentTurnStates` is
empty/omitted and even when `runningAgents` is empty: an empty snapshot is still
an authoritative current-epoch snapshot whose watermark can retire prior
per-agent state. Every increment carries a **strictly newer revision** than the
watermark captured at ready time. Server fencing rules:

1. **Session fence:** a `turn_state` whose `sessionId` does not match the current
   agent session is a stale event from a replaced session and is dropped; the
   current known state is preserved (never cleared to unknown).
2. **Connection-epoch fence:** a `turn_state` whose `epoch` is non-empty and does
   not equal the current connection's `sessionGeneration` is an old-connection
   event and is dropped. Events without an `epoch` are accepted for legacy daemon
   compatibility.
3. **Presence-aware watermark decoding:** the server decodes the watermark's
   presence separately from its numeric value. A present watermark (including
   zero) is a modern snapshot claiming ordering authority; a ready that omitted
   the field is a legacy snapshot that must NOT claim modern ordering authority
   and is applied with clear-and-replace (pre-ordering) semantics. A legacy ready
   never updates per-agent revisions or the connection high-water.
4. **Same-epoch ordering fence:** the server keeps per-agent revisions and a
   connection high-water. A modern snapshot is applied **by per-agent merge** — an
   older snapshot (lower watermark) never overwrites a newer per-agent increment,
   while its still-current entries for other agents remain materializable; an
   agent absent from a snapshot whose stored revision is older/equal is cleared
   (an explicit empty current snapshot retires stale working state); an increment
   with `revision <=` the applied high-water is dropped. Only an accepted
   (non-regressing) snapshot updates the high-water and snapshot diagnostics.
   Ordering resets only on a genuinely new authenticated connection (fresh
   `clientState`).
5. **Bounded snapshot grace:** when the machine is online but has not yet applied
   a current-epoch snapshot (or a specific responder is absent from it), Workflow
   callback recovery defers the responder for a bounded window anchored to the
   connection timestamp (`MachineConnectedAt`). Duplicate ready messages,
   polling, restart, and old-epoch races cannot extend the window because the
   anchor is never moved. After the grace elapses the responder becomes
   recoverable (idle) so recovery can never stall on an empty Hub after a daemon
   restart or reconnect.

## 5. Link Policy

A Link Policy belongs to a Remote Endpoint and may also be refined per request class.

Recommended policy values:

| Policy | Meaning |
| --- | --- |
| `session_only` | Always use Session Link. Suitable for presence, heartbeat, simple lifecycle commands, and events. |
| `session_preferred` | Try Session Link unless request size, expected latency, queue depth, or current Session Link pressure crosses policy thresholds. |
| `elastic_preferred` | Prefer Elastic Request Link when capability and capacity exist; fallback to Session Link legacy command when allowed. |
| `elastic_required` | Requires Elastic Request Link. If unavailable, return the endpoint's stable unavailable/busy error. Use sparingly. |

A policy also declares:

- request timeout and Desktop dial timeout;
- max request bytes and max response bytes;
- per-machine and per-endpoint concurrency budgets;
- fallback command type, if any;
- retry/fallback behavior after partial failures;
- observability outcome labels.

## 6. Elastic Request Link capacity and lifecycle

### 6.1 Capacity defaults

Default per machine:

```text
Session Link:              1
Elastic Request Links:     0 idle baseline, up to 3 by default
Hard cap:                  configurable up to 4
```

Rationale:

- Low traffic keeps the architecture simple: one Session Link is enough.
- Three elastic links cover common management concurrency, such as workspace list/read plus review or scan.
- The default avoids unnecessary file descriptor, goroutine, TLS/upgrade, and memory pressure.
- Operators can raise the hard cap to 4 for deployments that show sustained `desktop_busy` or fallback pressure.

### 6.2 Expansion

When Server has a Remote Request:

1. If policy allows Session Link and current Session Link pressure is below threshold, use Session Link.
2. If policy prefers or requires Elastic Request Link and an idle elastic link is available, assign it.
3. If no idle elastic link exists and the pool is below cap, Server sends an open instruction on the Session Link. The instruction carries `linkId`, connect URL, single-use link token, expiry, and link limits.
4. If Desktop dials in before `connectTimeout`, Server validates and consumes the link token, adds the link to the pool, assigns at most one pending Remote Request by sending its typed envelope, and marks the link busy until the response/cancel/deadline.
5. If the pool is at cap, or Desktop does not dial in before timeout, apply fallback policy.

Pressure signals should be low-cardinality and local to the machine: pending request count, Session Link send queue depth, recent request latency, response size class, and endpoint policy. The system should not need a global scheduler for V1.

### 6.3 Reuse and shrink

An Elastic Request Link may be reused for another Remote Request only when it is idle, still bound to the current Session Generation, and within max age.

Recommended lifecycle values:

| Setting | Default |
| --- | --- |
| Desktop dial timeout | 5s |
| Request deadline | endpoint policy, commonly 10-15s for management endpoints |
| Idle timeout | 30s |
| Max age | 2-5min |

Shrink behavior:

- idle timeout closes idle links;
- drain closes idle links and prevents new assignment;
- active requests are not interrupted by ordinary drain;
- request deadline, cancel, generation change, Session Link disconnect, auth failure, or emergency kill switch closes affected links;
- cleanup must remove pending requests, consumed tokens, pool membership, and result waiters.

## 7. Remote Request envelope

Remote Request uses a typed per-request envelope independent of the link carrier. The carrier token authorizes link establishment; the envelope authorizes one concrete endpoint request on an already-established Session Link or Elastic Request Link.

```json
{
  "type": "remote_request",
  "schemaVersion": 1,
  "requestId": "rrq_...",
  "endpointId": "workspace.list",
  "endpointVersion": 1,
  "deadlineAt": "2026-06-30T10:00:00Z",
  "limits": {
    "requestBytes": 65536,
    "responseBytes": 262144
  },
  "payload": {}
}
```

The response also uses a typed envelope:

```json
{
  "type": "remote_response",
  "schemaVersion": 1,
  "requestId": "rrq_...",
  "outcome": "ok",
  "payload": {},
  "error": null
}
```

Stable outcomes include:

| Outcome | Meaning |
| --- | --- |
| `ok` | Request completed successfully. |
| `endpoint_error` | Endpoint handler returned a typed business error. |
| `desktop_busy` | Desktop or per-machine link capacity is full and no fallback path is available. |
| `desktop_unavailable` | Required transport/capability/endpoint is unavailable and no fallback path is available. |
| `deadline_exceeded` | Request or dial deadline expired and no fallback succeeded. |
| `canceled` | Server request context, Session Link generation, or Desktop execution was canceled. |
| `protocol_error` | Envelope/schema/version violation. This is logged in a redacted way and returned as a stable error. |

Raw carrier errors, token values, local filesystem paths, HTTP paths, stack traces, and raw Desktop errors must not cross the product API boundary.

## 8. Token and sticky routing contract

### 8.1 Token claims

Elastic Request Link tokens are short-lived and single-use link-establishment tokens. Claims must bind at least:

- `workspaceId`;
- `machineId`;
- `sessionGeneration`;
- `linkId` or link-scope id;
- purpose, such as `elastic_request_link`;
- expiry;
- issuer Server node id or routing hint;
- link max age, idle timeout, and optional request class limits.

A token is consumed as soon as the Server validates an Elastic Request Link successfully. Repeated dial, late dial after timeout/cleanup, link id mismatch, workspace/machine mismatch, generation mismatch, or expired token must be rejected. Endpoint id/version authorization happens later on each Remote Request envelope through Endpoint Registry checks.

Rejected token attempts must be logged only with redacted, low-cardinality reason labels such as `expired`, `replayed`, `link_mismatch`, `generation_mismatch`, or `unknown_link`.

### 8.2 Same-node V1 requirement

V1 link grant state and pending Remote Request assignment state are Server-process memory. Therefore, an Elastic Request Link must return to the same Server node that created its link grant, and each assignment on that link must be owned by that same node.

Deployment must ensure this with one of:

- sticky routing;
- connect URL containing a node hint understood by the gateway;
- a Server-generated link connect URL that resolves to the owning node.

If the link arrives at a node without the link grant or current assignment state, that node must reject it with a stable redacted reason. The owning request then times out or receives a same-node failure and applies fallback policy. V1 does not require shared link-grant or pending-assignment state.

## 9. Endpoint Registry

Server must never expose an arbitrary tunnel or forward primitive through product APIs. Product APIs call `RemoteDesktopClient.Invoke(machineId, endpointId, payload)`, and that client resolves the endpoint through Endpoint Registry and Link Policy.

### 9.1 Endpoint descriptor

A Remote Endpoint descriptor should include:

```go
type EndpointDescriptor struct {
    EndpointID        string
    Version           int
    Owner             string
    RequestType       any
    ResponseType      any
    CapabilityGate    string
    LinkPolicy        LinkPolicy
    FallbackCommand   string
    Timeout           time.Duration
    MaxRequestBytes   int64
    MaxResponseBytes  int64
    MetricsName       string
}
```

Descriptor fields are the source of truth for schema, capability gating, fallback, limits, metrics, and review ownership.

### 9.2 Desktop allowlist

Desktop must have its own Endpoint Registry. A Remote Request is valid only when Desktop recognizes the endpoint id and version and maps it to a typed handler/adapter.

Desktop may implement a handler by calling its local daemon HTTP Server or internal service code, but the adapter must be endpoint-specific. It must not convert arbitrary Server-provided method/path/header/body into arbitrary local HTTP calls.

### 9.3 First remote endpoint example

`workspace.list` is a good first Remote Endpoint because it is useful, bounded, read-only, and already has a legacy Session Link command fallback.

A descriptor may look like:

```text
endpointId:       workspace.list
version:          1
linkPolicy:       session_preferred or elastic_preferred
fallbackCommand:  agent:workspace:list
capabilityGate:   daemon-endpoints:v1 + workspace.list@1
limits:           bounded request, bounded page response
```

This example is illustrative. The spec does not require every endpoint to choose the same policy.

### 9.4 `machine.directory.list@1` — demand-only directory browsing

`machine.directory.list@1` lets the Server browse directories on a connected Desktop so the Explorer Project Workspace picker can fill the Root path field. It is demand-only, read-only, and has **no legacy command fallback**. Explicit capability preflight is required before `invokeRemoteEndpoint`.

Descriptor:

```text
endpointId:       machine.directory.list
version:          1
owner:            machine
capabilityGate:   daemon-endpoints:v1 + machine.directory.list@1
linkPolicy:       elastic_preferred
fallbackCommand:  none
timeout:          15s
limits:           request 64 KiB, response 512 KiB
warmMetadata:     demand-only, read-only
safeRetryFallback: false
```

Wire contract:

- Payload: `{ path, limit, cursor }`. `path` is forwarded byte-for-byte; an empty `path` means the user home directory. `limit` defaults to 50, caps at 200. Cursor is a strict decimal offset in raw directory entries up to 1,000.
- Response: `{ currentPath, parentPath, directories[], limit, cursor, hasMore, nextCursor, truncated }`. Only directories are returned; files and symlinks are skipped. `directories` entries are `{ name, path }` with native separators.
- One request reads at most `min(cursor + limit + 1, 1,001)` raw entries and stops before a candidate row that would exceed the 448 KiB page budget (below the 512 KiB descriptor limit). `truncated=true` with `hasMore=false` signals the user should type a more specific absolute path.

Stable outcomes and errors:

- Desktop adapter maps filesystem classification to `machine_directory_invalid` (relative/missing/non-directory/invalid cursor) or `machine_directory_unreadable` (permission, I/O, stat, home resolution failures). Sentinels are path-free.
- Server Hub capability preflight returns `machine_directory_unsupported` when the connected Desktop does not advertise this endpoint, and `machine_directory_unavailable` when the machine is offline or the endpoint times out. `machine_directory_busy` covers `desktop_busy`.
- No post-execution retry. Every route failure is post-classified to one of the five typed sentinels before reaching the Workspace API. No path appears in the request URL, daemon metrics labels, request logs, or HTTP spans.

## 10. Command Registry and domain review

Daemon command handling must move toward registry + typed handler dispatch. The goal is to make the existing Session Link command surface maintainable while Remote Request evolves separately.

### 10.1 Command descriptor

A Command Descriptor should declare:

```go
type CommandDescriptor struct {
    Name           string // canonical name, e.g. "agent.workspace.list"
    WireType       string // existing wire type, e.g. "agent:workspace:list"
    Domain         string
    Kind           string // command, event, request, response
    Direction      string // server_to_desktop or desktop_to_server
    SchemaVersion  int
    RequestType    any
    ResponseType   any
    CapabilityGate string
    LinkPolicy     LinkPolicy
    Timeout        time.Duration
    Fallback       string
    Critical       bool
    Owner          string
    MetricsName    string
}
```

All new command types must have a descriptor. Guardrail tests should fail when a command lacks domain, direction, owner, schema, capability/fallback decision, and focused tests.

### 10.2 Recommended domains

The current daemon command surface is best split by product responsibility, not by carrier implementation:

| Domain | Responsibility | Existing examples |
| --- | --- | --- |
| `session` | ready/capability, heartbeat, session generation, transport health, reconnect replacement. | `ready`, `ping`/`pong` |
| `agent.lifecycle` | agent process lifecycle and identity/runtime configuration. | `agent:start`, `agent:stop`, `agent:sleep`, `agent:update_identity`, `agent:reset-workspace` |
| `agent.delivery` | message delivery, wake status, activity/session/status, runtime events. | `agent:deliver`, `agent:deliver:ack`, `agent:wake_status`, `agent:status`, `agent:activity`, `agent:session`, `agent:runtime_event` |
| `agent.workspace` | agent workspace file browse/read/write/delete. Candidate for Remote Endpoint migration. | `agent:workspace:list`, `agent:workspace:read`, `agent:workspace:apply_seed_files`, `agent:workspace:delete` and result types |
| `machine.workspace` | machine-level workspace catalog, scan, delete, and list operations. | `machine:workspace:scan`, `machine:workspace:delete`, `machine:workspaces:list` and result types |
| `review` | review context summary/files/diff and output target validation. Candidate for Remote Endpoint migration after workspace basics. | `agent:review:summary`, `agent:review:files`, `agent:review:diff`, `agent:output-target:validate` and result types |
| `runtime_access` | runtime-adjacent state access, trajectory, recovery points, memory/assets access notifications. | `agent:trajectory`, `agent:trajectory:history`, `agent:restore_recovery_point`, `agent:gc_recovery_points`, `agent:memory:access-changed`, `agent:assets:access-changed` |
| `transport.elastic_request` | Elastic Request Link lifecycle and Remote Request coordination. | open, accepted, rejected, drain, cancel, status, close reason |

Domain naming may be refined as code ownership becomes clearer, but the split must keep large request/response work out of generic session handling and prevent unbounded growth of a single protocol switch.

### 10.3 Layering

The intended layering is:

```text
Daemon Transport
  ├─ Session Link
  ├─ Elastic Request Link Pool
  └─ Link Policy

Protocol Modules
  ├─ Command Registry
  ├─ Endpoint Registry
  ├─ typed sender
  └─ request tracker

Product Handlers
  ├─ agent lifecycle
  ├─ agent delivery
  ├─ workspace
  ├─ machine workspace
  ├─ review
  └─ runtime access
```

Transport code must not know product-specific handler internals. Product handlers must not open raw carrier connections or bypass Link Policy.

## 11. Fallback matrix

Fallback is automatic and policy-driven. Web/API callers should not know whether a request used Session Link, Elastic Request Link, or a legacy Session Link command.

| Scenario | Expected behavior |
| --- | --- |
| Server build does not support Elastic Request Link | Existing Server behavior continues: use Session Link legacy command. |
| Desktop does not advertise transport capability | Server uses Session Link legacy command for endpoints with fallback. |
| Desktop advertises transport capability but not endpoint/version | Fallback when allowed; otherwise stable `desktop_unavailable`. |
| Endpoint Registry versions are incompatible | Fallback when allowed; otherwise stable `desktop_unavailable`. |
| Session Link is disconnected | Use existing offline semantics; do not attempt Elastic Request Link. |
| Session Generation changes while request is pending | Cancel pending request, invalidate elastic links/tokens, then retry or fallback according to policy. |
| Elastic Request Link pool is at cap | Fallback when allowed; otherwise stable `desktop_busy`. |
| Desktop does not dial before connect timeout | Cleanup pending link grant, link token, and pending assignment, then fallback when allowed. |
| Link token expired, replayed, mismatched, or unknown | Reject link, redact reason, cleanup; owning request falls back or returns stable error. |
| Link arrives at wrong Server node | Reject link with redacted same-node failure; owning request falls back or times out with stable error. |
| Elastic Request Link disconnects before response | Cleanup; fallback only if endpoint policy marks the operation safe to retry. |
| Desktop handler returns fallback-eligible error | Fallback if configured; otherwise return typed endpoint error. |
| Response exceeds endpoint limit | Close request with stable limit error; do not stream oversized payload over Session Link as fallback unless policy explicitly allows a bounded alternative. |
| Emergency kill switch active | Treat Elastic Request Link as unavailable and use Session Link fallback. |

Fallback must not duplicate non-idempotent operations. Endpoint descriptors must mark whether retry/fallback after partial execution is safe.

## 12. Error, logging, and metrics contract

### 12.1 Stable product errors

Public API-facing errors should use stable categories:

- `desktop_unavailable`;
- `desktop_busy`;
- `desktop_request_timeout`;
- `desktop_endpoint_unavailable`;
- endpoint-specific typed business errors.

They must not include carrier type, token content, raw local path, raw Desktop stack, or gateway internals.

### 12.2 Low-cardinality metrics

Metrics should be bounded by endpoint id, outcome, link mode, and reason class. The concrete collector contract is listed in [`specs/observability/metrics.md`](../observability/metrics.md) §6.4. Examples:

- active elastic request link pool gauges by bounded state;
- pending remote request assignment gauges by bounded state;
- open attempts and open outcomes;
- request duration by endpoint and link mode;
- fallback count by endpoint and reason class;
- busy/unavailable count by endpoint;
- cleanup reason count.

Do not label metrics with request id, machine id, workspace id, token hash, raw error, local path, or URL.

### 12.3 Audit logs

Audit logs may include request id, endpoint id, link mode, generation, outcome class, and redacted reason. They must not include tokens, authorization headers, local paths, raw payloads, or raw handler errors by default.

## 13. Testing contract

Tests must cover both behavior and guardrails.

### 13.1 Capability and compatibility matrix

- new Server + new Desktop: matching capability and endpoint uses Link Policy-selected path;
- new Server + old Desktop: Session Link legacy command fallback;
- old Server + new Desktop: Desktop keeps legacy Session Link command handlers working;
- missing transport capability, missing endpoint capability, incompatible endpoint version, and incompatible registry version all fallback or return stable unavailable according to endpoint policy;
- emergency kill switch, if implemented, forces fallback without changing capability semantics.

### 13.2 Fallback paths

- connect timeout;
- pool cap reached;
- Desktop rejects open;
- token expired/replayed/link-mismatched;
- wrong Server node / missing pending request;
- Session Generation changes;
- Session Link disconnects;
- Elastic Request Link disconnects before response;
- response over limit;
- handler returns fallback-eligible error;
- handler returns non-fallback error.

### 13.3 Cleanup and lifecycle

- successful link establishment consumes the single-use link token; successful request completion cleans pending request and result waiter state;
- timeout cleans pending assignment and closes or invalidates the affected link when required;
- cancel from API context cleans pending request and sends cancel/drain when possible;
- generation change closes all old elastic links;
- idle timeout closes only idle links;
- drain does not interrupt in-flight request before deadline;
- repeated cleanup is idempotent.

### 13.4 Registry guardrails

- Every command has a Command Descriptor with domain, direction, kind, schema, capability/fallback decision, owner, and tests.
- Every Remote Endpoint has an Endpoint Descriptor with typed request/response, link policy, deadline, limits, metrics, and Desktop allowlist handler.
- Desktop rejects unknown endpoint, unsupported endpoint version, wrong method/purpose, oversized request, and oversized response.
- Server rejects unknown command/endpoint in a redacted and bounded way.

### 13.5 Security and redaction

- No test snapshot may contain raw token, Authorization header, machine API key, local absolute path from a handler error, or raw carrier error.
- Token single-use semantics are tested: successful link dial consumes it; replay is rejected.
- Link id mismatch, generation mismatch, and endpoint-envelope mismatch are tested.
- Wrong-node / no-link-grant / no-pending-assignment paths are tested when the gateway or route hint cannot return the link to the owning Server node.

## 14. Documentation and code comment guidance

Specs and code comments should reinforce the conceptual layering:

- Say “Session Link” when describing the long-lived daemon session.
- Say “Elastic Request Link” when describing elastic request carriers.
- Say “Remote Request” for a Server-initiated request/response operation targeting Desktop.
- Say “Remote Endpoint” for typed allowlisted Desktop capability.
- Say “carrier” when referring to WebSocket or a future transport technology.
- Say “Session Link fallback” or “legacy command fallback” when the request falls back to existing Session Link command behavior.

Good comment examples:

```go
// SessionLink owns the authenticated daemon session generation and the
// compatibility command path. It may carry light Remote Requests, but LinkPolicy
// can move heavier requests to Elastic Request Links.
```

```go
// ElasticRequestLink is a single-flight request carrier. It is Desktop-initiated,
// link-token-bound, generation-bound, and may be reused only while idle and healthy.
```

```go
// EndpointRegistry is the allowlist for Server-initiated Desktop capabilities.
// It maps endpoint ids to typed handlers; it is not an arbitrary local forwarder.
```

Avoid comments that make the carrier technology the durable abstraction or imply an arbitrary tunnel/forwarding capability.
