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:
2026-08-06 01:23:19 +04:00
parent 70b32af8a7
commit c8a5b5416e
5 changed files with 236 additions and 32 deletions
+13 -10
View File
@@ -207,23 +207,26 @@ func (s *ClarifyStore) Depth(id string) int {
return len(s.stacks[id])
}
// TakeExpired reports whether a question was parked here but its TTL ran out,
// and drops it. Get drops such a question silently, which leaves the user
// thinking his request is still alive — the caller uses this to tell him it is
// gone before treating his words as a fresh utterance.
// TakeExpired reports HOW MANY parked questions were dropped because the TTL
// ran out, and drops them. 0 ⇒ nothing was parked, or what was parked is still
// live. Get drops such a question silently, which leaves the user thinking his
// 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
// notice is what a reply can carry, and anything parked under a question that
// timed out has been waiting at least as long.
func (s *ClarifyStore) TakeExpired(id string, now time.Time) bool {
// It looks at the top only, and drops the whole stack when that one is dead:
// anything parked under a question that timed out has been waiting at least as
// long. The COUNT rather than a bool since V-561, because the stack can now
// 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()
defer s.mu.Unlock()
stack := s.stacks[id]
if len(stack) == 0 || !stack[len(stack)-1].IsExpired(now) {
return false
return 0
}
delete(s.stacks, id)
return true
return len(stack)
}
// Delete drops every question parked for this id. The old single-slot Delete