Keyed singleton scheduled tasks (scheduleTask) + migrate connectors off leak-prone renewal pattern - #205

Merged
KrisBraun merged 2 commits into
mainfrom
keyed-scheduled-tasks
Jun 17, 2026
Merged

Keyed singleton scheduled tasks (scheduleTask) + migrate connectors off leak-prone renewal pattern#205
KrisBraun merged 2 commits into
mainfrom
keyed-scheduled-tasks

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Why

A connector's self-renewing scheduled task (watch/webhook renewal, polling, daily digest) was hand-managed with runTask({ runAt }) + a stored token + cancelTask() before re-scheduling. That pattern is easy to get wrong: a renewal callback that re-schedules itself, plus any extra entry into the setup path (an onChannelEnabled re-dispatch, a re-init, a race), leaks parallel self-perpetuating task chains that accumulate without bound and eventually trip the runtime execution quota.

This is exactly what auto-suspended the Google Drive connector (≈350 → 3351 worker invocations/day, near-zero DB writes — a steady 24/7 background climb from accumulated renewal chains). google-calendar had the identical latent bug, and several pollers (airtable, google-tasks) had no token/cancel at all, so they could fork unboundedly with no way to stop them.

The leaky pattern was even documented as the recommended approach in connectors/AGENTS.md, so connectors copied it faithfully.

What

New SDK primitive (@plotday/twister) — makes the leak impossible by construction:

  • this.scheduleTask(key, callback, { runAt }) — schedules a singleton task per key; re-scheduling under the same key atomically cancels-and-replaces any pending task. At most one live scheduled task per key.
  • this.cancelScheduledTask(key) — tears it down (no-op if none / already ran).

Exposed on the Tasks tool interface and as Tool/Twist helpers. Changeset included (minor). The runtime side (the task_key column + atomic replace-on-create in CallbacksState, and the Tasks-tool wiring) ships in the matching core PR, which also bumps the submodule pointer to this branch.

Docs — fixed the connectors/AGENTS.md watch-renewal snippet (it was teaching the leaky pattern), added a checklist item + pitfall, and updated twister/docs/RUNTIME.md (periodic-work example) and TOOLS_GUIDE.md (scheduling section) to steer recurring tasks to scheduleTask.

Connector migration — moved every connector with a self-renewing/recurring scheduled task onto the keyed primitive:

ClassConnectors
Watch/subscription renewalgoogle-drive, google-calendar, google-chat, ms-teams, jira, outlook-calendar
Periodic pollairtable (+ webhook renewal), google-tasks, asana, apple-calendar
Daily digestslack (members + custom-emoji), ms-teams, google-chat

Each chain is keyed per resource (watch-renewal:<id>, poll:<id>, members-sync:<channelId>, …) so re-scheduling replaces rather than forks, and disable paths cancel by key. Pollers that previously had no teardown now cancel on disable. google-drive/google-calendar additionally stop the prior provider watch before re-creating it (so a redundant setup can't orphan a still-firing watch). Added a google-drive regression test.

Left as-is (not affected): gmail and outlook-mail already cancel-before-reschedule (no leak) and keep their self-heal logic; github's runTask override is a pass-through; linkedin's retry terminates.

Backwards compatibility

scheduleTask/taskKey are purely additive. Any pre-deploy in-flight (un-keyed) renewal task fires once more after deploy, then the chain becomes keyed — no lingering leak. Suspended connectors un-suspend on their next deploy.

Verification

  • All 13 touched connectors build + lint clean; no leftover *_renewal_task/poll_task/cancelTask references.
  • twister builds; changeset validates.
  • Runtime behavior (atomic replace / independent keys / cancel) is covered by an integration test in the core PR (5 tests), plus a google-drive connector regression test here.

🤖 Generated with Claude Code

KrisBraunand others added 2 commits June 17, 2026 00:44
…asks
Recurring/self-renewing jobs (watch & webhook renewals, polling, deferred
cleanup) were hand-managed with runTask({ runAt }) + a stored token +
cancelTask() before re-scheduling. That pattern is easy to get wrong: a
renewal callback that re-schedules itself, plus any extra entry into the
setup path (an onChannelEnabled re-dispatch, a re-init, a race), leaks
parallel self-perpetuating task chains that accumulate without bound and
eventually trip the runtime execution quota.
Add a keyed primitive that makes the leak impossible by construction:
- this.scheduleTask(key, callback, { runAt }) — schedules a singleton task
per key; re-scheduling under the same key atomically cancels-and-replaces
any pending task. At most one live scheduled task per key.
- this.cancelScheduledTask(key) — tears it down (no-op if none/already ran).
Exposed on the Tasks tool interface and as helpers on Tool/Twist. Docs
(RUNTIME.md periodic-work guidance, TOOLS_GUIDE.md scheduling section)
updated to steer recurring tasks to scheduleTask.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed chains)
Replace the leak-prone runTask({ runAt }) + stored-token renewal/poll
pattern with the keyed singleton scheduleTask/cancelScheduledTask primitive
across every connector that runs a self-renewing or recurring scheduled task:
- Watch/subscription renewals: google-drive, google-calendar, google-chat,
ms-teams, jira, outlook-calendar. (google-drive & google-calendar also now
stop the prior provider watch before re-creating it, so a redundant setup
can't orphan a still-firing watch.)
- Periodic polls: airtable (poll + webhook renewal), google-tasks, asana,
apple-calendar — several of which previously had no token/cancel at all and
could fork unboundedly.
- Daily digests: slack (members + custom-emoji), and the per-channel daily
passes in ms-teams/google-chat.
Each chain is keyed per resource (e.g. watch-renewal:<id>, poll:<id>) so
re-scheduling replaces rather than forks, and disable paths cancel by key.
gmail and outlook-mail already cancel-before-reschedule (no leak) and keep
their existing self-heal logic. Adds a google-drive regression test.
Fixes the Google Drive connector auto-suspension (500+ worker invocations/24h
from accumulated renewal chains).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit 59cb509 into mainJun 17, 2026
1 check passed
@KrisBraun
KrisBraun deleted the keyed-scheduled-tasks branch June 17, 2026 13:05
@plot-app-builderplot-app-builderBot mentioned this pull request Jun 17, 2026
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)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n 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;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Keyed singleton scheduled tasks (scheduleTask) + migrate connectors off leak-prone renewal pattern - #205

