Every given test compiled against a panic-only interface skeleton (go vet clean); nothing was implemented. modernc.org/sqlite v1.59.0 vetted in a scratch module (WAL works); go.sum given. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3.8 KiB
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:
// 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:
- Decode
{"messages":[{"role":…,"content":…}, …]}.contentis either a string or an array of parts[{"type":"text","text":"…"}, …]; for an array, join thetextof the text parts with""(other part types are ignored). Usejson.Unmarshalinto a struct withContent json.RawMessage, then decide. - System prompt = content of the first message with
role == "system"(or""if none). First user message = content of the first message withrole == "user"; no user message → return"". Not JSON, or nomessages→"". - Truncate each of the two strings to its first 4096 bytes, hash
system + "\n" + userwithcrypto/sha256, returnhex.EncodeToString.
internal/config gains, in Config:
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.
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; extendconfig.go.gofmt -w internal/. - 4. See them pass.
go test -race -count=1 ./internal/fingerprint/ ./internal/config/. Expected: bothok(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.
git add internal/fingerprint internal/config docs/implementer-log.md
git commit
Done when
- Both packages pass with
-race;make gateprintsgate: ok; both copied files are byte-identical to_files/.