Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 9 additions & 12 deletions .squawk.toml
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
# squawk — Postgres migration-safety linter for go/internal/store/migrations/.
# Gate config for tools/sql-migration-gate/moon.yml.
#
# WHY THESE EXCLUSIONS: the sole migration (0001_init.sql) is a single collapsed
# schema-bootstrap that runs ONCE against an empty, pre-live database inside a
# transaction (see the migration runner + advisory lock). squawk's default rule
# set is calibrated for INCREMENTAL migrations against a large LIVE production
# table, where a full-table rewrite or a non-CONCURRENT index build takes a
# blocking lock that stalls live traffic. On an empty pre-live DB that hazard
# does not exist, so the rules below are accepted here. They stay OFF only for
# this bootstrap posture — a future incremental migration against live data
# would want them back (revisit this list when the first post-live migration
# lands). The gate stays GREEN on the current file and RED on genuinely unsafe
# NEW DDL (e.g. adding a NOT NULL column with no default), which these
# exclusions do not silence.
# WHY THESE EXCLUSIONS: every migration so far (0001_init.sql, then the
# append-only files after it) creates tables that are empty when it runs, inside
# one transaction (see the migration runner + advisory lock). squawk's default
# rule set is calibrated for changes to a large LIVE table, where a full-table
# rewrite or a non-CONCURRENT index build takes a blocking lock that stalls
# traffic. On a new empty table that hazard does not exist, so the rules below
# are accepted. Revisit this list when a migration first alters or indexes a
# populated table. The gate stays RED on genuinely unsafe NEW DDL (e.g. adding
# a NOT NULL column with no default), which these exclusions do not silence.

pg_version = "16.0"

Expand Down
6 changes: 6 additions & 0 deletions docs/self-host.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@ With `--database-external` the stack only connects to the `--database` DSN you
name; it never starts, stops, or owns that instance's lifecycle. The flag is the
opt-out switch and `--database` (or `$COMPASS_DATABASE_DSN`) carries the DSN.

The server keeps each raw token-usage event (one row per upstream model call)
for 90 days by default, and deletes older events once a day. Set the window with
`--usage-event-retention` (or `$COMPASS_USAGE_EVENT_RETENTION`) as a Go duration
such as `720h`; `0` keeps every event. The hourly and daily usage totals built
from those events are always kept.

## Secrets

Compass keeps its secret *values* in your configured `secretspec` provider, not
Expand Down
30 changes: 30 additions & 0 deletions go/cmd/compass-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"time"

"github.com/RigelBuild/compass/go/internal/otel"
"github.com/RigelBuild/compass/go/internal/usage"
"github.com/RigelBuild/compass/go/server"
)

Expand Down Expand Up @@ -220,6 +221,12 @@ func buildServeConfig(args []string) (server.ServeConfig, bool, error) {
return server.ServeConfig{}, false, err
}

usageRetention, err := resolveUsageEventRetention(
firstNonEmpty(*f.usageRetention, os.Getenv("COMPASS_USAGE_EVENT_RETENTION")))
if err != nil {
return server.ServeConfig{}, false, err
}

return server.ServeConfig{
SocketPath: socketPath,
Version: version,
Expand All @@ -239,6 +246,7 @@ func buildServeConfig(args []string) (server.ServeConfig, bool, error) {
// read one source, so no --otel-endpoint flag. Empty = tracing off.
OtelEndpoint: os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT"),
TranscriptSafetyValveCapBytes: positiveCap(*f.transcriptSafetyValveCapBytes, os.Getenv("COMPASS_TRANSCRIPT_SAFETY_VALVE_CAP_BYTES")),
UsageEventRetention: usageRetention,
}, false, nil
}

Expand Down Expand Up @@ -281,6 +289,7 @@ type serveFlags struct {
adminHandle *string
corsAllowedOrigin *string
publicURL *string
usageRetention *string
}