Merged
KrisBraun merged 2 commits into
mainfrom
keyed-scheduled-tasks
Jun 17, 2026
Merged

Keyed singleton scheduled tasks (scheduleTask) + migrate connectors off leak-prone renewal pattern#205
KrisBraun merged 2 commits into
mainfrom
keyed-scheduled-tasks

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Why

A connector's self-renewing scheduled task (watch/webhook renewal, polling, daily digest) was hand-managed with runTask({ runAt }) + a stored token + cancelTask() before re-scheduling. That pattern is easy to get wrong: a renewal callback that re-schedules itself, plus any extra entry into the setup path (an onChannelEnabled re-dispatch, a re-init, a race), leaks parallel self-perpetuating task chains that accumulate without bound and eventually trip the runtime execution quota.

This is exactly what auto-suspended the Google Drive connector (≈350 → 3351 worker invocations/day, near-zero DB writes — a steady 24/7 background climb from accumulated renewal chains). google-calendar had the identical latent bug, and several pollers (airtable, google-tasks) had no token/cancel at all, so they could fork unboundedly with no way to stop them.

The leaky pattern was even documented as the recommended approach in connectors/AGENTS.md, so connectors copied it faithfully.

What

New SDK primitive (@plotday/twister) — makes the leak impossible by construction:

  • this.scheduleTask(key, callback, { runAt }) — schedules a singleton task per key; re-scheduling under the same key atomically cancels-and-replaces any pending task. At most one live scheduled task per key.
  • this.cancelScheduledTask(key) — tears it down (no-op if none / already ran).

Exposed on the Tasks tool interface and as Tool/Twist helpers. Changeset included (minor). The runtime side (the task_key column + atomic replace-on-create in CallbacksState, and the Tasks-tool wiring) ships in the matching core PR, which also bumps the submodule pointer to this branch.

Docs — fixed the connectors/AGENTS.md watch-renewal snippet (it was teaching the leaky pattern), added a checklist item + pitfall, and updated twister/docs/RUNTIME.md (periodic-work example) and TOOLS_GUIDE.md (scheduling section) to steer recurring tasks to scheduleTask.

Connector migration — moved every connector with a self-renewing/recurring scheduled task onto the keyed primitive:

ClassConnectors
Watch/subscription renewalgoogle-drive, google-calendar, google-chat, ms-teams, jira, outlook-calendar
Periodic pollairtable (+ webhook renewal), google-tasks, asana, apple-calendar
Daily digestslack (members + custom-emoji), ms-teams, google-chat

Each chain is keyed per resource (watch-renewal:<id>, poll:<id>, members-sync:<channelId>, …) so re-scheduling replaces rather than forks, and disable paths cancel by key. Pollers that previously had no teardown now cancel on disable. google-drive/google-calendar additionally stop the prior provider watch before re-creating it (so a redundant setup can't orphan a still-firing watch). Added a google-drive regression test.

Left as-is (not affected): gmail and outlook-mail already cancel-before-reschedule (no leak) and keep their self-heal logic; github's runTask override is a pass-through; linkedin's retry terminates.

Backwards compatibility

scheduleTask/taskKey are purely additive. Any pre-deploy in-flight (un-keyed) renewal task fires once more after deploy, then the chain becomes keyed — no lingering leak. Suspended connectors un-suspend on their next deploy.

Verification

  • All 13 touched connectors build + lint clean; no leftover *_renewal_task/poll_task/cancelTask references.
  • twister builds; changeset validates.
  • Runtime behavior (atomic replace / independent keys / cancel) is covered by an integration test in the core PR (5 tests), plus a google-drive connector regression test here.

🤖 Generated with Claude Code

KrisBraunand others added 2 commits June 17, 2026 00:44
…asks
Recurring/self-renewing jobs (watch & webhook renewals, polling, deferred
cleanup) were hand-managed with runTask({ runAt }) + a stored token +
cancelTask() before re-scheduling. That pattern is easy to get wrong: a
renewal callback that re-schedules itself, plus any extra entry into the
setup path (an onChannelEnabled re-dispatch, a re-init, a race), leaks
parallel self-perpetuating task chains that accumulate without bound and
eventually trip the runtime execution quota.
Add a keyed primitive that makes the leak impossible by construction:
- this.scheduleTask(key, callback, { runAt }) — schedules a singleton task
per key; re-scheduling under the same key atomically cancels-and-replaces
any pending task. At most one live scheduled task per key.
- this.cancelScheduledTask(key) — tears it down (no-op if none/already ran).
Exposed on the Tasks tool interface and as helpers on Tool/Twist. Docs
(RUNTIME.md periodic-work guidance, TOOLS_GUIDE.md scheduling section)
updated to steer recurring tasks to scheduleTask.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed chains)
Replace the leak-prone runTask({ runAt }) + stored-token renewal/poll
pattern with the keyed singleton scheduleTask/cancelScheduledTask primitive
across every connector that runs a self-renewing or recurring scheduled task:
- Watch/subscription renewals: google-drive, google-calendar, google-chat,
ms-teams, jira, outlook-calendar. (google-drive & google-calendar also now
stop the prior provider watch before re-creating it, so a redundant setup
can't orphan a still-firing watch.)
- Periodic polls: airtable (poll + webhook renewal), google-tasks, asana,
apple-calendar — several of which previously had no token/cancel at all and
could fork unboundedly.
- Daily digests: slack (members + custom-emoji), and the per-channel daily
passes in ms-teams/google-chat.
Each chain is keyed per resource (e.g. watch-renewal:<id>, poll:<id>) so
re-scheduling replaces rather than forks, and disable paths cancel by key.
gmail and outlook-mail already cancel-before-reschedule (no leak) and keep
their existing self-heal logic. Adds a google-drive regression test.
Fixes the Google Drive connector auto-suspension (500+ worker invocations/24h
from accumulated renewal chains).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit 59cb509 into mainJun 17, 2026
1 check passed
@KrisBraun
KrisBraun deleted the keyed-scheduled-tasks branch June 17, 2026 13:05
@plot-app-builderplot-app-builderBot mentioned this pull request Jun 17, 2026
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)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Keyed singleton scheduled tasks (scheduleTask) + migrate connectors off leak-prone renewal pattern - #205

