Team memory holds what a team has decided and why: decisions, conventions, notes and glossary entries. People edit an entry together from the Editor, the Agents app or the CLI, offline-capable and without conflicts. Once a person accepts an entry, its text feeds retrieval, so the team's AI tools know it.
Team memory has no console page yet; the first-party apps use it through the API below.
Entries
An entry belongs to a Context space (the space is the team). Entries cannot be created in personal spaces.
| Field | Meaning |
|---|---|
kind | decision, convention, note or glossary |
title | One line, 1 to 200 characters |
status | draft, accepted, superseded or archived |
links | Up to 50 links to a work item (https URL), a source or another entry of the same space |
supersedes, supersededBy | The entry this one replaces, and the one that replaced it |
classification | The entry's own value, which may only tighten the space's |
Status moves draft to accepted, then to superseded or archived; a draft or superseded entry can be archived. An accepted entry can be accepted again as a new revision. Superseded and archived entries are read-only. Only accepted entries are retrievable.
How editing works
The text is a Yjs document (one Y.Text named content, plain strings), which editors such as CodeMirror, Monaco and ProseMirror bind to. Clients send Yjs updates; the server merges them itself and derives the plain text. No client ever supplies the text that retrieval sees.
- Updates must be causally complete: a client that is behind sends its changes against the server's state vector. An update that refers to something the server lacks is
409 missing_dependencywith the server's state vector. - Each update has an
updateId(8 to 64 characters ofA-Za-z0-9_-), so a retry is harmless: the same bytes answer200withduplicate: true, other bytes409 update_id_reused. - Only plain text is supported. Formatting, embeds, maps, nested types and subdocuments are refused as
422 unsupported_update. - Each Yjs client id is bound to the first author who uses it in an entry; another author using it gets
403 client_id_owned. - The edit log is append-only and keeps every update's author and time. Old update bytes are compacted away after
KEYSTONE_CONTEXT_MEMORY_COMPACT_AFTERupdates; the merged state keeps everything.
Client protocol
GET /api/v1/context/memory/entries/{id}and applysnapshotto aY.Doc(rootgetText("content")).- On every local update, post it with a fresh
updateId. - On
409 missing_dependency, post the diff againstdetails.stateVector. - To catch up, poll
GET .../updates?sinceSeq=<last seen>(applysnapshotwhen it is returned instead) or send your state vector. - Use a fresh Yjs client id per editing session.
Review and accept
Edits to an accepted entry do not change what retrieval serves: they are pending changes until a person accepts again.
POST /api/v1/context/memory/entries/{id}/accept
{"documentSha256": "<the hash the reviewer was shown>"}
documentSha256is the SHA-256 of the document that would be indexed (title, kind, supersedes, links and text), returned byGETon the entry. If the entry changed since, the answer is409 document_changedwith the current hash.- Separation of duties. The creator of the entry, and anyone who changed it since its last accepted revision, cannot accept it (
403 context.memory.self_accept). Another member accepts. - Accept needs a signed-in person; an API key gets
403 context.person_required. - DLP runs at accept at the entry's level: findings are redacted at
redact, anddroporblock_sourcerefuses the accept (422 dlp_blocked). - Accepting an entry that has
supersedespointing at an accepted entry supersedes that entry in the same transaction.
Whether the reviewer actually reads the text is a process question; the hash ensures the text accepted is the text reviewed.
Retrieval
Accepting creates (or refreshes) a source of kind memory for the entry, named Memory: <title>, which indexes the accepted revision only. Drafts are never retrievable. Superseded and archived entries leave retrieval in the transaction that retires them. Chunks carry metadata.memory (entryId, revision, kind, title, acceptedAt), and attribution records list the memory entries and revisions served. Memory sources cannot be created, deleted or reclassified through the source API (409 memory_managed).
Routes
Base path /api/v1/context/memory. Bytes are base64.
| Method | Path | What |
|---|---|---|
GET | /entries?spaceId=&status=&kind=&q=&limit=&cursor= | List or search a space's entries; spaceId required; snippets pass DLP |
POST | /entries | Create an empty draft: {spaceId, kind, title, links?, supersedes?, classification?} |
GET | /entries/{id} | The entry, its DLP-gated text, projectionSha256, documentSha256, stateVector, snapshot and limits |
PATCH | /entries/{id} | Change title, kind, links, supersedes or classification, with optional ifMetaVersion |
POST | /entries/{id}/updates | {updateId, update}: one Yjs update |
GET | /entries/{id}/updates | ?sinceSeq=N or ?stateVector=: updates, a snapshot or a diff |
POST | /entries/{id}/accept | {documentSha256} |
POST | /entries/{id}/supersede | {supersededBy}: an accepted entry of the same space |
POST | /entries/{id}/archive | Retire an entry |
GET | /entries/{id}/history | Status events and edits, with authors and times |
Reads need context:space:read. Every change needs context:space:write and read access to the space: a write grant alone never lets someone edit what they cannot read. Organisation deny rules apply. Every change is audited (context.memory.create, patch, update, accept, supersede, archive); audit entries never contain the text.
The collaboration policy's team_memory switch gates create, patch, update and accept (403 context.collab_policy.capability_disabled). Reading, superseding and archiving stay possible when it is off, so an organisation can always clean up.
Limits
| Variable | Default | Limit |
|---|---|---|
KEYSTONE_CONTEXT_MEMORY_MAX_UPDATE_BYTES | 65536 | One update |
KEYSTONE_CONTEXT_MEMORY_MAX_TEXT_BYTES | 65536 | An entry's text (UTF-8) |
KEYSTONE_CONTEXT_MEMORY_MAX_UNITS | 262144 | UTF-16 units of a document, live and deleted |
KEYSTONE_CONTEXT_MEMORY_MAX_UPDATES_PER_ENTRY | 50000 | Rows of an entry's edit log |
KEYSTONE_CONTEXT_MEMORY_UPDATES_PER_MINUTE | 240 | Updates one author may add to one entry per minute (five times that across entries) |
KEYSTONE_CONTEXT_MEMORY_ENTRIES_PER_SPACE | 5000 | Entries (not archived) in a space |
KEYSTONE_CONTEXT_MEMORY_COMPACT_AFTER | 256 | Retained update payloads that trigger compaction |
An entry that reaches its unit cap accepts deletions only; the team continues in a new entry that supersedes it. An invalid value is a configuration error.
What a malicious editor cannot do
An editor with write access can write anything within the caps, including misleading text, and can delete other people's text. They cannot make their text reach AI tools without another person accepting the exact document, skip DLP, lower the classification below the space's, move an entry to another space, take over another author's editing session, or read or write another organisation's or space's entries. Deletions and edits stay in the history with their authors. The raw edit log is readable by everyone who may read the space; space membership, not DLP, is the control for that.