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>
87 lines
3.8 KiB
Markdown
87 lines
3.8 KiB
Markdown
# 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/`.
|