Merged
KrisBraun merged 2 commits into
mainfrom
keyed-scheduled-tasks
Jun 17, 2026
Merged

Keyed singleton scheduled tasks (scheduleTask) + migrate connectors off leak-prone renewal pattern#205
KrisBraun merged 2 commits into
mainfrom
keyed-scheduled-tasks

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Why

A connector's self-renewing scheduled task (watch/webhook renewal, polling, daily digest) was hand-managed with runTask({ runAt }) + a stored token + cancelTask() before re-scheduling. That pattern is easy to get wrong: a renewal callback that re-schedules itself, plus any extra entry into the setup path (an onChannelEnabled re-dispatch, a re-init, a race), leaks parallel self-perpetuating task chains that accumulate without bound and eventually trip the runtime execution quota.

This is exactly what auto-suspended the Google Drive connector (≈350 → 3351 worker invocations/day, near-zero DB writes — a steady 24/7 background climb from accumulated renewal chains). google-calendar had the identical latent bug, and several pollers (airtable, google-tasks) had no token/cancel at all, so they could fork unboundedly with no way to stop them.

The leaky pattern was even documented as the recommended approach in connectors/AGENTS.md, so connectors copied it faithfully.

What

New SDK primitive (@plotday/twister) — makes the leak impossible by construction:

  • this.scheduleTask(key, callback, { runAt }) — schedules a singleton task per key; re-scheduling under the same key atomically cancels-and-replaces any pending task. At most one live scheduled task per key.
  • this.cancelScheduledTask(key) — tears it down (no-op if none / already ran).

Exposed on the Tasks tool interface and as Tool/Twist helpers. Changeset included (minor). The runtime side (the task_key column + atomic replace-on-create in CallbacksState, and the Tasks-tool wiring) ships in the matching core PR, which also bumps the submodule pointer to this branch.

Docs — fixed the connectors/AGENTS.md watch-renewal snippet (it was teaching the leaky pattern), added a checklist item + pitfall, and updated twister/docs/RUNTIME.md (periodic-work example) and TOOLS_GUIDE.md (scheduling section) to steer recurring tasks to scheduleTask.

Connector migration — moved every connector with a self-renewing/recurring scheduled task onto the keyed primitive:

ClassConnectors
Watch/subscription renewalgoogle-drive, google-calendar, google-chat, ms-teams, jira, outlook-calendar
Periodic pollairtable (+ webhook renewal), google-tasks, asana, apple-calendar
Daily digestslack (members + custom-emoji), ms-teams, google-chat

Each chain is keyed per resource (watch-renewal:<id>, poll:<id>, members-sync:<channelId>, …) so re-scheduling replaces rather than forks, and disable paths cancel by key. Pollers that previously had no teardown now cancel on disable. google-drive/google-calendar additionally stop the prior provider watch before re-creating it (so a redundant setup can't orphan a still-firing watch). Added a google-drive regression test.

Left as-is (not affected): gmail and outlook-mail already cancel-before-reschedule (no leak) and keep their self-heal logic; github's runTask override is a pass-through; linkedin's retry terminates.

Backwards compatibility

scheduleTask/taskKey are purely additive. Any pre-deploy in-flight (un-keyed) renewal task fires once more after deploy, then the chain becomes keyed — no lingering leak. Suspended connectors un-suspend on their next deploy.

Verification

  • All 13 touched connectors build + lint clean; no leftover *_renewal_task/poll_task/cancelTask references.
  • twister builds; changeset validates.
  • Runtime behavior (atomic replace / independent keys / cancel) is covered by an integration test in the core PR (5 tests), plus a google-drive connector regression test here.

🤖 Generated with Claude Code

KrisBraunand others added 2 commits June 17, 2026 00:44
…asks
Recurring/self-renewing jobs (watch & webhook renewals, polling, deferred
cleanup) were hand-managed with runTask({ runAt }) + a stored token +
cancelTask() before re-scheduling. That pattern is easy to get wrong: a
renewal callback that re-schedules itself, plus any extra entry into the
setup path (an onChannelEnabled re-dispatch, a re-init, a race), leaks
parallel self-perpetuating task chains that accumulate without bound and
eventually trip the runtime execution quota.
Add a keyed primitive that makes the leak impossible by construction:
- this.scheduleTask(key, callback, { runAt }) — schedules a singleton task
per key; re-scheduling under the same key atomically cancels-and-replaces
any pending task. At most one live scheduled task per key.
- this.cancelScheduledTask(key) — tears it down (no-op if none/already ran).
Exposed on the Tasks tool interface and as helpers on Tool/Twist. Docs
(RUNTIME.md periodic-work guidance, TOOLS_GUIDE.md scheduling section)
updated to steer recurring tasks to scheduleTask.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed chains)
Replace the leak-prone runTask({ runAt }) + stored-token renewal/poll
pattern with the keyed singleton scheduleTask/cancelScheduledTask primitive
across every connector that runs a self-renewing or recurring scheduled task:
- Watch/subscription renewals: google-drive, google-calendar, google-chat,
ms-teams, jira, outlook-calendar. (google-drive & google-calendar also now
stop the prior provider watch before re-creating it, so a redundant setup
can't orphan a still-firing watch.)
- Periodic polls: airtable (poll + webhook renewal), google-tasks, asana,
apple-calendar — several of which previously had no token/cancel at all and
could fork unboundedly.
- Daily digests: slack (members + custom-emoji), and the per-channel daily
passes in ms-teams/google-chat.
Each chain is keyed per resource (e.g. watch-renewal:<id>, poll:<id>) so
re-scheduling replaces rather than forks, and disable paths cancel by key.
gmail and outlook-mail already cancel-before-reschedule (no leak) and keep
their existing self-heal logic. Adds a google-drive regression test.
Fixes the Google Drive connector auto-suspension (500+ worker invocations/24h
from accumulated renewal chains).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit 59cb509 into mainJun 17, 2026
1 check passed
@KrisBraun
KrisBraun deleted the keyed-scheduled-tasks branch June 17, 2026 13:05
@plot-app-builderplot-app-builderBot mentioned this pull request Jun 17, 2026
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)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Keyed singleton scheduled tasks (scheduleTask) + migrate connectors off leak-prone renewal pattern - #205

