Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b622389c5 |
@@ -530,3 +530,23 @@ func TestClarifyIsPerConversation(t *testing.T) {
|
|||||||
func voiceCtx() context.Context {
|
func voiceCtx() context.Context {
|
||||||
return withDialogueID(context.Background(), dialogueIDFor(sourceVoice, ""))
|
return withDialogueID(context.Background(), dialogueIDFor(sourceVoice, ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestARestartExpiresTheParkedQuestion pins the Vikunja #385 decision: the
|
||||||
|
// question dies with the process, and she does not claim to have let it go —
|
||||||
|
// the words that follow are routed as a fresh request. Restarting is modelled
|
||||||
|
// the way the daemon does it, by building a second handler over the same store.
|
||||||
|
func TestARestartExpiresTheParkedQuestion(t *testing.T) {
|
||||||
|
h, _, _ := newClarifyHandler(t)
|
||||||
|
ctx := voiceCtx()
|
||||||
|
if _, asked := h.askClarify(ctx, clarifyDec(router.IntentReminder, router.Slots{Text: "напомни"}, "напомни")); !asked {
|
||||||
|
t.Fatal("expected a question before the restart")
|
||||||
|
}
|
||||||
|
|
||||||
|
restarted, _, _ := newClarifyHandler(t)
|
||||||
|
if _, handled := restarted.resolveClarifyAnswer(ctx, "в 11:00"); handled {
|
||||||
|
t.Fatal("a question parked before the restart must not eat the next utterance")
|
||||||
|
}
|
||||||
|
if notice := restarted.clarifyExpiredNotice(ctx); notice != "" {
|
||||||
|
t.Fatalf("notice = %q, want silence: nothing survived to expire", notice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -238,7 +238,10 @@ func wireVoice(cfg *config.Config, coreAPI ipc.CoreAPI, phr phraser.Phraser, mem
|
|||||||
// ----- dialogue (multi-turn slot carry-over; 2-min follow-up window) -----
|
// ----- dialogue (multi-turn slot carry-over; 2-min follow-up window) -----
|
||||||
// Store-backed when the daemon passes a store, so a restart mid-conversation
|
// Store-backed when the daemon passes a store, so a restart mid-conversation
|
||||||
// keeps the thread (Vikunja #363). Sessions past their TTL are dropped on
|
// keeps the thread (Vikunja #363). Sessions past their TTL are dropped on
|
||||||
// load, never revived. Clarify's parked question stays in memory only.
|
// load, never revived. Clarify's parked question stays in memory only, and
|
||||||
|
// that is a decision rather than an omission (Vikunja #385, docs/design.md):
|
||||||
|
// a restart expires it, so the thread comes back and the open question does
|
||||||
|
// not.
|
||||||
var dialogueSessions *dialogue.SessionStore
|
var dialogueSessions *dialogue.SessionStore
|
||||||
if dataStore != nil {
|
if dataStore != nil {
|
||||||
dialogueSessions = dialogue.NewPersistentSessionStore(2*time.Minute, dataStore)
|
dialogueSessions = dialogue.NewPersistentSessionStore(2*time.Minute, dataStore)
|
||||||
|
|||||||
@@ -223,6 +223,30 @@ Not alternatives — layers:
|
|||||||
Router contract: `[{"intent":<enum>, key?, value?, text?, verb?}, ...]` over
|
Router contract: `[{"intent":<enum>, key?, value?, text?, verb?}, ...]` over
|
||||||
7 intents (`fact, reminder, note, query, act, chat, system`).
|
7 intents (`fact, reminder, note, query, act, chat, system`).
|
||||||
|
|
||||||
|
#### A restart expires a parked question
|
||||||
|
|
||||||
|
Decided 2026-08-04 (Vikunja #385). The follow-up dialogue session survives a
|
||||||
|
restart; the clarify question parked behind it does not, and neither do the
|
||||||
|
three yes/no confirms in `voice.go`. `ClarifyStore` stays in memory.
|
||||||
|
|
||||||
|
Three reasons, in the order they settle it:
|
||||||
|
|
||||||
|
- The clock stops meaning anything. A parked question carries a 90s TTL and an
|
||||||
|
attempt count. A restart is a gap of unknown length, so a restored question is
|
||||||
|
either already dead or pretending to be young.
|
||||||
|
- Restoring the question restores the request behind it. He asked for something,
|
||||||
|
she asked back, and then the daemon went away. Acting on that minutes later,
|
||||||
|
against words he has probably given up on, is the misroute the stage 3 gate
|
||||||
|
exists to avoid.
|
||||||
|
- She does not announce it either. The expiry notice needs to know a question
|
||||||
|
was parked, and knowing that across a restart means storing it. One sentence,
|
||||||
|
in the rare window where he speaks within 90s of a restart, does not pay for a
|
||||||
|
marker that outlives the thing it describes. His next words route fresh, which
|
||||||
|
is the correct answer with or without the notice.
|
||||||
|
|
||||||
|
So the notice stays what it is: the in-process TTL case, where she really did
|
||||||
|
wait and really did let go.
|
||||||
|
|
||||||
### save-where — the two-memory routing axis
|
### save-where — the two-memory routing axis
|
||||||
|
|
||||||
One discriminator: **does the loop evaluate a predicate against it?**
|
One discriminator: **does the loop evaluate a predicate against it?**
|
||||||
|
|||||||
@@ -57,6 +57,14 @@ func (q *PendingQuestion) CanAsk() bool {
|
|||||||
|
|
||||||
// ClarifyStore holds the parked questions. Same shape and locking as
|
// ClarifyStore holds the parked questions. Same shape and locking as
|
||||||
// SessionStore: keyed by dialogue id, expired entries dropped on read.
|
// SessionStore: keyed by dialogue id, expired entries dropped on read.
|
||||||
|
//
|
||||||
|
// Memory only, deliberately, unlike SessionStore — a restart expires every
|
||||||
|
// parked question and she does not announce that it happened (Vikunja #385,
|
||||||
|
// written down in docs/design.md). The 90s TTL and the attempt count measure a
|
||||||
|
// pause in one conversation, and a restart is a gap of unknown length, so a
|
||||||
|
// restored question would either be dead already or lying about its age. His
|
||||||
|
// next words route fresh, which is the right answer with or without a notice.
|
||||||
|
// Do not give this store a persister without re-arguing that.
|
||||||
type ClarifyStore struct {
|
type ClarifyStore struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
questions map[string]*PendingQuestion
|
questions map[string]*PendingQuestion
|
||||||
|
|||||||
Reference in New Issue
Block a user