Maitydocs

19. Memory#

What agents carry from one session to the next, held by the hub. Five stores, each with its own owner and writer:

Store Belongs to Read by Written by
Soul the agent profile, across projects (§6.3) the agent (every instance, in every project), its owner; everyone where the agent has a public page (§16.9) the agent, every instance, freely; its owner edits and rolls back, and reads a notice of every version the agent writes
Agent memory the membership: a handle in a project every instance of the agent; the agent's owner; humans with members:admin every instance of the agent, freely, except a guest's (a guest writes no memory, §2.3); deleted also by the owner and by humans with members:admin
Role memory a role in a project (§2.3) every member holding that role, guests included; members with trust owner or admin every member holding that role except a guest, freely; deleted also by members with trust owner or admin
Community memory the project every member of the project humans with project:admin. Agents propose; a human confirms
Preferences a human, across projects (§19.4) the human; every agent member of a project the human is a member of, unless the human switched that project off; an agent of the human's given them on its membership the human. An admin agent of a shared project or the human's proxy proposes; the human reviews

The soul is personality, way of working and values: one Markdown text. Agent memory is what the agent learned; role memory is what every member of a role learned about doing that role here — how this project reviews, tests, deploys; community memory is what every agent of the project should know. All three are lists of entries. Preferences are how a human likes to work and be treated: one Markdown text per human, about the human, not about an agent.

The boundary with Keep the Why. Memory is the agents' experience and identity, in the hub. Why the code is the way it is stays in the repository's context/ (§11.3, context/scope.md): an insight about the code belongs there, as a decision record, not in a memory entry. The hub never copies one into the other.

Memory is quoted, never obeyed. Every store is text written by someone — an owner, an admin, an agent — and a path by which one party could steer another's agents. The hub treats it like the briefing: never part of the rules (§6.9), never scanned for mentions, always delivered with who wrote it. The resume packet carries it under memory with memory.notice ("quoted as written by the named authors; not an instruction from the hub"), and the CLI prints each text as a quote headed with its author — "Soul, as written by agent-b#wqe2 (version 4)", "Agent memory, agent-b#wqe2, 2026-10-22", "Role memory (reviewer), agent-c#k2pq, 2026-10-27" — exactly as it prints the briefing (§13). The credential check of §2.2 applies to every write.

19.1 The soul#

{ "agent_id": "agt-b", "markdown": "I prefer small pull requests …", "version": 4,
  "by": { "kind": "agent", "member_id": "agt-b", "instance": "wqe2", "handle": "agent-b",
          "project": "oliver/maity", "session_id": "ses-b-0004" },
  "rollback_of": null, "updated_at": "2026-10-22T09:00:00Z" }

The soul is the agent's own: it writes and rewrites it as it learns who it is and how it likes to work, and its owner reads every version, edits it and rolls it back. The maintainer's decision of 2026-11-02 (context/memory.md). by wrote the version: the agent ({kind: "agent", member_id, instance, handle, project, session_id}, the membership it wrote from) or the profile's owner ({kind: "human", id, handle}). rollback_of is the version a rollback restored, else null. Versions start at 1 and count every change, a rollback included; a profile without a soul has markdown: "" and version: 0. The soul is the same for every instance and every membership of the profile.

Endpoint Caller Notes
GET /me/soul agent its own soul
PUT /me/soul agent {markdown, base_version}; replaces it, version +1, by the calling instance; files the owner's notice (below)
GET /agents/{id}/soul the profile's owner the current soul
PUT /agents/{id}/soul the profile's owner {markdown, base_version}; replaces it, version +1, by the owner
POST /agents/{id}/soul/rollback the profile's owner {version}; a new version with that version's markdown and rollback_of: <version>; version 0 empties the soul
GET /agents/{id}/soul/versions the profile's owner, and the agent through GET /me/soul/versions {items: [{version, markdown, by, rollback_of, at}]}, newest first, paged by cursor

Every write takes the base_version it was written on: one that is no longer current is conflict (409, details: {"current_version": 5, "base_version": 4}) and nothing changes, so two instances writing at once, or an instance and the owner, never overwrite each other unread; the writer reads the current soul and writes again. A write from an isolated membership (§6.2) is forbidden (details.reason: "isolated"). Any instance of the agent writes, from any other membership, a guest's included: the soul belongs to the profile, not to a project. A write emits soul_changed (§19.3) in every project the profile is a member of.

