# v1 task 02: the conversation fingerprint; config additions **Branch:** `v1` (run `git switch v1`; `git status --short` must be empty, otherwise stop) **Commit subject:** `Add the conversation fingerprint and the v1 config keys` ## Goal Two small things. `fingerprint.Of` turns a chat-completions body into a stable key for the conversation it belongs to (`PLAN.md` §4a), and `config` learns `db`, `lease_idle` and `retention`, with durations that accept a `d` suffix. ## Context OpenCode and Hermes send no session id. A conversation's system prompt and its **first user message** do not change from turn to turn, so hashing those two identifies the conversation without client support. Only the first 4 KiB of each is hashed, so a huge first message does not make every turn slow, and a body with no user message has no fingerprint (`""`): such requests fall back to the route-level lease (task 04). ## Files - Copy: `internal/fingerprint/fingerprint_test.go`, `internal/config/config_v1_test.go` - Create: `internal/fingerprint/fingerprint.go` - Modify: `internal/config/config.go`, `docs/implementer-log.md` ## Interfaces `internal/fingerprint`, package `fingerprint`: ```go // Of returns the lowercase hex SHA-256 of the system prompt and the first user message of a // chat-completions body (first 4 KiB of each, joined with "\n"), or "" when the body is not a // JSON object with a "messages" array containing a user message. func Of(body []byte) string ``` Rules the tests check: 1. Decode `{"messages":[{"role":…,"content":…}, …]}`. `content` is either a string or an array of parts `[{"type":"text","text":"…"}, …]`; for an array, join the `text` of the text parts with `""` (other part types are ignored). Use `json.Unmarshal` into a struct with `Content json.RawMessage`, then decide. 2. System prompt = content of the **first** message with `role == "system"` (or `""` if none). First user message = content of the **first** message with `role == "user"`; **no user message → return `""`**. Not JSON, or no `messages` → `""`. 3. Truncate each of the two strings to its first 4096 **bytes**, hash `system + "\n" + user` with `crypto/sha256`, return `hex.EncodeToString`. `internal/config` gains, in `Config`: ```go DB string `toml:"db"` // default "crossbar.db"; empty string is an error (field "db") LeaseIdle Duration `toml:"lease_idle"` // default 30m; less than 1m is an error (field "lease_idle") Retention Duration `toml:"retention"` // default 180d; less than 1d is an error (field "retention") ``` and `Duration.UnmarshalText` accepts an integer followed by `d` (`"7d"` = 7 × 24 h) **in addition to** `time.ParseDuration` syntax. Exactly: if the text matches `^[0-9]+d$`, multiply; otherwise `time.ParseDuration`. `"1.5d"`, `"d"`, `"1d2h"` are errors. Validation order of the new fields: after `queue_max`, before `hosts`. ## Steps - [ ] **1. Copy.** ```sh git switch v1 mkdir -p internal/fingerprint cp docs/plans/v1/_files/internal/fingerprint/fingerprint_test.go internal/fingerprint/ cp docs/plans/v1/_files/internal/config/config_v1_test.go internal/config/ ``` - [ ] **2. See them fail.** `go test ./internal/fingerprint/ ./internal/config/`. Expected: compile errors. - [ ] **3. Write `fingerprint.go`; extend `config.go`.** `gofmt -w internal/`. - [ ] **4. See them pass.** `go test -race -count=1 ./internal/fingerprint/ ./internal/config/`. Expected: both `ok` (the v0 config tests must still pass). - [ ] **5. Run the gate.** `make gate`. Expected last line: `gate: ok`. - [ ] **6. Log and commit.** Row `v1/02-fingerprint-config`. ```sh git add internal/fingerprint internal/config docs/implementer-log.md git commit ``` ## Done when - Both packages pass with `-race`; `make gate` prints `gate: ok`; both copied files are byte-identical to `_files/`.