Skip to content

feat(meetings): poll the calendar and pre-create the meeting about to start - #8080

Merged
bolichen97 merged 1 commit into
kirodotdev:mainfrom
kaizawa97:pr/meetings-calendar-poller
Sep 3, 2026
Merged

feat(meetings): poll the calendar and pre-create the meeting about to start#8080
bolichen97 merged 1 commit into
kirodotdev:mainfrom
kaizawa97:pr/meetings-calendar-poller

Conversation

@kaizawa97

@kaizawa97 kaizawa97 commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

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, cancelling stop, a constant cadence, one loop that survives a bad
tick). 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 of POST /calendar/sync,
    factored out so the route and the poller are the same sync (same provider
    resolution, same cache write, same meetings.calendar_sync audit record).
    calendar_settings() normalizes the calendar config block with typed,
    clamped defaults.
  • backend/calendar_poller.py — the loop. Each tick: skip silently when the app
    is disabled, calendar.auto_sync is off, or the provider is none; otherwise
    sync, then for every timed event that starts within
    calendar.precreate_lead_minutes (or has started and not ended) create the
    meeting through the dashboard's own idempotent init, on a worker thread under
    START_LOCK, stamping created_by: "calendar" into its session.json (a
    user-initiated meeting carries no created_by, so a cleanup of never-attended
    auto-created meetings never has to guess), and auditing
    meetings.calendar_precreate per meeting actually created. Pre-creation never
    starts 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 a
    meeting begins at), an existing meeting is never touched, a cache row whose id
    fails safe_meeting_id is skipped as corruption. Every store call runs off the
    event loop. The cadence is the constant CALENDAR_POLL_INTERVAL_SECS (five
    minutes). A provider failure logs at INFO and keeps the schedule; any other
    exception logs at WARNING and the loop continues.
  • A deleted meeting stays deleted. DELETE /meetings/{id} records the id in
    calendar-precreate-skip.json; the poller skips recorded ids and prunes the
    file 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.
  • Hooks: start_poller appended to on_startup; stop_poller appended to
    on_cleanup before the session-teardown hook, so no tick can pre-create a
    meeting mid-shutdown.
  • Config: two bounded calendar.* keys — auto_sync (bool, default on) and
    precreate_lead_minutes (0..1440, default 15; 0 keeps the sync and stops
    pre-creation) — validated by PUT /config and filled into older calendar
    blocks 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.py exposes the existing blocking init under a public
    name; the init handler is unchanged, the delete handler gains the record.
  • Frontend: MeetingsPage re-reads the calendar cache and the meetings list
    every minute so a pre-created meeting appears while the page sits open.
  • Docs: docs/system-specs/modules/meetings.md (layout, data, lifecycle, a new
    "Background sync and pre-creation" section, security posture, tests) and the
    bundled meetings skill.

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_by stamp 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 lead
    window, 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 imminent
    meeting created as idle with a tasks file and the created_by stamp, the far
    one not; a user-opened meeting carries no stamp; the pre-created id equals what
    the dashboard's init would 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_sync off, and a disabled app cost no fetch; a provider
    failure 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.
  • TestSettingsAndSyncread_config fills the new keys, calendar_settings
    clamps hostile values, PUT /config round-trips and bounds them, a non-boolean
    auto_sync is the default, and the sync route and the poller call one shared
    sync.

test_meetings_routes.py's blocking-call AST scan now also covers the two new
modules. Per-file coverage: calendar_poller.py 99%, calendar_sync.py 94%.
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 the MeetingsPage vitest suites pass.

Manual verification

N/A beyond the .ics-driven tick tests — they exercise the real provider, the
real 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 refetchInterval on two
existing 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 ics provider on main today. #8081 adds the calendar
credentials UI and is the natural home for the two knobs' controls.

Checklist

  • At most two commits (one), Conventional Commits title
  • Existing tests pass and new tests added for new functionality
  • Self-review completed; code follows project style guidelines
  • Documentation updated (meetings.md, the meetings skill)
  • No secrets, credentials, or internal references in the diff

… 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.
@kaizawa97
kaizawa97 requested a review from a team September 3, 2026 04:48
@kaizawa97
kaizawa97 requested a review from a team as a code owner September 3, 2026 04:48
@kaizawa97
kaizawa97 requested a review from patrigao September 3, 2026 04:48
@github-actions github-actions Bot added fork Pull request from a fork (external contributor) readiness: checking Automated validation is still running labels Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

