package main import ( "context" "sync" ) // The query source that claimed a turn was visible in the daemon log and // nowhere else (V-539). A QA step reading /chat could see a wrong answer but // not tell a wrong answer from a wrongly ordered chain: "почему небо голубое" // answered badly reads the same whether search claimed it, the ZIM did, or the // resident model answered from memory. // // It rides the context rather than a return value because handleText answers // every reach through one string, and threading a second value through the // whole action dispatch would change a signature the mic, telegram and the web // all share. The sink is per turn, created by the caller that wants to read it; // a turn with no sink notes nothing, which is what the mic path does. type querySourceKey struct{} // querySourceSink holds the name of the source that claimed one turn. The mutex // is there because a query source may fan out to goroutines of its own, not // because two turns share a sink. type querySourceSink struct { mu sync.Mutex name string } func (s *querySourceSink) note(name string) { s.mu.Lock() defer s.mu.Unlock() s.name = name } // Name is the source that claimed, or empty when nothing did or the turn was // not a query at all. func (s *querySourceSink) Name() string { s.mu.Lock() defer s.mu.Unlock() return s.name } // withQuerySourceSink returns a context that collects the claiming source, and // the sink to read after the turn has answered. func withQuerySourceSink(ctx context.Context) (context.Context, *querySourceSink) { sink := &querySourceSink{} return context.WithValue(ctx, querySourceKey{}, sink), sink } // noteQuerySource records which source claimed the turn. It is a no-op when the // caller did not ask for one. func noteQuerySource(ctx context.Context, name string) { if sink, ok := ctx.Value(querySourceKey{}).(*querySourceSink); ok { sink.note(name) } }