the store keeps a routing trace for fourteen days (V-629)
Migration #23 adds routing_traces, and internal/store/routingtraces.go writes, lists and prunes it. Nothing calls it yet; the daemon side is the next commit. The utterance is stored in clear. A 384-dimension vector of a short sentence is substantially recoverable, so storing vectors instead would be a privacy claim we cannot support. Retention is 14 days, enforced on write, and an age rather than a row count so a busy Tuesday cannot push last Friday out. Store.Wipe already deletes it with everything else, so explicit deletion needs no new surface. A correction is not covered by that bound. When the owner corrects a turn the pair is promoted out into a seed-shaped row and kept, because a label is not a transcript. What stays here is the transcript, and the transcript expires. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117tgnmbgZpHVV3XSNw8Qua
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// routingTraceRetention is how long a raw trace lives (owner's call,
|
||||
// 06-08-2026). A trace is read within a day or two of the turn that produced it,
|
||||
// or never, so two weeks is diagnosis with room for a weekend. It is deliberately
|
||||
// an age and not a row count: the useful question is "what did she do this week",
|
||||
// and a busy Tuesday must not push last Friday out.
|
||||
//
|
||||
// A correction is not covered by this bound. The moment the owner corrects a
|
||||
// turn, the pair is promoted out of the trace into a seed-shaped row and kept
|
||||
// indefinitely, because a label is not a transcript. Keeping the transcript that
|
||||
// carried it would defeat the point of the bound.
|
||||
const routingTraceRetention = 14 * 24 * time.Hour
|
||||
|
||||
// RoutingTrace is one turn's arbitration, persisted. It is internal/decision's
|
||||
// Record plus the four things the ring never had to carry: which reach the
|
||||
// utterance arrived on, whether stage 0 answered before the classifier was
|
||||
// consulted, which encoder body was live, and what the turn actually did.
|
||||
type RoutingTrace struct {
|
||||
ID int64 `json:"id"`
|
||||
Ts time.Time `json:"ts"`
|
||||
Utterance string `json:"utterance"`
|
||||
Source string `json:"source"`
|
||||
Winner string `json:"winner"`
|
||||
Intent string `json:"intent"`
|
||||
// ClaimedBeforeHead — stage 0 or a pre-route resolver answered, so the turn
|
||||
// teaches nothing about the classifier. It is a large share of real traffic,
|
||||
// and counting those turns as training signal would fit the head to the
|
||||
// grammars rather than to him.
|
||||
ClaimedBeforeHead bool `json:"claimed_before_head"`
|
||||
// EncoderID names the encoder body that was live. A fitted distance means
|
||||
// nothing under another body, and V-546 trains a copy of the weights.
|
||||
EncoderID string `json:"encoder_id"`
|
||||
// Outcome is what happened, not what was routed: a route that reached a gap
|
||||
// and a route that ran are different turns.
|
||||
Outcome string `json:"outcome"`
|
||||
// Correction is the owner's label, empty until he gives one (V-630).
|
||||
Correction string `json:"correction"`
|
||||
// Claims is internal/decision's per-claimant detail, stored as JSON because
|
||||
// nothing queries inside it: it is read whole, beside the turn it explains.
|
||||
Claims json.RawMessage `json:"claims"`
|
||||
}
|
||||
|
||||
// WriteRoutingTrace appends one turn and drops the ones past retention.
|
||||
func (s *Store) WriteRoutingTrace(ctx context.Context, tr RoutingTrace) (int64, error) {
|
||||
claims := "[]"
|
||||
if len(tr.Claims) > 0 {
|
||||
claims = string(tr.Claims)
|
||||
}
|
||||
res, err := s.db.ExecContext(ctx, `
|
||||
INSERT INTO routing_traces
|
||||
(ts, utterance, source, winner, intent, claimed_before_head, encoder_id, outcome, correction, claims)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?)`,
|
||||
tr.Ts.UnixMilli(), tr.Utterance, tr.Source, tr.Winner, tr.Intent,
|
||||
tr.ClaimedBeforeHead, tr.EncoderID, tr.Outcome, tr.Correction, claims)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("write routing trace: %w", err)
|
||||
}
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("last insert id: %w", err)
|
||||
}
|
||||
// Prune rarely. Turns arrive at human rate, so the bound is a ceiling and
|
||||
// paying for a delete on every one of them buys nothing. 64 turns is hours.
|
||||
if id%64 == 0 {
|
||||
if err := s.PruneRoutingTraces(ctx, tr.Ts.Add(-routingTraceRetention)); err != nil {
|
||||
return id, err
|
||||
}
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// PruneRoutingTraces deletes every trace older than before. A corrected turn is
|
||||
// deleted with the rest: the label was promoted out when the owner wrote it, so
|
||||
// what is left here is the transcript, and the transcript is what expires.
|
||||
func (s *Store) PruneRoutingTraces(ctx context.Context, before time.Time) error {
|
||||
if _, err := s.db.ExecContext(ctx,
|
||||
`DELETE FROM routing_traces WHERE ts < ?`, before.UnixMilli()); err != nil {
|
||||
return fmt.Errorf("prune routing traces: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RecentRoutingTraces returns the newest n turns, newest first.
|
||||
func (s *Store) RecentRoutingTraces(ctx context.Context, n int) ([]RoutingTrace, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT id, ts, utterance, source, winner, intent, claimed_before_head,
|
||||
encoder_id, outcome, correction, claims
|
||||
FROM routing_traces
|
||||
ORDER BY id DESC
|
||||
LIMIT ?`, n)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("recent routing traces: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []RoutingTrace
|
||||
for rows.Next() {
|
||||
var tr RoutingTrace
|
||||
var tsMilli int64
|
||||
var claims string
|
||||
if err := rows.Scan(&tr.ID, &tsMilli, &tr.Utterance, &tr.Source, &tr.Winner,
|
||||
&tr.Intent, &tr.ClaimedBeforeHead, &tr.EncoderID, &tr.Outcome,
|
||||
&tr.Correction, &claims); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tr.Ts = time.UnixMilli(tsMilli).UTC()
|
||||
tr.Claims = json.RawMessage(claims)
|
||||
out = append(out, tr)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
Reference in New Issue
Block a user