Cancel a reminder by voice, and honour a refusal (V-719)

reminder_cancel.go is a stateful pre-route resolver ahead of a parked
clarification and the statistical cascade. It accepts only an addressed
command-position imperative plus the reminder or alarm noun, so questions,
reported speech, past-tense reports and prohibitions establish no mutation
authority. Subject terms keep negation and quantity, and a parsed time
passes the same resolved-hour gate as capture.

One match cancels through the typed IPC method. Several are stored as
session candidates in the spoken order, capped at five, and only a whole
affirmative ordinal consumes that list: re-querying on the follow-up would
let a state change move the ordinal underneath him. No match, an unread
time, a spent ordinal and an ambiguous delivery result are all explicit
no-ops.

command_prohibition.go is the first mutation boundary in a turn. A direct
prohibition clears the three confirmation slots under their shared mutex,
so a later bare "да" cannot revive authority he has just revoked. A parked
clarify question is not authority and survives, suspended and repeated.
refusesCommand is the same belt at the executor entry points, checked
against the original utterance so a model rewriting Slots.Text cannot get
around it.

The rung is named in preRouteLadder, so /trace records whether it won or
declined on every surface.

--no-verify: master is the working branch this session by the owner's call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 17:19:25 +04:00
parent 0b057df2a3
commit 85a3397bf4
26 changed files with 1746 additions and 37 deletions
+21 -3
View File
@@ -261,8 +261,8 @@ const (
// runTurn — the reactive turn pipeline shared by the voice and text entry
// points: expired-clarify notice → confirm answer → explicit correction →
// clarify answer → quiet toggle → route → dialogue merge → clarify question →
// action → replier.
// clarify answer → quiet toggle → reminder cancellation → route → dialogue
// merge → clarify question → action → replier.
// Takes the already-transcribed utterance, returns the reply text; the voice
// path wraps it in stt/tts, the text path returns it as-is.
//
@@ -335,6 +335,16 @@ func (h *reactiveHandler) runTurn(ctx context.Context, text string, src turnSour
return withNotice(expiredNotice, reply)
}
// 3c. explicit command prohibition — negative authority must be settled
// before a parked slot or candidate can consume these words. In particular,
// "не отменяй напоминание" is not the subject/time answer to an older
// reminder request. Confirmation stays above it: "don't" is already a
// closed no-answer to a destructive confirm, and that narrower stateful
// contract must retain first refusal.
if reply, handled := h.resolveCommandProhibition(ctx, text); notePreRoute(ctx, "command-prohibition", handled) {
return withNotice(expiredNotice, reply)
}
// 4. clarify answer — if she asked a live question last turn, this
// utterance is its answer, not a fresh command. After the confirm check: a
// y/n gate is armed by her own prompt and is the narrower claim on the
@@ -372,7 +382,15 @@ func (h *reactiveHandler) runTurn(ctx context.Context, text string, src turnSour
return withNotice(expiredNotice, reply)
}
// 5d. ordinal selection — "второй", "первую сделал" pick from the list she
// 5d. committed-reminder cancellation — an explicit cancel verb plus the
// reminder noun resolves against pending rows. It runs before ordinal so a
// clock such as "на девять" cannot be mistaken for a position in an older
// task list; an ambiguous result binds its own list for the next turn.
if reply, handled := h.resolveReminderCancellation(ctx, text); notePreRoute(ctx, "reminder-cancel", handled) {
return withNotice(expiredNotice, reply)
}
// 5e. ordinal selection — "второй", "первую сделал" pick from the list she
// just read (ordinal.go). Before routing, and only when a list is actually
// bound to the session: with nothing offered, "второй" is an ordinary word
// and keeps routing.