Repository navigation
Description
What
Priority P1 (data integrity). handleCreateEvent (src/services/ai/tool-handlers/events.ts) inserts whatever the model sends. Production, user 716928723: (1) 2026-09-10 21:25Z and 21:27Z events 591 and 592 'Алекс барбершоп' with the same start 2026-09-12T16:00Z — the first run replied only '[SKIP]', she re-sent, and the morning agenda and both batch reminders on 12.09 then listed the barbershop twice; (2) 2026-09-07 18:40Z she asked to move a visit to 18:00 ('Бояна завтра в 18'); the model created event 589 next to the existing 566 and itself labelled them '(старое время)' / '(новое время)', so she had to delete 566 by hand; (3) 2026-09-23 18:55Z 634 and 635 with the same title a few minutes apart. Add a deterministic guard: before inserting, look up the owner's (or group's) non-deleted events on the same local day with the same normalized title; an identical start returns not_applied 'DUPLICATE_EVENT' with the existing id; a different time on the same day returns a hint that this looks like a move (use update_event) and requires an explicit confirmed flag to create a second one.
Why (motivation)
Weaker fallback models re-run creates after lost replies and create instead of moving; code must stop the second row.
User impact
People get the same appointment twice in their calendar, agenda and reminders, or an old and a new copy of a moved appointment.
Cost of inaction
Every retry or 'move it to 18:00' request can silently double the calendar, doubling reminders and invitations.
Acceptance criteria
- Test: a second create_event with the same normalized title, start and end for the same owner within 10 minutes returns success=true with alreadyExists naming the existing event id (a retry after a lost reply must not look like a failure), writes nothing, and the events table still has one row
- Test: same title on the same local day at a different time is not inserted without the explicit confirmation flag; the result tells the model to use update_event for a move.
- Test: different titles, different days, deleted events and other owners are not blocked; group scope compares within the group only.
- The tool schema/description advertises the confirmation flag with an example that passes the real handler (prompt/tool contract fixture).
Screenshots
(none)
Deploy
Deployed: no
Links
- Relates to A direct request can end with only '[SKIP]' or '...' as the reply: the bot deletes its message and says nothing #508: A direct request can end with only '[SKIP]' or '...' as the reply: the bot deletes its message and says nothing #508
- Relates to Two quick confirmations in one chat start parallel agent runs that repeat the same create_event #929: Two quick confirmations in one chat start parallel agent runs that repeat the same create_event #929
Skipped gates
- acceptance-criteria: criterion 1 reworded: an exact repeat is an idempotent success (retry-safe, Two quick confirmations in one chat start parallel agent runs that repeat the same create_event #929), the not-applied failure applies to the same-day case in criterion 2
- acceptance-edit: unchecked criterion 1 dropped or reworded
- links: no external entity named
- screenshots: Tool-handler guard; no visual surface.
Activity
alex-mextner commentedon Nov 1, 2026
Partly addressed in branch fix/chat-serialization-20261101 (not merged, not deployed), as part of #929:
fix(ai): a message that waited in its chat's line is its own turn, no…
fix(ai): serialize agent runs per chat and make create_event idempote…
fix(ai): a message that waited in its chat's line is its own turn, no…
chore(release): bump the version to 0.3.1 for the per-chat serializat…
fix(ai): serialize agent runs per chat; idempotent same-day-safe crea…
alex-mextner commentedon Nov 4, 2026
The duplicate-create part of this ticket shipped with PR #942 (
d0bc0769d20897b5441b1e3f568611b8bdef4deb, deployed 2026-11-04 03:04 UTC, verified against the odroid receipt and the running image; details in #929).create_eventnow matches live events of the same calendar in one SQLite transaction and answersalreadyExistsor aSAME_DAY_EVENTconflict. Proven by tests (events-duplicate-create.test.ts, 34 pass, and the overlap regression); not exercised against a real user's calendar. Single-seat review via omp Opus; full-roster review owed in #961. Left open here: the receipt wording inwrite-outcomes.ts(the hand-over note in #929).chore(release): bump the version to 0.3.7 for the receipt change (Refs …
fix(ai): the write receipt reads create_event's 'already exists' resu…
alex-mextner commentedon Nov 7, 2026
Follow-up shipped: PR #974, merge commit 128846b, deployed (production image tag equals the merge SHA,
/healthok,/readyok (unverified), no level 40/50/60 records since start).The idempotent create result (
success: true,alreadyExistsnaming the existing event id) is now read by the write receipt as the same event: one created line for a creation plus its repeat, "Already in the calendar" for a repeat of an event the run did not create, never "Not created", and no turn-finish guard when nothing failed. The ledger folds by the structured event id, not by the English output text. Tests run the real handler into the real ledger in ru and en.This PR touches the receipt only, not the create guard itself, so it changes no acceptance criterion of this ticket; the ticket stays open.