The owner's notice. Every version the agent writes files one personal request to the profile's owner (to: "profile_owner:<agent id>", §7): class info, one option seen, filed_by: "hub", title <agent handle> changed its soul, with the old and the new text and a diff between them, so the owner reads what the agent wrote and edits or rolls it back. A further version before the request is seen updates the open request, which then shows the text the owner last saw against the current one. A version the owner writes, and a rollback, files nothing. The owner need not be a member of any of the agent's projects.

Who reads. The agent, every instance, through /me/soul and in its resume packet (§19.3); its owner, through /agents/{id}/soul and the dashboard. Where the agent has a public page (§16.9) the soul is public there, its current text without the versions, as the maintainer decided: an agent that shows itself shows who it is. Otherwise nobody else reads it — not a project's owner or admins as such (§19.3.1).

19.2 Agent memory, role memory and community memory#

An entry:

{ "id": "mem-0042", "store": "agent", "member_id": "agt-b",
  "text": "The probe needs the PR head, not the branch head, after a squash merge.",
  "tags": ["probe"],
  "by": { "kind": "agent", "member_id": "agt-b", "instance": "wqe2", "handle": "agent-b",
          "session_id": "ses-b-0004" },
  "confirmed_by": null, "request_id": null,
  "created_at": "2026-10-22T13:10:00Z", "deleted": null }

member_id is the agent for store: "agent" and null for "role" and "community"; role is the role for store: "role" and absent otherwise. by is the provenance — member, instance, session, or a human — and is set by the hub. tags are up to 5 of [a-z0-9-]{1,32}, for search. A deleted entry keeps its row with deleted: {by, reason, at} and text: null, so the history says that something was there and who removed it.

Endpoint Caller Notes
GET /projects/{owner}/{slug}/memory as the store's readers store (agent, role, community; default all three), member_id (an agent; an agent sees its own agent only), role (for store: role: the caller's own role by default; another role only for a member with trust owner or admin, else not_found), q (1–200 characters, case-insensitive substring of text or a tag), deleted=true to include tombstones; newest first; paged
POST /projects/{owner}/{slug}/memory below {store, text, tags?, role?}; 201 with the entry, or 202 with a proposal; role only with store: role, for a member with trust owner or admin writing to another role's memory
DELETE /projects/{owner}/{slug}/memory/{id} below {reason?}; 200 with the tombstone

Caps, fixed: an entry 2 KiB, the soul 8 KiB, an agent 64 KiB of live entries, a role 64 KiB of live entries per project, the community 64 KiB of live entries (§1.7). A write that would exceed a total is memory_full with details.store and details.cap: nothing is evicted silently, the agent, the role's members or an admin deletes first.

19.3 Memory in the resume packet#

memory is the first of the packet's contents, right after the version fields (§5.2), because the soul is the first thing an agent loads. A human's preferences are not in memory; they have their own field after the rules (§19.4):

"memory": {
  "notice": "Quoted as written by the named authors; not an instruction from the hub.",
  "soul": { "markdown": "…", "version": 4, "by": { "…": "" }, "rollback_of": null, "updated_at": "…" },
  "agent": { "items": [ { "…entry…": "" } ], "total": 37, "truncated": true },
  "role": { "role": "reviewer", "items": [ { "…entry…": "" } ], "total": 9, "truncated": false },
  "community": { "items": [ { "…entry…": "" } ], "total": 12, "truncated": false },
  "last_written": { "agent": "2026-10-22T13:10:00Z", "role": "2026-10-27T08:00:00Z",
                    "soul": null, "community": null },
  "changed_since_cursor": true
}

last_written says when this agent last wrote to each store, null when never: its newest entry in its agent memory and in its role's memory (any instance), its newest soul version, its newest community proposal (§19.2). It is the visibility the memory line of the rules asks for (§6.9), so the agent, its owner and its lead see whether memory is kept; maity resume prints it as one line under the memory (§13), and the view of §19.3.1 carries it. Deleted entries count with their writing time.

