Design: split into two homebrew services - puller + reads - around the shared DuckDB file #3

Open
opened 2026-05-23 20:55:33 +00:00 by coilysiren · 0 comments
Owner

Originally filed by @coilysiren on 2026-05-20T06:58:51Z - https://github.com/coilysiren/session-lattice/issues/27

Problem

Today session_lattice/service.py puts the puller and the read API in one process. create_app() spawns refresh.run(config, stop_event) as an asyncio task inside the FastAPI lifespan; the same uvicorn worker serves HTTP and holds the DuckDB RW handle.

That's fine pre-cable, but once the puller actually pulls from repo-recall:

  • A wedged or slow pull stalls the HTTP event loop (or shares its FD budget, CPU, GC pressure).
  • One restart drops the read API. Independent restart cadence is impossible.
  • Backpressure on the puller (e.g. retrying a 502 from repo-recall) bleeds into read latency.
  • A panic in materialization kills the read API.

Design: split into two homebrew services around the shared .duckdb file

DuckDB is an embedded library + a file. Many processes can attach read-only to the same file concurrently; only one can hold read-write. So the natural cleave is:

  1. session-lattice-puller - long-running worker. Holds the RW handle. Pulls from SESSION_LATTICE_REPO_RECALL_URL on a tick, runs the per-view materialization. No HTTP listener. Probably no port at all (status via logs and a stamp row in DuckDB).
  2. session-lattice-reads - FastAPI HTTP server on the existing port (7778 staging / 7781 dev / tailnet prod). Opens RO handles per request. Never writes.

Both ship from this repo, both consume the same session_lattice package, both target the same SESSION_LATTICE_HOME. Different bin/ entry points (session-lattice-puller and session-lattice-reads, or one binary with serve-reads / serve-puller subcommands - cleaner).

Homebrew shape

Two service do blocks in Formula/session-lattice.rb (modern brew supports this via per-service name), or one block per env variant. Each block keeps keep_alive true. mcporter slots stay pointed at the reads-service ports.

~/.session-lattice/session-lattice.duckdb is the contract surface between them. DuckDB's WAL semantics mean RO readers see a snapshot at attach time, so brief writer pauses are invisible to in-flight read handlers.

Rollout order

This lands as part of, not before, the puller's first real implementation:

  1. First commit: add serve-puller / serve-reads subcommands to the CLI, keep serve as a transitional wrapper that runs both in one process (so existing brew installs keep working).
  2. Second commit: implement the actual puller against repo-recall (today refresh.py is a stub).
  3. Third commit: add the second service do block, retire serve in favor of the split.
  4. Formula bump + brew upgrade + brew services start session-lattice-puller.

Out of scope

The MCP host (/mcp endpoint that mcporter points at) lives on the reads service. The puller doesn't serve HTTP.

Touches: AGENTS.md (rule about "brew binary is the contract" generalizes to both services). README install steps grow a second brew services start. coilysiren/session-lattice#23 (no-uv-run-from-checkout wording) needs revisiting once the split lands.

_Originally filed by @coilysiren on 2026-05-20T06:58:51Z - [https://github.com/coilysiren/session-lattice/issues/27](https://github.com/coilysiren/session-lattice/issues/27)_ **Problem** Today `session_lattice/service.py` puts the puller and the read API in one process. `create_app()` spawns `refresh.run(config, stop_event)` as an asyncio task inside the FastAPI lifespan; the same uvicorn worker serves HTTP and holds the DuckDB RW handle. That's fine pre-cable, but once the puller actually pulls from repo-recall: - A wedged or slow pull stalls the HTTP event loop (or shares its FD budget, CPU, GC pressure). - One restart drops the read API. Independent restart cadence is impossible. - Backpressure on the puller (e.g. retrying a 502 from repo-recall) bleeds into read latency. - A panic in materialization kills the read API. **Design: split into two homebrew services around the shared `.duckdb` file** DuckDB is an embedded library + a file. Many processes can attach read-only to the same file concurrently; only one can hold read-write. So the natural cleave is: 1. **`session-lattice-puller`** - long-running worker. Holds the RW handle. Pulls from `SESSION_LATTICE_REPO_RECALL_URL` on a tick, runs the per-view materialization. No HTTP listener. Probably no port at all (status via logs and a stamp row in DuckDB). 2. **`session-lattice-reads`** - FastAPI HTTP server on the existing port (7778 staging / 7781 dev / tailnet prod). Opens RO handles per request. Never writes. Both ship from this repo, both consume the same `session_lattice` package, both target the same `SESSION_LATTICE_HOME`. Different `bin/` entry points (`session-lattice-puller` and `session-lattice-reads`, or one binary with `serve-reads` / `serve-puller` subcommands - cleaner). **Homebrew shape** Two `service do` blocks in `Formula/session-lattice.rb` (modern brew supports this via per-service `name`), or one block per env variant. Each block keeps `keep_alive true`. mcporter slots stay pointed at the reads-service ports. `~/.session-lattice/session-lattice.duckdb` is the contract surface between them. DuckDB's WAL semantics mean RO readers see a snapshot at attach time, so brief writer pauses are invisible to in-flight read handlers. **Rollout order** This lands as part of, not before, the puller's first real implementation: 1. First commit: add `serve-puller` / `serve-reads` subcommands to the CLI, keep `serve` as a transitional wrapper that runs both in one process (so existing brew installs keep working). 2. Second commit: implement the actual puller against repo-recall (today `refresh.py` is a stub). 3. Third commit: add the second `service do` block, retire `serve` in favor of the split. 4. Formula bump + brew upgrade + `brew services start session-lattice-puller`. **Out of scope** The MCP host (`/mcp` endpoint that mcporter points at) lives on the reads service. The puller doesn't serve HTTP. Touches: AGENTS.md (rule about "brew binary is the contract" generalizes to both services). README install steps grow a second `brew services start`. coilysiren/session-lattice#23 (no-uv-run-from-checkout wording) needs revisiting once the split lands.
coilysiren added
P3
and removed
P2
labels 2026-05-31 07:01:23 +00:00
Sign in to join this conversation.
No labels
P0
P1
P2
P3
P4
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
coilyco-flight-deck/session-lattice#3
No description provided.