Merged
KrisBraun merged 2 commits into
mainfrom
keyed-scheduled-tasks
Jun 17, 2026
Merged

Keyed singleton scheduled tasks (scheduleTask) + migrate connectors off leak-prone renewal pattern#205
KrisBraun merged 2 commits into
mainfrom
keyed-scheduled-tasks

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Why

A connector's self-renewing scheduled task (watch/webhook renewal, polling, daily digest) was hand-managed with runTask({ runAt }) + a stored token + cancelTask() before re-scheduling. That pattern is easy to get wrong: a renewal callback that re-schedules itself, plus any extra entry into the setup path (an onChannelEnabled re-dispatch, a re-init, a race), leaks parallel self-perpetuating task chains that accumulate without bound and eventually trip the runtime execution quota.

This is exactly what auto-suspended the Google Drive connector (≈350 → 3351 worker invocations/day, near-zero DB writes — a steady 24/7 background climb from accumulated renewal chains). google-calendar had the identical latent bug, and several pollers (airtable, google-tasks) had no token/cancel at all, so they could fork unboundedly with no way to stop them.

The leaky pattern was even documented as the recommended approach in connectors/AGENTS.md, so connectors copied it faithfully.

What

New SDK primitive (@plotday/twister) — makes the leak impossible by construction:

  • this.scheduleTask(key, callback, { runAt }) — schedules a singleton task per key; re-scheduling under the same key atomically cancels-and-replaces any pending task. At most one live scheduled task per key.
  • this.cancelScheduledTask(key) — tears it down (no-op if none / already ran).

Exposed on the Tasks tool interface and as Tool/Twist helpers. Changeset included (minor). The runtime side (the task_key column + atomic replace-on-create in CallbacksState, and the Tasks-tool wiring) ships in the matching core PR, which also bumps the submodule pointer to this branch.

Docs — fixed the connectors/AGENTS.md watch-renewal snippet (it was teaching the leaky pattern), added a checklist item + pitfall, and updated twister/docs/RUNTIME.md (periodic-work example) and TOOLS_GUIDE.md (scheduling section) to steer recurring tasks to scheduleTask.

Connector migration — moved every connector with a self-renewing/recurring scheduled task onto the keyed primitive:

ClassConnectors
Watch/subscription renewalgoogle-drive, google-calendar, google-chat, ms-teams, jira, outlook-calendar
Periodic pollairtable (+ webhook renewal), google-tasks, asana, apple-calendar
Daily digestslack (members + custom-emoji), ms-teams, google-chat

Each chain is keyed per resource (watch-renewal:<id>, poll:<id>, members-sync:<channelId>, …) so re-scheduling replaces rather than forks, and disable paths cancel by key. Pollers that previously had no teardown now cancel on disable. google-drive/google-calendar additionally stop the prior provider watch before re-creating it (so a redundant setup can't orphan a still-firing watch). Added a google-drive regression test.

Left as-is (not affected): gmail and outlook-mail already cancel-before-reschedule (no leak) and keep their self-heal logic; github's runTask override is a pass-through; linkedin's retry terminates.

Backwards compatibility

scheduleTask/taskKey are purely additive. Any pre-deploy in-flight (un-keyed) renewal task fires once more after deploy, then the chain becomes keyed — no lingering leak. Suspended connectors un-suspend on their next deploy.

Verification

  • All 13 touched connectors build + lint clean; no leftover *_renewal_task/poll_task/cancelTask references.
  • twister builds; changeset validates.
  • Runtime behavior (atomic replace / independent keys / cancel) is covered by an integration test in the core PR (5 tests), plus a google-drive connector regression test here.

🤖 Generated with Claude Code

KrisBraunand others added 2 commits June 17, 2026 00:44
…asks
Recurring/self-renewing jobs (watch & webhook renewals, polling, deferred
cleanup) were hand-managed with runTask({ runAt }) + a stored token +
cancelTask() before re-scheduling. That pattern is easy to get wrong: a
renewal callback that re-schedules itself, plus any extra entry into the
setup path (an onChannelEnabled re-dispatch, a re-init, a race), leaks
parallel self-perpetuating task chains that accumulate without bound and
eventually trip the runtime execution quota.
Add a keyed primitive that makes the leak impossible by construction:
- this.scheduleTask(key, callback, { runAt }) — schedules a singleton task
per key; re-scheduling under the same key atomically cancels-and-replaces
any pending task. At most one live scheduled task per key.
- this.cancelScheduledTask(key) — tears it down (no-op if none/already ran).
Exposed on the Tasks tool interface and as helpers on Tool/Twist. Docs
(RUNTIME.md periodic-work guidance, TOOLS_GUIDE.md scheduling section)
updated to steer recurring tasks to scheduleTask.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed chains)
Replace the leak-prone runTask({ runAt }) + stored-token renewal/poll
pattern with the keyed singleton scheduleTask/cancelScheduledTask primitive
across every connector that runs a self-renewing or recurring scheduled task:
- Watch/subscription renewals: google-drive, google-calendar, google-chat,
ms-teams, jira, outlook-calendar. (google-drive & google-calendar also now
stop the prior provider watch before re-creating it, so a redundant setup
can't orphan a still-firing watch.)
- Periodic polls: airtable (poll + webhook renewal), google-tasks, asana,
apple-calendar — several of which previously had no token/cancel at all and
could fork unboundedly.
- Daily digests: slack (members + custom-emoji), and the per-channel daily
passes in ms-teams/google-chat.
Each chain is keyed per resource (e.g. watch-renewal:<id>, poll:<id>) so
re-scheduling replaces rather than forks, and disable paths cancel by key.
gmail and outlook-mail already cancel-before-reschedule (no leak) and keep
their existing self-heal logic. Adds a google-drive regression test.
Fixes the Google Drive connector auto-suspension (500+ worker invocations/24h
from accumulated renewal chains).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit 59cb509 into mainJun 17, 2026
1 check passed
@KrisBraun
KrisBraun deleted the keyed-scheduled-tasks branch June 17, 2026 13:05
@plot-app-builderplot-app-builderBot mentioned this pull request Jun 17, 2026
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)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Keyed singleton scheduled tasks (scheduleTask) + migrate connectors off leak-prone renewal pattern - #205

