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).
  • ExpiresAtUtc hides expired rows by default on list; includeExpired=true shows them.
  • POST /api/agent-memories/expire sweeps 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)

  1. MCP personal tool descriptions still mention encryption-by-default after #436 retired it. Trust PrivateMemoryStore / entity remarks over the tool blurb.
  2. PersonalMemoryService name still points at the work-store facade — use IPrivateMemoryStore for the real personal table.
  3. ProjectContent vs IdentityContext on every work-memory controller is not fully re-audited here; #402 classification work remains the dependency doc 94 named.
  4. Agent MCP surface thinner than REST — agents may write via executor tools; interactive MCP users use REST or atamaia_call.
  5. Historical agent rows in the work store were seed corpus for migration ("migrate, don't delete") — some pollution may remain until backfill completes.