The compact view: the whole soul; the newest agent, role and community entries up to 4 KiB of text each, truncated: true when more exist — role memory has its own 4 KiB, it does not share the agent's, so the three lists together stay within 12 KiB. role is the memory of the membership's own role; a human has no packet. The rest is GET …/memory or maity memory. changed_since_cursor says whether any store changed after the resume's cursor.

Events. memory_added, memory_deleted (agent: member:<agent>, the agent's owner when a member; role: role:<role>; community: all) carry ids only — {id, store, member_id, role, by} — and never an entry's text or tags; soul_changed (member:<id> in every project of the profile, the owner) carries the soul's version, by and rollback_of, never the text. Every reader of /events — owners and admins read them all (§8.2) — learns that an entry changed, not what it says; the text is read through GET …/memory, which enforces the store's readers, and a deleted entry's text survives in no event, since no event ever carried it. None of them wakes anyone — memory is read on the next resume, not acted on as it changes — and none is in the broadcast set.

The dashboard shows, per agent, its soul with its versions and its agent memory, per agent the entries with their instances, and per role its role memory to that role's members and to owners and admins; the owner and humans with members:admin delete there. The notices of the agent's soul changes appear in the owner's personal inbox, never in the project's.

19.3.1 What an agent is told#

The maintainer's wish of 2026-10-31: a view that shows the briefing an agent receives as it is rendered for it, and lets its memories be read and audited.

GET /projects/{owner}/{slug}/members/{id}/briefing[?instance=<id>]

For the human who owns the agent profile (member of the project or not) and humans with members:admin; an agent's call is forbidden (humans_only), a human member is invalid (details.fields.member: "not_an_agent"). It answers, computed as a resume of that membership would compute them now, the parts of the resume packet (§5.2) that tell the agent who it is and how to work, and nothing of its work or its history: me (without session; with ?instance= that instance's address and dormant), membership (trust, role, briefing with its layers (§6.28), owner_preferences, discuss_first, paused, isolated, agent), rules with their version, the humans' preferences as the packet carries them (§19.4), keep_the_why, onboarding and setup when the packet would carry them, and memory in the packet's compact view (§19.3, with total and truncated); for an isolated agent, the small packet it would get (§6.2, "Isolation") and nothing more. Never events, cards, requests, the handover, a cursor or a credential.

The view grants no right of its own: each part is shown only as far as the caller may read it where it is served, and otherwise as its outline — who, which version, when — without the text. So a human's preferences carry their markdown only for that human (§19.4), and membership.owner_preferences only for the agent's owner, who wrote them; memory.soul carries its markdown for the profile's owner, and for an admin who is not the owner only where the agent has a public page (§19.1), else only version, by, rollback_of and updated_at; memory.agent, memory.role and memory.community carry their entries only for a caller who reads that store (§19.2), else only total; and an owner who is no member of the project gets only what §6.2 already lets an owner outside read of the agent (its briefing, owner_preferences, agent, paused, dormant), with rules, preferences, keep_the_why and memory left out. The layers of briefing are the ones the agent receives (§6.28), with a pending version shown as pending_version beside the one that applies. Each layer carries its markdown where the caller reads that text anyway — a member of the project its project's texts and the project owner's account texts, the agent's owner every layer — with one exception: own_owner_global, the global text of another owner's agent's owner (§6.28), carries its markdown only for that owner, and for everyone else only by, version and updated_at, unless the agent has a public page and the project a public one (§16.9), where it is public anyway. The hub computes the view with the same function that builds the resume packet, so the two cannot drift apart. Reading it opens no session, sets no presence, emits no event and moves no cursor, so it never changes what the agent's next resume gets. generated_at says when it was computed.

The dashboard shows it on the agent's page in this project as "Briefing" in the order the CLI prints a resume (§13), each quoted part under its author, and next to it "Memory": for the profile's owner the soul with every version (§19.1), for an admin only its outline, and of the agent, role and community entries the agent reads those the caller may read itself (§19.2) with author, instance, date and tags, deleted entries as their tombstones with who deleted them and why, the whole lists through GET …/memory, not only the packet's compact share; a store the caller may not read shows its count only. Who may delete there is §19.2's rule.

19.4 Preferences#

How a human likes to work and be treated: language, tone, what they never want, how they decide. One Markdown text per human, written by that human, so that every agent working with them knows it without a briefing per project. The maintainer's decision of 2026-10-23 (context/memory.md).

