docs(spec): draft 2 — answer both reviews
GPT Sol (12 findings) and Fable (21) reviewed draft 1 independently against the same adversarial brief. Both returned REQUEST CHANGES, and both found the same three structural gaps. Gap 1, the one that mattered: "is a member of this team" is not one fact. Sheets, Docs and memoria each keep their own membership table, the home is forbidden from being the authority, and no contract call carries membership between instances — so removing someone in Sheets left them reading Sheets content through memoria. The one-ACL ruling killed per-item drift and moved it onto the membership predicate. Fixed by making S2 carry the asker: memoria asks the owning app "can person P see these items at this revision", and evaluates nothing itself. Membership and visibility collapse into one question answered by the only correct evaluator. Team-visibility survives as a custody rule bounding what memoria holds, which is a different job. Gap 2: the search floor bypassed the gate entirely — §0's promise held only for generated answers. One gate now, both paths, with a test that fails any new render path lacking it. Gap 3: external sources have no owner to ask, so memoria owns them outright and says so, rather than implying a re-check that cannot exist. Also: §9 specified the push surface decision 7 forbids (both reviewers); the README was false about egress volume, since embeddings ship the whole corpus at ingest, not just answer-time passages (Fable); a restore could serve deleted content from a superseded snapshot (GPT Sol); token rotation had no handover protocol (GPT Sol); §6.3 claimed three independent cross-team barriers and has two; and §7's arithmetic was wrong in three places, including a ceiling I called "tidy" about a coincidence I had manufactured. The wake budget binds at ~150k chunks, not 100k, and ANN is not what fixes it.
Reviewed-by: gpt-5.6-sol (commandcode), Fable Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SiWVaR69Vb2Lzc9wgfit1v
1 file changed,
+681
−406
docs/superpowers/specs/2026-09-03-memoria-design.md+681 −406
diff --git a/docs/superpowers/specs/2026-09-03-memoria-design.md b/docs/superpowers/specs/2026-09-03-memoria-design.md| index ea233b9..fa611d6 100644 |
| --- a/docs/superpowers/specs/2026-09-03-memoria-design.md |
| +++ b/docs/superpowers/specs/2026-09-03-memoria-design.md |
| @@ -3,25 +3,47 @@ |
| The Oficina suite's memory. One instance per team, at |
| `<team>.memoria.oficina.works`. |
| -**Status:** draft, 2026-09-03. Brainstormed across six Fable passes, one |
| -of them an adversarial review that returned REJECT against the proposal |
| -as first worded. §1 records what changed in response. Not yet reviewed |
| -under AGENTS.md §Review — that pass is owed before this is approved. |
| +**Status:** draft 2, 2026-09-03. |
| + |
| +Draft 1 was reviewed independently by **GPT Sol** (via commandcode) and |
| +by **Fable**, both against the same adversarial brief, and both returned |
| +**REQUEST CHANGES** — 12 and 21 findings, three structural gaps found by |
| +both. This draft answers all of them. §1.2 records what changed and why, |
| +because two corrections reverse a settled decision and a future reader is |
| +owed the reason. |
| + |
| +Per AGENTS.md §Review, the reviewing models are recorded above; neither |
| +wrote draft 1, which was written by Opus. **This draft has not itself |
| +been reviewed** — that pass is owed before approval. |
| --- |
| ## 0. The one rule |
| -> **memoria indexes only what the whole team can already see, and it |
| -> never speaks a passage without re-checking that this is still true.** |
| +> **memoria never speaks a passage without asking whoever owns it |
| +> whether this person may see it, right now.** |
| + |
| +Draft 1 said *"memoria indexes only what the whole team can already see, |
| +and it never speaks a passage without re-checking that this is still |
| +true"*. Both reviewers showed that sentence does not survive contact with |
| +the suite: "the whole team" is not one fact (§1.2 gap 1), and the |
| +re-checking was specified for generated answers but not for ordinary |
| +search (§1.2 gap 2). |
| -Everything below follows from that sentence. It is the reason memoria |
| -has no per-item ACL to copy, no revocation queue to drain, and no way to |
| -recite a document to a person who has lost access to it. |
| +The rule above replaces it, and it splits by provenance because the suite |
| +does: |
| + |
| +- **Content copied from a sibling app** — the sibling is the authority. |
| + memoria asks it about *this person* on every path that speaks a |
| + passage. memoria evaluates nothing. |
| +- **Content that has no other owner** — Notion, Drive, forwarded mail, |
| + uploads. There is nobody to ask, so **memoria is the app of record** |
| + and its own grants are authoritative rather than a stale copy of |
| + something else. |
| The second rule is Sheets' rule, unchanged: **the bucket never holds a |
| -plaintext cell.** For memoria this binds harder than it looks, because |
| -an embedding is a plaintext cell in a costume — see §3.1. |
| +plaintext cell.** For memoria this binds harder than it looks, because an |
| +embedding is a plaintext cell in a costume — §3.1. |
| --- |
| @@ -31,46 +53,88 @@ an embedding is a plaintext cell in a costume — see §3.1. |
| |---|---|---|---| |
| | 1 | **The suite is the app; the team is the boundary; the implementer declares the process set.** A team's instances of sheets, docs, calendar and memoria are one trust domain. | Four boundaries, one per process — memoria could then hold only person-initiated snapshots, stale the moment taken | Paul, 2026-09-03 | |
| | 2 | **Not a doctrine change.** CARLOS apps to date have been single processes, so process and app coincided. Oficina's app is a suite. The doctrine is that the implementer chooses the boundary; this is applying it, not amending it. | Filing it as a family ruling at `carlos/pegamento`, the way home §2.3 filed its divergence | Paul, 2026-09-03 | |
| -| 3 | **One ACL: "is a member of this team".** Nothing enters memoria that is not team-visible. Guests and link viewers get nothing at all. | Replicating each app's grants join; a per-person index; query-time per-person filtering over a superset corpus | Fable ingestion pass, adopted 2026-09-03 | |
| +| 3 | **Team-visibility is a CUSTODY rule; the per-asker check is the AUTHORIZATION rule.** Nothing enters memoria from a sibling unless it is team-visible there, which bounds what memoria holds. Who may *read* it is asked of the owning app, per person, per query. | Draft 1's "one ACL: is a member of this team", which conflated the two and left membership drifting across four tables | Paul, 2026-09-03, after review | |
| | 4 | **Model egress is permitted to one named provider**, per-team opt-in, fixed by the team owner, named on every answer, disclosed in the README's first paragraph. | No egress at all; OpenRouter choosing per question; a local model only | Paul, 2026-09-03 | |
| -| 5 | **The search floor works with AI switched off**, and is the product. The generated answer is an enhancement on top of it. | An answer-first product with search as a fallback | AGENTS.md §Authorship, applied | |
| +| 5 | **The search floor works with AI switched off**, and is the product. **With AI off, nothing leaves the instance at all** — which §3.2 shows is a larger difference than draft 1 realised. | An answer-first product with search as a fallback | AGENTS.md §Authorship, applied | |
| | 6 | **Embeddings and FTS5 postings are content.** They never reach replicated SQLite or the bucket unsealed. | Treating a vector as a derived artefact outside §0's scope, on the grounds that no plaintext bytes appear | Fable trust pass, adopted 2026-09-03 | |
| -| 7 | **Every sibling call is a pull. There is no push.** | An outbox per sibling, mirroring Sheets §9.1.1 | Fable contract pass, adopted 2026-09-03 | |
| -| 8 | **Provenance or deletion.** A derived artefact whose provenance no longer resolves is deleted, not kept. memoria never fine-tunes on the corpus. | Retaining synthesised facts after their sources go | Fable ingestion pass, adopted 2026-09-03 | |
| -| 9 | **Name: `memoria`.** House convention is plain Spanish office nouns. A *memoria* is both a memory and, in Spanish office usage, a written record. | `archivo` (the records room — undersells an app that answers); `cerebro`; `base`; anything English | Paul, 2026-09-03 | |
| +| 7 | **Every SIBLING call is a pull. memoria exposes no sibling-facing surface.** Its ingest endpoint serves external connectors only, which are memoria-owned content, not a sibling push. | An outbox per sibling; and draft 1's §9, which specified the push surface this decision forbids | Fable contract pass; corrected after review | |
| +| 8 | **Provenance or deletion, at `(id, revision)` granularity.** A derived artefact whose provenance stops resolving *or whose source changed* is deleted or regenerated. memoria never fine-tunes. | Draft 1's id-level rule, which left "the Q3 target is X" standing after the source said Y | Fable ingestion pass; corrected after review | |
| +| 9 | **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 | |
| -### 1.1 What the adversarial review killed, and what replaced it |
| +### 1.1 The proposal review, and what it killed |
| -The proposal as first worded — ingest everything from every app, store a |
| -copy, filter per viewer, route questions to whichever model OpenRouter |
| -picks — was reviewed against the specs and rejected on six counts. Four |
| -of them attached to a single choice, *store a per-viewer copy*. They are |
| -recorded here because each is a live trap for anyone who tries to widen |
| -this design later. |
| +Before draft 1 existed, the *proposal* — ingest everything, store a |
| +per-viewer copy, route to whichever model OpenRouter picks — was reviewed |
| +and rejected on six counts. Four attached to one choice, *store a |
| +per-viewer copy*. They remain live traps for anyone widening this design. |
| - **A per-viewer ACL on a synthesised answer is not expressible.** Sheets |
| §4.5's access is a hand-written grants join; memoria cannot evaluate it |
| - and a copy drifts. The failure is worse than escritorio's: not a stale |
| - title in a list, but a paraphrase of a document the reader was removed |
| - from, inside a confident answer, with no line to point at. **Killed by |
| - decision 3** — with one team-wide ACL there is no per-item predicate to |
| - drift. |
| + and a copy drifts. **Killed by decision 3** — memoria now evaluates |
| + nothing, it asks. |
| - **Revoke has a weeks-long tail.** Sheets §9.1.1: the outbox drains on |
| wake and on share mutation, "not on a ticker… a failed upsert may wait |
| - weeks". **Killed by decisions 3 and 7** plus the pre-answer re-check |
| - (§6): the only fact that can change is "still team-visible?", and it is |
| - asked at query time, not cached. |
| -- **The home cannot query memoria.** Home §0 says it "never learns what |
| - is in anything"; §2.2 forbids rendering content from an instance and |
| - forbids holding an authorization model. A generated answer is content. |
| - **Accepted, not worked around** — see §9.1. |
| -- **Blocked on person refs** (home §13 step 4, not started). **Accepted** |
| - — it is the same ship gate Sheets and Docs already sit behind, not a |
| - new one. Everything here builds against stubs. |
| - |
| -Two counts were work rather than kills: the sealed-index design (§7), |
| -which Docs §15.2 had recorded as having no design, and the egress ruling |
| -(§8.3), which decision 4 settles. |
| + weeks". **Killed by decisions 3 and 7**: authorization is asked live, |
| + never cached, never pushed. |
| +- **The home cannot query memoria.** Home §0, home §2.2. **Accepted, not |
| + worked around** — §9.2. |
| +- **Blocked on person refs** (home §13 step 4, not started). **Accepted, |
| + and now load-bearing**: decision 3's per-asker S2 needs a person ref, |
| + so memoria cannot ship before step 4. Same gate as Sheets and Docs, but |
| + memoria depends on it more deeply. |
| + |
| +**A citation note.** That review is recorded in session memory and in |
| +this section; it is **not** in any repo file, so a reader cannot open it. |
| +Draft 1 cited it as though it were a document. Nothing here should rest |
| +on it that is not also argued here. |
| + |
| +### 1.2 What the two reviews changed |
| + |
| +Three structural gaps, each found by both reviewers independently. |
| + |
| +**Gap 1 — membership was four unreconciled tables.** Sheets has its own |
| +`members`, Docs its own `memberships`, memoria its own. The home is |
| +forbidden from being the authority (home §2.2: "Never holds an |
| +authorization model") and no home call carries membership between |
| +instances. Draft 1's §4 claimed a person removed from the team is |
| +"refused at the door with zero latency"; in fact removing them in Sheets |
| +did nothing to memoria, and memoria kept serving them Sheets content. |
| +Draft 1's S2 asked *"is item X team-visible?"*, never *"is this asker |
| +entitled to it?"*. |
| + |
| +The one-ACL ruling did kill per-item drift. It moved the drift onto the |
| +**membership** predicate, and draft 1 did not notice. |
| + |
| +**Fixed by decision 3:** S2 now carries the asker. memoria asks the |
| +owning app *"can person P see items X at revision R?"*, and that app |
| +answers from the grants join it already maintains. Membership and |
| +visibility become one question answered by the only correct evaluator, so |
| +there is nothing left to reconcile. |
| + |
| +**Gap 2 — the search floor bypassed the gate.** `GET /query` returns |
| +passages with or without `answer=1`, and nothing required the |
| +passage-only path to call S2. §0's promise therefore held for generated |
| +answers — the smaller half of the product — and not for search, which |
| +decision 5 calls the product itself. **Fixed by §8.1:** one gate, both |
| +paths. |
| + |
| +**Gap 3 — external sources had no owner to ask.** Drive, Notion, mail and |
| +uploads cannot map their native ACLs onto team-visibility, and draft 1 |
| +gave them no re-check at all; revocation surfaced only on a later |
| +enumeration diff, and memoria runs no timers. Draft 1's claim that it |
| +"forgets exactly when the sources do" was false for exactly the sources |
| +§3.3 says make memoria a bigger target. |
| + |
| +**Fixed by §0's split:** for this content there is nobody to ask, so |
| +memoria owns it outright. Its own grants are authoritative rather than a |
| +copy, and §4.2 says so instead of implying a re-check that cannot exist. |
| + |
| +Beyond the three, both reviews found the same arithmetic errors (§7), the |
| +same contradiction between §9 and decision 7, and the same over-claim in |
| +§6.3. GPT Sol alone found the restore-serves-deleted-content ordering bug |
| +(§7.4) and the missing token-rotation handover (§6.2). Fable alone found |
| +that the README was false about egress volume (§3.2) — the single most |
| +product-damaging finding of the two. |
| --- |
| @@ -88,13 +152,11 @@ getting an answer grounded in the team's own documents is the gap. |
| **What it never does.** |
| -- **Never holds anything not visible to the whole team.** A guest's |
| - individually-granted sheet is not in memoria, and cannot be put there. |
| -- **Never writes to a sibling.** There is no write path in the contract, |
| - in either direction. |
| +- **Never decides who may read sibling content.** It asks (§4.1). |
| +- **Never writes to a sibling**, and exposes no sibling-facing surface. |
| - **Never acts as a person.** It holds no signing key; every assertion's |
| `aud` binds to "app + instance host" (home §5.3). |
| -- **Never crosses a team label.** §6.3. |
| +- **Never crosses a team label** (§6.3). |
| - **Never trains or fine-tunes.** Weights are the one copy that cannot be |
| forgotten (decision 8). |
| - **Never recomputes a number.** A question with an arithmetic answer |
| @@ -122,33 +184,30 @@ verbatim, and 32 tokens is the size of a cell, a title, or a sentence. |
| **Ruled:** chunk text, embeddings and the index graph are all §0 content. |
| An unsealed embedding is a plaintext cell that happens to beat a byte |
| -grep — which is precisely the failure home §5.1 names in its own words, |
| -"turning a guarantee into a comment". |
| +grep — precisely the failure home §5.1 names as "turning a guarantee into |
| +a comment". |
| -**This applies to FTS5 too, and it is the constraint that shapes §7.** |
| -FTS5 stores term postings in plaintext, so the index cannot live in the |
| -replicated SQLite file at all. |
| +**This applies to FTS5 too, and it shapes §7.** FTS5 stores term postings |
| +in plaintext, so the index cannot live in the replicated SQLite file. |
| **Searching sealed vectors directly is not available.** Encrypted-ANN |
| -schemes are research-grade and orders of magnitude slower. Rejected. The |
| -working shape is the one Sheets §4.4 already uses for its handle: seal |
| -the serialised index as one blob, decrypt into a refcounted in-memory |
| -handle on wake, evict on idle. The graph is sealed *with* the vectors, |
| -because neighbour edges alone reveal which passages resemble each other. |
| +schemes are research-grade and orders of magnitude slower. The working |
| +shape is the one Sheets §4.4 uses for its handle: seal the serialised |
| +index as one blob, decrypt into a refcounted in-memory handle on wake, |
| +evict on idle. The graph is sealed *with* the vectors, because neighbour |
| +edges alone reveal which passages resemble each other. |
| ### 3.2 The leak list |
| **From the bucket:** document and chunk counts; sealed blob sizes; source |
| -app and coordinates per chunk, which stay plaintext exactly as Sheets |
| -§2.2 keeps coordinates; ingest and update timestamps, therefore which |
| -sources are busy and when; embedding dimension, therefore the model. |
| +app and coordinates per chunk, plaintext exactly as Sheets §2.2 keeps |
| +coordinates; ingest and update timestamps, therefore which sources are |
| +busy and when; embedding dimension, therefore the model. |
| **From a live host — the largest entry, and new.** A compromised memoria |
| -host reaches the team's corpus across every app at once. Home §2's |
| -"blast radius is bounded by what a service holds" is now the argument |
| -*against* memoria, because memoria holds the most. Decision 1 accepts |
| -this knowingly; §3.3 works through exactly how it compares to Sheets, |
| -and §3.5 lists what shrinks it. |
| +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; |
| +§3.5 lists what shrinks it. |
| **From logs, errors and metrics:** a question is content — *"what does |
| Bob earn"* is a cell value phrased as a query. Sheets AGENTS.md's rule |
| @@ -162,451 +221,639 @@ which team is awake. |
| **From a sibling:** bodies transit the router in plaintext on every S3 |
| fetch (§6.1). |
| -**From the provider, when AI is on:** the retrieved top-k, roughly 10 |
| -chunks × 500 tokens = 5k tokens of team content per question. At 100 |
| -questions a day that is half a million tokens a day of the team's |
| -*most-asked-about* content. Over months, the frequently-consulted part of |
| -the corpus has left in full. This is the entry most likely to be |
| -understated, so it is stated in volume rather than in principle. |
| +**From the provider, when AI is on — and this is bigger than draft 1 |
| +said.** Draft 1 stated only query-time egress: ~10 chunks × 500 tokens |
| +per question, so that over months "the frequently-consulted part of the |
| +corpus has left in full". That understated it by the corpus. |
| + |
| +**Embeddings come from a hosted API (§7.2). So with AI on, every chunk of |
| +every document leaves at ingest, on day one.** §8.6 prices it: 36M tokens |
| +for a 30k-document corpus. The frequently-consulted part leaving is the |
| +*second* leak, not the first. |
| + |
| +Two consequences the spec carries rather than buries: |
| + |
| +- **A chunk later downgraded or deleted has already left.** Decision 8's |
| + provenance-or-deletion governs memoria's copy. It cannot reach the |
| + provider's. |
| +- **Decision 5's AI-off floor is therefore a strong position, not a |
| + degraded one.** With AI off, BM25 over sealed chunks, nothing leaves |
| + the instance — not at query time and not at ingest. That is why §11 |
| + builds it first. |
| ### 3.3 memoria is not less sealed than Sheets. It is a bigger target. |
| -Asked directly by Paul, 2026-09-03, and worth answering in the spec |
| -because every future reader will ask it too: *if memoria's data is sealed |
| -too, why is it more exposed?* |
| +Asked directly by Paul, 2026-09-03, and answered here because every |
| +future reader will ask it. |
| **At rest there is no difference.** memoria seals under its own DEK with |
| the same construction as Sheets, and the same plaintext-grep gate runs |
| -against its bucket from the first commit. Nothing here is weaker. |
| +against its bucket from the first commit. |
| -**An earlier draft of this section argued the difference was RAM** — that |
| -a Sheets host holds decrypted handles only for *open* sheets (§4.4) while |
| -memoria holds everything. That argument is mostly wrong and is recorded |
| -here so it is not reintroduced. A compromised Sheets host holds the DEK, |
| -so it can decrypt its entire bucket on demand; the open-handle |
| -distinction is the difference between reading memory and running a |
| -decrypt loop, which is no barrier to anyone with code execution. |
| +**An earlier draft argued the difference was RAM** — that a Sheets host |
| +holds decrypted handles only for *open* sheets (Sheets §4.4) while |
| +memoria holds everything. That is mostly wrong and is recorded so it is not |
| +reintroduced. A compromised Sheets host holds the DEK and can decrypt its |
| +entire bucket on demand; the open-handle distinction is the difference |
| +between reading memory and running a decrypt loop, which is no barrier to |
| +anyone with code execution. |
| **The real difference is that the corpus is a union.** Compromise Sheets |
| and an attacker gets that team's sheets. Compromise memoria and they get |
| sheets *and* docs *and* calendar *and* Notion *and* uploads *and* |
| -forwarded mail. Same lock, more in the room. That is the whole of it, and |
| -it is enough. |
| - |
| -**Two refinements, and the first favours memoria.** |
| - |
| -- **Per app, memoria holds strictly less than the app does.** Decision 3 |
| - keeps individually-granted content out entirely, so memoria's copy of |
| - Sheets is a strict subset — team-visible items only. The union is |
| - larger; every part of it is smaller. An attacker who wants a guest's |
| - individually-granted sheet gains nothing by taking memoria. |
| -- **The RAM point is real, but narrow, and worth keeping for that.** |
| - memoria's index must be fully decrypted to be searchable at all (§3.1: |
| - BM25 and cosine do not run over sealed bytes), so its whole corpus sits |
| - in cleartext memory as routine operation, where Sheets decrypts per |
| - sheet and evicts on idle. Against RCE this is marginal. Against |
| - **partial memory disclosure** it is the entire difference: a crash |
| - dump, a core file, swap, a hypervisor snapshot or a Heartbleed-class |
| - over-read leaks what is *in memory*, not what the key could reach. For |
| - that threat class alone, memoria is meaningfully worse than Sheets, and |
| - §3.5's idle eviction is the mitigation that matters most. |
| - |
| -**What memoria does not do is weaken its sources.** It holds no sibling's |
| -DEK. Compromising memoria yields a copy of team-visible content, not the |
| -ability to decrypt Sheets' bucket, and not a write path (§6.4). It does |
| -yield the sibling read tokens, so an attacker keeps reading team-visible |
| -content going forward — but that is the same content memoria already |
| -holds, so it widens the window rather than the blast radius. |
| +forwarded mail. Same lock, more in the room. |
| + |
| +**Three refinements. The first favours memoria; the third draft 1 got |
| +backwards.** |
| + |
| +- **Per sibling app, memoria holds strictly less than the app does.** |
| + Decision 3's custody rule keeps individually-granted content out, so |
| + memoria's copy of Sheets is a strict subset. The union is larger; every |
| + sibling part of it is smaller. |
| +- **The RAM point is real but narrow.** memoria's index must be fully |
| + decrypted to be searchable (§3.1), so its corpus sits in cleartext |
| + memory as routine operation. Against RCE this is marginal. It bites for |
| + **bounded memory over-reads** — a Heartbleed-class bug reads live |
| + memory, not the key's reach. Draft 1 also listed core files, swap and |
| + hypervisor snapshots; that was too broad, since those capture the DEK |
| + too and reduce to the decrypt loop. Narrowed on review. |
| +- **Connector credentials are blast radius, not window, and draft 1 said |
| + the opposite.** Draft 1 claimed a compromised memoria "yields the |
| + sibling read tokens… the same content memoria already holds, so it |
| + widens the window rather than the blast radius". True of *sibling* |
| + tokens. False of §5.1's connector credentials: a Drive OAuth grant |
| + reaches the authorising account's whole Drive, and a Gmail label |
| + connector needs mailbox-wide read scope. Neither is bounded by what |
| + memoria ingested. **A compromised memoria reaches further than memoria |
| + ever held.** §3.5 mitigates it. |
| + |
| +**What memoria does not do is weaken its siblings' sealing.** It holds no |
| +sibling's DEK, gains no write path (§6.4), and cannot read a sibling's |
| +individually-granted content, because S3 404s it. |
| ### 3.4 A citation is exactly as trustworthy as the app it cites |
| A compromised sibling has no inbound path to memoria and holds no memoria |
| token, so it cannot read or write. What it *can* do is lie: serve |
| -fabricated content on S3, answer "visible" on S2 for a document that is |
| -not, withhold tombstones. memoria's re-check asks the same lying app. |
| +fabricated content on S3, answer `VISIBLE` for a person not entitled, |
| +withhold tombstones. memoria's re-check asks the same lying app. |
| There is no mitigation, only disclosure. The README says this sentence. |
| ### 3.5 Cheap mitigations |
| -- **memoria hibernates.** Home §2.1 made the home the one exception on the |
| - estate; memoria is not another. It reloads its sealed index on wake. |
| +- **memoria hibernates.** Home §2.1 made the home the estate's one |
| + exception; memoria is not another. |
| +- **Idle eviction** of the decrypted handle — the mitigation that matters |
| + most for §3.3's bounded-over-read class. |
| - **Per-source subsets**, owner-selected, default all. |
| -- **A published-only mode**, in which the source app carries an explicit |
| - "offer to memoria" mark that *it* evaluates — so the one-ACL rule |
| - holds and memoria's corpus is a strict subset of team-visible. |
| +- **A published-only mode**, in which the sibling carries an explicit |
| + "offer to memoria" mark that *it* evaluates. |
| - **Refusing sources by kind.** Calendar bodies off by default. |
| -- **Idle eviction** of the decrypted handle. |
| +- **Least-privilege connector credentials — now a requirement, not a |
| + nicety** (§3.3). Prefer per-folder or per-label scopes over |
| + account-wide ones; prefer a dedicated service identity over a person's |
| + OAuth grant; store them under a distinct HKDF purpose key so a |
| + chunk-store disclosure does not yield them. |
| ### 3.6 The honest claim |
| -The README's first paragraph, in substance: |
| +The README's first paragraph, in substance. Rewritten after review: the |
| +draft-1 version was false about egress volume, which PRODUCT.md principle |
| +1 treats as a defect rather than a wording choice. |
| > memoria's index and passages are sealed at rest under this team's own |
| > key, the same way your sheets and documents are. Nothing readable |
| > reaches storage. What is different about memoria is not the lock, it is |
| > what is behind it: memoria gathers your team's work from every app into |
| > one place, so whoever breaks into it gets all of it at once rather than |
| -> one app's worth. That is the point of it and the risk of it. When the |
| -> assistant is switched on, the passages used to answer a question are |
| -> sent to the provider named on that answer, who can read them; we rely |
| -> on their retention terms and cannot verify them. We never claim we |
| -> can't read your data, and with the assistant on we never claim nobody |
| -> else can either. |
| +> one app's worth. That is the point of it and the risk of it. |
| +> |
| +> With the assistant switched off, nothing leaves this instance — |
| +> memoria searches your content where it sits. |
| +> |
| +> **Switching the assistant on sends your content to the provider named |
| +> in your settings — not just the passages behind an answer, but every |
| +> document in the corpus, once, when it is indexed.** That is how the |
| +> search understands meaning rather than matching words. The provider can |
| +> read what it receives; we rely on their retention terms and cannot |
| +> verify them, and content sent before you deleted something cannot be |
| +> recalled. We never claim we can't read your data, and with the |
| +> assistant on we never claim nobody else can either. |
| --- |
| -## 4. The one ACL |
| +## 4. Authorization |
| + |
| +### 4.1 Sibling content: memoria asks, never decides |
| + |
| +Every path that would speak a passage originating in a sibling app calls |
| +**S2** first, carrying the asker: |
| + |
| +``` |
| +S2(person_ref, [(item_id, revision), …]) |
| + → per item: VISIBLE | NOT_VISIBLE | UNKNOWN |
| +``` |
| + |
| +The sibling answers from the grants join it already maintains — Sheets |
| +§4.5, "rows this team can see, plus rows granted to me individually, |
| +minus rows I have been removed from". memoria evaluates none of it. |
| + |
| +**Three-valued, deliberately.** `UNKNOWN` is what a hibernating or broken |
| +sibling returns, and it is *not* `NOT_VISIBLE`. Collapsing them is how a |
| +suite produces "a plausible empty list, not an error" — the failure shape |
| +sheets-core AGENTS.md names as the worst available. `UNKNOWN` triggers |
| +loud withholding (§8.1); `NOT_VISIBLE` silently omits, correctly. |
| -**A person may use memoria if they are a member of this team.** Evaluated |
| -from memoria's own members table on every request, exactly as every other |
| -instance does (home §5.5: the instance "decides capability entirely from |
| -its own grants"). No guests. No link viewers. No anonymous read. |
| +**This subsumes membership.** There is no separate "is P a member?" |
| +question and no membership synchronisation, because a non-member's items |
| +all come back `NOT_VISIBLE` from the app that knows. The four |
| +unreconciled tables of §1.2 are simply not consulted. |
| -**A document may be in memoria if it is visible to the whole team in its |
| -source app.** One predicate, owned and evaluated by the source app, never |
| -by memoria. |
| +**Revision matters.** A document that stays team-visible but whose newer |
| +revision removed a sentence must not have its stale chunk spoken: a |
| +revision behind the sibling's current one answers `NOT_VISIBLE`, and |
| +memoria re-fetches on the next pull. |
| -The consequence worth stating plainly: **memoria cannot express a |
| -per-item ACL, so it cannot drift into a wrong one.** This is the property |
| -that the escritorio review's third count says a suite index cannot have, |
| -bought by giving up individually-granted content entirely. |
| +**Batch cap: 256 pairs per call**, multiple calls permitted. Draft 1's 64 |
| +was tied to answer top-k and did not survive paginated search or |
| +derived-artefact expansion (§5.2). The number's reason: one page of |
| +search results (50) plus the provenance fan-out of the derived artefacts |
| +on it — measured before the constant is committed. |
| -A person removed from the team is refused at the door with zero latency. |
| -A document narrowed from team-visible to a subset is withheld at the next |
| -query it would have appeared in, and purged on the next feed pull. |
| +### 4.2 memoria-owned content: memoria is the app of record |
| + |
| +Notion, Drive, forwarded mail and uploads have no sibling to ask. For |
| +this content **memoria's own members and grants are authoritative** — not |
| +a copy, so there is nothing to drift from. |
| + |
| +This is the honest resolution of gap 3, and it costs something that must |
| +be said plainly rather than implied: **revoking a Notion page from the |
| +integration does not revoke it in memoria.** The content was published |
| +into memoria and memoria now owns it; removing it is an act performed *in |
| +memoria*. §5.1's enumeration diff is a convenience that eventually |
| +notices upstream deletions, not an authorization mechanism, and §3.6's |
| +claim does not pretend otherwise. |
| + |
| +### 4.3 Using memoria at all |
| + |
| +A person reaches memoria with a home assertion whose `aud` is memoria's |
| +host, and memoria decides admission from its own grants (home §5.5). |
| +Admission is not authorization: an admitted person still sees only what |
| +§4.1 and §4.2 allow. No guests, no anonymous link viewers. |
| --- |
| ## 5. Ingestion |
| -**Ingestion is an authorisation event, never a sweep.** Content enters |
| -two ways. |
| +**Ingestion is an authorisation event, never a sweep.** |
| -**From a sibling app (§6):** the change feed offers items already marked |
| -team-visible. No human act is needed because the team-visibility mark |
| -*is* the authorisation, and the source app owns it. |
| +**From a sibling (§6):** the change feed offers items the sibling marks |
| +team-visible. That mark is the *custody* gate of decision 3 — it bounds |
| +what memoria holds. It is not the authorization gate; §4.1 is. |
| **From outside the suite:** an admin configures a connector, and the |
| -sharing act at the far end is the publish — a Notion space shared with |
| -the integration, a Drive folder, a mail label. |
| +sharing act at the far end is the publish. |
| ### 5.1 What a connector must supply |
| -Five things: a stable external id, a change feed (or an enumeration cheap |
| -enough to diff), a mapping onto memoria's single ACL, a content |
| -extractor, and a delete signal. |
| +A stable external id, a change feed (or an enumeration cheap enough to |
| +diff), a mapping onto memoria's custody rule, an extractor, and a delete |
| +signal. |
| -| Source | Id | Feed | ACL maps | Extract | Delete | Verdict | |
| +| Source | Id | Feed | Custody maps | Extract | Delete | Verdict | |
| |---|---|---|---|---|---|---| |
| -| Oficina siblings | ✅ | ✅ | ✅ team-visible mark | ✅ | ✅ tombstone | Full | |
| -| Google Drive | ✅ | ✅ changes API | ❌ per-file | ✅ | ✅ | Publish-gated | |
| -| Notion | ✅ | ⚠️ `last_edited_time` polling | ❌ | ✅ | ❌ diff only | Publish-gated | |
| -| Email | ⚠️ Message-ID | ✅ UID / history id | ❌ **fundamentally** | ✅ | ⚠️ Gmail only | Forward-only | |
| -| Files | content hash | ❌ | ❌ | ✅ | ❌ | Upload only | |
| +| Oficina siblings | ✅ | ✅ | ✅ team-visible mark | ✅ | ✅ tombstone | Full; §4.1 governs reads | |
| +| Google Drive | ✅ | ✅ changes API | ❌ per-file | ✅ | ✅ | Publish-gated; §4.2 governs | |
| +| Notion | ✅ | ⚠️ `last_edited_time` polling | ❌ | ✅ | ❌ diff only | Publish-gated; §4.2 | |
| +| Email | ⚠️ Message-ID | ✅ UID / history id | ❌ **fundamentally** | ✅ | ⚠️ Gmail only | Forward-only; §4.2 | |
| +| Files | content hash | ❌ | ❌ | ✅ | ❌ | Upload only; §4.2 | |
| + |
| +**Only the siblings supply all five**, and only they get §4.1's live |
| +re-check. Everything else is memoria-owned by §4.2. |
| -**Only the siblings supply all five.** Every external connector is |
| -publish-gated with deletion by enumeration diff. |
| +**Email cannot be swept.** A thread's audience is the three people on it; |
| +the team is twenty. Mail enters by forward-or-label only. |
| -**Email cannot be swept, and this is not a limitation to design around.** |
| -A thread's audience is the three people on it; the team is twenty. A |
| -connector that pulls a mailbox publishes to twenty people what three |
| -received. Mail enters by forward-or-label only — a person moves a thread |
| -into memoria and it becomes the team's, like a document they wrote. |
| +**An enumeration diff must distinguish an authoritative empty result from |
| +a failure** before deleting anything. A timed-out, rate-limited, |
| +partially-paginated or credential-expired enumeration returns *no |
| +information*, not *no documents*; treating the second as the first purges |
| +the corpus silently. Gate 7. |
| ### 5.2 Deletion, and the hard case |
| Chunks and vectors are rows under memoria's DEK; they go on the tombstone |
| -or the next diff. |
| +or the next diff. §7.4 covers the snapshot ordering that makes a deletion |
| +survive a restore. |
| The hard case is derived content, because a fact outlives its source. |
| -Once memoria has synthesised *"the Q3 target is X"* from a document it can |
| -no longer see, the sentence survives the ACL that governed it. |
| -**Ruled (decision 8): every derived artefact carries provenance to source |
| -ids, and one whose provenance no longer resolves is deleted, not kept.** A |
| -summary of three documents with one gone is regenerated from two, or |
| -removed. "The Q3 target is X" with no live source does not exist. Cached |
| -answers are content too: provenance-stamped, invalidated when any source |
| -changes. |
| +**Ruled (decision 8): every derived artefact carries provenance as |
| +`(source_id, revision)` pairs, and is deleted or regenerated when any of |
| +them stops resolving *or moves to a new revision*.** Draft 1's rule was |
| +id-level, which left "the Q3 target is X" standing after the source |
| +changed to Y — provenance that resolves and is wrong. |
| + |
| +**A derived artefact expands to its full provenance before §4.1 runs.** |
| +Speaking a summary means asking S2 about *every* source behind it, and |
| +**every one must return `VISIBLE`**. One `NOT_VISIBLE` withholds the |
| +artefact; one `UNKNOWN` withholds it loudly. Provenance larger than the |
| +batch cap spans several calls; all must pass. Without this a summary is a |
| +laundering channel for a single downgraded input. |
| -The corollary is decision 8's second half. **memoria never fine-tunes on |
| -the corpus**, because weights are the one copy that cannot be forgotten. |
| -Retrieval at query time only. |
| +**Where derived artefacts live:** memoria's own store, sealed like any |
| +chunk, indexed and searchable — and therefore subject to §4.1 through |
| +their provenance, never on their own account. They exist only with AI on, |
| +and their regeneration is a model call whose cost §8.6 counts. |
| + |
| +**memoria never fine-tunes**, because weights are the one copy that |
| +cannot be forgotten. |
| ### 5.3 What "continuously evolving memory" honestly means |
| -Three readings. Re-index on change — a search index, honestly named. |
| -Derived notes regenerated when their sources change — a materialised view |
| -with provenance. Updated weights — never, per above. |
| +Re-index on change — a search index, honestly named. Derived notes |
| +regenerated when sources change — a materialised view with provenance. |
| +Updated weights — never. |
| -**The smallest honest version is the first two:** a team-visible corpus |
| -re-indexed off change feeds, with provenance-stamped answers that expire |
| -when a source moves. It is a cache over sources, and it forgets exactly |
| -when the sources do. What it cannot honestly be is a system that knows |
| -things nobody can produce a document for. |
| +**The smallest honest version is the first two.** It is a cache over |
| +sources. For sibling content it forgets when the sources do, because |
| +§4.1 asks them. **For memoria-owned content it forgets when someone tells |
| +it to** (§4.2); draft 1's claim that it "forgets exactly when the sources |
| +do" was false for that half. |
| --- |
| ## 6. The sibling contract |
| Decision 1 makes cross-app pull legal. Nothing yet makes it possible: |
| -every home contract call is instance→home or person→home, §6 rejected a |
| -re-announce sweep because it "has no channel to reach instances", and |
| -`aud` binds every assertion to one host. This section is the missing |
| -channel. |
| +every home contract call is instance→home or person→home, home §6 |
| +rejected a re-announce sweep because it "has no channel to reach |
| +instances", and `aud` binds every assertion to one host. |
| ### 6.1 The calls |
| -All are memoria → sibling, and all are pulls. **Push is rejected on three |
| +All are memoria → sibling, all pulls. **Push is rejected on three |
| grounds:** a sibling outbox drains on wake and share mutation, not on a |
| -ticker (Sheets §9.1.1), which is adequate for a hint and useless for |
| -content; push would wake a hibernating memoria on every keystroke in three |
| -apps; and push inverts custody, since each sibling would then hold a |
| -memoria token and memoria would grow an inbound surface it does not have. |
| - |
| -Pull is safe because of the pre-answer re-check: **a stale index costs a |
| -miss, never a leak.** |
| +ticker (Sheets §9.1.1), adequate for a hint and useless for content; push |
| +would wake a hibernating memoria on every keystroke in three apps; and it |
| +inverts custody, since each sibling would hold a memoria token and |
| +memoria would grow an inbound surface it does not have. |
| -memoria pulls on its own wake and on each question. It runs no timers. A |
| -quiet team's first question after a fortnight pays the catch-up, and the |
| -page says so. |
| +memoria pulls on its own wake and on each question. It runs no timers. |
| -| # | Call | Direction | Notes | |
| -|---|---|---|---| |
| -| S1 | Change feed | pull | Cursor is an opaque position in the sibling's own event log, issued by the sibling, stored per consumer. A row is `(item id, revision, visible-to-team, gone, touched_at)` — **no title, no content**, so the feed passes the plaintext-grep gate unaided. Retention is a **per-consumer TTL**: home §9.1's generalisation is that "any 'prune when all consumers have caught up' is unbounded in a hibernating fleet". A consumer past its TTL gets a full re-announce, not a delta. A restore-from-backup cursor rewind takes the same path. | |
| -| S2 | Visibility re-check | pull, batched | Ids → yes/no each, evaluated by the sibling's own grants join, never from the feed. Body cap **64 ids**: the retrieval top-k ceiling, so one answer's candidate set fits one call. | |
| -| S3 | Content fetch | pull | `(id, revision)` → plaintext body over TLS into memoria's RAM only. The sibling re-runs its grants join at fetch time and **404s, never 403s**, so a guest-granted item is indistinguishable from a missing one. | |
| -| S4 | Tombstone | *a feed row* | `gone` covers deletion **and** downgrade from team-visible to individually-granted, indistinguishably. Deliberate: telling them apart is a fact about a document memoria is no longer allowed to hold. | |
| -| S5 | Consumer expiry | *no call* | TTL expiry is the decommission verb. Home §9.1 needed an explicit one; here the TTL is it. | |
| - |
| -**When a sibling does not answer S2, memoria withholds loudly:** *"Sheets |
| -did not answer; 7 results withheld."* Never a silently narrower answer. |
| -The house rule is that a plausible empty list is the worst available |
| -failure shape, and this is the place memoria would produce one. |
| - |
| -### 6.2 Authentication |
| - |
| -**Ruled: per-consumer bearer tokens, minted by the operator drain, |
| -delivered as platform config at provisioning.** Home §9 already works this |
| -way — "a fresh instance gets its token as platform config and its |
| -allowlist entry from the drain, in the step that creates it". The drain |
| -mints three tokens for memoria and writes each sibling's `consumers` row |
| -(origin, hashed token, cursor, TTL, epoch). Rotation is a drain verb, and |
| -it exists before launch rather than after the first leak. |
| - |
| -Three alternatives rejected: |
| - |
| -- **The home issues the token.** Rejected on home §7's load-bearing safety |
| - fact: "the home holds no provisioning credential", so an RCE on an |
| - internet-facing sign-in surface reaching instance creation is impossible |
| - by construction. A home that mints corpus-read credentials breaks that, |
| - and breaches §2.2 the moment it decides *which* origin gets one. |
| -- **A team-scoped shared secret.** Cannot rotate per consumer; discards |
| - "consumer identity is its origin". |
| -- **Mutual TLS.** Impossible on this platform: TLS terminates at Carloku's |
| - router under a wildcard certificate per app zone (home §4), so the app |
| - never sees a client certificate. |
| +| # | Call | Notes | |
| +|---|---|---| |
| +| S1 | Change feed | Cursor is an opaque position in the sibling's event log, issued by the sibling, stored per consumer. A row is `(item_id, revision, gone, touched_at)` — **no title, no content, no visibility flag** — so it passes the plaintext-grep gate unaided. Retention is a **per-consumer TTL** (home §9.1: "any 'prune when all consumers have caught up' is unbounded in a hibernating fleet"). Past TTL, or a restore-rewound cursor, takes a full re-announce. | |
| +| S2 | Authorization re-check | `(person_ref, [(item_id, revision)]) → VISIBLE / NOT_VISIBLE / UNKNOWN`. §4.1 specifies it. Cap 256 pairs. | |
| +| S3 | Content fetch | `(id, revision)` → plaintext body over TLS into memoria's RAM only. The sibling re-runs its grants join at fetch time and **404s, never 403s**. | |
| +| S4 | Tombstone | *a feed row.* `gone` covers deletion and departure from team-visibility, indistinguishably. | |
| +| S5 | Consumer expiry | *no call.* TTL expiry is the decommission verb. | |
| + |
| +**Draft 1's S1 row carried a `visible-to-team` field, and that was a |
| +leak.** With it, a `visible=false, gone=false` row tells memoria that a |
| +guest-only item exists, at what revision, and when it was touched — |
| +exactly what S3's "404s, never 403s" was designed to hide, and unnamed in |
| +the leak list. Removed: departure from team-visibility is simply `gone`. |
| + |
| +**"A stale index costs a miss, never a leak" — narrowed.** Draft 1 stated |
| +it unconditionally and it was false for retraction-by-edit. It holds now |
| +only because S2 carries the revision (§4.1): a stale chunk fails its |
| +re-check. The two clauses must travel together. |
| + |
| +### 6.2 Authentication, and rotation |
| + |
| +**Per-consumer bearer tokens, minted by the operator drain, delivered as |
| +platform config at provisioning.** Home §9 already works this way. The |
| +drain mints memoria's tokens and writes each sibling's `consumers` row. |
| +The home never sees a sibling token. |
| + |
| +Three alternatives rejected: **the home issues the token** (breaks home |
| +§7's "the home holds no provisioning credential", and breaches home §2.2 the |
| +moment it decides which origin gets one); **a team-scoped shared secret** |
| +(cannot rotate per consumer); **mutual TLS** (TLS terminates at Carloku's |
| +router under a wildcard certificate per app zone, home §4, so the app |
| +never sees a client certificate). |
| + |
| +**Rotation is a three-phase handover, and draft 1 specified none.** A |
| +single-token row cannot rotate without a window in which one side is |
| +wrong: update the sibling first and the old token breaks; update memoria |
| +first and it sends one the sibling will not accept. So a `consumers` row |
| +holds **up to two hashed tokens, each with an epoch**, and the drain: |
| + |
| +1. **Expands acceptance** — writes the new token alongside the old at |
| + every sibling. Both accepted. |
| +2. **Switches the sender** — delivers the new token to memoria as |
| + platform config. |
| +3. **Contracts acceptance** — retires the old token at every sibling, |
| + only after memoria is observed using the new one. |
| + |
| +Each phase is idempotent and re-runnable, so a partial delivery across |
| +three siblings is recovered by repeating the phase. Emergency revocation |
| +skips to phase 3 and accepts the outage. |
| + |
| +**The mutable `(cursor, TTL, epoch)` state is created lazily by the |
| +sibling on first contact.** Platform config delivers the token, not the |
| +row: home §6 is explicit that no channel exists to push one. |
| ### 6.3 What stops team X reaching team Y |
| Team identity is the hostname label, per home ruling 16: |
| -`<team>.<app>.oficina.works`. Cross-team access fails three independent |
| -ways, each a test rather than a policy: |
| - |
| -1. **No credential.** Team X's memoria holds tokens whose hashes live only |
| - in team X's siblings. |
| -2. **No allowlist row.** A sibling's `consumers` table holds exactly one |
| - origin, written once by the drain. |
| -3. **No dial.** memoria's client refuses any host whose `<team>` label |
| - differs from its own, test-enforced against a fixture of hostile |
| - hostnames. |
| - |
| -Same-team membership is therefore *provenance*, not a runtime query: the |
| -drain provisioned the hosts together and nothing re-derives membership at |
| -runtime. **The home is not on this path at all** — no instance ever asks |
| -it "may memoria read this" — which is how §2.2 survives decision 1 |
| -intact. |
| +`<team>.<app>.oficina.works`. |
| + |
| +**Two barriers, not the three draft 1 claimed.** Both reviewers caught |
| +this and they were right: |
| + |
| +1. **The sibling's `consumers` row** — a credential *and* an origin |
| + allowlist, but one row read two ways. A bearer token is the origin |
| + claim; nothing verifies origin independently. One drain mis-write |
| + defeats both. Counting them separately overstated the defence. |
| +2. **memoria's client refuses any host whose `<team>` label differs from |
| + its own** — genuinely independent, test-enforced against a fixture of |
| + hostile hostnames, **and it must refuse to follow redirects**, which |
| + draft 1's fixture would not have exercised. |
| + |
| +**Barrier 2 is not a boundary against the §3.3 threat model.** An |
| +attacker with code execution on memoria bypasses memoria's own client |
| +trivially. It is a real guard against accidental cross-team dialling and |
| +against a mis-provisioned host, and that is what it is claimed as. |
| + |
| +Same-team membership is *provenance*, not a runtime query: the drain |
| +provisioned the hosts together. **The home is not on this path at all**, |
| +which is how home §2.2 survives decision 1. |
| ### 6.4 What decision 1 does not license |
| -Any hop across a `<team>` label. Any instance presenting an assertion — |
| -assertions belong to persons. Any write. Any item outside the team-wide |
| -grant. The home in the loop. Content leaving the four hosts, except §8.3's |
| -named provider, which is a disclosed exception and not a sibling. |
| +Any hop across a `<team>` label. Any instance presenting an assertion. |
| +Any write. Any sibling item outside the team-visible custody rule. The |
| +home in the loop. Content leaving the four hosts, except §8.3's named |
| +provider, which is a disclosed exception and not a sibling. |
| --- |
| ## 7. The index |
| -The design Docs §15.2 recorded as missing: *"An FTS5 index is plaintext in |
| -SQLite and would fail our own gate; decrypting the corpus per query is |
| -O(corpus). Full text waits for a design, not for a decision."* |
| +The design Docs §15 item 2 recorded as missing: *"An FTS5 index is |
| +plaintext in SQLite and would fail our own gate; decrypting the corpus |
| +per query is O(corpus)."* |
| **Hybrid BM25 + dense vectors, fused by reciprocal rank, both sealed at |
| -rest and both resident in RAM while awake.** |
| +rest and resident in RAM while awake.** |
| + |
| +### 7.1 FTS5 is available cgo-free |
| + |
| +**Verified independently by both reviewers**, each building a static |
| +binary against the suite's pinned `modernc.org/sqlite v1.55.0`: |
| +`CGO_ENABLED=0` created `fts5(body, tokenize='porter unicode61')`, ran |
| +`bm25()`, reported SQLite 3.53.3, and `pragma_compile_options` contained |
| +`ENABLE_FTS5`. |
| + |
| +Per §3.1 its postings are plaintext, so the FTS table lives on |
| +non-replicated scratch or in memory and is snapshotted to the bucket as a |
| +sealed blob. **Whether CARLOS provides non-replicated scratch is |
| +unverified — §13.** |
| + |
| +### 7.2 The arithmetic, derived |
| -**FTS5 is available cgo-free.** Verified this session against the suite's |
| -pinned `modernc.org/sqlite v1.55.0`: a `CGO_ENABLED=0` binary created |
| -`fts5(body, tokenize='porter unicode61')`, ran `bm25()`, and reported |
| -SQLite 3.53.3. So BM25 costs nothing new — but per §3.1 its postings are |
| -plaintext, so the FTS table lives on non-replicated scratch or in memory |
| -and is snapshotted to the bucket as a sealed blob. **Whether CARLOS |
| -provides non-replicated scratch is unverified — §13.** |
| +**One corpus assumption drives every number below.** Draft 1 used 90k |
| +chunks for memory and 60M tokens for cost, which are inconsistent; both |
| +reviewers caught it. |
| -**Vectors: brute-force cosine over an int8 matrix. No ANN index.** |
| +> **30,000 documents × 3 chunks × 400 tokens = 90,000 chunks = 36M |
| +> tokens**, ≈1.6 KB of text per chunk, ≈144 MB of text. |
| -| chunks | 768-dim f32 | 1536-dim f32 | per-query, one core | |
| +**Vectors.** 512-dim Matryoshka embeddings (OpenAI's `-3` models accept a |
| +`dimensions` parameter), stored **int8-quantised**: |
| + |
| +| chunks | int8 @512 | f32 @512 | f32 @768 | |
| |---|---|---|---| |
| -| 10k | 31 MB | 61 MB | 4–15 ms | |
| -| 100k | 307 MB | 614 MB | 40–160 ms | |
| -| 1M | 3.1 GB | 6.1 GB | 0.4–1.6 s, and the RAM is not there | |
| - |
| -At dogfooding scale — 30k documents at roughly 3 chunks of 400 tokens — |
| -the corpus is about 90k chunks. Requesting 512-dim Matryoshka embeddings |
| -gives 90k × 512 × 4 B = 184 MB, or **46 MB int8-quantised**, inside the |
| -brute-force band with room to spare. An approximate index at this size |
| -buys nothing and adds a persistence format. |
| - |
| -**Trigger for ANN: 250k chunks.** `coder/hnsw` is pure Go and available |
| -when it fires. |
| - |
| -**Cold start: snapshot, never rebuild.** Sealed snapshots at 90k chunks |
| -are roughly 60 MB of FTS index and 46 MB of vectors. Download plus |
| -AES-GCM decryption puts that at **1–3 s over process start**. Rebuilding |
| -FTS5 from sealed chunks instead would be 10–30 s. Budget ≤5 s, which caps |
| -the sealed load at about 250 MB and therefore **~100k chunks** — the same |
| -ceiling the brute-force arithmetic gives, which is tidy. |
| - |
| -Load order: FTS snapshot first, then serve; vectors in the background. A |
| -query landing before the vectors are ready is answered lexically, and the |
| -page's footer says so in one line. |
| - |
| -**Embeddings come from a hosted API**, because there is no mature pure-Go |
| -ONNX runtime and `onnxruntime-go` needs a shared library. This makes |
| -embedding part of the AI-on layer, not the floor — §8.1 says what the |
| -floor does without it. |
| +| 10k | 5 MB | 20 MB | 31 MB | |
| +| 90k | **46 MB** | 184 MB | 276 MB | |
| +| 250k | 128 MB | 512 MB | 768 MB | |
| + |
| +Draft 1's table mixed an int8 design with f32 columns without saying so. |
| + |
| +**Query latency, derived.** A brute-force pass is `2 × N × D` flops. On |
| +one core of this machine, measured at **3.2 GFLOP/s** (pure Go, no SIMD): |
| + |
| +| chunks × dims | flops | latency | |
| +|---|---|---| |
| +| 10k × 512 | 10.2 M | 3 ms | |
| +| 90k × 512 | 92 M | 29 ms | |
| +| 100k × 768 | 154 M | **48 ms — measured, matches** | |
| +| 250k × 512 | 256 M | 80 ms | |
| +| 1M × 512 | 1.02 G | 320 ms | |
| + |
| +**FTS index size.** Measured at **104 MB** for 90k chunks — but on a |
| +synthetic corpus of random 4–9 letter words, whose vocabulary inflates a |
| +postings list relative to English. Real English will be smaller. **Taken |
| +as 104 MB because it is the only measurement in hand, and flagged as |
| +conservative rather than accurate.** Draft 1 asserted 60 MB with no |
| +derivation. |
| + |
| +**Rebuild vs snapshot.** Building that index took **19 s**. Loading a |
| +sealed snapshot does not. **Snapshot, never rebuild.** |
| + |
| +### 7.3 The wake budget, and the real ceiling |
| + |
| +Snapshot at 90k chunks: 46 MB vectors + 104 MB FTS = **150 MB**, ≈1.67 KB |
| +per chunk. |
| + |
| +**Stated assumption, which draft 1 omitted:** bucket read at 50–100 MB/s, |
| +AES-GCM at ~1 GB/s. So 150 MB loads in **1.5–3.0 s plus 0.15 s decrypt**. |
| + |
| +A **5 s budget** therefore admits 250–500 MB, i.e. **150k–300k chunks**. |
| +Taking the conservative bandwidth: **~150k chunks**. |
| + |
| +**Draft 1 claimed ~100k and called it "the same ceiling the brute-force |
| +arithmetic gives, which is tidy". Both reviewers showed that was |
| +manufactured**, and they were right twice: 250 MB ÷ 1.67 KB is ~150k, not |
| +100k; and brute-force cosine is nowhere near binding at that size — §7.2 |
| +shows 250k chunks answering in 80 ms. |
| + |
| +**The honest conclusion draft 1 missed: the wake budget binds first, and |
| +ANN does not help it.** An HNSW graph makes the snapshot *larger*. So the |
| +deferred item is not "ANN at 250k" but **an on-disk sealed index, or |
| +partial load with background fill** — §12. |
| + |
| +### 7.4 Snapshots, deletion, and restore |
| + |
| +**A snapshot is not a cache; it is a durable copy that outlives the |
| +deletion it predates.** GPT Sol's finding 6, which neither the other |
| +reviewer nor draft 1 caught. |
| + |
| +- **A deletion is not complete until a superseding snapshot is written |
| + and the prior snapshot is retired.** Snapshots carry a monotonic |
| + generation; retirement deletes the blob. |
| +- **On wake, sibling content may serve immediately**, because §4.1 asks |
| + the sibling live and a deleted item answers `NOT_VISIBLE`. |
| +- **memoria-owned content (§4.2) has no such backstop**, so its deletions |
| + must be applied to the loaded index before it serves. A snapshot |
| + records the deletion cursor it is current through; a gap is replayed |
| + from the tombstone log at wake, before first query. |
| +- **A restore from backup takes the re-announce path** and replays |
| + memoria-owned tombstones before serving. |
| --- |
| ## 8. Retrieval and the answer |
| -### 8.1 The floor, with AI switched off |
| +### 8.1 One gate, both paths |
| + |
| +**Every path that renders a passage calls §4.1 first** — search results, |
| +snippets, generated answers, derived artefacts, API responses. There is |
| +no passage-only path that skips it, which is the whole of gap 2's fix. |
| -A search engine over the team's content. A GET form, `?q=`, ranked |
| -passages, each carrying app, title, locator (tab and range; heading), |
| -👨/🤖 marker, date, and a deep link to the owning instance. Scriptless, |
| -paginated, rastrillo `server` shape. |
| +With AI off, the floor: a GET form, `?q=`, ranked passages, each carrying |
| +app, title, locator, 👨/🤖 marker, date, and a deep link. Scriptless, |
| +paginated, rastrillo `server` shape. Lexical only, since embeddings need |
| +a provider — and per §3.2, nothing leaves the instance in this mode. |
| -Lexical only — BM25 alone, since embeddings need a provider. This is a |
| -real product, not a degraded one: it is the full-text search Docs has |
| -been waiting for, over the whole suite. |
| +**Loud withholding, naming the shape of the loss:** |
| + |
| +- `UNKNOWN` from a sibling → *"Sheets did not answer; 7 results |
| + withheld."* |
| +- A failed S3 after a successful S2 → the passage is listed as |
| + unavailable, never dropped. |
| +- A failed or stale S1 → the page states the corpus is current as of a |
| + time, rather than implying freshness. |
| +- **A circuit breaker.** If a sibling returns `NOT_VISIBLE` for |
| + *everything*, or S3 404s wholesale, that is far more likely a |
| + mis-scoped predicate on their side than a real mass revocation. |
| + memoria states the anomaly and does not purge. Gate 8. |
| ### 8.2 Routing, decided after retrieval |
| -Retrieval is free, so the router runs on results rather than on the |
| -question. Five classes, three of which call no model: |
| +Retrieval is free, so the router runs on results. Five classes, three |
| +calling no model: |
| | Class | Signal | Route | |
| |---|---|---| |
| | Navigational | "where is the Q3 budget" | **No model.** Result list. | |
| | Numeric / aggregate | asks for a computed value | **No model.** Cite the cell or range; Sheets is the app of record. | |
| -| Who knows about X | asks about people | **No model.** Authorship markers on top-K, aggregated, filtered to what the asker can see. | |
| +| Who knows about X | asks about people | **No model.** Authorship markers on passages that already passed §4.1, aggregated. | |
| | Single-source factual | large top-1 BM25 margin, one document | Cheap model, extractive. | |
| | Synthesis | top-K spread across ≥3 documents | Frontier model. | |
| +Draft 1 wrote "filtered to what the asker can see" on the third row, |
| +which either restated §4.1 or smuggled a second per-person filter |
| +alongside it. The passages have already passed the gate. |
| + |
| Two configuration names — `model.cheap`, `model.frontier` — with the |
| -route printed on the answer and in the prompt ledger. |
| +route printed on the answer. |
| **`openrouter/auto` is disqualified.** It selects by trailing-7-day |
| -community spend, changes weekly, and cannot be attributed — which fails |
| -AGENTS.md §Review's "record which model", and fails decision 4's |
| -requirement to name the recipient in advance. |
| +community spend, changes weekly, and cannot be attributed — failing |
| +AGENTS.md §Review's "record which model" and decision 4's naming |
| +requirement. |
| ### 8.3 Egress |
| Per decision 4: one provider, fixed by the team owner, named on every |
| answer, disclosed in the README's first paragraph, opt-in per team, with |
| -§8.1 working when it is off. |
| +§8.1's floor working when it is off. |
| **This qualifies Docs §11.2**, which rejected third-party conversion APIs |
| -because they ship "customer documents to a third party, contradicting |
| -§2". The difference is opt-in, naming, and the AI-off floor. **It is not a |
| -difference in what the provider sees.** Because Docs is a separate repo, |
| -the qualification must be recorded there too, or the two specs |
| -contradict each other — §13. |
| +for shipping "customer documents to a third party, contradicting §2". The |
| +comparison must be like with like: Docs rejected whole-document egress, |
| +and per §3.2 **memoria's embedding ingest is also whole-corpus egress**, |
| +not merely answer-time top-k. Draft 1 compared Docs' whole documents |
| +against memoria's retrieved passages, which flattered memoria. The real |
| +difference is opt-in, naming, and the AI-off floor. **It is not a |
| +difference in what the provider sees, nor now in how much.** |
| + |
| +Because Docs is a separate repo, the qualification must be recorded there |
| +too, or the two specs contradict — §13. |
| + |
| +**A "model call ledger" is content.** Any runtime record of prompts holds |
| +retrieved passages, so it falls under §3.2's log rule and §5.2's |
| +provenance rule. It is not the development prompt ledger of docs |
| +AGENTS.md, which records prompts to `refs/amadan/ledger/<branch>`; draft |
| +1's "the prompt ledger" ambiguously named both. If a runtime ledger is |
| +built it is sealed, provenance-stamped, and expires with its sources. |
| ### 8.4 Citations with an optional answer, never the reverse |
| A citation list is always served without an answer. **An answer is never |
| -served without its list.** With AI on, a 🤖 paragraph appears *above* the |
| +served without its list.** With AI on, a 🤖 paragraph appears above the |
| list; every sentence carries a passage id from it, and a sentence citing |
| -nothing is dropped before render. |
| +nothing is dropped before render. **If every sentence is dropped, say |
| +so** — an empty paragraph is indistinguishable from AI being off. |
| ### 8.5 Prompt injection |
| -Ingested content becomes model input, and an email or a Notion page can |
| -carry instructions. Containment that actually exists: |
| - |
| - **No tools.** memoria retrieves and answers. It never writes to a |
| - sibling, and never fetches a URL at answer time. |
| + sibling and never fetches a URL at answer time. |
| - **No remote loads in rendered answers.** `` is |
| exfiltration without a tool. Strip remote images; refuse links to |
| non-suite hosts. |
| -- **Retrieval is scoped before the model exists.** The model is never the |
| - authorisation layer. Injection cannot cross a grant line because there |
| +- **Authorization runs before the model exists.** §4.1 has already |
| + filtered the passages, so injection cannot cross a grant line — there |
| is nothing on the other side to retrieve. |
| - **Every claim carries an openable citation.** Poisoning one document to |
| - lie about another survives all of the above; a snippet the reader can |
| - check is the only mitigation, and it is a partial one. |
| + lie about another survives all of the above; a checkable snippet is the |
| + only mitigation, and a partial one. |
| ### 8.6 Cost |
| -**Ingest:** 30k documents ≈ 60M tokens ≈ **$1.20 once** on |
| -`text-embedding-3-small`, $7.80 on `-large`. Re-embed only changed chunks. |
| +On §7.2's single corpus assumption — 30k documents, 90k chunks, **36M |
| +tokens**: |
| + |
| +**Ingest:** 36M × $0.02/M (`text-embedding-3-small`) = **$0.72 once**; |
| +$4.68 on `-large`. Re-embed only changed chunks. Draft 1 said $1.20 and |
| +$7.80, priced against the 60M figure it should not have used. |
| **Query:** retrieval is free. The generative layer sends ~8 passages plus |
| instructions — ~5k tokens in, ~500 out. Cheap model ~$0.001–0.003; |
| frontier ~$0.02–0.10. Five people × 20 queries/day × 22 days = 2,200 |
| queries: **~$5/month if most route cheap, ~$100 if most route frontier.** |
| +Derived-artefact regeneration (§5.2) adds a synthesis-class call per |
| +invalidated artefact and is unbudgeted until the feature exists. |
| -The money is recurring query tokens, by at least an order of magnitude. |
| -That is the economic argument for §8.2's router, and for the three |
| -classes that call no model at all. |
| +The money is recurring query tokens by an order of magnitude — the |
| +economic argument for §8.2's router and its three model-free classes. |
| --- |
| -## 9. The API surface |
| +## 9. Surfaces |
| + |
| +### 9.1 What memoria exposes |
| + |
| +Draft 1's §9 specified `POST /ingest`, `DELETE /ingest/{doc}` and |
| +`GET /feed?since=` with per-consumer bearer tokens — **the inbound push |
| +surface decision 7 forbids and §6.1 argues against**. Both reviewers |
| +caught the contradiction; it was carried over from an earlier pass that |
| +predated the pull-only ruling. |
| -Plain REST, following home §9's conventions: per-consumer bearer tokens |
| -stored hashed, consumer identity is its origin, origins allowlisted, hard |
| -body caps whose number carries its reason in a comment, rotation before |
| -launch. |
| +memoria exposes exactly two surfaces: |
| -- **`POST /ingest`** — chunks with locator and author marker. Idempotent |
| - on `(consumer, doc, version)`. Body cap 1 MiB, with the reason measured |
| - before the number is committed. |
| -- **`GET /query?q=`** — passages as JSON; `&answer=1` adds the model |
| - layer. `q` capped at 1 KiB. **Never logged.** |
| -- **`GET /feed?since=<cursor>`** — a polled cursor, not SSE, because |
| - consumers hibernate and run no timers. Per-consumer TTL; a consumer past |
| - its TTL gets a full re-announce. This is the home spec's lesson applied |
| - a third time. |
| -- **`DELETE /ingest/{doc}`**. |
| +- **A person-facing web surface**, authenticated by home assertion, |
| + scriptless-first. `GET /query?q=` renders passages; `&answer=1` adds |
| + the model layer. `q` capped at 1 KiB and **never logged**. |
| +- **A connector ingest surface for memoria-owned content only** (§4.2) — |
| + external connectors and manual upload. Per-connector bearer token |
| + stored hashed, origin allowlisted, body cap 1 MiB with the reason |
| + measured before the constant is committed. **This is not a sibling |
| + path**, and a sibling presenting a connector token is refused: the two |
| + credential sets are disjoint and separately scoped. |
| -**MCP is deferred**, as a thin JSON-RPC adapter over the same three calls. |
| -Trigger: the first consumer that speaks it. |
| +**No feed. Nothing consumes memoria.** If something ever does, that is a |
| +new decision with its own leak-list entry, not an endpoint left lying |
| +around for it. |
| -### 9.1 The home links; it never renders |
| +**MCP is deferred** as a thin adapter over the query surface. Trigger: |
| +the first consumer that speaks it. |
| + |
| +### 9.2 The home links; it never renders |
| Home §2.2 forbids the home rendering content from an instance, and a |
| -generated answer is content. It also forbids it holding an authorization |
| -model, which deciding whose answer to show would be. And memoria is |
| -per-team, so "the home knows everything about all the apps" across teams |
| -is federation. |
| +generated answer is content; it also forbids it holding an authorization |
| +model, which choosing whose answer to show would be. memoria is per-team, |
| +so "the home knows everything about all the apps" across teams is |
| +federation. |
| **The home gets one directory row (`app = memoria`) and a search box that |
| -is a GET form redirecting to the team's memoria, call-6 style.** memoria |
| +is a GET form redirecting to the team's memoria**, call-6 style. memoria |
| renders on its own origin. The home never sees `q`. |
| --- |
| @@ -614,48 +861,66 @@ renders on its own origin. The home never sees `q`. |
| ## 10. Testing gates |
| 1. **Plaintext grep** over raw SQLite bytes and bucket objects, from the |
| - first commit, as Sheets. `plaintextExempt` starts empty. |
| + first commit. `plaintextExempt` starts empty. |
| 2. **Inversion probe** — the gate the byte grep cannot see. A seeded |
| corpus, an embedding-inversion attempt against whatever is at rest, |
| and a threshold. Without this, §3.1's ruling is a comment. |
| -3. **Feed rows carry no content** — plaintext grep extended over S1 rows. |
| -4. **No content in logs**, extended to cover query text, answer text and |
| - cited snippets. |
| -5. **Cross-team dial refusal**, against a fixture of hostile hostnames. |
| -6. **Withhold loudly** — a silent sibling produces a stated withholding, |
| - never a shorter list. |
| -7. **Two-consumer isolation** on the sibling endpoints. |
| -8. **Answer sentences cite listed passages**; an uncited sentence is |
| - dropped before render. |
| -9. **AI-off floor** — every affordance in §8.1 works scriptless with the |
| - provider unreachable. |
| -10. **Restore drill** with a rewound cursor, proving the re-announce path. |
| +3. **Feed rows carry no content and no visibility flag** — grep extended |
| + over S1 rows, asserting the absence of the field draft 1 had. |
| +4. **No content in logs**, covering query text, answer text, cited |
| + snippets and any model-call ledger. |
| +5. **Every render path calls §4.1.** Enumerate the passage-rendering |
| + entry points and assert each one's authorization call — the gate that |
| + would have caught gap 2. A new render path with no S2 call fails. |
| +6. **`UNKNOWN` is never treated as `NOT_VISIBLE`.** A sibling returning |
| + `UNKNOWN` produces a stated withholding, never a shorter list. |
| +7. **An enumeration diff distinguishes empty from failed**, and deletes |
| + nothing on a timeout, credential error or partial page. |
| +8. **Circuit breaker** — a wholesale `NOT_VISIBLE` or 404 response states |
| + an anomaly and purges nothing. |
| +9. **Derived artefacts expand to full provenance**, and one |
| + `NOT_VISIBLE` source withholds the artefact. |
| +10. **Deletion survives a restore** — a tombstone applied, a snapshot |
| + superseded and retired, then a restore from the prior generation must |
| + not serve the deleted passage. |
| +11. **Cross-team dial refusal**, including redirects. |
| +12. **Token rotation** — all three phases, each interrupted and resumed, |
| + across a partial multi-sibling delivery. |
| +13. **Two-consumer isolation** on the sibling endpoints. |
| +14. **Answer sentences cite listed passages**; an all-dropped answer says |
| + so rather than rendering empty. |
| +15. **AI-off floor** — every affordance in §8.1 works scriptless with the |
| + provider unreachable, **and no egress occurs**, asserted by a network |
| + guard rather than by inspection. |
| --- |
| ## 11. Sequencing |
| **Slice 0 — buildable today, no contract, no blockers.** Scaffold, sealed |
| -store, and §7 proven end to end: chunk, embed, seal, wake, hybrid search, |
| -scriptless UI with citations. Content enters by upload and paste only. |
| -Worth doing first on its own merits: it is the design Docs §15.2 deferred |
| -for want of one, so proving it here unblocks Docs whether or not memoria |
| -ever reads a sibling. |
| - |
| -**Slice 1 — the sibling contract (§6).** One source app first — Docs, |
| -since it has bodies. Feed, re-check, fetch, tombstones. Needs the drain to |
| -grow atomic multi-host provisioning and token minting. |
| - |
| -**Slice 2 — the model layer (§8).** Named provider, router, answer-above- |
| -citations, README disclosure, Docs §11.2 qualification recorded in Docs. |
| - |
| -**Slice 3 — external connectors (§5.1).** Drive, Notion, mail-by-forward. |
| - |
| -**Ship gate throughout:** home §13 step 4 (person refs) before any of it |
| -reaches a shared host — the same gate Sheets and Docs sit behind. |
| - |
| -Slice 0 carries no doctrinal debt. Everything contested lives in slices 1 |
| -and 2. |
| +store, §7 proven end to end: chunk, seal, wake, BM25 search, scriptless |
| +UI with citations, gates 1–4 and 15. Content by upload and paste only, so |
| +§4.2 governs and no person ref is needed. **No embeddings, therefore no |
| +egress at all.** This is the design Docs §15 item 2 deferred for want of |
| +one, so proving it unblocks Docs regardless of memoria's fate. |
| + |
| +**Slice 1 — the sibling contract (§6) and §4.1.** One source app first — |
| +Docs, since it has bodies. S1–S4, the per-asker gate, loud withholding, |
| +the circuit breaker. Needs person refs, so gated on home §13 step 4. |
| +Needs the drain to grow multi-host provisioning and three-phase rotation. |
| + |
| +**Slice 2 — the model layer (§8).** Named provider, router, |
| +answer-above-citations, the §3.6 README, the Docs §11.2 qualification |
| +recorded in Docs. **This is the slice that turns on whole-corpus egress**, |
| +so it does not land before its disclosure copy does. |
| + |
| +**Slice 3 — external connectors (§5.1)** under §4.2, with least-privilege |
| +credentials (§3.5) and gate 7. |
| + |
| +**Ship gate throughout:** home §13 step 4 before anything reaches a |
| +shared host. memoria depends on it more deeply than Sheets or Docs, |
| +because §4.1 needs a person ref on every query rather than only on grant |
| +rows. |
| --- |
| @@ -663,13 +928,15 @@ and 2. |
| | Thing | Trigger | |
| |---|---| |
| -| ANN index (`coder/hnsw`) | The corpus passes 250k chunks | |
| +| An on-disk sealed index, or partial load with background fill | The corpus passes ~150k chunks (§7.3). **This, not ANN, is the next scaling problem** | |
| +| ANN index (`coder/hnsw`) | Query latency exceeds ~200 ms, i.e. ~600k chunks at 512 dims — which §7.3 shows is *later* than the wake ceiling, not earlier | |
| | MCP adapter | The first consumer that speaks it | |
| +| 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 connector beyond forward-or-label | Never by sweep. A mail repo exists **and** a per-thread audience maps onto a team | |
| +| 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 | |
| -| Per-source subsets in the UI | An owner asks to exclude a source | |
| -| A local model | Egress becomes unacceptable to a team, or on-instance generation stops being mediocre | |
| +| A runtime model-call ledger | Someone needs to audit routing. Sealed and provenance-stamped when it lands (§8.3) | |
| +| A local embedding model | A pure-Go or cgo-free runtime exists. Would remove ingest egress entirely, which §3.2 makes the largest single leak | |
| | Cross-team anything | Never, under §6.4 | |
| | Fine-tuning on the corpus | Never, under decision 8 | |
| @@ -677,23 +944,31 @@ and 2. |
| ## 13. Open |
| -- **§Review pass** by the highest-capability model available, per |
| - AGENTS.md. This spec has had six brainstorming passes and one |
| - adversarial review of the *proposal*; it has had no review of *itself*. |
| -- **Does CARLOS provide non-replicated scratch storage?** Unverified. |
| - §7's index has to live somewhere that is not the replicated SQLite |
| - file; in memory works but makes every wake pay the download. |
| -- **OpenRouter's embeddings endpoint** is reported by several sources to |
| - be OpenAI-compatible at `/api/v1/embeddings`, but their documentation |
| - page 404'd when checked. Verify before depending on one key covering |
| - both embeddings and chat. |
| +- **§Review pass on this draft.** Draft 1 was reviewed by GPT Sol and |
| + Fable; draft 2 answers them and has been reviewed by nobody. |
| +- **Does CARLOS provide non-replicated scratch storage?** Unverified, and |
| + §7.1 needs it or pays a download on every wake. |
| +- **The FTS index size is measured on a synthetic corpus** whose random |
| + vocabulary inflates it. Re-measure on real English before §7.3's |
| + ceiling is trusted as anything but conservative. |
| +- **OpenRouter's embeddings endpoint** is reported OpenAI-compatible at |
| + `/api/v1/embeddings`, but the documentation page 404'd when checked. If |
| + embeddings and chat both route through OpenRouter, **two parties see |
| + content** — the proxy and the upstream provider — and decision 4's "one |
| + named provider" then names one of two. Resolve before slice 2. |
| - **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 that the app boundary is the |
| - implementer's to declare, and that Oficina declares it at the suite — |
| +- **A `carlos/platform` discussion** noting the app boundary is the |
| + implementer's to declare, and Oficina declares it at the suite — |
| informational, not a doctrine proposal (decision 2). |
| -- **Chunking strategy** is unspecified: a spreadsheet is not prose, and a |
| - 400-token chunk of a grid may be meaningless. Sheets may need a |
| - per-tab or per-region strategy that Docs does not. |
| -- **Whether Sheets and Docs will accept the S1–S3 endpoints**, which is |
| - work in their repos, not this one. |
| +- **Chunking is unspecified**, and §8.2's numeric route depends on it: a |
| + spreadsheet is not prose, and a 400-token chunk of a grid may be |
| + meaningless. Sheets likely needs a per-tab or per-region strategy Docs |
| + does not. |
| +- **S2's latency budget.** Every query now makes a live call to each |
| + contributing sibling, any of which may be cold. Measure before slice 1 |
| + ships; if a cold wake is seconds, the floor needs a stated behaviour |
| + beyond "withhold loudly". |
| +- **Whether Sheets and Docs will accept S1–S3**, which is work in their |
| + repos, not this one — and §4.1 asks more of them than draft 1 did, |
| + since S2 must evaluate their grants join per asker. |