Skip to content

feat(lifeops): support multi-calendar Google feeds#7072

Merged
lalalune merged 7 commits intoelizaOS:developfrom
dutchiono:feat/lifeops-managed-calendar-fallback
Apr 24, 2026
Merged

feat(lifeops): support multi-calendar Google feeds#7072
lalalune merged 7 commits intoelizaOS:developfrom
dutchiono:feat/lifeops-managed-calendar-fallback

Conversation

@dutchiono
Copy link
Copy Markdown
Contributor

@dutchiono dutchiono commented Apr 23, 2026

This completes the LifeOps multi-calendar stack on the app/runtime side and adds the last safety fallback for hosted cloud lag.

Included

  • list and persist available calendars in LifeOps settings
  • merge included calendar feeds across calendars/accounts
  • surface calendar origin in merged feeds
  • support cloud-managed secondary calendars
  • fall back to primary aggregation when managed calendar discovery is temporarily unavailable

Dependency

  • hosted connector route is tracked separately in cloud#472
  • this PR intentionally leaves the cloud submodule pointer alone until that cloud change lands upstream

Why

Hosted users were getting blank or incorrect calendar views when the connector only exposed primary or when the new managed calendar-list route was not yet deployed. This keeps the UI and agent behavior stable while the cloud route lands.

Validation

  • bun run --cwd C:\Users\epj33\Documents\Playground\milady\eliza\apps\app-lifeops test -- service-mixin-calendar.test.ts

Greptile Summary

This PR completes the LifeOps multi-calendar stack: it adds listCalendars / setCalendarIncluded service methods, a getCalendarFeed path that aggregates across all user-enabled calendars (with a fallback to primary when managed calendar discovery is unavailable), per-calendar includeInFeed preferences stored in the scheduler task metadata, and UI for toggling calendar visibility in Settings.

  • P1 — multi-account preference key collision: calendarFeedIncludes is keyed only by calendarId. Users with two connected Google accounts both have a calendar with calendarId: \"primary\", so toggling one will silently override the other's preference. The UI already uses a ${grantId}:${calendarId} composite key for React rendering — the backend persistence and lookup should do the same.

Confidence Score: 4/5

Safe to merge for single-account users; multi-account users will see incorrect calendar visibility preferences until the key collision is fixed.

One P1 correctness issue: calendarFeedIncludes keyed by calendarId alone causes a data collision for any user with two Google accounts sharing a 'primary' calendarId. All other findings are P2 (fragile error string match, unnecessary API call on toggle). The fallback, merge, and UI logic are otherwise well-structured and the new tests give good coverage of the happy paths.

apps/app-lifeops/src/lifeops/service-mixin-calendar.ts (preference lookup) and apps/app-lifeops/src/lifeops/owner-profile.ts (preference storage key)

Important Files Changed

Filename Overview
apps/app-lifeops/src/lifeops/service-mixin-calendar.ts Core multi-calendar logic: adds listCalendars, setCalendarIncluded, aggregateCalendarFeedsAcrossCalendars, and getCalendarFeed path selection. Has a P1 preference-key collision bug (calendarId-only key breaks multi-account users who both have "primary") and a fragile 503 fallback.
apps/app-lifeops/src/lifeops/owner-profile.ts Adds LifeOpsCalendarFeedPreferences storage: ensureLifeOpsCalendarFeedIncludes and setLifeOpsCalendarFeedIncluded. Logic is clean but the key type (calendarId only) propagates the P1 multi-account collision described in service-mixin-calendar.ts.
apps/app-lifeops/src/lifeops/google-calendar.ts Adds listGoogleCalendars function fetching calendarList with minAccessRole=reader. Passes showHidden=false in query params and also filters hidden items in the loop (harmless redundancy). Parsing and normalization look correct.
apps/app-lifeops/src/components/LifeOpsSettingsSection.tsx Adds calendar list + toggle UI in GoogleConnectorSideCard. Correctly uses cancelled-flag cleanup in useEffect. Local state update after toggle correctly matches on both grantId and calendarId, though the backend ignores grantId when persisting.
apps/app-lifeops/src/routes/lifeops-routes.ts Adds GET /api/lifeops/calendar/calendars and PUT /api/lifeops/calendar/calendars/:id/include routes. Body/path calendarId consistency check is a good defensive guard.
packages/shared/src/contracts/lifeops.ts Adds LifeOpsCalendarSummary, ListLifeOpsCalendarsRequest/Response, SetLifeOpsCalendarIncludedRequest/Response types, and extends GetLifeOpsCalendarFeedRequest and LifeOpsCalendarEvent. Type design looks correct.
apps/app-lifeops/src/lifeops/service-mixin-calendar.test.ts New test file covering mergeAggregatedCalendarFeedEvents deduplication and fallback to primary aggregation on empty or unavailable calendar list. Good coverage of the happy path and the 503 fallback.
apps/app-lifeops/src/lifeops/owner-profile.test.ts Tests for ensureLifeOpsCalendarFeedIncludes (default-true for new, preserve false for existing) and setLifeOpsCalendarFeedIncluded explicit toggle. Tests verify the calendarId-only key behavior — which means they pass even with the multi-account collision issue.
apps/app-lifeops/src/lifeops/google-managed-client.ts Adds ManagedGoogleCalendarSummaryResponse interface and listCalendars method on GoogleManagedClient. Straightforward delegation to the managed API.
apps/app-lifeops/src/components/LifeOpsWorkspaceView.tsx Adds eventOriginLabel helper combining calendarSummary and accountEmail; replaces raw accountEmail with it in AccountBadge. Clean change.
apps/app-lifeops/src/components/chat/widgets/plugins/lifeops-channels.tsx Adds empty-state label and sub-line calendarSummary display to the calendar widget. Structural change from items-center to items-start is appropriate for multi-line event rows.
packages/app-core/src/api/client-lifeops.ts Adds getLifeOpsCalendars and setLifeOpsCalendarIncluded client methods using appendOptionalParam; mirrors the app-lifeops client implementation.

