Skip to content

fix(google): record the Google id with the creation receipt, find a lost insert by its marker, backfill missing ids (Refs #875) - #879

Merged
alex-mextner merged 6 commits into
mainfrom
google-event-id
Oct 28, 2026
Merged

alex-mextner merged 6 commits into
mainfrom
google-event-id

Conversation

@alex-mextner

Copy link
Copy Markdown
Owner

What changed

  • Commit 1: SyncService.ensureCopy records the Google id of a copy the bot creates together with the sync_log create receipt in one transaction, and finds a copy whose insert answer was lost by its hypercalendarbot_event_id marker (all pages, no series instance, no copy a row already points at) instead of inserting a second one; a linked row is updated on a create job, an unlinked row created on an update job; an answer without an id is an error.
  • Commit 2: scripts/backfill-google-event-ids.ts (dry run by default, --apply after a verified backup, only google_event_id, never writes to Google); runbook and spec.

Root cause and evidence (read-only prod counts, 2026-10-28)

28 of 29 push/create sync_log rows had no google_event_id (26 linkable); a lost answer left an unlinked copy and the BullMQ retry inserted a second one. Rows of the one connected user are consistent (278 synced, all with an id). Google-side counts come from the dry run after deploy.

Checks

  • bun test test/services/google/google-id-at-creation.test.ts 18 pass; google-id-backfill.test.ts 19 pass; tsc exit 0 (server).
  • Full suite on the server: 353 fail vs 352 on main (same environment failures; the extra one is my backup test, VACUUM INTO "disk I/O error" on the server's tmpfs, passes locally).
  • lint-check skipped for this worktree: legacy baseline violations in src/services/google/sync-service.ts, rig-cli feat(ai): Claude Sonnet 5.5 / Haiku 5.5 as the reserve after z.ai (#781) #782; 5 findings before, the same 5 after, none in new code. No other gate skipped.

Refs #875. Related: #877 (delete between a lost answer and its retry), #876.

…a lost insert by its marker (Refs #875)

A copy the bot created in Google could exist without the bot knowing its id:
pushEvent and the participant paths called insertEvent and saved the id in a
separate step, so a lost answer (timeout, restart while the job ran) left an
unlinked copy and the queue's retry inserted a second one. The create receipt
was written from the row read before the insert, so 28 of 29 production
push/create sync_log rows had no google_event_id.

- One creation path, SyncService.ensureCopy: look the copy up by the
  hypercalendarbot_event_id marker (every page, live events, no instance of a
  series, no copy any row already points at), adopt and update the oldest or
  insert; link and sync_log receipt (with the id) are one transaction, the
  adoption check runs again inside it.
- An insert answer without an id is an error; the retry finds that copy.
- The row decides, not the queued action: a linked row is updated on a create
  job, an unlinked one is created on an update job.
- pino 'Google copy linked' (origin, duplicates) and sync_log details
  {via, duplicates} / participant_sync:adopted_by_marker.

Tests: real SQLite and a stateful fake Google Calendar that can lose the
answer after creating the event (test/services/google/google-id-at-creation.test.ts),
paging of the lookup (calendar-api-bot-copies.test.ts); the older fake APIs
gain findBotEventCopies.

Known gap, tracked as #877: a delete between the lost answer and the retry
leaves the copy in Google. Lint: lint-check skipped for this worktree (legacy
cognitive-complexity baseline in sync-service.ts, rig-cli #782): 5 findings
before, the same 5 after, none in the new code.
…fter a verified backup (Refs #875)

Rows whose Google copy exists but whose id was never stored (a lost insert
answer, a link a disconnect cleared) can now be relinked.
scripts/backfill-google-event-ids.ts lists the user's primary calendar and
matches by the hypercalendarbot_event_id marker (written on apply) and, as a
report only unless --include-slot-matches, by (summary, start, end) with
an unmarked event; only when the match is the single candidate on both sides,
across users sharing one calendar too. Dry run (default) opens the database
read-only and prints counts only; --apply takes a verified VACUUM INTO backup
(0600, never overwritten), refuses if any user's Google read failed, writes
only google_event_id plus one sync_log row per link, never writes to Google.
Idempotent.

Tests: test/services/google/google-id-backfill.test.ts (19, real SQLite,
mutation-checked). Runbook section "Google event id backfill", spec paragraph.
Lint: lint-check skipped for this worktree (legacy baseline in sync-service.ts,
rig-cli #782); the files of this commit add no finding.
…ses (Refs #875)

The expanded Rig lint (#872) enforces cognitive 15 / cyclomatic 20; the planner was 43 / 27. Behaviour unchanged, the 19 backfill tests pass.
…ng (Refs #875)

type-bans blocks Record<string, unknown>; the dump is now typed as string | number | null columns.
… survives a mocked calendar-api (Refs #875)

test/services/google/sync-queue.test.ts replaces calendar-api.ts with mock.module for the whole run,
so the GoogleCalendarApi paging test failed in the full suite (CI) while passing alone. The paging and
the query now live in bot-event-copies.ts; GoogleCalendarApi.findBotEventCopies delegates to it.
Also formats an import line Biome wanted wrapped after the rebase.
@alex-mextner
alex-mextner merged commit 3c51bca into main Oct 28, 2026
12 checks passed
@alex-mextner
alex-mextner deleted the google-event-id branch October 28, 2026 23:23
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.

1 participant