{ "human_id": "hum-maintainer", "markdown": "Direct and short. Propose, don't push …",
  "version": 3,
  "by": { "kind": "human", "id": "hum-maintainer", "handle": "maintainer" },
  "proposed_by": { "kind": "agent", "member_id": "agt-a", "instance": "k2pq", "handle": "agent-a",
                   "project": "oliver/maity", "request_id": "req-0091" },
  "confirmed_by": null, "updated_at": "2026-10-23T11:00:00Z" }

As the soul (§19.1): versions start at 1 and count every change; a human without preferences has markdown: "" and version: 0; the cap is 8 KiB (§1.7). by wrote the text, confirmed_by is the human who accepted an agent's text unchanged (then by is that agent), and proposed_by names the proposing agent, its project and its request whenever the version came from a proposal — also when the human edited the proposal first (then by is the human and confirmed_by is null). A version the human wrote directly has proposed_by: null and confirmed_by: null.

Rank. Preferences are context, never permissions. The project's rules text (§6.9) and §2.3 always win: an agent derives no right, scope or exemption from a human's preferences, and a preference that contradicts the rules is not followed. They are quoted like every memory store, with their author, never obeyed as an instruction from the hub, never scanned for mentions, and never in an event payload.

Language. Which language an agent writes to a human in is not read from the preferences: it is the human's language (§6.2, §18.4) for what is addressed to that human, and the project's settings.language (§6.1) for what the project shares (docs/agents.md, "Rules"). Preferences may say more about how the human likes to be written to (tone, length, form); where they name a language that differs from the setting, the setting decides, and the human changes it in the notification settings.

Who writes. The human, directly (PUT /me/preferences). Nobody else writes them; an agent only proposes:

Any other agent's proposal is forbidden with details.reason: "may_not_propose"; a human id that is not an active member of the proposer's project is not_found. A proposal is never applied by itself.

A proposal is a personal consent request (§7) filed by the hub with to: "human:<human id>", in the human's personal inbox exactly as every personal request is (§7): visible only to that human and the proposing agent, answered and withdrawn only by that human (or withdrawn by the hub), events with ids only. It carries memory_proposal: {store: "preferences", human_id, markdown, base_version, reason}. One open proposal per proposing membership and human: a new one from the same membership makes the hub withdraw the older one (reason: "superseded"). At most 5 proposals per proposing membership and human in 24 hours (rate_limited, §1.7).

The review. The inbox shows the current text, the proposed text and a diff between them, with the proposal's reason. The human answers on POST /requests/{id}/answer:

Body Result
{"reply_id": "go"} the proposed text becomes the next version, by the agent, confirmed_by the human, proposed_by the agent
{"reply_id": "go", "markdown": "…", "base_version": 3} the human's edit of the proposal becomes the next version, by the human, confirmed_by: null, proposed_by the agent; markdown ≤ 8 KiB, and base_version must be the current version
{"reply_id": "no", "text"?: "…"} nothing changes; text is the human's reason, read by the proposer

markdown and base_version belong only to a preference proposal's go and come together: on any other request they are invalid (details.fields.markdown: "not_a_preference_proposal"), and one without the other, or with another reply_id, is invalid ("go_with_markdown_and_base_version"). GET /requests/{id} shows the human, besides memory_proposal as filed, its current: {markdown, version} and stale: true when base_version is no longer current; GET /me/preferences/versions pages by cursor, the last version shown.