Sequence Diagram

sequenceDiagram
    participant UI as Settings UI
    participant Client as ElizaClient
    participant Route as lifeops-routes
    participant Svc as LifeOpsService
    participant GMC as GoogleManagedClient
    participant GCal as Google CalendarList API
    participant Profile as owner-profile (Task)

    UI->>Client: getLifeOpsCalendars({side, mode})
    Client->>Route: GET /api/lifeops/calendar/calendars
    Route->>Svc: listCalendars(url, request)
    alt cloud_managed grant
        Svc->>GMC: listCalendars({side, grantId})
        GMC-->>Svc: ManagedGoogleCalendarSummaryResponse[]
    else browser/local grant
        Svc->>GCal: GET calendarList (Bearer token)
        GCal-->>Svc: GoogleCalendarListEntry[]
    end
    Svc->>Profile: ensureLifeOpsCalendarFeedIncludes(calendarIds)
    Profile-->>Svc: LifeOpsCalendarFeedPreferences
    Svc-->>Route: LifeOpsCalendarSummary[] (includeInFeed merged)
    Route-->>Client: { calendars }
    Client-->>UI: render toggle list

    UI->>Client: setLifeOpsCalendarIncluded({calendarId, includeInFeed, grantId})
    Client->>Route: PUT /api/lifeops/calendar/calendars/:id/include
    Route->>Svc: setCalendarIncluded(url, request)
    Svc->>Svc: listCalendars() — verify existence
    Svc->>Profile: setLifeOpsCalendarFeedIncluded(calendarId, included)
    Profile-->>Svc: updated LifeOpsCalendarFeedPreferences
    Svc-->>Route: updated LifeOpsCalendarSummary
    Route-->>Client: { calendar }
    Client-->>UI: optimistic local state update
Loading

Comments Outside Diff (3)

  1. apps/app-lifeops/src/lifeops/service-mixin-calendar.ts, line 1350-1358 (link)

    P1 Calendar preference key collision for multi-account users

    calendarFeedIncludes is stored as Record<string, boolean> keyed only by calendarId. Google's calendarList API returns "primary" as the calendarId for every account's primary calendar — so two connected Google accounts will share the same key in the preferences store. Toggling the primary calendar on account A will silently affect account B's primary calendar visibility as well.

    The UI already uses the composite key ${calendar.grantId}:${calendar.calendarId} for React's key prop and for local-state matching after a toggle, but the backend drops grantId before persisting. The preference lookup here also ignores grantId:

    preferences.calendarFeedIncludes[summary.calendarId] !== false

    The fix is to key preferences by ${grantId}:${calendarId} throughout (ensureLifeOpsCalendarFeedIncludes, setLifeOpsCalendarFeedIncluded, and the lookup in listCalendars).

  2. apps/app-lifeops/src/lifeops/service-mixin-calendar.ts, line 1432-1458 (link)

    P2 Fragile error-message substring match for 503 fallback

    The fallback to primary aggregation is gated on error.message.includes("managed calendar-list route"). This is a coupling between the error-throw site in listCalendars (same file) and this catch site; if the message is ever rephrased or if a different 503 from the managed client triggers listCalendars, the fallback silently does not fire.

    Consider using a dedicated error subclass or a typed error code (error.code === "MANAGED_CALENDAR_LIST_UNAVAILABLE") that is stable across refactors and locale-agnostic.

  3. apps/app-lifeops/src/lifeops/service-mixin-calendar.ts, line 1380-1401 (link)

    P2 setCalendarIncluded issues a full Google API round-trip to validate the calendar

    setCalendarIncluded calls this.listCalendars(...) — which hits either the managed cloud API or Google's calendarList endpoint for every grant — purely to confirm the target calendarId exists before writing the preference. For users with several connected accounts this means multiple external calls on every toggle.

    Consider persisting a local snapshot of known calendars (already returned to the UI) so this validation can be done from cache, or skip the existence check if it's acceptable to trust the client-supplied calendarId.

Reviews (1): Last reviewed commit: "fix(lifeops): fall back when managed cal..." | Re-trigger Greptile

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 76c78dd3-6742-40be-b505-9292f2ee0a25

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@dutchiono dutchiono force-pushed the feat/lifeops-managed-calendar-fallback branch from 1b9da11 to 08fdc40 Compare April 23, 2026 20:11
@dutchiono
Copy link
Copy Markdown
Contributor Author

Upstream dependency is cloud#472 for the hosted managed-calendar list route. Once that lands, the follow-up top-level Milady PR should only advance the \�liza\ pointer; this PR intentionally does not move the \cloud\ submodule pointer.

@lalalune lalalune merged commit 0dd29f5 into elizaOS:develop Apr 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants