Skip to main content
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

The peer keeps one representation. A scope is a projection of it: a view built only from evidence in the member sessions.
  • 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.
Passing a scope option with a workspace/admin key is cooperative filtering. A named-scope key enforces the boundary on the server, even when the caller omits the option. Existing admin, workspace, peer, and session keys keep their authorization behavior; they do not need to be replaced.

Named-scope API keys

With authentication enabled, an admin can mint a key bound to one named scope:
Create the workspace and scope with a privileged key before using the restricted key. The key can recall and ingest; there are no read-only/write-only roles. It carries w (workspace), sc (unprefixed scope name), and the audience honcho:named-scope, optionally exp. It cannot also carry an admin grant, peer, or session restriction. Existing JWT claims and minting parameters are unchanged. For self-hosting, the signing script also supports this key type:
These are core Honcho JWTs. Honcho Cloud’s managed hch- keys require separate Groudon gateway support for named-scope grants; do not assume a core JWT can be used at api.honcho.dev or that the managed key endpoint preserves scope_id. Use an instance running this core implementation directly until managed support is available. Upgrade every API replica before issuing scope keys. Older servers reject the dedicated audience, so a mixed-version fleet or rollback rejects new scope keys rather than widening their access. Existing keys continue working on both versions without rotation. Tokens with sc but without the dedicated audience are rejected by the new server as well. For recall, omit scope or pass exactly the key’s scope name as a string. filters/session_id cannot replace that projection, and session context rejects sessions, limit_to_session, or peer_perspective overrides. A scope key may query any target peer, but only through what its scope knows about that target. An empty scope has no evidence; it never falls back to a global view. On session creation, omit scopes or pass exactly ["support"]. Naming an existing unrelated session cannot enroll it. Message authors are ordinary peers, created as needed through ingestion; scope peers cannot author messages. A session shared by multiple scopes is one underlying conversation: writes feed all its observers. Scope keys are trusted writers for their member sessions. The current high-level SDK/CLI helpers attempt workspace provisioning, including before many reads. A named-scope key correctly rejects that global operation. Use the SDK’s HTTP transport or REST for scope-key operations until those helpers support restricted credentials. Existing high-level clients with existing key types are unchanged. For Python:
In TypeScript, use the public client.http transport for these endpoints. Direct new Peer(id, client.workspaceId, client.http) and new Session(id, client.workspaceId, client.http) handles also avoid the factory’s workspace provisioning callback. Do not catch a scope-key rejection and retry with a broader credential. Membership is checked on each session request. Removing a session immediately denies subsequent direct session reads and writes and excludes it from raw message recall. Learned memory cleanup remains asynchronous: existing conclusions and cards may contain removed evidence until reconciliation finishes. Requests already in flight and application caches are not retroactively erased. Applications needing immediate revocation must suspend affected scope recall, invalidate cached context, and wait for verified cleanup or rebuild a fresh scope. JWTs retain existing expiration and signing-secret semantics; this change does not add a token revocation registry. Prefer expiring grants. Scope IDs should not be reused to repurpose an outstanding grant.

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.

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.
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.
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 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

Scope IDs are unprefixed, must match ^[a-zA-Z0-9_-]+$, and are at most 506 characters. Get-or-create is idempotent: if the scope already exists, the same call returns it, and any metadata you pass is written onto it. When a lookup must not create a scope — resolving a user-supplied name, for example — use get_scope / getScope instead, which fails with a not-found error.
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. 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 copies the session’s existing explicit conclusions into the scope, and removing it retracts that session’s contributions — including conclusions derived from them. Copying rather than re-deriving is why membership changes are cheap and deterministic — and why they are also asynchronous. It also means a freshly backfilled scope starts at explicit depth and accrues deeper reasoning through subsequent dreams. 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”:
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

scope is accepted on these surfaces:

Rules

scope is mutually exclusive with filters, sessions, and session / session_id — and on session context, with peer_perspective (where it also requires peer_target). Like the session allowlist, it fails closed: a contradiction is rejected with a 422 rather than silently widened, a scope with no member sessions recalls nothing, and an empty list (scope=[]) is rejected rather than treated as “no boundary”. On workspace chat the same exclusion holds: pass scope or session_id, never both. Per-surface caps and error shapes are in the API reference.

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.

Guardrails

A few behaviors follow from how scopes are built:
  • The scope. prefix is reserved. Creating a peer, or adding a peer to a session, with a scope.-prefixed name is rejected.
  • List scopes through the scopes surface. honcho.scopes() / POST /scopes/list returns unprefixed ids. Peer listings hide scopes by default; kind="scope" on POST /peers/list returns the backing peers named scope.<id>, and kind="all" includes both regular peers and those backing peers.
  • 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 names and point you at /scopes/{scope_id}/sessions or the scopes field on session create.
If you want the exact mechanics for scopes, read: src/routers/scopes.py, src/crud/scope.py, and src/deriver/scope_backfill.py.

Limits

Full request and response shapes are in the API reference.