diff --git a/docs/docs.json b/docs/docs.json
index b9d0e498..6ddbe0cb 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -10,6 +10,10 @@
{
"source": "/v3/guides/integrations/claudecode",
"destination": "/v3/guides/integrations/claude-code"
+ },
+ {
+ "source": "/v3/documentation/features/advanced/representation-scopes",
+ "destination": "/v3/documentation/features/advanced/directional-representations"
}
],
"colors": {
@@ -62,7 +66,8 @@
"v3/documentation/features/advanced/reasoning-configuration",
"v3/documentation/features/advanced/summarizer",
"v3/documentation/features/advanced/peer-card",
- "v3/documentation/features/advanced/representation-scopes",
+ "v3/documentation/features/advanced/directional-representations",
+ "v3/documentation/features/advanced/scopes",
"v3/documentation/features/advanced/dreaming",
"v3/documentation/features/advanced/queue-status",
"v3/documentation/features/advanced/webhooks",
diff --git a/docs/v3/documentation/features/advanced/representation-scopes.mdx b/docs/v3/documentation/features/advanced/directional-representations.mdx
similarity index 99%
rename from docs/v3/documentation/features/advanced/representation-scopes.mdx
rename to docs/v3/documentation/features/advanced/directional-representations.mdx
index 30e2ae32..12e9329b 100644
--- a/docs/v3/documentation/features/advanced/representation-scopes.mdx
+++ b/docs/v3/documentation/features/advanced/directional-representations.mdx
@@ -1,6 +1,6 @@
---
-title: 'Representation Scopes'
-description: 'Advanced configuration and querying for representations'
+title: 'Directional Representations'
+description: 'How peers build and query representations of other peers'
icon: 'circle'
---
diff --git a/docs/v3/documentation/features/advanced/overview.mdx b/docs/v3/documentation/features/advanced/overview.mdx
index 09f92fea..d0d7cc2c 100644
--- a/docs/v3/documentation/features/advanced/overview.mdx
+++ b/docs/v3/documentation/features/advanced/overview.mdx
@@ -12,7 +12,8 @@ Advanced features give you fine-grained control over Honcho's behavior and imple
- [Configuration](/v3/documentation/features/advanced/reasoning-configuration) - Configure reasoning models and behavior
- [Summarizer](/v3/documentation/features/advanced/summarizer) - Automatic session summarization
- [Peer Card](/v3/documentation/features/advanced/peer-card) - Quick-reference profile of stable biographical facts about a peer
-- [Representation Scopes](/v3/documentation/features/advanced/representation-scopes) - Directional representations for multi-peer scenarios
+- [Directional Representations](/v3/documentation/features/advanced/directional-representations) - How peers build separate representations of each other
+- [Scopes](/v3/documentation/features/advanced/scopes) - Named sets of sessions that act as visibility boundaries for recall
- [Dreaming](/v3/documentation/features/advanced/dreaming) - Autonomous memory consolidation and self-improvement
- [Queue Status](/v3/documentation/features/advanced/queue-status) - Monitor background processing and reasoning tasks
diff --git a/docs/v3/documentation/features/advanced/peer-card.mdx b/docs/v3/documentation/features/advanced/peer-card.mdx
index fbc08772..5db6a1db 100644
--- a/docs/v3/documentation/features/advanced/peer-card.mdx
+++ b/docs/v3/documentation/features/advanced/peer-card.mdx
@@ -79,7 +79,7 @@ console.log(card);
## Directional Peer Cards
-Peer cards follow the same observer-observed model as [representations](/v3/documentation/features/advanced/representation-scopes). When `observe_others` is enabled, a peer can have a **different** card for each peer it observes.
+Peer cards follow the same observer-observed model as [representations](/v3/documentation/features/advanced/directional-representations). When `observe_others` is enabled, a peer can have a **different** card for each peer it observes.
For example, if Alice and Bob are in a session together and Alice has `observe_others: true`, Alice will build her own peer card for Bob--separate from Honcho's peer card for Bob. You can read and write these directional cards using the `target` parameter.
diff --git a/docs/v3/documentation/features/advanced/reasoning-configuration.mdx b/docs/v3/documentation/features/advanced/reasoning-configuration.mdx
index 8642254a..492dd4b9 100644
--- a/docs/v3/documentation/features/advanced/reasoning-configuration.mdx
+++ b/docs/v3/documentation/features/advanced/reasoning-configuration.mdx
@@ -157,7 +157,7 @@ You may therefore disable observation of a peer by setting the `observe_me` flag
If the peer has a session-level configuration, it will override this configuration. If the flag is not set, or is set to `true`, the peer will be observed.
-For session-level observation controls and local representations (where peers build separate models of each other), see [Representation Scopes](/v3/documentation/features/advanced/representation-scopes).
+For session-level observation controls and local representations (where peers build separate models of each other), see [Directional Representations](/v3/documentation/features/advanced/directional-representations).
diff --git a/docs/v3/documentation/features/advanced/scopes.mdx b/docs/v3/documentation/features/advanced/scopes.mdx
new file mode 100644
index 00000000..5d538d7b
--- /dev/null
+++ b/docs/v3/documentation/features/advanced/scopes.mdx
@@ -0,0 +1,397 @@
+---
+title: 'Scopes'
+description: 'Named sets of sessions that act as visibility boundaries for recall'
+icon: 'shield-halved'
+---
+
+A **scope** is a named set of sessions that acts as a visibility boundary. Recall
+performed through a scope sees only what happened in that scope's sessions,
+while the peer keeps its single unified representation of everything it has ever
+participated in.
+
+Use scopes when one peer's history spans contexts that must not leak into each
+other — a therapy app where the clinical sessions must not inform the billing
+assistant, a support product where a reseller's agent may only answer from its
+own tickets, a multi-tenant deployment where one human works across tenants.
+
+## Projection, Not Partition
+
+A scope does not split a peer into pieces. The peer's representation stays
+whole; a scope is a **projection** of it — a view built only from the evidence
+in that scope's member sessions.
+
+```mermaid
+graph TB
+ P[Peer: user-123
one unified representation]
+
+ P --> S1[session: therapy-1]
+ P --> S2[session: therapy-2]
+ P --> S3[session: billing-1]
+ P --> S4[session: onboarding-1]
+
+ SC1[scope: therapy] -.->|projects| S1
+ SC1 -.->|projects| S2
+ SC2[scope: billing] -.->|projects| S3
+
+ style P fill:#B6DBFF,stroke:#333,color:#000
+ style S1 fill:#B6DBFF,stroke:#333,color:#000
+ style S2 fill:#B6DBFF,stroke:#333,color:#000
+ style S3 fill:#B6DBFF,stroke:#333,color:#000
+ style S4 fill:#B6DBFF,stroke:#333,color:#000
+ style SC1 fill:#FFE0B2,stroke:#333,color:#000
+ style SC2 fill:#FFE0B2,stroke:#333,color:#000
+```
+
+Three consequences worth internalizing:
+
+- **Sessions can belong to more than one scope.** Membership is many-to-many.
+- **Sessions can belong to no scope.** `onboarding-1` above is reachable
+ by an unscoped request and by nothing else.
+- **An unscoped request still sees everything.** A scope constrains the requests
+ that name it; it does not hide the sessions from requests that don't.
+
+
+Scopes are a recall boundary, not an authorization boundary. Anyone holding a
+workspace key can read any session with or without a scope. Enforcing that a
+given caller *must* go through a given scope is a separate feature (scope-bound
+API keys) and is not shipped.
+
+
+## The Two Arms
+
+There are two ways to confine recall, and they behave differently. Picking the
+wrong one is the most common mistake with this feature.
+
+| | `scope="therapy"` (named scope) | `sessions=[...]` / `scope=["a","b"]` (allowlist) |
+|---|---|---|
+| **Mechanism** | Reads the scope's own representation of the peer | Restricts the peer's own representation to a set of sessions |
+| **Conclusions** | All levels — `explicit`, plus `deductive` / `inductive` reasoned **within** the scope | `explicit` only |
+| **Reasoning chains** | Available | Unavailable |
+| **Setup required** | Yes — create the scope, add sessions, wait for backfill | None — pass session IDs ad hoc |
+| **Accepts** | One scope name, or a list of up to 100 | Up to 1,000 session IDs |
+
+### Named scope: depth
+
+Passing a **single** scope name swaps the observer. Recall runs against the
+scope's own view of the target peer, which the deriver and dreamer have been
+building from the scope's member sessions all along. That view contains
+higher-order inferences — but only ones reasoned from evidence inside the scope.
+
+```python
+answer = user.chat("What is stressing them out?", scope="therapy")
+```
+
+This is the arm you want for a durable, meaningful boundary.
+
+### Allowlist: breadth
+
+Passing a **list** of scopes, or a bare list of session IDs, keeps the peer as
+the observer and restricts recall to the union of those sessions. Because a
+dream-derived conclusion is synthesized across sessions, it cannot be attributed
+to any one of them — so this arm recalls `explicit` conclusions only, and answers
+from directly-stated facts rather than inference.
+
+```python
+answer = user.chat("What did they say about billing?", sessions=[s1, s2])
+answer = user.chat("What did they say?", scope=["therapy", "intake"])
+```
+
+Reach for this when the set of sessions is decided per-request, or when you want
+a quick boundary without provisioning a scope. See
+[Scoping Recall to Sessions](/v3/documentation/features/advanced/using-filters#scoping-recall-to-sessions)
+for the full allowlist rules.
+
+
+A list of scopes is the allowlist arm, not "several named scopes at once". It
+gives you the union of their *sessions*, at explicit-only depth — it does not
+give you the union of their reasoned views. If you need depth, query one scope.
+
+
+## Creating a Scope and Managing Membership
+
+
+```python Python
+from honcho import Honcho
+
+honcho = Honcho(workspace_id="my-app")
+
+# Get or create — idempotent
+therapy = honcho.scope("therapy")
+
+# Add existing sessions (max 100 per call)
+therapy.add_sessions(["therapy-session-1", "therapy-session-2"])
+
+# Or attach at session creation — the scope is created if it doesn't exist
+session = honcho.session("therapy-session-3", scopes=["therapy"])
+
+# Inspect
+for s in therapy.sessions():
+ print(s.id)
+
+therapy.remove_session("therapy-session-1")
+
+for scope in honcho.scopes():
+ print(scope.id, scope.metadata)
+```
+
+```typescript TypeScript
+import { Honcho } from "@honcho-ai/sdk";
+
+const honcho = new Honcho({ workspaceId: "my-app" });
+
+// Get or create — idempotent
+const therapy = await honcho.scope("therapy");
+
+// Add existing sessions (max 100 per call)
+await therapy.addSessions(["therapy-session-1", "therapy-session-2"]);
+
+// Or attach at session creation — the scope is created if it doesn't exist
+const session = await honcho.session("therapy-session-3", {
+ scopes: ["therapy"],
+});
+
+// Inspect
+for await (const s of await therapy.sessions()) {
+ console.log(s.id);
+}
+
+await therapy.removeSession("therapy-session-1");
+
+for await (const scope of await honcho.scopes()) {
+ console.log(scope.id, scope.metadata);
+}
+```
+
+```bash REST
+# Get or create (201 created / 200 existing)
+curl -X POST "$HONCHO_URL/v3/workspaces/my-app/scopes" \
+ -H "Authorization: Bearer $HONCHO_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{"id": "therapy"}'
+
+# Add sessions
+curl -X POST "$HONCHO_URL/v3/workspaces/my-app/scopes/therapy/sessions" \
+ -H "Authorization: Bearer $HONCHO_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{"session_ids": ["therapy-session-1", "therapy-session-2"]}'
+
+# List membership
+curl -X POST "$HONCHO_URL/v3/workspaces/my-app/scopes/therapy/sessions/list" \
+ -H "Authorization: Bearer $HONCHO_API_KEY"
+
+# Remove one session
+curl -X DELETE "$HONCHO_URL/v3/workspaces/my-app/scopes/therapy/sessions/therapy-session-1" \
+ -H "Authorization: Bearer $HONCHO_API_KEY"
+```
+
+
+Scope IDs are unprefixed, must match `^[a-zA-Z0-9_-]+$`, and are at most 506
+characters.
+
+
+Every scopes route — and every read that passes `scope` — requires a
+**workspace-level or admin key**. A scope's membership can exceed any single
+peer's own session membership, so peer- and session-scoped keys are rejected
+with `401`.
+
+
+## Membership Changes Copy, They Don't Re-Derive
+
+A session added to a scope while empty needs nothing special: messages sent
+after the change flow into the scope through the normal deriver fan-out.
+
+```mermaid
+graph LR
+ M[new message
in therapy-1] --> D[Deriver]
+ D -->|fan-out| PR[peer's own
representation]
+ D -->|fan-out| SR[scope: therapy
representation]
+
+ style M fill:#B6DBFF,stroke:#333,color:#000
+ style D fill:#FFE0B2,stroke:#333,color:#000
+ style PR fill:#B6DBFF,stroke:#333,color:#000
+ style SR fill:#B6DBFF,stroke:#333,color:#000
+```
+
+A session that **already has messages** is handled retroactively by a background
+job rather than by re-running the LLM over its history: adding it enqueues a
+`scope_backfill` that copies the session's existing `explicit` conclusions into
+the scope's collections, and removing it enqueues a `scope_removal` that
+soft-deletes those copies, follows dependent derived conclusions out
+(fail-closed), and rebuilds the scope's peer card from whatever evidence remains.
+Copying rather than re-deriving is why membership changes are cheap and
+deterministic — and why they are also **asynchronous**.
+
+Poll `status()` to tell "the scope hasn't caught up yet" apart from "the scope
+has caught up and there is genuinely nothing to recall":
+
+
+```python Python
+therapy.add_sessions(["old-session-with-history"])
+
+status = therapy.status()
+# {"old-session-with-history": {"state": "pending", "updated_at": "..."}}
+# → later: {"state": "completed", "docs_copied": 42, "updated_at": "..."}
+```
+
+```typescript TypeScript
+await therapy.addSessions(["old-session-with-history"]);
+
+const status = await therapy.status();
+// { "old-session-with-history": { state: "pending", updatedAt: "..." } }
+```
+
+```bash REST
+curl "$HONCHO_URL/v3/workspaces/my-app/scopes/therapy/status" \
+ -H "Authorization: Bearer $HONCHO_API_KEY"
+```
+
+
+`state` is `pending`, `completed`, or `failed`; `docs_copied` appears once a
+backfill completes. Only sessions that have had a backfill enqueued appear, so an
+empty result means none have — not that the scope is empty.
+
+## Reading Through a Scope
+
+```mermaid
+graph LR
+ Q["chat(..., scope='therapy')"] --> R{scope
resolution}
+ R -->|single scope| O[observer swaps
to the scope]
+ R -->|list of scopes| A[union of member sessions
as an allowlist]
+ O --> D1[all conclusion levels
+ reasoning chains]
+ A --> D2[explicit conclusions only]
+
+ style Q fill:#B6DBFF,stroke:#333,color:#000
+ style R fill:#FFE0B2,stroke:#333,color:#000
+ style O fill:#B6DBFF,stroke:#333,color:#000
+ style A fill:#B6DBFF,stroke:#333,color:#000
+ style D1 fill:#B6DBFF,stroke:#333,color:#000
+ style D2 fill:#B6DBFF,stroke:#333,color:#000
+```
+
+`scope` is accepted on four surfaces:
+
+| Surface | Accepts | Notes |
+|---------|---------|-------|
+| [`peer.chat()`](/v3/documentation/features/chat) | one scope or a list | Confines both conclusion recall and the messages the agent reads |
+| `peer.representation()` | one scope or a list | Confines conclusion recall |
+| [`session.context()`](/v3/documentation/features/get-context) | one scope only | Perspective source for `peer_target`'s representation and card. Requires `peer_target`; mutually exclusive with `peer_perspective` |
+| `honcho.search()` | one scope only | Restricts message search to the scope's member sessions |
+
+
+```python Python
+# Chat — answered only from the therapy sessions
+answer = user.chat("What is stressing them out?", scope="therapy")
+
+# Representation
+rep = user.representation(scope="therapy")
+
+# Session context, using the scope as the perspective source
+ctx = session.context(peer_target="user-123", scope="therapy")
+
+# Message search, restricted to the scope's sessions
+messages = honcho.search("insomnia", scope="therapy")
+```
+
+```typescript TypeScript
+// Chat — answered only from the therapy sessions
+const answer = await user.chat("What is stressing them out?", {
+ scope: "therapy",
+});
+
+// Representation
+const rep = await user.representation({ scope: "therapy" });
+
+// Session context, using the scope as the perspective source
+const ctx = await session.context({
+ peerTarget: "user-123",
+ scope: "therapy",
+});
+
+// Message search, restricted to the scope's sessions
+const messages = await honcho.search("insomnia", { scope: "therapy" });
+```
+
+```bash REST
+curl -X POST "$HONCHO_URL/v3/workspaces/my-app/peers/user-123/chat" \
+ -H "Authorization: Bearer $HONCHO_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{"query": "What is stressing them out?", "scope": "therapy"}'
+```
+
+
+### Rules
+
+Like the session allowlist, `scope` **fails closed** — a contradiction is
+rejected rather than silently widened, and an empty boundary recalls nothing
+rather than everything.
+
+| Rule | Behavior |
+|------|----------|
+| `scope` with `filters` | `422` |
+| `scope` with `session` / `session_id` | `422` |
+| `scope` with `sessions` | `422` |
+| `scope` with `peer_perspective` (context only) | `422` |
+| `scope` without `peer_target` (context only) | `422` |
+| `scope` with a `session_id` filter (search only) | `422` |
+| A scope with no member sessions | Recalls nothing |
+| An empty list (`scope=[]`) | Rejected — never treated as "no boundary" |
+| More than 100 scopes in one `scope` option | `422` |
+| A resolved session union over 1,000 sessions | `422` |
+| A peer- or session-scoped key | `401` |
+
+## Provenance, Not Topic
+
+A scope is defined by **where a fact was said**, not what it is about.
+
+If a user mentions a therapy detail in a billing session, that conclusion is
+formed from the billing session and lands in the `billing` scope. Querying
+`scope="therapy"` will not find it, and querying `scope="billing"` will.
+
+
+Scopes give you provenance-based privacy, not topic-based privacy. If you need
+"no clinical content in the billing assistant's answers" regardless of where it
+was said, that is content classification and has to be enforced above Honcho —
+by controlling what reaches which session in the first place, or by filtering
+the answer.
+
+
+Design accordingly: keep the session boundary aligned with the confidentiality
+boundary you actually care about, since that session boundary is the one scopes
+can enforce.
+
+## Underneath the Facade
+
+The `/scopes` routes are a facade. A scope named `therapy` is a peer named
+`scope.therapy` that observes its member sessions (`observe_others=true`) and
+never speaks (`observe_me=false`), marked with `{"kind": "scope"}` in
+server-controlled internal metadata. That is why the single-scope arm has depth:
+the scope occupies an observer position, so the deriver and dreamer build a real
+representation there, exactly as they do for a peer observing another peer — see
+[Directional Representations](/v3/documentation/features/advanced/directional-representations)
+for that mechanism.
+
+You never need this detail to use scopes, but it explains the guardrails:
+
+- **The `scope.` prefix is reserved.** Creating a peer, or adding a peer to a
+ session, with a `scope.`-prefixed name is rejected — the prefix contains `.`,
+ which is outside the peer-name charset, so no validated API call can occupy the
+ namespace.
+- **Scope peers are hidden from peer listings by default.** Pass
+ `kind="scope"` to list only scopes, or `kind="all"` to see both.
+- **A scope can't be observed.** No representation is formed *of* a scope, so a
+ scope is rejected in any `target` / observed position, including as a dream
+ target.
+- **Membership is managed only through the scopes surface.** The session
+ add-peers, set-peers, and remove-peers routes reject scope peers and point you
+ at `/scopes/{scope_id}/sessions` or the `scopes` field on session create.
+
+## Limits
+
+| Limit | Value |
+|-------|-------|
+| Scope ID length | 506 characters |
+| Scope ID charset | `^[a-zA-Z0-9_-]+$` |
+| Sessions per membership call | 100 |
+| Scopes in one `scope` read option | 100 |
+| Scopes on session create | 100 |
+| Sessions in a resolved allowlist | 1,000 |
diff --git a/docs/v3/documentation/features/advanced/using-filters.mdx b/docs/v3/documentation/features/advanced/using-filters.mdx
index 4a1eefec..3af0fdbe 100644
--- a/docs/v3/documentation/features/advanced/using-filters.mdx
+++ b/docs/v3/documentation/features/advanced/using-filters.mdx
@@ -875,10 +875,34 @@ curl -X POST "$HONCHO_URL/v3/workspaces/my-app/peers/user-123/representation" \
```
+Both SDKs expose this as a `sessions` option, which goes on the wire as the
+`filters` body above:
+
+
+```python Python
+answer = user.chat("What did the user ask about billing?",
+ sessions=["support-chat-1", "support-chat-2"])
+
+rep = user.representation(sessions=["support-chat-1", "support-chat-2"])
+```
+
+```typescript TypeScript
+const answer = await user.chat("What did the user ask about billing?", {
+ sessions: ["support-chat-1", "support-chat-2"],
+});
+
+const rep = await user.representation({
+ sessions: ["support-chat-1", "support-chat-2"],
+});
+```
+
+
-The session allowlist is REST-only today. The SDKs cover the single-session case
-with `session`, but do not yet expose the allowlist — call the endpoint directly
-when you need a set of sessions.
+If the same set of sessions is a boundary you reuse, name it: a
+[scope](/v3/documentation/features/advanced/scopes) is a persistent version of
+this allowlist, and querying a single scope recalls at full depth rather than
+`explicit`-only. `sessions` is the right tool when the set is decided
+per-request.
### Rules
diff --git a/docs/v3/guides/integrations/paperclip.mdx b/docs/v3/guides/integrations/paperclip.mdx
index 3b2aa156..d5c216a4 100644
--- a/docs/v3/guides/integrations/paperclip.mdx
+++ b/docs/v3/guides/integrations/paperclip.mdx
@@ -130,7 +130,7 @@ The plugin registers the following Honcho tools for Paperclip agents:
Review how workspaces, peers, and sessions fit together.
-
+
Review how `observe_me` and `observe_others` change what peers can model.