UX Review (Fable 5, fork) — 🟡 CONCERNS

UX-level review of 23ee5239326b4d1ad3f1a43561cca00fdec256a2 via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

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

  • Delete → resurrection. _precreate_one recreates any due event whose directory is missing; the dashboard's delete confirm promises to "permanently remove" (deleteConfirm, lifecycle doc: "permanently remove an inactive meeting"). A user skipping a call deletes the auto-created row, and within one poll interval it reappears — repeatedly, until the event ends. Moderate frequency × broken-promise confusion × recurs every tick. Fix: skip pre-creation for an event id whose meeting was deleted this window (tombstone or deleted-ids memo), or soften the confirm copy for calendar-sourced rows.
  • Default-on behavior with no in-product off switch. auto_sync: True ships background polling and auto-appearing meeting rows to every user who already configured a calendar provider, and the Settings UI for the three knobs is explicitly deferred — the only opt-out is a raw PUT /config. One release with the knobs invisible is tolerable; make sure the Settings-card PR lands in the same release.

Suggestions

  • Background sync failure is INFO-log-only; now that users will stop pressing Sync, a broken .ics URL means a silently stale list they believe is current. Surface last-sync time/error on the calendar section (even just in the existing sync-failure toast path on load).

[UX-REVIEWED] 23ee523

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5, fork) — 🟡 CONCERNS

Design-level review of 23ee5239326b4d1ad3f1a43561cca00fdec256a2 via the fork AI-review pipeline — updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

Design-Verdict: CONCERNS

Pre-creation materializes every calendar event to disk but nothing ever unmaterializes one — accumulation has no lifecycle story.

Watch

  • Unbounded accumulation of never-attended meetings. Every timed event auto-becomes a meeting directory as its start approaches ("create the meeting through the dashboard's own idempotent init"), on by default for anyone with a provider configured. list_meetings shows every dir with metadata, forever; once the event ages out of the 7-day cache the untouched idle row remains until manually deleted one at a time. Before this PR the list held only meetings the user chose to open; after it, a busy calendar grows the list by every event that ever passed the 15-minute window, and neither the PR nor its "Not in this PR" list names a reaper or retention answer.
  • Pre-created meetings are indistinguishable from user-created ones. _precreate_one writes the same new_meeting_meta the init route does, so a future cleanup pass cannot tell "auto-created, never touched" from "deliberately created, still empty" — the audit log records the difference but no per-meeting state does. This makes the accumulation above hard to fix later without guessing.

Suggestions

  • Stamp provenance (e.g. created_by: "calendar") in the pre-created meeting's metadata now; it is one key in a dict this PR already writes, and it keeps the eventual reaper trivial instead of heuristic.

[DESIGN-REVIEWED] 23ee523

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

First Principles Review (Fable 5, fork) — 🟡 CONCERNS

Premise-level review of 23ee5239326b4d1ad3f1a43561cca00fdec256a2 via the fork AI-review pipeline — why this exists and whether the shipped surface is the smallest honest version. Updated in place on each push. A BLOCK verdict blocks PR readiness; PASS/CONCERNS are advisory.

All verification done. The issue-radar precedent is real (issue_radar/backend/watch.py — same module-task shape, but with a constant cadence, no config knob), the reused init/lock/audit symbols all exist in the base, SettingsView.tsx spreads ...latest.calendar so the new TS fields have no reader, and is_running() has no consumer outside the new tests. Here is the review:

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 ships

Intent: have the calendar stay current and the imminent meeting already exist when the user looks — an ADDITION.

  1. Calendar syncs itself every 5 min once a provider is configured — justified
  2. A meeting starting within 15 min is pre-created as idle — justified
  3. Changed default: background sync ON for provider-configured installs — declared, opt-out ships with it
  4. Three new calendar.* config keys accepted by PUT /config — declared; poll_interval_secs one consumer, generalized
  5. Meetings list and calendar re-read every minute while the page is open — justified
  6. Manual Sync and the poller now run one shared sync — justified, mechanism-level reuse
  7. is_running() helper — undeclared, zero consumers
  8. Three optional CalendarConfig TS fields — zero frontend readers
  9. New meetings.calendar_precreate audit record — justified (SEL invariant)
  10. init_meeting_blocking public alias — one consumer, reuse not duplication

