122 lines
8.7 KiB
Markdown
122 lines
8.7 KiB
Markdown
# 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
|
||
|
||
```sh
|
||
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`.
|