Google Tasks
Google Calendar's range prefetch (sync.prefetcheventrange) surfaces task entries in the primary
calendar feed (eventType = 7), but does not carry task completion state. Completed and open
tasks look identical on the calendar wire.
To show tasks accurately in the popover and widgets, Magical polls Google Tasks' internal web sync endpoint and overlays completion state onto the calendar event cache.
Wire protocol and authentication
- Endpoint:
POST https://tasks-pa.clients6.google.com/$rpc/google.internal.tasks.v1.TasksApiService/Sync - Content-Type:
application/json+protobuf - Authentication:
Authorization: SAPISIDHASH <timestamp>_<sha1>_u- Preimage:
${gaiaId} ${timestamp} ${sapisid} https://calendar.google.com - Signed with SHA-1 over the active session's
SAPISIDcookie. - Unlike Calendar's prefetch, this gRPC-web endpoint rejects requests with HTTP 401 unless
SAPISIDHASHcontains the user's Gaia ID. - The request must carry the partition's cookies and an
Originheader, so the fetch setscredentials: 'include'and sendsOrigin: https://calendar.google.com. Electron'ssession.fetchruns outside any document, so it sets neither by itself, and Google answers 401 however well formed the hash is. Measured against the live endpoint: both present returns 200, either one omitted returns 401, and theRefererheader makes no difference.
- Preimage:
- Gaia ID discovery: extracted from Calendar's
initialdatapayload (indices[52]and[0][1]) orwindow.WIZ_global_data?.CTox0eduring calendar list discovery.
Tuple indices
The response is a JSON-encoded Protocol Buffer array. response[0] is a flat run of change records,
each wrapped in a sparse list, and the run holds task lists, recurrence definitions and tasks side by
side:
record[0]: Google Tasks task ID string.record[1]: Task details tuple.record[1][0]:1if completed,nullif open.record[1][1]: Title.record[1][3]: Due date[year, month, day].record[1][4]: Completion timestamp tuple[secondsString, nanos].record[1][9]: The recurrence this is an occurrence of, on a recurring task only.
record[8]: Schedule tuple,[[null, [y, m, d], [h, m], timezone, null, [epochSeconds]]].record[23]: Calendar binding tuple, on a task Google draws on the calendar.record[23][0][1][0]: The exact Calendar event ID corresponding toevent.idincalendar-decoder.ts.
Records are matched by shape, not by position. The wrapper's populated slot moves between records: measured against a live account, a task sat at index 2 of its wrapper in one response and at index 5 in another. A task is known by carrying a string id and a details tuple whose title is a string; a task list carries its own id where a task carries details, and a recurrence definition carries the list id there, so neither is mistaken for a task.
Integration with the event cache and UI
CalendarService.syncTasksCompletionruns after calendar events are expanded; it matches tasks bycalendarEventIdand setsisCompletedandcompletedAt.- Only a task Google draws on the calendar carries a binding. An occurrence of a recurring task carries none; see Recurring tasks come from the Tasks sync.
- A sync that answered settles every task event, not only the ones it names. The response is the whole of Google's answer, so a task missing from it is one Tasks does not hold as complete, and its cached tick is cleared. A sync that fails leaves the cached state alone, because it is the last thing Google actually said. Letting a cached tick survive whatever the sync said cannot work: a completion reaches the cache from two places, the optimistic write and the sync, and only the sync can retract one, so a task ticked off and then reopened in the browser would stay greyed out across restarts.
- An answer naming none of the account's tasks settles nothing. Tasks holds a record for an open task as much as a finished one, so recognising none of them is a decode that failed rather than an account with nothing in it. Settling on it would clear every tick the user has, silently; leaving the cached state alone and logging it says so instead.
- Completed tasks are excluded from alert scheduling (
AlertScheduler), preventing stale notifications for finished tasks. - If the user marks a task complete or incomplete from the context menu (
setTaskCompleted), Magical updates the event cache optimistically and schedules an immediate poll. That write outlives a sync that disagrees for 30 seconds: Google Tasks takes a moment to record a completion driven through Calendar's own UI, and the poll that follows the click answers in well under a second, so the sync is not allowed to settle the write before then. After the window the sync overrules the write like any other. - The
showCompletedTaskspreference controls whether completed tasks appear in the menu bar popover and widgets (default:true, keeping completed tasks visible and greyed out).
Recurring tasks come from the Tasks sync
A recurring task appears on Google Calendar's own grid, but not in anything Magical polls as a
calendar. The range prefetch does not carry it: searched for a recurring task's title across a live
account's whole payload, 211 KB on one account and 6.9 MB on another, it is absent from both, while a
one-off task on the same day arrives as an ordinary eventType = 7 entry. Google's own web client
draws these from the Tasks service rather than from the event feed.
The Tasks sync carries them already expanded: one record per occurrence, each with its own Tasks
id, the shared title, a due date, and a schedule holding the start as epoch seconds. A live account's
monthly task returned 31 of them in one response, covering a month. So there is no recurrence rule to
expand locally, and calendar-recurrence.ts is not involved.
occurrencesInRange builds an event from each occurrence that falls in the range being fetched, and
syncTasksCompletion appends them to what the caller folds into the cache. They are filed under
tasks@tasks.google.com, which is what gives them the Tasks calendar's name and colour and lets
hiding that calendar hide them. The start is taken from the epoch seconds rather than rebuilt from
the date and time parts, which would reintroduce every question about the zone they are in; the
[h, m] tuple is read only to tell a timed occurrence from a date with no time of day. A timed one is
drawn for an hour, the way Google draws a task.
The grid names an occurrence's chip by its Tasks id. There is no Calendar event id and no
webUrl to read an eid out of, but Google still draws a chip and calls it tasks_<taskId>.
Measured on a live account: the occurrence the sync called O7mOrHptGofMR7Dj is the chip
[data-eventid="tasks_O7mOrHptGofMR7Dj"], beside a one-off task drawn as ttb_ and the base64 of
<eventId> <address>. So the occurrence carries that id in chipEid, extractEid prefers it, and a
link reaches the occurrence rather than the day it sits on. Everything driven through a chip works
from there, reveal and the completion control alike.
An occurrence alerts at its own time. The record says nothing about notifications, so it is given
reminders: [0], which is what a one-off task carries from the calendar wire: a task is due at a
moment rather than starting at one, and the account's global lead would warn some minutes before
that. Nothing else is needed, because an occurrence is an ordinary event to AlertScheduler from
there, and MAX_TIMER_MS keeps the far ones unarmed until a later poll, exactly as it does for the
occurrences of a recurring meeting.