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 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 03:12:18 +04:00
parent 316fb197a8
commit 901354002e
4 changed files with 28 additions and 13 deletions
+4 -1
View File
@@ -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
}
+1 -3
View File
@@ -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 {
+17 -8
View File
@@ -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]
+6 -1
View File
@@ -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),