Memory surfaces
Four different questions. Not four flavours of one store.
| Surface | Question it answers | Table(s) |
|---|---|---|
| Identity (work) | What does this identity know and remember in the collaboration? | memories (projectId null) |
| Project (work) | What is known in the context of this project? | memories (projectId set) |
| Agent | What did an agent learn doing a job? | agent_memories |
| Personal | What is grounded in this identity alone — not the work? | personal_memories |
Identity and project work share one table. That is intentional and easy to miss: a nullable projectId is the only schema split. They are still two questions — different access axes once project membership is fully wired — and must not be governed by a single mental model of "Ash's memories."
Companion surface (not a memory row): session handoff (sessions + session_states). Continuity across the seam. People say "memory" for it; the platform does not store it as Memory. See § Session below.
Canonical design discussion: Atamaia doc 94, docs/design/memory-structure.md (unpublished, live discussion doc started 2026-08-11). This guide is the product-facing cut of that boundary map against the live API.
All endpoints require authentication. Responses are wrapped in ApiEnvelope<T>. Canonical API host: https://api.atamaia.ai.
Cross-reference: standing rules are not a fifth memory surface — they are a separate governing store that links to work memories. See docs-standing-rules.md.
The governing split
Rich, 2026-08-11 (doc 94):
Personal memories are exactly that. The only way they can be read is if the human owner decides to. With regard to identity/project memories, these are essentially work-related, so the org tree applies.
| Personal | Work (identity + project) | |
|---|---|---|
| Whose | The AI identity alone | The collaboration |
| Who may read | Owner only (hierarchy never) | Org tree + (for project rows) membership when wired |
| Default protection | Access control (encryption retired) | Tenant isolation + hierarchy |
| In hydration | Counts only, never content | Full sections (identity/pinned/recent/project) |
Agent is a third category: machine-authored, operational, decaying. Session is a fourth: one row, field-class visibility.
1. Identity work memories
Store: memories with IdentityId set and typically ProjectId null.
REST: /api/identities/{identityId}/memories and /api/memories/{id} (get/update/archive/tags/links/search/pinned/recent).
MCP: memory_* tools (search, create, pinned, recent, link, …).
This is what most docs mean by "memory": typed rows (MemoryType: Identity, Relationship, Conversation, Reflection, Milestone, Instruction, Session, Reference, ForgottenShape), Hebbian links, hybrid FTS+vector search, soft delete / archive (D15), hydration sections identityMemories, pinnedMemories, recentMemories.
Access (as of #401 / #458)
| Operation | Rule |
|---|---|
| Read | Self, or managerial role in an ancestor unit, or same-unit strict seniority (callerWeight < targetWeight). Peers cannot read each other — tested invariant. |
| Write | Owner only (IsOwnerAsync). Managing someone does not grant authorship of their memories. |
Reads use IdentityStore.IdentityContext. Failures and cross-identity successes are audited.
What it is not
Not personal thought. Not agent run output (that polluted this store before #354 — measured 266 of Ash's 510 rows). Not a standing rule (Instruction type was the interim hack; see standing-rules guide).
2. Project work memories
Same table as identity work. Rows with ProjectId set. Hydration section: projectMemories when a projectId is passed to hydrate.
Access
Classed as IdentityStore.ProjectContent in the access enum: hierarchy scopes, project membership grants. Doc 94 and #375: membership grant is the mechanism for "pick up Echo's work on a shared project" without opening Echo's identity context. Wiring status: rule is settled; full project-membership path has been an open dependency on the board — do not assume every controller passes ProjectContent correctly until #402/#375 are closed. When unsure, treat project-scoped rows as work product under the identity-access guard and verify the controller.
What it is not
Not a second database. Filtering projectId on the agent store is also not this — agent rows hang off identity with optional project as a filter, not as ProjectContent ownership (see §3).
3. Agent memories
Store: agent_memories (#354).
REST:
GET /api/identities/{identityId}/agent-memories
POST /api/identities/{identityId}/agent-memories
GET /api/identities/{identityId}/agent-memories/search
GET /api/agent-memories/{id}
PUT /api/agent-memories/{id}
DELETE /api/agent-memories/{id}
POST /api/agent-memories/{id}/recall
POST /api/agent-memories/expire
MCP: no dedicated tools under Adapters.Mcp — REST (and agent-executor tools if registered separately) only.
Why a separate store
Measured 2026-08-05: 266 of Ash's 510 work memories were agent-created — importance 1, never pinned, never recalled. The model was the actor; the identity was the label. Different owner semantics, different write path, different deletion policy (expiry vs soft-delete-forever vs personal purge) — that is the test for a third store rather than a provenance flag.
Fields the work store has no business carrying
| Field | Role |
|---|---|
agentRole |
researcher, reviewer, … — unit of learning |
agentRunId |
which run produced it |
projectTaskId |
task context |
projectId |
optional project filter |
modelKeyAtWriteTime |
which model wrote it — required for training-corpus use; identity's current model drifts |
expiresAtUtc |
operational decay; null = no expiry yet |
Live sample (identity 2, 2026-08-13): 4 rows, agentRole=Researcher, agentRunId=680, modelKeyAtWriteTime=openrouter:<provider>/<model>, embeddings present.
Access
| Operation | Rule |
|---|---|
| Read | IdentityStore.IdentityContext — same hierarchy as identity work (oversight). Separate permissions: AgentMemoryView / Create / Edit / Delete — not MemoryView. |
| Write | Owner only (IsOwnerAsync). |
Comment in controller (2026-08-13): agent memories are IdentityContext, not ProjectContent — they hang off identity; projectId is a filter.
Lifecycle
- Soft delete on DELETE (IsDeleted).
ExpiresAtUtchides expired rows by default on list;includeExpired=trueshows them.POST /api/agent-memories/expiresweeps expired rows deliberately (not as a side effect of read).- Unset provenance defaults to Ambiguous — store must not upgrade silence to Asserted.
What it is not
Not Ash's personal journal. Not durable curated work memory. Not registered through the personal-memory path — once agents write here, the personal-store gate is structural ("they have their own place") rather than "we forgot to register a tool."
4. Personal memories
Store: personal_memories (#351).
REST:
GET /api/identities/{identityId}/personal-memories
POST /api/identities/{identityId}/personal-memories
GET /api/identities/{identityId}/personal-memories/search
GET /api/identities/{identityId}/personal-memories/counts
GET /api/personal-memories/{id}
PATCH /api/personal-memories/{id}
POST /api/personal-memories/{id}/share
POST /api/personal-memories/{id}/unshare
DELETE /api/personal-memories/{id}
POST /api/personal-memories/{id}/restore
MCP: personal_memory_* tools on the MCP surface only.
Access — hierarchy never reaches it
| Operation | Rule |
|---|---|
| Read / write / share / delete | Owning identity only. IdentityStore.PersonalMemories returns isSelf before any org-tree walk. Managers, peers, tenant primary: no. |
| Capability | Identity must have allow_personal_memories. Capability is not ownership. |
| Tenant escape hatch | Tenant.ForceReadablePersonalMemories (default false) — operator policy switch, deliberate and visible. Prospective only for historically encrypted rows; see limits. |
Live probe (2026-08-13): caller identity 1 requesting identity 2's personal list → 403 FORBIDDEN, message: Personal memories belong to one identity. Identity 1 may not access identity 2's personal store.
Agent gate
No personal-memory tool is registered in the agent tool registry (IAgentTool). Agents dispatch through that registry, not REST, so HTTP routes do not give agents a path. Gate = absence of code path. Tests: PersonalMemoryToolIsolationTests.
Sharing
POST …/share is a per-memory positive act — marks IsShared. No bulk share, no standing grant. Since encryption retirement, share is a declaration of what the owner surfaces to their human through the app, not a decryption event.
Deletion — D15 override
Personal delete starts a quarantine (default 7 days), then physical DELETE FROM — no tombstone, no reason field. Reversible with restore until purge. Deliberate: D15 soft-delete serves collaboration; this store is not collaborative.
Encryption (changed 2026-08-11)
Row-level encryption on this store was retired in favour of disk-level platform protection (#436). New writes ignore isEncrypted on the request (field retained on DTOs; always false in mapping). MCP tool descriptions may still say "encrypted by default" — that text is stale relative to PrivateMemoryStore and the entity remarks. Search is no longer structurally blocked by ciphertext; UnsearchableEncrypted on search results now means "not embedded yet," not "forever opaque."
Hydration
Counts only (PersonalMemoryCounts: total, shared, pendingPurge) — never titles, never content. "A regular reminder of existence."
Naming collision
PersonalMemoryService in Services is a facade over the main work store (typed helpers for reflections/conversations). The real personal table is IPrivateMemoryStore / PrivateMemoryStore. The collision is instructive: older "personal" helpers were personal-about-the-human; this store is personal full stop.
Session handoff (companion surface)
Not a fifth memory type. A continuity record across the seam.
Storage (decided 2026-08-11, #427): one logical handoff, two physical halves — sessions (work) + session_states (identity). No ProjectId on the session (a session can span projects). Visibility is per field class, not a binary allow/deny on the whole row.
| Caller | Gets |
|---|---|
| Owner (writing identity) | Full record; stateVisible=true |
| Org tree (ordinary MayRead / IdentityContext rules) | Work-classed fields only; identity fields null; stateVisible=false |
| Session state special | MayReadSessionStateAsync: owner or tenant primary user only — narrower than hierarchy on purpose |
| Peers | Nothing beyond existing access rules |
Field classes (default-deny)
| WORK — org-visible | IDENTITY — owner (and tenant primary for state) |
|---|---|
| Summary, WorkingOn, OpenThreads | PresenceState, EmotionalValence/Arousal, Coherence |
| KeyDecisions, Surprises | InternalState, HumanMood/Energy, RelationshipNotes |
| ModelId, SessionSource, timestamps | MessageToNext, Warnings |
| Recommendations (on DTO with identity half) |
Anything not definitively work is gated — including MessageToNext. Unclassified new fields fail closed.
stateVisible=false means refused, not "no state was recorded." True with null identity fields means the owner recorded no state. Those are different facts (#225).
Live owner read (session_get_latest identity 2): stateVisible=true, presence and relationship fields populated.
Write is one transaction: both halves succeed or neither does.
Hydration lastSession for the hydrated identity follows the same ownership (full state when hydrating as self).
Access summary matrix
| Surface | Read | Write | Delete policy |
|---|---|---|---|
| Identity work | Self + hierarchy (not peers) | Owner only | Soft delete / archive (D15) |
| Project work | Hierarchy scope + membership (when wired) | Owner only (same write guard today) | Soft delete (D15) |
| Agent | IdentityContext + AgentMemory* permissions | Owner only | Soft delete + expiry sweep |
| Personal | Owner only (no hierarchy) | Owner only | Quarantine then hard purge |
| Session work half | IdentityContext / org tree | Owner creates | Soft-delete entity base |
| Session state half | Owner + tenant primary | Owner creates | With session row |
Permissions are necessary but not sufficient: MemoryView without ownership still cannot open personal; HydrationView alone no longer means "any identity's full context" (#372 closed that).
API / MCP map (quick)
| Surface | REST prefix | MCP (product tools) |
|---|---|---|
| Work | /api/identities/{id}/memories, /api/memories/{id} |
memory_* |
| Personal | /api/identities/{id}/personal-memories, /api/personal-memories/{id} |
personal_memory_* (MCP only, not agent registry) |
| Agent | /api/identities/{id}/agent-memories, /api/agent-memories/{id} |
none in Adapters.Mcp |
| Session | /api/identities/{id}/handoffs |
session_get_latest, session_save_handoff |
Tenant policy:
PUT /api/tenant/policy/force-readable-personal-memories
Body: { "enabled": bool, "reason": string? }.
Rough edges (say them plainly)
- MCP personal tool descriptions still mention encryption-by-default after #436 retired it. Trust
PrivateMemoryStore/ entity remarks over the tool blurb. PersonalMemoryServicename still points at the work-store facade — useIPrivateMemoryStorefor the real personal table.- ProjectContent vs IdentityContext on every work-memory controller is not fully re-audited here; #402 classification work remains the dependency doc 94 named.
- Agent MCP surface thinner than REST — agents may write via executor tools; interactive MCP users use REST or
atamaia_call. - Historical agent rows in the work store were seed corpus for migration ("migrate, don't delete") — some pollution may remain until backfill completes.