oficina / memoria Public

Clone
git clone https://amadan.net/oficina/memoria

Plain git — no account needed to clone.

Download

Download this file

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.