| index f6e0556..c49bda9 100644 |
| --- a/docs/superpowers/specs/2026-09-03-memoria-design.md |
| +++ b/docs/superpowers/specs/2026-09-03-memoria-design.md |
| @@ -3,7 +3,8 @@ |
| The Oficina suite's memory. One instance per team, at |
| `<team>.memoria.oficina.works`. |
| |
| -**Status:** draft 3, 2026-09-03; amended 2026-09-04 (§8.3 provider). |
| +**Status:** draft 3, 2026-09-03; amended 2026-09-04 (§8.3 provider, §8.4 |
| +shape, decisions 13–14). |
| |
| Review history, per AGENTS.md §Review. None of these models wrote the |
| draft they reviewed; all drafts were written by Opus. |
| @@ -14,6 +15,7 @@ draft they reviewed; all drafts were written by Opus. |
| | 2 | GPT Sol via Codex on a ChatGPT subscription — **it reported its own model id as `gpt-5`, which disagrees with the `gpt-5.6-sol` Codex labelled it; recorded as the discrepancy it is rather than resolved** | REQUEST CHANGES — 4 ANSWERED, 1 REWORDED, 6 PARTIAL, 1 blocker and 6 new findings | |
| | 3 | Fable | REQUEST CHANGES — 22 findings. (This row read "none yet" while §13 recorded the review as done; §13 was right.) | |
| | 3, §8.3 provider amendment | Fable | REQUEST CHANGES — 4 MAJOR, 4 MINOR, 2 NIT. All applied; §1.5 records what it changed | |
| +| 3, §8.4 and decisions 13–14 | Fable | REQUEST CHANGES — 5 MAJOR, 4 MINOR, 1 NIT. All applied. It found §8.4's central argument resting on a misreading of §6.5 and §4.1; §1.6 records what it changed | |
| |
| §1.2 records what the draft-1 reviews changed; §1.3 what the draft-2 |
| review changed. **Draft 3 has not been reviewed.** |
| @@ -68,7 +70,8 @@ embedding is a plaintext cell in a costume — §3.1. |
| | 10 | **memoria never does arithmetic. A number comes from a cell, or from Sheets' engine via S6.** S6 takes an enumerated function over cell refs — never expression text — so memoria is never a formula author. | memoria computing and showing its working (a second calculator, arrived at one `mean` at a time); writing a formula into a scratch sheet (a write, forbidden by §6.4) | Paul asked for numeric questions, 2026-09-03; shape ruled by Fable and GPT Sol independently, converging | |
| | 11 | **Admission is derived from the siblings, not from a members table memoria maintains.** A person may enter memoria if at least one sibling says they are a member of this team. | Draft 2's own members table, which left a person removed everywhere else still able to read memoria-owned content | after the draft-2 review, 2026-09-03 | |
| | 12 | **Name: `memoria`.** House convention is plain Spanish office nouns. A *memoria* is both a memory and, in Spanish office usage, a written record. | `archivo`; `cerebro`; `base`; anything English | Paul, 2026-09-03 | |
| -| 13 | **The provider is TensorX, and memoria supports exactly one.** A team owner's choice is on or off, not which — a configurable base URL would let an owner reinstate a proxy while the answer still printed "one named provider". Chosen for EU jurisdiction and open weights, criteria decision 4 did not state, at a known cost in id-pinning and frontier quality (§8.3). | OpenRouter (a proxy: two parties see content); OpenAI direct (better id-pinning, worse jurisdiction and cost) | Paul, 2026-09-04, both halves — *"we go with exactly one for now: TensorX. If we want to swap that later we can."* Swapping is a code change and a review, not a setting | |
| +| 13 | **The provider is TensorX, and memoria supports exactly one.** A team owner's choice is on or off, not which — a configurable base URL would let an owner reinstate a proxy while the answer still printed "one named provider". Chosen for EU jurisdiction and open weights, criteria decision 4 did not state, at a known cost in id-pinning and frontier quality (§8.3). | OpenRouter (a proxy: two parties see content); OpenAI direct (better id-pinning, worse jurisdiction and cost) | Paul, 2026-09-04, both halves — *"we go with exactly one for now: TensorX. If we want to swap that later we can."* Swapping the *generation* provider is a code change and a review, not a setting; swapping the *embedding* provider is that plus a full corpus re-embed unless the new host serves the same open weights (§8.3, §13) | |
| +| 14 | **memoria is a search page, not a chat.** One GET search form, `?q=`, one query in and one page out, with `/documents/{id}` as the only other passage surface. This follows from decision 5, not from decision 3 — §8.4's invariant is what decision 3 requires, and a conversational surface could satisfy it. | A chat UI with citations; a threaded follow-up. Deferred to §12, not forbidden — the invariant forbids the leak, this row chooses the shape | **Spec author, 2026-09-04, answering a question Paul asked. Paul has not confirmed it** — QUESTIONS #3 | |
| |
| ### 1.1 The proposal review, and what it killed |
| |
| @@ -270,6 +273,49 @@ application should be reviewed by different people.** Whoever writes the |
| rule has just persuaded themselves, and is the worst-placed reader to |
| notice that the very next paragraph fails it. |
| |
| +### 1.6 What the second provider review changed |
| + |
| +The §8.4 amendment — *memoria is a search page, not a chat* — was reviewed |
| +by Fable, which returned REQUEST CHANGES with five MAJOR findings. The |
| +ruling survived; the argument for it did not, and the way it failed is |
| +worth more than the ruling. |
| + |
| +**The case was built on a misreading of this spec.** §8.4 claimed a chat |
| +could not be built correctly, because re-authorizing a transcript each turn |
| +would breach §6.5's deadline. §4.1 caps a batch at **256 pairs per call** |
| +and §6.5 says **one page is one round of concurrent calls, the floor is the |
| +slowest sibling and not their sum**. A four-turn transcript of eight |
| +passages is 32 pairs: one batch, one round, the same floor as a fresh |
| +query. A correct chat is affordable. The draft had also conceded the point |
| +in the act of denying it — *"re-authorizing the whole transcript on every |
| +turn is the only correct version"* says a correct version exists. |
| + |
| +**Its other argument proved too much.** *A chat transcript is a cache of |
| +authorized passages* — true, and equally true of a search results page left |
| +open in a tab, and of `/documents/{id}`, which renders a whole document and |
| +stays open indefinitely. That is a fact about rendered pages, not about |
| +chat; it now sits in §3.2's leak list where it applies to everything. |
| + |
| +**What survived is narrower and enforceable.** The real rule is about |
| +**re-speaking**: no passage crosses a request, in a response or in a model |
| +prompt, without §4.1 in the request that carries it. That is decision 3 |
| +applied to rendering, it is what §8.5 already said about the model, and |
| +gate 26 asserts it against the prompt builder. Carrying query *text* |
| +forward breaks nothing. |
| + |
| +**And the shape is a choice, so it is recorded as one.** Decision 14 rests |
| +on decision 5 — the search floor is the product — not on decision 3. A |
| +conversational surface that keeps the invariant is deferred to §12, to be |
| +argued on its merits. The row is attributed to the spec author with Paul |
| +unconfirmed, because unlike decisions 13's two halves it is not his ruling. |
| + |
| +The lesson, distinct from §1.4's unfailable gates and §1.5's rule broken in |
| +the paragraph that states it: **a preference will always find a rule that |
| +seems to compel it.** The search page was the right answer for |
| +decision 5's reasons. Reaching past those for decision 3 made a choice look |
| +forced, and cost two misread sections to do it. When a constraint arrives |
| +conveniently, check it against the text rather than the memory of the text. |
| + |
| --- |
| |
| ## 2. What memoria is |
| @@ -345,6 +391,15 @@ is a truncation of a native 4096. Either way the stored width names a |
| choice of ours. It narrows the candidate models; it does not name one. Do |
| not lean on that. |
| |
| +**From a rendered page.** A response body is a copy in someone's browser, |
| +and no revocation reaches it. A results tab left open on Tuesday still |
| +shows Tuesday's authorized snippets on Friday; `/documents/{id}` shows a |
| +whole document for as long as it stays open. This is true of every surface |
| +memoria has or could have, so it is not an argument against any particular |
| +one (§8.4 tried to use it as one and was wrong). memoria's guarantee ends |
| +at the response body, and §8.4's invariant is what keeps the *next* |
| +response honest. |
| + |
| **From a live host — the largest entry, and new.** A compromised memoria |
| host reaches the team's corpus across every app at once, plus §5.1's |
| connector credentials. §3.3 works through how this compares to Sheets; |
| @@ -1190,9 +1245,16 @@ is narrower than decision 4's "fixed by the team owner", and deliberately: |
| a configurable base URL on an OpenAI-compatible client would let an owner |
| point memoria at a proxy while the answer still printed "one named |
| provider", which is the failure this whole section is about. Decision 13 |
| -records it. Swapping providers later stays cheap — one client, one base |
| -URL, two model ids — but it goes through code and §Review, where the |
| -two-parties question gets asked again. |
| +records it. |
| + |
| +**Swapping later is cheap on one side and not the other.** Generation is |
| +one client, one base URL and two model ids, through code and §Review, where |
| +the two-parties question gets asked again. **Embeddings are a third id and |
| +the expensive one:** a new host that serves the same open `qwen3-embedding-8b` |
| +weights yields compatible vectors and costs a migration, and any other host |
| +costs a re-embed of the whole corpus — 24M tokens at an unmeasured |
| +wall-clock (§13). Saying "swapping stays cheap" without that split would |
| +contradict §13 two pages later. |
| |
| **Why this one, honestly stated.** Not because it beats OpenRouter and |
| OpenAI on decision 4's own words — on one half of them it loses: |
| @@ -1261,40 +1323,71 @@ list; every sentence carries a passage id from it, and a sentence citing |
| nothing is dropped before render. **If every sentence is dropped, say |
| so** — an empty paragraph is indistinguishable from AI being off. |
| |
| -**This is a search page, not a chat.** The spec implied it everywhere and |
| -stated it nowhere, which is how a chat box gets added by someone reading |
| -§8.2's "frontier model" and reaching for the familiar shape. Written down: |
| -one GET form, `?q=`, one query in and one page out. No turns, no history, |
| -no thread, no follow-up that refers to "it". §8.1's floor is already this |
| -page, and it is what slice 0 shipped. |
| - |
| -**The reason is decision 3, not taste.** Authorization is asked of the |
| -owning sibling per person *per query*, live, never cached, because caching |
| -authorization is what makes a stale answer a leak. **A chat transcript is a |
| -cache of authorized passages.** Turn 1's snippets sit on screen while turn |
| -4 is asked; their grants were evaluated minutes ago; a revocation in |
| -between is invisible, and the page is now doing exactly what decision 3 |
| -forbids — with the added twist that the passages are on the reader's screen |
| -rather than in a database, so no purge reaches them. Re-authorizing the |
| -whole transcript on every turn is the only correct version, and it makes |
| -each turn cost the previous turns' S2 calls again, against §6.5's 800 ms |
| -deadline. The shape does not survive its own rule. |
| - |
| -Two consequences worth stating, since both are places a chat UI would have |
| -been the easy answer: |
| - |
| -- **A refined search is a new query, not a follow-up.** The affordance is |
| - editing `?q=` and asking again. Nothing carries over, and nothing needs |
| - to, because retrieval is free (§8.2). |
| -- **The 🤖 paragraph is disposable.** It is one paragraph above one list, |
| - regenerated per query and stored nowhere. Nothing accumulates that §5.2's |
| - provenance rule would then have to expire, and no runtime record of |
| - prompts exists to fall under §8.3's ledger rule. |
| - |
| -Hybrid, then, in the only sense that matters: **the search page is the |
| -product and the answer is a garnish on it** (decision 5). Not a chat with |
| -citations bolted on, which is the same components in the order that breaks |
| -the authorization rule. |
| +#### The invariant, which is the part that binds |
| + |
| +> **Every passage in a response, and every passage in a model prompt, |
| +> passed §4.1 in the request that produced it. Nothing memoria holds, and |
| +> nothing it accepts from the client, carries a passage across requests.** |
| + |
| +This is decision 3 applied to rendering, and §8.5 already states its other |
| +half — *"authorization runs before the model exists"*. It is a rule about |
| +what may enter a response or a prompt **per request**. It says nothing |
| +about interface shape, and it is enforceable: gates 17 and 26. |
| + |
| +What it forbids is **re-speaking**. A surface that puts previously |
| +retrieved passages back into a model prompt, re-renders them server-side, |
| +or stores them in a transcript row is memoria speaking those passages |
| +again, at a moment when nobody has asked whether this person may still see |
| +them. Carrying *query text* forward is not re-speaking and is not |
| +forbidden: retrieval and S2 then run from scratch, which is the whole point |
| +of retrieval being free (§8.2). |
| + |
| +**An earlier draft of this section argued the case badly, and the argument |
| +is worth recording as wrong.** It said a chat transcript is a cache of |
| +authorized passages because turn 1's snippets sit on screen while turn 4 is |
| +asked. That proves too much: it is equally true of a search results page |
| +left open in a tab, and of `/documents/{id}`, which renders a whole |
| +document's passages and stays open as long as the reader likes. **A |
| +rendered page is a copy in a browser and memoria's revocation never reaches |
| +it** — true of every page here, chat or not, and now recorded as such in |
| +§3.2 rather than argued from here. The draft also claimed re-authorizing a |
| +transcript would breach §6.5's deadline. It would not: §4.1 batches 256 |
| +pairs per call and §6.5 makes a page one round of concurrent calls, so a |
| +four-turn transcript of eight passages is 32 pairs — one batch, one round, |
| +the same floor as a fresh query. **A correct chat is affordable under this |
| +spec.** The invariant, not the latency, is what does the work. |
| + |
| +#### The shape, which is a product ruling and not a consequence |
| + |
| +**memoria is a search page: one GET search form, `?q=`, one query in and |
| +one page out**, with `/documents/{id}` as the only other passage surface. |
| +That follows from decision 5 — the search floor *is* the product — and not |
| +from decision 3, which a conversational surface could satisfy. Recorded as |
| +decision 14. **It is the spec author's ruling, in answer to a question Paul |
| +asked on 2026-09-04, and Paul has not confirmed it** — QUESTIONS #3. A |
| +conversational surface that keeps the invariant above is not forbidden by |
| +this section; it is deferred by §12, where it can be argued on its merits |
| +rather than smuggled in as a corollary. |
| + |
| +Two things this settles for now: |
| + |
| +- **A refined search is a new query.** Edit `?q=` and ask again; nothing |
| + carries over, and nothing needs to. |
| +- **The 🤖 paragraph is disposable — today.** One paragraph above one list, |
| + regenerated per query, stored nowhere; it is not a §5.2 derived artefact |
| + and no runtime prompt record exists to fall under §8.3's ledger rule. |
| + **Both halves expire if §12's cached-answers or model-call-ledger rows |
| + trigger**, and this paragraph changes with them. |
| + |
| +**What slice 0 actually shipped is a subset of §8.1's floor**, not the |
| +floor: `index.html` renders source, title, ordinal, snippet and deep link, |
| +but **no 👨/🤖 marker, no date, and no pagination** — `Search(ctx, q, 25)` |
| +is a hard cap and nothing renders a second page. §9.1 also specifies |
| +`GET /query?q=` where the app serves `GET /?q=`. Three gaps and a path, |
| +listed rather than glossed. |
| + |
| +Hybrid, then, in the sense that matters: **the search page is the product |
| +and the answer is a garnish on it** (decision 5). |
| |
| ### 8.5 Prompt injection |
| |
| @@ -1510,6 +1603,12 @@ renders on its own origin. The home never sees `q`. |
| 17. **No sibling-facing endpoint exists.** Enumerate memoria's routes and |
| fail any inbound route not on the person-facing or connector list — |
| the regression gate for the push surface draft 1 shipped by accident. |
| + **The list is literal, not descriptive:** `GET /`, `GET /documents/{id}`, |
| + `POST /documents`, `POST /documents/{id}/delete`, plus the auth routes. |
| + Written that way so that adding a thread or chat route goes red on the |
| + way in (decision 14) rather than being argued about afterwards. |
| + Mutation: register `POST /chat`, expect red. **Not built** — nothing in |
| + `internal/memoriatest` walks the router yet. |
| 18. **External-source authorization and disclosure.** A memoria-owned |
| document is governed by memoria's grants, an upstream revocation does |
| *not* remove it, and the copy the README promises is deletable is |
| @@ -1546,6 +1645,13 @@ renders on its own origin. The home never sees `q`. |
| 4xx can echo the input that caused it, so a provider error path is a |
| content path. Gate 4 covers our own log lines; this covers the bytes |
| the provider hands back. |
| +26. **Every passage id in a model prompt is one S2 returned `VISIBLE` for |
| + in this same request.** Asserted on the prompt builder's input, not on |
| + a table name or a schema grep — a grep for "transcript" would be a gate |
| + written to pass, since the leak does not need a table. This is §8.4's |
| + invariant and the only thing that stops a future surface re-speaking a |
| + revoked passage. Mutation: hand the prompt builder one passage from a |
| + previous request's result set, expect red. Slice 2. |
| |
| --- |
| |
| @@ -1595,7 +1701,8 @@ rows. |
| | A feed surface on memoria | Something needs to consume memoria. A new decision, with its own leak-list entry | |
| | Calendar as a source | Calendar has a spec and content; bodies off by default even then | |
| | Mail beyond forward-or-label | Never by sweep. A mail repo exists **and** a per-thread audience maps onto a team | |
| -| Cached answers | Query cost becomes visible. Requires §5.2's provenance invalidation first | |
| +| Cached answers | Query cost becomes visible. Requires §5.2's provenance invalidation first. **Triggering this expires §8.4's "stored nowhere"** | |
| +| A conversational surface | Someone wants one, and it keeps §8.4's invariant: no passage crosses a request, in a prompt or a response. Decision 14 chose the shape on decision 5's grounds, not decision 3's, so this is a product argument to have — not a rule to break. §6.5 affords it: a four-turn transcript re-authorizes in one batch, one round | |
| | A runtime model-call ledger | Someone needs to audit routing. Sealed and provenance-stamped when it lands (§8.3) | |
| | A local embedding model | Would remove ingest egress entirely, which §3.2 makes the largest single leak. **Not the cgo-free in-process runtime this row first imagined:** §8.3's `qwen3-embedding-8b` is open-weight, so self-hosting is possible in principle, but 8B parameters means a second box to run and keep, not a library linked into the one binary | |
| | Cross-team anything | Never, under §6.4 | |
| @@ -1719,19 +1826,21 @@ rows. |
| - **§7.2's 24M tokens is now an estimate under a different tokeniser.** It |
| came from an OpenAI-style ~1.34 tokens/word; billing is Qwen's. Probably |
| close, unverified, and §8.6 inherits it. |
| -- **Answer quality for the synthesis class is unmeasured, and Paul has |
| - ruled it need not block.** *"Frontier open weight is fine. This is |
| - lightweight LLM inference based on categorisation and memory. It's not |
| - complex reasoning."* (2026-09-04.) That is the right reading of §8.2: the |
| - three model-free classes carry the navigational, numeric and |
| - who-knows-about-X work, §8.7 sends arithmetic to Sheets, and what is left |
| - for a model is extractive summary over passages that have already been |
| - retrieved, filtered and authorized. **It also raises a question this spec |
| - should answer rather than inherit:** if the work is not complex |
| - reasoning, does §8.2 need two tiers at all, or is the frontier tier a |
| - habit? Measure the cheap tier on the synthesis class before building the |
| - expensive one — the router's cost argument (§8.6) gets stronger, not |
| - weaker, if the answer is one tier. |
| +- **Answer quality for the synthesis class is unmeasured, and the |
| + measurement is still owed before slice 2** (§11). What Paul settled on |
| + 2026-09-04 is the *product call* — *"Frontier open weight is fine. This |
| + is lightweight LLM inference based on categorisation and memory. It's not |
| + complex reasoning."* — not the obligation to measure it, which came from |
| + the code review and stands. It reads §8.2 right: three of five classes |
| + call no model, and §8.7 sends arithmetic to Sheets, so a model sees only |
| + what survives retrieval, filtering and authorization. |
| +- **Does §8.2 need two tiers at all?** The spec's own question, not Paul's. |
| + If the residual work is as light as decision 14's shape suggests, measure |
| + the cheap tier on the synthesis class *before* building the expensive |
| + one; §8.6's cost argument gets stronger, not weaker, if the answer is one |
| + tier. Note the premise is not free: §8.2 calls the single-source class |
| + extractive but synthesis spans ≥3 documents, which is the one place the |
| + work is genuinely more than extraction. |
| - **Docs §11.2's qualification** must be recorded in the Docs repo by |
| `docs-e6`, or the two specs contradict. |
| - **A `carlos/platform` discussion** noting the app boundary is the |