Inside hostel, a Mac app, as built 22 to 25 September 2026

A local model keeps the books

hostel is a Mac app where a model running on your own machine keeps your books through MCP servers, with the same instructions Claude gets. I built it this week. This is what the pieces are, how one turn runs, and what the numbers say.

The finding it rests on

LM Studio, Jan and Open WebUI all hand a local model the tool descriptions of an MCP server and drop the server's instructions. saybooks puts its rules there: money in cents, never invent a date, ask one question at a time. Without them a small model guesses. With them, and with a few rules of the host's own, Gemma 4 26B went from one statement import in four to four in four, in a third of the time. hostel is the layer between the model and the record that nobody else owned.

The pieces

stays on this Macgoes out, and only when you choose it
THIS MAC hostel.app SwiftUI window, conversations, panels, PDF reader Ollama gemma4:26b, 17 GB loaded /api/chat, context 32k and up Node host (serve.mjs) the turn loop, the rules, the MCP client saybooks on this Mac bundled Node process, free port /mcp/<space>?modules=… and the UI web server (servers/web.mjs) stdio, two tools, off by default JSON lines events prompt, tools, think HTTP, tools/call stdio starts, pulls model saybooks.io your space, by sign-in only tool calls cross the internet search queries and pages, when a conversation asks HTTPS + token
Four processes on the Mac and two doors out of it. The books, the documents, what you type and attach, and the model's reading of all of it never cross the dashed line. Only a signed-in conversation's tool calls, or a web-enabled conversation's searches, do.

The window app is Swift. It draws the conversation, keeps the panels, reads PDFs, and starts two Node processes from copies bundled inside the app: the host and, when a conversation is on local books, saybooks itself. The host is where everything interesting happens. It talks to Ollama over its native API, because that is the only door where thinking can be switched per request, and it connects to MCP servers over stdio or HTTP with the official SDK. The app and the host speak JSON lines over a pipe: an op down (connect, say, stop, pull, signout), events up (ready, call, progress, turn, authorize).

One turn

the person words, maybe a file (PDF read to lines first) the prompt instructions + tool catalogue + history + file, with a note: pass it as "@name" the model thinks or not, then prose or tool calls the answer prose ends the turn; stats on the line the host checks repeat? missing field? "@name"? then runs the call the MCP server writes or refuses; the result goes into the prompt prose tool calls call result, next hop per-hop budget 4,000 tokens to answer, 3,000 more to think, 1,200 on a hop that carries a file; capped by the room left in the model's memory; up to 10 hops a turn
One turn is a loop of hops. Each hop is one request to the model. The host stands between the model and the server on every call, and it decides how many tokens each hop may spend.

Most of what makes hostel work is a short list of rules the host applies in that loop. Each one came from a failure I measured, and each says a sentence to the model rather than fixing anything silently.

Files by reference
The file's text is in the prompt so the model can read it, but a tool gets it as @name, and the host substitutes the contents. A whole statement is a tool call of under 300 characters. Retyping rows was where phantom rows and wrong digits came from.
Think on, off on file steps
Thinking helps the model pick tools and is dropped on a hop that carries a file, and on any hop where the memory has no room for it.
Verbatim retry
The exact call that was just refused, unchanged, is not run again. The model is told to change it.
Named field still missing
A refusal said "lines[1] has no description"; a retry that still lacks that field is not run.
Stuck
The same blocked call a third time, or one tool refused three times in a turn, ends the turn with the refusal as the answer.
Claimed, not done
A refused call followed by prose that says "I've corrected that and prepared the draft" gets one turn back: send the call, or say plainly it did not happen.
Dropped call
A call the runtime's parser could not read arrives as text with template markers in it. The host names it and asks once for a proper call.
Cut off
An answer cut by the budget is not acted on. The notice says to pass the file as @name. With the 1,200-token cap on file hops, a retype is cut in about a minute instead of running for five.
Room
When a hop would be cut by the memory left, the context doubles first, 32k to 64k to 128k, while the Mac's memory pressure is not critical. With no room at all, the turn ends in one sentence instead of nudging.
Step cap
Ten hops with no answer end with a sentence saying what was tried, not with silence.

A PDF becomes rows

The app reads a PDF's text layer in a tenth of a second. A page with no text layer, or a picture of a page, is drawn on white at 200 dpi and read by Vision. The model never sees pixels. Bank statements then get one more pass: the reader finds the table by its header, reads each number by its column position, drops the running balance, takes the year from the period line, joins descriptions that wrap, and ends the text with the row count and the deposit and withdrawal totals. On a real six-page bank statement that gave every row, summing exactly to the balance change. The import's own reconciliation is what catches a misread digit: opening balance plus rows must equal closing balance, or nothing is written.

Conversations own their books

one conversation, one file lines: what you saw history: what the model saw, with the attachments connection: mode: on this Mac | saybooks.io | none space and modules (local) sign-in token file (hosted) web: on or off locked after the first message on this Mac /mcp/<space>?modules=core,solo,purchases 58 tools; all modules would be 144 saybooks.io one token file per sign-in, one space each 63 tools, the space's own mounts + web (2 tools) the host connect: servers + history same books as before? swap the history only other books? reconnect, history in hand; the tool list changes, so the model re-reads its prompt once the sidebar names the books under each title
A conversation carries its connection for life. A new one starts where the current one is and can be pointed elsewhere until its first message.

Modules matter because tool count is prompt size. Core with invoices and purchases is 58 tools; everything is 144. Fewer tools means a smaller prompt and a model that picks the right tool more often, so the default for a new local space is small.

Signing in to saybooks.io

the app the host the browser saybooks.io Sign in with Google registers hostel as a public client, once per sign-in authorize {url} opens the consent page Google sign-in, pick the space and the role, Allow redirect to a loopback port the host listens on, with the code code and PKCE verifier for tokens (the refresh token lasts a year), then connect
Nothing is pasted by hand. The token file stays on the Mac, one per space, and the next launch reconnects without asking. Sign out forgets it; the key itself is revoked on saybooks.io.

On disk

Everything the app writes lives in its own folder under Application Support: the conversations, the local books as one SQLite database per space, and the saybooks.io sign-ins. Nothing inside the app bundle is written at runtime, so its signature survives use. The model lives in Ollama's own store. The saybooks copy inside the app carries the commit it was built from, and the app shows it, so a stale bundle can be told apart.

What the numbers say

MeasuredResult
Statement script, four tasks, Gemma 4 26B in LM Studio1 of 4 in 15 min, 4 refusals
The same, in hostel4 of 4 in 6 min, 0 refusals
A real bank statement as PDF, first working run in a long conversation1,226 s
Same, fresh conversation, model cold277 s
Same, with the 1,200-token cap on file hops91 s warm
Same, after saybooks put text first in the import's schema85 s, first attempt right
Context 32k, 64k, 128k: model in memory17, 17, 18 GB
Re-read of a 65k-token prompt after a context swap380 s
Tools mounted: everything; core; core and invoices; core, invoices and purchases144, 25, 35, 58
The statement rows are the ones to remember. Five runs of prose from either side could not stop the model retyping rows on its first attempt. Putting the text argument first in the tool's schema did it in one. The shape of a tool decides more than any instruction about it.

What is not there yet

The design article from earlier this week says why the layer exists and what the first measurements were. This page is what it became by the end of the week.