Example: Observer Mid-Session Join
Example: Observer Mid-Session Join
A controller (Alice) has been running a session for several minutes. An observer (Bob) attaches mid-stream to watch. He receives a `Keyframe` that captures the current screen state so he sees a coherent terminal without having to replay the entire history.
Pre-conditions
- Session `01HW8Q3M9C7K5P0XZ8N4F2RV1Q` has been running ~5 minutes.
- One controller (Alice, `c-alice-7f3a`) is attached.
- Server's `current_sequence = 248` (frames `0..248` already produced and partly evicted).
- Replay buffer retains frames since `sequence = 50` (frames 0..49 evicted).
- Last server-emitted Keyframe was at `sequence = 240` with `anchor_sequence = 240`.
1. WebSocket upgrade
Bob connects to the same URL with his bearer token:
GET /agents/inst-42/sessions/01HW8Q3M9C7K5P0XZ8N4F2RV1Q/attach HTTP/1.1
Sec-WebSocket-Protocol: pty-ws.v1
Authorization: Bearer eyJhbGciOi...<bob's token>
2. Server → Bob: `binding_hello`
{
"op": "binding_hello",
"id": "01hw8qhz5x0a8m7r9z3b4tc6a0",
"ts": "2026-05-09T14:35:00.000Z",
"sequence": 0,
"payload": {
"binding_uri": "https://agentic-sandbox.aiwg.io/bindings/pty-ws/v1",
"binding_version": "1.0.0",
"supported_operations": [
"SendMessage", "SendStreamingMessage", "GetTask",
"ListTasks", "CancelTask", "SubscribeToTask"
],
"activated_extensions": [],
"session": {
"session_id": "01HW8Q3M9C7K5P0XZ8N4F2RV1Q",
"current_sequence": 248
}
}
}
The `current_sequence` lets Bob know where the session is in its lifecycle. Bob does not request `replay_from` because he's joining for the first time — he wants the live stream, not history.
3. Bob → server: `pty.join_session` as observer
{
"op": "pty.join_session",
"id": "01hw8qj175a8m7r9z3b4tc6a1",
"ts": "2026-05-09T14:35:00.080Z",
"extensions": [
"https://agentic-sandbox.aiwg.io/extensions/pty-extensions/v1"
],
"service_parameters": {
"trace_id": "00-aabbccddeeff00112233445566778899-1122334455667788-01"
},
"payload": {
"role": "observer",
"client_label": "bob@review"
}
}
4. Server → Bob: `RoleAssigned`
{
"op": "pty.session_frame",
"id": "01hw8qj1a3a8m7r9z3b4tc6a2",
"ts": "2026-05-09T14:35:00.110Z",
"sequence": 249,
"payload": {
"kind": "RoleAssigned",
"role": "observer",
"client_id": "c-bob-9f1e"
}
}
5. Server → all attached: `MembershipChanged`
Both Alice and Bob receive this frame:
{
"op": "pty.session_frame",
"id": "01hw8qj1b4a8m7r9z3b4tc6a3",
"ts": "2026-05-09T14:35:00.111Z",
"sequence": 250,
"payload": {
"kind": "MembershipChanged",
"controllers": [
{ "client_id": "c-alice-7f3a", "label": "alice@laptop" }
],
"observers": [
{ "client_id": "c-bob-9f1e", "label": "bob@review" }
]
}
}
6. Server → Bob: `Keyframe`
The server sends Bob a fresh Keyframe so his terminal has a coherent starting screen state without needing to replay earlier output:
{
"op": "pty.session_frame",
"id": "01hw8qj1c5a8m7r9z3b4tc6a4",
"ts": "2026-05-09T14:35:00.112Z",
"sequence": 251,
"payload": {
"kind": "Keyframe",
"snapshot": "<base64 snapshot of the current 140x40 terminal state>",
"snapshot_format": "vt100-screen-state-v1",
"snapshot_cols": 140,
"snapshot_rows": 40,
"anchor_sequence": 251
}
}
Bob renders the snapshot. He now sees the same screen Alice sees — including in-progress command output, cursor position, and SGR (color/style) state.
The Keyframe is emitted only to Bob, not broadcast. Other attached clients are not interrupted.
7. Live frames flow to all clients
From this point forward, every server-initiated frame goes to both Alice and Bob:
Alice types another keystroke → server emits an `Output` frame:
{
"op": "pty.session_frame",
"id": "01hw8qj4a6a8m7r9z3b4tc6a5",
"ts": "2026-05-09T14:35:03.200Z",
"sequence": 252,
"payload": {
"kind": "Output",
"stream": "stdout",
"data": "ZWNobyBoZWxsbwo="
}
}
Both Alice and Bob receive this frame. Bob renders it on top of the snapshot from §6.
8. Bob attempts input — denied
If Bob tries to send `pty.session_input`:
{
"op": "pty.session_input",
"id": "01hw8qj7d7a8m7r9z3b4tc6a6",
"ts": "2026-05-09T14:35:10.000Z",
"payload": { "data": "ZXZpbAo=" }
}
The server replies with a binding-level `Error` frame and does not apply the input:
{
"op": "Error",
"id": "01hw8qj7d7a8m7r9z3b4tc6a6",
"ts": "2026-05-09T14:35:10.005Z",
"payload": {
"code": "PERMISSION_DENIED",
"message": "observers cannot send pty.session_input",
"a2a_error": { "type": "PermissionDenied" },
"retryable": false
}
}
Note: the `id` echoes Bob's request `id` so he can correlate the rejection. The connection remains open; this is an operation-level rejection, not a connection-level fault.
9. Bob leaves
{ "op": "pty.leave_session", "id": "01hw8qjz188a8m7r9z3b4tc6a7", "ts": "2026-05-09T14:40:00.000Z", "payload": {} }
Server broadcasts `MembershipChanged` to remaining clients:
{
"op": "pty.session_frame",
"id": "01hw8qjz199a8m7r9z3b4tc6a8",
"ts": "2026-05-09T14:40:00.010Z",
"sequence": 312,
"payload": {
"kind": "MembershipChanged",
"controllers": [
{ "client_id": "c-alice-7f3a", "label": "alice@laptop" }
],
"observers": []
}
}
Bob's connection closes (`1000`). Alice's session continues uninterrupted.
Sequence summary
Bob Server Alice
│ HTTP upgrade ───────────▶│ │
│◀── binding_hello │ │
│ pty.join_session(obs) ──▶│ │
│◀── RoleAssigned (249) │ │
│◀── MembershipChanged ────┼───────────────────────────────▶│
│◀── Keyframe (251) │ │
│ │ (Alice keystroke arrives) │
│◀── Output (252) ─────────┼───────────────────────────────▶│
│ pty.session_input ──────▶│ │
│◀── Error PERMISSION_DEN. │ │
│ pty.leave_session ──────▶│ │
│◀── MembershipChanged ────┼───────────────────────────────▶│
│◀── WS close 1000 │ │
Notes
- The Keyframe at step 6 is the mechanism that makes mid-session join feasible. Without it, Bob would either need to replay from `sequence = 0` (potentially expensive and likely outside retention) or accept a corrupted screen.
- `anchor_sequence: 251` tells Bob he can discard any previously buffered frames at or below that sequence. Since he just joined, he has no such buffer.
- Bob's read access is identical to Alice's. If Alice typed a password between sequences 240 and 251, that password is part of the Keyframe snapshot Bob receives. See `pty-extensions/v1` spec §10.3.