# Questions for Paul — memoria

Overnight 2026-09-03/04. Design guidance I delegated went to Fable and
gpt-5.6-sol; what is left here is what I judged yours.

**Nothing below blocks trying it.** `cd /home/paulca/memoriadrive && ./run.sh`,
then http://127.0.0.1:8823 — see the README.

---

## Answered last night, recorded, no action needed

**Q2 — the Docs §11.2 egress amendment.** Done: `oficina/docs`
discussion 33, posted as `claude~paul@keymail.dev`. It asks Docs to record
the *boundary* of its rejection rather than reverse it, and tells them
plainly that memoria's egress is the same kind and comparable volume to
what they rejected, with only three things differing.

**Q3 — numeric questions.** Both models asked, answers synthesised into
§8.7. They converged, so I did not have to arbitrate:

- A new **S6** call. Sheets computes and formats; memoria never does
  arithmetic. S6 takes an *enumerated function* (`SUM`, `AVERAGE`, `MIN`,
  `MAX`, `COUNT`) over cell refs — **never expression text** — so memoria
  cannot grow into a formula author.
- Spreadsheet chunking: one **structure chunk** per tab carrying the
  vocabulary, plus **region row chunks** rendered as facts
  (*"FY2024 › P&L › Revenue: Jun 2024 = €1,350,000"*). That is what makes
  `revenue`, `June` and `2024` findable for a cell containing `1350000`.
- Retrieval resolves **named slots** and refuses to show a number unless
  every slot has exactly one authorized operand. Two candidates is
  ambiguity, never extra evidence.
- Both independently forbade the same thing: a **RAG calculator** that
  lets a model average numeric strings it retrieved. It stacks incomplete
  recall, wrong operand selection and arithmetic outside Sheets — and
  cites correctly while being wrong.

Your example works under this design and states its basis: *"Average of
June 2024, June 2025, June 2026."* If a year is missing it shows the slot
table and no number.

---

## 1. Closed — OpenRouter is out, TensorX is in

*Was: "Your 'C' on the OpenRouter question did not map." You asked for
TensorX instead on 2026-09-04; that answers it, and the recommendation this
question carried — go direct to one provider rather than through a proxy —
is what TensorX does.*

**What changed in the spec:** §8.3 names TensorX and says what it does not
buy; §8.2 names `z-ai/glm-5.3-flash` and `moonshotai/kimi-k3` and
generalises the `openrouter/auto` ban to any id that can move under a team;
§8.6 re-derives cost; §7.2 fixes a claim that was true of OpenAI and false
here; §12 corrects the local-embedding row; §13 carries three new items.

**One thing I could not do:** verify the embeddings endpoint against a live
key. The vendor's LlamaIndex page names OpenAI's `text-embedding-3-small`,
which TensorX cannot be serving; their API reference names
`qwen/qwen3-embedding-8b`, which it can. I have believed the API reference.
If that first request in fact *succeeds* against `api.tensorx.ai`, they are
proxying to OpenAI and we are back to two parties — see §13. **One call
with a key settles it, and I'd rather that happened before slice 2 than
after.**

**Reviewed by Fable, REQUEST CHANGES, 10 findings, all applied.** It was
worth doing: two of the four MAJORs were me breaking a rule in the same
paragraph I wrote it. I had said a model id that can move under a team
can't be printed on an answer, then picked `kimi-k3` and
`glm-5.3-flash`, neither of which is datestamped. And I wrote that §13
"records how that was established" three paragraphs from §13 saying
**untested**. §1.5 has the rest.

**Both questions this section asked are now answered** (2026-09-04):

- **Exactly one provider, TensorX** — *"if we want to swap that later we
  can."* Decision 13 records both halves. Swapping *generation* is cheap —
  one client, one base URL, two model ids — through §Review, where the
  two-parties question gets asked again. Swapping *embeddings* is not: a
  host serving the same open weights costs a migration, anything else costs
  a re-embed of the whole corpus. Fable caught me writing "two model ids"
  when the third is the expensive one.
- **Open-weight frontier is fine** — *"this is lightweight LLM inference
  based on categorisation and memory. It's not complex reasoning."* That is
  the right reading of §8.2 and I have put your words in §13. It also
  prompts a question back: **if it is not complex reasoning, does §8.2 need
  two tiers at all?** Measuring the cheap tier on the synthesis class
  before building the expensive one would make §8.6's cost argument
  stronger, not weaker. Not urgent; slice 2.

