Walk a chain of confirm resolvers instead of three copied blocks

This commit is contained in:
kami
2026-08-01 00:42:23 +04:00
parent 5253123d99
commit ad5eb2d1cf
+85 -74
View File
@@ -63,89 +63,100 @@ func (h *reactiveHandler) resolveConfirm(ctx context.Context, text string) (stri
h.mu.Lock()
defer h.mu.Unlock()
// Check routine proposal first (newer feature; checked before tool confirm
// so a routine confirm doesn't get eaten by a stale tool pending).
pr := h.pendingRoutine
if pr != nil && !h.now().After(pr.expiry) {
for _, r := range h.confirmResolvers(ctx) {
if !r.claim() {
continue
}
// The slot is already cleared by claim(): every branch below drops the
// pending, including the unclear one — a confirm that can't be
// answered clearly is safer abandoned than left armed.
switch classifyConfirm(text) {
case confirmYes:
h.pendingRoutine = nil
// Only record the acceptance. The tick loop reads accepted
// routines and nudges on their own interval. Building a reminder
// here made a routine fire exactly once (Vikunja #366).
if err := h.dataStore.AcceptProposedRoutine(ctx, pr.routineID, h.now()); err != nil {
log.Printf("voice: accept proposed routine: %v", err)
return "не получилось запомнить рутину.", true
}
return "буду напоминать.", true
return r.yes(), true
case confirmNo:
h.pendingRoutine = nil
if err := h.dataStore.DismissProposedRoutine(ctx, pr.routineID); err != nil {
log.Printf("voice: dismiss proposed routine: %v", err)
}
return "хорошо, не буду.", true
return r.no(), true
default:
// unclear: abandon the routine proposal, route normally.
h.pendingRoutine = nil
return "", false
}
}
// Clear expired routine if it existed.
if pr != nil {
h.pendingRoutine = nil
}
return "", false
}
// Check pending Hexis execution confirm. Bound to the exact capability +
// target that was proposed; a stray "да" can only run that, nothing else.
if hx := h.pendingHexis; hx != nil {
if h.now().After(hx.expiry) {
h.pendingHexis = nil
} else {
switch classifyConfirm(text) {
case confirmYes:
h.pendingHexis = nil
return h.execHexis(ctx, hx.capabilityID, hx.capName, hx.entityID, hx.displayName), true
case confirmNo:
h.pendingHexis = nil
return "отменила.", true
default:
h.pendingHexis = nil
return "", false
}
}
}
// confirmResolver — one parked-confirm slot in the chain. claim() reports
// whether this slot holds a live pending, taking it (and dropping an expired
// one) as it goes; yes/no then run the answer. Only ever called with h.mu held.
type confirmResolver struct {
claim func() bool
yes func() string
no func() string
}
// Check tool confirm (existing behavior).
p := h.pending
if p == nil {
return "", false
}
if h.now().After(p.expiry) {
h.pending = nil
return "", false
}
switch classifyConfirm(text) {
case confirmYes:
h.pending = nil
out, err := h.tools.Exec(ctx, p.fn, p.args, true) // confirmed
if err != nil {
log.Printf("voice: tool %s (confirmed): %v", p.fn, err)
if out != "" {
return "не получилось выполнить команду: " + firstLine(out), true
}
return "не получилось выполнить команду.", true
}
if out != "" {
return "готово: " + firstLine(out), true
}
return "готово.", true
case confirmNo:
h.pending = nil
return "отменила.", true
default:
// unclear answer: abandon the confirm, route this utterance normally.
h.pending = nil
return "", false
// confirmResolvers builds the ordered chain resolveConfirm walks. Order is
// deliberate: the routine proposal is checked before the tool confirm so a
// routine confirm doesn't get eaten by a stale tool pending.
func (h *reactiveHandler) confirmResolvers(ctx context.Context) []confirmResolver {
var pr *pendingRoutineConfirm
var hx *pendingHexisExec
var p *pendingAct
return []confirmResolver{
// Routine proposal.
{
claim: func() bool {
pr, h.pendingRoutine = h.pendingRoutine, nil
return pr != nil && !h.now().After(pr.expiry)
},
yes: func() string {
// Only record the acceptance. The tick loop reads accepted
// routines and nudges on their own interval. Building a
// reminder here made a routine fire exactly once (Vikunja #366).
if err := h.dataStore.AcceptProposedRoutine(ctx, pr.routineID, h.now()); err != nil {
log.Printf("voice: accept proposed routine: %v", err)
return "не получилось запомнить рутину."
}
return "буду напоминать."
},
no: func() string {
if err := h.dataStore.DismissProposedRoutine(ctx, pr.routineID); err != nil {
log.Printf("voice: dismiss proposed routine: %v", err)
}
return "хорошо, не буду."
},
},
// Hexis execution confirm. Bound to the exact capability + target that
// was proposed; a stray "да" can only run that, nothing else.
{
claim: func() bool {
hx, h.pendingHexis = h.pendingHexis, nil
return hx != nil && !h.now().After(hx.expiry)
},
yes: func() string {
return h.execHexis(ctx, hx.capabilityID, hx.capName, hx.entityID, hx.displayName)
},
no: func() string { return "отменила." },
},
// Tool confirm.
{
claim: func() bool {
p, h.pending = h.pending, nil
return p != nil && !h.now().After(p.expiry)
},
yes: func() string {
out, err := h.tools.Exec(ctx, p.fn, p.args, true) // confirmed
if err != nil {
log.Printf("voice: tool %s (confirmed): %v", p.fn, err)
if out != "" {
return "не получилось выполнить команду: " + firstLine(out)
}
return "не получилось выполнить команду."
}
if out != "" {
return "готово: " + firstLine(out)
}
return "готово."
},
no: func() string { return "отменила." },
},
}
}