From 901354002e8833e959570804bf913abaf918f06b Mon Sep 17 00:00:00 2001 From: claude Date: Thu, 6 Aug 2026 03:12:18 +0400 Subject: [PATCH] capture and dialogue: name two TTLs, fix a drifted comment (V-581) Sweep of internal/capture and internal/dialogue. Both packages were already in good shape, so this is four small corrections rather than a rework. windowBytes truncated the STT window to whole seconds. A sub-second window therefore came out as zero bytes, which transcribeFile reads as "no window" and answers by handing the transcriber the whole meeting in one call. The multiplication is now done in float, so a fractional window is a real window. Peek's comment claimed expired entries below the top are left alone. The code deletes the whole stack, which is what Pop and TakeExpired both document and what the clock argues for. The comment now says so, and it names the ordering the silent drop depends on: TakeExpired has to run before Peek on a turn or the expiry notice becomes unreachable. The two store default TTLs were unnamed literals. They are DefaultClarifyTTL and DefaultSessionTTL now, next to DefaultMaxAttempts, and the comment on each says why the clarify one is the shorter of the two. PendingQuestion was not gofmt clean and ChunkText copied a slice one element at a time. Full suite passes with -race. Co-Authored-By: Claude Opus 5 --- internal/capture/capture.go | 5 ++++- internal/capture/summarize.go | 4 +--- internal/dialogue/clarify.go | 25 +++++++++++++++++-------- internal/dialogue/session.go | 7 ++++++- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/internal/capture/capture.go b/internal/capture/capture.go index 4607d67..3d1bfb1 100644 --- a/internal/capture/capture.go +++ b/internal/capture/capture.go @@ -626,6 +626,9 @@ func windowBytes(f audio.Format, window time.Duration) int64 { if bps <= 0 || f.SampleRate <= 0 || window <= 0 { return 0 } - per := int64(window.Seconds()) * bytesPerSecond(f) + // Fractional seconds count. Truncating the window to whole seconds turned + // any sub-second window into zero bytes, which the caller reads as "no + // window" and answers by handing the transcriber the entire meeting at once. + per := int64(window.Seconds() * float64(bytesPerSecond(f))) return per - per%bps } diff --git a/internal/capture/summarize.go b/internal/capture/summarize.go index 9dfa399..388a2cd 100644 --- a/internal/capture/summarize.go +++ b/internal/capture/summarize.go @@ -193,9 +193,7 @@ func ChunkText(text string, maxRunes int) []string { // Oversized sentence: emit what is buffered, then cut this one on // word boundaries. flush() - for _, piece := range splitWords(sr, maxRunes) { - out = append(out, piece) - } + out = append(out, splitWords(sr, maxRunes)...) continue } if len(cur)+len(sr) > maxRunes { diff --git a/internal/dialogue/clarify.go b/internal/dialogue/clarify.go index 474cd67..9337095 100644 --- a/internal/dialogue/clarify.go +++ b/internal/dialogue/clarify.go @@ -37,8 +37,8 @@ type PendingQuestion struct { // request rather than alone: "завтра" names a day for an hour said earlier. WhenText string Asked time.Time - TTL time.Duration - Attempts int // questions already asked + TTL time.Duration + Attempts int // questions already asked // MaxAttempts caps Attempts. 0 ⇒ DefaultMaxAttempts. MaxAttempts int } @@ -104,11 +104,14 @@ type ClarifyStore struct { // reply can carry. const MaxStackDepth = 2 +// DefaultClarifyTTL — how long a parked question stays his answer to give. +// Short, like confirmTTL in voice.go: a clarifying question is a same-breath +// gesture, and a stale one should not eat a later utterance. +const DefaultClarifyTTL = 90 * time.Second + func NewClarifyStore(defaultTTL time.Duration) *ClarifyStore { if defaultTTL <= 0 { - // Short, like confirmTTL in voice.go: a clarifying question is a - // same-breath gesture, a stale one should not eat a later utterance. - defaultTTL = 90 * time.Second + defaultTTL = DefaultClarifyTTL } return &ClarifyStore{ stacks: make(map[string][]*PendingQuestion), @@ -151,9 +154,15 @@ func (s *ClarifyStore) Push(id string, q *PendingQuestion) *PendingQuestion { return dropped } -// Peek returns the live question on top, or nil when there is none. Expired -// entries below it are left alone: TakeExpired is what reports those, and -// dropping one here would be the silent death this store is careful about. +// Peek returns the live question on top, or nil when there is none. An expired +// top takes the whole stack with it, exactly as Pop does: the clock that killed +// it has been running for everything underneath too. +// +// That drop is silent, which is the death this store is otherwise careful +// about, so TakeExpired has to run BEFORE Peek on a turn — it is what counts the +// dropped questions and tells him they are gone. cmd/mavend/voice.go calls +// clarifyExpiredNotice first for that reason, and reordering the two makes the +// notice unreachable rather than wrong. func (s *ClarifyStore) Peek(id string, now time.Time) *PendingQuestion { s.mu.RLock() stack := s.stacks[id] diff --git a/internal/dialogue/session.go b/internal/dialogue/session.go index d71cbf1..32c9051 100644 --- a/internal/dialogue/session.go +++ b/internal/dialogue/session.go @@ -87,9 +87,14 @@ type SessionStore struct { persist SessionPersister // may be nil: memory only (tests, no-store paths) } +// DefaultSessionTTL — how long a turn stays available to inherit from. Longer +// than DefaultClarifyTTL because this is not a question waiting on an answer: +// it is the last thing said, and a follow-up may land after a real pause. +const DefaultSessionTTL = 2 * time.Minute + func NewSessionStore(defaultTTL time.Duration) *SessionStore { if defaultTTL <= 0 { - defaultTTL = 2 * time.Minute + defaultTTL = DefaultSessionTTL } return &SessionStore{ sessions: make(map[string]*Session),