compose.yaml joins the Pi's existing network as external and reaches the
Postgres and Caddy already there by name; nothing is published to the
host. The schema applies itself on boot, so there is no migration step to
run by hand.
The Caddy snippet in the README is the part worth reading. Excluding
/api/tutor from `encode` matters more than flush_interval: compression
delays the header flush until body bytes arrive and holds already-flushed
events inside an unfinished frame, which presents as a stream that hangs
rather than as an error.
Untested against the actual Pi — written from PORT.md and verified only
as far as building the image and running it against a local Postgres.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
POST /api/tutor takes the assembled system prompt, the transcript the
client owns, and the new message, and streams tokens back. It holds
nothing between requests, so a dropped connection costs one turn rather
than the conversation.
The seam moved first: Sample took the assembled prompt as messages[0] with
role user. It now has an explicit system field, which is what lets the
backend put it in the API's system parameter as a cached block. The gate
is ~12k characters and is byte-identical for as long as the learner stays
in one unit, so every turn after the first reads the prefix at a fraction
of the input price. That is the single biggest cost lever in the design,
and it was unreachable through the old shape.
prompt/tutor-system.md still ships unchanged; only where the string is
placed changed.
SSE has three rules that are silent when broken, and all three are
handled: every event ends with a blank line, payloads are JSON-encoded
because a raw newline in Korean text would break the framing, and a `:`
heartbeat every 15s keeps intermediaries from timing the stream out.
Cache-Control is set on the returned Response rather than inside
streamSSE, which writes its own and would overwrite it; `no-transform` is
there because compression, not buffering, is what usually makes SSE look
like it hangs behind a proxy.
The client uses fetch + getReader, not EventSource — EventSource cannot
POST, and the body is {system, history, message}. Aborting closes the
connection, the server aborts upstream, and a cancelled turn stops
billing. With no server configured the app falls back to the stub, so the
offline build is untouched.
backends/anthropic.ts is the default. backends/agent-sdk.ts is deliberately
unimplemented and documents why the plain API was chosen over PORT.md's
Agent SDK — chiefly that its prompt accepts only user-role messages, so
the transcript would have to be flattened into a single turn.
The endpoint's own tests use a mock backend and need no API key.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Hono and pg, run under --experimental-strip-types, so the deployed thing
is the source. GET /api/sync?cursor=N pages rows above the cursor;
POST /api/sync upserts last-write-wins. Bearer token on everything under
/api; /health is open, for the container healthcheck.
Rows are stored generically — primary key as text, body as JSONB —
because the server never reads inside a row. It stores and orders them and
the client interprets them, which keeps the two schemas from having to
move in lockstep.
change_seq is bumped by a BEFORE UPDATE trigger rather than by the write
path. A row edited after a client last pulled would otherwise keep its old
sequence, sit below that client's cursor, and never be delivered; putting
it in the database means no future write path can forget.
The last-write-wins comparison is in the ON CONFLICT clause itself, so a
losing row is not written at all and does not bump change_seq — a
conflict does not become traffic for every other device.
test/sync/roundtrip.test.ts runs two clients against a real Postgres and
asserts what actually goes wrong in sync: that a fresh client's seeded rows
cannot overwrite the server's history (the artifact's bug, as an executable
test), that a delete propagates, and that dict.loadedBands never crosses
the wire. It skips without HANKAN_TEST_SERVER, so npm test still runs
anywhere.
POST /api/test/reset exists only when HANKAN_TEST_MODE=1, so it cannot be
reached on the Pi even if the token leaks.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
README covers how the pieces fit: the storage interface, the inverted
frequency join, the gate's three refinements, and the checks that guard
each one.
server/ holds a README only. Steps 1-3 give a fully working offline app with
no server at all, and that is the version that gets used first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>