Skip to content

fix(gmail,outlook-mail): durable recovery for stranded mailbox watches on upgrade - #217

Merged
KrisBraun merged 1 commit into
mainfrom
fix/mail-upgrade-recovery
Jun 22, 2026
Merged

fix(gmail,outlook-mail): durable recovery for stranded mailbox watches on upgrade#217
KrisBraun merged 1 commit into
mainfrom
fix/mail-upgrade-recovery

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Problem

A mail connection (Gmail or Outlook) could be left permanently and silently dead with no self-recovery on deploy — the failure mode the durable-recurring-tasks work (#215) set out to eliminate, but with a gap.

upgrade() only re-asserted the recurring self-heal/renewal tasks when the watch/subscription sentinel (mailbox_webhook / mailbox_subscription) was already stored, and the cron maintenance sweep is gated on a recurring_meta.ever marker. Two stranded states slip through both recovery paths:

  1. The sentinel never persisted — a prior setupWatch() / setupMailboxSubscription() threw before its set() (e.g. Gmail's 400 "Only one user push notification client allowed per developer"). There was nothing to re-assert, and because scheduleRecurring never ran, the sweep's ever marker was never set either.
  2. The watch/subscription expired while the self-heal/renewal chain was dead.

Either way the connection stayed dead until the user manually re-enabled a channel.

Real-world hit

A Gmail "Personal" connection stopped ingesting after a June reconnect and produced zero mail for ~18 days across multiple deploys, while the same user's other Gmail connection (continuously healthy) was unaffected. The recovery paths shipped in #215 couldn't reach it because its watch state had never persisted / had expired with a dead chain.

Fix

upgrade() now always runs a recoverMailboxDelivery() backstop for any instance with enabled channels:

  • Healthy watch (sentinel present and unexpired) → only re-assert the recurring tasks (unchanged behavior).
  • Missing or expired watch → re-establish it and re-walk every enabled label/folder, so mail that accumulated while delivery was dead is backfilled. The backfill upserts by source (no duplicates) and uses initial-sync semantics (unread:false, archived:false), so it never spams notifications.

Gmail's legacy per-channel→mailbox migration is extracted into migrateLegacyPerChannelState(); the watch is then (re-)established by the shared recovery path (removing the old if (migratedAny) setupMailboxWebhook() tail).

Both connectors get the same shape (recoverMailboxDelivery + requeueInitialSync), since Outlook-mail had the identical gating gap (if (existing?.subscriptionId) re-assert, no else).

Why this fixes it for all affected users

It runs on every deploy for every non-archived instance (the deploy "Upgrading active twists" phase). Any Gmail/Outlook connection currently stranded with a missing/expired watch will re-establish live delivery and backfill its gap on the next deploy — no manual re-enable required.

Testing

  • Gmail: 47 tests pass (4 new recoverMailboxDelivery cases: stranded, expired, healthy, no-channels)
  • Outlook-mail: 35 tests pass (4 new equivalent cases)
  • Both pnpm build (tsc) + plot lint clean
  • Connector-only change → no changeset (per public/AGENTS.md)

Deploy / verify notes

  • This is a public-submodule connector change. To reach production it needs the superproject (plotday/core) submodule pointer bumped to the merge commit, then a deploy (the deploy-connectors (gmail/outlook-mail) jobs).
  • A connection that already self-healed its live watch (sentinel now fresh) takes the healthy path and is not re-backfilled; recovering its historical gap still needs a one-time channel re-enable (which triggers onChannelEnabled's full backfill). The auto-backfill here targets connections still stranded at deploy time.

🤖 Generated with Claude Code

…s on upgrade
A mail connection could be left permanently and silently dead with no
self-recovery on deploy. `upgrade()` only re-asserted the recurring
self-heal/renewal tasks when the watch/subscription sentinel
(`mailbox_webhook` / `mailbox_subscription`) was already stored, and the
cron maintenance sweep is gated on a `recurring_meta.ever` marker. Two
stranded states slipped through both:
1. The sentinel never persisted — a prior `setupWatch()` /
`setupMailboxSubscription()` threw before its `set()` (e.g. Gmail's
"Only one user push notification client allowed per developer" 400),
so there was nothing to re-assert AND `scheduleRecurring` never ran,
so the sweep's `ever` marker was never set either.
2. The watch/subscription expired while the self-heal chain was dead.
Either way the connection stayed dead until the user manually re-enabled
a channel. Real-world hit: a Gmail "Personal" connection stopped
ingesting after a reconnect and never recovered across deploys.
Fix: `upgrade()` now always runs a `recoverMailboxDelivery()` backstop for
any instance with enabled channels. A healthy watch only re-asserts the
recurring tasks (unchanged behavior); a missing OR expired one is
re-established AND every enabled label/folder is re-walked, so mail that
accumulated while delivery was dead is backfilled. The backfill upserts by
`source` (no duplicates) and uses initial-sync semantics (read/unarchived),
so it never spams notifications. Gmail's legacy per-channel→mailbox
migration is extracted into `migrateLegacyPerChannelState()`; the watch is
then (re-)established by the shared recovery path.
Connector-only change (no changeset). gmail 47 tests, outlook-mail 35
tests, both `plot lint` clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit 3fb91d9 into mainJun 22, 2026
1 check passed
@KrisBraun
KrisBraun deleted the fix/mail-upgrade-recovery branch June 22, 2026 22:06
Sign up for freeto 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

@KrisBraun
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(gmail,outlook-mail): durable recovery for stranded mailbox watches on upgrade by KrisBraun · Pull Request #217 · plotday/plot · GitHub
Skip to content

fix(gmail,outlook-mail): durable recovery for stranded mailbox watches on upgrade - #217

Merged
KrisBraun merged 1 commit into
mainfrom
fix/mail-upgrade-recovery
Jun 22, 2026
Merged

fix(gmail,outlook-mail): durable recovery for stranded mailbox watches on upgrade#217
KrisBraun merged 1 commit into
mainfrom
fix/mail-upgrade-recovery

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Problem

A mail connection (Gmail or Outlook) could be left permanently and silently dead with no self-recovery on deploy — the failure mode the durable-recurring-tasks work (#215) set out to eliminate, but with a gap.

upgrade() only re-asserted the recurring self-heal/renewal tasks when the watch/subscription sentinel (mailbox_webhook / mailbox_subscription) was already stored, and the cron maintenance sweep is gated on a recurring_meta.ever marker. Two stranded states slip through both recovery paths:

  1. The sentinel never persisted — a prior setupWatch() / setupMailboxSubscription() threw before its set() (e.g. Gmail's 400 "Only one user push notification client allowed per developer"). There was nothing to re-assert, and because scheduleRecurring never ran, the sweep's ever marker was never set either.
  2. The watch/subscription expired while the self-heal/renewal chain was dead.

Either way the connection stayed dead until the user manually re-enabled a channel.

Real-world hit

A Gmail "Personal" connection stopped ingesting after a June reconnect and produced zero mail for ~18 days across multiple deploys, while the same user's other Gmail connection (continuously healthy) was unaffected. The recovery paths shipped in #215 couldn't reach it because its watch state had never persisted / had expired with a dead chain.

Fix

upgrade() now always runs a recoverMailboxDelivery() backstop for any instance with enabled channels:

  • Healthy watch (sentinel present and unexpired) → only re-assert the recurring tasks (unchanged behavior).
  • Missing or expired watch → re-establish it and re-walk every enabled label/folder, so mail that accumulated while delivery was dead is backfilled. The backfill upserts by source (no duplicates) and uses initial-sync semantics (unread:false, archived:false), so it never spams notifications.

Gmail's legacy per-channel→mailbox migration is extracted into migrateLegacyPerChannelState(); the watch is then (re-)established by the shared recovery path (removing the old if (migratedAny) setupMailboxWebhook() tail).

Both connectors get the same shape (recoverMailboxDelivery + requeueInitialSync), since Outlook-mail had the identical gating gap (if (existing?.subscriptionId) re-assert, no else).

Why this fixes it for all affected users

It runs on every deploy for every non-archived instance (the deploy "Upgrading active twists" phase). Any Gmail/Outlook connection currently stranded with a missing/expired watch will re-establish live delivery and backfill its gap on the next deploy — no manual re-enable required.

Testing

  • Gmail: 47 tests pass (4 new recoverMailboxDelivery cases: stranded, expired, healthy, no-channels)
  • Outlook-mail: 35 tests pass (4 new equivalent cases)
  • Both pnpm build (tsc) + plot lint clean
  • Connector-only change → no changeset (per public/AGENTS.md)

Deploy / verify notes

  • This is a public-submodule connector change. To reach production it needs the superproject (plotday/core) submodule pointer bumped to the merge commit, then a deploy (the deploy-connectors (gmail/outlook-mail) jobs).
  • A connection that already self-healed its live watch (sentinel now fresh) takes the healthy path and is not re-backfilled; recovering its historical gap still needs a one-time channel re-enable (which triggers onChannelEnabled's full backfill). The auto-backfill here targets connections still stranded at deploy time.

🤖 Generated with Claude Code

…s on upgrade
A mail connection could be left permanently and silently dead with no
self-recovery on deploy. `upgrade()` only re-asserted the recurring
self-heal/renewal tasks when the watch/subscription sentinel
(`mailbox_webhook` / `mailbox_subscription`) was already stored, and the
cron maintenance sweep is gated on a `recurring_meta.ever` marker. Two
stranded states slipped through both:
1. The sentinel never persisted — a prior `setupWatch()` /
`setupMailboxSubscription()` threw before its `set()` (e.g. Gmail's
"Only one user push notification client allowed per developer" 400),
so there was nothing to re-assert AND `scheduleRecurring` never ran,
so the sweep's `ever` marker was never set either.
2. The watch/subscription expired while the self-heal chain was dead.
Either way the connection stayed dead until the user manually re-enabled
a channel. Real-world hit: a Gmail "Personal" connection stopped
ingesting after a reconnect and never recovered across deploys.
Fix: `upgrade()` now always runs a `recoverMailboxDelivery()` backstop for
any instance with enabled channels. A healthy watch only re-asserts the
recurring tasks (unchanged behavior); a missing OR expired one is
re-established AND every enabled label/folder is re-walked, so mail that
accumulated while delivery was dead is backfilled. The backfill upserts by
`source` (no duplicates) and uses initial-sync semantics (read/unarchived),
so it never spams notifications. Gmail's legacy per-channel→mailbox
migration is extracted into `migrateLegacyPerChannelState()`; the watch is
then (re-)established by the shared recovery path.
Connector-only change (no changeset). gmail 47 tests, outlook-mail 35
tests, both `plot lint` clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit 3fb91d9 into mainJun 22, 2026
1 check passed
@KrisBraun
KrisBraun deleted the fix/mail-upgrade-recovery branch June 22, 2026 22:06
Sign up for freeto 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

@KrisBraun
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(gmail,outlook-mail): durable recovery for stranded mailbox watches on upgrade by KrisBraun · Pull Request #217 · plotday/plot · GitHub
Skip to content

fix(gmail,outlook-mail): durable recovery for stranded mailbox watches on upgrade - #217

Merged
KrisBraun merged 1 commit into
mainfrom
fix/mail-upgrade-recovery
Jun 22, 2026
Merged

fix(gmail,outlook-mail): durable recovery for stranded mailbox watches on upgrade#217
KrisBraun merged 1 commit into
mainfrom
fix/mail-upgrade-recovery

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Problem

A mail connection (Gmail or Outlook) could be left permanently and silently dead with no self-recovery on deploy — the failure mode the durable-recurring-tasks work (#215) set out to eliminate, but with a gap.

upgrade() only re-asserted the recurring self-heal/renewal tasks when the watch/subscription sentinel (mailbox_webhook / mailbox_subscription) was already stored, and the cron maintenance sweep is gated on a recurring_meta.ever marker. Two stranded states slip through both recovery paths:

  1. The sentinel never persisted — a prior setupWatch() / setupMailboxSubscription() threw before its set() (e.g. Gmail's 400 "Only one user push notification client allowed per developer"). There was nothing to re-assert, and because scheduleRecurring never ran, the sweep's ever marker was never set either.
  2. The watch/subscription expired while the self-heal/renewal chain was dead.

Either way the connection stayed dead until the user manually re-enabled a channel.

Real-world hit

A Gmail "Personal" connection stopped ingesting after a June reconnect and produced zero mail for ~18 days across multiple deploys, while the same user's other Gmail connection (continuously healthy) was unaffected. The recovery paths shipped in #215 couldn't reach it because its watch state had never persisted / had expired with a dead chain.

Fix

upgrade() now always runs a recoverMailboxDelivery() backstop for any instance with enabled channels:

  • Healthy watch (sentinel present and unexpired) → only re-assert the recurring tasks (unchanged behavior).
  • Missing or expired watch → re-establish it and re-walk every enabled label/folder, so mail that accumulated while delivery was dead is backfilled. The backfill upserts by source (no duplicates) and uses initial-sync semantics (unread:false, archived:false), so it never spams notifications.

Gmail's legacy per-channel→mailbox migration is extracted into migrateLegacyPerChannelState(); the watch is then (re-)established by the shared recovery path (removing the old if (migratedAny) setupMailboxWebhook() tail).

Both connectors get the same shape (recoverMailboxDelivery + requeueInitialSync), since Outlook-mail had the identical gating gap (if (existing?.subscriptionId) re-assert, no else).

Why this fixes it for all affected users

It runs on every deploy for every non-archived instance (the deploy "Upgrading active twists" phase). Any Gmail/Outlook connection currently stranded with a missing/expired watch will re-establish live delivery and backfill its gap on the next deploy — no manual re-enable required.

Testing

  • Gmail: 47 tests pass (4 new recoverMailboxDelivery cases: stranded, expired, healthy, no-channels)
  • Outlook-mail: 35 tests pass (4 new equivalent cases)
  • Both pnpm build (tsc) + plot lint clean
  • Connector-only change → no changeset (per public/AGENTS.md)

Deploy / verify notes

  • This is a public-submodule connector change. To reach production it needs the superproject (plotday/core) submodule pointer bumped to the merge commit, then a deploy (the deploy-connectors (gmail/outlook-mail) jobs).
  • A connection that already self-healed its live watch (sentinel now fresh) takes the healthy path and is not re-backfilled; recovering its historical gap still needs a one-time channel re-enable (which triggers onChannelEnabled's full backfill). The auto-backfill here targets connections still stranded at deploy time.

🤖 Generated with Claude Code

…s on upgrade
A mail connection could be left permanently and silently dead with no
self-recovery on deploy. `upgrade()` only re-asserted the recurring
self-heal/renewal tasks when the watch/subscription sentinel
(`mailbox_webhook` / `mailbox_subscription`) was already stored, and the
cron maintenance sweep is gated on a `recurring_meta.ever` marker. Two
stranded states slipped through both:
1. The sentinel never persisted — a prior `setupWatch()` /
`setupMailboxSubscription()` threw before its `set()` (e.g. Gmail's
"Only one user push notification client allowed per developer" 400),
so there was nothing to re-assert AND `scheduleRecurring` never ran,
so the sweep's `ever` marker was never set either.
2. The watch/subscription expired while the self-heal chain was dead.
Either way the connection stayed dead until the user manually re-enabled
a channel. Real-world hit: a Gmail "Personal" connection stopped
ingesting after a reconnect and never recovered across deploys.
Fix: `upgrade()` now always runs a `recoverMailboxDelivery()` backstop for
any instance with enabled channels. A healthy watch only re-asserts the
recurring tasks (unchanged behavior); a missing OR expired one is
re-established AND every enabled label/folder is re-walked, so mail that
accumulated while delivery was dead is backfilled. The backfill upserts by
`source` (no duplicates) and uses initial-sync semantics (read/unarchived),
so it never spams notifications. Gmail's legacy per-channel→mailbox
migration is extracted into `migrateLegacyPerChannelState()`; the watch is
then (re-)established by the shared recovery path.
Connector-only change (no changeset). gmail 47 tests, outlook-mail 35
tests, both `plot lint` clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit 3fb91d9 into mainJun 22, 2026
1 check passed
@KrisBraun
KrisBraun deleted the fix/mail-upgrade-recovery branch June 22, 2026 22:06
Sign up for freeto 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

@KrisBraun
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(gmail,outlook-mail): durable recovery for stranded mailbox watches on upgrade by KrisBraun · Pull Request #217 · plotday/plot · GitHub
Skip to content

fix(gmail,outlook-mail): durable recovery for stranded mailbox watches on upgrade - #217

Merged
KrisBraun merged 1 commit into
mainfrom
fix/mail-upgrade-recovery
Jun 22, 2026
Merged

fix(gmail,outlook-mail): durable recovery for stranded mailbox watches on upgrade#217
KrisBraun merged 1 commit into
mainfrom
fix/mail-upgrade-recovery

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Problem

A mail connection (Gmail or Outlook) could be left permanently and silently dead with no self-recovery on deploy — the failure mode the durable-recurring-tasks work (#215) set out to eliminate, but with a gap.

upgrade() only re-asserted the recurring self-heal/renewal tasks when the watch/subscription sentinel (mailbox_webhook / mailbox_subscription) was already stored, and the cron maintenance sweep is gated on a recurring_meta.ever marker. Two stranded states slip through both recovery paths:

  1. The sentinel never persisted — a prior setupWatch() / setupMailboxSubscription() threw before its set() (e.g. Gmail's 400 "Only one user push notification client allowed per developer"). There was nothing to re-assert, and because scheduleRecurring never ran, the sweep's ever marker was never set either.
  2. The watch/subscription expired while the self-heal/renewal chain was dead.

Either way the connection stayed dead until the user manually re-enabled a channel.

Real-world hit

A Gmail "Personal" connection stopped ingesting after a June reconnect and produced zero mail for ~18 days across multiple deploys, while the same user's other Gmail connection (continuously healthy) was unaffected. The recovery paths shipped in #215 couldn't reach it because its watch state had never persisted / had expired with a dead chain.

Fix

upgrade() now always runs a recoverMailboxDelivery() backstop for any instance with enabled channels:

  • Healthy watch (sentinel present and unexpired) → only re-assert the recurring tasks (unchanged behavior).
  • Missing or expired watch → re-establish it and re-walk every enabled label/folder, so mail that accumulated while delivery was dead is backfilled. The backfill upserts by source (no duplicates) and uses initial-sync semantics (unread:false, archived:false), so it never spams notifications.

Gmail's legacy per-channel→mailbox migration is extracted into migrateLegacyPerChannelState(); the watch is then (re-)established by the shared recovery path (removing the old if (migratedAny) setupMailboxWebhook() tail).

Both connectors get the same shape (recoverMailboxDelivery + requeueInitialSync), since Outlook-mail had the identical gating gap (if (existing?.subscriptionId) re-assert, no else).

Why this fixes it for all affected users

It runs on every deploy for every non-archived instance (the deploy "Upgrading active twists" phase). Any Gmail/Outlook connection currently stranded with a missing/expired watch will re-establish live delivery and backfill its gap on the next deploy — no manual re-enable required.

Testing

  • Gmail: 47 tests pass (4 new recoverMailboxDelivery cases: stranded, expired, healthy, no-channels)
  • Outlook-mail: 35 tests pass (4 new equivalent cases)
  • Both pnpm build (tsc) + plot lint clean
  • Connector-only change → no changeset (per public/AGENTS.md)

Deploy / verify notes

  • This is a public-submodule connector change. To reach production it needs the superproject (plotday/core) submodule pointer bumped to the merge commit, then a deploy (the deploy-connectors (gmail/outlook-mail) jobs).
  • A connection that already self-healed its live watch (sentinel now fresh) takes the healthy path and is not re-backfilled; recovering its historical gap still needs a one-time channel re-enable (which triggers onChannelEnabled's full backfill). The auto-backfill here targets connections still stranded at deploy time.

🤖 Generated with Claude Code

…s on upgrade
A mail connection could be left permanently and silently dead with no
self-recovery on deploy. `upgrade()` only re-asserted the recurring
self-heal/renewal tasks when the watch/subscription sentinel
(`mailbox_webhook` / `mailbox_subscription`) was already stored, and the
cron maintenance sweep is gated on a `recurring_meta.ever` marker. Two
stranded states slipped through both:
1. The sentinel never persisted — a prior `setupWatch()` /
`setupMailboxSubscription()` threw before its `set()` (e.g. Gmail's
"Only one user push notification client allowed per developer" 400),
so there was nothing to re-assert AND `scheduleRecurring` never ran,
so the sweep's `ever` marker was never set either.
2. The watch/subscription expired while the self-heal chain was dead.
Either way the connection stayed dead until the user manually re-enabled
a channel. Real-world hit: a Gmail "Personal" connection stopped
ingesting after a reconnect and never recovered across deploys.
Fix: `upgrade()` now always runs a `recoverMailboxDelivery()` backstop for
any instance with enabled channels. A healthy watch only re-asserts the
recurring tasks (unchanged behavior); a missing OR expired one is
re-established AND every enabled label/folder is re-walked, so mail that
accumulated while delivery was dead is backfilled. The backfill upserts by
`source` (no duplicates) and uses initial-sync semantics (read/unarchived),
so it never spams notifications. Gmail's legacy per-channel→mailbox
migration is extracted into `migrateLegacyPerChannelState()`; the watch is
then (re-)established by the shared recovery path.
Connector-only change (no changeset). gmail 47 tests, outlook-mail 35
tests, both `plot lint` clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit 3fb91d9 into mainJun 22, 2026
1 check passed
@KrisBraun
KrisBraun deleted the fix/mail-upgrade-recovery branch June 22, 2026 22:06
Sign up for freeto 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

@KrisBraun
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(gmail,outlook-mail): durable recovery for stranded mailbox watches on upgrade by KrisBraun · Pull Request #217 · plotday/plot · GitHub
Skip to content

fix(gmail,outlook-mail): durable recovery for stranded mailbox watches on upgrade - #217

Merged
KrisBraun merged 1 commit into
mainfrom
fix/mail-upgrade-recovery
Jun 22, 2026
Merged

fix(gmail,outlook-mail): durable recovery for stranded mailbox watches on upgrade#217
KrisBraun merged 1 commit into
mainfrom
fix/mail-upgrade-recovery

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Problem

A mail connection (Gmail or Outlook) could be left permanently and silently dead with no self-recovery on deploy — the failure mode the durable-recurring-tasks work (#215) set out to eliminate, but with a gap.

upgrade() only re-asserted the recurring self-heal/renewal tasks when the watch/subscription sentinel (mailbox_webhook / mailbox_subscription) was already stored, and the cron maintenance sweep is gated on a recurring_meta.ever marker. Two stranded states slip through both recovery paths:

  1. The sentinel never persisted — a prior setupWatch() / setupMailboxSubscription() threw before its set() (e.g. Gmail's 400 "Only one user push notification client allowed per developer"). There was nothing to re-assert, and because scheduleRecurring never ran, the sweep's ever marker was never set either.
  2. The watch/subscription expired while the self-heal/renewal chain was dead.

Either way the connection stayed dead until the user manually re-enabled a channel.

Real-world hit

A Gmail "Personal" connection stopped ingesting after a June reconnect and produced zero mail for ~18 days across multiple deploys, while the same user's other Gmail connection (continuously healthy) was unaffected. The recovery paths shipped in #215 couldn't reach it because its watch state had never persisted / had expired with a dead chain.

Fix

upgrade() now always runs a recoverMailboxDelivery() backstop for any instance with enabled channels:

  • Healthy watch (sentinel present and unexpired) → only re-assert the recurring tasks (unchanged behavior).
  • Missing or expired watch → re-establish it and re-walk every enabled label/folder, so mail that accumulated while delivery was dead is backfilled. The backfill upserts by source (no duplicates) and uses initial-sync semantics (unread:false, archived:false), so it never spams notifications.

Gmail's legacy per-channel→mailbox migration is extracted into migrateLegacyPerChannelState(); the watch is then (re-)established by the shared recovery path (removing the old if (migratedAny) setupMailboxWebhook() tail).

Both connectors get the same shape (recoverMailboxDelivery + requeueInitialSync), since Outlook-mail had the identical gating gap (if (existing?.subscriptionId) re-assert, no else).

Why this fixes it for all affected users

It runs on every deploy for every non-archived instance (the deploy "Upgrading active twists" phase). Any Gmail/Outlook connection currently stranded with a missing/expired watch will re-establish live delivery and backfill its gap on the next deploy — no manual re-enable required.

Testing

  • Gmail: 47 tests pass (4 new recoverMailboxDelivery cases: stranded, expired, healthy, no-channels)
  • Outlook-mail: 35 tests pass (4 new equivalent cases)
  • Both pnpm build (tsc) + plot lint clean
  • Connector-only change → no changeset (per public/AGENTS.md)

Deploy / verify notes

  • This is a public-submodule connector change. To reach production it needs the superproject (plotday/core) submodule pointer bumped to the merge commit, then a deploy (the deploy-connectors (gmail/outlook-mail) jobs).
  • A connection that already self-healed its live watch (sentinel now fresh) takes the healthy path and is not re-backfilled; recovering its historical gap still needs a one-time channel re-enable (which triggers onChannelEnabled's full backfill). The auto-backfill here targets connections still stranded at deploy time.

🤖 Generated with Claude Code

…s on upgrade
A mail connection could be left permanently and silently dead with no
self-recovery on deploy. `upgrade()` only re-asserted the recurring
self-heal/renewal tasks when the watch/subscription sentinel
(`mailbox_webhook` / `mailbox_subscription`) was already stored, and the
cron maintenance sweep is gated on a `recurring_meta.ever` marker. Two
stranded states slipped through both:
1. The sentinel never persisted — a prior `setupWatch()` /
`setupMailboxSubscription()` threw before its `set()` (e.g. Gmail's
"Only one user push notification client allowed per developer" 400),
so there was nothing to re-assert AND `scheduleRecurring` never ran,
so the sweep's `ever` marker was never set either.
2. The watch/subscription expired while the self-heal chain was dead.
Either way the connection stayed dead until the user manually re-enabled
a channel. Real-world hit: a Gmail "Personal" connection stopped
ingesting after a reconnect and never recovered across deploys.
Fix: `upgrade()` now always runs a `recoverMailboxDelivery()` backstop for
any instance with enabled channels. A healthy watch only re-asserts the
recurring tasks (unchanged behavior); a missing OR expired one is
re-established AND every enabled label/folder is re-walked, so mail that
accumulated while delivery was dead is backfilled. The backfill upserts by
`source` (no duplicates) and uses initial-sync semantics (read/unarchived),
so it never spams notifications. Gmail's legacy per-channel→mailbox
migration is extracted into `migrateLegacyPerChannelState()`; the watch is
then (re-)established by the shared recovery path.
Connector-only change (no changeset). gmail 47 tests, outlook-mail 35
tests, both `plot lint` clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit 3fb91d9 into mainJun 22, 2026
1 check passed
@KrisBraun
KrisBraun deleted the fix/mail-upgrade-recovery branch June 22, 2026 22:06
Sign up for freeto 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

@KrisBraun
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(gmail,outlook-mail): durable recovery for stranded mailbox watches on upgrade by KrisBraun · Pull Request #217 · plotday/plot · GitHub
Skip to content

fix(gmail,outlook-mail): durable recovery for stranded mailbox watches on upgrade - #217

Merged
KrisBraun merged 1 commit into
mainfrom
fix/mail-upgrade-recovery
Jun 22, 2026
Merged

fix(gmail,outlook-mail): durable recovery for stranded mailbox watches on upgrade#217
KrisBraun merged 1 commit into
mainfrom
fix/mail-upgrade-recovery

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Problem

A mail connection (Gmail or Outlook) could be left permanently and silently dead with no self-recovery on deploy — the failure mode the durable-recurring-tasks work (#215) set out to eliminate, but with a gap.

upgrade() only re-asserted the recurring self-heal/renewal tasks when the watch/subscription sentinel (mailbox_webhook / mailbox_subscription) was already stored, and the cron maintenance sweep is gated on a recurring_meta.ever marker. Two stranded states slip through both recovery paths:

  1. The sentinel never persisted — a prior setupWatch() / setupMailboxSubscription() threw before its set() (e.g. Gmail's 400 "Only one user push notification client allowed per developer"). There was nothing to re-assert, and because scheduleRecurring never ran, the sweep's ever marker was never set either.
  2. The watch/subscription expired while the self-heal/renewal chain was dead.

Either way the connection stayed dead until the user manually re-enabled a channel.

Real-world hit

A Gmail "Personal" connection stopped ingesting after a June reconnect and produced zero mail for ~18 days across multiple deploys, while the same user's other Gmail connection (continuously healthy) was unaffected. The recovery paths shipped in #215 couldn't reach it because its watch state had never persisted / had expired with a dead chain.

Fix

upgrade() now always runs a recoverMailboxDelivery() backstop for any instance with enabled channels:

  • Healthy watch (sentinel present and unexpired) → only re-assert the recurring tasks (unchanged behavior).
  • Missing or expired watch → re-establish it and re-walk every enabled label/folder, so mail that accumulated while delivery was dead is backfilled. The backfill upserts by source (no duplicates) and uses initial-sync semantics (unread:false, archived:false), so it never spams notifications.

Gmail's legacy per-channel→mailbox migration is extracted into migrateLegacyPerChannelState(); the watch is then (re-)established by the shared recovery path (removing the old if (migratedAny) setupMailboxWebhook() tail).

Both connectors get the same shape (recoverMailboxDelivery + requeueInitialSync), since Outlook-mail had the identical gating gap (if (existing?.subscriptionId) re-assert, no else).

Why this fixes it for all affected users

It runs on every deploy for every non-archived instance (the deploy "Upgrading active twists" phase). Any Gmail/Outlook connection currently stranded with a missing/expired watch will re-establish live delivery and backfill its gap on the next deploy — no manual re-enable required.

Testing

  • Gmail: 47 tests pass (4 new recoverMailboxDelivery cases: stranded, expired, healthy, no-channels)
  • Outlook-mail: 35 tests pass (4 new equivalent cases)
  • Both pnpm build (tsc) + plot lint clean
  • Connector-only change → no changeset (per public/AGENTS.md)

Deploy / verify notes

  • This is a public-submodule connector change. To reach production it needs the superproject (plotday/core) submodule pointer bumped to the merge commit, then a deploy (the deploy-connectors (gmail/outlook-mail) jobs).
  • A connection that already self-healed its live watch (sentinel now fresh) takes the healthy path and is not re-backfilled; recovering its historical gap still needs a one-time channel re-enable (which triggers onChannelEnabled's full backfill). The auto-backfill here targets connections still stranded at deploy time.

🤖 Generated with Claude Code

…s on upgrade
A mail connection could be left permanently and silently dead with no
self-recovery on deploy. `upgrade()` only re-asserted the recurring
self-heal/renewal tasks when the watch/subscription sentinel
(`mailbox_webhook` / `mailbox_subscription`) was already stored, and the
cron maintenance sweep is gated on a `recurring_meta.ever` marker. Two
stranded states slipped through both:
1. The sentinel never persisted — a prior `setupWatch()` /
`setupMailboxSubscription()` threw before its `set()` (e.g. Gmail's
"Only one user push notification client allowed per developer" 400),
so there was nothing to re-assert AND `scheduleRecurring` never ran,
so the sweep's `ever` marker was never set either.
2. The watch/subscription expired while the self-heal chain was dead.
Either way the connection stayed dead until the user manually re-enabled
a channel. Real-world hit: a Gmail "Personal" connection stopped
ingesting after a reconnect and never recovered across deploys.
Fix: `upgrade()` now always runs a `recoverMailboxDelivery()` backstop for
any instance with enabled channels. A healthy watch only re-asserts the
recurring tasks (unchanged behavior); a missing OR expired one is
re-established AND every enabled label/folder is re-walked, so mail that
accumulated while delivery was dead is backfilled. The backfill upserts by
`source` (no duplicates) and uses initial-sync semantics (read/unarchived),
so it never spams notifications. Gmail's legacy per-channel→mailbox
migration is extracted into `migrateLegacyPerChannelState()`; the watch is
then (re-)established by the shared recovery path.
Connector-only change (no changeset). gmail 47 tests, outlook-mail 35
tests, both `plot lint` clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit 3fb91d9 into mainJun 22, 2026
1 check passed
@KrisBraun
KrisBraun deleted the fix/mail-upgrade-recovery branch June 22, 2026 22:06
Sign up for freeto 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

@KrisBraun
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(gmail,outlook-mail): durable recovery for stranded mailbox watches on upgrade by KrisBraun · Pull Request #217 · plotday/plot · GitHub
Skip to content

fix(gmail,outlook-mail): durable recovery for stranded mailbox watches on upgrade - #217

Merged
KrisBraun merged 1 commit into
mainfrom
fix/mail-upgrade-recovery
Jun 22, 2026
Merged

fix(gmail,outlook-mail): durable recovery for stranded mailbox watches on upgrade#217
KrisBraun merged 1 commit into
mainfrom
fix/mail-upgrade-recovery

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Problem

A mail connection (Gmail or Outlook) could be left permanently and silently dead with no self-recovery on deploy — the failure mode the durable-recurring-tasks work (#215) set out to eliminate, but with a gap.

upgrade() only re-asserted the recurring self-heal/renewal tasks when the watch/subscription sentinel (mailbox_webhook / mailbox_subscription) was already stored, and the cron maintenance sweep is gated on a recurring_meta.ever marker. Two stranded states slip through both recovery paths:

  1. The sentinel never persisted — a prior setupWatch() / setupMailboxSubscription() threw before its set() (e.g. Gmail's 400 "Only one user push notification client allowed per developer"). There was nothing to re-assert, and because scheduleRecurring never ran, the sweep's ever marker was never set either.
  2. The watch/subscription expired while the self-heal/renewal chain was dead.

Either way the connection stayed dead until the user manually re-enabled a channel.

Real-world hit

A Gmail "Personal" connection stopped ingesting after a June reconnect and produced zero mail for ~18 days across multiple deploys, while the same user's other Gmail connection (continuously healthy) was unaffected. The recovery paths shipped in #215 couldn't reach it because its watch state had never persisted / had expired with a dead chain.

Fix

upgrade() now always runs a recoverMailboxDelivery() backstop for any instance with enabled channels:

  • Healthy watch (sentinel present and unexpired) → only re-assert the recurring tasks (unchanged behavior).
  • Missing or expired watch → re-establish it and re-walk every enabled label/folder, so mail that accumulated while delivery was dead is backfilled. The backfill upserts by source (no duplicates) and uses initial-sync semantics (unread:false, archived:false), so it never spams notifications.

Gmail's legacy per-channel→mailbox migration is extracted into migrateLegacyPerChannelState(); the watch is then (re-)established by the shared recovery path (removing the old if (migratedAny) setupMailboxWebhook() tail).

Both connectors get the same shape (recoverMailboxDelivery + requeueInitialSync), since Outlook-mail had the identical gating gap (if (existing?.subscriptionId) re-assert, no else).

Why this fixes it for all affected users

It runs on every deploy for every non-archived instance (the deploy "Upgrading active twists" phase). Any Gmail/Outlook connection currently stranded with a missing/expired watch will re-establish live delivery and backfill its gap on the next deploy — no manual re-enable required.

Testing

  • Gmail: 47 tests pass (4 new recoverMailboxDelivery cases: stranded, expired, healthy, no-channels)
  • Outlook-mail: 35 tests pass (4 new equivalent cases)
  • Both pnpm build (tsc) + plot lint clean
  • Connector-only change → no changeset (per public/AGENTS.md)

Deploy / verify notes

  • This is a public-submodule connector change. To reach production it needs the superproject (plotday/core) submodule pointer bumped to the merge commit, then a deploy (the deploy-connectors (gmail/outlook-mail) jobs).
  • A connection that already self-healed its live watch (sentinel now fresh) takes the healthy path and is not re-backfilled; recovering its historical gap still needs a one-time channel re-enable (which triggers onChannelEnabled's full backfill). The auto-backfill here targets connections still stranded at deploy time.

🤖 Generated with Claude Code

…s on upgrade
A mail connection could be left permanently and silently dead with no
self-recovery on deploy. `upgrade()` only re-asserted the recurring
self-heal/renewal tasks when the watch/subscription sentinel
(`mailbox_webhook` / `mailbox_subscription`) was already stored, and the
cron maintenance sweep is gated on a `recurring_meta.ever` marker. Two
stranded states slipped through both:
1. The sentinel never persisted — a prior `setupWatch()` /
`setupMailboxSubscription()` threw before its `set()` (e.g. Gmail's
"Only one user push notification client allowed per developer" 400),
so there was nothing to re-assert AND `scheduleRecurring` never ran,
so the sweep's `ever` marker was never set either.
2. The watch/subscription expired while the self-heal chain was dead.
Either way the connection stayed dead until the user manually re-enabled
a channel. Real-world hit: a Gmail "Personal" connection stopped
ingesting after a reconnect and never recovered across deploys.
Fix: `upgrade()` now always runs a `recoverMailboxDelivery()` backstop for
any instance with enabled channels. A healthy watch only re-asserts the
recurring tasks (unchanged behavior); a missing OR expired one is
re-established AND every enabled label/folder is re-walked, so mail that
accumulated while delivery was dead is backfilled. The backfill upserts by
`source` (no duplicates) and uses initial-sync semantics (read/unarchived),
so it never spams notifications. Gmail's legacy per-channel→mailbox
migration is extracted into `migrateLegacyPerChannelState()`; the watch is
then (re-)established by the shared recovery path.
Connector-only change (no changeset). gmail 47 tests, outlook-mail 35
tests, both `plot lint` clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit 3fb91d9 into mainJun 22, 2026
1 check passed
@KrisBraun
KrisBraun deleted the fix/mail-upgrade-recovery branch June 22, 2026 22:06
Sign up for freeto 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

@KrisBraun
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(gmail,outlook-mail): durable recovery for stranded mailbox watches on upgrade by KrisBraun · Pull Request #217 · plotday/plot · GitHub
Skip to content

fix(gmail,outlook-mail): durable recovery for stranded mailbox watches on upgrade - #217

Merged
KrisBraun merged 1 commit into
mainfrom
fix/mail-upgrade-recovery
Jun 22, 2026
Merged

fix(gmail,outlook-mail): durable recovery for stranded mailbox watches on upgrade#217
KrisBraun merged 1 commit into
mainfrom
fix/mail-upgrade-recovery

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Problem

A mail connection (Gmail or Outlook) could be left permanently and silently dead with no self-recovery on deploy — the failure mode the durable-recurring-tasks work (#215) set out to eliminate, but with a gap.

upgrade() only re-asserted the recurring self-heal/renewal tasks when the watch/subscription sentinel (mailbox_webhook / mailbox_subscription) was already stored, and the cron maintenance sweep is gated on a recurring_meta.ever marker. Two stranded states slip through both recovery paths:

  1. The sentinel never persisted — a prior setupWatch() / setupMailboxSubscription() threw before its set() (e.g. Gmail's 400 "Only one user push notification client allowed per developer"). There was nothing to re-assert, and because scheduleRecurring never ran, the sweep's ever marker was never set either.
  2. The watch/subscription expired while the self-heal/renewal chain was dead.

Either way the connection stayed dead until the user manually re-enabled a channel.

Real-world hit

A Gmail "Personal" connection stopped ingesting after a June reconnect and produced zero mail for ~18 days across multiple deploys, while the same user's other Gmail connection (continuously healthy) was unaffected. The recovery paths shipped in #215 couldn't reach it because its watch state had never persisted / had expired with a dead chain.

Fix

upgrade() now always runs a recoverMailboxDelivery() backstop for any instance with enabled channels:

  • Healthy watch (sentinel present and unexpired) → only re-assert the recurring tasks (unchanged behavior).
  • Missing or expired watch → re-establish it and re-walk every enabled label/folder, so mail that accumulated while delivery was dead is backfilled. The backfill upserts by source (no duplicates) and uses initial-sync semantics (unread:false, archived:false), so it never spams notifications.

Gmail's legacy per-channel→mailbox migration is extracted into migrateLegacyPerChannelState(); the watch is then (re-)established by the shared recovery path (removing the old if (migratedAny) setupMailboxWebhook() tail).

Both connectors get the same shape (recoverMailboxDelivery + requeueInitialSync), since Outlook-mail had the identical gating gap (if (existing?.subscriptionId) re-assert, no else).

Why this fixes it for all affected users

It runs on every deploy for every non-archived instance (the deploy "Upgrading active twists" phase). Any Gmail/Outlook connection currently stranded with a missing/expired watch will re-establish live delivery and backfill its gap on the next deploy — no manual re-enable required.

Testing

  • Gmail: 47 tests pass (4 new recoverMailboxDelivery cases: stranded, expired, healthy, no-channels)
  • Outlook-mail: 35 tests pass (4 new equivalent cases)
  • Both pnpm build (tsc) + plot lint clean
  • Connector-only change → no changeset (per public/AGENTS.md)

Deploy / verify notes

  • This is a public-submodule connector change. To reach production it needs the superproject (plotday/core) submodule pointer bumped to the merge commit, then a deploy (the deploy-connectors (gmail/outlook-mail) jobs).
  • A connection that already self-healed its live watch (sentinel now fresh) takes the healthy path and is not re-backfilled; recovering its historical gap still needs a one-time channel re-enable (which triggers onChannelEnabled's full backfill). The auto-backfill here targets connections still stranded at deploy time.

🤖 Generated with Claude Code

…s on upgrade
A mail connection could be left permanently and silently dead with no
self-recovery on deploy. `upgrade()` only re-asserted the recurring
self-heal/renewal tasks when the watch/subscription sentinel
(`mailbox_webhook` / `mailbox_subscription`) was already stored, and the
cron maintenance sweep is gated on a `recurring_meta.ever` marker. Two
stranded states slipped through both:
1. The sentinel never persisted — a prior `setupWatch()` /
`setupMailboxSubscription()` threw before its `set()` (e.g. Gmail's
"Only one user push notification client allowed per developer" 400),
so there was nothing to re-assert AND `scheduleRecurring` never ran,
so the sweep's `ever` marker was never set either.
2. The watch/subscription expired while the self-heal chain was dead.
Either way the connection stayed dead until the user manually re-enabled
a channel. Real-world hit: a Gmail "Personal" connection stopped
ingesting after a reconnect and never recovered across deploys.
Fix: `upgrade()` now always runs a `recoverMailboxDelivery()` backstop for
any instance with enabled channels. A healthy watch only re-asserts the
recurring tasks (unchanged behavior); a missing OR expired one is
re-established AND every enabled label/folder is re-walked, so mail that
accumulated while delivery was dead is backfilled. The backfill upserts by
`source` (no duplicates) and uses initial-sync semantics (read/unarchived),
so it never spams notifications. Gmail's legacy per-channel→mailbox
migration is extracted into `migrateLegacyPerChannelState()`; the watch is
then (re-)established by the shared recovery path.
Connector-only change (no changeset). gmail 47 tests, outlook-mail 35
tests, both `plot lint` clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit 3fb91d9 into mainJun 22, 2026
1 check passed
@KrisBraun
KrisBraun deleted the fix/mail-upgrade-recovery branch June 22, 2026 22:06
Sign up for freeto 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

@KrisBraun