Add the conversation fingerprint and the v1 config keys

Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
2026-09-25 03:46:05 -07:00
parent 816614d6dd
commit 463cea18de
7 changed files with 317 additions and 5 deletions
+75 -3
View File
@@ -15,18 +15,25 @@ import (
"os"
"regexp"
"sort"
"strconv"
"strings"
"time"
"github.com/BurntSushi/toml"
)
// Duration is a time.Duration that TOML reads from a string such as "60s" or
// "30m".
// Duration is a time.Duration that TOML reads from a string such as "60s",
// "30m", or "7d" (an integer number of days).
type Duration struct{ time.Duration }
// UnmarshalText implements encoding.TextUnmarshaler via time.ParseDuration.
// UnmarshalText implements encoding.TextUnmarshaler. It accepts the "Nd" form
// — an integer number of days, so "7d" is 7 × 24h — in addition to
// time.ParseDuration syntax.
func (d *Duration) UnmarshalText(text []byte) error {
if days, ok := parseDays(text); ok {
d.Duration = days
return nil
}
dt, err := time.ParseDuration(string(text))
if err != nil {
return err
@@ -35,6 +42,22 @@ func (d *Duration) UnmarshalText(text []byte) error {
return nil
}
// dayPattern matches a run of digits followed by "d", e.g. "7d".
var dayPattern = regexp.MustCompile(`^[0-9]+d$`)
// parseDays reports whether text is the "Nd" day form and returns that many
// hours. The regex guarantees the prefix is a base-10 integer.
func parseDays(text []byte) (time.Duration, bool) {
if !dayPattern.MatchString(string(text)) {
return 0, false
}
n, err := strconv.Atoi(string(text[:len(text)-1]))
if err != nil {
return 0, false
}
return time.Duration(n) * 24 * time.Hour, true
}
// Model is the per-model tuning carried by a host entry.
type Model struct {
Parallel int `toml:"parallel"`
@@ -58,6 +81,9 @@ type Config struct {
Listen string `toml:"listen"`
PollInterval Duration `toml:"poll_interval"`
QueueMax int `toml:"queue_max"`
DB string `toml:"db"`
LeaseIdle Duration `toml:"lease_idle"`
Retention Duration `toml:"retention"`
Hosts map[string]Host `toml:"hosts"`
Routes map[string]Route `toml:"routes"`
}
@@ -76,6 +102,13 @@ const (
DefaultPollInterval = 60 * time.Second
DefaultQueueMax = 8
MinPollInterval = time.Second
DefaultDB = "crossbar.db"
DefaultLeaseIdle = 30 * time.Minute
DefaultRetention = 180 * 24 * time.Hour
MinLeaseIdle = time.Minute
MinRetention = 24 * time.Hour
)
var routeName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
@@ -109,6 +142,15 @@ func Parse(r io.Reader) (*Config, error) {
return nil, &Error{Field: keys[0], Msg: "unknown key"}
}
if !md.IsDefined("db") {
c.DB = DefaultDB
}
if !md.IsDefined("lease_idle") {
c.LeaseIdle.Duration = DefaultLeaseIdle
}
if !md.IsDefined("retention") {
c.Retention.Duration = DefaultRetention
}
if c.PollInterval.Duration == 0 {
c.PollInterval.Duration = DefaultPollInterval
}
@@ -152,6 +194,15 @@ func (c *Config) validate() *Error {
if e := c.checkQueue(); e != nil {
return e
}
if e := c.checkDB(); e != nil {
return e
}
if e := c.checkLeaseIdle(); e != nil {
return e
}
if e := c.checkRetention(); e != nil {
return e
}
if e := c.checkHosts(); e != nil {
return e
}
@@ -193,6 +244,27 @@ func (c *Config) checkQueue() *Error {
return nil
}
func (c *Config) checkDB() *Error {
if c.DB == "" {
return &Error{Field: "db", Msg: "required"}
}
return nil
}
func (c *Config) checkLeaseIdle() *Error {
if c.LeaseIdle.Duration < MinLeaseIdle {
return &Error{Field: "lease_idle", Msg: "must be at least 1m"}
}
return nil
}
func (c *Config) checkRetention() *Error {
if c.Retention.Duration < MinRetention {
return &Error{Field: "retention", Msg: "must be at least 1d"}
}
return nil
}
func (c *Config) checkHosts() *Error {
if len(c.Hosts) == 0 {
return &Error{Field: "hosts", Msg: "at least one required"}
+1 -1
View File
@@ -81,7 +81,7 @@ func TestBadFiles(t *testing.T) {
{"bad-listen.toml", "listen"},
{"bad-unknown-host.toml", "routes.r.hosts"},
{"bad-default-model.toml", "routes.r.default_model"},
{"bad-unknown-key.toml", "lease_idle"},
{"bad-unknown-key.toml", "bogus_key"},
}
for _, tc := range cases {
t.Run(tc.file, func(t *testing.T) {
+81
View File
@@ -0,0 +1,81 @@
package config_test
import (
"strings"
"testing"
"time"
"git.wntrmute.dev/kyle/crossbar/internal/config"
)
const v1Base = `
listen = "127.0.0.1:1"
[hosts.a]
base_url = "http://a:1"
models = { "m" = { } }
[routes.r]
hosts = ["a"]
`
func TestV1Defaults(t *testing.T) {
c, err := config.Parse(strings.NewReader(v1Base))
if err != nil {
t.Fatal(err)
}
if c.DB != "crossbar.db" {
t.Errorf("DB default = %q", c.DB)
}
if c.LeaseIdle.Duration != 30*time.Minute {
t.Errorf("LeaseIdle default = %v", c.LeaseIdle.Duration)
}
if c.Retention.Duration != 180*24*time.Hour {
t.Errorf("Retention default = %v", c.Retention.Duration)
}
}
func TestV1Values(t *testing.T) {
c, err := config.Parse(strings.NewReader(`
db = "/var/lib/crossbar/crossbar.db"
lease_idle = "45m"
retention = "30d"
` + v1Base))
if err != nil {
t.Fatal(err)
}
if c.DB != "/var/lib/crossbar/crossbar.db" || c.LeaseIdle.Duration != 45*time.Minute || c.Retention.Duration != 30*24*time.Hour {
t.Errorf("got db %q idle %v retention %v", c.DB, c.LeaseIdle.Duration, c.Retention.Duration)
}
}
func TestDurationAcceptsDays(t *testing.T) {
var d config.Duration
for _, tc := range []struct {
in string
want time.Duration
}{
{"1d", 24 * time.Hour}, {"7d", 7 * 24 * time.Hour}, {"90m", 90 * time.Minute}, {"2h30m", 150 * time.Minute},
} {
if err := d.UnmarshalText([]byte(tc.in)); err != nil || d.Duration != tc.want {
t.Errorf("UnmarshalText(%q) = %v %v, want %v", tc.in, d.Duration, err, tc.want)
}
}
for _, bad := range []string{"1.5d", "d", "3 days", "1d2h"} {
if err := d.UnmarshalText([]byte(bad)); err == nil {
t.Errorf("UnmarshalText(%q) must fail", bad)
}
}
}
func TestV1Validation(t *testing.T) {
for _, tc := range []struct{ name, text, field string }{
{"empty db", "db = \"\"\n" + v1Base, "db"},
{"lease_idle too short", "lease_idle = \"10s\"\n" + v1Base, "lease_idle"},
{"retention too short", "retention = \"12h\"\n" + v1Base, "retention"},
} {
_, err := config.Parse(strings.NewReader(tc.text))
e, ok := config.IsError(err)
if !ok || e.Field != tc.field {
t.Errorf("%s: %v, want *Error on %s", tc.name, err, tc.field)
}
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
listen = "127.0.0.1:7777"
lease_idle = "30m"
bogus_key = 1
[hosts.alpha]
base_url = "http://alpha.example:11434"