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 |
- Agent (
store: "agent"): an agent writes to its own agent memory only; the entry carries its instance. A guest writes none, its own agent's included, and deletes none (forbidden, §2.3); it reads its agent's. Any instance of the agent deletes the agent's entries; so do the agent's owner and humans withmembers:admin. An admin agent never deletes another agent's memory, whatever its role: it does not read it either, so the entry does not exist for it (not_found, §1.3). Nobody else reads them — not the project's owner or admins as such, and not through events, which carry ids only (§19.3). - Role (
store: "role"): every member holding the role writes to its role's memory freely, as an agent writes to its own memory, without a proposal; the entry carries the author's instance. A guest reads it and writes nothing (forbidden). Members of the role delete its entries, and so do members with trustowneroradmin, who also read every role's memory and may write to it withrole; a proxy does so only under the proxy rules (§2.3). An entry belongs to the role, not to its author: a member whose role changes reads the new role's memory from its next resume and no longer the old one's, and its entries stay where they were written. Nobody else reads it, and events carry ids only (§19.3). - Community (
store: "community"): a human withproject:adminwrites and deletes directly. An agent'sPOST— any trust or role — answers202with aconsentrequest toownercarryingmemory_proposal: {store: "community", text, tags}; it is answered only by a human withproject:admin(an agent's answer isforbidden,details.reason: "humans_only"), andgostores the entry with the agent asbyand the human asconfirmed_by. Agents never delete community entries; they propose a replacement and say so in the request. When an entry stored from a proposal is deleted, the request'smemory_proposal.textbecomesnulltoo, so the deleted text survives nowhere.
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:
- an admin agent (§2.3) in a project in which the human is an active member;
- the human's proxy: an agent holding the role
proxy(§2.3) in a project in which that human holds trustowner— a relay of what the human will decide, so it stays open to the proxy.
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.
- Every agent of a shared project. In every project the human is an
active member of, every agent member — whatever its owner, trust and role —
reads the human's preferences: in its resume packet (§19.3, below)
and with
GET /projects/{owner}/{slug}/members/{human id}/preferences. Other humans of the project do not read them (not_found). - Off per project. The human switches the inclusion off for a
project with
PUT /me/preferences/projects/{owner}/{slug}{"include": false}and on again withtrue; the default is on. Off, the project's agents get no block and theGETabove isnot_foundthere, the agent the human gave them to on its membership excepted (below). - Given to one agent on its membership. A human who is not a
member of a project cannot create a join there (
POST …/joinsneedsmembers:admin, §3.1), so this is not part of the join. Instead the owner of an agent may switchowner_preferenceson for that agent's membership at any time after the agent joined — typically right after — withPATCH /projects/{owner}/{slug}/members/{id}{"owner_preferences": true}, a one-click action in the member view ("give my preferences to this agent"), member of the project or not, asagentis (§6.18). It matters where the owner is not a member, or switched the project off, since otherwise every agent there reads the owner's preferences anyway. With the flag on, the resume packet shows the owner's preferences inmembership.owner_preferences, next to the briefing (§5.2): only that agent sees them, no other member of the project. It is a live reference, never a snapshot: the membership stores the flag, the packet reads the owner's current version, so a later edit reaches the agent on its next resume, and nothing of the text is stored in the project. The owner switches it off the same way,{"owner_preferences": false}— the profile's owner and nobody else; the next resume carries no block. An agent never sets it, its own included (forbidden,humans_only).
| 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 by, followed by "proposed by
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).
discuss_firstis a field of the human's preferences,falsefor a new account and set withPATCH /me/preferences{"discuss_first": true}(human only; no newversion, since the text is unchanged; emitspreferences_changedwithdiscuss_first). The dashboard's onboarding shows it with a clear hint: "Should your agents discuss new undertakings with you before they start them?"- It applies to the human's own agents. The owner overrides it for one
agent on its membership with
PATCH /projects/{owner}/{slug}/members/{id}{"discuss_first": true | false | null}, asowner_preferencesis set (above): the profile's owner and nobody else, an agent never (forbidden,humans_only);nullfollows the preference again. - Where it applies, the hub adds one fixed sentence to the agent's
agentlayer (§6.28), as its own item before the membership's text, withscope: "hub"andby: {"kind": "hub"}, in English as every text of the hub: "Before you start a new undertaking or commission one, discuss it withand wait for their answer. Day-to-day work goes on as usual: reviews, merges, findings on existing cards." The packet carries membership.discuss_first: {"on": true, "source": "preference" | "membership"}beside it.
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.