capture: answer the stop before summarising, and always leave a note

capture_stop held the IPC request open for the whole map reduce, up to
twenty minutes. A voice turn that says "хватит" waited for forty model
calls before Maven said anything. Stop now returns the transcript and the
summary runs on a goroutine in the daemon's WaitGroup, on the daemon
context so a client that hung up does not cancel the only readable record
of the meeting.

With no summary and save_transcript false, writeNotes wrote nothing at
all: an hour of meeting left a blob that prunes in seven days and no
trace in the note store. The transcript is written instead when the
summary is missing. That flag decides whether the verbatim record is kept
in addition to a summary, not whether the meeting is remembered.

The wire carries the session token now, and the contract comments say
what the code does: the summary is usually absent from the stop
response, and re running a stored blob is a manual job because no method
takes a blob id. The save_transcript comment says the cost is recall
corpus rather than disk.

Found in review of #73.
This commit is contained in:
kami
2026-08-01 14:36:17 +04:00
parent 77888c1a9c
commit 2ca5ffa4f9
7 changed files with 235 additions and 46 deletions
+9 -4
View File
@@ -27,8 +27,8 @@ import (
"github.com/kami/maven/internal/morning"
"github.com/kami/maven/internal/netscan"
"github.com/kami/maven/internal/smarthome"
"github.com/kami/maven/internal/vision"
"github.com/kami/maven/internal/update"
"github.com/kami/maven/internal/vision"
"github.com/robfig/cron/v3"
)
@@ -787,9 +787,14 @@ type CaptureConfig struct {
MaxChunks int `json:"max_chunks,omitempty"`
// SaveTranscript — write the full transcript as a note alongside the
// summary. Default false: a verbatim record of what other people said in a
// room is a heavier thing to keep than a four-line summary, so it takes a
// deliberate yes. The audio blob is pruned by media.retention either way.
// summary. Default false, and the cost is not disk: a note is embedded and
// becomes recall corpus, so every later question can surface verbatim words
// other people said in a room. That is the reason it takes a deliberate yes.
// The audio blob is pruned by media.retention either way; the notes are not.
//
// A meeting with no summary writes its transcript regardless. The choice
// here is transcript IN ADDITION to a summary, not whether the meeting is
// remembered at all.
SaveTranscript bool `json:"save_transcript,omitempty"`
}
+8 -8
View File
@@ -228,14 +228,14 @@ func TestSpeakerBlockParsesFromJSON(t *testing.T) {
// both fail at startup now.
func TestSensesBlocksAreValidatedAtStartup(t *testing.T) {
bad := map[string]string{
"media with no dir": `{"media":{"retention":"48h"}}`,
"negative budget": `{"media":{"dir":"/srv/media","max_total_bytes":-1}}`,
"blob over the budget": `{"media":{"dir":"/srv/media","max_bytes":100,"max_total_bytes":10}}`,
"vision with no media dir": `{"vision":{"enabled":true,"endpoint":"http://127.0.0.1:8081"}}`,
"vision endpoint typo": `{"media":{"dir":"/srv/media"},"vision":{"enabled":true,"endpoint":"127.0.0.1:8081"}}`,
"vision on the wan": `{"media":{"dir":"/srv/media"},"vision":{"enabled":true,"endpoint":"http://8.8.8.8:8081"}}`,
"vision, empty endpoint": `{"media":{"dir":"/srv/media"},"vision":{"enabled":true}}`,
"capture with no store": `{"capture":{"enabled":true}}`,
"media with no dir": `{"media":{"retention":"48h"}}`,
"negative budget": `{"media":{"dir":"/srv/media","max_total_bytes":-1}}`,
"blob over the budget": `{"media":{"dir":"/srv/media","max_bytes":100,"max_total_bytes":10}}`,
"vision with no media dir": `{"vision":{"enabled":true,"endpoint":"http://127.0.0.1:8081"}}`,
"vision endpoint typo": `{"media":{"dir":"/srv/media"},"vision":{"enabled":true,"endpoint":"127.0.0.1:8081"}}`,
"vision on the wan": `{"media":{"dir":"/srv/media"},"vision":{"enabled":true,"endpoint":"http://8.8.8.8:8081"}}`,
"vision, empty endpoint": `{"media":{"dir":"/srv/media"},"vision":{"enabled":true}}`,
"capture with no store": `{"capture":{"enabled":true}}`,
}
for name, body := range bad {
t.Run(name, func(t *testing.T) {