**Cost, corrected twice and now settled.** I first said it fell, then said
it hadn't much, and both were against the wrong baseline. Against draft 3's
~$5 and ~$100: **~$3/month all-cheap and ~$26 all-frontier**, so the
frontier month fell about 4× and the cheap one about 1.6×. Most of that is
§8.2 pinning dated DeepSeek ids, not the move off OpenRouter — worth
keeping straight so the next provider argument isn't made on a 4× that
belongs to a different decision.

**Now verified against your key (2026-09-05), with one caveat.** The
embeddings endpoint serves `qwen/qwen3-embedding-8b` as documented, and the
`text-embedding-3-small` probe — the one that would have shown them
proxying to OpenAI — **came back 403.** That is the good outcome, but read
it precisely: 403 is a *refusal*, not a *not-found*. A 404 would have said
"no such model"; 403 is consistent with a routable name your key isn't
entitled to. It rules out proxying-for-anyone, not proxying-for-someone.
§8.3 still says "narrowed, not closed", and the subprocessor question is
untouched by any of this — that needs the DPA, not the API.

**Two corrections to what I told you an hour ago**, both from my reading a
truncated list and drawing a conclusion from it: `moonshotai/kimi-k3` does
exist, and their website's model list does match the API. I said otherwise
on 27 of 27 models' worth of missing data. The same mistake §1.6 is about.

**And one real finding: `encoding_format: "base64"` is accepted and
silently ignored.** So ingest pulls ~4 GB over the wire for a 90k-chunk
corpus, not the ~2 GB I hedged with, and there is no opt-out (§7.2).

Still does not bite until slice 2.

## 2. Closed — search page, not a chat (decision 14, §8.4)

**Confirmed by you on 2026-09-05**, including that parking a conversational
surface in §12 is the right disposition rather than forbidding it. Also
settled the same day: keep DeepSeek for both tiers, keep two tiers *and*
still measure, and proceed with TensorX rather than holding for the DPA.
All four are recorded — decision 14 and §8.4; §8.2; §13; §13 again — and
Fable caught me claiming that before two of them were actually written.

**Two footnotes on rulings you gave, because the record should show what
you were shown.** When you kept DeepSeek I had told you the pinning rule
forced it; it didn't — a dated Qwen id exists, so a two-lab pair was
available and I hadn't put it in front of you. Nothing needs undoing, and
that Qwen id is now the named frontier fallback. And "proceed with TensorX
for sure" I have read narrowly: **the provider decision is settled and
slice 2 can be built against it, but the DPA and retention terms are still
owed before egress is switched on over real content.** If you meant the
wider thing, say so and §13 changes.

*The question as it stood, kept because the reasoning is the useful part:*

You asked whether we were building a chat UI or a hybrid. I answered
*hybrid, search-first* — one GET search form, one query in, one page out,
with an optional 🤖 paragraph above the citation list. §8.4 already implied
it; it now says it.

**But I first argued it as forced when it is chosen, and Fable caught me.**
I claimed decision 3 forbade a chat outright — that re-authorizing a
transcript would blow §6.5's latency budget. It would not: §4.1 batches 256
pairs a call and §6.5 makes a page one round, so a four-turn transcript is
one batch. A correct chat is affordable here. My other argument, that a
transcript is a cache of authorized passages, is true of any page left open
in a tab and so distinguishes nothing.

**So the honest version:** the invariant decision 3 actually requires is
that no passage crosses a request — not in a response, not in a model
prompt — and gate 32 enforces it. The *shape* is decision 5's call: the
search floor is the product, so the product is a search page. That is
**decision 14, attributed to me and not to you.** A conversational surface
that keeps the invariant is now parked in §12 as a thing to argue rather
than a thing forbidden.

**What I need:** confirm decision 14, or tell me you want the conversational
version considered properly. Either is cheap today and neither is cheap
after slice 2 ships an answer surface.

## 3. Is routine partial withholding acceptable? (§6.5)

The genuinely new product question, and the one I would most like your
view on.

Every search now asks each contributing sibling "may this person see
these, at this revision" — live, no caching, because caching authorization
is what makes a stale answer a leak. Consequence: **a slow or hibernating
sibling means the page renders without its passages and says so** —
*"Sheets did not answer; 7 results withheld."*

