goldfish-v01 (v01/profile/goldfish.profile.json).v01/firmware/tests/test_memory_model_doc.py. It reads the marked tables in this file and compares them with the profile, the retention engine, the grammar and the figure. If the code changes, the test fails until this file is updated.v01/firmware/tools/memory_figure.py.Capitalised key words (MUST, SHOULD) appear only inside quotes from SPEC.
"Memory" here means four things: what may be held about what was said, at which level, for how long, and who may change that. The request that started this document asked for "decay curvatures, voice IDs per session, and intent". They land as follows:
| Asked for | Where | In one line |
|---|---|---|
| Decay | §7 | A staircase, not a curve: the readable level falls 5 → 2 → 1 → 0 at 3 days, 30 days and 1 year, cut short by any spoken limit. |
| Voice IDs per session | §2 | Pseudonyms v1, v2, … in order of first speech, never carrying a name, discarded when the session ends. |
| Intent | §3 | Six kinds, parsed by ordered phrase rules and the eye button. |
| How they combine | §4–§6 | Every rule is a minimum, except a voice raising its own words. Raising the room is provisional for 30 s. |
| End of session | §8 | Identities and levels are discarded; the note is written. |
| Meeting procedure | §11 | Chatham House Rule, Robert's Rules of Order, press attribution, IETF, sociocracy, Quaker practice, Open Space. |
profile.py:parse_duration).Each level keeps everything below it (SPEC §3).
| ℓ | SPEC id | SPEC: what may be kept | Goldfish name | Ring |
|---|---|---|---|---|
| 0 | off | Only that a gap occurred, and its length | Off the record | full red |
| 1 | outcomes | Decisions and commitments, without saying who argued what | Just the decision | ¼ orange |
| 2 | gist | A summary of the reasoning, unattributed | The gist | ½ yellow |
| 3 | attributed | Who said what, paraphrased | Name me | ¾ green |
| 4 | verbatim | The exact words | Quote me | full blue |
| 5 | audio | The recording itself | (unreachable in v01) | — |
At level 1 a commitment names its owner (SPEC §3).
| Symbol | Profile key | goldfish-v01 |
|---|---|---|
| δ₀ | default_level |
3 |
| D̂ | max_default_level |
4 |
| V̂ | max_voice_level |
4 |
| W | objection_window |
30 s |
| G | session_gap |
600 s |
| θ | attribution_threshold |
0.8 |
| N | max_participants |
10 |
| — | room_raise |
invocation |
| — | invocation |
goldfish |
| — | locales |
en, es |
| — | extensions |
side_conversations |
| — | reply_window |
6 s |
| — | replies_for_group |
2 |
| S | retention |
(3 d, 5), (30 d, 2), (365 d, 1) |
The default δ₀ = 3 is the operator's choice, not anyone's consent (SPEC §8). Where every party must consent, a profile should use 0 (backlog B-19).
session.py:49-68).session.py:38-46).session.py:28-35).Def 2.1 (session). A session runs from the first speech heard or filed until last + G (600 s of silence), or until the binding ends it (Goldfish: switching off). At a gap the end is dated last + G, not the moment the gap is noticed (session.py:75-82).
Def 2.2 (identity). id : labels → {v1, v2, …} is injective and assigned at first hearing. slot(x) is the number of identities assigned before x. Labels come from the binding (a diarisation cluster, a stream, a roster) and never appear in any output (SPEC §4.1).
Def 2.3 (capacity). A session has at most N identities. A label heard after that joins O: its applied level is 0 and nothing it says is filed. It may still lower the room (SPEC §6.4) or object (§5).
Inv 2.4 (fresh pseudonyms). An ID is never reused within one engine, carries nothing of its label, and is discarded when the session ends. The reference engines number IDs per engine run, not per session (D4 in §10): after a gap, the first voice of the next session gets the next number, not v1.
Inv 2.5 (unlinkability). No output relates an identity in one session to one in another. The session ID is 64 random bits (secrets.token_hex(8)). Voice features stay in RAM (SPEC §4.2; a binding duty, not yet built in v01). The claiming extension, which would let a voice claim its past records, is off in goldfish-v01.
Def 2.6 (anonymous withdrawal). forget(self, all) removes the voice's ID from the event log, so the log no longer shows that the voice spoke (session.py:230-232).
Def 3.1. An intent i = ⟨k, scope, ℓ, s, addr, src⟩ with kind k ∈ K = {lower, raise, forget, shorten, resume, object}, scope ∈ {self, room}, an optional level ℓ, an optional span or limit s in seconds, addr (whether the device's name was said) and src (speech, ui, preference, exchange). claim is an extension kind.
lower raise forget shorten resume object claim
Def 3.2 (parsing). normalise(text) lowercases, applies the locale's replacements, optionally folds accents, turns every run of characters other than letters, digits and apostrophes into one space, and pads with a space at each end. parse(text) is the intent of the first (locale, rule) pair that matches, trying locales in profile order. One utterance gives at most one intent. addr holds if an invocation word occurs anywhere in the utterance (grammar.py:51-107).
Inv 3.3 (protection order). Every rule of kind object, forget, lower or shorten comes before every rule of kind raise or resume, and grammar.py's lint checks it. So a sentence that both lowers and raises parses as a lowering (vectors 47, 57).
Table 3.4 (the English rules, in order; protocol/locales/en.json).
| Rule | Matches, for example | Intent |
|---|---|---|
objection |
the whole utterance is "no", "nope", "stop", "wait" or "I object", optionally after the name | object, room |
erase-me |
"erase everything I said", "erase me" | forget, self, all |
forget-what-i-said-span |
"forget what I said in the last five minutes" | forget, self, N |
forget-what-i-said |
"forget what I just said", "scratch that", "strike that" | forget, self, 120 s |
forget-span |
"delete the last ten minutes" | forget, room, N |
leave-me-out |
"leave me out", "don't record me", "count me out" | lower, self, 0 |
off-the-record |
"off the record", "between us", "stop recording" | lower, room, 0 |
my-words-outcomes |
"keep my words to just the decisions" | lower, self, 1 |
outcomes-only |
"just the decisions", "only action items" | lower, room, 1 |
my-words-gist |
"keep my words to the gist" | lower, self, 2 |
gist |
"high level", "no names", "don't attribute", "the gist" | lower, room, 2 |
dont-quote-me |
"don't quote me" | lower, self, 3 |
no-verbatim |
"no quotes", "not word for word" | lower, room, 3 |
keep-for |
"keep this for a week" | shorten, room, N |
quote-me |
"you can quote me", "keep my words verbatim" | raise, self, 4 |
back-on-record |
"back on the record", "we're back on" | resume, room |
word-for-word |
"Goldfish, word for word" (counts only with the name) | raise, room, 4 |
remember-me |
"remember me" | claim, self (extension; off in v01) |
The eye as intents (Goldfish binding, device.py:62-81). A press while the room is on gives lower(room, 0, src = ui). A press while the room is off does nothing ("still off"), unless it comes within 0.6 s of a press that found the room off, in which case it gives resume(room, src = ui). Holding the eye for 2 s shows the battery. Eye intents are addressed and carry confidence 1.
Def 3.5 (sure). sure(x, q) ⇔ x is known ∧ x ∉ O ∧ q ≥ θ, where q is the binding's confidence that x said it.
Def 3.6 (uncertainty falls toward less). Before an intent applies:
unsure-speaker);Every intent ends as applied, provisional, reverted, or ignored with a reason: unsupported, unsure-speaker, no-slot, nothing-to-undo, not-lowered, room-raise-disabled, not-addressed or not-a-raise.
Def 4.1.
base(x) = r(x) if r(x) ≠ ⊥, else δ
A(x) = 0 if x ∈ O
= base(x) ∧ c ∧ κ(x) otherwise
A(*) = δ ∧ c (a voice that has set nothing)
This is Session.applied (session.py:135-141), exactly.
Table 4.2 (transitions, after Def 3.6).
| Intent | Guard | Effect | Outcome |
|---|---|---|---|
| self lower ℓ | x has a slot | r(x) := (r(x) or δ) ∧ ℓ | applied |
| self raise ℓ | sure | r(x) := (r(x) or δ) ∨ (ℓ ∧ V̂), at once | applied |
| self resume | sure | r(x) := ⊥ | applied |
| self forget s | — | F := F ∪ {(t − s, t, id(x))} | applied |
| self shorten s | — | λ(x) := λ(x) ∧ s | applied |
| room lower ℓ | — | undo any open window; then κ(y) := κ(y) ∧ ℓ for y in the speaker's group if the session is split and the speaker sure, else c := c ∧ ℓ | applied |
| room forget s | — | F := F ∪ {(t − s, t, everyone)} | applied |
| room shorten s | — | ρ := ρ ∧ s | applied |
| object | a window is open | undo (§5) | reverted |
| room resume | something is lowered; src ≠ speech or sure | open or restart the window; lift caps in scope, or c := ⊤ if there are no groups | provisional |
| room raise ℓ | raises enabled; addressed; src ≠ speech or sure; ℓ ∧ D̂ > δ | open or restart the window; δ := ℓ ∧ D̂ | provisional |
Thm 4.3 (only minima, plus one maximum on the diagonal). Every transition except self raise, room raise and room resume can only lower: no applied level, stamped level or limit rises. Self raise changes only the speaker's own request. Room raise changes only δ, so only voices with r = ⊥, and it is provisional. Resume lifts c and caps, and is provisional. Hence SPEC §6.4: nobody raises anyone else, except provisionally, by the room, under a veto.
For n voices, write each voice's constraints as a row and take the minimum across it:
C[x] = ( base(x), c, κ(x), ω(x) ) ω(x) = 0 if x ∈ O, else ⊤
A(x) = min_j C[x][j]
In the (min, max) semiring this is A = C ⊗ 𝟙. The ceiling c is one column shared by every row; κ is constant within a group; base is the row's own.
Who may move whose level is a pair of n × n matrices, read "row voice may change column voice":
Lower = J_n every voice may lower every voice (via the room)
Raise = I_n a voice may raise only itself, at once, to at most V̂
Veto = 𝟙_n any voice, sure or not, may undo a provisional room raise
RoomRaise a provisional rank-one update δ := ℓ ∧ D̂ on rows with r = ⊥
With side conversations on, "lower" by a sure speaker in a split session reaches only the speaker's group: Lower becomes block diagonal over the groups.
side_conversations)R[x][y] += 1 when y speaks within 6 s of x's last turn, x ≠ y
B = [R ≥ 2] groups = connected components of B
split ⇔ at least two components have two or more members
A round-robin meeting can split falsely (backlog B-62).
At t = 95.3 s in the roadmap session (§8.4): r = (Alice 1, Bob ⊥, Charlie ⊥, Dana 2, Eve 4), δ = 3, c = ⊤, κ = ⊤. So A = (1, 3, 3, 2, 4).
Def 5.1. An open window π = ⟨since, opened, δ⁻, c⁻, κ⁻⟩ remembers when it began and the state before it.
Def 5.2. before(x) = (r(x) or δ⁻) ∧ c⁻ ∧ κ⁻(x).
Normal ──room raise / resume [guards]──▶ Provisional(since = opened = t)
Provisional ──raise / resume──▶ Provisional(opened := t; since and before-state kept)
Provisional ──object | room lower | end | gap──▶ Normal [undo]
Provisional ──t > opened + W──▶ Normal [confirm, dated opened + W]
At a gap, the window is confirmed if it would have closed before the session ended, else undone.
Inv 5.3 (undo reaches back). On undo, every segment u with t₁ ≥ since gets s(u) := s(u) ∧ before(voice), and the level history is rewritten, so speech filed later also gets the earlier level (vector 44).
Inv 5.4 (hold). While a window is open, derived artefacts use h(u), the level before the raise (vector 13).
Inv 5.5. An objection at exactly t = opened + W still counts: confirmation needs t − opened > W, strictly (vector 58).
Inv 5.6. A second raise restarts the clock and keeps the original before-state (vector 17).
Indication. The ring shows an amber arc that shrinks as 1 − (t − opened)/W.
Def 6.1 (filing). When the binding files speech [t₀, t₁] for voice x:
s(u) = A(x, now) ∧ A_T(x, t₀) ∧ ⋀{ A_T(x, τ) : τ ∈ T, t₀ < τ ≤ t₁ }
s(u) = 0 if some (a, b, y) ∈ F has y ∈ {everyone, id(x)} and t₀ ≤ b and a ≤ t₁
h(u) = s(u) ∧ before(x) if a window is open and t₁ ≥ since
refused if x ∈ O, or t₀ is before this session's start (or the previous session's end)
A_T(x, τ) is x's level in the timeline at τ. A segment is stamped at the lowest level that applied at any point while it was spoken (Session.stamp).
G6.2 (Goldfish binding). The line that carries an intent is filed first, then lowered to x's level after the intent (daemon.py:120-124). So "leave me out" also leaves out the sentence that says it, while "you can quote me" keeps that sentence at the earlier level, and "back on the record" is itself filed off the record.
Def 6.3 (forget). F := F ∪ {(t − s, t, who)}. Every segment overlapping the span gets s := 0 and h := ⊥, and so does speech filed later that overlaps it (vector 27). Spans never cross a session boundary.
Def 7.1. A schedule S = ((u₁, m₁), …, (u_k, m_k)) has increasing ages u and decreasing levels m (profile.py).
Def 7.2. L_S(a) = m_j for the least j with a < u_j, and 0 if a ≥ u_k. For goldfish-v01:
L(a) = 3·H(3 d − a) + H(30 d − a) + H(365 d − a) H(z) = 1 if z > 0, else 0
= 5 for a < 3 d, 2 for a < 30 d, 1 for a < 365 d, 0 after
Def 7.3 (limit). λ*(x) = ρ ∧ λ(x), or ⊥ if neither is set. Limits only shrink (vectors 31, 32).
Def 7.4 (readable level). For a segment u of voice x at time t, with age a = t − t₁(u):
X_u(t) = 0 if λ*(x) ≠ ⊥ and a ≥ λ*(x)
= s(u) ∧ h(u) ∧ L(a) otherwise
This is Session.readable with retention.readable_level. Age counts from the end of the segment. Boundaries are exclusive: at exactly 3 d, the level is already 2.
Table 7.5 (readable level by stamped level and age, no limit).
| Stamped | < 3 d | 3 d to < 30 d | 30 d to < 365 d | ≥ 365 d |
|---|---|---|---|---|
| 5 | 5 | 2 | 1 | 0 |
| 4 | 4 | 2 | 1 | 0 |
| 3 | 3 | 2 | 1 | 0 |
| 2 | 2 | 2 | 1 | 0 |
| 1 | 1 | 1 | 1 | 0 |
| 0 | 0 | 0 | 0 | 0 |
Def 7.6. unreadable_after(ℓ) = max{u_j : m_j ≥ ℓ} ∧ λ*: the age after which content kept at ℓ must be unrecoverable.
| Level | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|
| Unrecoverable after | 3 d | 3 d | 3 d | 30 d | 365 d | 365 d |
Note 7.7 (why a staircase, not a curve). On a logarithmic age axis the steps sit at log₁₀ 3 ≈ 0.48, log₁₀ 30 ≈ 1.48 and log₁₀ 365 ≈ 2.56 (in days): about one decade apart. Each finer level of detail lasts about a tenth as long as the one below it. A smooth forgetting curve would make the readable level uncertain at any age. The staircase makes it exact, which is why the note can print a date on every section.
7.8 The store (Goldfish binding; designed and tested in software, not yet wired into the daemon).
copies at r ∈ {1, 2, 3} with r ≤ s(u); class k ∈ {1d, 7d, 30d, keep} store.py:35-48
keep(r, k) = min((3, 30, 365)[r] days, days(k)) keys.py:93-102
at the UTC midnight that starts day D′, each chain forgets every day ≤ D′ − keep
⇒ a copy's lifetime lies in (keep − 1 d, keep]: it can be forgotten early, never late
Forgetting is by destroying keys, not by deleting files. Seeds are hash-chained, so a destroyed day key cannot be recomputed from a later one. v01 stores nothing above level 3: level 4 exists in the engine and on the ring, but the note keeps a paraphrase (D6).
7.9 The event log. SPEC §9 says the log must not outlive the last step of the schedule. In v01 the log lives in RAM only and is not pruned while the device stays on (D4).
8.1 End of session (SPEC §6.7). An open raise is undone. δ returns to δ₀. c, κ, ρ, F and the reply graph are cleared. Identities are discarded and a new session ID is drawn. Records already filed keep their stamped levels.
8.2 Derived artefacts. An artefact at level ℓ may use only {u : X_u(t) ≥ ℓ}. So each voice's contribution is bounded by its own level: a summary at level 2 cannot use the words of a voice at 1.
8.3 The Goldfish note. Written once to the NFC tag when the session ends. Three sections, Outcomes (level 1), Gist (2) and Attributed (3), each with a "kept until" date of start + unreadable_after(ℓ). The front matter gives a count of voices, never their IDs. If the note is too big for the 8 KB tag, the most detailed section is dropped first, and the note says so. A note never contains audio, per-statement times, voice features, or anything above a voice's level (v01/note-format.md).
8.4 Worked replay: the roadmap session. The site's script (v01/firmware/scenarios/meeting.json) run through the real daemon on the simulated board. The levels, filings and dates are the firmware's. The paraphrases in the note are hand-written stand-ins for the summariser, which is not built.
| t (s) | Event | Effect |
|---|---|---|
| 4–16.5 | Alice, Bob, Charlie speak | filed at 3 |
| 16.8 | eye press | room off (c = 0) |
| 17.2–24.1 | Dana: "Keep my words to the gist for this part…" | r(Dana) = 2; the line itself is filed at 0 |
| 31.0 | double press | resume, provisional until 61.0 |
| 51.4–57.4 | Eve: "You can quote me on this one." | r(Eve) = 4; the line stays at 3 |
| 61.0 | window closes | confirmed |
| 62.6 | hold | room off, battery shown |
| 67.6–71.2 | Bob: "Between us, …" | filed at 0 |
| 71.9–77.1 | Charlie: "Okay, back on the record. …" | provisional resume until 107.1; the line is filed at 0 |
| 77.8–82.4 | Eve | filed at 4, held at 0 |
| 82.7 | press inside the window | resume undone; Eve's line restamped 0; room off |
| 88.6 | double press | resume, provisional until 118.6 |
| 88.7–95.3 | Alice: "Keep my words to just the decisions from here…" | r(Alice) = 1 |
| 111.7–117.3 | Dana: "Please leave me out of this last bit…" | r(Dana) = 0 |
| 118.6 | window closes | confirmed |
| 717.3 | 600 s of silence | session ends; the note goes to the tag |
The note: Outcomes 6 lines, kept until 2027-10-14; Gist 11 lines, until 2026-11-13; Attributed 10 lines, until 2026-10-17 (the simulator's clock starts 2026-10-14 00:00 UTC). Bob's aside is absent. Dana never appears in Attributed. Charlie's proposal, said while the room was off, is lost, because the resume in that same sentence was later undone.
Vector numbers are case indexes in protocol/vectors/sessions.json.
| Invariant | Checked by |
|---|---|
| I1 Lowering is immediate, needs no name, and only lowers | vectors 0, 1, 2, 5, 47, 57; test_device |
| I2 Self intents touch only the speaker | vectors 3, 4, 6, 7 |
| I3 Nobody raises anyone else | vectors 21, 56; test_device |
| I4 Uncertainty falls toward less | vectors 22, 23, 24, 54, 55 |
| I5 A room raise needs the name, is capped, can be disabled | vectors 8, 10, 11, 12 |
| I6 Raises are provisional and undone retroactively | vectors 9, 13–20, 44, 58 |
| I7 Filing takes the lowest level over the span | vectors 29, 30, 45, 46 |
| I8 Forgetting reaches later filings and stays in its session | vectors 25–28, 48, 51 |
| I9 Retention | vectors 31, 32; retention.json; test_scenario, test_keys |
| I10 Identity and session | vectors 33, 34, 35 |
| I11 Artefacts are bounded per voice | vectors 13, 36; test_device |
| I12 Exchange and standing preferences (unused in v01, which has no radio) | vectors 37, 38, 49, 50, 52, 53 |
Both reference engines (Python and JS) run every vector, and protocol/tools/fuzz_parity.py compares them on random sessions.
| ID | What | Where | Backlog |
|---|---|---|---|
| D1 | Fail closed keeps level 1. SPEC §5.3 says to act "as if the longest step has passed", which would keep nothing. | keys.py:104-108; ADR-0005 |
B-91 |
| D2 | The note's "kept until" dates ignore spoken limits. After "Keep this for a week." the note still prints 1 year, 30 days and 3 days. | daemon.py:52 |
B-92 |
| D3 | A limit shorter than one day snaps up to the 1-day key class, so it is kept longer than asked. | keys.py:46-53 |
B-93 |
| D4 | The voice-ID counter, the event counter and the event log survive the end of a session. | session.py, resolution.js |
B-94 |
| D5 | Gap markers (level 0) are never pruned; the schedule implies 365 d. | store.py:36-38 |
B-95 |
| D6 | v01 keeps no level-4 content although the ring shows blue; the site's JS stand-in does quote. | store.py:40; goldfish.js |
B-100 |
| D7 | The store is not wired into the daemon, and the secure-element driver is not written. | 01-concept/landing/privacy-review.md; firmware README |
B-99 |
| D8 | The store counts in UTC days, the engine in continuous age. The store forgets up to 1 day early, never late. | keys.py vs session.py |
informative |
| D9 | SPEC §4.3 lets a voice shorten its own record, but no English or Spanish phrase does. | en.json |
B-97 |
Human ground rules control two things separately: how much of the content survives, and how much of the identity. RP's levels are a single chain through that plane:
Content = ∅ < outcomes < gist < paraphrase < words < audio
Identity = none < role < name
φ(0) = (∅, none) φ(3) = (paraphrase, name)
φ(1) = (outcomes, none*) φ(4) = (words, name)
φ(2) = (gist, none) φ(5) = (audio, name)
* a commitment names its owner
level(H) = max{ ℓ : φ(ℓ) ≤ (content_H, identity_H) } (componentwise)
loss(H) = what H allows that φ(level(H)) does not
The map is safe: a recorder following level(H) never keeps more than the human rule H allows. Whatever H allows off the chain is lost.
| Practice | Goldfish counterpart | Main difference |
|---|---|---|
| Chatham House Rule | room level 2, The gist ("no names", "don't attribute"); the note shows only a count of voices | The Rule lets you use exact words without a name; the chain has no such point, so Goldfish keeps less (B-96). The Rule binds people's later conduct; RP binds the recorder. Each voice can go lower than the room. |
| Chatham "off the record" | level 0; the eye press | The gap and its length are kept. The device keeps listening for "back on the record". |
| Robert's minutes: "what was done and not what was said" | level 1, Just the decision | Robert's minutes name the mover and roll-call voters; level 1 names only a commitment's owner. Robert's minutes are approved by the assembly; Goldfish reads nothing back (B-98). |
| Robert's general consent | a provisional raise and the 30 s window | After an objection, Robert's moves to a motion and a vote. RP simply undoes, with no vote and no reason. Only raises need consent; lowering needs nobody's. |
| Robert's objection to consideration | the objection window | Robert's needs a two-thirds vote; RP needs one voice. |
| Robert's expunge | forget |
Robert's keeps the expunged words readable, to audit the order. RP keeps the span and its length, not the words. Robert's needs a majority of the whole membership; RP lets one voice forget its own words. |
| Robert's decorum: "the member who spoke last" | session pseudonyms v1, v2, … | A courtesy in debate there; what the record stores here. |
| Robert's committees | side_conversations group caps |
A partial analogue: groups are inferred from reply timing. |
| AP "on the record" | level 4, Quote me | v01's note keeps a paraphrase (D6). |
| AP "background" | no exact level; 2 is the floor | Neither the role description nor the words survive. |
| AP "deep background" | level 2 | The same loss as Chatham. |
| AP "off the record" | level 0 | — |
| AP: ground rules set at the start | a fixed grammar with test vectors; the ring shows the result | In RP anyone can set their own level at any time, alone, without negotiating. |
| IETF Note Well | the opposite of SPEC §8 | IETF participation implies agreement to recording; in RP a default above 0 is never consent. |
| IETF rough consensus (RFC 7282) | a provisional raise ("lack of disagreement") | RP always gives way to an objection against a raise. |
| Sociocratic consent | the window: "Do you object?" | An RP objection needs no reason and is never tested. |
| Quaker minute | none | The note is written at the end with no read-back (B-98). |
| Open Space: "When it is over it is over." | end of session (§8.1) | Levels, identities and forget spans do not cross it. |
Each quote was checked against the source on 2026-10-19. Quotes marked verify were read from a copy, not the official page.
Chatham House Rule. "When a meeting, or part thereof, is held under the Chatham House Rule, participants are free to use the information received, but neither the identity nor the affiliation of the speaker(s), nor that of any other participant, may be revealed." From the FAQ: "In cases where the Rule is not considered sufficiently strict, an event may be held 'off the record'." (https://www.chathamhouse.org/about-us/chatham-house-rule; the site refuses scripts, so verify in a browser. The same text is in v01/research/market.md.)
Robert's Rules of Order, 1876 (Project Gutenberg #9097, public domain):
Robert's Rules of Order Revised, 1915 (Project Gutenberg #75990, public domain):
The current edition (RONR, 12th ed., 2020) was not consulted and is not quoted.
AP, News Values and Principles (https://www.ap.org/about/news-values-and-principles/telling-the-story/): "Not everyone understands “off the record” or “on background” to mean the same things. Before any interview in which any degree of anonymity is expected, there should be a discussion in which the ground rules are set explicitly." And: "Deep background. The information can be used but without attribution. The source does not want to be identified in any way, even on condition of anonymity."
IETF. Note Well (https://www.ietf.org/about/note-well/): "The IETF routinely makes public written, audio, video, and photographic records of IETF activities, including your personal information as set out in the IETF Privacy Statement." RFC 7282 §2 is titled "Lack of disagreement is more important than agreement".
Sociocracy For All (https://www.sociocracyforall.org/consent-decision-making/): "But instead of asking everyone, “Do you agree?”, we ask, “Do you object?” If no one objects, there is consent."
Quaker faith & practice, chapter 3 (https://qfp.quaker.org.uk/chapter/3/), 3.14: "Remember that any minute you present is only a draft minute until the meeting has accepted it as its own."
Open Space (Harrison Owen's principles, as given at https://co-intelligence.org/P-Openspace.html): "When it is over it is over."
# Resolution Protocol: the memory model — version 0.1, draft
- **Status:** working draft, 2026-10-19. An informative companion to [SPEC.md](SPEC.md) (RP 0.2). It restates SPEC §3–§6 and §9 as one formal model, using the Goldfish v01 profile as the worked example. It adds no requirements. Where this document and SPEC differ, SPEC governs; §10 lists where the code differs from both.
- **Editors:** Rafael David; drafted with Claude (machine-generated text, checked against the code and the vectors).
- **Profile used for the numbers:** `goldfish-v01` ([v01/profile/goldfish.profile.json](../v01/profile/goldfish.profile.json)).
- **Kept honest by:** `v01/firmware/tests/test_memory_model_doc.py`. It reads the marked tables in this file and compares them with the profile, the retention engine, the grammar and the figure. If the code changes, the test fails until this file is updated.
- **Figure:** [figures/memory-model.svg](figures/memory-model.svg), an animated pencil drawing in nine scenes, generated by `v01/firmware/tools/memory_figure.py`.
Capitalised key words (MUST, SHOULD) appear only inside quotes from SPEC.
## 0. Scope
"Memory" here means four things: what may be held about what was said, at which level, for how long, and who may change that. The request that started this document asked for "decay curvatures, voice IDs per session, and intent". They land as follows:
| Asked for | Where | In one line |
|---|---|---|
| Decay | §7 | A staircase, not a curve: the readable level falls 5 → 2 → 1 → 0 at 3 days, 30 days and 1 year, cut short by any spoken limit. |
| Voice IDs per session | §2 | Pseudonyms v1, v2, … in order of first speech, never carrying a name, discarded when the session ends. |
| Intent | §3 | Six kinds, parsed by ordered phrase rules and the eye button. |
| How they combine | §4–§6 | Every rule is a minimum, except a voice raising its own words. Raising the room is provisional for 30 s. |
| End of session | §8 | Identities and levels are discarded; the note is written. |
| Meeting procedure | §11 | Chatham House Rule, Robert's Rules of Order, press attribution, IETF, sociocracy, Quaker practice, Open Space. |
## 1. Notation
- Levels 𝕃 = {0, 1, 2, 3, 4, 5}, totally ordered. x ∧ y = min(x, y), x ∨ y = max(x, y). ⊤ = 5. ⊥ = not set.
- Durations are in seconds. 1 d = 86 400 s; in profiles, Y = 365 d and M = 30 d (`profile.py:parse_duration`).
- σ is the state of one session; x, y are voices; u is a segment (a span of one voice's speech); t is the time now.
### 1.1 Levels
Each level keeps everything below it (SPEC §3).
| ℓ | SPEC id | SPEC: what may be kept | Goldfish name | Ring |
|---|---|---|---|---|
| 0 | off | Only that a gap occurred, and its length | Off the record | full red |
| 1 | outcomes | Decisions and commitments, without saying who argued what | Just the decision | ¼ orange |
| 2 | gist | A summary of the reasoning, unattributed | The gist | ½ yellow |
| 3 | attributed | Who said what, paraphrased | Name me | ¾ green |
| 4 | verbatim | The exact words | Quote me | full blue |
| 5 | audio | The recording itself | (unreachable in v01) | — |
At level 1 a commitment names its owner (SPEC §3).
### 1.2 Profile parameters
<!-- profile-table -->
| Symbol | Profile key | goldfish-v01 |
|---|---|---|
| δ₀ | `default_level` | 3 |
| D̂ | `max_default_level` | 4 |
| V̂ | `max_voice_level` | 4 |
| W | `objection_window` | 30 s |
| G | `session_gap` | 600 s |
| θ | `attribution_threshold` | 0.8 |
| N | `max_participants` | 10 |
| — | `room_raise` | invocation |
| — | `invocation` | goldfish |
| — | `locales` | en, es |
| — | `extensions` | side_conversations |
| — | `reply_window` | 6 s |
| — | `replies_for_group` | 2 |
| S | `retention` | (3 d, 5), (30 d, 2), (365 d, 1) |
<!-- /profile-table -->
The default δ₀ = 3 is the operator's choice, not anyone's consent (SPEC §8). Where every party must consent, a profile should use 0 (backlog B-19).
### 1.3 State
- **Session** σ = ⟨δ, c, ρ, π, V, O, F, T, R, last, start, closed, E⟩: the default δ, the room ceiling c, the room limit ρ, the open objection window π, the voices V, the over-cap labels O, the forgotten spans F, the level timeline T, the reply counts R, the last time anyone was heard, the start, the end of the previous session, and the event log E (`session.py:49-68`).
- **Voice** x = ⟨id, label, slot, r, κ, λ, record⟩: its pseudonym, the binding's label (never output), its order of first speech, its own request r, its group cap κ, its own limit λ, and its segments (`session.py:38-46`).
- **Segment** u = ⟨voice, t₀, t₁, s, h⟩: whose speech, from when to when, the stamped level s, and the held level h while a raise is provisional (`session.py:28-35`).
## 2. Sessions and identities
**Def 2.1 (session).** A session runs from the first speech heard or filed until last + G (600 s of silence), or until the binding ends it (Goldfish: switching off). At a gap the end is dated last + G, not the moment the gap is noticed (`session.py:75-82`).
**Def 2.2 (identity).** id : labels → {v1, v2, …} is injective and assigned at first hearing. slot(x) is the number of identities assigned before x. Labels come from the binding (a diarisation cluster, a stream, a roster) and never appear in any output (SPEC §4.1).
**Def 2.3 (capacity).** A session has at most N identities. A label heard after that joins O: its applied level is 0 and nothing it says is filed. It may still lower the room (SPEC §6.4) or object (§5).
**Inv 2.4 (fresh pseudonyms).** An ID is never reused within one engine, carries nothing of its label, and is discarded when the session ends. The reference engines number IDs per engine run, not per session (D4 in §10): after a gap, the first voice of the next session gets the next number, not v1.
**Inv 2.5 (unlinkability).** No output relates an identity in one session to one in another. The session ID is 64 random bits (`secrets.token_hex(8)`). Voice features stay in RAM (SPEC §4.2; a binding duty, not yet built in v01). The `claiming` extension, which would let a voice claim its past records, is off in goldfish-v01.
**Def 2.6 (anonymous withdrawal).** forget(self, all) removes the voice's ID from the event log, so the log no longer shows that the voice spoke (`session.py:230-232`).
## 3. Intents
**Def 3.1.** An intent i = ⟨k, scope, ℓ, s, addr, src⟩ with kind k ∈ K = {lower, raise, forget, shorten, resume, object}, scope ∈ {self, room}, an optional level ℓ, an optional span or limit s in seconds, addr (whether the device's name was said) and src (speech, ui, preference, exchange). `claim` is an extension kind.
<!-- intent-kinds -->
`lower` `raise` `forget` `shorten` `resume` `object` `claim`
<!-- /intent-kinds -->
**Def 3.2 (parsing).** normalise(text) lowercases, applies the locale's replacements, optionally folds accents, turns every run of characters other than letters, digits and apostrophes into one space, and pads with a space at each end. parse(text) is the intent of the first (locale, rule) pair that matches, trying locales in profile order. One utterance gives at most one intent. addr holds if an invocation word occurs anywhere in the utterance (`grammar.py:51-107`).
**Inv 3.3 (protection order).** Every rule of kind object, forget, lower or shorten comes before every rule of kind raise or resume, and `grammar.py`'s lint checks it. So a sentence that both lowers and raises parses as a lowering (vectors 47, 57).
**Table 3.4 (the English rules, in order; `protocol/locales/en.json`).**
<!-- en-rules -->
| Rule | Matches, for example | Intent |
|---|---|---|
| `objection` | the whole utterance is "no", "nope", "stop", "wait" or "I object", optionally after the name | object, room |
| `erase-me` | "erase everything I said", "erase me" | forget, self, all |
| `forget-what-i-said-span` | "forget what I said in the last five minutes" | forget, self, N |
| `forget-what-i-said` | "forget what I just said", "scratch that", "strike that" | forget, self, 120 s |
| `forget-span` | "delete the last ten minutes" | forget, room, N |
| `leave-me-out` | "leave me out", "don't record me", "count me out" | lower, self, 0 |
| `off-the-record` | "off the record", "between us", "stop recording" | lower, room, 0 |
| `my-words-outcomes` | "keep my words to just the decisions" | lower, self, 1 |
| `outcomes-only` | "just the decisions", "only action items" | lower, room, 1 |
| `my-words-gist` | "keep my words to the gist" | lower, self, 2 |
| `gist` | "high level", "no names", "don't attribute", "the gist" | lower, room, 2 |
| `dont-quote-me` | "don't quote me" | lower, self, 3 |
| `no-verbatim` | "no quotes", "not word for word" | lower, room, 3 |
| `keep-for` | "keep this for a week" | shorten, room, N |
| `quote-me` | "you can quote me", "keep my words verbatim" | raise, self, 4 |
| `back-on-record` | "back on the record", "we're back on" | resume, room |
| `word-for-word` | "Goldfish, word for word" (counts only with the name) | raise, room, 4 |
| `remember-me` | "remember me" | claim, self (extension; off in v01) |
<!-- /en-rules -->
**The eye as intents (Goldfish binding, `device.py:62-81`).** A press while the room is on gives lower(room, 0, src = ui). A press while the room is off does nothing ("still off"), unless it comes within 0.6 s of a press that found the room off, in which case it gives resume(room, src = ui). Holding the eye for 2 s shows the battery. Eye intents are addressed and carry confidence 1.
**Def 3.5 (sure).** sure(x, q) ⇔ x is known ∧ x ∉ O ∧ q ≥ θ, where q is the binding's confidence that x said it.
**Def 3.6 (uncertainty falls toward less).** Before an intent applies:
- a self-scoped lower, forget or shorten from a speaker who is not sure becomes room-scoped, so it still protects;
- a self-scoped raise or resume from a speaker who is not sure is ignored (`unsure-speaker`);
- a room raise or resume from *speech* by a speaker who is not sure is ignored;
- object and room lowerings need no attribution at all.
Every intent ends as applied, provisional, reverted, or ignored with a reason: unsupported, unsure-speaker, no-slot, nothing-to-undo, not-lowered, room-raise-disabled, not-addressed or not-a-raise.
## 4. The applied level
**Def 4.1.**
```
base(x) = r(x) if r(x) ≠ ⊥, else δ
A(x) = 0 if x ∈ O
= base(x) ∧ c ∧ κ(x) otherwise
A(*) = δ ∧ c (a voice that has set nothing)
```
This is `Session.applied` (`session.py:135-141`), exactly.
**Table 4.2 (transitions, after Def 3.6).**
| Intent | Guard | Effect | Outcome |
|---|---|---|---|
| self lower ℓ | x has a slot | r(x) := (r(x) or δ) ∧ ℓ | applied |
| self raise ℓ | sure | r(x) := (r(x) or δ) ∨ (ℓ ∧ V̂), at once | applied |
| self resume | sure | r(x) := ⊥ | applied |
| self forget s | — | F := F ∪ {(t − s, t, id(x))} | applied |
| self shorten s | — | λ(x) := λ(x) ∧ s | applied |
| room lower ℓ | — | undo any open window; then κ(y) := κ(y) ∧ ℓ for y in the speaker's group if the session is split and the speaker sure, else c := c ∧ ℓ | applied |
| room forget s | — | F := F ∪ {(t − s, t, everyone)} | applied |
| room shorten s | — | ρ := ρ ∧ s | applied |
| object | a window is open | undo (§5) | reverted |
| room resume | something is lowered; src ≠ speech or sure | open or restart the window; lift caps in scope, or c := ⊤ if there are no groups | provisional |
| room raise ℓ | raises enabled; addressed; src ≠ speech or sure; ℓ ∧ D̂ > δ | open or restart the window; δ := ℓ ∧ D̂ | provisional |
**Thm 4.3 (only minima, plus one maximum on the diagonal).** Every transition except self raise, room raise and room resume can only lower: no applied level, stamped level or limit rises. Self raise changes only the speaker's own request. Room raise changes only δ, so only voices with r = ⊥, and it is provisional. Resume lifts c and caps, and is provisional. Hence SPEC §6.4: nobody raises anyone else, except provisionally, by the room, under a veto.
### 4.4 The relationship matrices
For n voices, write each voice's constraints as a row and take the minimum across it:
```
C[x] = ( base(x), c, κ(x), ω(x) ) ω(x) = 0 if x ∈ O, else ⊤
A(x) = min_j C[x][j]
```
In the (min, max) semiring this is A = C ⊗ 𝟙. The ceiling c is one column shared by every row; κ is constant within a group; base is the row's own.
Who may move whose level is a pair of n × n matrices, read "row voice may change column voice":
```
Lower = J_n every voice may lower every voice (via the room)
Raise = I_n a voice may raise only itself, at once, to at most V̂
Veto = 𝟙_n any voice, sure or not, may undo a provisional room raise
RoomRaise a provisional rank-one update δ := ℓ ∧ D̂ on rows with r = ⊥
```
With side conversations on, "lower" by a sure speaker in a split session reaches only the speaker's group: Lower becomes block diagonal over the groups.
### 4.5 The reply graph (extension `side_conversations`)
```
R[x][y] += 1 when y speaks within 6 s of x's last turn, x ≠ y
B = [R ≥ 2] groups = connected components of B
split ⇔ at least two components have two or more members
```
A round-robin meeting can split falsely (backlog B-62).
### 4.6 Worked example
At t = 95.3 s in the roadmap session (§8.4): r = (Alice 1, Bob ⊥, Charlie ⊥, Dana 2, Eve 4), δ = 3, c = ⊤, κ = ⊤. So A = (1, 3, 3, 2, 4).
## 5. Provisional raises and the objection window
**Def 5.1.** An open window π = ⟨since, opened, δ⁻, c⁻, κ⁻⟩ remembers when it began and the state before it.
**Def 5.2.** before(x) = (r(x) or δ⁻) ∧ c⁻ ∧ κ⁻(x).
```
Normal ──room raise / resume [guards]──▶ Provisional(since = opened = t)
Provisional ──raise / resume──▶ Provisional(opened := t; since and before-state kept)
Provisional ──object | room lower | end | gap──▶ Normal [undo]
Provisional ──t > opened + W──▶ Normal [confirm, dated opened + W]
```
At a gap, the window is confirmed if it would have closed before the session ended, else undone.
**Inv 5.3 (undo reaches back).** On undo, every segment u with t₁ ≥ since gets s(u) := s(u) ∧ before(voice), and the level history is rewritten, so speech filed later also gets the earlier level (vector 44).
**Inv 5.4 (hold).** While a window is open, derived artefacts use h(u), the level before the raise (vector 13).
**Inv 5.5.** An objection at exactly t = opened + W still counts: confirmation needs t − opened > W, strictly (vector 58).
**Inv 5.6.** A second raise restarts the clock and keeps the original before-state (vector 17).
**Indication.** The ring shows an amber arc that shrinks as 1 − (t − opened)/W.
## 6. Records
**Def 6.1 (filing).** When the binding files speech [t₀, t₁] for voice x:
```
s(u) = A(x, now) ∧ A_T(x, t₀) ∧ ⋀{ A_T(x, τ) : τ ∈ T, t₀ < τ ≤ t₁ }
s(u) = 0 if some (a, b, y) ∈ F has y ∈ {everyone, id(x)} and t₀ ≤ b and a ≤ t₁
h(u) = s(u) ∧ before(x) if a window is open and t₁ ≥ since
refused if x ∈ O, or t₀ is before this session's start (or the previous session's end)
```
A_T(x, τ) is x's level in the timeline at τ. A segment is stamped at the lowest level that applied at any point while it was spoken (`Session.stamp`).
**G6.2 (Goldfish binding).** The line that carries an intent is filed first, then lowered to x's level after the intent (`daemon.py:120-124`). So "leave me out" also leaves out the sentence that says it, while "you can quote me" keeps that sentence at the earlier level, and "back on the record" is itself filed off the record.
**Def 6.3 (forget).** F := F ∪ {(t − s, t, who)}. Every segment overlapping the span gets s := 0 and h := ⊥, and so does speech filed later that overlaps it (vector 27). Spans never cross a session boundary.
## 7. Forgetting: the staircase
**Def 7.1.** A schedule S = ((u₁, m₁), …, (u_k, m_k)) has increasing ages u and decreasing levels m (`profile.py`).
**Def 7.2.** L_S(a) = m_j for the least j with a < u_j, and 0 if a ≥ u_k. For goldfish-v01:
```
L(a) = 3·H(3 d − a) + H(30 d − a) + H(365 d − a) H(z) = 1 if z > 0, else 0
= 5 for a < 3 d, 2 for a < 30 d, 1 for a < 365 d, 0 after
```
**Def 7.3 (limit).** λ*(x) = ρ ∧ λ(x), or ⊥ if neither is set. Limits only shrink (vectors 31, 32).
**Def 7.4 (readable level).** For a segment u of voice x at time t, with age a = t − t₁(u):
```
X_u(t) = 0 if λ*(x) ≠ ⊥ and a ≥ λ*(x)
= s(u) ∧ h(u) ∧ L(a) otherwise
```
This is `Session.readable` with `retention.readable_level`. Age counts from the end of the segment. Boundaries are exclusive: at exactly 3 d, the level is already 2.
**Table 7.5 (readable level by stamped level and age, no limit).**
<!-- readable-table -->
| Stamped | < 3 d | 3 d to < 30 d | 30 d to < 365 d | ≥ 365 d |
|---|---|---|---|---|
| 5 | 5 | 2 | 1 | 0 |
| 4 | 4 | 2 | 1 | 0 |
| 3 | 3 | 2 | 1 | 0 |
| 2 | 2 | 2 | 1 | 0 |
| 1 | 1 | 1 | 1 | 0 |
| 0 | 0 | 0 | 0 | 0 |
<!-- /readable-table -->
**Def 7.6.** unreadable_after(ℓ) = max{u_j : m_j ≥ ℓ} ∧ λ*: the age after which content kept at ℓ must be unrecoverable.
<!-- unreadable-table -->
| Level | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|
| Unrecoverable after | 3 d | 3 d | 3 d | 30 d | 365 d | 365 d |
<!-- /unreadable-table -->
**Note 7.7 (why a staircase, not a curve).** On a logarithmic age axis the steps sit at log₁₀ 3 ≈ 0.48, log₁₀ 30 ≈ 1.48 and log₁₀ 365 ≈ 2.56 (in days): about one decade apart. Each finer level of detail lasts about a tenth as long as the one below it. A smooth forgetting curve would make the readable level uncertain at any age. The staircase makes it exact, which is why the note can print a date on every section.
**7.8 The store (Goldfish binding; designed and tested in software, not yet wired into the daemon).**
```
copies at r ∈ {1, 2, 3} with r ≤ s(u); class k ∈ {1d, 7d, 30d, keep} store.py:35-48
keep(r, k) = min((3, 30, 365)[r] days, days(k)) keys.py:93-102
at the UTC midnight that starts day D′, each chain forgets every day ≤ D′ − keep
⇒ a copy's lifetime lies in (keep − 1 d, keep]: it can be forgotten early, never late
```
Forgetting is by destroying keys, not by deleting files. Seeds are hash-chained, so a destroyed day key cannot be recomputed from a later one. v01 stores nothing above level 3: level 4 exists in the engine and on the ring, but the note keeps a paraphrase (D6).
**7.9 The event log.** SPEC §9 says the log must not outlive the last step of the schedule. In v01 the log lives in RAM only and is not pruned while the device stays on (D4).
## 8. End of session and what it leaves
**8.1 End of session (SPEC §6.7).** An open raise is undone. δ returns to δ₀. c, κ, ρ, F and the reply graph are cleared. Identities are discarded and a new session ID is drawn. Records already filed keep their stamped levels.
**8.2 Derived artefacts.** An artefact at level ℓ may use only {u : X_u(t) ≥ ℓ}. So each voice's contribution is bounded by its own level: a summary at level 2 cannot use the words of a voice at 1.
**8.3 The Goldfish note.** Written once to the NFC tag when the session ends. Three sections, Outcomes (level 1), Gist (2) and Attributed (3), each with a "kept until" date of start + unreadable_after(ℓ). The front matter gives a count of voices, never their IDs. If the note is too big for the 8 KB tag, the most detailed section is dropped first, and the note says so. A note never contains audio, per-statement times, voice features, or anything above a voice's level (`v01/note-format.md`).
**8.4 Worked replay: the roadmap session.** The site's script (`v01/firmware/scenarios/meeting.json`) run through the real daemon on the simulated board. The levels, filings and dates are the firmware's. The paraphrases in the note are hand-written stand-ins for the summariser, which is not built.
| t (s) | Event | Effect |
|---|---|---|
| 4–16.5 | Alice, Bob, Charlie speak | filed at 3 |
| 16.8 | eye press | room off (c = 0) |
| 17.2–24.1 | Dana: "Keep my words to the gist for this part…" | r(Dana) = 2; the line itself is filed at 0 |
| 31.0 | double press | resume, provisional until 61.0 |
| 51.4–57.4 | Eve: "You can quote me on this one." | r(Eve) = 4; the line stays at 3 |
| 61.0 | window closes | confirmed |
| 62.6 | hold | room off, battery shown |
| 67.6–71.2 | Bob: "Between us, …" | filed at 0 |
| 71.9–77.1 | Charlie: "Okay, back on the record. …" | provisional resume until 107.1; the line is filed at 0 |
| 77.8–82.4 | Eve | filed at 4, held at 0 |
| 82.7 | press inside the window | resume undone; Eve's line restamped 0; room off |
| 88.6 | double press | resume, provisional until 118.6 |
| 88.7–95.3 | Alice: "Keep my words to just the decisions from here…" | r(Alice) = 1 |
| 111.7–117.3 | Dana: "Please leave me out of this last bit…" | r(Dana) = 0 |
| 118.6 | window closes | confirmed |
| 717.3 | 600 s of silence | session ends; the note goes to the tag |
The note: Outcomes 6 lines, kept until 2027-10-14; Gist 11 lines, until 2026-11-13; Attributed 10 lines, until 2026-10-17 (the simulator's clock starts 2026-10-14 00:00 UTC). Bob's aside is absent. Dana never appears in Attributed. Charlie's proposal, said while the room was off, is lost, because the resume in that same sentence was later undone.
## 9. Invariants and the tests that check them
Vector numbers are case indexes in `protocol/vectors/sessions.json`.
| Invariant | Checked by |
|---|---|
| I1 Lowering is immediate, needs no name, and only lowers | vectors 0, 1, 2, 5, 47, 57; `test_device` |
| I2 Self intents touch only the speaker | vectors 3, 4, 6, 7 |
| I3 Nobody raises anyone else | vectors 21, 56; `test_device` |
| I4 Uncertainty falls toward less | vectors 22, 23, 24, 54, 55 |
| I5 A room raise needs the name, is capped, can be disabled | vectors 8, 10, 11, 12 |
| I6 Raises are provisional and undone retroactively | vectors 9, 13–20, 44, 58 |
| I7 Filing takes the lowest level over the span | vectors 29, 30, 45, 46 |
| I8 Forgetting reaches later filings and stays in its session | vectors 25–28, 48, 51 |
| I9 Retention | vectors 31, 32; `retention.json`; `test_scenario`, `test_keys` |
| I10 Identity and session | vectors 33, 34, 35 |
| I11 Artefacts are bounded per voice | vectors 13, 36; `test_device` |
| I12 Exchange and standing preferences (unused in v01, which has no radio) | vectors 37, 38, 49, 50, 52, 53 |
Both reference engines (Python and JS) run every vector, and `protocol/tools/fuzz_parity.py` compares them on random sessions.
## 10. Where the code and the spec disagree
| ID | What | Where | Backlog |
|---|---|---|---|
| D1 | Fail closed keeps level 1. SPEC §5.3 says to act "as if the longest step has passed", which would keep nothing. | `keys.py:104-108`; ADR-0005 | B-91 |
| D2 | The note's "kept until" dates ignore spoken limits. After "Keep this for a week." the note still prints 1 year, 30 days and 3 days. | `daemon.py:52` | B-92 |
| D3 | A limit shorter than one day snaps up to the 1-day key class, so it is kept longer than asked. | `keys.py:46-53` | B-93 |
| D4 | The voice-ID counter, the event counter and the event log survive the end of a session. | `session.py`, `resolution.js` | B-94 |
| D5 | Gap markers (level 0) are never pruned; the schedule implies 365 d. | `store.py:36-38` | B-95 |
| D6 | v01 keeps no level-4 content although the ring shows blue; the site's JS stand-in does quote. | `store.py:40`; `goldfish.js` | B-100 |
| D7 | The store is not wired into the daemon, and the secure-element driver is not written. | `01-concept/landing/privacy-review.md`; firmware README | B-99 |
| D8 | The store counts in UTC days, the engine in continuous age. The store forgets up to 1 day early, never late. | `keys.py` vs `session.py` | informative |
| D9 | SPEC §4.3 lets a voice shorten its own record, but no English or Spanish phrase does. | `en.json` | B-97 |
## 11. Meeting and attribution protocols (informative)
### 11.1 Two axes and one chain
Human ground rules control two things separately: how much of the *content* survives, and how much of the *identity*. RP's levels are a single chain through that plane:
```
Content = ∅ < outcomes < gist < paraphrase < words < audio
Identity = none < role < name
φ(0) = (∅, none) φ(3) = (paraphrase, name)
φ(1) = (outcomes, none*) φ(4) = (words, name)
φ(2) = (gist, none) φ(5) = (audio, name)
* a commitment names its owner
level(H) = max{ ℓ : φ(ℓ) ≤ (content_H, identity_H) } (componentwise)
loss(H) = what H allows that φ(level(H)) does not
```
The map is safe: a recorder following level(H) never keeps more than the human rule H allows. Whatever H allows off the chain is lost.
### 11.2 Summary
| Practice | Goldfish counterpart | Main difference |
|---|---|---|
| Chatham House Rule | room level 2, The gist ("no names", "don't attribute"); the note shows only a count of voices | The Rule lets you use exact words without a name; the chain has no such point, so Goldfish keeps less (B-96). The Rule binds people's later conduct; RP binds the recorder. Each voice can go lower than the room. |
| Chatham "off the record" | level 0; the eye press | The gap and its length are kept. The device keeps listening for "back on the record". |
| Robert's minutes: "what was done and not what was said" | level 1, Just the decision | Robert's minutes name the mover and roll-call voters; level 1 names only a commitment's owner. Robert's minutes are approved by the assembly; Goldfish reads nothing back (B-98). |
| Robert's general consent | a provisional raise and the 30 s window | After an objection, Robert's moves to a motion and a vote. RP simply undoes, with no vote and no reason. Only raises need consent; lowering needs nobody's. |
| Robert's objection to consideration | the objection window | Robert's needs a two-thirds vote; RP needs one voice. |
| Robert's expunge | `forget` | Robert's keeps the expunged words readable, to audit the order. RP keeps the span and its length, not the words. Robert's needs a majority of the whole membership; RP lets one voice forget its own words. |
| Robert's decorum: "the member who spoke last" | session pseudonyms v1, v2, … | A courtesy in debate there; what the record stores here. |
| Robert's committees | `side_conversations` group caps | A partial analogue: groups are inferred from reply timing. |
| AP "on the record" | level 4, Quote me | v01's note keeps a paraphrase (D6). |
| AP "background" | no exact level; 2 is the floor | Neither the role description nor the words survive. |
| AP "deep background" | level 2 | The same loss as Chatham. |
| AP "off the record" | level 0 | — |
| AP: ground rules set at the start | a fixed grammar with test vectors; the ring shows the result | In RP anyone can set their own level at any time, alone, without negotiating. |
| IETF Note Well | the opposite of SPEC §8 | IETF participation implies agreement to recording; in RP a default above 0 is never consent. |
| IETF rough consensus (RFC 7282) | a provisional raise ("lack of disagreement") | RP always gives way to an objection against a raise. |
| Sociocratic consent | the window: "Do you object?" | An RP objection needs no reason and is never tested. |
| Quaker minute | none | The note is written at the end with no read-back (B-98). |
| Open Space: "When it is over it is over." | end of session (§8.1) | Levels, identities and forget spans do not cross it. |
### 11.3 Sources, quoted
Each quote was checked against the source on 2026-10-19. Quotes marked **verify** were read from a copy, not the official page.
**Chatham House Rule.** "When a meeting, or part thereof, is held under the Chatham House Rule, participants are free to use the information received, but neither the identity nor the affiliation of the speaker(s), nor that of any other participant, may be revealed." From the FAQ: "In cases where the Rule is not considered sufficiently strict, an event may be held 'off the record'." (https://www.chathamhouse.org/about-us/chatham-house-rule; the site refuses scripts, so **verify** in a browser. The same text is in `v01/research/market.md`.)
**Robert's Rules of Order, 1876** (Project Gutenberg #9097, public domain):
- §3: "the presiding officer merely announcing that, if no objection is made, such will be considered the action of the assembly."
- §15: "An objection can be made to any principal motion [§ 6], but only when it is first introduced, before it has been debated."
- §36: "In referring to another member, he should, as much as possible, avoid using his name, rather referring to him as “the member who spoke last,” or in some other way describing him."
- §51: "In permanent societies, where the minutes are not published, they consist of a record of what was done and not what was said, and should be kept in a book."
**Robert's Rules of Order Revised, 1915** (Project Gutenberg #75990, public domain):
- §37: "The words expunged must not be so blotted as not to be readable, as otherwise it would be impossible to determine whether more was expunged than ordered."
- §48: "One negative vote defeats a motion to make a vote unanimous, as a single objection defeats a request for general consent."
- §48: "By general, or unanimous, or silent, consent the assembly can do business with little regard for the rules of procedure, as they are made for the protection of the minority, and when there is no minority to protect, there is little use for the restraint of the rules, except such as protect the rights of absent members, or the right to a secret vote."
- §59: "He should keep a record of the proceedings, stating what was done and not what was said, unless it is to be published, and never making criticisms, favorable or otherwise, on anything said or done."
The current edition (*RONR*, 12th ed., 2020) was not consulted and is not quoted.
**AP, News Values and Principles** (https://www.ap.org/about/news-values-and-principles/telling-the-story/): "Not everyone understands “off the record” or “on background” to mean the same things. Before any interview in which any degree of anonymity is expected, there should be a discussion in which the ground rules are set explicitly." And: "Deep background. The information can be used but without attribution. The source does not want to be identified in any way, even on condition of anonymity."
**IETF.** Note Well (https://www.ietf.org/about/note-well/): "The IETF routinely makes public written, audio, video, and photographic records of IETF activities, including your personal information as set out in the IETF Privacy Statement." RFC 7282 §2 is titled "Lack of disagreement is more important than agreement".
**Sociocracy For All** (https://www.sociocracyforall.org/consent-decision-making/): "But instead of asking everyone, “Do you agree?”, we ask, “Do you object?” If no one objects, there is consent."
**Quaker faith & practice**, chapter 3 (https://qfp.quaker.org.uk/chapter/3/), 3.14: "Remember that any minute you present is only a draft minute until the meeting has accepted it as its own."
**Open Space** (Harrison Owen's principles, as given at https://co-intelligence.org/P-Openspace.html): "When it is over it is over."
### 11.4 What RP adds that none of these has
- **Per voice, not per meeting.** Every practice above sets one rule for the room. RP lets each voice set its own level, and the room's rule only ever lowers it further.
- **Unilateral lowering.** No practice lets one participant make the record keep less without a vote or the chair. In RP any voice can, at once, with no reason.
- **A clock.** None of them forgets on a schedule. RP's staircase dates every section of the note.
- **Checkable.** The rules are code with test vectors. A meeting can know, not trust, what was kept.
### 11.5 What RP lacks
- "Exact words, no name" (Chatham, AP deep background) is not a level (B-96).
- "Chatham House rules" is not a phrase the grammar knows, and no phrase lets a voice shorten only its own record (B-97).
- The room cannot read back and accept its note before it is written, as a Quaker meeting does (B-98, with B-36).
## Appendix A. Changes
- 0.1 (2026-10-19): first draft.