feat(meetings): poll the calendar and pre-create the meeting about to start - #8080
Conversation
… start The calendar was pull-only: the cache changed when the user pressed Sync, and a meeting directory came into being the first time the user opened its row. A meetings assistant should already know what is about to start. One background asyncio task per gateway process (started with the app's other on_startup hooks, stopped before session teardown) now, on each tick: skips silently when the app is disabled, auto_sync is off, or the provider is `none`; otherwise runs the same sync as POST /calendar/sync (factored into calendar_sync.py so the two cannot drift), then creates the meeting directory for every timed event starting within `calendar.precreate_lead_minutes` (or already under way) through the dashboard's own idempotent init. Pre-creation never starts a session or spawns an agent; the meeting stays `idle`. All-day events are skipped, existing meetings are never touched, and one bad tick never ends the loop. Every store call in a tick runs off the event loop. Three bounded `calendar.*` config keys drive it (auto_sync, poll_interval_secs, precreate_lead_minutes), filled into older configs by read_config and validated by PUT /config. The dashboard list re-reads the cache and the meetings every minute so a pre-created meeting appears without a remount.
UX Review (Fable 5, fork) — 🟡 CONCERNSUX-level review of UX-Verdict: CONCERNS Deleting a pre-created meeting doesn't stick: the poller resurrects it on the next tick, silently overriding a confirmed "permanently remove." Watch
Suggestions
[UX-REVIEWED] 23ee523 |
Design Review (Fable 5, fork) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS Pre-creation materializes every calendar event to disk but nothing ever unmaterializes one — accumulation has no lifecycle story. Watch
Suggestions
[DESIGN-REVIEWED] 23ee523 |
First Principles Review (Fable 5, fork) — 🟡 CONCERNSPremise-level review of All verification done. The issue-radar precedent is real ( First-Principles-Verdict: CONCERNS Solid addition with a real harm and honest framing, but two knobs and one helper ship ahead of any consumer. What this change shipsIntent: have the calendar stay current and the imminent meeting already exist when the user looks — an ADDITION.
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] 23ee523 |
Opus 4.8 Review (fork) — ✅ no blocking findingsReviewed Review detailsBoth candidates describe behavior that the code plainly does, but neither clears the validation bar. CANDIDATE 1 (a user-deleted, still-imminent pre-created meeting reappears within a poll interval) is a re-derivation of intended behavior — the module exists specifically to keep the imminent meeting pre-created as No new grounded defect surfaced under falsification of the poller, the shared sync, the settings clamps, or the config merge. No findings. [OPUS-REVIEWED] 23ee523 |
GPT 5.6 Review (fork) — ✅ no blocking findingsReviewed Review detailsFINDING -- src/kiro_crew/apps/builtins/meetings/backend/routes/init.py:269 -- |
bolichen97
left a comment
There was a problem hiding this comment.
Approving on the strength of a full readiness audit of every open PR against main, not a
line-by-line reading of this diff — recording that plainly so the next reader knows what this
stamp does and does not cover.
Verified against this exact head SHA:
readiness: passedpresent, andPR Readiness— the one required status context onmain
(rulesetprotected-branches) — issuccesson this head.- No check run on this head is
failure,cancelled,timed_outor still in flight. Skipped
jobs are path-filtered conditionals, none of them required. mergeable: true, and the head is not far enough behindmainfor its green CI to describe a
base that no longer exists.- No surviving reviewer
CHANGES_REQUESTED: any such review is on an older commit and therefore
already dismissed bydismiss_stale_reviews_on_push. - Every issue comment, inline review comment and review thread was read and classified. Nothing
left is an unresolved human change request — the remainder is bot review-lane output, resolved
or outdated threads, explicitly non-blocking suggestions, and author status notes.
Auto-merge (squash) is armed, so this lands once every other ruleset requirement is met.
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
Problem / Motivation
The calendar is pull-only. The cache changes only when the user presses Sync, and a
meeting directory comes into being the first time the user opens its row. So the
app never knows what is about to start unless a person tells it, and a user who
opens the dashboard a minute before a meeting sees a stale list and an empty
meeting.
Why it matters
"Being ready for the meeting that is about to start" is the whole reason to
connect a calendar. Without a background sync the calendar integration is a
manual import button; without pre-creation the user still has to notice the
meeting and open it before anything exists on disk. Everything downstream — the
seeded agent outputs, the tasks file, a note the user wants to type before the
call — waits on that click.
What changed (motivation → approach → change)
Goal. Keep the cache current and have the imminent meeting's directory exist
before the user looks for it, without changing what a meeting is or how it
starts.
Approach. One background asyncio task per gateway process, in the shape the
repo already uses for issue-radar's watcher (module-level task, idempotent
start, cancellingstop, a constant cadence, one loop that survives a badtick). Polling rather than provider push, because the gateway is normally
reachable only on loopback and both Google's and Microsoft's push APIs need an
internet-reachable HTTPS endpoint.
Change.
backend/calendar_sync.py— the fetch-and-cache half ofPOST /calendar/sync,factored out so the route and the poller are the same sync (same provider
resolution, same cache write, same
meetings.calendar_syncaudit record).calendar_settings()normalizes thecalendarconfig block with typed,clamped defaults.
backend/calendar_poller.py— the loop. Each tick: skip silently when the appis disabled,
calendar.auto_syncis off, or the provider isnone; otherwisesync, then for every timed event that starts within
calendar.precreate_lead_minutes(or has started and not ended) create themeeting through the dashboard's own idempotent init, on a worker thread under
START_LOCK, stampingcreated_by: "calendar"into itssession.json(auser-initiated meeting carries no
created_by, so a cleanup of never-attendedauto-created meetings never has to guess), and auditing
meetings.calendar_precreateper meeting actually created. Pre-creation neverstarts a session, never spawns an agent, never opens a microphone: the meeting
stays
idle. All-day events are skipped (a date anchor is not an instant ameeting begins at), an existing meeting is never touched, a cache row whose id
fails
safe_meeting_idis skipped as corruption. Every store call runs off theevent loop. The cadence is the constant
CALENDAR_POLL_INTERVAL_SECS(fiveminutes). A provider failure logs at INFO and keeps the schedule; any other
exception logs at WARNING and the loop continues.
DELETE /meetings/{id}records the id incalendar-precreate-skip.json; the poller skips recorded ids and prunes thefile to the ids whose event is still due, so "permanently remove" holds for
exactly as long as the event could otherwise bring the folder back. The record
keys on the id, not on provenance, so a hand-made meeting for a calendar event
is protected the same way.
start_pollerappended toon_startup;stop_pollerappended toon_cleanupbefore the session-teardown hook, so no tick can pre-create ameeting mid-shutdown.
calendar.*keys —auto_sync(bool, default on) andprecreate_lead_minutes(0..1440, default 15; 0 keeps the sync and stopspre-creation) — validated by
PUT /configand filled into oldercalendarblocks by
read_config. Each names a harm a user may want to opt out of(unattended network fetches; folders appearing on disk).
routes/meeting_lifecycle.pyexposes the existing blocking init under a publicname; the init handler is unchanged, the delete handler gains the record.
MeetingsPagere-reads the calendar cache and the meetings listevery minute so a pre-created meeting appears while the page sits open.
docs/system-specs/modules/meetings.md(layout, data, lifecycle, a new"Background sync and pre-creation" section, security posture, tests) and the
bundled
meetingsskill.Not in this PR: a Settings UI for the two knobs (they have working defaults; the
calendar-card UI PR #8081 is a better home), a reaper for never-attended
auto-created meetings (the
created_bystamp is what makes one trivial later),a last-sync indicator in the UI, and any change to how a meeting starts.
Tests
test/test_meetings_calendar_poller.py(39 tests):TestDueEvents— the pure selection rule: inside/outside/edge of the leadwindow, already under way, ended, endless-and-stale, all-day never, unreadable
rows skipped, lead 0.
TestPollOnce— a tick against a real.ics: cache written, the imminentmeeting created as
idlewith a tasks file and thecreated_bystamp, the farone not; a user-opened meeting carries no stamp; the pre-created id equals what
the dashboard's
initwould use (opening the row later finds the same folder);a second tick creates and audits nothing new; an existing meeting's title
survives; a deleted pre-created meeting stays deleted on the next tick, its
record is kept while the event is due and pruned once it is not, and a
hand-made meeting's deletion is recorded too; lead 0 syncs without creating;
provider
none,auto_syncoff, and a disabled app cost no fetch; a providerfailure is logged; a corrupt cache id is skipped.
TestLoopAndHooks— hook membership and teardown order, idempotent start,cancelling stop, the loop ticking, one bad tick not ending the loop.
TestSettingsAndSync—read_configfills the new keys,calendar_settingsclamps hostile values,
PUT /configround-trips and bounds them, a non-booleanauto_syncis the default, and the sync route and the poller call one sharedsync.
test_meetings_routes.py's blocking-call AST scan now also covers the two newmodules. Per-file coverage:
calendar_poller.py99%,calendar_sync.py94%.Local: 300 meetings tests, isort / flake8 / mypy / baselined black, loop-bound
locks, sync-io-in-async, testpaths, brand and docs-lint gates all pass;
tsc -b, eslint and theMeetingsPagevitest suites pass.Manual verification
N/A beyond the
.ics-driven tick tests — they exercise the real provider, thereal store, the real init transaction and the real delete route on a temp data root. OAuth providers
are not exercised here; the poller only calls the provider seam the route
already uses.
Screenshots / video
Why no screenshot: the only frontend change is a
refetchIntervalon twoexisting React Query hooks in
MeetingsPage.tsx; nothing rendered changes.Related Issues
Follows #2190 (calendar providers), which this does not depend on: the poller
works with the
icsprovider onmaintoday. #8081 adds the calendarcredentials UI and is the natural home for the two knobs' controls.
Checklist
meetings.md, themeetingsskill)