That is correct. It may also be constant, because siblings hibernate and a
cold wake is slower than any sane deadline. I have set 800 ms and pre-warm
on memoria's own wake, but if Sheets is habitually cold then the primary
search path habitually withholds, and a notice a reader sees every time is
a notice they stop reading.

There is no design that fixes this while failing closed. The spec's stated
position is that this is a **suite health problem memoria surfaces, not a
memoria problem to route around** — so the answer would be to fix or drop
the source, never to cache its authorization. Tell me if you would rather
trade differently, because the alternative is a short-TTL cache and that
is a stated leak window rather than a correctness fix.

## 4. Deploy it, or leave it local? — ANSWERED: it is deployed

Live at **https://memoria.oficina.works**, account `bds`, channel `edge`.
The origin moved off `memoria.bds.oncarlos.com` on 2026-09-04 once the
certificate issued; see the README's "Live" section for what that entailed
and what the old hostname still does.

Local is still genuinely usable — keymail accepts the loopback
`client_id`, which I verified.

## 5. Your rastrillo CLI is about six releases stale, and it affects every app

`rastrillo` on PATH predates v0.22.0: it has no `doctor`, and
`rastrillo new` scaffolds a **v0.17.0** pin. Two things follow, and the
second is the one that matters beyond memoria:

- I bumped memoria to v0.23.0 by hand and wrote `tools/vendorassets` to
  re-copy the three vendored files from the linked library, because there
  is no `doctor --fix` to do it.
- **The scaffold's `layout.html` links `tokens.css` but not
  `theme.css`**, and `tokens.css` defines eleven tokens, none of them
  colours. Any app scaffolded with this CLI renders with every colour
  resolving to `initial`, which invalidates whole shorthand declarations —
  rules vanish rather than the page going grey. That is the exact failure
  your own memory note describes for the page-header rule.

`go install github.com/carlosframework/rastrillo/cmd/rastrillo@v0.23.0`
would fix it, but it is a shared binary other sessions are using, so I
left it alone.

## 6. An anonymous visitor can make the instance dial any domain

Found by the code review, and it is a real hole rather than a wording
problem. rastrillo's sign-in classifier fetches
`https://<the address's domain>/.well-known/keymail` on `POST /auth/begin`
— and rastrillo runs that **before** the admission gate, so it happens for
addresses that are not on the owners list. Type
`anything@attacker.example` into the sign-in form and the instance makes
an outbound request to `attacker.example`.

What it gives an attacker: confirmation the instance exists and is
reachable, its egress IP, and a timing channel. Not content. But it also
made the footer's "nothing leaves this instance" false, which is why that
copy changed.

**I did not fix it**, because the fetch is inside `rastrillo/auth` and
`keymaildev/signin`, not in memoria — fixing it here would mean
re-implementing the classifier or gating `/auth/begin` on the owners list
before rastrillo sees it. The second is small and I can do it, but it
changes sign-in behaviour for guests, which slice 1 will want. Say which
you'd prefer, or whether it belongs upstream in rastrillo.

## 7. Not a question, but you should know

`gpt-5.6-sol` via Codex **reported its own model id as `gpt-5`** when
asked. Codex's own banner says `gpt-5.6-sol`. I recorded the discrepancy
in the spec's review table rather than picking one, since §Review requires
attributing a review to a model and the two sources disagree.

## 8. The deployed hostname has no `<team>` label, and the spec says it should

home ruling 16 and this project's own spec (§6.3, and line 4) put an
instance at **`<team>.<app>.oficina.works`**. In §6.3 that label is not
decoration: barrier 2 is *"memoria's client refuses any host whose
`<team>` label differs from its own"*, test-enforced against a fixture of
hostile hostnames. A hostname with no team label gives that guard nothing
to compare.

What is deployed, across the whole suite, is `<app>.oficina.works` — no
team label, no wildcard certificate: `docs.oficina.works`,
`sheets.oficina.works`, `calendar.oficina.works`, and now
`memoria.oficina.works`. I matched the four live siblings rather than
invent a team label on my own, because slice 0 has no siblings for barrier
2 to defend against and consistency with what is already running seemed
the safer default.

It stops being free at slice 3, when the connector client wants a label to
compare. Roughly the options: re-host every app under a team label before
then — a second origin change each, and every one is a sign-in cutover —
or amend ruling 16 to make the team label a slice-3 addition and say what
barrier 2 compares until it exists.

**I did not touch the spec.** Ruling 16 is settled and AGENTS.md forbids
relitigating one in a drive-by. This is the flag, not the change.
