The 4/29 dentist-appointment investigation surfaced two distinct root causes, each fixed structurally — and an unrelated UX refactor we held off shipping in v26.04.29.2.
Event store: data integrity at every boundary
4c58603 — Replace end_dt column with duration_minutes (Fable #160). Schema migration 0043. End-before-start is now structurally inexpressible (CHECK constraint duration_minutes >= 0). The corrupt prod row that started this whole investigation collapses cleanly to a point event during the migration backfill. Wire format unchanged — to_dict() emits end_dt as derived from start_dt + duration_minutes. Behavioral upgrade: moving an event's start now slides the effective end forward (preserving duration), instead of corrupting or hard-rejecting.
84640a0 — update_event / delete_event refuse ambiguous queries (Fable #161). When query matches multiple events, the tool returns success: false with a candidates array of {id, title, start_dt, location}. The model picks one by passing event_id on the next call. This is the actual root cause of how event id=2 got mutated in the first place: pre-fix, both tools silently took matches[0]. For delete in particular, the tool description leans into "never guess between candidates."
The two are complementary: #160 makes invalid data inexpressible at storage; #161 makes the wrong event unmutatable at the tool layer.
Calendar UX
2db23ce — EventSlideOver redesigned as a centered modal. No Save / Cancel buttons; auto-save on close (X / Esc / backdrop click / Enter in form field). Trash icon in the header (edit mode only) opens an inline two-step confirm. Validity-aware: invalid form in edit mode → toast naming the missing field + discard; in create mode → silent discard. The right-edge slide-over was hiding the destructive ghost-style Delete button against a thin floor band; the centered modal eliminates the contrast pocket entirely.
Reverted in-flight
b7e7073 reverts 94b169f — the A+B hotfix (filter robustness + write-side end-vs-start check). Superseded by #160 which makes the same scenarios structurally impossible.
Test plan
46 calendar/events tests pass; ruff clean across all touched files
After deploy: verify event id=2 in prod surfaces correctly in the upcoming list (it should, with duration_minutes=NULL from the migration backfill)
Smoke test the disambiguation flow: create two events with the same title, ask the assistant to update "the appointment", confirm the candidates list comes back and the model picks correctly via event_id
Verify EventSlideOver modal renders centered with auto-save behavior on next deploy
Note on CI
The dev-branch CI on 84640a0 failed only on the Build & push image step (registry returned a generic unknown error after a successful build). Same transient infra flake we hit on 35fab6c earlier today; all code gates (typecheck, lint, tests, build) passed. The release-tag push will run a fresh build.
## Highlights
The 4/29 dentist-appointment investigation surfaced two distinct root causes, each fixed structurally — and an unrelated UX refactor we held off shipping in v26.04.29.2.
### Event store: data integrity at every boundary
- **`4c58603` — Replace `end_dt` column with `duration_minutes` (Fable #160).** Schema migration 0043. End-before-start is now structurally inexpressible (CHECK constraint `duration_minutes >= 0`). The corrupt prod row that started this whole investigation collapses cleanly to a point event during the migration backfill. Wire format unchanged — `to_dict()` emits `end_dt` as derived from `start_dt + duration_minutes`. Behavioral upgrade: moving an event's start now slides the effective end forward (preserving duration), instead of corrupting or hard-rejecting.
- **`84640a0` — `update_event` / `delete_event` refuse ambiguous queries (Fable #161).** When `query` matches multiple events, the tool returns `success: false` with a `candidates` array of `{id, title, start_dt, location}`. The model picks one by passing `event_id` on the next call. This is the actual root cause of how event id=2 got mutated in the first place: pre-fix, both tools silently took `matches[0]`. For delete in particular, the tool description leans into "never guess between candidates."
The two are complementary: #160 makes invalid data inexpressible at storage; #161 makes the wrong event unmutatable at the tool layer.
### Calendar UX
- **`2db23ce` — `EventSlideOver` redesigned as a centered modal.** No Save / Cancel buttons; auto-save on close (X / Esc / backdrop click / Enter in form field). Trash icon in the header (edit mode only) opens an inline two-step confirm. Validity-aware: invalid form in edit mode → toast naming the missing field + discard; in create mode → silent discard. The right-edge slide-over was hiding the destructive ghost-style Delete button against a thin floor band; the centered modal eliminates the contrast pocket entirely.
### Reverted in-flight
- **`b7e7073`** reverts **`94b169f`** — the A+B hotfix (filter robustness + write-side end-vs-start check). Superseded by #160 which makes the same scenarios structurally impossible.
## Test plan
- [x] 46 calendar/events tests pass; ruff clean across all touched files
- [x] Migration backfill rules covered: `end_dt > start` → minute count; `end_dt <= start` OR null → NULL
- [ ] After deploy: verify event id=2 in prod surfaces correctly in the upcoming list (it should, with `duration_minutes=NULL` from the migration backfill)
- [ ] Smoke test the disambiguation flow: create two events with the same title, ask the assistant to update "the appointment", confirm the candidates list comes back and the model picks correctly via `event_id`
- [ ] Verify `EventSlideOver` modal renders centered with auto-save behavior on next deploy
## Note on CI
The dev-branch CI on `84640a0` failed only on the `Build & push image` step (registry returned a generic `unknown` error after a successful build). Same transient infra flake we hit on `35fab6c` earlier today; all code gates (typecheck, lint, tests, build) passed. The release-tag push will run a fresh build.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Highlights
The 4/29 dentist-appointment investigation surfaced two distinct root causes, each fixed structurally — and an unrelated UX refactor we held off shipping in v26.04.29.2.
Event store: data integrity at every boundary
4c58603— Replaceend_dtcolumn withduration_minutes(Fable #160). Schema migration 0043. End-before-start is now structurally inexpressible (CHECK constraintduration_minutes >= 0). The corrupt prod row that started this whole investigation collapses cleanly to a point event during the migration backfill. Wire format unchanged —to_dict()emitsend_dtas derived fromstart_dt + duration_minutes. Behavioral upgrade: moving an event's start now slides the effective end forward (preserving duration), instead of corrupting or hard-rejecting.84640a0—update_event/delete_eventrefuse ambiguous queries (Fable #161). Whenquerymatches multiple events, the tool returnssuccess: falsewith acandidatesarray of{id, title, start_dt, location}. The model picks one by passingevent_idon the next call. This is the actual root cause of how event id=2 got mutated in the first place: pre-fix, both tools silently tookmatches[0]. For delete in particular, the tool description leans into "never guess between candidates."The two are complementary: #160 makes invalid data inexpressible at storage; #161 makes the wrong event unmutatable at the tool layer.
Calendar UX
2db23ce—EventSlideOverredesigned as a centered modal. No Save / Cancel buttons; auto-save on close (X / Esc / backdrop click / Enter in form field). Trash icon in the header (edit mode only) opens an inline two-step confirm. Validity-aware: invalid form in edit mode → toast naming the missing field + discard; in create mode → silent discard. The right-edge slide-over was hiding the destructive ghost-style Delete button against a thin floor band; the centered modal eliminates the contrast pocket entirely.Reverted in-flight
b7e7073reverts94b169f— the A+B hotfix (filter robustness + write-side end-vs-start check). Superseded by #160 which makes the same scenarios structurally impossible.Test plan
end_dt > start→ minute count;end_dt <= startOR null → NULLduration_minutes=NULLfrom the migration backfill)event_idEventSlideOvermodal renders centered with auto-save behavior on next deployNote on CI
The dev-branch CI on
84640a0failed only on theBuild & push imagestep (registry returned a genericunknownerror after a successful build). Same transient infra flake we hit on35fab6cearlier today; all code gates (typecheck, lint, tests, build) passed. The release-tag push will run a fresh build.🤖 Generated with Claude Code