Files
crossbar/docs/plans/v1
..

v1 implementation plan: leases, queueing, accounting

For the implementing model: do not work from this file. The owner gives you one task file at a time (01-… to 07-…). This file is the index for the owner and the reviewer.

Goal: PLAN.md §4–§7a. Every conversation gets a sticky lease on one host (chosen by free slots × weight when it starts), requests queue per (host, model) instead of overflowing a router, an operator can pin a route or drain a host, and SQLite keeps the leases and an accounting log that answers "which session used which host and model, for how long, at what cache-hit rate".

Architecture: five new packages — store (SQLite, modernc.org/sqlite), fingerprint (conversation key), choose (the scoring rule), limiter (per-(host, model) slots + bounded FIFO), lease (the sticky table, persisted through store) — and v1 versions of proxy, admin, config and cmd/crossbar. The proxy tees streamed responses through an SSE scanner to read the final usage/timings chunk; it never buffers or alters the stream.

How this plan was made: acceptance tests first, from PLAN.md; no reference implementation. Every given test file was compiled against a panic-only skeleton of the interfaces named in the tasks (go vet ./... clean), and nothing else was run. If a test turns out to be wrong, that is the owner's finding: stop and report as AGENTS.md says; do not edit it.

Tech stack: Go 1.26, stdlib, github.com/BurntSushi/toml v1.6.0, modernc.org/sqlite v1.59.0 (pure Go; go.sum given). No other module.

Global constraints

  • Everything in AGENTS.md. Branch v1. One task, one fresh OpenCode session, one commit.
  • Bodies are never logged or stored. Rows carry names, counts and timings only.
  • Given files (tests, example.toml, cmd/fakeupstream/main.go, tools/smoke.sh, go.sum) are copied and never edited. Some replace v0 files of the same name; the task says so.

Tasks

