a side query suspends the flow instead of ending it (V-561)
V-560 classified the side query correctly and then dropped the request behind it, saying "Прошлую просьбу отпускаю." The owner rejected that on sight: he asked about the weather in the middle of setting a reminder, and being told the reminder was let go reports a loss he did not ask for. It had not been lost — there was simply nowhere to put it. There is now. ClarifyStore grew a bounded stack in V-559 and nothing called Push; this is the caller it was built for. A side query leaves the question parked exactly as it is, the words are answered as themselves, and the question comes back on the end of the same reply — one utterance, two acts. The resumed question is not the first one again. "Когда?" works in the same breath as "напомни позвонить маме" and does not work after a turn about Rome, so the deck has a second form per slot that names the request: "На какое время поставить напоминание?". No attempt is spent, because he answered the side query and not the parked question, and charging a retry for a turn that was never an answer is the V-554 shape. clarifyDropped stays for new_request and cancel, where something really does die. Two things can now die at once, so TakeExpired reports a count instead of a bool and the expiry notice has a plural wording — "прошлую просьбу" when two were lost would be a lie about the number. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+171
-21
@@ -6,6 +6,8 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/kami/maven/internal/dialogue"
|
"github.com/kami/maven/internal/dialogue"
|
||||||
"github.com/kami/maven/internal/router"
|
"github.com/kami/maven/internal/router"
|
||||||
@@ -65,8 +67,28 @@ var clarifyExpiredVariants = []string{
|
|||||||
"Столько времени прошло, что я отпустила прошлую просьбу. Скажи заново, если она в силе.",
|
"Столько времени прошло, что я отпустила прошлую просьбу. Скажи заново, если она в силе.",
|
||||||
}
|
}
|
||||||
|
|
||||||
// clarifyExpiredLine picks one of them at random.
|
// clarifyExpiredPluralVariants — the same notice when TWO parked requests died
|
||||||
func clarifyExpiredLine() string {
|
// together (Vikunja #561). Since a side query suspends the flow instead of
|
||||||
|
// dropping it, the stack can hold both the flow and the thing he interrupted it
|
||||||
|
// with, and TakeExpired drops the whole stack when the top times out. "Прошлую
|
||||||
|
// просьбу" would then be a lie about the count: he loses two and hears about
|
||||||
|
// one.
|
||||||
|
//
|
||||||
|
// Two phrasings only, against five for the singular. This fires when he walks
|
||||||
|
// off in the middle of an interrupted exchange, which is rarer than walking off
|
||||||
|
// in the middle of a plain one, so it repeats less and needs less variety.
|
||||||
|
var clarifyExpiredPluralVariants = []string{
|
||||||
|
"Прости, я слишком долго ждала и отпустила обе прошлые просьбы. Если они ещё нужны, скажи заново.",
|
||||||
|
"Я не дождалась ответа и убрала обе прошлые просьбы. Повтори, если они всё ещё нужны.",
|
||||||
|
}
|
||||||
|
|
||||||
|
// clarifyExpiredLine picks one of them at random. n is how many requests died;
|
||||||
|
// anything above one gets the plural wording, because the bound is two today and
|
||||||
|
// a third would still be "обе" short of the truth only if MaxStackDepth grew.
|
||||||
|
func clarifyExpiredLine(n int) string {
|
||||||
|
if n > 1 {
|
||||||
|
return clarifyExpiredPluralVariants[rand.Intn(len(clarifyExpiredPluralVariants))]
|
||||||
|
}
|
||||||
return clarifyExpiredVariants[rand.Intn(len(clarifyExpiredVariants))]
|
return clarifyExpiredVariants[rand.Intn(len(clarifyExpiredVariants))]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,23 +96,34 @@ func clarifyExpiredLine() string {
|
|||||||
// notice is glued in front of this turn's reply (see withNotice), so a caller
|
// notice is glued in front of this turn's reply (see withNotice), so a caller
|
||||||
// checking for it has to match a prefix, not the whole string.
|
// checking for it has to match a prefix, not the whole string.
|
||||||
func isClarifyExpired(s string) bool {
|
func isClarifyExpired(s string) bool {
|
||||||
for _, v := range clarifyExpiredVariants {
|
_, ok := cutClarifyExpired(s)
|
||||||
if strings.HasPrefix(s, v) {
|
return ok
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// trimClarifyExpired strips a leading expiry notice, leaving this turn's actual
|
// trimClarifyExpired strips a leading expiry notice, leaving this turn's actual
|
||||||
// reply. "" ⇒ the notice was the whole thing.
|
// reply. "" ⇒ the notice was the whole thing.
|
||||||
func trimClarifyExpired(s string) string {
|
func trimClarifyExpired(s string) string {
|
||||||
for _, v := range clarifyExpiredVariants {
|
rest, ok := cutClarifyExpired(s)
|
||||||
if strings.HasPrefix(s, v) {
|
if !ok {
|
||||||
return strings.TrimSpace(strings.TrimPrefix(s, v))
|
return strings.TrimSpace(s)
|
||||||
|
}
|
||||||
|
return rest
|
||||||
|
}
|
||||||
|
|
||||||
|
// cutClarifyExpired matches either expiry deck as a prefix and returns what
|
||||||
|
// follows it. Both decks, since V-561 added the plural line: a caller asking
|
||||||
|
// "did she say a request timed out" means the fact, not which wording carried
|
||||||
|
// it, and a helper that knew only the singular would read the two-request
|
||||||
|
// notice as ordinary reply text.
|
||||||
|
func cutClarifyExpired(s string) (string, bool) {
|
||||||
|
for _, deck := range [][]string{clarifyExpiredVariants, clarifyExpiredPluralVariants} {
|
||||||
|
for _, v := range deck {
|
||||||
|
if strings.HasPrefix(s, v) {
|
||||||
|
return strings.TrimSpace(strings.TrimPrefix(s, v)), true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(s)
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
// clarifyExpiredNotice returns that line when a parked question had just timed
|
// clarifyExpiredNotice returns that line when a parked question had just timed
|
||||||
@@ -101,11 +134,12 @@ func (h *reactiveHandler) clarifyExpiredNotice(ctx context.Context) string {
|
|||||||
if h.clarifyStore == nil {
|
if h.clarifyStore == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
if !h.clarifyStore.TakeExpired(dialogueIDOf(ctx), h.now()) {
|
n := h.clarifyStore.TakeExpired(dialogueIDOf(ctx), h.now())
|
||||||
|
if n == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
log.Printf("voice: clarify — parked question expired, telling him and routing the words fresh")
|
log.Printf("voice: clarify — %d parked question(s) expired, telling him and routing the words fresh", n)
|
||||||
return clarifyExpiredLine()
|
return clarifyExpiredLine(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
// withNotice glues the expiry notice in front of this turn's reply. One turn
|
// withNotice glues the expiry notice in front of this turn's reply. One turn
|
||||||
@@ -121,6 +155,48 @@ func withNotice(notice, reply string) string {
|
|||||||
return notice + " " + reply
|
return notice + " " + reply
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// withResumed puts the resumed question on the END of this turn's reply, where
|
||||||
|
// withNotice puts the expiry notice on the front (Vikunja #561).
|
||||||
|
//
|
||||||
|
// The order is the owner's: "в Риме сейчас ..., на какое время поставить
|
||||||
|
// напоминание?" — answer first, then the open question. A question in front of
|
||||||
|
// its own answer would read as ignoring what he asked.
|
||||||
|
//
|
||||||
|
// A statement's full stop is folded into a comma, so the two acts read as one
|
||||||
|
// sentence — that is the owner's own punctuation, "в Риме сейчас ..., на какое
|
||||||
|
// время поставить напоминание?". An answer that is ITSELF a question keeps its
|
||||||
|
// mark and the resume starts a new sentence: she sometimes answers a side query
|
||||||
|
// by asking him to say it again, and "переформулировать?, на какое время" folds
|
||||||
|
// two questions into one unreadable line.
|
||||||
|
//
|
||||||
|
// A resume with no answer in front of it is just the question.
|
||||||
|
func withResumed(reply, resumed string) string {
|
||||||
|
if resumed == "" {
|
||||||
|
return reply
|
||||||
|
}
|
||||||
|
reply = strings.TrimSpace(reply)
|
||||||
|
if reply == "" {
|
||||||
|
return resumed
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(reply, "?") {
|
||||||
|
return reply + " " + resumed
|
||||||
|
}
|
||||||
|
if trimmed := strings.TrimRight(reply, ".!"); trimmed != "" {
|
||||||
|
reply = trimmed
|
||||||
|
}
|
||||||
|
return reply + ", " + lowerFirst(resumed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// lowerFirst lowercases the opening rune, so a deck line written as a standalone
|
||||||
|
// sentence reads as the second half of one. Only the first rune: "На какое
|
||||||
|
// время" must become "на какое время" and nothing else in it may move.
|
||||||
|
func lowerFirst(s string) string {
|
||||||
|
for i, r := range s {
|
||||||
|
return string(unicode.ToLower(r)) + s[i+utf8.RuneLen(r):]
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
// missingFor returns the slots a decision still needs, most important first.
|
// missingFor returns the slots a decision still needs, most important first.
|
||||||
// Empty ⇒ there is nothing identifiable to ask about.
|
// Empty ⇒ there is nothing identifiable to ask about.
|
||||||
func missingFor(dec router.Decision) []dialogue.Slot {
|
func missingFor(dec router.Decision) []dialogue.Slot {
|
||||||
@@ -161,7 +237,7 @@ func (h *reactiveHandler) askClarify(ctx context.Context, dec router.Decision) (
|
|||||||
log.Printf("voice: clarify — act %q matched no capability; saying so instead of asking", dec.Utterance)
|
log.Printf("voice: clarify — act %q matched no capability; saying so instead of asking", dec.Utterance)
|
||||||
return actNotRecognized, true
|
return actNotRecognized, true
|
||||||
}
|
}
|
||||||
h.clarifyStore.Put(dialogueIDOf(ctx), &dialogue.PendingQuestion{
|
q := &dialogue.PendingQuestion{
|
||||||
Intent: dialogue.Intent(dec.Intent),
|
Intent: dialogue.Intent(dec.Intent),
|
||||||
Slots: toDialogueSlots(dec.Slots),
|
Slots: toDialogueSlots(dec.Slots),
|
||||||
Missing: []dialogue.Slot{slot},
|
Missing: []dialogue.Slot{slot},
|
||||||
@@ -170,7 +246,33 @@ func (h *reactiveHandler) askClarify(ctx context.Context, dec router.Decision) (
|
|||||||
TTL: clarifyTTL,
|
TTL: clarifyTTL,
|
||||||
Attempts: 1, // this ask
|
Attempts: 1, // this ask
|
||||||
MaxAttempts: h.clarifyMaxAttempts,
|
MaxAttempts: h.clarifyMaxAttempts,
|
||||||
})
|
}
|
||||||
|
// Push, not Put, when this turn suspended a flow (Vikunja #561): the side
|
||||||
|
// query needs clarifying of ITS own, and Put would replace the top of the
|
||||||
|
// stack — which is the very question the side query was allowed to interrupt
|
||||||
|
// rather than kill. Push keeps both.
|
||||||
|
//
|
||||||
|
// Push returns whatever the depth bound forced out, and that one has to be
|
||||||
|
// spoken: MaxStackDepth is a promise that every level she keeps is a level
|
||||||
|
// she can name when it dies. It is glued in front, like every other notice
|
||||||
|
// about something let go.
|
||||||
|
rt := turnRouteFrom(ctx)
|
||||||
|
if rt != nil && rt.suspended {
|
||||||
|
if evicted := h.clarifyStore.Push(dialogueIDOf(ctx), q); evicted != nil {
|
||||||
|
log.Printf("voice: clarify — stack full at %d, letting go of the request behind %q", dialogue.MaxStackDepth, evicted.Utterance)
|
||||||
|
rt.dropped = withNotice(rt.dropped, clarifyDropped)
|
||||||
|
}
|
||||||
|
// One question per breath still holds. The side query turned out to need
|
||||||
|
// a question of its own, so THAT is the one she asks; resuming as well
|
||||||
|
// would put two questions in one reply, which is the interrogation
|
||||||
|
// askRemainingGap already refuses to run. The suspended flow keeps its
|
||||||
|
// place underneath and is not lost — if it is never reached it dies on
|
||||||
|
// the TTL, and the expiry notice (now plural-aware) says so.
|
||||||
|
rt.resume = ""
|
||||||
|
log.Printf("voice: clarify — asked about %s for intent=%s, stacked on a suspended flow", slot, dec.Intent)
|
||||||
|
return question, true
|
||||||
|
}
|
||||||
|
h.clarifyStore.Put(dialogueIDOf(ctx), q)
|
||||||
log.Printf("voice: clarify — asked about %s for intent=%s", slot, dec.Intent)
|
log.Printf("voice: clarify — asked about %s for intent=%s", slot, dec.Intent)
|
||||||
return question, true
|
return question, true
|
||||||
}
|
}
|
||||||
@@ -180,10 +282,17 @@ func (h *reactiveHandler) askClarify(ctx context.Context, dec router.Decision) (
|
|||||||
// self-reference ("отменила"), as everywhere.
|
// self-reference ("отменила"), as everywhere.
|
||||||
const clarifyCancelled = "Хорошо, отменила."
|
const clarifyCancelled = "Хорошо, отменила."
|
||||||
|
|
||||||
// clarifyDropped — he asked for something else instead, so the parked request
|
// clarifyDropped — he asked for something ELSE instead, so the parked request
|
||||||
// is gone. Glued in front of the answer to what he actually asked, because
|
// is gone. Glued in front of the answer to what he actually asked, because
|
||||||
// nothing may be dropped in silence. V-561 suspends and resumes it instead of
|
// nothing may be dropped in silence.
|
||||||
// letting it go, and this line goes away with it.
|
//
|
||||||
|
// Only new_request and cancel reach this since V-561. A side query used to as
|
||||||
|
// well, and the owner rejected it on sight: he asks about the weather in the
|
||||||
|
// middle of setting a reminder, and hearing "прошлую просьбу отпускаю" tells him
|
||||||
|
// a thing he did not ask to lose has been lost. It had not been — there was
|
||||||
|
// simply nowhere to put it. Now there is (ClarifyStore's stack), so a side query
|
||||||
|
// suspends and resumes, and apologising for a drop that did not happen is worse
|
||||||
|
// than saying nothing.
|
||||||
const clarifyDropped = "Прошлую просьбу отпускаю."
|
const clarifyDropped = "Прошлую просьбу отпускаю."
|
||||||
|
|
||||||
// resolveClarifyAnswer reads an utterance against the parked question and
|
// resolveClarifyAnswer reads an utterance against the parked question and
|
||||||
@@ -227,7 +336,19 @@ func (h *reactiveHandler) resolveClarifyAnswer(ctx context.Context, text string)
|
|||||||
case roleCancel:
|
case roleCancel:
|
||||||
h.clarifyStore.Delete(dialogueIDOf(ctx))
|
h.clarifyStore.Delete(dialogueIDOf(ctx))
|
||||||
return clarifyCancelled, true
|
return clarifyCancelled, true
|
||||||
case roleSideQuery, roleNewRequest:
|
case roleSideQuery:
|
||||||
|
// He asked something of his own WITHOUT leaving the flow. The question
|
||||||
|
// stays exactly where it is — same slot, same attempt, same parked
|
||||||
|
// utterance — and these words go on to be answered as themselves. The
|
||||||
|
// resumed question is then glued onto the back of that answer, so one
|
||||||
|
// reply carries both acts (Vikunja #561).
|
||||||
|
//
|
||||||
|
// No attempt is spent. He answered the side query, not the parked
|
||||||
|
// question, and charging a retry for a turn that was never an answer is
|
||||||
|
// the V-554 shape.
|
||||||
|
h.noteSuspended(ctx, q)
|
||||||
|
return "", false
|
||||||
|
case roleNewRequest:
|
||||||
// He moved on. A parked question used to swallow whatever came next, so
|
// He moved on. A parked question used to swallow whatever came next, so
|
||||||
// one act she could not fulfil ate the following three turns (Vikunja
|
// one act she could not fulfil ate the following three turns (Vikunja
|
||||||
// #554) and a world question set a reminder for a time nobody asked for
|
// #554) and a world question set a reminder for a time nobody asked for
|
||||||
@@ -283,6 +404,35 @@ func (h *reactiveHandler) noteDropped(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// noteSuspended keeps the parked question alive across a side query and records
|
||||||
|
// the words that bring it back, so runTurn can put them after this turn's answer
|
||||||
|
// (Vikunja #561).
|
||||||
|
//
|
||||||
|
// Two things happen to the question and neither is an attempt. Its clock is
|
||||||
|
// restarted, because she is about to ask it again and the 90s TTL measures the
|
||||||
|
// pause since she last spoke it — leaving Asked at the original ask would let a
|
||||||
|
// flow he is actively working through die of a wait he did not take. And the
|
||||||
|
// stack is left exactly as it is: the question is already on top, so suspending
|
||||||
|
// it is not a write.
|
||||||
|
//
|
||||||
|
// A slot with no resumed wording (clarifyResumedFor says so) resumes nothing and
|
||||||
|
// says nothing. She must not claim to be holding a question she cannot re-ask.
|
||||||
|
func (h *reactiveHandler) noteSuspended(ctx context.Context, q *dialogue.PendingQuestion) {
|
||||||
|
rt := turnRouteFrom(ctx)
|
||||||
|
if rt == nil || len(q.Missing) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
question, ok := clarifyResumedFor(q.Missing[0])
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
q.Asked = h.now()
|
||||||
|
h.clarifyStore.Put(dialogueIDOf(ctx), q)
|
||||||
|
rt.resume = question
|
||||||
|
rt.suspended = true
|
||||||
|
log.Printf("voice: clarify — is its own request; suspending the question about %s and resuming it in the same reply", q.Missing[0])
|
||||||
|
}
|
||||||
|
|
||||||
// foldAnswerIntoUtterance appends an answered subject to the original words,
|
// foldAnswerIntoUtterance appends an answered subject to the original words,
|
||||||
// unless they already carry it. "напомни" + "позвонить маме" reads as the
|
// unless they already carry it. "напомни" + "позвонить маме" reads as the
|
||||||
// request he would have made in one breath. Nothing is appended when the
|
// request he would have made in one breath. Nothing is appended when the
|
||||||
|
|||||||
@@ -50,6 +50,38 @@ var clarifyQuestionVariants = map[dialogue.Slot][]string{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clarifyResumedVariants — the wording for a question coming BACK after a side
|
||||||
|
// query took the turn away from it (Vikunja #561).
|
||||||
|
//
|
||||||
|
// It is not the first question again. "Когда?" works in the same breath as
|
||||||
|
// "напомни позвонить маме", because the thing it is about was just said. After
|
||||||
|
// a turn about the weather in Rome it does not: he has been thinking about
|
||||||
|
// something else, and a bare "Когда?" asks him to remember what she is holding.
|
||||||
|
// So the resumed form names the request — "напоминание", "заметка" — and the
|
||||||
|
// first form stays short.
|
||||||
|
//
|
||||||
|
// One wording per slot, not a rotation and not an attempt ladder. A resume does
|
||||||
|
// not spend an attempt (that is the point of suspending rather than re-asking),
|
||||||
|
// so there is no attempt number to vary on, and this line is heard once per
|
||||||
|
// interruption rather than repeatedly.
|
||||||
|
//
|
||||||
|
// Persona holds: infinitive, so no gender agreement, "ты" nowhere needed, no pet
|
||||||
|
// names.
|
||||||
|
var clarifyResumedVariants = map[dialogue.Slot]string{
|
||||||
|
dialogue.SlotTime: "На какое время поставить напоминание?",
|
||||||
|
dialogue.SlotText: "Так о чём напомнить?",
|
||||||
|
dialogue.SlotKey: "Так что записать?",
|
||||||
|
dialogue.SlotFn: "Так какое действие выполнить?",
|
||||||
|
}
|
||||||
|
|
||||||
|
// clarifyResumedFor gives the resumed wording for a slot. ("", false) when the
|
||||||
|
// slot has none, and the caller then resumes nothing rather than inventing a
|
||||||
|
// question — a flow it cannot re-ask is one it must not claim to be holding.
|
||||||
|
func clarifyResumedFor(slot dialogue.Slot) (string, bool) {
|
||||||
|
q, ok := clarifyResumedVariants[slot]
|
||||||
|
return q, ok
|
||||||
|
}
|
||||||
|
|
||||||
// actNotRecognized is what an act she cannot run gets (Vikunja #556).
|
// actNotRecognized is what an act she cannot run gets (Vikunja #556).
|
||||||
//
|
//
|
||||||
// The deck used to ask "Что сделать?" instead. That question has no answer he
|
// The deck used to ask "Что сделать?" instead. That question has no answer he
|
||||||
|
|||||||
@@ -31,6 +31,17 @@ type turnRoute struct {
|
|||||||
// dropped — what she let go of this turn and must say out loud. A parked
|
// dropped — what she let go of this turn and must say out loud. A parked
|
||||||
// request that dies without a word leaves him thinking it landed.
|
// request that dies without a word leaves him thinking it landed.
|
||||||
dropped string
|
dropped string
|
||||||
|
|
||||||
|
// resume — the parked question, re-worded, to put AFTER this turn's answer
|
||||||
|
// (Vikunja #561). A side query does not end the flow it interrupted, so the
|
||||||
|
// reply carries two acts: the answer he asked for, then the question he
|
||||||
|
// still owes her. Empty ⇒ nothing was suspended.
|
||||||
|
resume string
|
||||||
|
// suspended — a flow is parked underneath this turn. askClarify reads it to
|
||||||
|
// decide between Put (replace the top) and Push (keep the flow and stack the
|
||||||
|
// new question on it), because a side query that needs clarifying of its own
|
||||||
|
// must not overwrite the thing it interrupted.
|
||||||
|
suspended bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type turnRouteKey struct{}
|
type turnRouteKey struct{}
|
||||||
|
|||||||
+9
-1
@@ -255,7 +255,7 @@ const (
|
|||||||
// path wraps it in stt/tts, the text path returns it as-is.
|
// path wraps it in stt/tts, the text path returns it as-is.
|
||||||
//
|
//
|
||||||
// The ordering is load-bearing — see the step comments.
|
// The ordering is load-bearing — see the step comments.
|
||||||
func (h *reactiveHandler) runTurn(ctx context.Context, text string, src turnSource) string {
|
func (h *reactiveHandler) runTurn(ctx context.Context, text string, src turnSource) (reply string) {
|
||||||
// 0. the decision record (V-564). Installed here rather than in the IPC
|
// 0. the decision record (V-564). Installed here rather than in the IPC
|
||||||
// entry point, so the mic, telegram and the web all leave the same trail —
|
// entry point, so the mic, telegram and the web all leave the same trail —
|
||||||
// a record only the web produced would be missing exactly the turns that
|
// a record only the web produced would be missing exactly the turns that
|
||||||
@@ -311,6 +311,14 @@ func (h *reactiveHandler) runTurn(ctx context.Context, text string, src turnSour
|
|||||||
// with — carried on the same notice, so every exit below keeps it.
|
// with — carried on the same notice, so every exit below keeps it.
|
||||||
expiredNotice = withNotice(expiredNotice, rt.dropped)
|
expiredNotice = withNotice(expiredNotice, rt.dropped)
|
||||||
|
|
||||||
|
// 3b. and if it SUSPENDED a request instead of letting it go, the question
|
||||||
|
// comes back on the end of whatever these words are answered with (Vikunja
|
||||||
|
// #561). A deferred append rather than a call at each exit: there are eight
|
||||||
|
// returns between here and the replier, and the flow has to survive all of
|
||||||
|
// them — one that forgot would be a request parked for ever, waiting for an
|
||||||
|
// answer to a question he never heard asked.
|
||||||
|
defer func() { reply = withResumed(reply, rt.resume) }()
|
||||||
|
|
||||||
// 4. quiet-hours toggle — keyword match, not classifier-dependent.
|
// 4. quiet-hours toggle — keyword match, not classifier-dependent.
|
||||||
// "тихий режим" / "quiet on" would route through the classifier
|
// "тихий режим" / "quiet on" would route through the classifier
|
||||||
// unreliably (it's a command, not a free-form query), so we match it
|
// unreliably (it's a command, not a free-form query), so we match it
|
||||||
|
|||||||
@@ -207,23 +207,26 @@ func (s *ClarifyStore) Depth(id string) int {
|
|||||||
return len(s.stacks[id])
|
return len(s.stacks[id])
|
||||||
}
|
}
|
||||||
|
|
||||||
// TakeExpired reports whether a question was parked here but its TTL ran out,
|
// TakeExpired reports HOW MANY parked questions were dropped because the TTL
|
||||||
// and drops it. Get drops such a question silently, which leaves the user
|
// ran out, and drops them. 0 ⇒ nothing was parked, or what was parked is still
|
||||||
// thinking his request is still alive — the caller uses this to tell him it is
|
// live. Get drops such a question silently, which leaves the user thinking his
|
||||||
// gone before treating his words as a fresh utterance.
|
// request is still alive — the caller uses this to tell him it is gone before
|
||||||
|
// treating his words as a fresh utterance.
|
||||||
//
|
//
|
||||||
// It looks at the top only, and drops the whole stack when that one is dead: one
|
// It looks at the top only, and drops the whole stack when that one is dead:
|
||||||
// notice is what a reply can carry, and anything parked under a question that
|
// anything parked under a question that timed out has been waiting at least as
|
||||||
// timed out has been waiting at least as long.
|
// long. The COUNT rather than a bool since V-561, because the stack can now
|
||||||
func (s *ClarifyStore) TakeExpired(id string, now time.Time) bool {
|
// hold two — the flow and the side query that suspended it — and a notice
|
||||||
|
// saying "прошлую просьбу" when two died is a lie about the count.
|
||||||
|
func (s *ClarifyStore) TakeExpired(id string, now time.Time) int {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
stack := s.stacks[id]
|
stack := s.stacks[id]
|
||||||
if len(stack) == 0 || !stack[len(stack)-1].IsExpired(now) {
|
if len(stack) == 0 || !stack[len(stack)-1].IsExpired(now) {
|
||||||
return false
|
return 0
|
||||||
}
|
}
|
||||||
delete(s.stacks, id)
|
delete(s.stacks, id)
|
||||||
return true
|
return len(stack)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete drops every question parked for this id. The old single-slot Delete
|
// Delete drops every question parked for this id. The old single-slot Delete
|
||||||
|
|||||||
Reference in New Issue
Block a user