mavend: give the voice handler a CoreAPI that can serve the day plan
wireVoice runs before the tick loop exists, so it could only be handed the bare store adapter — and that adapter answers DayPlan with "not available via direct store API", because a day plan is assembled by the tick loop and is not a table to read. So queryDayPlan, which the query chain reaches for "какие у меня планы на сегодня", failed for every caller on the deployed daemon. main already back-patches the other direction (daemonAPI.chatFn = handler.handleText). This is the same seam in reverse, at both wiring sites. No recursion risk: nothing in the voice path calls api.Chat. With the plan reachable, it recited its reminders as literal JSON. The payload unwrapper existed but was private to the phraser, so the day plan had its own non-unwrapping copy. One owner now, store.ReminderText, with the phraser delegating to it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrVSBKe3RFDF4fGYKWYQnX
This commit is contained in:
@@ -24,7 +24,6 @@ package phraser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -32,6 +31,7 @@ import (
|
||||
"github.com/kami/maven/internal/delivery"
|
||||
"github.com/kami/maven/internal/dialogue"
|
||||
"github.com/kami/maven/internal/loop"
|
||||
"github.com/kami/maven/internal/store"
|
||||
)
|
||||
|
||||
// Phraser — the seam the daemon wires. one method per delivery path (nudge
|
||||
@@ -161,22 +161,11 @@ func phraseNudge(c loop.Candidate) (body, summary string) {
|
||||
}
|
||||
}
|
||||
|
||||
// extractReminderText — the reminder payload is raw JSON; the router's
|
||||
// reminder slot extraction owns the shape. the conventional field is "text".
|
||||
// fall back to the raw payload if it isn't JSON or lacks the field — the user
|
||||
// said it, it's the user's words.
|
||||
func extractReminderText(payload string) string {
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal([]byte(payload), &m); err == nil {
|
||||
if t, ok := m["text"].(string); ok && t != "" {
|
||||
return t
|
||||
}
|
||||
if t, ok := m["text"]; ok {
|
||||
return fmt.Sprintf("%v", t)
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(payload)
|
||||
}
|
||||
// extractReminderText — the reminder payload is raw JSON and store.ReminderText
|
||||
// owns the unwrapping. It used to be a second copy of that logic here, which is
|
||||
// how the day plan came to recite a reminder as its literal JSON: the copies
|
||||
// were never going to be kept in step.
|
||||
func extractReminderText(payload string) string { return store.ReminderText(payload) }
|
||||
|
||||
// humanDur — round a duration to the coarsest sensible unit for speech.
|
||||
// "4h12m" → "4 hours"; "92m" → "1h32m" → "an hour and a half". keep it simple:
|
||||
|
||||
@@ -3,8 +3,10 @@ package store
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
@@ -26,6 +28,33 @@ type Reminder struct {
|
||||
Collapsed []Reminder
|
||||
}
|
||||
|
||||
// Text — what the user actually asked for, out of the raw-JSON payload.
|
||||
//
|
||||
// The router's reminder slot extraction owns the payload shape and the
|
||||
// conventional field is "text". A payload that is not JSON, or that lacks the
|
||||
// field, is returned as-is: he said it, so they are his words, and showing
|
||||
// them beats showing nothing.
|
||||
//
|
||||
// Here rather than in a caller because there is more than one caller and they
|
||||
// disagreed. The phraser unwrapped the payload; the day plan did not, so
|
||||
// "какие у меня планы на сегодня" recited a reminder as the literal string
|
||||
// {"text":"..."} on the deployed daemon, 01-08-2026.
|
||||
func (r Reminder) Text() string { return ReminderText(r.Payload) }
|
||||
|
||||
// ReminderText — Reminder.Text for callers holding a bare payload string.
|
||||
func ReminderText(payload string) string {
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal([]byte(payload), &m); err == nil {
|
||||
if t, ok := m["text"].(string); ok && t != "" {
|
||||
return t
|
||||
}
|
||||
if t, ok := m["text"]; ok {
|
||||
return fmt.Sprintf("%v", t)
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(payload)
|
||||
}
|
||||
|
||||
// Reminder lifecycle states. Named for the same reason DigestStatus is: a
|
||||
// caller filtering on the string literal "pending" is one typo away from a
|
||||
// filter that silently matches nothing.
|
||||
|
||||
Reference in New Issue
Block a user