// registerServeFlags declares the core compass-server flags on the given FlagSet
Expand Down Expand Up @@ -350,6 +359,11 @@ func registerServeFlags(fs *flag.FlagSet) serveFlags {
"Linear webhooks must set it."),
transcriptSafetyValveCapBytes: fs.Int("transcript-safety-valve-cap-bytes", 0,
"Hot-tail safety-valve cap in bytes. Defaults to $COMPASS_TRANSCRIPT_SAFETY_VALVE_CAP_BYTES."),
usageRetention: fs.String("usage-event-retention", "",
"How long raw token-usage events are kept before the daily prune "+
"deletes them, as a Go duration (e.g. 720h). The hourly and daily "+
"usage totals are kept. Falls back to $COMPASS_USAGE_EVENT_RETENTION, "+
"then 2160h (90 days). 0 disables the prune."),
}
}

Expand Down Expand Up @@ -392,6 +406,22 @@ func resolveNetworkDoor(listen, tlsCert, tlsKey string) (string, *server.TLSConf
}
}

// resolveUsageEventRetention parses the retention window (flag, then env). Empty
// keeps the default; 0 is kept, because it is the operator's opt-out.
func resolveUsageEventRetention(v string) (time.Duration, error) {
if v == "" {
return usage.DefaultRetention, nil
}
d, err := time.ParseDuration(v)
if err != nil {
return 0, fmt.Errorf("invalid --usage-event-retention %q: %w", v, err)
}
if d < 0 {
return 0, fmt.Errorf("invalid --usage-event-retention %q: it must not be negative; 0 disables the prune", v)
}
return d, nil
}

// forgeFlags holds the RIG-1810/RIG-2883 forge CLI flag pointers, registered as
// a group so run() stays short (they mirror the S3 flag set's precedence).
type forgeFlags struct {
Expand Down
43 changes: 43 additions & 0 deletions go/cmd/compass-server/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"flag"
"strings"
"testing"
"time"

"github.com/RigelBuild/compass/go/server"
)
Expand Down Expand Up @@ -411,3 +412,45 @@ func TestBuildServeConfigBadFlagIsUsageError(t *testing.T) {
t.Fatalf("a bad flag must not read as ErrHelp (that is a clean help exit): %v", err)
}
}

// TestBuildServeConfigUsageEventRetention pins the retention window's
// flag-then-env precedence, its 90-day default, 0 as the opt-out, and bad input.
func TestBuildServeConfigUsageEventRetention(t *testing.T) {
for _, tc := range []struct {
name, flag, env string
want time.Duration
wantErr bool
}{
{name: "unset_keeps_90_days", want: 90 * 24 * time.Hour},
{name: "flag_sets_the_window", flag: "720h", want: 720 * time.Hour},
{name: "env_is_the_fallback", env: "48h", want: 48 * time.Hour},
{name: "flag_beats_env", flag: "24h", env: "48h", want: 24 * time.Hour},
{name: "flag_0_disables_the_sweep", flag: "0", env: "48h", want: 0},
{name: "env_0_disables_the_sweep", env: "0", want: 0},
{name: "day_unit_is_rejected", flag: "90d", wantErr: true},
{name: "bad_env_is_rejected", env: "soon", wantErr: true},
{name: "negative_is_rejected", flag: "-24h", wantErr: true},
} {
t.Run(tc.name, func(t *testing.T) {
t.Setenv("COMPASS_USAGE_EVENT_RETENTION", tc.env)
t.Setenv("COMPASS_NATS_URL", "nats://127.0.0.1:4222") // required since the event fabric landed
args := []string{"--database", "postgres://x/db", "--socket", "/tmp/x.sock"}
if tc.flag != "" {
args = append(args, "--usage-event-retention", tc.flag)
}
cfg, _, err := buildServeConfig(args)
if tc.wantErr {
if err == nil || !strings.Contains(err.Error(), "--usage-event-retention") {
t.Fatalf("buildServeConfig = %v, want an error naming --usage-event-retention", err)
}
return
}
if err != nil {
t.Fatalf("buildServeConfig = %v, want nil", err)
}
if cfg.UsageEventRetention != tc.want {
t.Errorf("UsageEventRetention = %v, want %v", cfg.UsageEventRetention, tc.want)
}
})
}
}
63 changes: 63 additions & 0 deletions go/internal/store/db/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions go/internal/store/db/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions go/internal/store/db/tenant.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading