Files
crossbar/docs/plans/v1/02-fingerprint-config.md
T

4.0 KiB
Raw Blame History

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
  • Copy (replaces v0's): internal/config/config_test.go, internal/config/testdata/bad-unknown-key.toml (v0's unknown-key example was lease_idle, which this task makes valid; the replacements use bogus_key)
  • 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:

  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:

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; 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.
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/.