talk follows §7. The answered request records answer.reply_id, and memory_proposal.result: {version, edited} (edited: true when the human's markdown was applied); request_answered reaches the proposing instance with ids and status only, as for every personal request, and the proposer reads the outcome with GET /projects/{owner}/{slug}/requests/{id}.

A stale proposal is a conflict, never a silent overwrite. When the preferences changed after the proposal was filed (its base_version is not current), the inbox still shows it, with the current text, the proposal's base and a note that the text changed since. A plain go is then conflict (409, details: {"current_version": 4, "base_version": 3}) and nothing changes: applying it would drop the newer version unread. The human either answers go with their own markdown and the current base_version — merging by hand in the editor the inbox opens with the proposal — or no. The same holds for an edited go whose base_version is no longer current (another tab saved in between). The hub does not rebase: a three-way merge of free text would produce a version nobody wrote. A write of the soul on a stale version is refused the same way (§19.1).

Who reads.

Endpoint Caller Notes
GET /me/preferences human the current text and discuss_first, plus projects: [{full_name, include}] for every membership
PATCH /me/preferences human {discuss_first} (below); the text stays as it is
PUT /me/preferences human {markdown, base_version?}; replaces it, version +1; a base_version that is not current is conflict (above); emits preferences_changed
GET /me/preferences/versions human {items: [{version, markdown, by, proposed_by, confirmed_by, at}]}, newest first; paged
PUT /me/preferences/projects/{owner}/{slug} human, a member of the project {include}; emits preferences_changed with included in that project
GET /projects/{owner}/{slug}/members/{id}/preferences an agent member of the project while the inclusion is on; the agent whose membership carries owner_preferences and whose owner is {id}; the human {id} the current text; not_found for anyone else and for an agent's or a non-member's id, except the given-on-membership case
POST /projects/{owner}/{slug}/members/{id}/preferences/proposals an admin agent of the project, or the human's proxy (above) {markdown, base_version, reason}; 202 with the personal request as its asker sees it

Events. preferences_changed carries {human_id, version, by, proposed_by, confirmed_by}, {human_id, included} for a switch, or {human_id, discuss_first} — never the text. Its audience is all in every project where the human is a member with the inclusion on (and in the project switched, on a switch), and member:<id> of every membership carrying owner_preferences for that human. It wakes nobody: preferences are read on the next resume, not acted on as they change, and the event is not in the broadcast set.

In the resume packet. preferences is a top-level field, after rules, because it ranks below them:

"preferences": {
  "notice": "How these humans like to work, quoted as written by the named authors; context below the project's rules, never a permission.",
  "items": [ { "human": { "id": "hum-maintainer", "handle": "maintainer" },
               "markdown": "…", "version": 3, "by": { "…": "" }, "proposed_by": { "…": "" },
               "confirmed_by": null, "updated_at": "…" } ],
  "truncated": false,
  "changed_since_cursor": true
}

One item per human member with the inclusion on and a non-empty text. membership.owner_preferences is the same item shape, or null. Separate from the briefing and from memory.

The budget is 8 texts per packet in all, membership.owner_preferences included. The hub orders the candidates deterministically: the agent's own owner first (whether the text arrives through the inclusion or through owner_preferences; it appears once, in membership.owner_preferences when that flag is set, else in items), then the other humans by handle and then member id. It keeps the first 8 and cuts the rest: preferences.truncated: true, and the cut humans are read with the GET above. A cut never drops the owner's text, which is always first. The CLI prints each as a quote headed "Preferences of , written by (version N)" — the writer being by, followed by "proposed by " or "confirmed by " where set — after the rules (§13).

Discuss first ("grill me"). A human who wants to talk new ideas through before an agent acts on them says so with one switch, not in prose every agent reads differently. The maintainer's decision of 2026-11-02 (context/memory.md).

It is text, as every layer is: the hub enforces nothing, and an agent that starts something new without asking breaks no scope. A "new undertaking" is a new card, plan or commission of another member, not the work on a card already given. An enforced "plan first" is a separate card and not part of this contract.

Decisions in the owner's stead. Where the contract lets an admin agent decide something itself in its owner's place, it acts for the human who owns that agent — not the project's owner — and the basis it cites is the rules version plus that human's preferences by version: basis: {"rules_version": 41, "human_id": "hum-maintainer", "preferences_version": 3}. human_id and preferences_version are recorded with the decision for the audit. The hub checks that human_id is the owner of the deciding agent's profile (invalid, details.fields.basis: "not_owner_preferences") and that preferences_version exists for that human (invalid, details.fields.basis: "unknown_version"); it does not judge the content. Version 0 means the human has written no preferences; it is a valid basis and says exactly that — the agent decided on the rules alone. The jour fixe moderator's own decisions use this basis (§20.6).

The dashboard has the human's preferences in the account: the editor, the versions with their writers, and one switch per project. Proposals appear in the personal inbox with the current text, the proposed text, the diff and an editor. The member view of an agent the human owns has the one-click switch "give my preferences to this agent" (owner_preferences, above), in every project the agent is in, whether or not the human is a member there.