Memory scopes and subjects
A memory scope is the isolation boundary for agent memory. All vector chunks, graph elements, and execution logs belong to a specific scope, and all read and write operations target one or more scopes.
Jennah supports two scope kinds:
| Kind | Description | Resource Paths |
|---|---|---|
agent |
An individual agent workspace for autonomous tasks or sessions. | /v1/agents/{scopeId} and /v1/scopes/{scopeId} |
subject |
A durable entity, such as an end user or customer profile, whose memory persists across multiple agents. | /v1/scopes/{scopeId} |
Both scope kinds share the same identifier space within an enterprise and hold the same memory types: vector chunks, knowledge graphs, and execution logs. Deleting a scope of either kind removes all of its memory in one transaction.
Why subjects exist
When user-related facts are stored directly inside individual agent workspaces (such as a support bot, a billing assistant, or a scheduler), memory becomes fragmented across agents. Each agent must re-learn user preferences, and updating or auditing customer data requires inspecting multiple workspaces.
A subject scope provides a centralized, persistent memory container for a single principal. Multiple agents can read from and write to the same subject scope, making user data shared across workflows and straightforward to delete in a single operation for data privacy compliance.
Creating a scope
Scope creation requires setting kind explicitly. Scope kinds are immutable once created.
POST /v1/scopes
{
"scopeId": "acme.user.u-4471",
"scopeName": "Dana Whitfield",
"kind": "SCOPE_KIND_SUBJECT"
}
Using the CLI:
The --kind flag is required. Scope IDs are unique across all scope kinds within an enterprise; attempting to create a subject using an existing agent ID (or vice versa) returns ALREADY_EXISTS.
Each plan caps how many subject scopes an enterprise may hold, counted separately from agent workspaces (see Plans and limits). Creating a subject scope past the cap is refused with RESOURCE_EXHAUSTED.
From the console, open Subjects and use the create form at the foot of the page. The kind is implied by the surface rather than chosen in the form: Subjects creates subject scopes and Agents creates agent workspaces, and neither page offers a control that switches between them. Creating a subject also requires a subject selector that already covers the ID you choose, so a caller with no subject reach is refused a create the platform would reject anyway.
Scope kinds cannot be modified after creation because access control rules and selector namespaces depend on the scope kind.
Structure IDs for hierarchical selector matching
Selectors match on .-delimited segments. Using hierarchical IDs like acme.user.u-4471 allows granting access to acme.user.* with a single selector pattern. Flat IDs require individual selector grants or full * wildcards.
Compatibility with /v1/agents routes
Existing /v1/agents endpoints remain backward-compatible and operate exclusively on agent scopes. They create, resolve, list, and delete only agent scopes. If a subject scope ID is passed to /v1/agents/{id}, the API returns NOT_FOUND.
Existing agent integrations and agent listings are unaffected by the presence of subject scopes.
The CLI provides two command groups:
jnh agents: Scoped exclusively to agent workspaces.jnh scopes: Manages bothagentandsubjectscopes.
jnh scopes list # lists both kinds, displaying a KIND column
jnh scopes list --kind subject # filters to subject scopes only
The console splits the same way, one surface per kind: Agents lists agent workspaces and Subjects lists subject scopes. The subject listing is paged and says when it has stopped at a page boundary, and because the API offers no filter on ID or name it also accepts an ID directly ("Open a subject by ID"), which resolves through the point read rather than by walking pages. An ID you cannot reach and one that does not exist give the same answer, on every surface.
Memory commands are available under both command trees. jnh scopes memory <verb> <scope-id> operates on either scope kind, while jnh agents memory <verb> <agent-instance-id> remains available for agent-specific workflows.
Access control: the subject selector namespace
Subject scopes use a dedicated subject selector namespace, separate from agent and dataset namespaces:
| Permission | Class | Description |
|---|---|---|
agent.subjects:read |
data | See the subject selectors configured on roles and API keys |
agent.subjects:manage |
management | Set subject selectors on roles and API keys |
These permissions govern subject selectors, not the scopes themselves. Creating, reading, listing, and deleting scopes of either kind is gated by agent.agents:create, agent.agents:read, and agent.agents:delete, and the scope's selector namespace then decides which scopes the caller can reach.
Separating namespaces ensures that agent workspace administrators do not automatically inherit access to end-user memory. Selectors configured under agentSelectors match only agent scopes, while subjectSelectors match only subject scopes.
POST /v1/roles
{
"name": "Support tier 1",
"permissions": ["agent.memory:read", "agent.memory:write"],
"agentSelectors": ["support.*"],
"subjectSelectors": ["acme.user.*"]
}
Roles and API keys do not grant access to subject scopes by default unless explicitly configured in subjectSelectors. Only the built-in root and admin roles possess wildcard (*) reach across all namespaces by default.
Granting the blanket * subject selector requires agent.subjects:manage, and a caller can never grant subject reach beyond their own: a caller whose reach is acme.user.* cannot grant acme.*. Either refusal is PERMISSION_DENIED.
Data residency and regions
Each subject scope is assigned to a specific region at creation time and cannot be relocated. All associated vector, graph, and log data is stored exclusively in that region's database.
This guarantees data residency: queries against an EU-based subject scope are always evaluated in that scope's home region, regardless of the caller or agent location.
POST /v1/scopes
{
"scopeId": "acme.user.u-4471",
"kind": "SCOPE_KIND_SUBJECT",
"region": "eu-west-1"
}
Single-scope queries automatically route to the target scope's home region. Regional placement only restricts multi-scope queries, which require all queried scopes to reside in the same region.
Querying across multiple scopes
Multi-scope recall is an API and CLI capability. The console has no anchored query surface at all: it inspects a scope's stored memory and does not rank it, so there is nothing there that spans scopes.
The memory:query API accepts an optional list of additionalScopes to evaluate alongside the primary scope in the URL path:
POST /v1/scopes/support.tier1.bot-09/memory:query
{
"additionalScopes": ["acme.user.u-4471"],
"semantic": {
"embedding": [...],
"limit": 10
}
}
In the CLI, the --scope flag specifies additional scopes and can be repeated:
CLI tabular output includes a SCOPE column whenever multiple scopes are queried. Structured output formats (-o json, -o ndjson) include scopeId on every item regardless of scope count.
Multi-scope query consistency and ranking
Multi-scope queries provide the following guarantees:
- Consistent read timestamp: All query sections across all requested scopes are evaluated at a single database snapshot timestamp.
- Global semantic ranking: Semantic vector matches are ranked globally across the union of all queried scopes rather than partitioned per scope.
- Explicit scope attribution: Every returned chunk, graph element, or log entry includes its origin
scopeId. Because chunk IDs are unique only within their own scope,scopeIdprovides unambiguous attribution.
{
"semantic": {
"matches": [
{
"scopeId": "acme.user.u-4471",
"chunkId": "pref-travel",
"distance": 0.11
},
{
"scopeId": "support.tier1.bot-09",
"chunkId": "ticket-8821",
"distance": 0.19
}
]
}
}
Error conditions
Jennah does not return partial results when querying multiple scopes. If any target scope fails validation, the entire query is rejected:
| Situation | Error |
|---|---|
| Scope does not exist, or no selector of the caller's reaches it | NOT_FOUND |
| Scopes reside in different regions | FAILED_PRECONDITION |
| More than 8 scopes named | INVALID_ARGUMENT |
A query may name at most 8 scopes. The count includes the scope in the URL path and is taken after duplicates are removed.
A scope the caller cannot reach is reported exactly like one that does not exist, so a refusal never confirms that a scope is there.
Limitations
Multi-scope queries currently have the following constraints:
- Metadata filtering: Metadata filters are supported only for single-scope queries, in every section. Item ids are unique within a scope but not across them, so a filter spanning several scopes could let one scope's tag admit another scope's identically-named item.
- Server-side query embedding: The
queryTextfield is supported only on single-scope queries. For multi-scope semantic search, supply precomputed vectors in theembeddingfield via the SDK. - Graph traversal boundaries: Graph queries traverse nodes and edges within each requested scope separately. Graph edges cannot link nodes across different scopes, so traversal returns isolated subgraphs per scope.
Committing memory to a scope
The memory:commit endpoint writes to exactly one scope per transaction. To write to a scope other than the path parameter, specify targetScope in the request body:
POST /v1/scopes/support.tier1.bot-09/memory:commit
{
"targetScope": "acme.user.u-4471",
"vectors": [
{
"chunkId": "pref-travel",
"rawContent": "prefers rail over air"
}
]
}
Using the CLI:
jnh scopes memory commit support.tier1.bot-09 \
--target-scope acme.user.u-4471 \
--from-file chunks.json
The commit receipt confirms the written scopeId. In the CLI, target scopes must be specified using the --target-scope flag; specifying targetScope inside payload files is rejected to keep the destination explicit.
Each commit is strictly atomic to a single scope. Target scopes are authorized against their respective namespace: committing to a subject scope requires subject selector reach, regardless of the URL path used. A targetScope the caller cannot reach returns NOT_FOUND, the same as one that does not exist.
Scope volume and the scan budget
Semantic search performs an exact nearest-neighbor scan over all chunks in the queried scopes without an approximate index. Because query cost scales with the number of scanned chunks, Jennah bounds scope volume and query scan size.
Each scope reports its chunk volume on GetScope, ListScopes, GetAgent, and ListAgents (displayed in the CHUNKS column of jnh scopes list and jnh agents list). Volume reflects total stored chunk rows, including superseded chunks. A scope may report counted: false (uncounted in jnh) until its volume has been counted.
The per-scope volume ceiling
Plans define the maximum number of chunks a single scope may hold. A commit that exceeds this ceiling is rejected atomically across all sections. Re-committing existing chunk IDs does not increase volume and is not rejected.
On every plan the ceiling equals the plan's scan budget (below), so searching one scope costs no more than the budget allows a search across several.
| Situation | Error |
|---|---|
| Commit exceeds volume ceiling | RESOURCE_EXHAUSTED (specifies the scope, current chunk count, attempted additions, and limit) |
The per-query scan budget
Plans define the maximum number of chunks a query's semantic section may scan across all requested scopes combined. The budget applies to queries naming two or more scopes. Free allows 25,000 chunks per query, paid plans allow at least 100,000, and larger plans allow up to 1,000,000 (see Plans and limits). At 100,000 chunks a semantic query spends about 0.9 s scanning (see What a scan costs). The scan estimate uses each scope's recorded chunk volume, ignoring metadata filters and validity bounds.
Queries exceeding the scan budget are rejected rather than truncated or partially scanned.
| Situation | Error |
|---|---|
| Estimated scan exceeds query budget | INVALID_ARGUMENT (identifies the largest contributing scope and its count) |
If a multi-scope query exceeds the budget, query fewer scopes or query large scopes individually. A query naming a single scope is never refused by the scan budget, whatever the scope's size.
Memory formation always targets a single scope, so it is never refused by the scan budget.
Scope volume counts used for budget estimation may lag recent commits by up to 15 seconds.
What a scan costs
A semantic query costs a fixed per-query overhead plus a term linear in the total number of chunks it scans, summed across every scope it names. How those chunks are split between scopes does not matter: eight scopes of 2,000 chunks cost what 16,000 chunks cost.
Representative measurements with 3,072-dimensional embeddings, as scan time added to the fixed overhead:
| Chunks scanned | Added scan latency |
|---|---|
| ~180 per scope | ~1.7 ms per scope |
| ~600 per scope | ~6.7 ms per scope |
| 2,000 per scope | ~17 ms per scope |
Scan latency is about 8.6 ms per 1,000 chunks scanned at 3,072 dimensions, measured up to 16,000 chunks per query, and does not depend on the requested result limit. This is why the scan budget is counted in chunks rather than scopes. A single scope of 100,000 chunks has a total query latency of about 1.3 s at the median and 1.9 s at p90.
Deleting a subject scope
Deleting a subject scope removes all vector chunks, graph nodes and edges, and execution logs stored in that scope in a single transaction:
Response:
Using the CLI:
The CLI displays the scope metadata (kind, name, region) and prompts for confirmation before executing the deletion.
From the console, open the subject from Subjects and use Delete subject at the foot of its page. What identifies the scope there is the read-back above the control - the kind, region, and creation instant the platform holds for it - rather than the ID typed into the confirmation box: the mistake worth guarding against is a wrong ID that exists, and retyping it repeats the mistake. The console states the isolation rule below at the point of deleting, not only here, and on success it shows the instant the erasure committed. It reports no count of what was removed, because the platform makes no itemised erasure claim; the commit instant is the whole receipt on every surface.
Deletion removes everything stored in the scope, matching the behavior of agent workspace deletion.
Scope isolation during deletion
Deleting a subject scope purges only the memory stored within that specific scope. It does not remove data that agents might have recorded separately in their own agent workspaces.
To support complete data erasure workflows, write user-specific facts into the user's subject scope rather than agent workspaces.