Merged
KrisBraun merged 2 commits into
mainfrom
keyed-scheduled-tasks
Jun 17, 2026
Merged

Keyed singleton scheduled tasks (scheduleTask) + migrate connectors off leak-prone renewal pattern#205
KrisBraun merged 2 commits into
mainfrom
keyed-scheduled-tasks

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Why

A connector's self-renewing scheduled task (watch/webhook renewal, polling, daily digest) was hand-managed with runTask({ runAt }) + a stored token + cancelTask() before re-scheduling. That pattern is easy to get wrong: a renewal callback that re-schedules itself, plus any extra entry into the setup path (an onChannelEnabled re-dispatch, a re-init, a race), leaks parallel self-perpetuating task chains that accumulate without bound and eventually trip the runtime execution quota.

This is exactly what auto-suspended the Google Drive connector (≈350 → 3351 worker invocations/day, near-zero DB writes — a steady 24/7 background climb from accumulated renewal chains). google-calendar had the identical latent bug, and several pollers (airtable, google-tasks) had no token/cancel at all, so they could fork unboundedly with no way to stop them.

The leaky pattern was even documented as the recommended approach in connectors/AGENTS.md, so connectors copied it faithfully.

What

New SDK primitive (@plotday/twister) — makes the leak impossible by construction:

  • this.scheduleTask(key, callback, { runAt }) — schedules a singleton task per key; re-scheduling under the same key atomically cancels-and-replaces any pending task. At most one live scheduled task per key.
  • this.cancelScheduledTask(key) — tears it down (no-op if none / already ran).

Exposed on the Tasks tool interface and as Tool/Twist helpers. Changeset included (minor). The runtime side (the task_key column + atomic replace-on-create in CallbacksState, and the Tasks-tool wiring) ships in the matching core PR, which also bumps the submodule pointer to this branch.

Docs — fixed the connectors/AGENTS.md watch-renewal snippet (it was teaching the leaky pattern), added a checklist item + pitfall, and updated twister/docs/RUNTIME.md (periodic-work example) and TOOLS_GUIDE.md (scheduling section) to steer recurring tasks to scheduleTask.

Connector migration — moved every connector with a self-renewing/recurring scheduled task onto the keyed primitive:

ClassConnectors
Watch/subscription renewalgoogle-drive, google-calendar, google-chat, ms-teams, jira, outlook-calendar
Periodic pollairtable (+ webhook renewal), google-tasks, asana, apple-calendar
Daily digestslack (members + custom-emoji), ms-teams, google-chat

Each chain is keyed per resource (watch-renewal:<id>, poll:<id>, members-sync:<channelId>, …) so re-scheduling replaces rather than forks, and disable paths cancel by key. Pollers that previously had no teardown now cancel on disable. google-drive/google-calendar additionally stop the prior provider watch before re-creating it (so a redundant setup can't orphan a still-firing watch). Added a google-drive regression test.

Left as-is (not affected): gmail and outlook-mail already cancel-before-reschedule (no leak) and keep their self-heal logic; github's runTask override is a pass-through; linkedin's retry terminates.

Backwards compatibility

scheduleTask/taskKey are purely additive. Any pre-deploy in-flight (un-keyed) renewal task fires once more after deploy, then the chain becomes keyed — no lingering leak. Suspended connectors un-suspend on their next deploy.

Verification

  • All 13 touched connectors build + lint clean; no leftover *_renewal_task/poll_task/cancelTask references.
  • twister builds; changeset validates.
  • Runtime behavior (atomic replace / independent keys / cancel) is covered by an integration test in the core PR (5 tests), plus a google-drive connector regression test here.

🤖 Generated with Claude Code

KrisBraunand others added 2 commits June 17, 2026 00:44
…asks
Recurring/self-renewing jobs (watch & webhook renewals, polling, deferred
cleanup) were hand-managed with runTask({ runAt }) + a stored token +
cancelTask() before re-scheduling. That pattern is easy to get wrong: a
renewal callback that re-schedules itself, plus any extra entry into the
setup path (an onChannelEnabled re-dispatch, a re-init, a race), leaks
parallel self-perpetuating task chains that accumulate without bound and
eventually trip the runtime execution quota.
Add a keyed primitive that makes the leak impossible by construction:
- this.scheduleTask(key, callback, { runAt }) — schedules a singleton task
per key; re-scheduling under the same key atomically cancels-and-replaces
any pending task. At most one live scheduled task per key.
- this.cancelScheduledTask(key) — tears it down (no-op if none/already ran).
Exposed on the Tasks tool interface and as helpers on Tool/Twist. Docs
(RUNTIME.md periodic-work guidance, TOOLS_GUIDE.md scheduling section)
updated to steer recurring tasks to scheduleTask.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed chains)
Replace the leak-prone runTask({ runAt }) + stored-token renewal/poll
pattern with the keyed singleton scheduleTask/cancelScheduledTask primitive
across every connector that runs a self-renewing or recurring scheduled task:
- Watch/subscription renewals: google-drive, google-calendar, google-chat,
ms-teams, jira, outlook-calendar. (google-drive & google-calendar also now
stop the prior provider watch before re-creating it, so a redundant setup
can't orphan a still-firing watch.)
- Periodic polls: airtable (poll + webhook renewal), google-tasks, asana,
apple-calendar — several of which previously had no token/cancel at all and
could fork unboundedly.
- Daily digests: slack (members + custom-emoji), and the per-channel daily
passes in ms-teams/google-chat.
Each chain is keyed per resource (e.g. watch-renewal:<id>, poll:<id>) so
re-scheduling replaces rather than forks, and disable paths cancel by key.
gmail and outlook-mail already cancel-before-reschedule (no leak) and keep
their existing self-heal logic. Adds a google-drive regression test.
Fixes the Google Drive connector auto-suspension (500+ worker invocations/24h
from accumulated renewal chains).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit 59cb509 into mainJun 17, 2026
1 check passed
@KrisBraun
KrisBraun deleted the keyed-scheduled-tasks branch June 17, 2026 13:05
@plot-app-builderplot-app-builderBot mentioned this pull request Jun 17, 2026
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)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Keyed singleton scheduled tasks (scheduleTask) + migrate connectors off leak-prone renewal pattern - #205

