---
title: "Handoffs, snapshots and pair sessions"
---

# Handoffs, snapshots and pair sessions

Freeze retrieved context as an immutable snapshot, hand it to a reviewer by pull request URL, and share live context in invite-only pair sessions.

## Snapshots

A snapshot is an immutable record of retrieved chunks, kept for replay and audit. Snapshots, and frozen lenses, are write-once in the database: an update or delete is refused during the organisation's life. Private (personal) chunks are never included.

| Method | Path | What |
|---|---|---|
| `POST` | `/api/v1/context/snapshots` | Create a snapshot (`201`): `{"spaceId", "chunkIds": [...], "lensId"?}` |
| `GET` | `/api/v1/context/snapshots` | The snapshots whose space the caller may read, newest first, at most 100; summaries only, never chunk content |
| `GET` | `/api/v1/context/snapshots/{id}` | One snapshot, filtered for the reader (`context:space:search` on its spaces) |

- A snapshot made from a lens is exactly as visible as the lens: unsharing the lens closes its snapshots.
- Viewing a snapshot re-scans its stored content with the stricter of the captured classification and the source's current one.
- A person may create 200 snapshots an hour (`KEYSTONE_CONTEXT_SNAPSHOTS_PER_HOUR`), freezing a lens included.

## Handoffs

A handoff passes working context, as a snapshot, from one person to others, usually the reviewers of a pull request.

### The reviewer flow

1. The author's Editor creates a snapshot of the context it used.
2. It creates a handoff with the pull request URL and the reviewers as recipients:

   ```http
   POST /api/v1/context/handoffs
   Content-Type: application/json

   {"snapshotId": "<id>", "workItemKind": "pr", "workItemId": "42",
    "workItemUrl": "https://github.com/acme/api/pull/42",
    "message": "Context I used for the refund changes",
    "recipients": [{"kind": "user", "id": "<reviewer id>"}, {"kind": "team", "id": "<team id>"}]}
   ```

   `workItemKind` is `pr`, `issue` or `task`; `workItemId` is required (at most 512 characters); `message` is at most 4000 characters. The message and the work item reference are scanned with the organisation's DLP patterns, and a finding refuses the handoff without echoing it.

3. The reviewer's Editor finds it with `GET /api/v1/context/handoffs?workItemUrl=<the PR URL as the reviewer has it>` or the inbox `GET /api/v1/context/handoffs/inbox`.
4. `GET /api/v1/context/handoffs/{id}` returns the handoff with the author's snapshot, filtered for the reviewer: chunks of spaces the reviewer may not search are withheld and DLP is applied.

### Who sees a handoff

- **With recipients** (up to 50 users, groups and teams of the organisation): the author, the recipients (groups include nested groups) and administrators.
- **Without recipients**: everyone who may read the snapshot's space.

In both cases the reader must also be able to read the snapshot's space, and the snapshot is shown through the reader's own authorisation.

### Finding a handoff by work item

The URL is matched exactly on a normalised form: scheme and host in lower case, default port, fragment and tracking parameters removed, and the path of a recognised forge cut at the item number. So `.../pull/42/files#diff` and `.../pull/42?utm_x=1` both find the handoff recorded for `.../pull/42`. Recognised forms: GitHub `pull` and `issues`, GitLab `merge_requests` and `issues`, Bitbucket `pull-requests`, Azure DevOps `pullrequest` and `_workitems`. A URL that is not http(s), carries credentials or exceeds 2048 characters is `422`.

Lists take `limit` (1 to 100, default 50) and `cursor`; the response body is the array and the next cursor is in the `X-Next-Cursor` header.

The [collaboration policy](https://neuralinverse.com/docs/context/collaboration-policy/) governs handoffs: the `handoffs` switch, the classification ceiling of the snapshot's spaces, and the scope and number of recipients. A handoff above the policy's approval line is refused with `approval_required` (handoffs have no pending form). A recipient that no longer complies with the author's policy is closed at read time and listed with its violations, never deleted.

## Pair sessions

A pair session is live, shared context: one lens that every participant retrieves with, and a recording of the retrievals made in it.

| Method | Path | What |
|---|---|---|
| `POST` | `/api/v1/context/sessions` | Start a session (`201`): `{"spaceId", "lensId"?, "recorded", "inviteOnly"?}`; `inviteOnly: true` admits only invited people and link holders |
| `GET` | `/api/v1/context/sessions` | Sessions the caller hosts or joined (`?state=active\|ended`) |
| `GET` | `/api/v1/context/sessions/invited` | Live sessions the caller is invited to and has not joined |
| `GET` | `/api/v1/context/sessions/{id}` | One session (participants only) |
| `POST` | `/api/v1/context/sessions/{id}/invitations` | Host: invite targets and/or create a join link |
| `GET` | `/api/v1/context/sessions/{id}/invitations` | Host: invitations with `expired`, `revoked`, `effective` and `violations` |
| `DELETE` | `/api/v1/context/sessions/{id}/invitations/{invitationId}` | Host: revoke at once; people who already joined stay |
| `POST` | `/api/v1/context/sessions/{id}/join` | Join; optional `{"token": "<join token>"}` |
| `POST` | `/api/v1/context/sessions/{id}/leave` | Leave (the host ends the session instead) |
| `PUT` | `/api/v1/context/sessions/{id}/lens` | Change the session's lens: `{"lensId": "..."}` (`""` removes it) |
| `POST` | `/api/v1/context/sessions/{id}/end` | End the session |
| `GET` | `/api/v1/context/sessions/{id}/recording` | The recorded queries |

### Invitations and join links

```http
POST /api/v1/context/sessions/{id}/invitations
{"targets": [{"kind": "group", "id": "<group id>"}], "link": true, "ttlSeconds": 3600}
```

- Up to 50 targets per request; `ttlSeconds` defaults to 86400 (60 seconds to 7 days). Inviting a target again renews it.
- With `link: true` the answer carries `joinToken` once. Only its SHA-256 is stored, and it works only for that session in that organisation.
- Whoever joins must still be allowed to read the session's space. A session that is not invite-only can be joined by anyone who may read its space.
- Without a live invitation, joining an invite-only session is `403 context.session.invite_required`.

### Inside a session

- A participant uses the session's lens within the session without the lens being shared with them; outside the session it stays invisible to them.
- Changing the lens notifies the others (`session.lens_changed`); when the lens owner adds or removes a source, participants hear `session.sources_changed` on the [real-time feed](https://neuralinverse.com/docs/context/realtime/).
- In the recording, other participants' chunks are shown only for spaces the reader may read now (`chunksWithheld` counts the rest), and their query text passes the DLP gate of the session space (`queryRedacted`, `queryWithheld`).
- Ending and leaving a session stay possible when the policy switches pair sessions off.

The host cannot remove a participant, and revoking an invitation does not remove someone who already joined through it.
