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:
2026-08-06 19:20:24 +04:00
parent 7852aad60f
commit e1f84a3474
6 changed files with 39 additions and 11 deletions
+1 -1
View File
@@ -311,7 +311,7 @@ ALTER TABLE reminders ADD COLUMN next_fire_ts INTEGER;`, // #2
// sentence is substantially recoverable, so storing vectors instead would be a
// privacy claim we cannot support. What makes it safe is the same thing that
// makes the fact store safe: it never leaves the box, retention is bounded at
// routingTraceRetention, and Wipe drops it with everything else.
// store.RoutingTraceRetention, and Wipe drops it with everything else.
//
// correction is empty until the owner corrects a turn on /chat (V-630). A
// corrected pair is promoted out of here into a seed-shaped row and kept, so
+3 -3
View File
@@ -7,7 +7,7 @@ import (
"time"
)
// routingTraceRetention is how long a raw trace lives (owner's call,
// 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",
@@ -17,7 +17,7 @@ import (
// 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
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
@@ -70,7 +70,7 @@ func (s *Store) WriteRoutingTrace(ctx context.Context, tr RoutingTrace) (int64,
// 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 {
if err := s.PruneRoutingTraces(ctx, tr.Ts.Add(-RoutingTraceRetention)); err != nil {
return id, err
}
}
+1 -1
View File
@@ -62,7 +62,7 @@ func TestPruneRoutingTracesByAge(t *testing.T) {
t.Fatal(err)
}
}
if err := s.PruneRoutingTraces(ctx, now.Add(-routingTraceRetention)); err != nil {
if err := s.PruneRoutingTraces(ctx, now.Add(-RoutingTraceRetention)); err != nil {
t.Fatal(err)
}
got, err := s.RecentRoutingTraces(ctx, 10)