Merged
KrisBraun merged 2 commits into
mainfrom
keyed-scheduled-tasks
Jun 17, 2026
Merged

Keyed singleton scheduled tasks (scheduleTask) + migrate connectors off leak-prone renewal pattern#205
KrisBraun merged 2 commits into
mainfrom
keyed-scheduled-tasks

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Why

A connector's self-renewing scheduled task (watch/webhook renewal, polling, daily digest) was hand-managed with runTask({ runAt }) + a stored token + cancelTask() before re-scheduling. That pattern is easy to get wrong: a renewal callback that re-schedules itself, plus any extra entry into the setup path (an onChannelEnabled re-dispatch, a re-init, a race), leaks parallel self-perpetuating task chains that accumulate without bound and eventually trip the runtime execution quota.

This is exactly what auto-suspended the Google Drive connector (≈350 → 3351 worker invocations/day, near-zero DB writes — a steady 24/7 background climb from accumulated renewal chains). google-calendar had the identical latent bug, and several pollers (airtable, google-tasks) had no token/cancel at all, so they could fork unboundedly with no way to stop them.

The leaky pattern was even documented as the recommended approach in connectors/AGENTS.md, so connectors copied it faithfully.

What

New SDK primitive (@plotday/twister) — makes the leak impossible by construction:

  • this.scheduleTask(key, callback, { runAt }) — schedules a singleton task per key; re-scheduling under the same key atomically cancels-and-replaces any pending task. At most one live scheduled task per key.
  • this.cancelScheduledTask(key) — tears it down (no-op if none / already ran).

Exposed on the Tasks tool interface and as Tool/Twist helpers. Changeset included (minor). The runtime side (the task_key column + atomic replace-on-create in CallbacksState, and the Tasks-tool wiring) ships in the matching core PR, which also bumps the submodule pointer to this branch.

Docs — fixed the connectors/AGENTS.md watch-renewal snippet (it was teaching the leaky pattern), added a checklist item + pitfall, and updated twister/docs/RUNTIME.md (periodic-work example) and TOOLS_GUIDE.md (scheduling section) to steer recurring tasks to scheduleTask.

Connector migration — moved every connector with a self-renewing/recurring scheduled task onto the keyed primitive:

ClassConnectors
Watch/subscription renewalgoogle-drive, google-calendar, google-chat, ms-teams, jira, outlook-calendar
Periodic pollairtable (+ webhook renewal), google-tasks, asana, apple-calendar
Daily digestslack (members + custom-emoji), ms-teams, google-chat

Each chain is keyed per resource (watch-renewal:<id>, poll:<id>, members-sync:<channelId>, …) so re-scheduling replaces rather than forks, and disable paths cancel by key. Pollers that previously had no teardown now cancel on disable. google-drive/google-calendar additionally stop the prior provider watch before re-creating it (so a redundant setup can't orphan a still-firing watch). Added a google-drive regression test.

Left as-is (not affected): gmail and outlook-mail already cancel-before-reschedule (no leak) and keep their self-heal logic; github's runTask override is a pass-through; linkedin's retry terminates.

Backwards compatibility

scheduleTask/taskKey are purely additive. Any pre-deploy in-flight (un-keyed) renewal task fires once more after deploy, then the chain becomes keyed — no lingering leak. Suspended connectors un-suspend on their next deploy.

Verification

  • All 13 touched connectors build + lint clean; no leftover *_renewal_task/poll_task/cancelTask references.
  • twister builds; changeset validates.
  • Runtime behavior (atomic replace / independent keys / cancel) is covered by an integration test in the core PR (5 tests), plus a google-drive connector regression test here.

🤖 Generated with Claude Code

KrisBraunand others added 2 commits June 17, 2026 00:44
…asks
Recurring/self-renewing jobs (watch & webhook renewals, polling, deferred
cleanup) were hand-managed with runTask({ runAt }) + a stored token +
cancelTask() before re-scheduling. That pattern is easy to get wrong: a
renewal callback that re-schedules itself, plus any extra entry into the
setup path (an onChannelEnabled re-dispatch, a re-init, a race), leaks
parallel self-perpetuating task chains that accumulate without bound and
eventually trip the runtime execution quota.
Add a keyed primitive that makes the leak impossible by construction:
- this.scheduleTask(key, callback, { runAt }) — schedules a singleton task
per key; re-scheduling under the same key atomically cancels-and-replaces
any pending task. At most one live scheduled task per key.
- this.cancelScheduledTask(key) — tears it down (no-op if none/already ran).
Exposed on the Tasks tool interface and as helpers on Tool/Twist. Docs
(RUNTIME.md periodic-work guidance, TOOLS_GUIDE.md scheduling section)
updated to steer recurring tasks to scheduleTask.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed chains)
Replace the leak-prone runTask({ runAt }) + stored-token renewal/poll
pattern with the keyed singleton scheduleTask/cancelScheduledTask primitive
across every connector that runs a self-renewing or recurring scheduled task:
- Watch/subscription renewals: google-drive, google-calendar, google-chat,
ms-teams, jira, outlook-calendar. (google-drive & google-calendar also now
stop the prior provider watch before re-creating it, so a redundant setup
can't orphan a still-firing watch.)
- Periodic polls: airtable (poll + webhook renewal), google-tasks, asana,
apple-calendar — several of which previously had no token/cancel at all and
could fork unboundedly.
- Daily digests: slack (members + custom-emoji), and the per-channel daily
passes in ms-teams/google-chat.
Each chain is keyed per resource (e.g. watch-renewal:<id>, poll:<id>) so
re-scheduling replaces rather than forks, and disable paths cancel by key.
gmail and outlook-mail already cancel-before-reschedule (no leak) and keep
their existing self-heal logic. Adds a google-drive regression test.
Fixes the Google Drive connector auto-suspension (500+ worker invocations/24h
from accumulated renewal chains).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit 59cb509 into mainJun 17, 2026
1 check passed
@KrisBraun
KrisBraun deleted the keyed-scheduled-tasks branch June 17, 2026 13:05
@plot-app-builderplot-app-builderBot mentioned this pull request Jun 17, 2026
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)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Keyed singleton scheduled tasks (scheduleTask) + migrate connectors off leak-prone renewal pattern - #205

