dialogue traces cover the owner's four reminder cases (V-579)
His two asks and his two commits, plus the check that no trace anywhere ends with a reminder firing at the current clock. The transcript row from V-561 keeps his verbatim words and loses its skip: "на 9" is read now, and under the commit rule it is a question rather than a reminder. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -114,8 +114,11 @@ type turn struct {
|
|||||||
wait time.Duration
|
wait time.Duration
|
||||||
// question — the reply must be exactly this clarify question, worded for
|
// question — the reply must be exactly this clarify question, worded for
|
||||||
// this attempt. Zero slot ⇒ not checked.
|
// this attempt. Zero slot ⇒ not checked.
|
||||||
question dialogue.Slot
|
question dialogue.Slot
|
||||||
attempt int
|
attempt int
|
||||||
|
// gap — which part of the time she is asking about, for a SlotTime question
|
||||||
|
// (V-579). Zero value is the missing hour, which is what she asks first.
|
||||||
|
gap whenGap
|
||||||
contains []string
|
contains []string
|
||||||
notContain []string
|
notContain []string
|
||||||
// noQuestion — the reply must not be any clarify question. Used where the
|
// noQuestion — the reply must not be any clarify question. Used where the
|
||||||
@@ -157,11 +160,29 @@ type trace struct {
|
|||||||
func newDialogueHandler(t *testing.T) (*reactiveHandler, *store.Store, *time.Time) {
|
func newDialogueHandler(t *testing.T) (*reactiveHandler, *store.Store, *time.Time) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
h, st, now := newClarifyHandler(t)
|
h, st, now := newClarifyHandler(t)
|
||||||
|
// A minute no trace ever says, so "fires at the current clock" is a defect
|
||||||
|
// and never a coincidence (V-577, V-579). checkEnd refuses any reminder
|
||||||
|
// landing on it, and at 09:00 the row that answers "на 9" would trip that.
|
||||||
|
*now = time.Date(2026, 7, 31, 9, 17, 0, 0, time.UTC)
|
||||||
h.router = buildRouter(router.NewHashEmbedder(1024), h.matcher, 0.55, nil)
|
h.router = buildRouter(router.NewHashEmbedder(1024), h.matcher, 0.55, nil)
|
||||||
h.recall = recallWiring{embedder: router.NewHashEmbedder(1024), memStore: memory.NewInMemoryStore()}
|
h.recall = recallWiring{embedder: router.NewHashEmbedder(1024), memStore: memory.NewInMemoryStore()}
|
||||||
return h, st, now
|
return h, st, now
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wantedQuestion builds the question a turn must be answered with, from the
|
||||||
|
// same code the daemon asks through. A time question is built from the gap,
|
||||||
|
// because she names the clock and asks about the part he left out (V-579).
|
||||||
|
func wantedQuestion(tn turn, now time.Time) (string, bool) {
|
||||||
|
if tn.question == dialogue.SlotTime {
|
||||||
|
gap := tn.gap
|
||||||
|
if gap == whenComplete {
|
||||||
|
gap = whenNoHour
|
||||||
|
}
|
||||||
|
return whenQuestion(gap, tn.attempt, now)
|
||||||
|
}
|
||||||
|
return clarifyQuestionFor(tn.question, tn.attempt)
|
||||||
|
}
|
||||||
|
|
||||||
// runTrace drives one trace through handleText and checks every turn, then the
|
// runTrace drives one trace through handleText and checks every turn, then the
|
||||||
// end state. Every failure carries the decision trace so far, so a wrong
|
// end state. Every failure carries the decision trace so far, so a wrong
|
||||||
// claimant reads differently from wrong copy.
|
// claimant reads differently from wrong copy.
|
||||||
@@ -217,7 +238,7 @@ func runTrace(t *testing.T, tr trace) {
|
|||||||
fail(i, "reply %q announced an expiry nothing asked for", reply)
|
fail(i, "reply %q announced an expiry nothing asked for", reply)
|
||||||
}
|
}
|
||||||
if tn.question != "" {
|
if tn.question != "" {
|
||||||
want, ok := clarifyQuestionFor(tn.question, tn.attempt)
|
want, ok := wantedQuestion(tn, h.now())
|
||||||
if !ok {
|
if !ok {
|
||||||
fail(i, "no question exists for slot %s attempt %d", tn.question, tn.attempt)
|
fail(i, "no question exists for slot %s attempt %d", tn.question, tn.attempt)
|
||||||
}
|
}
|
||||||
@@ -248,12 +269,16 @@ func runTrace(t *testing.T, tr trace) {
|
|||||||
func isAnyClarifyQuestion(reply string) bool {
|
func isAnyClarifyQuestion(reply string) bool {
|
||||||
for _, variants := range clarifyQuestionVariants {
|
for _, variants := range clarifyQuestionVariants {
|
||||||
for _, v := range variants {
|
for _, v := range variants {
|
||||||
if reply == v {
|
// HasSuffix, not equality: a question about the time opens with the
|
||||||
|
// clock she is reasoning from (V-579).
|
||||||
|
if strings.HasSuffix(reply, v) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
// The two questions with no deck behind them, asked when the hour is said
|
||||||
|
// and its half of the day or its day is not.
|
||||||
|
return strings.HasSuffix(reply, "утра или вечера?") || strings.HasSuffix(reply, "В какой день?")
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkParked(t *testing.T, fail func(int, string, ...any), i int, got *dialogue.PendingQuestion, want *parkedWant) {
|
func checkParked(t *testing.T, fail func(int, string, ...any), i int, got *dialogue.PendingQuestion, want *parkedWant) {
|
||||||
@@ -294,6 +319,17 @@ func checkEnd(t *testing.T, ctx context.Context, st *store.Store, h *reactiveHan
|
|||||||
if len(reminders) != len(want.reminders) {
|
if len(reminders) != len(want.reminders) {
|
||||||
t.Fatalf("end state: %d reminder(s), want %d: %+v%s", len(reminders), len(want.reminders), reminders, trace)
|
t.Fatalf("end state: %d reminder(s), want %d: %+v%s", len(reminders), len(want.reminders), reminders, trace)
|
||||||
}
|
}
|
||||||
|
// No trace may leave a reminder at the current clock, whatever else it
|
||||||
|
// asserts (V-577, V-579). Twice on the box a sentence naming a day and no
|
||||||
|
// hour was completed from time.Now(): "что у меня сегодня?" became 01:28 and
|
||||||
|
// "на завтра" became 01:38. Neither minute was ever spoken, and a row that
|
||||||
|
// only checked the payload would have passed both.
|
||||||
|
for _, r := range reminders {
|
||||||
|
if r.FireTs.In(h.now().Location()).Format("15:04") == h.now().Format("15:04") {
|
||||||
|
t.Fatalf("end state: reminder %q fires at %s, which is the clock — a time slot naming no hour is asked about, never filled from now()%s",
|
||||||
|
r.Payload, r.FireTs.Format("15:04"), trace)
|
||||||
|
}
|
||||||
|
}
|
||||||
for i, w := range want.reminders {
|
for i, w := range want.reminders {
|
||||||
if !strings.Contains(reminders[i].Payload, w.payload) {
|
if !strings.Contains(reminders[i].Payload, w.payload) {
|
||||||
t.Fatalf("end state: reminder %d payload %q does not carry %q%s", i, reminders[i].Payload, w.payload, trace)
|
t.Fatalf("end state: reminder %d payload %q does not carry %q%s", i, reminders[i].Payload, w.payload, trace)
|
||||||
@@ -355,11 +391,18 @@ func dialogueTraces() []trace {
|
|||||||
// from: she asks for the time, he gives it, the reminder lands with the
|
// from: she asks for the time, he gives it, the reminder lands with the
|
||||||
// subject he said in the FIRST turn.
|
// subject he said in the FIRST turn.
|
||||||
{
|
{
|
||||||
name: "reminder completed over two turns",
|
// Three turns since V-579, not two. An hour with no day named is
|
||||||
|
// not an answer she can act on: 11:00 today has passed as often as
|
||||||
|
// not, and picking one for him is the invention the whole rule is
|
||||||
|
// against. So she says the clock she is reasoning from and asks
|
||||||
|
// which day.
|
||||||
|
name: "reminder completed over three turns",
|
||||||
turns: []turn{
|
turns: []turn{
|
||||||
{say: "напомни позвонить маме", question: dialogue.SlotTime, attempt: 1,
|
{say: "напомни позвонить маме", question: dialogue.SlotTime, attempt: 1,
|
||||||
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 1, carries: "маме"}},
|
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 1, carries: "маме"}},
|
||||||
{say: "в 11:00", contains: []string{"11:00"}, notContain: []string{"?"}},
|
{say: "в 11:00", question: dialogue.SlotTime, attempt: 2, gap: whenNoDay,
|
||||||
|
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 2, carries: "маме"}},
|
||||||
|
{say: "сегодня", contains: []string{"11:00"}, notContain: []string{"?"}},
|
||||||
},
|
},
|
||||||
end: endState{reminders: []reminderWant{{payload: "позвонить маме", fireAt: "2026-07-31 11:00"}}},
|
end: endState{reminders: []reminderWant{{payload: "позвонить маме", fireAt: "2026-07-31 11:00"}}},
|
||||||
},
|
},
|
||||||
@@ -438,8 +481,147 @@ func dialogueTraces() []trace {
|
|||||||
end: endState{tasks: []string{"купить молоко"}},
|
end: endState{tasks: []string{"купить молоко"}},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// V-577 shape 1, the worst of the nine claimants measured on 2026-08-06.
|
||||||
|
// Every token of "что у меня сегодня?" is frame — an interrogative, a
|
||||||
|
// preposition, a particle and a day word — so the role classifier never
|
||||||
|
// looked at the route, the parked reminder read "сегодня" as its time,
|
||||||
|
// and the hour came from the clock. He got a reminder he never asked for
|
||||||
|
// at a minute he never said, and his question was answered nowhere.
|
||||||
|
//
|
||||||
|
// Two claims: the calendar answers, and nothing is written. The flow
|
||||||
|
// survives underneath, because a question of his own is not a request to
|
||||||
|
// abandon the one he was making.
|
||||||
|
{
|
||||||
|
name: "an agenda question mid-flow is answered, not eaten",
|
||||||
|
turns: []turn{
|
||||||
|
{say: "напомни забрать посылку", question: dialogue.SlotTime, attempt: 1,
|
||||||
|
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 1, carries: "посылку"}},
|
||||||
|
{say: "что у меня сегодня?", contains: []string{"31.07.2026"},
|
||||||
|
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 1, carries: "посылку"}},
|
||||||
|
},
|
||||||
|
end: endState{},
|
||||||
|
},
|
||||||
|
// V-577 shape 2. He states something in the middle of the flow. It is
|
||||||
|
// neither a slot value nor a cancel, and it was scored as a failed
|
||||||
|
// answer and dropped in silence: alone the same sentence is stored.
|
||||||
|
// Silence is the one option that is wrong, so it is stored, no retry is
|
||||||
|
// spent, and the question comes back on the end of the same reply.
|
||||||
|
//
|
||||||
|
// The words are a fact and not the owner's note, because the fact parser
|
||||||
|
// is deterministic and the offline floor marks every classifier route
|
||||||
|
// Clarify. The row below carries his own sentence and needs the model.
|
||||||
|
//
|
||||||
|
// What this floor can prove is the arbitration: no retry is spent, the
|
||||||
|
// flow survives on the same attempt, and the words are answered as
|
||||||
|
// themselves with the question coming back after them. Whether the fact
|
||||||
|
// is then WRITTEN is the routing engine's business — the hash embedder
|
||||||
|
// is unsure of every sentence it sees, and an unsure fact has never been
|
||||||
|
// stored.
|
||||||
|
{
|
||||||
|
name: "a fact stated mid-flow steps aside without spending a retry",
|
||||||
|
turns: []turn{
|
||||||
|
{say: "напомни позвонить врачу", question: dialogue.SlotTime, attempt: 1,
|
||||||
|
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 1, carries: "врачу"}},
|
||||||
|
{say: "я выпил воды", contains: []string{"напоминание?"},
|
||||||
|
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 1, carries: "врачу"}},
|
||||||
|
},
|
||||||
|
end: endState{},
|
||||||
|
},
|
||||||
|
// V-579 turn 3: the preposition decided whether the hour was read. "в 9"
|
||||||
|
// set the reminder and "на 9" was not read at all, on the same build and
|
||||||
|
// with the same cardinal.
|
||||||
|
{
|
||||||
|
// It is read, and being read is not the same as being enough: nine is
|
||||||
|
// either half of the day, so she asks which and then which day
|
||||||
|
// (V-579). Both answers are frame words and neither carries an hour
|
||||||
|
// of its own, so this row is also the proof that an answer is read
|
||||||
|
// against the whole request rather than alone.
|
||||||
|
name: "на 9 answers the time question like в 9",
|
||||||
|
turns: []turn{
|
||||||
|
{say: "напомни позвонить маме", question: dialogue.SlotTime, attempt: 1,
|
||||||
|
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 1}},
|
||||||
|
{say: "на 9", question: dialogue.SlotTime, attempt: 2, gap: whenAmbiguousHour,
|
||||||
|
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 2}},
|
||||||
|
{say: "утра", question: dialogue.SlotTime, attempt: 3, gap: whenNoDay,
|
||||||
|
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 3}},
|
||||||
|
{say: "завтра", contains: []string{"09:00"}},
|
||||||
|
},
|
||||||
|
end: endState{reminders: []reminderWant{{payload: "позвонить маме", fireAt: "2026-08-01 09:00"}}},
|
||||||
|
},
|
||||||
|
// The owner's own four, ruled 2026-08-06 (V-579). A reminder commits
|
||||||
|
// when what, what time and what day are all answered, and every ask
|
||||||
|
// states the clock she is reasoning from.
|
||||||
|
{
|
||||||
|
name: "his first example: a bare 3 is asked about",
|
||||||
|
turns: []turn{
|
||||||
|
{say: "напомни завтра в 3 заказать цветы",
|
||||||
|
question: dialogue.SlotTime, attempt: 1, gap: whenAmbiguousHour,
|
||||||
|
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 1, carries: "цветы"}},
|
||||||
|
},
|
||||||
|
end: endState{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// The hour is unambiguous and the day is still missing, so she asks.
|
||||||
|
// Today being a valid reading is not the same as him saying it.
|
||||||
|
name: "his second example: nine in the evening of which day",
|
||||||
|
turns: []turn{
|
||||||
|
{say: "напомни в 9 вечера разгрузить стиралку",
|
||||||
|
question: dialogue.SlotTime, attempt: 1, gap: whenNoDay,
|
||||||
|
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 1, carries: "стиралку"}},
|
||||||
|
{say: "завтра", contains: []string{"21:00"}},
|
||||||
|
},
|
||||||
|
end: endState{reminders: []reminderWant{{payload: "стиралку", fireAt: "2026-08-01 21:00"}}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// All three answered in one breath, so she does not ask at all.
|
||||||
|
name: "his third example: a full time commits",
|
||||||
|
turns: []turn{
|
||||||
|
{say: "напомни завтра в 15:00 заказать цветы", notContain: []string{"?"}},
|
||||||
|
},
|
||||||
|
end: endState{reminders: []reminderWant{{payload: "цветы", fireAt: "2026-08-01 15:00"}}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// An interval is one instant, so it answers the hour and the day
|
||||||
|
// together. Confirmed by the owner: "через час is fine as is".
|
||||||
|
name: "an interval commits without a question",
|
||||||
|
turns: []turn{
|
||||||
|
{say: "напомни через час позвонить маме", notContain: []string{"?"}},
|
||||||
|
},
|
||||||
|
end: endState{reminders: []reminderWant{{payload: "маме", fireAt: "2026-07-31 10:17"}}},
|
||||||
|
},
|
||||||
|
// V-579 turn 4: he named a day and no hour, and got the day at the
|
||||||
|
// current minute. She has to ask instead, and the global check in
|
||||||
|
// checkEnd refuses the invented minute for every row at once.
|
||||||
|
{
|
||||||
|
name: "a day with no hour is asked about, not taken from the clock",
|
||||||
|
turns: []turn{
|
||||||
|
{say: "напомни позвонить маме", question: dialogue.SlotTime, attempt: 1,
|
||||||
|
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 1}},
|
||||||
|
{say: "на завтра", question: dialogue.SlotTime, attempt: 2,
|
||||||
|
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 2}},
|
||||||
|
},
|
||||||
|
end: endState{},
|
||||||
|
},
|
||||||
|
|
||||||
// ---- rows below carry the CORRECT expectation and fail today ----
|
// ---- rows below carry the CORRECT expectation and fail today ----
|
||||||
|
|
||||||
|
// The owner's own sentence from V-577 shape 2, in his words. It needs
|
||||||
|
// an engine that can route it: the hash embedder marks it note with
|
||||||
|
// Clarify set, and a route she is not sure of is not evidence that he
|
||||||
|
// stated anything. The row above is the same contract in words the
|
||||||
|
// floor's deterministic fact parser reads.
|
||||||
|
{
|
||||||
|
name: "a note stated mid-flow is stored, not dropped",
|
||||||
|
skip: "the offline floor cannot route «у меня новый ноутбук» confidently; needs the resident model",
|
||||||
|
turns: []turn{
|
||||||
|
{say: "напомни позвонить врачу", question: dialogue.SlotTime, attempt: 1,
|
||||||
|
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 1, carries: "врачу"}},
|
||||||
|
{say: "у меня новый ноутбук",
|
||||||
|
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 1, carries: "врачу"}},
|
||||||
|
},
|
||||||
|
end: endState{notes: 1},
|
||||||
|
},
|
||||||
|
|
||||||
// The owner's target transcript, V-561. He asks for a reminder, she asks
|
// The owner's target transcript, V-561. He asks for a reminder, she asks
|
||||||
// when, he asks something else entirely, and then comes back to her
|
// when, he asks something else entirely, and then comes back to her
|
||||||
// question. On the box this created a reminder at 00:12 and never
|
// question. On the box this created a reminder at 00:12 and never
|
||||||
@@ -452,26 +634,35 @@ func dialogueTraces() []trace {
|
|||||||
// still standing, on the same attempt — a side query is not a failed
|
// still standing, on the same attempt — a side query is not a failed
|
||||||
// answer and must not spend a retry.
|
// answer and must not spend a retry.
|
||||||
//
|
//
|
||||||
// Unskipping this needs more than V-561, and V-561 landing did not change
|
// The skip came off with V-579. What held it was the parser, not the
|
||||||
// that. The suspend and resume it asked for is done — the row below is
|
// arbitration: neither the stub nor the production one read "на 9",
|
||||||
// the same shape in words the floor can parse and is green. What is left
|
// because only "в" framed a spoken hour, and "на завтра" was completed
|
||||||
// here is the parser: StubDateTimeParser does not read "на 9" or "на
|
// from the clock.
|
||||||
// завтра", so turn 3 lands as an answer that filled nothing and spends a
|
//
|
||||||
// retry, which is what this row now fails on. V-562 and V-543 own the
|
// Turn 3 now closes the flow, where the transcript has one more exchange
|
||||||
// ambiguous hour and the day correction behind those two words.
|
// in it. That is the 12-hour question — the owner's turn 4 answers "на
|
||||||
|
// 9" with "сейчас 15:23, на 9 сегодня вечером?" — and it is a decision of
|
||||||
|
// its own, not one to invent here. Nine o'clock is read as nine and, at
|
||||||
|
// 09:17, as tomorrow's, which is where the transcript ends up anyway.
|
||||||
|
// Turn 4 then has nothing to answer and must not write anything.
|
||||||
{
|
{
|
||||||
name: "the owner's transcript from V-561",
|
name: "the owner's transcript from V-561",
|
||||||
skip: "V-543/V-562: the floor's date parser reads neither «на 9» nor «на завтра»",
|
|
||||||
turns: []turn{
|
turns: []turn{
|
||||||
{say: "напомни позвонить маме", question: dialogue.SlotTime, attempt: 1,
|
{say: "напомни позвонить маме", question: dialogue.SlotTime, attempt: 1,
|
||||||
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 1}},
|
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 1}},
|
||||||
{say: "какая сейчас погода в Риме?",
|
{say: "какая сейчас погода в Риме?",
|
||||||
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 1, carries: "маме"}},
|
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 1, carries: "маме"}},
|
||||||
{say: "а, да, прости - на 9.",
|
// His words, unchanged. What changed under V-579 is that "на 9"
|
||||||
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 1, carries: "маме"}},
|
// is a question and not a commit: nine could be either half of
|
||||||
{say: "на завтра."},
|
// the day, so she says the clock she is reading from and asks.
|
||||||
|
// "на завтра." then answers the day and leaves the half open, so
|
||||||
|
// she asks that one again.
|
||||||
|
{say: "а, да, прости - на 9.", question: dialogue.SlotTime, attempt: 2, gap: whenAmbiguousHour,
|
||||||
|
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 2}},
|
||||||
|
{say: "на завтра.", question: dialogue.SlotTime, attempt: 3, gap: whenAmbiguousHour,
|
||||||
|
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 3}},
|
||||||
},
|
},
|
||||||
end: endState{reminders: []reminderWant{{payload: "позвонить маме", fireAt: "2026-08-01 09:00"}}},
|
end: endState{},
|
||||||
},
|
},
|
||||||
// The same shape said in words StubDateTimeParser reads. GREEN since
|
// The same shape said in words StubDateTimeParser reads. GREEN since
|
||||||
// V-561. Same three claims: Rome is answered, the question survives the
|
// V-561. Same three claims: Rome is answered, the question survives the
|
||||||
@@ -488,7 +679,9 @@ func dialogueTraces() []trace {
|
|||||||
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 1}},
|
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 1}},
|
||||||
{say: "какая сейчас погода в Риме?",
|
{say: "какая сейчас погода в Риме?",
|
||||||
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 1, carries: "маме"}},
|
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 1, carries: "маме"}},
|
||||||
{say: "в 11:00", contains: []string{"11:00"}},
|
{say: "в 11:00", question: dialogue.SlotTime, attempt: 2, gap: whenNoDay,
|
||||||
|
parked: &parkedWant{slot: dialogue.SlotTime, attempt: 2, carries: "маме"}},
|
||||||
|
{say: "сегодня", contains: []string{"11:00"}},
|
||||||
},
|
},
|
||||||
end: endState{reminders: []reminderWant{{payload: "позвонить маме", fireAt: "2026-07-31 11:00"}}},
|
end: endState{reminders: []reminderWant{{payload: "позвонить маме", fireAt: "2026-07-31 11:00"}}},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user