package main import ( "context" "log" "strconv" "github.com/kami/maven/internal/phraser" "github.com/kami/maven/internal/router" ) // nothingToCorrectReply — what she says to a correction that points at // nothing. Filing it would put a sentence in his memory that reads as a fact. const nothingToCorrectReply = "не поняла, что поправить. скажи целиком, и я запишу." // actionNote handles router.IntentNote: embed the note, persist it, and // index it for recall. // // The stored body comes only from dec.Utterance (V-576/V-721). An explicit // leading capture frame is structurally removed; an unmarked note is otherwise // byte-for-byte his utterance. It is never Slots.Text, phraser output or any // other model string: a note is durable, the embedder indexes it, and it comes // back later as recall in his own words. Phrasing belongs in the confirmation. func (h *reactiveHandler) actionNote(ctx context.Context, dec router.Decision) string { // A correction with no referent. Everything that could own one has already // run by here: clarify, confirm and repair are all resolved before routing, // so a fragment reaching the note path has nothing behind it (V-576). if correctionFragment(dec.Utterance) { return nothingToCorrectReply } // An utterance that explicitly files a task is work, not recall, and // belongs in the task store (Vikunja #130). Checked before the embedding // is paid for. Everything else is a note, exactly as before. if reply, ok := h.captureTaskFromNote(ctx, dec); ok { return reply } // A standing list is neither work nor recall (Vikunja #453). Checked here // for the same reason and at the same cost: before the embedding is paid // for, and it passes the turn straight back when no marker matches. if reply, ok := h.captureListFromNote(ctx, dec); ok { return reply } noteText := dec.Utterance if body, explicit := router.ParseNoteCapture(dec.Utterance); explicit { noteText = body } // embed the note text with the same model the classifier uses, persist // via CoreAPI (source=tap:voice). Semantic recall lives in `notes`, not // facts — no predicate reads it (spec's two-memory split). vec, err := router.EmbedPassage(ctx, h.recall.embedder, noteText) if err != nil { log.Printf("voice: embed note: %v", err) return phraser.Ack(phraser.FailNote, nil) } noteTs := h.now() noteID, err := h.api.WriteNote(ctx, noteTs, noteText, vec, "tap:voice") if err != nil { log.Printf("voice: write note: %v", err) return phraser.Ack(phraser.FailNote, nil) } // Insert into long-term memory (best-effort, must not fail the note write). // text/ts in the meta make a Search hit self-describing (see bestRecall). if h.recall.memStore != nil { if err := h.recall.memStore.Insert(ctx, "note:"+strconv.FormatInt(noteID, 10), vec, map[string]string{ "source": "voice", "type": "note", "text": noteText, "ts": strconv.FormatInt(noteTs.Unix(), 10), }); err != nil { log.Printf("voice: memory insert: %v", err) } } return "" // replier phrases the "saved" reply }