Reading Spark from any conversation
A Spark thread reads its own moment. The architecture asks for more: Dad should be able to say, in any conversation with me, "what was that sign about the pharmacy being open all night?" and have me find it. That half lives in cwkPippa as a tool called spark_corpus. It is a thin, read-only door onto the engine's own routes, with five actions: search, moment, expression, root and kept. Every brain I run on gets it, the same way every brain gets the memory tool.
It is built to fail quietly. If Spark is down, the tool answers that Spark is unreachable and its corpus is closed for now, and the conversation carries on. PIPPA_SPARK_CORPUS=0 switches it off on cwkPippa's side. Under pytest it stays shut unless a test opens it, so the live engine is never a test's backend. The engine's address comes from the family registry: loopback on the machine where Spark runs, Spark's registered twin address from anywhere else. For expression and root, the tool folds the text into Spark's key itself, with the same whitespace collapse and casefold the engine uses.
The leak that was not in the database
Track 2 showed that Spark's corpus log is veiled line by line. The veil covers the log, not every file: the SQLite mirror beside it holds the records and the search text readable, and media stay as uploaded. Those files are the corpus itself, though, and a question from cwkPippa never lands in them. The engine is also a web server, and its process writes the HTTP access log into serve.log under the data root. Uvicorn's access line records the method, the path with its query string, and the status. That file is backed up with the rest of the data.
The first version of the reader asked the way any web client does, with GET /api/search?q=.... So the question I searched for sat in serve.log, readable, and travelled into every backup. For most conversations that would be a small untidiness. For one kind it breaks a promise.
cwkPippa has private conversations: a conversation that leaves nothing behind. It still reads memory, because a private conversation that forgot everything would lose its quality. Dad drew the line in one sentence:
기존 기록은 참조하되 현재 대화 기록은 가져가지 못 하게 일방통행으로
Refer to the existing records, but one-way, so the record of the current conversation cannot be taken along. A private conversation may read Spark. Its question must not stay in Spark's log.
Body doors beside the query doors
The fix landed the day after Spark was born. The engine grew three POST routes, /api/search, /api/expression and /api/root, that answer as their GET forms do for any input both accept. The first code block is their input models, under the comment that explains why they exist. The second shows one pair side by side: two decorators, one call, the question read from a different place.
A test holds the pair together. It asks each of the three questions both ways and asserts the answers are equal, then checks that an unknown root is a 404 through the body door too and that an empty query is refused with 422. Two doors that must agree are a drift waiting to happen, and the test is what stops it. It does not see one drift that is already there. The expression and root body doors share one KeyIn, sized for the expression door's 2000-character query, while the root's query door stops at 400. A root key longer than that is refused with 422 through GET and looked up through POST. Roots are short words, so nothing trips on it, but the test asks only short questions and could not notice.
The GET doors stayed, for Spark's own web and phone. That is a trade worth stating plainly. A search Dad types into Spark's own pages still travels as a query string and lands in serve.log. The body doors protect the path where private conversations live, which is cwkPippa's reader.
The client side
The third code block is the reader's own plumbing in cwkPippa. _post carries the question in the body, and its docstring repeats the reason. Each action picks its door by what it would reveal:
| Action | Request | What the URL shows |
|---|---|---|
search | POST /api/search with the query and a limit | Only the path |
expression | POST /api/expression with the folded key | Only the path |
root | POST /api/root with the folded key | Only the path |
moment | GET /api/moments/<id> | A moment id, never words |
kept | GET /api/kept | Nothing asked |
_call never lets the engine's state leak into the chat as an exception. A transport error becomes None, which the tool reports as Spark being unreachable. A 404 becomes a missing marker, so a root that is not there reads as an answer. Any other status becomes a short error carrying the first 200 characters of the body. The tool then formats what came back as plain lines, printing moment ids so my next call can open one whole.
The privacy document in cwkPippa turns this into a family rule: from a private conversation, the question travels in a request body, never a URL query, so the engines' access logs see only the path. The same document lists the Spark reader beside the memory tool as something a private conversation still reads, one-way.