| 1 | # Questions for Paul — memoria |
| 2 | |
| 3 | Overnight 2026-09-03/04. Design guidance I delegated went to Fable and |
| 4 | gpt-5.6-sol; what is left here is what I judged yours. |
| 5 | |
| 6 | **Nothing below blocks trying it.** `cd /home/paulca/memoriadrive && ./run.sh`, |
| 7 | then http://127.0.0.1:8823 — see the README. |
| 8 | |
| 9 | --- |
| 10 | |
| 11 | ## Answered last night, recorded, no action needed |
| 12 | |
| 13 | **Q2 — the Docs §11.2 egress amendment.** Done: `oficina/docs` |
| 14 | discussion 33, posted as `claude~paul@keymail.dev`. It asks Docs to record |
| 15 | the *boundary* of its rejection rather than reverse it, and tells them |
| 16 | plainly that memoria's egress is the same kind and comparable volume to |
| 17 | what they rejected, with only three things differing. |
| 18 | |
| 19 | **Q3 — numeric questions.** Both models asked, answers synthesised into |
| 20 | §8.7. They converged, so I did not have to arbitrate: |
| 21 | |
| 22 | - A new **S6** call. Sheets computes and formats; memoria never does |
| 23 | arithmetic. S6 takes an *enumerated function* (`SUM`, `AVERAGE`, `MIN`, |
| 24 | `MAX`, `COUNT`) over cell refs — **never expression text** — so memoria |
| 25 | cannot grow into a formula author. |
| 26 | - Spreadsheet chunking: one **structure chunk** per tab carrying the |
| 27 | vocabulary, plus **region row chunks** rendered as facts |
| 28 | (*"FY2024 › P&L › Revenue: Jun 2024 = €1,350,000"*). That is what makes |
| 29 | `revenue`, `June` and `2024` findable for a cell containing `1350000`. |
| 30 | - Retrieval resolves **named slots** and refuses to show a number unless |
| 31 | every slot has exactly one authorized operand. Two candidates is |
| 32 | ambiguity, never extra evidence. |
| 33 | - Both independently forbade the same thing: a **RAG calculator** that |
| 34 | lets a model average numeric strings it retrieved. It stacks incomplete |
| 35 | recall, wrong operand selection and arithmetic outside Sheets — and |
| 36 | cites correctly while being wrong. |
| 37 | |
| 38 | Your example works under this design and states its basis: *"Average of |
| 39 | June 2024, June 2025, June 2026."* If a year is missing it shows the slot |
| 40 | table and no number. |
| 41 | |
| 42 | --- |
| 43 | |
| 44 | ## 1. Closed — OpenRouter is out, TensorX is in |
| 45 | |
| 46 | *Was: "Your 'C' on the OpenRouter question did not map." You asked for |
| 47 | TensorX instead on 2026-09-04; that answers it, and the recommendation this |
| 48 | question carried — go direct to one provider rather than through a proxy — |
| 49 | is what TensorX does.* |
| 50 | |
| 51 | **What changed in the spec:** §8.3 names TensorX and says what it does not |
| 52 | buy; §8.2 names `z-ai/glm-5.3-flash` and `moonshotai/kimi-k3` and |
| 53 | generalises the `openrouter/auto` ban to any id that can move under a team; |
| 54 | §8.6 re-derives cost; §7.2 fixes a claim that was true of OpenAI and false |
| 55 | here; §12 corrects the local-embedding row; §13 carries three new items. |
| 56 | |
| 57 | **One thing I could not do:** verify the embeddings endpoint against a live |
| 58 | key. The vendor's LlamaIndex page names OpenAI's `text-embedding-3-small`, |
| 59 | which TensorX cannot be serving; their API reference names |
| 60 | `qwen/qwen3-embedding-8b`, which it can. I have believed the API reference. |
| 61 | If that first request in fact *succeeds* against `api.tensorx.ai`, they are |
| 62 | proxying to OpenAI and we are back to two parties — see §13. **One call |
| 63 | with a key settles it, and I'd rather that happened before slice 2 than |
| 64 | after.** |
| 65 | |
| 66 | **Reviewed by Fable, REQUEST CHANGES, 10 findings, all applied.** It was |
| 67 | worth doing: two of the four MAJORs were me breaking a rule in the same |
| 68 | paragraph I wrote it. I had said a model id that can move under a team |
| 69 | can't be printed on an answer, then picked `kimi-k3` and |
| 70 | `glm-5.3-flash`, neither of which is datestamped. And I wrote that §13 |
| 71 | "records how that was established" three paragraphs from §13 saying |
| 72 | **untested**. §1.5 has the rest. |
| 73 | |
| 74 | **Both questions this section asked are now answered** (2026-09-04): |
| 75 | |
| 76 | - **Exactly one provider, TensorX** — *"if we want to swap that later we |
| 77 | can."* Decision 13 records both halves. Swapping *generation* is cheap — |
| 78 | one client, one base URL, two model ids — through §Review, where the |
| 79 | two-parties question gets asked again. Swapping *embeddings* is not: a |
| 80 | host serving the same open weights costs a migration, anything else costs |
| 81 | a re-embed of the whole corpus. Fable caught me writing "two model ids" |
| 82 | when the third is the expensive one. |
| 83 | - **Open-weight frontier is fine** — *"this is lightweight LLM inference |
| 84 | based on categorisation and memory. It's not complex reasoning."* That is |
| 85 | the right reading of §8.2 and I have put your words in §13. It also |
| 86 | prompts a question back: **if it is not complex reasoning, does §8.2 need |
| 87 | two tiers at all?** Measuring the cheap tier on the synthesis class |
| 88 | before building the expensive one would make §8.6's cost argument |
| 89 | stronger, not weaker. Not urgent; slice 2. |
| 90 | |
| 91 | **Cost, corrected twice and now settled.** I first said it fell, then said |
| 92 | it hadn't much, and both were against the wrong baseline. Against draft 3's |
| 93 | ~$5 and ~$100: **~$3/month all-cheap and ~$26 all-frontier**, so the |
| 94 | frontier month fell about 4× and the cheap one about 1.6×. Most of that is |
| 95 | §8.2 pinning dated DeepSeek ids, not the move off OpenRouter — worth |
| 96 | keeping straight so the next provider argument isn't made on a 4× that |
| 97 | belongs to a different decision. |
| 98 | |
| 99 | **Now verified against your key (2026-09-05), with one caveat.** The |
| 100 | embeddings endpoint serves `qwen/qwen3-embedding-8b` as documented, and the |
| 101 | `text-embedding-3-small` probe — the one that would have shown them |
| 102 | proxying to OpenAI — **came back 403.** That is the good outcome, but read |
| 103 | it precisely: 403 is a *refusal*, not a *not-found*. A 404 would have said |
| 104 | "no such model"; 403 is consistent with a routable name your key isn't |
| 105 | entitled to. It rules out proxying-for-anyone, not proxying-for-someone. |
| 106 | §8.3 still says "narrowed, not closed", and the subprocessor question is |
| 107 | untouched by any of this — that needs the DPA, not the API. |
| 108 | |
| 109 | **Two corrections to what I told you an hour ago**, both from my reading a |
| 110 | truncated list and drawing a conclusion from it: `moonshotai/kimi-k3` does |
| 111 | exist, and their website's model list does match the API. I said otherwise |
| 112 | on 27 of 27 models' worth of missing data. The same mistake §1.6 is about. |
| 113 | |
| 114 | **And one real finding: `encoding_format: "base64"` is accepted and |
| 115 | silently ignored.** So ingest pulls ~4 GB over the wire for a 90k-chunk |
| 116 | corpus, not the ~2 GB I hedged with, and there is no opt-out (§7.2). |
| 117 | |
| 118 | Still does not bite until slice 2. |
| 119 | |
| 120 | ## 2. Closed — search page, not a chat (decision 14, §8.4) |
| 121 | |
| 122 | **Confirmed by you on 2026-09-05**, including that parking a conversational |
| 123 | surface in §12 is the right disposition rather than forbidding it. Also |
| 124 | settled the same day: keep DeepSeek for both tiers, keep two tiers *and* |
| 125 | still measure, and proceed with TensorX rather than holding for the DPA. |
| 126 | All four are recorded — decision 14 and §8.4; §8.2; §13; §13 again — and |
| 127 | Fable caught me claiming that before two of them were actually written. |
| 128 | |
| 129 | **Two footnotes on rulings you gave, because the record should show what |
| 130 | you were shown.** When you kept DeepSeek I had told you the pinning rule |
| 131 | forced it; it didn't — a dated Qwen id exists, so a two-lab pair was |
| 132 | available and I hadn't put it in front of you. Nothing needs undoing, and |
| 133 | that Qwen id is now the named frontier fallback. And "proceed with TensorX |
| 134 | for sure" I have read narrowly: **the provider decision is settled and |
| 135 | slice 2 can be built against it, but the DPA and retention terms are still |
| 136 | owed before egress is switched on over real content.** If you meant the |
| 137 | wider thing, say so and §13 changes. |
| 138 | |
| 139 | *The question as it stood, kept because the reasoning is the useful part:* |
| 140 | |
| 141 | You asked whether we were building a chat UI or a hybrid. I answered |
| 142 | *hybrid, search-first* — one GET search form, one query in, one page out, |
| 143 | with an optional 🤖 paragraph above the citation list. §8.4 already implied |
| 144 | it; it now says it. |
| 145 | |
| 146 | **But I first argued it as forced when it is chosen, and Fable caught me.** |
| 147 | I claimed decision 3 forbade a chat outright — that re-authorizing a |
| 148 | transcript would blow §6.5's latency budget. It would not: §4.1 batches 256 |
| 149 | pairs a call and §6.5 makes a page one round, so a four-turn transcript is |
| 150 | one batch. A correct chat is affordable here. My other argument, that a |
| 151 | transcript is a cache of authorized passages, is true of any page left open |
| 152 | in a tab and so distinguishes nothing. |
| 153 | |
| 154 | **So the honest version:** the invariant decision 3 actually requires is |
| 155 | that no passage crosses a request — not in a response, not in a model |
| 156 | prompt — and gate 32 enforces it. The *shape* is decision 5's call: the |
| 157 | search floor is the product, so the product is a search page. That is |
| 158 | **decision 14, attributed to me and not to you.** A conversational surface |
| 159 | that keeps the invariant is now parked in §12 as a thing to argue rather |
| 160 | than a thing forbidden. |
| 161 | |
| 162 | **What I need:** confirm decision 14, or tell me you want the conversational |
| 163 | version considered properly. Either is cheap today and neither is cheap |
| 164 | after slice 2 ships an answer surface. |
| 165 | |
| 166 | ## 3. Is routine partial withholding acceptable? (§6.5) |
| 167 | |
| 168 | The genuinely new product question, and the one I would most like your |
| 169 | view on. |
| 170 | |
| 171 | Every search now asks each contributing sibling "may this person see |
| 172 | these, at this revision" — live, no caching, because caching authorization |
| 173 | is what makes a stale answer a leak. Consequence: **a slow or hibernating |
| 174 | sibling means the page renders without its passages and says so** — |
| 175 | *"Sheets did not answer; 7 results withheld."* |
| 176 | |
| 177 | That is correct. It may also be constant, because siblings hibernate and a |
| 178 | cold wake is slower than any sane deadline. I have set 800 ms and pre-warm |
| 179 | on memoria's own wake, but if Sheets is habitually cold then the primary |
| 180 | search path habitually withholds, and a notice a reader sees every time is |
| 181 | a notice they stop reading. |
| 182 | |
| 183 | There is no design that fixes this while failing closed. The spec's stated |
| 184 | position is that this is a **suite health problem memoria surfaces, not a |
| 185 | memoria problem to route around** — so the answer would be to fix or drop |
| 186 | the source, never to cache its authorization. Tell me if you would rather |
| 187 | trade differently, because the alternative is a short-TTL cache and that |
| 188 | is a stated leak window rather than a correctness fix. |
| 189 | |
| 190 | ## 4. Deploy it, or leave it local? — ANSWERED: it is deployed |
| 191 | |
| 192 | Live at **https://memoria.oficina.works**, account `bds`, channel `edge`. |
| 193 | The origin moved off `memoria.bds.oncarlos.com` on 2026-09-04 once the |
| 194 | certificate issued; see the README's "Live" section for what that entailed |
| 195 | and what the old hostname still does. |
| 196 | |
| 197 | Local is still genuinely usable — keymail accepts the loopback |
| 198 | `client_id`, which I verified. |
| 199 | |
| 200 | ## 5. Your rastrillo CLI is about six releases stale, and it affects every app |
| 201 | |
| 202 | `rastrillo` on PATH predates v0.22.0: it has no `doctor`, and |
| 203 | `rastrillo new` scaffolds a **v0.17.0** pin. Two things follow, and the |
| 204 | second is the one that matters beyond memoria: |
| 205 | |
| 206 | - I bumped memoria to v0.23.0 by hand and wrote `tools/vendorassets` to |
| 207 | re-copy the three vendored files from the linked library, because there |
| 208 | is no `doctor --fix` to do it. |
| 209 | - **The scaffold's `layout.html` links `tokens.css` but not |
| 210 | `theme.css`**, and `tokens.css` defines eleven tokens, none of them |
| 211 | colours. Any app scaffolded with this CLI renders with every colour |
| 212 | resolving to `initial`, which invalidates whole shorthand declarations — |
| 213 | rules vanish rather than the page going grey. That is the exact failure |
| 214 | your own memory note describes for the page-header rule. |
| 215 | |
| 216 | `go install github.com/carlosframework/rastrillo/cmd/rastrillo@v0.23.0` |
| 217 | would fix it, but it is a shared binary other sessions are using, so I |
| 218 | left it alone. |
| 219 | |
| 220 | ## 6. An anonymous visitor can make the instance dial any domain |
| 221 | |
| 222 | Found by the code review, and it is a real hole rather than a wording |
| 223 | problem. rastrillo's sign-in classifier fetches |
| 224 | `https://<the address's domain>/.well-known/keymail` on `POST /auth/begin` |
| 225 | — and rastrillo runs that **before** the admission gate, so it happens for |
| 226 | addresses that are not on the owners list. Type |
| 227 | `anything@attacker.example` into the sign-in form and the instance makes |
| 228 | an outbound request to `attacker.example`. |
| 229 | |
| 230 | What it gives an attacker: confirmation the instance exists and is |
| 231 | reachable, its egress IP, and a timing channel. Not content. But it also |
| 232 | made the footer's "nothing leaves this instance" false, which is why that |
| 233 | copy changed. |
| 234 | |
| 235 | **I did not fix it**, because the fetch is inside `rastrillo/auth` and |
| 236 | `keymaildev/signin`, not in memoria — fixing it here would mean |
| 237 | re-implementing the classifier or gating `/auth/begin` on the owners list |
| 238 | before rastrillo sees it. The second is small and I can do it, but it |
| 239 | changes sign-in behaviour for guests, which slice 1 will want. Say which |
| 240 | you'd prefer, or whether it belongs upstream in rastrillo. |
| 241 | |
| 242 | ## 7. Not a question, but you should know |
| 243 | |
| 244 | `gpt-5.6-sol` via Codex **reported its own model id as `gpt-5`** when |
| 245 | asked. Codex's own banner says `gpt-5.6-sol`. I recorded the discrepancy |
| 246 | in the spec's review table rather than picking one, since §Review requires |
| 247 | attributing a review to a model and the two sources disagree. |
| 248 | |
| 249 | ## 8. The deployed hostname has no `<team>` label, and the spec says it should |
| 250 | |
| 251 | home ruling 16 and this project's own spec (§6.3, and line 4) put an |
| 252 | instance at **`<team>.<app>.oficina.works`**. In §6.3 that label is not |
| 253 | decoration: barrier 2 is *"memoria's client refuses any host whose |
| 254 | `<team>` label differs from its own"*, test-enforced against a fixture of |
| 255 | hostile hostnames. A hostname with no team label gives that guard nothing |
| 256 | to compare. |
| 257 | |
| 258 | What is deployed, across the whole suite, is `<app>.oficina.works` — no |
| 259 | team label, no wildcard certificate: `docs.oficina.works`, |
| 260 | `sheets.oficina.works`, `calendar.oficina.works`, and now |
| 261 | `memoria.oficina.works`. I matched the four live siblings rather than |
| 262 | invent a team label on my own, because slice 0 has no siblings for barrier |
| 263 | 2 to defend against and consistency with what is already running seemed |
| 264 | the safer default. |
| 265 | |
| 266 | It stops being free at slice 3, when the connector client wants a label to |
| 267 | compare. Roughly the options: re-host every app under a team label before |
| 268 | then — a second origin change each, and every one is a sign-in cutover — |
| 269 | or amend ruling 16 to make the team label a slice-3 addition and say what |
| 270 | barrier 2 compares until it exists. |
| 271 | |
| 272 | **I did not touch the spec.** Ruling 16 is settled and AGENTS.md forbids |
| 273 | relitigating one in a drive-by. This is the flag, not the change. |
| 274 | |