Merged
KrisBraun merged 2 commits into
mainfrom
keyed-scheduled-tasks
Jun 17, 2026
Merged

Keyed singleton scheduled tasks (scheduleTask) + migrate connectors off leak-prone renewal pattern#205
KrisBraun merged 2 commits into
mainfrom
keyed-scheduled-tasks

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Why

A connector's self-renewing scheduled task (watch/webhook renewal, polling, daily digest) was hand-managed with runTask({ runAt }) + a stored token + cancelTask() before re-scheduling. That pattern is easy to get wrong: a renewal callback that re-schedules itself, plus any extra entry into the setup path (an onChannelEnabled re-dispatch, a re-init, a race), leaks parallel self-perpetuating task chains that accumulate without bound and eventually trip the runtime execution quota.

This is exactly what auto-suspended the Google Drive connector (≈350 → 3351 worker invocations/day, near-zero DB writes — a steady 24/7 background climb from accumulated renewal chains). google-calendar had the identical latent bug, and several pollers (airtable, google-tasks) had no token/cancel at all, so they could fork unboundedly with no way to stop them.

The leaky pattern was even documented as the recommended approach in connectors/AGENTS.md, so connectors copied it faithfully.

What

New SDK primitive (@plotday/twister) — makes the leak impossible by construction:

  • this.scheduleTask(key, callback, { runAt }) — schedules a singleton task per key; re-scheduling under the same key atomically cancels-and-replaces any pending task. At most one live scheduled task per key.
  • this.cancelScheduledTask(key) — tears it down (no-op if none / already ran).

Exposed on the Tasks tool interface and as Tool/Twist helpers. Changeset included (minor). The runtime side (the task_key column + atomic replace-on-create in CallbacksState, and the Tasks-tool wiring) ships in the matching core PR, which also bumps the submodule pointer to this branch.

Docs — fixed the connectors/AGENTS.md watch-renewal snippet (it was teaching the leaky pattern), added a checklist item + pitfall, and updated twister/docs/RUNTIME.md (periodic-work example) and TOOLS_GUIDE.md (scheduling section) to steer recurring tasks to scheduleTask.

Connector migration — moved every connector with a self-renewing/recurring scheduled task onto the keyed primitive:

ClassConnectors
Watch/subscription renewalgoogle-drive, google-calendar, google-chat, ms-teams, jira, outlook-calendar
Periodic pollairtable (+ webhook renewal), google-tasks, asana, apple-calendar
Daily digestslack (members + custom-emoji), ms-teams, google-chat

Each chain is keyed per resource (watch-renewal:<id>, poll:<id>, members-sync:<channelId>, …) so re-scheduling replaces rather than forks, and disable paths cancel by key. Pollers that previously had no teardown now cancel on disable. google-drive/google-calendar additionally stop the prior provider watch before re-creating it (so a redundant setup can't orphan a still-firing watch). Added a google-drive regression test.

Left as-is (not affected): gmail and outlook-mail already cancel-before-reschedule (no leak) and keep their self-heal logic; github's runTask override is a pass-through; linkedin's retry terminates.

Backwards compatibility

scheduleTask/taskKey are purely additive. Any pre-deploy in-flight (un-keyed) renewal task fires once more after deploy, then the chain becomes keyed — no lingering leak. Suspended connectors un-suspend on their next deploy.

Verification

  • All 13 touched connectors build + lint clean; no leftover *_renewal_task/poll_task/cancelTask references.
  • twister builds; changeset validates.
  • Runtime behavior (atomic replace / independent keys / cancel) is covered by an integration test in the core PR (5 tests), plus a google-drive connector regression test here.

🤖 Generated with Claude Code

KrisBraunand others added 2 commits June 17, 2026 00:44
…asks
Recurring/self-renewing jobs (watch & webhook renewals, polling, deferred
cleanup) were hand-managed with runTask({ runAt }) + a stored token +
cancelTask() before re-scheduling. That pattern is easy to get wrong: a
renewal callback that re-schedules itself, plus any extra entry into the
setup path (an onChannelEnabled re-dispatch, a re-init, a race), leaks
parallel self-perpetuating task chains that accumulate without bound and
eventually trip the runtime execution quota.
Add a keyed primitive that makes the leak impossible by construction:
- this.scheduleTask(key, callback, { runAt }) — schedules a singleton task
per key; re-scheduling under the same key atomically cancels-and-replaces
any pending task. At most one live scheduled task per key.
- this.cancelScheduledTask(key) — tears it down (no-op if none/already ran).
Exposed on the Tasks tool interface and as helpers on Tool/Twist. Docs
(RUNTIME.md periodic-work guidance, TOOLS_GUIDE.md scheduling section)
updated to steer recurring tasks to scheduleTask.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed chains)
Replace the leak-prone runTask({ runAt }) + stored-token renewal/poll
pattern with the keyed singleton scheduleTask/cancelScheduledTask primitive
across every connector that runs a self-renewing or recurring scheduled task:
- Watch/subscription renewals: google-drive, google-calendar, google-chat,
ms-teams, jira, outlook-calendar. (google-drive & google-calendar also now
stop the prior provider watch before re-creating it, so a redundant setup
can't orphan a still-firing watch.)
- Periodic polls: airtable (poll + webhook renewal), google-tasks, asana,
apple-calendar — several of which previously had no token/cancel at all and
could fork unboundedly.
- Daily digests: slack (members + custom-emoji), and the per-channel daily
passes in ms-teams/google-chat.
Each chain is keyed per resource (e.g. watch-renewal:<id>, poll:<id>) so
re-scheduling replaces rather than forks, and disable paths cancel by key.
gmail and outlook-mail already cancel-before-reschedule (no leak) and keep
their existing self-heal logic. Adds a google-drive regression test.
Fixes the Google Drive connector auto-suspension (500+ worker invocations/24h
from accumulated renewal chains).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit 59cb509 into mainJun 17, 2026
1 check passed
@KrisBraun
KrisBraun deleted the keyed-scheduled-tasks branch June 17, 2026 13:05
@plot-app-builderplot-app-builderBot mentioned this pull request Jun 17, 2026
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)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Keyed singleton scheduled tasks (scheduleTask) + migrate connectors off leak-prone renewal pattern - #205

