review: a cancelled turn keeps its trace, and a quiet box still expires (V-629)
Two defects found reviewing the PR. The insert ran on the turn's own context, so a caller that hung up or timed out cancelled it. That is exactly the turn worth having. It now runs detached, with a one-second bound of its own, because a write must not hold the reply. Retention was enforced on write alone, so a box that goes quiet for a month kept every row until the next sixty-fourth turn. pruneTracesOnStart closes that, and RoutingTraceRetention is exported so the daemon reads the same number the store enforces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0117tgnmbgZpHVV3XSNw8Qua
This commit is contained in:
@@ -16,6 +16,7 @@ import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kami/maven/internal/decision"
|
||||
"github.com/kami/maven/internal/store"
|
||||
@@ -37,6 +38,21 @@ func traceSink(s *store.Store) traceWriter {
|
||||
return s
|
||||
}
|
||||
|
||||
// pruneTracesOnStart enforces retention once at wiring time. Pruning on write
|
||||
// alone is not enough: a box that goes quiet for a month keeps every row until
|
||||
// the next sixty-fourth turn, and "kept for fourteen days" would then be true
|
||||
// only of a box in daily use. Called for its effect and never blocks a start.
|
||||
func pruneTracesOnStart(s *store.Store, now time.Time) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := s.PruneRoutingTraces(ctx, now.Add(-store.RoutingTraceRetention)); err != nil {
|
||||
log.Printf("routing trace: prune on start: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// persistDecision writes one finished record. It takes the same *decision.Record
|
||||
// the ring takes, so the two sinks cannot disagree about what the turn did.
|
||||
//
|
||||
@@ -46,6 +62,13 @@ func (h *reactiveHandler) persistDecision(ctx context.Context, rec *decision.Rec
|
||||
if h.traces == nil || rec == nil || strings.TrimSpace(rec.Utterance) == "" {
|
||||
return
|
||||
}
|
||||
// Detached from the turn's context, and bounded on its own. Two reasons, and
|
||||
// the first is the one that matters: the turn is over by the time this runs,
|
||||
// so a caller that hung up or timed out would cancel the insert, and the turn
|
||||
// he abandoned halfway is exactly the one worth having. The second is that a
|
||||
// write must not hold the reply, so it gets a second and no more.
|
||||
ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), time.Second)
|
||||
defer cancel()
|
||||
claims, err := json.Marshal(rec.Claims)
|
||||
if err != nil {
|
||||
log.Printf("routing trace: marshal claims: %v", err)
|
||||
|
||||
@@ -149,6 +149,9 @@ func wireVoice(cfg *config.Config, coreAPI ipc.CoreAPI, phr phraser.Phraser, mem
|
||||
w.embedder = emb
|
||||
repairFactVectors(dataStore, emb)
|
||||
checkStoredEmbedder(dataStore, emb)
|
||||
// Retention is enforced on write, which is not enough on its own: a box that
|
||||
// goes quiet keeps every trace until the next sixty-fourth turn (V-629).
|
||||
pruneTracesOnStart(dataStore, time.Now())
|
||||
|
||||
// ----- tool executor (the enabled act allowlist, store-backed) -----
|
||||
// Config tools are the declarative bootstrap: seed them into the store as
|
||||
|
||||
Reference in New Issue
Block a user