# File Delivers Tests that define it
01 01-store.md internal/store: SQLite state + accounting internal/store/store_test.go
02 02-fingerprint-config.md internal/fingerprint; config gains db, lease_idle, retention, day suffix fingerprint_test.go, config_v1_test.go
03 03-limiter-choose.md internal/limiter, internal/choose limiter_test.go, choose_test.go
04 04-lease.md internal/lease: the sticky table lease_test.go
05 05-proxy.md proxy v1: leases, queue, SSE tee, accounting, header route proxy_test.go (replaces v0's)
06 06-admin.md admin v1 (pin/release/drain/usage/metrics) admin_test.go (replaces)
07 07-main.md cmd/crossbar wiring, background loops start/stop check
08 08-smoke-readme.md tools/smoke.sh v1 run, README update make smoke

For the owner: running a task

tools/run-plan.sh docs/plans/v1          # from a clean checkout on master

For the reviewer: after task 08

  1. git log --oneline master..v1: eight task commits with the trailer (plus owner merges).
  2. Copied files byte-identical to _files/; git diff <merge-base>..v1 --stat -- PLAN.md AGENTS.md docs/plans empty.
  3. make gate, make smoke.
  4. Read every source file against its task. Probe outside the tests: a lease whose host is drained and unhealthy; lease_idle expiry while a request is in flight; a stream cut by the client mid-way (the accounting row must still be written, with the status it had); a body with "messages" that is not an array; two crossbars on the same db file; Prune while requests are being recorded.
  5. Every .( type assertion in internal/ is the two-value form or on a value we constructed.
  6. Findings under "Reviews" in docs/implementer-log.md, by fault (model / task / test).

Changes during the run

  • 2026-09-25, task 01, first attempt: three given test files under _files/ were not gofmt-clean, and the gate's gofmt -l . walks every file, so the gate failed on files the implementer may not edit. Ornith diagnosed it (gofmt on its own code was clean) and did not touch them. Owner's fault (the compile check ran go vet, not gofmt, on the given files). Fixed by formatting the given files; the plan checklist now includes gofmt -l docs/ before handover. Ornith stopped correctly (a stopped row, code left in the tree, only the log committed); a second session was told where the first had stopped and finished steps 6–7 without starting over — the boxmaker precedent (M3a/19). The driver was then resumed from task 02.
  • 2026-09-25, task 02: v0's given testdata/bad-unknown-key.toml used lease_idle as its unknown-key example, and v1 makes lease_idle a real key, so v0's TestBadFiles broke — the task did not hand over replacements for the two v0 given files (tip T19). Ornith changed the example to bogus_key in both files and logged it in Deviations; the content is right, but the files were protected and the rule was to stop. Owner's fault for the conflict; the model's deviation is noted. The corrected files now sit in _files/internal/config/ as the reference copies for the reviewer's byte-exact check.
  • 2026-09-25, task 04: my TestPinAndUnpin asserted the pin event at exactly len-3 and the release event at len-1, but the task's own rules make acquires under a pin record events too, so a faithful implementation produces [new pin pin new new release] and the assertion cannot hold. Ornith spent its first ten minutes puzzling over exactly that. Test fault (mine): the assertion now checks order and content (a pin event naming beta, followed later by a release), not positions.
  • 2026-09-25, before task 05 ran: a hand walk of the remaining given tests against the task rules (which I should have done before handover) found two more faults of mine and one flake: TestDifferentConversationsSpreadByFreeSlots assumed two conversations would both land on the weight-2 host, but the scoring rule's tie-break sends the second to the other host — it now uses a config where beta's weight is 10; TestPinReleaseDrain (admin) pins a route no request has used, which lease.Pin's "host must have been seen" rule refuses — task 06 now adds lease.Table.Candidates and has the admin and main register configured hosts; TestQueueFullIs503 read the store right after the responses without the retry loop the accounting test has. Task 05 restarted from a clean tree on the corrected files.
  • 2026-09-25, task 05, first session: ended after ~25 min without a commit or a row, 6 of 8 given tests passing. Three things happened. (a) My replacement proxy_test.go dropped the fakeHealth helper that v0.1's recorder_test.go uses; Ornith recreated it as helpers_test.go — right call, unlisted file; it is now a given file (task fault). (b) My fake upstream's /v1/models handler did not record requests, so TestV0BehaviourStillHolds read an empty record — test fault, fixed. (c) Ornith tried to write an experiment under /tmp, the sandbox refused, and it ended its turn with "Let me experiment…" and no tool call — the known Ornith failure mode (I9), here triggered by a denied tool. Model fault; task 05 now says a refusal is not a reason to stop. The tee's token extraction (TestAccountingRows…) was the genuinely unfinished part. Resumed from the working tree.
  • 2026-09-25, task 05, resume session: my given proxy_test.go was 433 lines, over the gate's 400-line limit that scripts/check-lines.sh applies to every .go file including the copied test and the plan copy under docs/. Ornith found it and went digging in git history instead of stopping. Test-file fault (mine): the rig and fake-upstream scaffolding moved into helpers_test.go (211 + 271 lines). Ornith's own proxy.go was also at 411 lines; splitting it is part of the task as written.
  • 2026-09-25, task 06, first session: 50 minutes, admin package never compiled (duplicate Handler, re-reading the same files in a loop) while the store and lease additions it made were correct. Too large a task for one session — the size lesson from boxmaker, mine to apply. Split into 06-admin.md (handler only; inherits the store/lease additions from the tree) and 07-main.md (wiring); the smoke task became 08. Session stopped by the owner; the broken admin.go draft was left in the tree for the next session to replace.
  • 2026-09-25, task 06 (admin), during the run: the task text said the wiring in cmd/crossbar "does not fail the gate", but go vet ./... compiles main.go, whose two-argument admin.Handler call no longer matches — the gate does fail. Ornith noticed while reading. Task fault (mine): step 5 now allows the one-call edit to main.go.
  • 2026-09-25, task 08, first session: smoke check 1 sent conversation A to alpha, not beta. Cause: my task 05 rule Free: lim.FreeSlots(host) sums a host's slots across all its models, so alpha's six small-9b slots outscored beta's two ornith slots for an ornith request. The smoke's expectation (per-model slots) is the right semantics. Task fault (mine): task 05's rule is corrected and task 08 gained step 1b, the one code change allowed in it. Ornith had diagnosed the summing correctly before it was stopped.