Merged
KrisBraun merged 2 commits into
mainfrom
keyed-scheduled-tasks
Jun 17, 2026
Merged

Keyed singleton scheduled tasks (scheduleTask) + migrate connectors off leak-prone renewal pattern#205
KrisBraun merged 2 commits into
mainfrom
keyed-scheduled-tasks

Conversation

@KrisBraun

Copy link
Copy Markdown
Contributor

Why

A connector's self-renewing scheduled task (watch/webhook renewal, polling, daily digest) was hand-managed with runTask({ runAt }) + a stored token + cancelTask() before re-scheduling. That pattern is easy to get wrong: a renewal callback that re-schedules itself, plus any extra entry into the setup path (an onChannelEnabled re-dispatch, a re-init, a race), leaks parallel self-perpetuating task chains that accumulate without bound and eventually trip the runtime execution quota.

This is exactly what auto-suspended the Google Drive connector (≈350 → 3351 worker invocations/day, near-zero DB writes — a steady 24/7 background climb from accumulated renewal chains). google-calendar had the identical latent bug, and several pollers (airtable, google-tasks) had no token/cancel at all, so they could fork unboundedly with no way to stop them.

The leaky pattern was even documented as the recommended approach in connectors/AGENTS.md, so connectors copied it faithfully.

What

New SDK primitive (@plotday/twister) — makes the leak impossible by construction:

  • this.scheduleTask(key, callback, { runAt }) — schedules a singleton task per key; re-scheduling under the same key atomically cancels-and-replaces any pending task. At most one live scheduled task per key.
  • this.cancelScheduledTask(key) — tears it down (no-op if none / already ran).

Exposed on the Tasks tool interface and as Tool/Twist helpers. Changeset included (minor). The runtime side (the task_key column + atomic replace-on-create in CallbacksState, and the Tasks-tool wiring) ships in the matching core PR, which also bumps the submodule pointer to this branch.

Docs — fixed the connectors/AGENTS.md watch-renewal snippet (it was teaching the leaky pattern), added a checklist item + pitfall, and updated twister/docs/RUNTIME.md (periodic-work example) and TOOLS_GUIDE.md (scheduling section) to steer recurring tasks to scheduleTask.

Connector migration — moved every connector with a self-renewing/recurring scheduled task onto the keyed primitive:

ClassConnectors
Watch/subscription renewalgoogle-drive, google-calendar, google-chat, ms-teams, jira, outlook-calendar
Periodic pollairtable (+ webhook renewal), google-tasks, asana, apple-calendar
Daily digestslack (members + custom-emoji), ms-teams, google-chat

Each chain is keyed per resource (watch-renewal:<id>, poll:<id>, members-sync:<channelId>, …) so re-scheduling replaces rather than forks, and disable paths cancel by key. Pollers that previously had no teardown now cancel on disable. google-drive/google-calendar additionally stop the prior provider watch before re-creating it (so a redundant setup can't orphan a still-firing watch). Added a google-drive regression test.

Left as-is (not affected): gmail and outlook-mail already cancel-before-reschedule (no leak) and keep their self-heal logic; github's runTask override is a pass-through; linkedin's retry terminates.

Backwards compatibility

scheduleTask/taskKey are purely additive. Any pre-deploy in-flight (un-keyed) renewal task fires once more after deploy, then the chain becomes keyed — no lingering leak. Suspended connectors un-suspend on their next deploy.

Verification

  • All 13 touched connectors build + lint clean; no leftover *_renewal_task/poll_task/cancelTask references.
  • twister builds; changeset validates.
  • Runtime behavior (atomic replace / independent keys / cancel) is covered by an integration test in the core PR (5 tests), plus a google-drive connector regression test here.

🤖 Generated with Claude Code

KrisBraunand others added 2 commits June 17, 2026 00:44
…asks
Recurring/self-renewing jobs (watch & webhook renewals, polling, deferred
cleanup) were hand-managed with runTask({ runAt }) + a stored token +
cancelTask() before re-scheduling. That pattern is easy to get wrong: a
renewal callback that re-schedules itself, plus any extra entry into the
setup path (an onChannelEnabled re-dispatch, a re-init, a race), leaks
parallel self-perpetuating task chains that accumulate without bound and
eventually trip the runtime execution quota.
Add a keyed primitive that makes the leak impossible by construction:
- this.scheduleTask(key, callback, { runAt }) — schedules a singleton task
per key; re-scheduling under the same key atomically cancels-and-replaces
any pending task. At most one live scheduled task per key.
- this.cancelScheduledTask(key) — tears it down (no-op if none/already ran).
Exposed on the Tasks tool interface and as helpers on Tool/Twist. Docs
(RUNTIME.md periodic-work guidance, TOOLS_GUIDE.md scheduling section)
updated to steer recurring tasks to scheduleTask.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed chains)
Replace the leak-prone runTask({ runAt }) + stored-token renewal/poll
pattern with the keyed singleton scheduleTask/cancelScheduledTask primitive
across every connector that runs a self-renewing or recurring scheduled task:
- Watch/subscription renewals: google-drive, google-calendar, google-chat,
ms-teams, jira, outlook-calendar. (google-drive & google-calendar also now
stop the prior provider watch before re-creating it, so a redundant setup
can't orphan a still-firing watch.)
- Periodic polls: airtable (poll + webhook renewal), google-tasks, asana,
apple-calendar — several of which previously had no token/cancel at all and
could fork unboundedly.
- Daily digests: slack (members + custom-emoji), and the per-channel daily
passes in ms-teams/google-chat.
Each chain is keyed per resource (e.g. watch-renewal:<id>, poll:<id>) so
re-scheduling replaces rather than forks, and disable paths cancel by key.
gmail and outlook-mail already cancel-before-reschedule (no leak) and keep
their existing self-heal logic. Adds a google-drive regression test.
Fixes the Google Drive connector auto-suspension (500+ worker invocations/24h
from accumulated renewal chains).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@KrisBraun
KrisBraun merged commit 59cb509 into mainJun 17, 2026
1 check passed
@KrisBraun
KrisBraun deleted the keyed-scheduled-tasks branch June 17, 2026 13:05
@plot-app-builderplot-app-builderBot mentioned this pull request Jun 17, 2026
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