Watch

  • poll_interval_secs is a knob with one consumer (calendar_poller.poll_once), no UI ("Not in this PR: a Settings UI"), and a repo sibling that chose the other shape: issue_radar/backend/watch.py:65 pins its cadence as a module constant. The bounds rationale in constants.py justifies the clamp given the knob, not the knob.
  • auto_sync and precreate_lead_minutes do carry nameable harms (opt-out of unattended network fetches and of folders appearing on disk) — those two are earned.

Subtractions

  • Delete is_running() (calendar_poller.py) — the "status surface" its docstring names does not exist; grep for callers found only the new tests. Tests can assert on _poll_task.
  • Drop the poll_interval_secs config key (store default, settings.py validation, calendar_settings clamp); use CALENDAR_POLL_INTERVAL_SECS directly, and add the knob in the Settings-UI PR that gives it a second consumer.
  • Defer the three optional fields on CalendarConfig (website/src/apps/meetings/api.ts) — zero readers; SettingsView spreads ...latest.calendar, so the round trip never needed them.

[FIRST-PRINCIPLES-REVIEWED] 23ee523

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review (fork) — ✅ no blocking findings

Reviewed 23ee5239326b4d1ad3f1a43561cca00fdec256a2 via the fork AI-review pipeline; updated in place on each push.

Review details

Both 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 idle, and its own confidence line concedes this is an unsettled product judgment, not an observable wrong outcome. CANDIDATE 2 (idle-folder accumulation) is self-described as low confidence, is bounded by due_events (only events inside the lead window or currently under way are ever created, so an ended occurrence never generates a new folder) and by the sync window / MAX_CALENDAR_EVENTS, and rests on an assumed calendar-export shape ("many real calendar exports…") rather than code I opened. Neither reaches 80, and neither is a crash, data-loss, corruption, security hole, or a removed guard.

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

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review (fork) — ✅ no blocking findings

Reviewed 23ee5239326b4d1ad3f1a43561cca00fdec256a2 via the fork AI-review pipeline; updated in place on each push.

Review details

FINDING -- src/kiro_crew/apps/builtins/meetings/backend/routes/init.py:269 -- "no tick can pre-create" is false when cancellation releases a to_thread await while its worker continues through cleanup -> Fix: reword the changed shutdown claims to cover only new ticks, not in-flight worker work.
[GPT-REVIEWED] 23ee523

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Sep 3, 2026
@bolichen97
bolichen97 enabled auto-merge (squash) September 3, 2026 09:22

@bolichen97 bolichen97 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: passed present, and PR Readiness — the one required status context on main
    (ruleset protected-branches) — is success on this head.
  • No check run on this head is failure, cancelled, timed_out or still in flight. Skipped
    jobs are path-filtered conditionals, none of them required.
  • mergeable: true, and the head is not far enough behind main for 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 by dismiss_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.

@bolichen97
bolichen97 merged commit c894f81 into kirodotdev:main Sep 3, 2026
83 of 85 checks passed
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Sep 3, 2026
@bolichen97

Copy link
Copy Markdown
Collaborator

Open PR relationship audit

This 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

  • PR #2190 is OVERLAPPING relative to this PR. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #2190: REBASE. A merged PR refactored the one function 2190 also touches, which is what the 'merge conflict' label reflects. The conflict is mechanical and confined to one import list; nothing in 8080 implements any of 2190's behaviour. Files: src/kiro_crew/apps/builtins/meetings/backend/routes/calendar.py.
  • PR #8081 is OVERLAPPING relative to this PR. The goals differ or the implementations can complement each other; this is not a duplicate claim. Recommended action for PR #8081: REBASE. PR #8080 landed on 2026-09-03T09:27:25Z, after this branch was cut, and is what produced the 'merge conflict' label and the PR #8080 cross-reference on this PR's timeline. Read-only git merge-tree against live origin/main confirms exactly two content conflicts (docs/system-specs/modules/meetings.md and backend/routes/calendar.py); everything else auto-merges. Nothing in PR #8080 implements any part of PR #8081. Files: src/kiro_crew/apps/builtins/meetings/backend/routes/calendar.py, docs/system-specs/modules/meetings.md, src/kiro_crew/apps/builtins/meetings/backend/constants.py, routes/__init__.py, website/src/apps/meetings/api.ts, test/test_meetings_routes.py.

No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fork Pull request from a fork (external contributor)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants