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-1above 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.
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.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 recallsexplicit conclusions only, and answers
from directly-stated facts rather than inference.
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
^[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.
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 existingexplicit 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”. 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 thebilling scope. Querying
scope="therapy" will not find it, and querying scope="billing" will.
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 ascope.-prefixed name is rejected. - List scopes through the scopes surface.
honcho.scopes()/POST /scopes/listreturns unprefixed ids. Peer listings hide scopes by default;kind="scope"onPOST /peers/listreturns the backing peers namedscope.<id>, andkind="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}/sessionsor thescopesfield on session create.
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.